Merge pull request #5918 from betagouv/main

2021-02-17-01
This commit is contained in:
krichtof 2021-02-17 17:08:06 +01:00 committed by GitHub
commit 7cdf71f1c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 1855 additions and 1000 deletions

View file

@ -9,7 +9,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Sentry Release
uses: getsentry/action-release@v1.0.0
uses: getsentry/action-release@v1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}

View file

@ -1320,6 +1320,8 @@ Rails/ActiveRecordCallbacksOrder:
Enabled: false
Rails/AfterCommitOverride:
Enabled: true
Rails/AttributeDefaultBlockValue:
Enabled: true
Rails/FindById:
Enabled: true
Rails/Inquiry:
@ -1342,6 +1344,8 @@ Rails/ShortI18n:
Enabled: true
Rails/SquishedSQLHeredocs:
Enabled: true
Rails/WhereEquals:
Enabled: true
Rails/WhereExists:
Enabled: true
Rails/WhereNot:

View file

@ -22,38 +22,38 @@ GEM
specs:
aasm (5.1.1)
concurrent-ruby (~> 1.0)
actioncable (6.0.3.4)
actionpack (= 6.0.3.4)
actioncable (6.0.3.5)
actionpack (= 6.0.3.5)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.3.4)
actionpack (= 6.0.3.4)
activejob (= 6.0.3.4)
activerecord (= 6.0.3.4)
activestorage (= 6.0.3.4)
activesupport (= 6.0.3.4)
actionmailbox (6.0.3.5)
actionpack (= 6.0.3.5)
activejob (= 6.0.3.5)
activerecord (= 6.0.3.5)
activestorage (= 6.0.3.5)
activesupport (= 6.0.3.5)
mail (>= 2.7.1)
actionmailer (6.0.3.4)
actionpack (= 6.0.3.4)
actionview (= 6.0.3.4)
activejob (= 6.0.3.4)
actionmailer (6.0.3.5)
actionpack (= 6.0.3.5)
actionview (= 6.0.3.5)
activejob (= 6.0.3.5)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.3.4)
actionview (= 6.0.3.4)
activesupport (= 6.0.3.4)
actionpack (6.0.3.5)
actionview (= 6.0.3.5)
activesupport (= 6.0.3.5)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.3.4)
actionpack (= 6.0.3.4)
activerecord (= 6.0.3.4)
activestorage (= 6.0.3.4)
activesupport (= 6.0.3.4)
actiontext (6.0.3.5)
actionpack (= 6.0.3.5)
activerecord (= 6.0.3.5)
activestorage (= 6.0.3.5)
activesupport (= 6.0.3.5)
nokogiri (>= 1.8.5)
actionview (6.0.3.4)
activesupport (= 6.0.3.4)
actionview (6.0.3.5)
activesupport (= 6.0.3.5)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -61,32 +61,31 @@ GEM
active_link_to (1.0.5)
actionpack
addressable
active_model_serializers (0.10.10)
actionpack (>= 4.1, < 6.1)
activemodel (>= 4.1, < 6.1)
active_model_serializers (0.10.12)
actionpack (>= 4.1, < 6.2)
activemodel (>= 4.1, < 6.2)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_storage_validations (0.9.0)
active_storage_validations (0.9.2)
rails (>= 5.2.0)
activejob (6.0.3.4)
activesupport (= 6.0.3.4)
activejob (6.0.3.5)
activesupport (= 6.0.3.5)
globalid (>= 0.3.6)
activemodel (6.0.3.4)
activesupport (= 6.0.3.4)
activerecord (6.0.3.4)
activemodel (= 6.0.3.4)
activesupport (= 6.0.3.4)
activestorage (6.0.3.4)
actionpack (= 6.0.3.4)
activejob (= 6.0.3.4)
activerecord (= 6.0.3.4)
activemodel (6.0.3.5)
activesupport (= 6.0.3.5)
activerecord (6.0.3.5)
activemodel (= 6.0.3.5)
activesupport (= 6.0.3.5)
activestorage (6.0.3.5)
actionpack (= 6.0.3.5)
activejob (= 6.0.3.5)
activerecord (= 6.0.3.5)
marcel (~> 0.3.1)
activestorage-openstack (1.4.1)
activestorage-openstack (1.5.1)
fog-openstack (~> 1.0)
marcel
mime-types
rails (>= 5.2.2)
activesupport (6.0.3.4)
activesupport (6.0.3.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -111,11 +110,11 @@ GEM
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
ast (2.4.1)
ast (2.4.2)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
autoprefixer-rails (10.0.1.0)
autoprefixer-rails (10.2.4.0)
execjs
axe-matchers (2.6.1)
dumb_delegator (~> 0.8)
@ -124,7 +123,7 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
axlsx_styler (1.0.0)
axlsx_styler (1.1.0)
activesupport (>= 3.1)
caxlsx (>= 2.0.2)
babel-source (5.8.35)
@ -134,22 +133,22 @@ GEM
bcrypt (3.1.16)
bindata (2.4.8)
bindex (0.8.1)
brakeman (4.9.1)
browser (5.0.0)
brakeman (5.0.0)
browser (5.3.0)
builder (3.2.4)
byebug (11.1.3)
capybara (3.33.0)
capybara (3.35.3)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (~> 1.5)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
capybara-email (3.0.2)
capybara (>= 2.4, < 4.0)
mail
capybara-screenshot (1.0.24)
capybara-screenshot (1.0.25)
capybara (>= 1.0, < 4)
launchy
capybara-selenium (0.0.6)
@ -157,15 +156,15 @@ GEM
selenium-webdriver
case_transform (0.2)
activesupport
caxlsx (3.0.2)
caxlsx (3.0.4)
htmlentities (~> 4.3, >= 4.3.4)
mimemagic (~> 0.3)
nokogiri (~> 1.10, >= 1.10.4)
rubyzip (>= 1.3.0, < 3)
chartkick (3.4.0)
chartkick (3.4.2)
childprocess (3.0.0)
choice (0.2.0)
chunky_png (1.3.11)
chunky_png (1.4.0)
clamav-client (3.2.0)
coderay (1.1.3)
coercible (1.0.0)
@ -179,22 +178,28 @@ GEM
coffee-script-source (1.12.2)
concurrent-ruby (1.1.8)
connection_pool (2.2.3)
crack (0.4.4)
crack (0.4.5)
rexml
crass (1.0.6)
css_parser (1.7.1)
css_parser (1.9.0)
addressable
daemons (1.3.1)
database_cleaner (1.8.5)
database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0)
database_cleaner-active_record (2.0.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1)
deep_cloneable (3.0.0)
activerecord (>= 3.1.0, < 7)
delayed_cron_job (0.7.3)
delayed_cron_job (0.7.4)
delayed_job (>= 4.1)
delayed_job (4.1.8)
activesupport (>= 3.0, < 6.1)
delayed_job_active_record (4.1.4)
activerecord (>= 3.0, < 6.1)
delayed_job (4.1.9)
activesupport (>= 3.0, < 6.2)
delayed_job_active_record (4.1.5)
activerecord (>= 3.0, < 6.2)
delayed_job (>= 3.0, < 5)
delayed_job_web (1.4.3)
activerecord (> 3.0.0)
@ -225,9 +230,9 @@ GEM
railties (>= 3.2)
dry-inflector (0.2.0)
dumb_delegator (0.8.1)
ecma-re-validator (0.2.1)
regexp_parser (~> 1.2)
em-websocket (0.5.1)
ecma-re-validator (0.3.0)
regexp_parser (~> 2.0)
em-websocket (0.5.2)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
encryptor (3.0.0)
@ -239,28 +244,28 @@ GEM
ethon (0.12.0)
ffi (>= 1.3.0)
eventmachine (1.2.7)
excon (0.76.0)
excon (0.79.0)
execjs (2.7.0)
factory_bot (5.1.2)
activesupport (>= 4.2.0)
factory_bot (6.1.0)
activesupport (>= 5.0.0)
faraday (1.3.0)
faraday-net_http (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords
faraday-net_http (1.0.1)
ffi (1.13.1)
ffi (1.14.2)
ffi-geos (2.1.0)
ffi (>= 1.0.0)
flipper (0.19.0)
flipper-active_record (0.19.0)
flipper (0.20.3)
flipper-active_record (0.20.3)
activerecord (>= 5.0, < 7)
flipper (~> 0.19.0)
flipper-ui (0.19.0)
flipper (~> 0.20.3)
flipper-ui (0.20.3)
erubi (>= 1.0.0, < 2.0.0)
flipper (~> 0.19.0)
flipper (~> 0.20.3)
rack (>= 1.4, < 3)
rack-protection (>= 1.5.3, < 2.1.0)
fog-core (2.2.0)
rack-protection (>= 1.5.3, < 2.2.0)
fog-core (2.2.3)
builder
excon (~> 0.71)
formatador (~> 0.2)
@ -268,16 +273,16 @@ GEM
fog-json (1.2.0)
fog-core
multi_json (~> 1.10)
fog-openstack (1.0.10)
fog-openstack (1.0.11)
fog-core (~> 2.1)
fog-json (>= 1.0)
ipaddress (>= 0.8)
formatador (0.2.5)
fugit (1.3.4)
fugit (1.4.2)
et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.1)
geo_coord (0.1.0)
geocoder (1.6.3)
raabro (~> 1.4)
geo_coord (0.2.0)
geocoder (1.6.5)
globalid (0.4.2)
activesupport (>= 4.2.0)
gon (6.4.0)
@ -285,11 +290,11 @@ GEM
i18n (>= 0.7)
multi_json
request_store (>= 1.0)
graphql (1.10.9)
graphql (1.12.4)
graphql-batch (0.4.3)
graphql (>= 1.3, < 2)
promise.rb (~> 0.7.2)
graphql-rails_logger (1.2.2)
graphql-rails_logger (1.2.3)
actionpack (> 5.0)
activesupport (> 5.0)
railties (> 5.0)
@ -300,7 +305,7 @@ GEM
thor (>= 0.19, < 2.0)
graphql_playground-rails (2.1.0)
rails (>= 5.1.0)
groupdate (5.2.1)
groupdate (5.2.2)
activesupport (>= 5)
guard (2.16.2)
formatador (>= 0.2.4)
@ -321,7 +326,7 @@ GEM
guard (~> 2.1)
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
haml (5.1.2)
haml (5.2.1)
temple (>= 0.8.0)
tilt
haml-lint (0.999.999)
@ -332,8 +337,9 @@ GEM
haml (>= 4.0.6, < 6.0)
html2haml (>= 1.0.1)
railties (>= 5.1)
haml_lint (0.35.0)
haml (>= 4.0, < 5.2)
haml_lint (0.37.0)
haml (>= 4.0, < 5.3)
parallel (~> 1.10)
rainbow
rubocop (>= 0.50.0)
sysexits (~> 1.1)
@ -353,7 +359,7 @@ GEM
http_accept_language (2.1.1)
http_parser.rb (0.6.0)
httpclient (2.8.3)
i18n (1.8.7)
i18n (1.8.9)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.33)
activesupport (>= 4.0.2)
@ -375,15 +381,15 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.3.1)
json (2.5.1)
json-jwt (1.13.0)
activesupport (>= 4.2)
aes_key_wrap
bindata
json_schemer (0.2.16)
ecma-re-validator (~> 0.2)
json_schemer (0.2.17)
ecma-re-validator (~> 0.3)
hana (~> 1.3)
regexp_parser (~> 1.5)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jsonapi-renderer (0.2.2)
jwt (2.2.2)
@ -407,7 +413,7 @@ GEM
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
listen (3.2.1)
listen (3.4.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
lograge (0.11.2)
@ -419,19 +425,19 @@ GEM
loofah (2.9.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lumberjack (1.2.4)
lumberjack (1.2.8)
mail (2.7.1)
mini_mime (>= 0.1.1)
mailjet (1.5.4)
mailjet (1.6.0)
activesupport (>= 3.1.0)
rack (>= 1.4.0)
rest-client
rest-client (>= 2.0.0)
marcel (0.3.3)
mimemagic (~> 0.3.2)
method_source (1.0.0)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2020.0512)
mime-types-data (3.2021.0212)
mimemagic (0.3.5)
mini_magick (4.11.0)
mini_mime (1.0.2)
@ -445,7 +451,7 @@ GEM
ruby2_keywords (~> 0.0.1)
nenv (0.3.0)
netrc (0.11.0)
nio4r (2.5.4)
nio4r (2.5.5)
nokogiri (1.11.1)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
@ -453,7 +459,7 @@ GEM
nenv (~> 0.1)
shellany (~> 0.0)
open4 (1.3.4)
openid_connect (1.1.8)
openid_connect (1.2.0)
activemodel
attr_required (>= 1.0.0)
json-jwt (>= 1.5.0)
@ -464,25 +470,25 @@ GEM
validate_url
webfinger (>= 1.0.1)
orm_adapter (0.5.0)
parallel (1.19.2)
parser (2.7.1.5)
parallel (1.20.1)
parser (3.0.0.0)
ast (~> 2.4.1)
pdf-core (0.7.0)
pdf-core (0.9.0)
pg (1.2.3)
phonelib (0.6.45)
prawn (2.2.2)
pdf-core (~> 0.7.0)
ttfunk (~> 1.5)
phonelib (0.6.48)
prawn (2.4.0)
pdf-core (~> 0.9.0)
ttfunk (~> 1.7)
prawn-rails (1.3.0)
prawn
prawn-table
rails (>= 3.1.0)
prawn-svg (0.30.0)
prawn-svg (0.31.0)
css_parser (~> 1.6)
prawn (>= 0.11.1, < 3)
prawn-table (0.2.2)
prawn (>= 1.3.0, < 3.0.0)
premailer (1.14.1)
premailer (1.14.2)
addressable
css_parser (>= 1.6.0)
htmlentities (>= 4.0.0)
@ -497,43 +503,43 @@ GEM
byebug (~> 11.0)
pry (~> 0.13.0)
public_suffix (4.0.6)
puma (5.0.2)
puma (5.2.1)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
raabro (1.1.6)
raabro (1.4.0)
racc (1.5.2)
rack (2.2.3)
rack-attack (6.3.1)
rack-attack (6.5.0)
rack (>= 1.0, < 3)
rack-mini-profiler (2.1.0)
rack-mini-profiler (2.3.1)
rack (>= 1.2.0)
rack-oauth2 (1.15.0)
rack-oauth2 (1.16.0)
activesupport
attr_required
httpclient
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (2.0.8.1)
rack-protection (2.1.0)
rack
rack-proxy (0.6.5)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.3.4)
actioncable (= 6.0.3.4)
actionmailbox (= 6.0.3.4)
actionmailer (= 6.0.3.4)
actionpack (= 6.0.3.4)
actiontext (= 6.0.3.4)
actionview (= 6.0.3.4)
activejob (= 6.0.3.4)
activemodel (= 6.0.3.4)
activerecord (= 6.0.3.4)
activestorage (= 6.0.3.4)
activesupport (= 6.0.3.4)
rails (6.0.3.5)
actioncable (= 6.0.3.5)
actionmailbox (= 6.0.3.5)
actionmailer (= 6.0.3.5)
actionpack (= 6.0.3.5)
actiontext (= 6.0.3.5)
actionview (= 6.0.3.5)
activejob (= 6.0.3.5)
activemodel (= 6.0.3.5)
activerecord (= 6.0.3.5)
activestorage (= 6.0.3.5)
activesupport (= 6.0.3.5)
bundler (>= 1.3.0)
railties (= 6.0.3.4)
railties (= 6.0.3.5)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -552,9 +558,9 @@ GEM
rails-i18n (6.0.0)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
railties (6.0.3.4)
actionpack (= 6.0.3.4)
activesupport (= 6.0.3.4)
railties (6.0.3.5)
actionpack (= 6.0.3.5)
activesupport (= 6.0.3.5)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
@ -570,7 +576,7 @@ GEM
execjs
railties (>= 3.2)
tilt
regexp_parser (1.8.0)
regexp_parser (2.0.3)
request_store (1.5.0)
rack (>= 1.4)
responders (3.0.1)
@ -582,7 +588,7 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rexml (3.2.4)
rgeo (2.1.1)
rgeo (2.2.0)
rgeo-geojson (2.1.1)
rgeo (>= 1.0.0)
rodf (1.1.1)
@ -591,56 +597,59 @@ GEM
rubyzip (>= 1.0)
rotp (4.1.0)
addressable (~> 2.5)
rouge (3.17.0)
rqrcode (1.1.2)
rouge (3.26.0)
rqrcode (1.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 0.1)
rqrcode_core (0.1.2)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-core (3.9.2)
rspec-support (~> 3.9.3)
rspec-expectations (3.9.2)
rqrcode_core (~> 0.2)
rqrcode_core (0.2.0)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
rspec-mocks (~> 3.10.0)
rspec-core (3.10.1)
rspec-support (~> 3.10.0)
rspec-expectations (3.10.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
rspec-support (~> 3.10.0)
rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.1)
rspec-support (~> 3.10.0)
rspec-rails (4.0.2)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.9)
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (3.9.3)
rspec-core (~> 3.10)
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.10.2)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (0.92.0)
rubocop (1.10.0)
parallel (~> 1.10)
parser (>= 2.7.1.5)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 0.5.0)
rubocop-ast (>= 1.2.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (0.7.0)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.4.1)
parser (>= 2.7.1.5)
strscan (>= 1.0.0)
rubocop-performance (1.8.1)
rubocop (>= 0.87.0)
rubocop-packaging (0.5.1)
rubocop (>= 0.89, < 2.0)
rubocop-performance (1.9.2)
rubocop (>= 0.90.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.8.1)
rubocop-rails (2.9.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 0.87.0)
rubocop-rails_config (0.12.6)
rubocop (>= 0.90.0, < 2.0)
rubocop-rails_config (1.3.0)
railties (>= 5.0)
rubocop (~> 0.82)
rubocop (>= 1.8)
rubocop-ast (>= 1.0.1)
rubocop-packaging (~> 0.5)
rubocop-performance (~> 1.3)
rubocop-rails (~> 2.0)
rubocop-rspec-focused (1.0.0)
@ -649,12 +658,12 @@ GEM
rake (>= 0.8.1)
ruby-graphviz (1.2.5)
rexml
ruby-progressbar (1.10.1)
ruby-progressbar (1.11.0)
ruby-saml-idp (0.3.5)
ruby-vips (2.0.17)
ffi (~> 1.9)
ruby2_keywords (0.0.4)
ruby_parser (3.15.0)
ruby_parser (3.15.1)
sexp_processor (~> 4.9)
rubyzip (2.3.0)
sanitize-url (0.1.4)
@ -677,7 +686,7 @@ GEM
selenium-webdriver (3.142.7)
childprocess (>= 0.5, < 4.0)
rubyzip (>= 1.2.2)
semantic_range (2.3.0)
semantic_range (2.3.1)
sentry-delayed_job (4.2.0)
sentry-ruby-core (~> 4.2.0)
sentry-rails (4.2.1)
@ -690,31 +699,31 @@ GEM
sentry-ruby-core (4.2.1)
concurrent-ruby
faraday
sexp_processor (4.15.1)
sexp_processor (4.15.2)
shellany (0.0.1)
shoulda-matchers (4.4.1)
shoulda-matchers (4.5.1)
activesupport (>= 4.2.0)
sib-api-v3-sdk (7.0.0)
sib-api-v3-sdk (7.4.0)
json (~> 2.1, >= 2.1.0)
typhoeus (~> 1.0, >= 1.0.1)
simple_xlsx_reader (1.0.4)
nokogiri
rubyzip
sinatra (2.0.8.1)
sinatra (2.1.0)
mustermann (~> 1.0)
rack (~> 2.0)
rack-protection (= 2.0.8.1)
rack (~> 2.2)
rack-protection (= 2.1.0)
tilt (~> 2.0)
skylight (4.3.1)
skylight-core (= 4.3.1)
skylight-core (4.3.1)
skylight (4.3.2)
skylight-core (= 4.3.2)
skylight-core (4.3.2)
activesupport (>= 4.2.0)
smart_listing (1.2.3)
coffee-rails
jquery-rails
kaminari (>= 0.17)
rails (>= 3.2)
spreadsheet_architect (4.0.0)
spreadsheet_architect (4.1.0)
axlsx_styler (>= 1.0.0, < 2)
caxlsx (>= 2.0.2, < 4)
rodf (>= 1.0.0, < 2)
@ -728,8 +737,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
strscan (1.0.3)
swd (1.1.2)
swd (1.2.0)
activesupport (>= 3)
attr_required (>= 0.0.5)
httpclient (>= 2.4)
@ -740,8 +748,8 @@ GEM
thor (1.1.0)
thread_safe (0.3.6)
tilt (2.0.10)
timecop (0.9.1)
ttfunk (1.6.2.1)
timecop (0.9.4)
ttfunk (1.7.0)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (1.2.9)
@ -754,7 +762,7 @@ GEM
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
validate_url (1.0.11)
validate_url (1.0.13)
activemodel (>= 3.0.0)
public_suffix
vcr (6.0.0)
@ -765,19 +773,19 @@ GEM
equalizer (~> 0.0, >= 0.0.9)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.0.4)
web-console (4.1.0)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webdrivers (4.4.1)
webdrivers (4.5.0)
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (>= 3.0, < 4.0)
webfinger (1.1.0)
activesupport
httpclient (>= 2.4)
webmock (3.9.1)
webmock (3.11.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -798,7 +806,7 @@ GEM
zipline (1.3.0)
actionpack (>= 3.2.1, < 7.0)
zip_tricks (>= 4.2.1, < 6.0)
zxcvbn-ruby (1.1.0)
zxcvbn-ruby (1.2.0)
PLATFORMS
ruby

View file

@ -128,8 +128,14 @@ Le projet utilise plusieurs linters pour vérifier la lisibilité et la qualité
## Déploiement
- Tout nouveau commit ajouté à la branche `dev` est automatiquement déployé [en intégration](https://dev.demarches-simplifiees.fr/)
- Tout nouveau commit ajouté à la branche `master` est automatiquement déployé [en production](https://www.demarches-simplifiees.fr/)
Dans le cas dun déploiement sur plusieurs serveurs, lapplication peut être déployée avec la tâche :
```
DOMAINS="web1 web2" BRANCH="main" bin/rake deploy
```
En interne, cette tâche utilise [mina](https://github.com/mina-deploy/mina) pour lancer les commandes
de déploiement sur tous les serveurs spécifiés.
## Tâches courantes

View file

@ -5,34 +5,4 @@
.select-instructeurs {
width: 100%;
}
.select2-container--default {
.select2-selection--multiple {
border: solid 1px $border-grey;
.select2-selection__choice, // scss-lint:disable SelectorFormat
.select2-search--inline {
padding: $default-spacer;
}
}
&.select2-container--focus {
.select2-selection--multiple {
border: 1px solid $blue;
box-shadow: 0px 0px 2px 1px $blue;
}
}
.select2-results__option { // scss-lint:disable SelectorFormat
padding: $default-spacer;
}
.custom-select2-option {
.icon {
margin-right: $default-spacer;
}
}
}
}

View file

@ -4,6 +4,5 @@
// = require ./utils
// = require ./fonts
// = require leaflet
// = require select2
// = require_tree .
// = stub ./print.scss

View file

@ -281,18 +281,6 @@
.dropdown-form {
padding: 2 * $default-spacer;
.select2-container {
margin-bottom: 2 * $default-spacer;
}
.select2-selection {
border: 1px solid $border-grey;
&.select2-selection--multiple {
border: 1px solid $border-grey;
}
}
&.large {
width: 340px;
}
@ -310,10 +298,6 @@
}
}
.select2-dropdown {
border: 1px solid $border-grey;
}
.link {
color: $blue;
}

View file

@ -260,8 +260,7 @@
max-width: 180px;
}
select,
.select2-selection {
select {
// hack found here: https://stackoverflow.com/questions/1895476/how-to-style-a-select-dropdown-with-css-only-without-javascript
-webkit-appearance: none;
-moz-appearance: none;
@ -305,39 +304,26 @@
border-color: $blue;
}
.select2 {
min-width: 50%;
[data-reach-combobox-token-list] {
padding: $default-padding;
display: flex;
}
.select2-container {
display: block;
margin-bottom: $default-fields-spacer;
[data-reach-combobox-token] {
border: solid 1px $border-grey;
color: $black;
margin-top: $default-padding;
margin-bottom: $default-padding;
margin-right: 0.5 * $default-padding;
border-radius: 4px;
padding: $default-padding;
cursor: pointer;
list-style: none;
}
&.select2-container--focus {
.select2-selection {
border-color: $border-grey;
}
}
.select2-selection--single {
min-height: 62px;
// scss-lint:disable SelectorFormat
.select2-selection__arrow {
display: none;
}
// scss-lint:enable
}
// scss-lint:disable SelectorFormat
.select2-selection__rendered {
padding: $default-padding;
}
.select2-selection__choice {
background-color: #FFFFFF;
}
// scss-lint:enable
[data-reach-combobox-token]:focus {
background-color: $black;
color: $white;
}
.editable-champ {
@ -481,11 +467,55 @@
}
}
[data-react-class="ComboMultipleDropdownList"] {
margin-bottom: $default-fields-spacer;
[data-reach-combobox-input] {
outline: none;
border: none;
flex-grow: 1;
margin: 0.25rem;
background-image: image-url("icons/chevron-down");
background-size: 14px;
background-repeat: no-repeat;
background-position: right 10px center;
}
[data-reach-combobox-input]:focus {
outline: solid;
outline-color: $light-blue;
}
}
[data-combobox-token-label] {
border: 1px solid #CCCCCC;
border-radius: 4px;
display: flex;
flex-wrap: wrap;
}
[data-reach-combobox-option] {
font-size: 16px;
list-style-type: none;
}
[data-reach-combobox-option][aria-selected="true"] {
background: $light-blue !important;
color: $white;
}
[data-combobox-separator] {
font-size: 16px;
color: $dark-grey;
margin-top: 6px;
}
[data-combobox-remove-token] {
color: $dark-grey;
padding-right: 4px;
}
[data-reach-combobox-input]:focus {
outline-color: $light-blue;
}

View file

@ -1,14 +1,50 @@
@import "constants";
@import "colors";
.personnes-impliquees {
padding-bottom: 50px;
ul {
ul.tab-list {
list-style-type: disc;
margin-left: 16px;
}
// scss-lint:disable SelectorFormat
.form .select2-container .select2-selection__rendered {
padding: 12px;
[data-react-class="ComboMultipleDropdownList"] {
margin-bottom: $default-fields-spacer;
[data-reach-combobox-token-list] {
padding: 0.5 * $default-padding;
display: flex;
}
[data-reach-combobox-token] {
border: solid 1px $border-grey;
color: $black;
margin-top: 0.5 * $default-padding;
margin-bottom: 0.5 * $default-padding;
margin-right: 0.5 * $default-padding;
border-radius: 4px;
padding: 0.5 * $default-padding;
cursor: pointer;
list-style: none;
}
[data-reach-combobox-token]:focus {
background-color: $black;
color: $white;
}
[data-reach-combobox-input] {
outline: none;
border: none;
flex-grow: 1;
margin: 0.25rem;
}
[data-reach-combobox-input]:focus {
outline: solid;
outline-color: $light-blue;
}
}
// scss-lint:enable
}

View file

@ -23,6 +23,10 @@
flex: 10;
padding: 0 $default-padding;
background-color: $light-grey;
&.no-background {
background-color: transparent;
}
}
.procedure-form__column--preview {

View file

@ -61,4 +61,42 @@
margin-bottom: 3 * $default-spacer;
text-align: center;
}
[data-react-class="ComboMultipleDropdownList"] {
margin-bottom: $default-fields-spacer;
[data-reach-combobox-token-list] {
padding: 0.25 * $default-padding;
display: inline-block;
width: 100%;
}
[data-reach-combobox-token] {
border: solid 1px $border-grey;
color: $black;
margin: 0.25 * $default-padding;
border-radius: 2px;
padding: 0.25 * $default-padding;
cursor: pointer;
list-style: none;
}
[data-reach-combobox-token]:focus {
background-color: $black;
color: $white;
}
[data-reach-combobox-input] {
outline: none;
border: none;
flex-grow: 1;
margin: 0.25rem;
}
[data-reach-combobox-input]:focus {
outline: solid;
outline-color: $light-blue;
}
}
}

View file

@ -9,10 +9,10 @@ class API::V2::GraphqlController < API::V2::BaseController
render json: result
rescue => exception
if Rails.env.development?
handle_error_in_development(exception)
else
if Rails.env.production?
handle_error_in_production(exception)
else
handle_error_in_development(exception)
end
end

View file

@ -72,7 +72,7 @@ module Instructeurs
end
def send_to_instructeurs
recipients = Instructeur.find(params[:recipients])
recipients = Instructeur.find(JSON.parse(params[:recipients]))
recipients.each do |recipient|
recipient.follow(dossier)

View file

@ -138,7 +138,7 @@ module Instructeurs
end
def update_displayed_fields
procedure_presentation.update_displayed_fields(params[:values])
procedure_presentation.update_displayed_fields(JSON.parse(params[:values]))
redirect_back(fallback_location: instructeur_procedure_url(procedure))
end

View file

@ -60,7 +60,7 @@ module NewAdministrateur
def uninterlaced_png(uploaded_file)
if uploaded_file&.content_type == 'image/png'
chunky_img = ChunkyPNG::Image.from_io(uploaded_file)
chunky_img = ChunkyPNG::Image.from_io(uploaded_file.to_io)
chunky_img.save(uploaded_file.tempfile.to_path, interlace: false)
uploaded_file.tempfile.reopen(uploaded_file.tempfile.to_path, 'rb')
end

View file

@ -80,8 +80,8 @@ module NewAdministrateur
end
def add_instructeur
emails = params['emails'].presence || []
emails = emails.map(&:strip).map(&:downcase)
emails = params['emails'].presence || [].to_json
emails = JSON.parse(emails).map(&:strip).map(&:downcase)
correct_emails, bad_emails = emails
.partition { |email| URI::MailTo::EMAIL_REGEXP.match?(email) }

View file

@ -1,6 +1,6 @@
module NewAdministrateur
class ProceduresController < AdministrateurController
before_action :retrieve_procedure, only: [:champs, :annotations, :edit, :monavis, :update_monavis, :jeton, :update_jeton, :publication, :publish, :transfert, :allow_expert_review, :invited_expert_list]
before_action :retrieve_procedure, only: [:champs, :annotations, :edit, :monavis, :update_monavis, :jeton, :update_jeton, :publication, :publish, :transfert, :allow_expert_review, :invited_expert_list, :update_allow_decision_access]
before_action :procedure_locked?, only: [:champs, :annotations]
ITEMS_PER_PAGE = 25
@ -186,7 +186,14 @@ module NewAdministrateur
end
def invited_expert_list
@invited_expert_emails = ExpertsProcedure.invited_expert_emails(@procedure)
@experts_procedure = @procedure.experts_procedures.sort_by { |expert_procedure| expert_procedure.expert.email }
end
def update_allow_decision_access
@procedure
.experts_procedures
.find(params[:expert_procedure])
.update!(allow_decision_access_params)
end
private
@ -215,5 +222,9 @@ module NewAdministrateur
def publish_params
params.permit(:path, :lien_site_web)
end
def allow_decision_access_params
params.require(:experts_procedure).permit(:allow_decision_access)
end
end
end

View file

@ -0,0 +1,66 @@
module Mutations
class DossierModifierAnnotation < Mutations::BaseMutation
argument :dossier_id, ID, "Dossier ID", required: true, loads: Types::DossierType
argument :instructeur_id, ID, "Instructeur qui demande la modification.", required: true, loads: Types::ProfileType
argument :annotation_id, ID, "Annotation ID", required: true
field :annotation, Types::ChampType, null: true
field :errors, [Types::ValidationErrorType], null: true
def resolve_with_type(type, dossier, annotation_id, instructeur, value)
annotation = find_annotation(dossier, type, annotation_id)
if block_given?
annotation.value = yield annotation.type_champ, value
else
annotation.value = value
end
if annotation.save
dossier.log_modifier_annotation!(annotation, instructeur)
{ annotation: annotation }
else
{ errors: annotation.errors.full_messages }
end
end
def authorized?(dossier:, instructeur:, **args)
dossier_authorized_for?(dossier, instructeur)
end
private
def find_annotation(dossier, type, annotation_id)
_, stable_id = GraphQL::Schema::UniqueWithinType.decode(annotation_id)
dossier.champs_private
.joins(:type_de_champ)
.find_by!(types_de_champ: {
type_champ: annotation_type_champ(type),
stable_id: stable_id
})
end
def annotation_type_champ(type)
case type
when :text
[
TypeDeChamp.type_champs.fetch(:text),
TypeDeChamp.type_champs.fetch(:textarea)
]
when :checkbox
[
TypeDeChamp.type_champs.fetch(:checkbox),
TypeDeChamp.type_champs.fetch(:yes_no),
TypeDeChamp.type_champs.fetch(:engagement)
]
when :date
TypeDeChamp.type_champs.fetch(:date)
when :datetime
TypeDeChamp.type_champs.fetch(:datetime)
when :integer_number
TypeDeChamp.type_champs.fetch(:integer_number)
end
end
end
end

View file

@ -0,0 +1,23 @@
module Mutations
class DossierModifierAnnotationCheckbox < Mutations::DossierModifierAnnotation
description "Modifier lannotation au format oui/non."
argument :value, Boolean, required: true
def resolve(dossier:, annotation_id:, instructeur:, value:)
resolve_with_type(
:checkbox,
dossier,
annotation_id,
instructeur,
value
) do |type_champ, value|
if type_champ == TypeDeChamp.type_champs.fetch(:yes_no)
value ? 'true' : 'false'
else
value ? 'on' : 'off'
end
end
end
end
end

View file

@ -0,0 +1,17 @@
module Mutations
class DossierModifierAnnotationDate < Mutations::DossierModifierAnnotation
description "Modifier lannotation au format date."
argument :value, GraphQL::Types::ISO8601Date, required: true
def resolve(dossier:, annotation_id:, instructeur:, value:)
resolve_with_type(
:date,
dossier,
annotation_id,
instructeur,
value
)
end
end
end

View file

@ -0,0 +1,17 @@
module Mutations
class DossierModifierAnnotationDatetime < Mutations::DossierModifierAnnotation
description "Modifier lannotation au format date et heure."
argument :value, GraphQL::Types::ISO8601DateTime, required: true
def resolve(dossier:, annotation_id:, instructeur:, value:)
resolve_with_type(
:datetime,
dossier,
annotation_id,
instructeur,
value
)
end
end
end

View file

@ -0,0 +1,17 @@
module Mutations
class DossierModifierAnnotationIntegerNumber < Mutations::DossierModifierAnnotation
description "Modifier lannotation au format nombre entier."
argument :value, Int, required: true
def resolve(dossier:, annotation_id:, instructeur:, value:)
resolve_with_type(
:integer_number,
dossier,
annotation_id,
instructeur,
value
)
end
end
end

View file

@ -0,0 +1,17 @@
module Mutations
class DossierModifierAnnotationText < Mutations::DossierModifierAnnotation
description "Modifier lannotation au format text."
argument :value, String, required: true
def resolve(dossier:, annotation_id:, instructeur:, value:)
resolve_with_type(
:text,
dossier,
annotation_id,
instructeur,
value
)
end
end
end

View file

@ -685,6 +685,201 @@ type DossierLinkChamp implements Champ {
stringValue: String
}
"""
Autogenerated input type of DossierModifierAnnotationCheckbox
"""
input DossierModifierAnnotationCheckboxInput {
"""
Annotation ID
"""
annotationId: ID!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Dossier ID
"""
dossierId: ID!
"""
Instructeur qui demande la modification.
"""
instructeurId: ID!
value: Boolean!
}
"""
Autogenerated return type of DossierModifierAnnotationCheckbox
"""
type DossierModifierAnnotationCheckboxPayload {
annotation: Champ
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
errors: [ValidationError!]
}
"""
Autogenerated input type of DossierModifierAnnotationDate
"""
input DossierModifierAnnotationDateInput {
"""
Annotation ID
"""
annotationId: ID!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Dossier ID
"""
dossierId: ID!
"""
Instructeur qui demande la modification.
"""
instructeurId: ID!
value: ISO8601Date!
}
"""
Autogenerated return type of DossierModifierAnnotationDate
"""
type DossierModifierAnnotationDatePayload {
annotation: Champ
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
errors: [ValidationError!]
}
"""
Autogenerated input type of DossierModifierAnnotationDatetime
"""
input DossierModifierAnnotationDatetimeInput {
"""
Annotation ID
"""
annotationId: ID!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Dossier ID
"""
dossierId: ID!
"""
Instructeur qui demande la modification.
"""
instructeurId: ID!
value: ISO8601DateTime!
}
"""
Autogenerated return type of DossierModifierAnnotationDatetime
"""
type DossierModifierAnnotationDatetimePayload {
annotation: Champ
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
errors: [ValidationError!]
}
"""
Autogenerated input type of DossierModifierAnnotationIntegerNumber
"""
input DossierModifierAnnotationIntegerNumberInput {
"""
Annotation ID
"""
annotationId: ID!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Dossier ID
"""
dossierId: ID!
"""
Instructeur qui demande la modification.
"""
instructeurId: ID!
value: Int!
}
"""
Autogenerated return type of DossierModifierAnnotationIntegerNumber
"""
type DossierModifierAnnotationIntegerNumberPayload {
annotation: Champ
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
errors: [ValidationError!]
}
"""
Autogenerated input type of DossierModifierAnnotationText
"""
input DossierModifierAnnotationTextInput {
"""
Annotation ID
"""
annotationId: ID!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Dossier ID
"""
dossierId: ID!
"""
Instructeur qui demande la modification.
"""
instructeurId: ID!
value: String!
}
"""
Autogenerated return type of DossierModifierAnnotationText
"""
type DossierModifierAnnotationTextPayload {
annotation: Champ
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
errors: [ValidationError!]
}
"""
Autogenerated input type of DossierPasserEnInstruction
"""
@ -986,42 +1181,132 @@ type Mutation {
"""
File information required to prepare a direct upload
"""
createDirectUpload(input: CreateDirectUploadInput!): CreateDirectUploadPayload
createDirectUpload(
"""
Parameters for CreateDirectUpload
"""
input: CreateDirectUploadInput!
): CreateDirectUploadPayload
"""
Accepter le dossier.
"""
dossierAccepter(input: DossierAccepterInput!): DossierAccepterPayload
dossierAccepter(
"""
Parameters for DossierAccepter
"""
input: DossierAccepterInput!
): DossierAccepterPayload
"""
Archiver le dossier.
"""
dossierArchiver(input: DossierArchiverInput!): DossierArchiverPayload
dossierArchiver(
"""
Parameters for DossierArchiver
"""
input: DossierArchiverInput!
): DossierArchiverPayload
"""
Changer le grope instructeur du dossier.
"""
dossierChangerGroupeInstructeur(input: DossierChangerGroupeInstructeurInput!): DossierChangerGroupeInstructeurPayload
dossierChangerGroupeInstructeur(
"""
Parameters for DossierChangerGroupeInstructeur
"""
input: DossierChangerGroupeInstructeurInput!
): DossierChangerGroupeInstructeurPayload
"""
Classer le dossier sans suite.
"""
dossierClasserSansSuite(input: DossierClasserSansSuiteInput!): DossierClasserSansSuitePayload
dossierClasserSansSuite(
"""
Parameters for DossierClasserSansSuite
"""
input: DossierClasserSansSuiteInput!
): DossierClasserSansSuitePayload
"""
Envoyer un message à l'usager du dossier.
"""
dossierEnvoyerMessage(input: DossierEnvoyerMessageInput!): DossierEnvoyerMessagePayload
dossierEnvoyerMessage(
"""
Parameters for DossierEnvoyerMessage
"""
input: DossierEnvoyerMessageInput!
): DossierEnvoyerMessagePayload
"""
Modifier lannotation au format oui/non.
"""
dossierModifierAnnotationCheckbox(
"""
Parameters for DossierModifierAnnotationCheckbox
"""
input: DossierModifierAnnotationCheckboxInput!
): DossierModifierAnnotationCheckboxPayload
"""
Modifier lannotation au format date.
"""
dossierModifierAnnotationDate(
"""
Parameters for DossierModifierAnnotationDate
"""
input: DossierModifierAnnotationDateInput!
): DossierModifierAnnotationDatePayload
"""
Modifier lannotation au format date et heure.
"""
dossierModifierAnnotationDatetime(
"""
Parameters for DossierModifierAnnotationDatetime
"""
input: DossierModifierAnnotationDatetimeInput!
): DossierModifierAnnotationDatetimePayload
"""
Modifier lannotation au format nombre entier.
"""
dossierModifierAnnotationIntegerNumber(
"""
Parameters for DossierModifierAnnotationIntegerNumber
"""
input: DossierModifierAnnotationIntegerNumberInput!
): DossierModifierAnnotationIntegerNumberPayload
"""
Modifier lannotation au format text.
"""
dossierModifierAnnotationText(
"""
Parameters for DossierModifierAnnotationText
"""
input: DossierModifierAnnotationTextInput!
): DossierModifierAnnotationTextPayload
"""
Passer le dossier en instruction.
"""
dossierPasserEnInstruction(input: DossierPasserEnInstructionInput!): DossierPasserEnInstructionPayload
dossierPasserEnInstruction(
"""
Parameters for DossierPasserEnInstruction
"""
input: DossierPasserEnInstructionInput!
): DossierPasserEnInstructionPayload
"""
Refuser le dossier.
"""
dossierRefuser(input: DossierRefuserInput!): DossierRefuserPayload
dossierRefuser(
"""
Parameters for DossierRefuser
"""
input: DossierRefuserInput!
): DossierRefuserPayload
}
enum Order {

View file

@ -9,5 +9,11 @@ module Types
field :dossier_accepter, mutation: Mutations::DossierAccepter
field :dossier_archiver, mutation: Mutations::DossierArchiver
field :dossier_changer_groupe_instructeur, mutation: Mutations::DossierChangerGroupeInstructeur
field :dossier_modifier_annotation_text, mutation: Mutations::DossierModifierAnnotationText
field :dossier_modifier_annotation_checkbox, mutation: Mutations::DossierModifierAnnotationCheckbox
field :dossier_modifier_annotation_date, mutation: Mutations::DossierModifierAnnotationDate
field :dossier_modifier_annotation_datetime, mutation: Mutations::DossierModifierAnnotationDatetime
field :dossier_modifier_annotation_integer_number, mutation: Mutations::DossierModifierAnnotationIntegerNumber
end
end

View file

@ -1,9 +1,9 @@
import React, { useCallback } from 'react';
import { ReactQueryCacheProvider } from 'react-query';
import { QueryClientProvider } from 'react-query';
import PropTypes from 'prop-types';
import ComboSearch from './ComboSearch';
import { queryCache } from './shared/queryCache';
import { queryClient } from './shared/queryClient';
function ComboAdresseSearch({
mandatory,
@ -16,7 +16,7 @@ function ComboAdresseSearch({
const transformResults = useCallback((_, { features }) => features);
return (
<ReactQueryCacheProvider queryCache={queryCache}>
<QueryClientProvider client={queryClient}>
<ComboSearch
placeholder={placeholder}
required={mandatory}
@ -28,7 +28,7 @@ function ComboAdresseSearch({
transformResult={transformResult}
transformResults={transformResults}
/>
</ReactQueryCacheProvider>
</QueryClientProvider>
);
}

View file

@ -1,12 +1,12 @@
import React from 'react';
import { ReactQueryCacheProvider } from 'react-query';
import { QueryClientProvider } from 'react-query';
import ComboSearch from './ComboSearch';
import { queryCache } from './shared/queryCache';
import { queryClient } from './shared/queryClient';
function ComboAnnuaireEducationSearch(params) {
return (
<ReactQueryCacheProvider queryCache={queryCache}>
<QueryClientProvider client={queryClient}>
<ComboSearch
required={params.mandatory}
hiddenFieldId={params.hiddenFieldId}
@ -21,7 +21,7 @@ function ComboAnnuaireEducationSearch(params) {
}
}) => [id, `${nom_etablissement}, ${nom_commune} (${id})`]}
/>
</ReactQueryCacheProvider>
</QueryClientProvider>
);
}

View file

@ -1,12 +1,12 @@
import React from 'react';
import { ReactQueryCacheProvider } from 'react-query';
import { QueryClientProvider } from 'react-query';
import ComboSearch from './ComboSearch';
import { queryCache } from './shared/queryCache';
import { queryClient } from './shared/queryClient';
function ComboCommunesSearch(params) {
return (
<ReactQueryCacheProvider queryCache={queryCache}>
<QueryClientProvider client={queryClient}>
<ComboSearch
required={params.mandatory}
hiddenFieldId={params.hiddenFieldId}
@ -17,7 +17,7 @@ function ComboCommunesSearch(params) {
`${nom} (${codesPostaux[0]})`
]}
/>
</ReactQueryCacheProvider>
</QueryClientProvider>
);
}

View file

@ -1,9 +1,9 @@
import React, { useCallback } from 'react';
import { ReactQueryCacheProvider } from 'react-query';
import matchSorter from 'match-sorter';
import { QueryClientProvider } from 'react-query';
import { matchSorter } from 'match-sorter';
import ComboSearch from './ComboSearch';
import { queryCache } from './shared/queryCache';
import { queryClient } from './shared/queryClient';
const extraTerms = [{ code: '99', nom: 'Etranger' }];
@ -16,7 +16,7 @@ function ComboDepartementsSearch(params) {
]);
return (
<ReactQueryCacheProvider queryCache={queryCache}>
<QueryClientProvider client={queryClient}>
<ComboSearch
required={params.mandatory}
hiddenFieldId={params.hiddenFieldId}
@ -25,7 +25,7 @@ function ComboDepartementsSearch(params) {
transformResult={({ code, nom }) => [code, `${code} - ${nom}`]}
transformResults={transformResults}
/>
</ReactQueryCacheProvider>
</QueryClientProvider>
);
}

View file

@ -0,0 +1,250 @@
import React, {
useMemo,
useState,
useRef,
useContext,
createContext,
useEffect,
useLayoutEffect
} from 'react';
import PropTypes from 'prop-types';
import {
Combobox,
ComboboxInput,
ComboboxList,
ComboboxOption,
ComboboxPopover
} from '@reach/combobox';
import '@reach/combobox/styles.css';
import { matchSorter } from 'match-sorter';
import { fire } from '@utils';
const Context = createContext();
function ComboMultipleDropdownList({
options,
hiddenFieldId,
selected,
label,
acceptNewValues = false
}) {
if (label == undefined) {
label = 'Choisir une option';
}
if (Array.isArray(options[0]) == false) {
options = options.map((o) => [o, o]);
}
const inputRef = useRef();
const [term, setTerm] = useState('');
const [selections, setSelections] = useState(selected);
const [newValues, setNewValues] = useState([]);
const results = useMemo(
() =>
(term
? matchSorter(
options.filter((o) => !o[0].startsWith('--')),
term
)
: options
).filter((o) => o[0] && !selections.includes(o[1])),
[term, selections.join(',')]
);
const hiddenField = useMemo(
() => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
[hiddenFieldId]
);
const handleChange = (event) => {
setTerm(event.target.value);
};
const onKeyDown = (event) => {
if (event.key === 'Enter') {
if (term && options.map((o) => o[0]).includes(term)) {
event.preventDefault();
return onSelect(term);
}
if (
acceptNewValues &&
term &&
matchSorter(
options.map((o) => o[0]),
term
).length == 0 // ignore when was pressed for selecting popover option
) {
event.preventDefault();
setNewValues([...newValues, term]);
saveSelection([...selections, term]);
setTerm('');
}
}
};
const saveSelection = (selections) => {
setSelections(selections);
if (hiddenField) {
hiddenField.setAttribute('value', JSON.stringify(selections));
fire(hiddenField, 'autosave:trigger');
}
};
const onSelect = (value) => {
let sel = options.find((o) => o[0] == value)[1];
saveSelection([...selections, sel]);
setTerm('');
};
const onRemove = (value) => {
saveSelection(
selections.filter((s) =>
newValues.includes(value)
? s != value
: s !== options.find((o) => o[0] == value)[1]
)
);
inputRef.current.focus();
};
return (
<Combobox openOnFocus={true} onSelect={onSelect} aria-label={label}>
<ComboboxTokenLabel onRemove={onRemove}>
<ul
aria-live="polite"
aria-atomic={true}
data-reach-combobox-token-list
>
{selections.map((selection) => (
<ComboboxToken
key={selection}
value={
newValues.find((newValue) => newValue == selection) ||
options.find((o) => o[1] == selection)[0]
}
/>
))}
</ul>
<ComboboxInput
ref={inputRef}
value={term}
onChange={handleChange}
onKeyDown={onKeyDown}
autocomplete={false}
/>
</ComboboxTokenLabel>
{results && (
<ComboboxPopover portal={false}>
{results.length === 0 && (
<p>
Aucun résultat{' '}
<button
onClick={() => {
setTerm('');
}}
>
Effacer
</button>
</p>
)}
<ComboboxList>
{results.map((value, index) => {
if (value[0].startsWith('--')) {
return <ComboboxSeparator key={index} value={value[0]} />;
}
return <ComboboxOption key={index} value={value[0]} />;
})}
</ComboboxList>
</ComboboxPopover>
)}
</Combobox>
);
}
function ComboboxTokenLabel({ onRemove, ...props }) {
const selectionsRef = useRef([]);
useLayoutEffect(() => {
selectionsRef.current = [];
return () => (selectionsRef.current = []);
});
const context = {
onRemove,
selectionsRef
};
return (
<Context.Provider value={context}>
<div data-combobox-token-label {...props} />
</Context.Provider>
);
}
ComboboxTokenLabel.propTypes = {
onRemove: PropTypes.func
};
function ComboboxSeparator({ value }) {
return (
<li aria-disabled="true" role="option" data-combobox-separator>
{value.slice(2, -2)}
</li>
);
}
ComboboxSeparator.propTypes = {
value: PropTypes.string
};
function ComboboxToken({ value, ...props }) {
const { selectionsRef, onRemove } = useContext(Context);
useEffect(() => {
selectionsRef.current.push(value);
});
return (
<li
data-reach-combobox-token
tabIndex="0"
onKeyDown={(event) => {
if (event.key === 'Backspace') {
onRemove(value);
}
}}
{...props}
>
<span
role="presentation"
data-combobox-remove-token
onClick={() => {
onRemove(value);
}}
>
x
</span>
{value}
</li>
);
}
ComboboxToken.propTypes = {
value: PropTypes.string,
label: PropTypes.string
};
ComboMultipleDropdownList.propTypes = {
options: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.arrayOf(
PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
)
)
]),
hiddenFieldId: PropTypes.string,
selected: PropTypes.arrayOf(PropTypes.string),
arraySelected: PropTypes.arrayOf(PropTypes.array),
label: PropTypes.string,
acceptNewValues: PropTypes.bool
};
export default ComboMultipleDropdownList;

View file

@ -1,12 +1,12 @@
import React from 'react';
import { ReactQueryCacheProvider } from 'react-query';
import { QueryClientProvider } from 'react-query';
import ComboSearch from './ComboSearch';
import { queryCache } from './shared/queryCache';
import { queryClient } from './shared/queryClient';
function ComboPaysSearch(params) {
return (
<ReactQueryCacheProvider queryCache={queryCache}>
<QueryClientProvider client={queryClient}>
<ComboSearch
required={params.mandatory}
hiddenFieldId={params.hiddenFieldId}
@ -14,7 +14,7 @@ function ComboPaysSearch(params) {
minimumInputLength={0}
transformResult={({ nom }) => [nom, nom]}
/>
</ReactQueryCacheProvider>
</QueryClientProvider>
);
}

View file

@ -1,12 +1,12 @@
import React from 'react';
import { ReactQueryCacheProvider } from 'react-query';
import { QueryClientProvider } from 'react-query';
import ComboSearch from './ComboSearch';
import { queryCache } from './shared/queryCache';
import { queryClient } from './shared/queryClient';
function ComboRegionsSearch(params) {
return (
<ReactQueryCacheProvider queryCache={queryCache}>
<QueryClientProvider client={queryClient}>
<ComboSearch
required={params.mandatory}
hiddenFieldId={params.hiddenFieldId}
@ -14,7 +14,7 @@ function ComboRegionsSearch(params) {
minimumInputLength={0}
transformResult={({ code, nom }) => [code, nom]}
/>
</ReactQueryCacheProvider>
</QueryClientProvider>
);
}

View file

@ -1,5 +1,5 @@
import React, { useState, useMemo, useCallback, useRef } from 'react';
import { useDebounce } from 'react-use';
import { useDebounce } from 'use-debounce';
import { useQuery } from 'react-query';
import PropTypes from 'prop-types';
import {
@ -41,7 +41,7 @@ function ComboSearch({
);
const initialValue = hiddenValueField && hiddenValueField.value;
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebounce(searchTerm, 300);
const [value, setValue] = useState(initialValue);
const resultsMap = useRef({});
const setExternalValue = useCallback((value) => {
@ -64,14 +64,6 @@ function ComboSearch({
}
});
useDebounce(
() => {
setDebouncedSearchTerm(searchTerm);
},
300,
[searchTerm]
);
const handleOnChange = useCallback(
({ target: { value } }) => {
setValue(value);

View file

@ -1,4 +1,3 @@
import scrollToComponent from 'react-scroll-to-component';
import { debounce } from '@utils';
import {
createTypeDeChampOperation,
@ -53,8 +52,10 @@ function addTypeDeChamp(state, typeDeChamps, insertAfter, done) {
state.flash.success();
done();
if (insertAfter) {
scrollToComponent(insertAfter.target.nextElementSibling, {
duration: 300
insertAfter.target.nextElementSibling.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest'
});
}
})

View file

@ -1,12 +1,12 @@
import { QueryCache } from 'react-query';
import { QueryClient } from 'react-query';
import { isNumeric } from '@utils';
import matchSorter from 'match-sorter';
import { matchSorter } from 'match-sorter';
const { api_geo_url, api_adresse_url, api_education_url } =
gon.autocomplete || {};
export const queryCache = new QueryCache({
defaultConfig: {
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: defaultQueryFn
}
@ -35,7 +35,7 @@ function buildOptions() {
return [{}, null];
}
async function defaultQueryFn(scope, term) {
async function defaultQueryFn({ queryKey: [scope, term] }) {
if (scope == 'pays') {
return matchSorter(await getPays(), term, { keys: ['nom'] });
}

View file

@ -0,0 +1,5 @@
import Loadable from '../components/Loadable';
export default Loadable(() =>
import('../components/ComboMultipleDropdownList')
);

View file

@ -1,80 +0,0 @@
import $ from 'jquery';
import 'select2';
const language = {
errorLoading: function () {
return 'Les résultats ne peuvent pas être chargés.';
},
inputTooLong: function (args) {
var overChars = args.input.length - args.maximum;
return 'Supprimez ' + overChars + ' caractère' + (overChars > 1 ? 's' : '');
},
inputTooShort: function (args) {
var remainingChars = args.minimum - args.input.length;
return (
'Saisissez au moins ' +
remainingChars +
' caractère' +
(remainingChars > 1 ? 's' : '')
);
},
loadingMore: function () {
return 'Chargement de résultats supplémentaires…';
},
maximumSelected: function (args) {
return (
'Vous pouvez seulement sélectionner ' +
args.maximum +
' élément' +
(args.maximum > 1 ? 's' : '')
);
},
noResults: function () {
return 'Aucun résultat trouvé';
},
searching: function () {
return 'Recherche en cours…';
},
removeAllItems: function () {
return 'Supprimer tous les éléments';
}
};
const baseOptions = {
language,
width: '100%'
};
const templateOption = ({ text }) =>
$(
`<span class="custom-select2-option"><span class="icon person"></span>${text}</span>`
);
addEventListener('ds:page:update', () => {
$('select.select2').select2(baseOptions);
$('.columns-form select.select2-limited').select2({
width: '300px',
placeholder: 'Sélectionnez des colonnes',
maximumSelectionLength: '5'
});
$('.recipients-form select.select2-limited').select2({
language,
width: '300px',
placeholder: 'Sélectionnez des instructeurs',
maximumSelectionLength: '30'
});
$('select.select2-limited.select-instructeurs').select2({
language,
dropdownParent: $('.instructeur-wrapper'),
placeholder: 'Saisir ladresse email de linstructeur',
tags: true,
tokenSeparators: [',', ' '],
templateResult: templateOption,
templateSelection: templateOption
});
});

View file

@ -17,7 +17,6 @@ import '../new_design/dropdown';
import '../new_design/form-validation';
import '../new_design/procedure-context';
import '../new_design/procedure-form';
import '../new_design/select2';
import '../new_design/spinner';
import '../new_design/support';
import '../new_design/dossiers/auto-save';

View file

@ -679,6 +679,10 @@ class Dossier < ApplicationRecord
end
end
def log_modifier_annotation!(champ, instructeur)
log_dossier_operation(instructeur, :modifier_annotation, champ)
end
def demander_un_avis!(avis)
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end

View file

@ -14,15 +14,4 @@ class ExpertsProcedure < ApplicationRecord
belongs_to :procedure
has_many :avis, dependent: :destroy
def email_to_display
expert&.email
end
def self.invited_expert_emails(procedure)
joins(:expert)
.where(procedure: procedure)
.map(&:email_to_display)
.sort
end
end

View file

@ -62,7 +62,7 @@ class DossierSearchService
def self.to_tsquery(search_terms)
(search_terms || "")
.gsub(/['?\\:&|!<>\(\)]/, "") # drop disallowed characters
.gsub(/['?\\:&|!<>()]/, "") # drop disallowed characters
.strip
.split(/\s+/) # split words
.map { |x| "#{x}:*" } # enable prefix matching

View file

@ -8,9 +8,8 @@
Le destinataire suivra automatiquement le dossier
= form_for dossier, url: send_to_instructeurs_instructeur_dossier_path(dossier.procedure, dossier), method: :post, html: { class: 'form recipients-form' } do |f|
.flex.justify-start.align-start
= select_tag(:recipients,
options_from_collection_for_select(potential_recipients, :id, :email),
multiple: true,
class: 'select2-limited',
placeholder: '')
- hidden_field_id = SecureRandom.uuid
= hidden_field_tag :recipients, nil, data: { uuid: hidden_field_id }
= react_component("ComboMultipleDropdownList", options: potential_recipients.map{|r| [r.email, r.id]}, selected: [], disabled: [], hiddenFieldId: hidden_field_id, label: "email instructeur")
= f.submit "Envoyer", class: "button large send gap-left"

View file

@ -122,10 +122,10 @@
Personnaliser
#custom-menu.dropdown-content.fade-in-down
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form columns-form' do
= select_tag :values,
options_for_select(@displayed_fields_options, selected: @displayed_fields_selected),
multiple: true,
class: 'select2-limited'
- hidden_field_id = SecureRandom.uuid
= hidden_field_tag :values, nil, data: { uuid: hidden_field_id }
= react_component("ComboMultipleDropdownList", options: @displayed_fields_options, selected: @displayed_fields_selected, disabled: [], hiddenFieldId: hidden_field_id, label: 'colonne')
= submit_tag "Enregistrer", class: 'button'
%tbody

View file

@ -25,12 +25,15 @@
.instructeur-wrapper
- if !@procedure.routee?
%p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
= select_tag :emails,
options_for_select(@available_instructeur_emails),
multiple: true,
class: 'select-instructeurs select2-limited'
- hidden_field_id = SecureRandom.uuid
= hidden_field_tag :emails, nil, data: { uuid: hidden_field_id }
= react_component("ComboMultipleDropdownList",
options: @available_instructeur_emails, selected: [], disabled: [],
hiddenFieldId: hidden_field_id,
label: 'email instructeur',
acceptNewValues: true)
= f.submit 'Affecter', class: 'button primary send'
= f.submit 'Affecter', class: 'button primary send'
%table.table.mt-2
%thead

View file

@ -1,16 +1,36 @@
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Liste des experts'] }
.container
%h1.mt-2 Experts invités sur #{@procedure.libelle}
- if @invited_expert_emails.present?
%h1.page-title.mt-2 Experts invités sur #{@procedure.libelle}
- if @experts_procedure.present?
%table.table.mt-2
%thead
%tr
%th Liste des experts
- if feature_enabled_for?(:make_experts_notifiable, @procedure)
%th Notifier des décisions sur les dossiers
%tbody
- @invited_expert_emails.each do |expert|
- @experts_procedure.each do |expert_procedure|
%tr
%td
%span.icon.person
= expert
= expert_procedure.expert.email
- if feature_enabled_for?(:make_experts_notifiable, @procedure)
%td
= form_for expert_procedure,
url: admin_procedure_update_allow_decision_access_path(expert_procedure: expert_procedure),
remote: true,
method: :put,
authenticity_token: true,
html: { class: 'form procedure-form__column--form no-background' } do |f|
%label.toggle-switch
= f.check_box :allow_decision_access, class: 'toggle-switch-checkbox', onchange: 'this.form.submit()'
%span.toggle-switch-control.round
%span.toggle-switch-label.on
%span.toggle-switch-label.off
- else
.blank-tab
%h2.empty-text Aucun expert invité pour le moment.

View file

@ -167,7 +167,7 @@
%p.card-admin-subtitle Liste des experts invités par les instructeurs
.card-admin-action
= link_to "Voir", invited_expert_list_admin_procedure_path(@procedure), class: 'button'
= link_to "Voir", admin_procedure_invited_expert_list_path(@procedure), class: 'button'
.card-admin

View file

@ -7,10 +7,7 @@
= b.text
- else
= form.select :value,
champ.options,
{ selected: champ.selected_options,
disabled: champ.disabled_options },
multiple: true,
class: 'select2'
- hidden_field_id = SecureRandom.uuid
= form.hidden_field :value, { data: { uuid: hidden_field_id } }
= react_component("ComboMultipleDropdownList", options: champ.options, selected: champ.selected_options, disabled: champ.disabled_options, hiddenFieldId: hidden_field_id, label: champ.libelle)

View file

@ -5,7 +5,7 @@ Rails.application.configure do
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
config.cache_classes = false
# Do not eager load code on boot. This avoids loading your whole application
# just for the purpose of running a single test. If you are using a tool that

View file

@ -5,11 +5,10 @@ Rails.application.config.assets.version = '1.0'
# Add additional assets to the asset load path
Rails.application.config.assets.paths << Rails.root.join('node_modules', 'trix', 'dist')
Rails.application.config.assets.paths << Rails.root.join('node_modules', 'select2', 'dist', 'css')
Rails.application.config.assets.paths << Rails.root.join('node_modules', 'mapbox-gl', 'dist')
Rails.application.config.assets.paths << Rails.root.join('node_modules', '@reach', 'combobox')
Rails.application.config.assets.paths << Rails.root.join('node_modules', '@mapbox', 'mapbox-gl-draw', 'dist')
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
Rails.application.config.assets.precompile += ['print.css', 'new_design/new_application.css', 'new_design/print.css', 'new_design/application.js', 'new_design/manager.css']
Rails.application.config.assets.precompile += ['print.css']

View file

@ -3,5 +3,5 @@ Sentry.init do |config|
config.send_default_pii = false
config.enabled_environments = ['production']
config.breadcrumbs_logger = [:active_support_logger]
config.traces_sample_rate = 0.01
config.traces_sample_rate = 0.001
end

View file

@ -379,13 +379,14 @@ Rails.application.routes.draw do
get 'jeton'
patch 'update_jeton'
put :allow_expert_review
get 'invited_expert_list'
end
get 'publication' => 'procedures#publication', as: :publication
put 'publish' => 'procedures#publish', as: :publish
get 'transfert' => 'procedures#transfert', as: :transfert
post 'transfer' => 'procedures#transfer', as: :transfer
get 'invited_expert_list'
put 'update_allow_decision_access' => 'procedures#update_allow_decision_access', as: :update_allow_decision_access
resources :mail_templates, only: [:edit, :update]

View file

@ -36,6 +36,7 @@ default: &default
extensions:
- .mjs
- .js
- .jsx
- .sass
- .scss
- .css

View file

@ -1,15 +1,15 @@
{
"dependencies": {
"@babel/preset-react": "^7.10.4",
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/react-fontawesome": "^0.1.9",
"@babel/preset-react": "^7.12.13",
"@fortawesome/fontawesome-svg-core": "^1.2.34",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/react-fontawesome": "^0.1.14",
"@mapbox/mapbox-gl-draw": "^1.2.0",
"@rails/actiontext": "^6.0.3",
"@rails/activestorage": "^6.0.3",
"@rails/ujs": "^6.0.3",
"@rails/webpacker": "5.1.1",
"@reach/combobox": "^0.10.2",
"@reach/combobox": "^0.13.0",
"@sentry/browser": "^5.15.5",
"@tmcw/togeojson": "^4.0.0",
"babel-plugin-macros": "^2.8.0",
@ -20,23 +20,20 @@
"dom4": "^2.1.5",
"email-butler": "^1.0.13",
"highcharts": "^8.1.1",
"intersection-observer": "^0.10.0",
"jquery": "^3.5.1",
"intersection-observer": "^0.12.0",
"mapbox-gl": "^1.11.1",
"match-sorter": "^4.2.1",
"match-sorter": "^6.2.0",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-intersection-observer": "^8.26.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-intersection-observer": "^8.31.0",
"react-mapbox-gl": "^4.8.6",
"react-mapbox-gl-draw": "^2.0.4",
"react-query": "^2.23.1",
"react-scroll-to-component": "^1.0.2",
"react-query": "^3.9.7",
"react-sortable-hoc": "^1.11.0",
"react-use": "^15.3.4",
"react_ujs": "^2.6.1",
"select2": "^4.0.13",
"trix": "^1.2.3",
"use-debounce": "^5.2.0",
"whatwg-fetch": "^3.0.0"
},
"devDependencies": {
@ -45,15 +42,15 @@
"eslint": "^7.0.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.0.2",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"netlify-cli": "^2.61.2",
"prettier": "^2.0.5",
"webpack-bundle-analyzer": "^3.7.0",
"webpack-dev-server": "^3.11.0"
},
"scripts": {
"lint:js": "eslint ./app/javascript ./config/webpack",
"lint:js": "eslint --ext .js,.jsx,.ts,.tsx ./app/javascript ./config/webpack",
"webpack:build": "NODE_ENV=production bin/webpack",
"graphql:docs:build": "graphdoc --force",
"graphql:docs:deploy": "netlify deploy -d ./docs/graphql --prod",

View file

@ -1,11 +1,12 @@
describe API::V2::GraphqlController do
let(:admin) { create(:administrateur) }
let(:token) { admin.renew_api_token }
let(:procedure) { create(:procedure, :published, :for_individual, :with_service, :with_all_champs, administrateurs: [admin]) }
let(:procedure) { create(:procedure, :published, :for_individual, :with_service, :with_all_champs, :with_all_annotations, administrateurs: [admin]) }
let(:dossier) do
dossier = create(:dossier,
:en_construction,
:with_all_champs,
:with_all_annotations,
:with_individual,
procedure: procedure)
create(:commentaire, :with_file, dossier: dossier, email: 'test@test.com')
@ -1117,6 +1118,240 @@ describe API::V2::GraphqlController do
end
end
end
describe 'dossierModifierAnnotation' do
describe 'text' do
let(:query) do
"mutation {
dossierModifierAnnotationText(input: {
dossierId: \"#{dossier.to_typed_id}\",
annotationId: \"#{dossier.champs_private.first.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
value: \"hello\"
}) {
annotation {
stringValue
}
errors {
message
}
}
}"
end
context "success" do
it 'should be a success' do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierModifierAnnotationText: {
annotation: {
stringValue: 'hello'
},
errors: nil
})
end
end
end
describe 'checkbox' do
let(:value) { 'true' }
let(:query) do
"mutation {
dossierModifierAnnotationCheckbox(input: {
dossierId: \"#{dossier.to_typed_id}\",
annotationId: \"#{dossier.champs_private.find { |c| c.type_champ == 'checkbox' }.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
value: #{value}
}) {
annotation {
stringValue
}
errors {
message
}
}
}"
end
context "success when true" do
it 'should be a success' do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierModifierAnnotationCheckbox: {
annotation: {
stringValue: 'true'
},
errors: nil
})
end
end
context "success when false" do
let(:value) { 'false' }
it 'should be a success' do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierModifierAnnotationCheckbox: {
annotation: {
stringValue: 'false'
},
errors: nil
})
end
end
end
describe 'yes_no' do
let(:value) { 'true' }
let(:query) do
"mutation {
dossierModifierAnnotationCheckbox(input: {
dossierId: \"#{dossier.to_typed_id}\",
annotationId: \"#{dossier.champs_private.find { |c| c.type_champ == 'yes_no' }.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
value: #{value}
}) {
annotation {
stringValue
}
errors {
message
}
}
}"
end
context "success when true" do
it 'should be a success' do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierModifierAnnotationCheckbox: {
annotation: {
stringValue: 'true'
},
errors: nil
})
end
end
context "success when false" do
let(:value) { 'false' }
it 'should be a success' do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierModifierAnnotationCheckbox: {
annotation: {
stringValue: 'false'
},
errors: nil
})
end
end
end
describe 'date' do
let(:query) do
"mutation {
dossierModifierAnnotationDate(input: {
dossierId: \"#{dossier.to_typed_id}\",
annotationId: \"#{dossier.champs_private.find { |c| c.type_champ == 'date' }.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
value: \"#{1.day.from_now.to_date.iso8601}\"
}) {
annotation {
stringValue
}
errors {
message
}
}
}"
end
context "success" do
it 'should be a success' do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierModifierAnnotationDate: {
annotation: {
stringValue: dossier.reload.champs_private.find { |c| c.type_champ == 'date' }.to_s
},
errors: nil
})
end
end
end
describe 'datetime' do
let(:query) do
"mutation {
dossierModifierAnnotationDatetime(input: {
dossierId: \"#{dossier.to_typed_id}\",
annotationId: \"#{dossier.champs_private.find { |c| c.type_champ == 'datetime' }.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
value: \"#{1.day.from_now.iso8601}\"
}) {
annotation {
stringValue
}
errors {
message
}
}
}"
end
context "success" do
it 'should be a success' do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierModifierAnnotationDatetime: {
annotation: {
stringValue: dossier.reload.champs_private.find { |c| c.type_champ == 'datetime' }.to_s
},
errors: nil
})
end
end
end
describe 'integer_number' do
let(:query) do
"mutation {
dossierModifierAnnotationIntegerNumber(input: {
dossierId: \"#{dossier.to_typed_id}\",
annotationId: \"#{dossier.champs_private.find { |c| c.type_champ == 'integer_number' }.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
value: 42
}) {
annotation {
stringValue
}
errors {
message
}
}
}"
end
context "success" do
it 'should be a success' do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierModifierAnnotationIntegerNumber: {
annotation: {
stringValue: '42'
},
errors: nil
})
end
end
end
end
end
end

View file

@ -38,7 +38,7 @@ describe Instructeurs::DossiersController, type: :controller do
post(
:send_to_instructeurs,
params: {
recipients: [recipient],
recipients: [recipient.id].to_json,
procedure_id: procedure.id,
dossier_id: dossier.id
}

View file

@ -207,11 +207,11 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
describe '#add_instructeur_procedure_non_routee' do
let(:procedure) { create :procedure, administrateur: admin }
let(:emails) { ['instructeur_3@ministere_a.gouv.fr', 'instructeur_4@ministere_b.gouv.fr'] }
let(:emails) { ['instructeur_3@ministere_a.gouv.fr', 'instructeur_4@ministere_b.gouv.fr'].to_json }
subject { post :add_instructeur, params: { emails: emails, procedure_id: procedure.id, id: gi_1_1.id } }
context 'when all emails are valid' do
let(:emails) { ['test@b.gouv.fr', 'test2@b.gouv.fr'] }
let(:emails) { ['test@b.gouv.fr', 'test2@b.gouv.fr'].to_json }
it { expect(response.status).to eq(200) }
it { expect(subject.request.flash[:alert]).to be_nil }
it { expect(subject.request.flash[:notice]).to be_present }
@ -219,7 +219,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
end
context 'when there is at least one bad email' do
let(:emails) { ['badmail', 'instructeur2@gmail.com'] }
let(:emails) { ['badmail', 'instructeur2@gmail.com'].to_json }
it { expect(response.status).to eq(200) }
it { expect(subject.request.flash[:alert]).to be_present }
it { expect(subject.request.flash[:notice]).to be_present }
@ -227,7 +227,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
end
context 'when the admin wants to assign an instructor who is already assigned on this procedure' do
let(:emails) { ['instructeur_1@ministere_a.gouv.fr'] }
let(:emails) { ['instructeur_1@ministere_a.gouv.fr'].to_json }
it { expect(subject.request.flash[:alert]).to be_present }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur) }
end
@ -247,7 +247,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
params: {
procedure_id: procedure.id,
id: gi_1_2.id,
emails: new_instructeur_emails
emails: new_instructeur_emails.to_json
}
end
@ -281,7 +281,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
end
context 'of an empty string' do
let(:new_instructeur_emails) { '' }
let(:new_instructeur_emails) { [''] }
it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) }
end

View file

@ -532,4 +532,34 @@ describe NewAdministrateur::ProceduresController, type: :controller do
it { expect(procedure.allow_expert_review).to be_truthy }
end
end
describe 'PUT #update_allow_decision_access' do
let!(:procedure) { create :procedure, :with_service, administrateur: admin }
let(:expert) { create(:expert) }
let(:expert_procedure) { ExpertsProcedure.create(procedure: procedure, expert: expert) }
subject do
put :update_allow_decision_access, params: { procedure_id: procedure.id, experts_procedure: { allow_decision_access: !expert_procedure.allow_decision_access }, expert_procedure: expert_procedure }, format: :js
end
context 'when the experts_procedure is true' do
let(:expert_procedure) { ExpertsProcedure.create(procedure: procedure, expert: expert, allow_decision_access: true) }
before do
subject
expert_procedure.reload
end
it { expect(expert_procedure.allow_decision_access).to be_falsy }
end
context 'when the experts_procedure is false' do
before do
subject
expert_procedure.reload
end
it { expect(expert_procedure.allow_decision_access).to be_truthy }
end
end
end

View file

@ -216,8 +216,8 @@ FactoryBot.define do
trait :with_all_annotations do
after(:create) do |dossier, _evaluator|
dossier.champs = dossier.types_de_champ.map do |type_de_champ|
build(:"champ_#{type_de_champ.type_champ}", dossier: dossier, type_de_champ: type_de_champ)
dossier.champs_private = dossier.types_de_champ_private.map do |type_de_champ|
build(:"champ_#{type_de_champ.type_champ}", private: true, dossier: dossier, type_de_champ: type_de_champ)
end
dossier.save!
end

View file

@ -128,10 +128,8 @@ feature 'Instructing a dossier:' do
click_on 'Personnes impliquées'
first('.select2-container', minimum: 1).click
find('li.select2-results__option[role="option"]', text: instructeur_2.email).click
first('.select2-container', minimum: 1).click
find('li.select2-results__option[role="option"]', text: instructeur_3.email).click
select_multi('email instructeur', instructeur_2.email)
select_multi('email instructeur', instructeur_3.email)
click_on 'Envoyer'

View file

@ -125,15 +125,13 @@ feature "procedure filters" do
def add_column(column_name)
click_on 'Personnaliser'
find("span.select2-container").click
find(:xpath, "//li[text()='#{column_name}']").click
select_multi('colonne', column_name)
click_button "Enregistrer"
end
def remove_column(column_name)
click_on 'Personnaliser'
find(:xpath, "//li[contains(@title, '#{column_name}')]/span[contains(text(), '×')]").click
find(:xpath, "//form[contains(@class, 'columns-form')]//span[contains(@class, 'select2-container')]").click
find(:xpath, "//li[contains(text(), '#{column_name}')]/span[contains(text(), 'x')]").click
click_button "Enregistrer"
end
end

View file

@ -30,14 +30,14 @@ feature 'The routing', js: true do
expect(page).to have_field('Nom du groupe', with: 'littéraire')
# add victor to littéraire groupe
find('input.select2-search__field').send_keys('victor@inst.com', :enter)
find("input[aria-label='email instructeur'").send_keys('victor@inst.com', :enter)
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
victor = User.find_by(email: 'victor@inst.com').instructeur
# add superwoman to littéraire groupe
find('input.select2-search__field').send_keys('superwoman@inst.com', :enter)
find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter)
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
@ -50,14 +50,14 @@ feature 'The routing', js: true do
expect(page).to have_text('Le groupe dinstructeurs « scientifique » a été créé.')
# add marie to scientifique groupe
find('input.select2-search__field').send_keys('marie@inst.com', :enter)
find("input[aria-label='email instructeur'").send_keys('marie@inst.com', :enter)
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("Linstructeur marie@inst.com a été affecté")
marie = User.find_by(email: 'marie@inst.com').instructeur
# add superwoman to scientifique groupe
find('input.select2-search__field').send_keys('superwoman@inst.com', :enter)
find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter)
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("Linstructeur superwoman@inst.com a été affecté")

View file

@ -25,8 +25,9 @@ feature 'The user' do
check('val1')
check('val3')
select('bravo', from: form_id_for('simple_choice_drop_down_list_long'))
select('alpha', from: form_id_for('multiple_choice_drop_down_list_long'))
select('charly', from: form_id_for('multiple_choice_drop_down_list_long'))
select_multi('multiple_choice_drop_down_list_long', 'alpha')
select_multi('multiple_choice_drop_down_list_long', 'charly')
select_champ_geo('pays', 'aust', 'AUSTRALIE')
select_champ_geo('regions', 'Ma', 'Martinique')
@ -83,7 +84,7 @@ feature 'The user' do
expect(page).to have_checked_field('val1')
expect(page).to have_checked_field('val3')
expect(page).to have_selected_value('simple_choice_drop_down_list_long', selected: 'bravo')
expect(page).to have_selected_value('multiple_choice_drop_down_list_long', selected: ['alpha', 'charly'])
check_selected_values('multiple_choice_drop_down_list_long', ['alpha', 'charly'])
expect(page).to have_hidden_field('pays', with: 'AUSTRALIE')
expect(page).to have_hidden_field('regions', with: 'Martinique')
expect(page).to have_hidden_field('departements', with: '02 - Aisne')

View file

@ -7,7 +7,7 @@ RSpec.describe ExpertsProcedure, type: :model do
let(:experts_procedure) { ExpertsProcedure.create(expert: expert, procedure: procedure) }
let(:experts_procedure2) { ExpertsProcedure.create(expert: expert2, procedure: procedure) }
let(:experts_procedure3) { ExpertsProcedure.create(expert: expert3, procedure: procedure) }
subject { ExpertsProcedure.invited_expert_emails(procedure) }
subject { procedure.experts_procedures }
context 'when there is one dossier' do
let!(:dossier) { create(:dossier, procedure: procedure) }
@ -15,14 +15,18 @@ RSpec.describe ExpertsProcedure, type: :model do
context 'when a procedure has one avis and known instructeur' do
let!(:avis) { create(:avis, dossier: dossier, instructeur: create(:instructeur, email: expert.email), experts_procedure: experts_procedure) }
it { is_expected.to eq([expert.email]) }
it { is_expected.to eq([experts_procedure]) }
it { expect(procedure.experts.count).to eq(1) }
it { expect(procedure.experts.first.email).to eq(expert.email) }
end
context 'when a dossier has 2 avis from the same expert' do
let!(:avis) { create(:avis, dossier: dossier, experts_procedure: experts_procedure) }
let!(:avis2) { create(:avis, dossier: dossier, experts_procedure: experts_procedure) }
it { is_expected.to eq([expert.email]) }
it { is_expected.to eq([experts_procedure]) }
it { expect(procedure.experts.count).to eq(1) }
it { expect(procedure.experts.first).to eq(expert) }
end
end
@ -35,7 +39,9 @@ RSpec.describe ExpertsProcedure, type: :model do
let!(:avis2) { create(:avis, dossier: dossier2, experts_procedure: experts_procedure2) }
let!(:avis3) { create(:avis, dossier: dossier2, experts_procedure: experts_procedure3) }
it { is_expected.to eq([expert.email, expert2.email, expert3.email].sort) }
it { is_expected.to match_array([experts_procedure, experts_procedure2, experts_procedure3]) }
it { expect(procedure.experts.count).to eq(3) }
it { expect(procedure.experts).to match_array([expert, expert2, expert3]) }
end
end
end

View file

@ -914,9 +914,11 @@ describe ProcedurePresentation do
it 'should downcase and transform value' do
procedure_presentation.add_filter("suivis", "type_de_champ/#{first_type_de_champ_id}", "Oui")
expect(procedure_presentation.filters).to eq({ "suivis" => [
{ "label" => first_type_de_champ.libelle, "table" => "type_de_champ", "column" => first_type_de_champ_id, "value" => "true" }
]
expect(procedure_presentation.filters).to eq({
"suivis" =>
[
{ "label" => first_type_de_champ.libelle, "table" => "type_de_champ", "column" => first_type_de_champ_id, "value" => "true" }
]
})
end
end
@ -927,9 +929,10 @@ describe ProcedurePresentation do
it 'should passthrough value' do
procedure_presentation.add_filter("suivis", "type_de_champ/#{first_type_de_champ_id}", "Oui")
expect(procedure_presentation.filters).to eq({ "suivis" => [
{ "label" => first_type_de_champ.libelle, "table" => "type_de_champ", "column" => first_type_de_champ_id, "value" => "Oui" }
]
expect(procedure_presentation.filters).to eq({
"suivis" => [
{ "label" => first_type_de_champ.libelle, "table" => "type_de_champ", "column" => first_type_de_champ_id, "value" => "Oui" }
]
})
end
end

View file

@ -27,8 +27,8 @@ module FeatureHelpers
if sign_in_by_link
mail = ActionMailer::Base.deliveries.last
message = mail.html_part.body.raw_source
instructeur_id = message[/\".+\/connexion-par-jeton\/(.+)\?jeton=(.*)\"/, 1]
jeton = message[/\".+\/connexion-par-jeton\/(.+)\?jeton=(.*)\"/, 2]
instructeur_id = message[/".+\/connexion-par-jeton\/(.+)\?jeton=(.*)"/, 1]
jeton = message[/".+\/connexion-par-jeton\/(.+)\?jeton=(.*)"/, 2]
visit sign_in_by_link_path(instructeur_id, jeton: jeton)
end
@ -103,6 +103,25 @@ module FeatureHelpers
end
end
def select_multi(champ, with)
input = find("input[aria-label='#{champ}'")
input.click
# hack because for unknown reason, the click on input doesn't show combobox-popover with selenium driver
script = "document.evaluate(\"//input[@aria-label='#{champ}']//ancestor::div[@data-reach-combobox]/div[@data-reach-combobox-popover]\", document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null).iterateNext().removeAttribute(\"hidden\")"
execute_script(script)
element = find(:xpath, "//input[@aria-label='#{champ}']/ancestor::div[@data-reach-combobox]//div[@data-reach-combobox-popover]//li/span[normalize-space(text())='#{with}']")
element.click
end
def check_selected_values(champ, values)
combobox = find(:xpath, "//input[@aria-label='#{champ}']/ancestor::div[@data-react-class='ComboMultipleDropdownList']")
hiddenFieldId = JSON.parse(combobox["data-react-props"])["hiddenFieldId"]
hiddenField = find("input[data-uuid='#{hiddenFieldId}']")
expect(values.sort).to eq(JSON.parse(hiddenField.value).sort)
end
# Keep the brower window open after a test success of failure, to
# allow inspecting the page or the console.
#

View file

@ -13,7 +13,7 @@ describe 'instructeurs/dossiers/envoyer_dossier_block.html.haml', type: :view do
let(:instructeur) { create(:instructeur, email: 'yop@totomail.fr') }
let(:potential_recipients) { [instructeur] }
it { is_expected.to have_css("select > option[value='#{instructeur.id}']") }
it { is_expected.to match(/data-react-props.*#{instructeur.email}/) }
it { is_expected.to have_css(".button.send") }
end

View file

@ -12,13 +12,13 @@ describe 'new_administrateur/procedures/invited_expert_list.html.haml', type: :v
context 'when the procedure has 0 avis' do
let!(:dossier) { create(:dossier, procedure: procedure) }
before do
@invited_expert_emails = ExpertsProcedure.invited_expert_emails(procedure)
@invited_experts = procedure.experts_procedures
subject
end
it 'has 0 experts into the page' do
expect(@invited_expert_emails.count).to eq(0)
expect(@invited_expert_emails).to eq([])
expect(@invited_experts.count).to eq(0)
expect(@invited_experts).to eq([])
end
end
@ -32,13 +32,13 @@ describe 'new_administrateur/procedures/invited_expert_list.html.haml', type: :v
let!(:avis2) { create(:avis, dossier: dossier, experts_procedure: experts_procedure2) }
before do
@invited_expert_emails = ExpertsProcedure.invited_expert_emails(procedure)
@invited_experts = procedure.experts_procedures
subject
end
it 'has 2 experts and match array' do
expect(@invited_expert_emails.count).to eq(2)
expect(@invited_expert_emails).to eq([expert.email, expert2.email].sort)
expect(@invited_experts.count).to eq(2)
expect(@invited_experts).to match_array([experts_procedure, experts_procedure2])
end
end
end

View file

@ -82,7 +82,7 @@ describe 'shared/dossiers/edit.html.haml', type: :view do
let(:type_de_champ) { create(:type_de_champ_multiple_drop_down_list, :long, procedure: dossier.procedure) }
it 'renders the list as a multiple-selection dropdown' do
expect(subject).to have_selector('select.select2')
expect(subject).to have_selector('[data-react-class="ComboMultipleDropdownList"]')
end
end
end

814
yarn.lock

File diff suppressed because it is too large Load diff