diff --git a/Gemfile b/Gemfile
index 13763e699..3a224b34a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -13,12 +13,9 @@ gem 'bcrypt'
gem 'bootstrap-sass', '>= 3.4.1'
gem 'bootstrap-wysihtml5-rails', '~> 0.3.3.8'
gem 'browser'
-gem 'carrierwave'
-gem 'carrierwave-i18n'
gem 'chartkick'
gem 'chunky_png'
gem 'clamav-client', require: 'clamav/client'
-gem 'copy_carrierwave_file'
gem 'daemons'
gem 'deep_cloneable' # Enable deep clone of active record models
gem 'delayed_cron_job' # Cron jobs
@@ -93,6 +90,7 @@ group :test do
gem 'shoulda-matchers', require: false
gem 'timecop'
gem 'vcr'
+ gem 'webdrivers', '~> 4.0'
gem 'webmock'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 7eaa820eb..9efd53568 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -20,25 +20,25 @@ GEM
specs:
aasm (5.0.1)
concurrent-ruby (~> 1.0)
- actioncable (5.2.2.1)
- actionpack (= 5.2.2.1)
+ actioncable (5.2.3)
+ actionpack (= 5.2.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailer (5.2.2.1)
- actionpack (= 5.2.2.1)
- actionview (= 5.2.2.1)
- activejob (= 5.2.2.1)
+ actionmailer (5.2.3)
+ actionpack (= 5.2.3)
+ actionview (= 5.2.3)
+ activejob (= 5.2.3)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (5.2.2.1)
- actionview (= 5.2.2.1)
- activesupport (= 5.2.2.1)
+ actionpack (5.2.3)
+ actionview (= 5.2.3)
+ activesupport (= 5.2.3)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.2.2.1)
- activesupport (= 5.2.2.1)
+ actionview (5.2.3)
+ activesupport (= 5.2.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@@ -51,25 +51,25 @@ GEM
activemodel (>= 4.1, < 6)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
- activejob (5.2.2.1)
- activesupport (= 5.2.2.1)
+ activejob (5.2.3)
+ activesupport (= 5.2.3)
globalid (>= 0.3.6)
- activemodel (5.2.2.1)
- activesupport (= 5.2.2.1)
- activerecord (5.2.2.1)
- activemodel (= 5.2.2.1)
- activesupport (= 5.2.2.1)
+ activemodel (5.2.3)
+ activesupport (= 5.2.3)
+ activerecord (5.2.3)
+ activemodel (= 5.2.3)
+ activesupport (= 5.2.3)
arel (>= 9.0)
- activestorage (5.2.2.1)
- actionpack (= 5.2.2.1)
- activerecord (= 5.2.2.1)
+ activestorage (5.2.3)
+ actionpack (= 5.2.3)
+ activerecord (= 5.2.3)
marcel (~> 0.3.1)
- activestorage-openstack (1.0.0)
+ activestorage-openstack (1.2.0)
fog-openstack (~> 1.0)
marcel
mime-types
- rails (<= 6)
- activesupport (5.2.2.1)
+ rails (>= 5.2.2)
+ activesupport (5.2.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@@ -136,11 +136,6 @@ GEM
capybara-selenium (0.0.6)
capybara
selenium-webdriver
- carrierwave (1.3.1)
- activemodel (>= 4.0.0)
- activesupport (>= 4.0.0)
- mime-types (>= 1.16)
- carrierwave-i18n (0.2.0)
case_transform (0.2)
activesupport
chartkick (3.2.0)
@@ -158,11 +153,9 @@ GEM
coffee-script-source (1.12.2)
concurrent-ruby (1.1.5)
connection_pool (2.2.2)
- copy_carrierwave_file (1.3.0)
- carrierwave (>= 0.9)
crack (0.4.3)
safe_yaml (~> 1.0.0)
- crass (1.0.4)
+ crass (1.0.5)
css_parser (1.6.0)
addressable
curb (0.9.10)
@@ -203,7 +196,7 @@ GEM
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
- erubi (1.8.0)
+ erubi (1.9.0)
erubis (2.7.0)
ethon (0.11.0)
ffi (>= 1.3.0)
@@ -311,7 +304,7 @@ GEM
domain_name (~> 0.5)
http_parser.rb (0.6.0)
httpclient (2.8.3)
- i18n (1.6.0)
+ i18n (1.7.0)
concurrent-ruby (~> 1.0)
ipaddress (0.8.3)
jaro_winkler (1.5.2)
@@ -320,7 +313,7 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.2.0)
- json-jwt (1.10.0)
+ json-jwt (1.11.0)
activesupport (>= 4.2)
aes_key_wrap
bindata
@@ -356,7 +349,7 @@ GEM
railties (>= 4)
request_store (~> 1.0)
logstash-event (1.2.02)
- loofah (2.2.3)
+ loofah (2.3.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lumberjack (1.0.13)
@@ -375,7 +368,7 @@ GEM
mimemagic (0.3.3)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
- minitest (5.11.3)
+ minitest (5.13.0)
momentjs-rails (2.20.1)
railties (>= 3.1)
multi_json (1.14.1)
@@ -384,8 +377,8 @@ GEM
mustermann (1.0.3)
nenv (0.3.0)
netrc (0.11.0)
- nio4r (2.3.1)
- nokogiri (1.10.4)
+ nio4r (2.5.2)
+ nokogiri (1.10.5)
mini_portile2 (~> 2.4.0)
notiffany (0.1.1)
nenv (~> 0.1)
@@ -469,18 +462,18 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
- rails (5.2.2.1)
- actioncable (= 5.2.2.1)
- actionmailer (= 5.2.2.1)
- actionpack (= 5.2.2.1)
- actionview (= 5.2.2.1)
- activejob (= 5.2.2.1)
- activemodel (= 5.2.2.1)
- activerecord (= 5.2.2.1)
- activestorage (= 5.2.2.1)
- activesupport (= 5.2.2.1)
+ rails (5.2.3)
+ actioncable (= 5.2.3)
+ actionmailer (= 5.2.3)
+ actionpack (= 5.2.3)
+ actionview (= 5.2.3)
+ activejob (= 5.2.3)
+ activemodel (= 5.2.3)
+ activerecord (= 5.2.3)
+ activestorage (= 5.2.3)
+ activesupport (= 5.2.3)
bundler (>= 1.3.0)
- railties (= 5.2.2.1)
+ railties (= 5.2.3)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4)
actionpack (>= 5.0.1.x)
@@ -489,14 +482,14 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
- rails-html-sanitizer (1.2.0)
- loofah (~> 2.2, >= 2.2.2)
+ rails-html-sanitizer (1.3.0)
+ loofah (~> 2.3)
rails-i18n (5.1.2)
i18n (>= 0.7, < 2)
railties (>= 5.0, < 6)
- railties (5.2.2.1)
- actionpack (= 5.2.2.1)
- activesupport (= 5.2.2.1)
+ railties (5.2.3)
+ actionpack (= 5.2.3)
+ activesupport (= 5.2.3)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
@@ -678,6 +671,10 @@ GEM
activemodel (>= 5.0)
bindex (>= 0.4.0)
railties (>= 5.0)
+ webdrivers (4.1.3)
+ nokogiri (~> 1.6)
+ rubyzip (>= 1.3.0)
+ selenium-webdriver (>= 3.0, < 4.0)
webfinger (1.1.0)
activesupport
httpclient (>= 2.4)
@@ -689,9 +686,9 @@ GEM
activesupport (>= 4.2)
rack-proxy (>= 0.6.1)
railties (>= 4.2)
- websocket-driver (0.7.0)
+ websocket-driver (0.7.1)
websocket-extensions (>= 0.1.0)
- websocket-extensions (0.1.3)
+ websocket-extensions (0.1.4)
xpath (3.2.0)
nokogiri (~> 1.8)
xray-rails (0.3.1)
@@ -726,12 +723,9 @@ DEPENDENCIES
capybara-email
capybara-screenshot
capybara-selenium
- carrierwave
- carrierwave-i18n
chartkick
chunky_png
clamav-client
- copy_carrierwave_file
daemons
database_cleaner
deep_cloneable
@@ -812,6 +806,7 @@ DEPENDENCIES
vcr
warden
web-console
+ webdrivers (~> 4.0)
webmock
webpacker
xray-rails
diff --git a/README.md b/README.md
index 97683c010..faf159341 100644
--- a/README.md
+++ b/README.md
@@ -43,8 +43,14 @@ Les informations nécessaire à l'initialisation de la base doivent être pré-c
> create user tps_test with password 'tps_test' superuser;
> \q
+
### Initialisation de l'environnement de développement
+Sous Ubuntu, certains packages doivent être installés au préalable :
+
+ sudo apt-get install libcurl3 libcurl3-gnutls libcurl4-openssl-dev libcurl4-gnutls-dev zlib1g-dev
+
+
Afin d'initialiser l'environnement de développement, exécutez la commande suivante :
bin/setup
diff --git a/app/assets/stylesheets/new_design/_placeholders.scss b/app/assets/stylesheets/new_design/_placeholders.scss
index 53e98918e..2dc8cbaad 100644
--- a/app/assets/stylesheets/new_design/_placeholders.scss
+++ b/app/assets/stylesheets/new_design/_placeholders.scss
@@ -1,3 +1,4 @@
+@import "colors";
@import "constants";
%horizontal-list {
@@ -17,3 +18,10 @@
animation-fill-mode: forwards;
animation-duration: 0.3s;
}
+
+%outline {
+ &:active,
+ &:focus {
+ outline: 3px solid $blue;
+ }
+}
diff --git a/app/assets/stylesheets/new_design/buttons.scss b/app/assets/stylesheets/new_design/buttons.scss
index a36b7a719..e66e2c9fa 100644
--- a/app/assets/stylesheets/new_design/buttons.scss
+++ b/app/assets/stylesheets/new_design/buttons.scss
@@ -1,7 +1,10 @@
@import "colors";
@import "constants";
+@import "placeholders";
.button {
+ @extend %outline;
+
display: inline-block;
padding: 8px 16px;
border-radius: 30px;
@@ -20,11 +23,6 @@
text-decoration: none;
}
- &:active,
- &:focus {
- outline: none;
- }
-
&:disabled {
opacity: 0.5;
filter: saturate(50%);
diff --git a/app/assets/stylesheets/new_design/custom_reset.scss b/app/assets/stylesheets/new_design/custom_reset.scss
index 054f1c141..f1825a12e 100644
--- a/app/assets/stylesheets/new_design/custom_reset.scss
+++ b/app/assets/stylesheets/new_design/custom_reset.scss
@@ -1,3 +1,6 @@
+@import "colors";
+@import "placeholders";
+
html,
body {
height: 100%;
@@ -14,5 +17,7 @@ html {
}
a {
+ @extend %outline;
+
text-decoration: none;
}
diff --git a/app/assets/stylesheets/new_design/forms.scss b/app/assets/stylesheets/new_design/forms.scss
index f12f5681a..8136b53e9 100644
--- a/app/assets/stylesheets/new_design/forms.scss
+++ b/app/assets/stylesheets/new_design/forms.scss
@@ -1,5 +1,6 @@
@import "constants";
@import "colors";
+@import "placeholders";
.form {
h1 {
@@ -177,6 +178,8 @@
input[type=checkbox],
input[type=radio] {
+ @extend %outline;
+
margin-left: 5px;
margin-right: 4px;
margin-bottom: 2 * $default-padding;
diff --git a/app/assets/stylesheets/new_design/help_dropdown.scss b/app/assets/stylesheets/new_design/help_dropdown.scss
index 01d8bd70c..d0f0d66d4 100644
--- a/app/assets/stylesheets/new_design/help_dropdown.scss
+++ b/app/assets/stylesheets/new_design/help_dropdown.scss
@@ -11,7 +11,7 @@
}
}
-h4.help-dropdown-title {
+.help-dropdown-title {
font-size: 16px;
color: $blue;
}
diff --git a/app/assets/stylesheets/new_design/landing.scss b/app/assets/stylesheets/new_design/landing.scss
index f6bec1754..1b18fea58 100644
--- a/app/assets/stylesheets/new_design/landing.scss
+++ b/app/assets/stylesheets/new_design/landing.scss
@@ -365,6 +365,11 @@ $cta-panel-button-border-size: 2px;
color: #FFFFFF;
text-decoration: none;
}
+
+ &:active,
+ &:focus {
+ outline: 3px solid #FFFFFF;
+ }
}
.cta-panel-button-blue {
diff --git a/app/assets/stylesheets/new_design/new_footer.scss b/app/assets/stylesheets/new_design/new_footer.scss
index 092ee442b..7694d8d34 100644
--- a/app/assets/stylesheets/new_design/new_footer.scss
+++ b/app/assets/stylesheets/new_design/new_footer.scss
@@ -113,3 +113,19 @@ footer {
margin-bottom: 0;
}
}
+
+.footer-site-links {
+ li {
+ display: inline;
+
+
+ &::before {
+ content: "-";
+ margin: $default-spacer;
+ }
+
+ &:first-child::before {
+ content: none;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/new_design/title.scss b/app/assets/stylesheets/new_design/title.scss
new file mode 100644
index 000000000..d02120e2f
--- /dev/null
+++ b/app/assets/stylesheets/new_design/title.scss
@@ -0,0 +1,12 @@
+@import "constants";
+
+.huge-title {
+ text-align: center;
+ margin-bottom: 20px;
+ font-size: 35px;
+ font-weight: bold;
+
+ @media (max-width: $two-columns-breakpoint) {
+ font-size: 25px;
+ }
+}
diff --git a/app/controllers/admin/assigns_controller.rb b/app/controllers/admin/assigns_controller.rb
index 5a59330c4..cf2899643 100644
--- a/app/controllers/admin/assigns_controller.rb
+++ b/app/controllers/admin/assigns_controller.rb
@@ -17,8 +17,9 @@ class Admin::AssignsController < AdminController
not_assign_scope = current_administrateur.instructeurs.where.not(id: assign_scope.ids)
- if params[:filter]
- not_assign_scope = not_assign_scope.where("email LIKE ?", "%#{params[:filter]}%")
+ if params[:filter].present?
+ filter = params[:filter].downcase.strip
+ not_assign_scope = not_assign_scope.where('users.email LIKE ?', "%#{filter}%")
end
@instructeurs_not_assign = smart_listing_create :instructeurs_not_assign,
diff --git a/app/controllers/api/v1/dossiers_controller.rb b/app/controllers/api/v1/dossiers_controller.rb
index 980ba7e81..222e786dd 100644
--- a/app/controllers/api/v1/dossiers_controller.rb
+++ b/app/controllers/api/v1/dossiers_controller.rb
@@ -2,6 +2,7 @@ class API::V1::DossiersController < APIController
before_action :fetch_procedure_and_check_token
DEFAULT_PAGE_SIZE = 100
+ MAX_PAGE_SIZE = 1000
ORDER_DIRECTIONS = { 'asc' => :asc, 'desc' => :desc }
def index
@@ -33,7 +34,12 @@ class API::V1::DossiersController < APIController
end
def per_page # inherited value from will_paginate
- [params[:resultats_par_page]&.to_i || DEFAULT_PAGE_SIZE, 1000].min
+ resultats_par_page = params[:resultats_par_page]&.to_i
+ if resultats_par_page && resultats_par_page > 0
+ [resultats_par_page, MAX_PAGE_SIZE].min
+ else
+ DEFAULT_PAGE_SIZE
+ end
end
def fetch_procedure_and_check_token
@@ -47,7 +53,7 @@ class API::V1::DossiersController < APIController
end
order = ORDER_DIRECTIONS.fetch(params[:order], :asc)
- @dossiers = @procedure.dossiers.state_not_brouillon.order_for_api(order)
+ @dossiers = @procedure.dossiers.state_not_brouillon.order_by_created_at(order)
rescue ActiveRecord::RecordNotFound
render json: {}, status: :not_found
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 03c8bcd57..1ee6f1411 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -250,7 +250,7 @@ class ApplicationController < ActionController::Base
payload: {
DS_SIGN_IN_COUNT: current_user&.sign_in_count,
DS_CREATED_AT: current_administrateur&.created_at,
- DS_ACTIVE: current_administrateur&.active?,
+ DS_ACTIVE: current_user&.active?,
DS_ID: current_administrateur&.id,
DS_GESTIONNAIRE_ID: current_instructeur&.id,
DS_ROLES: current_user_roles
diff --git a/app/controllers/instructeurs/groupe_instructeurs_controller.rb b/app/controllers/instructeurs/groupe_instructeurs_controller.rb
index 0e5a3b7e0..e55d7bb79 100644
--- a/app/controllers/instructeurs/groupe_instructeurs_controller.rb
+++ b/app/controllers/instructeurs/groupe_instructeurs_controller.rb
@@ -14,7 +14,7 @@ module Instructeurs
end
def add_instructeur
- @instructeur = Instructeur.find_by(email: instructeur_email) ||
+ @instructeur = Instructeur.by_email(instructeur_email) ||
create_instructeur(instructeur_email)
if groupe_instructeur.instructeurs.include?(@instructeur)
diff --git a/app/controllers/new_administrateur/groupe_instructeurs_controller.rb b/app/controllers/new_administrateur/groupe_instructeurs_controller.rb
index b8923d4a3..4bacdb64d 100644
--- a/app/controllers/new_administrateur/groupe_instructeurs_controller.rb
+++ b/app/controllers/new_administrateur/groupe_instructeurs_controller.rb
@@ -47,7 +47,7 @@ module NewAdministrateur
end
def add_instructeur
- @instructeur = Instructeur.find_by(email: instructeur_email) ||
+ @instructeur = Instructeur.by_email(instructeur_email) ||
create_instructeur(instructeur_email)
if groupe_instructeur.instructeurs.include?(@instructeur)
diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb
index 23cb10410..8c0ccad0f 100644
--- a/app/controllers/users/dossiers_controller.rb
+++ b/app/controllers/users/dossiers_controller.rb
@@ -48,8 +48,11 @@ module Users
end
def attestation
- if dossier.attestation.pdf.attached?
+ if dossier.attestation&.pdf&.attached?
redirect_to url_for(dossier.attestation.pdf)
+ else
+ flash.notice = "L'attestation n'est plus disponible sur ce dossier."
+ redirect_to dossier_path(dossier)
end
end
diff --git a/app/dashboards/service_dashboard.rb b/app/dashboards/service_dashboard.rb
index 5e3ea65fe..36134116a 100644
--- a/app/dashboards/service_dashboard.rb
+++ b/app/dashboards/service_dashboard.rb
@@ -18,8 +18,7 @@ class ServiceDashboard < Administrate::BaseDashboard
email: Field::String,
telephone: Field::String,
horaires: Field::String,
- adresse: Field::String,
- siret: Field::String
+ adresse: Field::String
}.freeze
# COLLECTION_ATTRIBUTES
@@ -45,8 +44,7 @@ class ServiceDashboard < Administrate::BaseDashboard
:email,
:telephone,
:horaires,
- :adresse,
- :siret
+ :adresse
].freeze
# FORM_ATTRIBUTES
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
index 84d59a361..c543a75f6 100644
--- a/app/graphql/mutations/base_mutation.rb
+++ b/app/graphql/mutations/base_mutation.rb
@@ -1,4 +1,4 @@
module Mutations
- class BaseMutation < GraphQL::Schema::Mutation
+ class BaseMutation < GraphQL::Schema::RelayClassicMutation
end
end
diff --git a/app/graphql/mutations/create_direct_upload.rb b/app/graphql/mutations/create_direct_upload.rb
new file mode 100644
index 000000000..d4c3a90e0
--- /dev/null
+++ b/app/graphql/mutations/create_direct_upload.rb
@@ -0,0 +1,41 @@
+module Mutations
+ class CreateDirectUpload < Mutations::BaseMutation
+ description "File information required to prepare a direct upload"
+
+ argument :dossier_id, ID, "Dossier ID", required: true, loads: Types::DossierType
+ argument :filename, String, "Original file name", required: true
+ argument :byte_size, Int, "File size (bytes)", required: true
+ argument :checksum, String, "MD5 file checksum as base64", required: true
+ argument :content_type, String, "File content type", required: true
+
+ class DirectUpload < Types::BaseObject
+ description "Represents direct upload credentials"
+
+ field :url, String, "Upload URL", null: false
+ field :headers, String, "HTTP request headers (JSON-encoded)", null: false
+ field :blob_id, ID, "Created blob record ID", null: false
+ field :signed_blob_id, ID, "Created blob record signed ID", null: false
+ end
+
+ field :direct_upload, DirectUpload, null: false
+
+ def resolve(filename:, byte_size:, checksum:, content_type:, dossier:)
+ blob = ActiveStorage::Blob.create_before_direct_upload!(
+ filename: filename,
+ byte_size: byte_size,
+ checksum: checksum,
+ content_type: content_type
+ )
+
+ {
+ direct_upload: {
+ url: blob.service_url_for_direct_upload,
+ # NOTE: we pass headers as JSON since they have no schema
+ headers: blob.service_headers_for_direct_upload.to_json,
+ blob_id: blob.id,
+ signed_blob_id: blob.signed_id
+ }
+ }
+ end
+ end
+end
diff --git a/app/graphql/mutations/dossier_envoyer_message.rb b/app/graphql/mutations/dossier_envoyer_message.rb
new file mode 100644
index 000000000..601e448de
--- /dev/null
+++ b/app/graphql/mutations/dossier_envoyer_message.rb
@@ -0,0 +1,27 @@
+module Mutations
+ class DossierEnvoyerMessage < Mutations::BaseMutation
+ description "Envoyer un message à l'usager du dossier."
+
+ argument :dossier_id, ID, required: true, loads: Types::DossierType
+ argument :instructeur_id, ID, required: true, loads: Types::ProfileType
+ argument :body, String, required: true
+ argument :attachment, ID, required: false
+
+ field :message, Types::MessageType, null: true
+ field :errors, [Types::ValidationErrorType], null: true
+
+ def resolve(dossier:, instructeur:, body:, attachment: nil)
+ message = CommentaireService.build(instructeur, dossier, body: body, piece_jointe: attachment)
+
+ if message.save
+ { message: message }
+ else
+ { errors: message.errors.full_messages }
+ end
+ end
+
+ def authorized?(dossier:, instructeur:, body:)
+ instructeur.is_a?(Instructeur) && instructeur.dossiers.exists?(id: dossier.id)
+ end
+ end
+end
diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql
index 6b6c5e355..b60fc03ae 100644
--- a/app/graphql/schema.graphql
+++ b/app/graphql/schema.graphql
@@ -10,27 +10,66 @@ type Avis {
type CarteChamp implements Champ {
geoAreas: [GeoArea!]!
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
}
interface Champ {
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
}
type ChampDescriptor {
+ """
+ Description du champ.
+ """
description: String
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ Est-ce que le champ est obligatoire ?
+ """
required: Boolean!
+
+ """
+ Type de la valeur du champ.
+ """
type: TypeDeChamp!
}
type CheckboxChamp implements Champ {
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
value: Boolean!
}
@@ -40,16 +79,78 @@ GeoJSON coordinates
"""
scalar Coordinates
+"""
+Autogenerated input type of CreateDirectUpload
+"""
+input CreateDirectUploadInput {
+ """
+ File size (bytes)
+ """
+ byteSize: Int!
+
+ """
+ MD5 file checksum as base64
+ """
+ checksum: String!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ File content type
+ """
+ contentType: String!
+
+ """
+ Dossier ID
+ """
+ dossierId: ID!
+
+ """
+ Original file name
+ """
+ filename: String!
+}
+
+"""
+Autogenerated return type of CreateDirectUpload
+"""
+type CreateDirectUploadPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+ directUpload: DirectUpload!
+}
+
type DateChamp implements Champ {
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
value: ISO8601DateTime
}
type DecimalNumberChamp implements Champ {
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
value: Float
}
@@ -82,25 +183,35 @@ type Demarche {
"""
before: String
+ """
+ Dossiers déposés depuis la date.
+ """
+ createdSince: ISO8601DateTime
+
"""
Returns the first _n_ elements from the list.
"""
first: Int
- """
- Filtrer les dossiers par ID.
- """
- ids: [ID!]
-
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
- Dossiers crées depuis la date.
+ L'ordre des dossiers.
"""
- since: ISO8601DateTime
+ order: Order = ASC
+
+ """
+ Dossiers avec statut.
+ """
+ state: DossierState
+
+ """
+ Dossiers mis à jour depuis la date.
+ """
+ updatedSince: ISO8601DateTime
): DossierConnection!
groupeInstructeurs: [GroupeInstructeur!]!
id: ID!
@@ -108,7 +219,11 @@ type Demarche {
"""
Le numero de la démarche.
"""
- number: ID!
+ number: Int!
+
+ """
+ L'état de la démarche.
+ """
state: DemarcheState!
title: String!
updatedAt: ISO8601DateTime!
@@ -131,6 +246,31 @@ enum DemarcheState {
publiee
}
+"""
+Represents direct upload credentials
+"""
+type DirectUpload {
+ """
+ Created blob record ID
+ """
+ blobId: ID!
+
+ """
+ HTTP request headers (JSON-encoded)
+ """
+ headers: String!
+
+ """
+ Created blob record signed ID
+ """
+ signedBlobId: ID!
+
+ """
+ Upload URL
+ """
+ url: String!
+}
+
"""
Un dossier
"""
@@ -163,7 +303,7 @@ type Dossier {
"""
Le numero du dossier.
"""
- number: ID!
+ number: Int!
"""
L'état du dossier.
@@ -212,10 +352,45 @@ type DossierEdge {
node: Dossier
}
+"""
+Autogenerated input type of DossierEnvoyerMessage
+"""
+input DossierEnvoyerMessageInput {
+ attachment: ID
+ body: String!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+ dossierId: ID!
+ instructeurId: ID!
+}
+
+"""
+Autogenerated return type of DossierEnvoyerMessage
+"""
+type DossierEnvoyerMessagePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+ errors: [ValidationError!]
+ message: Message
+}
+
type DossierLinkChamp implements Champ {
dossier: Dossier
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
}
@@ -254,22 +429,17 @@ interface GeoArea {
enum GeoAreaSource {
"""
- translation missing: fr.activerecord.attributes.geo_area.source.cadastre
+ Parcelle cadastrale
"""
cadastre
"""
- translation missing: fr.activerecord.attributes.geo_area.source.parcelle_agricole
- """
- parcelle_agricole
-
- """
- translation missing: fr.activerecord.attributes.geo_area.source.quartier_prioritaire
+ Quartier prioritaire
"""
quartier_prioritaire
"""
- translation missing: fr.activerecord.attributes.geo_area.source.selection_utilisateur
+ Sélection utilisateur
"""
selection_utilisateur
}
@@ -295,16 +465,32 @@ scalar ISO8601DateTime
type IntegerNumberChamp implements Champ {
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
value: Int
}
type LinkedDropDownListChamp implements Champ {
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
primaryValue: String
secondaryValue: String
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
}
@@ -318,12 +504,41 @@ type Message {
type MultipleDropDownListChamp implements Champ {
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
values: [String!]!
}
type Mutation {
+ """
+ File information required to prepare a direct upload
+ """
+ createDirectUpload(input: CreateDirectUploadInput!): CreateDirectUploadPayload
+
+ """
+ Envoyer un message à l'usager du dossier.
+ """
+ dossierEnvoyerMessage(input: DossierEnvoyerMessageInput!): DossierEnvoyerMessagePayload
+}
+
+enum Order {
+ """
+ L‘ordre ascendant.
+ """
+ ASC
+
+ """
+ L‘ordre descendant.
+ """
+ DESC
}
"""
@@ -383,7 +598,15 @@ type PersonneMorale {
type PieceJustificativeChamp implements Champ {
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
url: URL
}
@@ -410,7 +633,7 @@ type Query {
"""
Numéro de la démarche.
"""
- number: ID!
+ number: Int!
): Demarche!
"""
@@ -420,14 +643,22 @@ type Query {
"""
Numéro du dossier.
"""
- number: ID!
+ number: Int!
): Dossier!
}
type RepetitionChamp implements Champ {
champs: [Champ!]!
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
}
@@ -440,13 +671,29 @@ type SelectionUtilisateur implements GeoArea {
type SiretChamp implements Champ {
etablissement: PersonneMorale
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
}
type TextChamp implements Champ {
id: ID!
+
+ """
+ Libellé du champ.
+ """
label: String!
+
+ """
+ La valeur du champ sous forme texte.
+ """
stringValue: String
value: String
}
@@ -591,4 +838,14 @@ enum TypeDeChamp {
"""
A valid URL, transported as a string
"""
-scalar URL
\ No newline at end of file
+scalar URL
+
+"""
+Éreur de validation
+"""
+type ValidationError {
+ """
+ A description of the error
+ """
+ message: String!
+}
\ No newline at end of file
diff --git a/app/graphql/types/champ_descriptor_type.rb b/app/graphql/types/champ_descriptor_type.rb
index 2a9a18db1..ade23605e 100644
--- a/app/graphql/types/champ_descriptor_type.rb
+++ b/app/graphql/types/champ_descriptor_type.rb
@@ -9,9 +9,9 @@ module Types
end
global_id_field :id
- field :type, TypeDeChampType, null: false, method: :type_champ
- field :label, String, null: false, method: :libelle
- field :description, String, null: true
- field :required, Boolean, null: false, method: :mandatory?
+ field :type, TypeDeChampType, "Type de la valeur du champ.", null: false, method: :type_champ
+ field :label, String, "Libellé du champ.", null: false, method: :libelle
+ field :description, String, "Description du champ.", null: true
+ field :required, Boolean, "Est-ce que le champ est obligatoire ?", null: false, method: :mandatory?
end
end
diff --git a/app/graphql/types/champ_type.rb b/app/graphql/types/champ_type.rb
index dc76378d9..8383e409f 100644
--- a/app/graphql/types/champ_type.rb
+++ b/app/graphql/types/champ_type.rb
@@ -3,8 +3,8 @@ module Types
include Types::BaseInterface
global_id_field :id
- field :label, String, null: false, method: :libelle
- field :string_value, String, null: true, method: :for_api_v2
+ field :label, String, "Libellé du champ.", null: false, method: :libelle
+ field :string_value, String, "La valeur du champ sous forme texte.", null: true, method: :for_api_v2
definition_methods do
def resolve_type(object, context)
diff --git a/app/graphql/types/demarche_type.rb b/app/graphql/types/demarche_type.rb
index 37782aafe..4b1ed3129 100644
--- a/app/graphql/types/demarche_type.rb
+++ b/app/graphql/types/demarche_type.rb
@@ -9,10 +9,10 @@ module Types
description "Une demarche"
global_id_field :id
- field :number, ID, "Le numero de la démarche.", null: false, method: :id
+ field :number, Int, "Le numero de la démarche.", null: false, method: :id
field :title, String, null: false, method: :libelle
field :description, String, "Déscription de la démarche.", null: false
- field :state, DemarcheState, null: false
+ field :state, DemarcheState, "L'état de la démarche.", null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
@@ -21,8 +21,10 @@ module Types
field :groupe_instructeurs, [Types::GroupeInstructeurType], null: false
field :dossiers, Types::DossierType.connection_type, "Liste de tous les dossiers d'une démarche.", null: false do
- argument :ids, [ID], required: false, description: "Filtrer les dossiers par ID."
- argument :since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers crées depuis la date."
+ argument :order, Types::Order, default_value: :asc, required: false, description: "L'ordre des dossiers."
+ argument :created_since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers déposés depuis la date."
+ argument :updated_since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers mis à jour depuis la date."
+ argument :state, Types::DossierType::DossierState, required: false, description: "Dossiers avec statut."
end
field :champ_descriptors, [Types::ChampDescriptorType], null: false, method: :types_de_champ
@@ -36,15 +38,21 @@ module Types
Loaders::Association.for(object.class, groupe_instructeurs: { procedure: [:administrateurs] }).load(object)
end
- def dossiers(ids: nil, since: nil)
- dossiers = object.dossiers.for_api_v2
+ def dossiers(updated_since: nil, created_since: nil, state: nil, order:)
+ dossiers = object.dossiers.state_not_brouillon.for_api_v2
- if ids.present?
- dossiers = dossiers.where(id: ids)
+ if state.present?
+ dossiers = dossiers.where(state: state)
end
- if since.present?
- dossiers = dossiers.since(since)
+ if updated_since.present?
+ dossiers = dossiers.updated_since(updated_since).order_by_updated_at(order)
+ else
+ if created_since.present?
+ dossiers = dossiers.created_since(created_since)
+ end
+
+ dossiers = dossiers.order_by_created_at(order)
end
dossiers
diff --git a/app/graphql/types/dossier_type.rb b/app/graphql/types/dossier_type.rb
index 30892ca55..eecdc8816 100644
--- a/app/graphql/types/dossier_type.rb
+++ b/app/graphql/types/dossier_type.rb
@@ -9,7 +9,7 @@ module Types
description "Un dossier"
global_id_field :id
- field :number, ID, "Le numero du dossier.", null: false, method: :id
+ field :number, Int, "Le numero du dossier.", null: false, method: :id
field :state, DossierState, "L'état du dossier.", null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, "Date de dernière mise à jour.", null: false
diff --git a/app/graphql/types/geo_area_type.rb b/app/graphql/types/geo_area_type.rb
index 1d55b2fac..c54c054c2 100644
--- a/app/graphql/types/geo_area_type.rb
+++ b/app/graphql/types/geo_area_type.rb
@@ -4,9 +4,11 @@ module Types
class GeoAreaSource < Types::BaseEnum
GeoArea.sources.each do |symbol_name, string_name|
- value(string_name,
- I18n.t(symbol_name, scope: [:activerecord, :attributes, :geo_area, :source]),
- value: symbol_name)
+ if string_name != "parcelle_agricole"
+ value(string_name,
+ I18n.t(symbol_name, scope: [:activerecord, :attributes, :geo_area, :source]),
+ value: symbol_name)
+ end
end
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 113861978..92da07a80 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -1,4 +1,7 @@
module Types
class MutationType < Types::BaseObject
+ field :create_direct_upload, mutation: Mutations::CreateDirectUpload
+
+ field :dossier_envoyer_message, mutation: Mutations::DossierEnvoyerMessage
end
end
diff --git a/app/graphql/types/order.rb b/app/graphql/types/order.rb
new file mode 100644
index 000000000..885e80c22
--- /dev/null
+++ b/app/graphql/types/order.rb
@@ -0,0 +1,6 @@
+module Types
+ class Order < Types::BaseEnum
+ value('ASC', 'L‘ordre ascendant.', value: :asc)
+ value('DESC', 'L‘ordre descendant.', value: :desc)
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 9d29ef0f8..ed1114e53 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -1,11 +1,11 @@
module Types
class QueryType < Types::BaseObject
field :demarche, DemarcheType, null: false, description: "Informations concernant une démarche." do
- argument :number, ID, "Numéro de la démarche.", required: true
+ argument :number, Int, "Numéro de la démarche.", required: true
end
field :dossier, DossierType, null: false, description: "Informations sur un dossier d'une démarche." do
- argument :number, ID, "Numéro du dossier.", required: true
+ argument :number, Int, "Numéro du dossier.", required: true
end
def demarche(number:)
diff --git a/app/graphql/types/validation_error_type.rb b/app/graphql/types/validation_error_type.rb
new file mode 100644
index 000000000..7df3a6fb4
--- /dev/null
+++ b/app/graphql/types/validation_error_type.rb
@@ -0,0 +1,11 @@
+module Types
+ class ValidationErrorType < Types::BaseObject
+ description "Éreur de validation"
+
+ field :message, String, "A description of the error", null: false
+
+ def message
+ object
+ end
+ end
+end
diff --git a/app/helpers/string_to_html_helper.rb b/app/helpers/string_to_html_helper.rb
index 76a595699..1bdb188f0 100644
--- a/app/helpers/string_to_html_helper.rb
+++ b/app/helpers/string_to_html_helper.rb
@@ -1,6 +1,6 @@
module StringToHtmlHelper
- def string_to_html(str)
- html_formatted = simple_format(str)
+ def string_to_html(str, wrapper_tag = 'p')
+ html_formatted = simple_format(str, {}, { wrapper_tag: wrapper_tag })
with_links = html_formatted.gsub(URI.regexp, '\0')
sanitize(with_links, attributes: ['target', 'rel', 'href'])
end
diff --git a/app/javascript/components/TypesDeChampEditor/typeDeChampsReducer.js b/app/javascript/components/TypesDeChampEditor/typeDeChampsReducer.js
index 5d27c8399..81e603082 100644
--- a/app/javascript/components/TypesDeChampEditor/typeDeChampsReducer.js
+++ b/app/javascript/components/TypesDeChampEditor/typeDeChampsReducer.js
@@ -56,7 +56,9 @@ function addTypeDeChamp(state, typeDeChamps, insertAfter, done) {
state.flash.success();
done();
if (insertAfter) {
- scrollToComponent(insertAfter.target.nextElementSibling);
+ scrollToComponent(insertAfter.target.nextElementSibling, {
+ duration: 300
+ });
}
})
.catch(message => state.flash.error(message));
@@ -219,7 +221,7 @@ function getUpdateHandler(typeDeChamp, { queue, flash }) {
}
function findItemToInsertAfter() {
- const target = getFirstTarget();
+ const target = getLastVisibleTypeDeChamp();
return {
target,
@@ -227,8 +229,10 @@ function findItemToInsertAfter() {
};
}
-function getFirstTarget() {
- const [target] = document.querySelectorAll('[data-in-view]');
+function getLastVisibleTypeDeChamp() {
+ const typeDeChamps = document.querySelectorAll('[data-in-view]');
+ const target = typeDeChamps[typeDeChamps.length - 1];
+
if (target) {
const parentTarget = target.closest('[data-repetition]');
if (parentTarget) {
diff --git a/app/lib/api_carto/api.rb b/app/lib/api_carto/api.rb
index f94c11e2d..eadb813f8 100644
--- a/app/lib/api_carto/api.rb
+++ b/app/lib/api_carto/api.rb
@@ -12,11 +12,14 @@ class ApiCarto::API
private
def self.call(url, geojson)
- params = geojson.to_s
- RestClient.post(url, params, content_type: 'application/json')
+ response = Typhoeus.post(url, body: geojson.to_s, headers: { 'content-type' => 'application/json' })
- rescue RestClient::InternalServerError, RestClient::BadGateway, RestClient::GatewayTimeout, RestClient::ServiceUnavailable => e
- Rails.logger.error "[ApiCarto] Error on #{url}: #{e}"
- raise RestClient::ResourceNotFound
+ if response.success?
+ response.body
+ else
+ message = response.code == 0 ? response.return_message : response.code.to_s
+ Rails.logger.error "[ApiCarto] Error on #{url}: #{message}"
+ raise RestClient::ResourceNotFound
+ end
end
end
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 3504f1a7a..b444548f4 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -3,6 +3,11 @@ class ApplicationMailer < ActionMailer::Base
default from: "demarches-simplifiees.fr <#{CONTACT_EMAIL}>"
layout 'mailer'
+ # Don’t retry to send a message if the server rejects the recipient address
+ rescue_from Net::SMTPSyntaxError do |_error|
+ message.perform_deliveries = false
+ end
+
# Attach the procedure logo to the email (if any).
# Returns the attachment url.
def attach_logo(procedure)
diff --git a/app/models/administrateur.rb b/app/models/administrateur.rb
index a874fbe27..d097ebd74 100644
--- a/app/models/administrateur.rb
+++ b/app/models/administrateur.rb
@@ -46,7 +46,7 @@ class Administrateur < ApplicationRecord
end
def registration_state
- if active?
+ if user.active?
'Actif'
elsif user.reset_password_period_valid?
'En attente'
@@ -56,17 +56,7 @@ class Administrateur < ApplicationRecord
end
def invitation_expired?
- !active? && !user.reset_password_period_valid?
- end
-
- def self.reset_password(reset_password_token, password)
- administrateur = self.reset_password_by_token({
- password: password,
- password_confirmation: password,
- reset_password_token: reset_password_token
- })
-
- administrateur
+ !user.active? && !user.reset_password_period_valid?
end
def owns?(procedure)
@@ -80,8 +70,4 @@ class Administrateur < ApplicationRecord
def can_be_deleted?
dossiers.state_instruction_commencee.none? && procedures.none?
end
-
- def active?
- user.last_sign_in_at.present?
- end
end
diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb
index 9296dbdca..7d31812cd 100644
--- a/app/models/champs/repetition_champ.rb
+++ b/app/models/champs/repetition_champ.rb
@@ -33,6 +33,12 @@ class Champs::RepetitionChamp < Champ
end
end
+ # We have to truncate the label here as spreadsheets have a (30 char) limit on length.
+ def libelle_for_export
+ str = "(#{type_de_champ.stable_id}) #{libelle}"
+ ActiveStorage::Filename.new(str).sanitized.truncate(30)
+ end
+
class Row < Hashie::Dash
property :index
property :dossier_id
diff --git a/app/models/commentaire.rb b/app/models/commentaire.rb
index 5f7c4f11c..9a93319d4 100644
--- a/app/models/commentaire.rb
+++ b/app/models/commentaire.rb
@@ -10,7 +10,7 @@ class Commentaire < ApplicationRecord
has_one_attached :piece_jointe
- validates :body, presence: { message: "Votre message ne peut être vide" }
+ validates :body, presence: { message: "ne peut être vide" }
default_scope { order(created_at: :asc) }
scope :updated_since?, -> (date) { where('commentaires.updated_at > ?', date) }
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index d7d25ad7f..0b2bc94a1 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -105,7 +105,9 @@ class Dossier < ApplicationRecord
scope :not_archived, -> { where(archived: false) }
scope :order_by_updated_at, -> (order = :desc) { order(updated_at: order) }
- scope :order_for_api, -> (order = :asc) { order(en_construction_at: order, created_at: order, id: order) }
+ scope :order_by_created_at, -> (order = :asc) { order(en_construction_at: order, created_at: order, id: order) }
+ scope :updated_since, -> (since) { where('dossiers.updated_at >= ?', since) }
+ scope :created_since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
scope :all_state, -> { not_archived.state_not_brouillon }
scope :en_construction, -> { not_archived.state_en_construction }
@@ -134,7 +136,6 @@ class Dossier < ApplicationRecord
scope :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) }
scope :with_champs, -> { includes(champs: :type_de_champ) }
scope :nearing_end_of_retention, -> (duration = '1 month') { joins(:procedure).where("en_instruction_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - now() < interval ?", duration) }
- scope :since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
scope :for_api, -> {
includes(commentaires: { piece_jointe_attachment: :blob },
champs: [
@@ -472,7 +473,19 @@ class Dossier < ApplicationRecord
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end
- def spreadsheet_columns
+ def spreadsheet_columns_csv
+ spreadsheet_columns(with_etablissement: true)
+ end
+
+ def spreadsheet_columns_xlsx
+ spreadsheet_columns
+ end
+
+ def spreadsheet_columns_ods
+ spreadsheet_columns
+ end
+
+ def spreadsheet_columns(with_etablissement: false)
columns = [
['ID', id.to_s],
['Email', user.email]
@@ -485,6 +498,39 @@ class Dossier < ApplicationRecord
['Prénom', individual&.prenom],
['Date de naissance', individual&.birthdate]
]
+ elsif with_etablissement
+ columns += [
+ ['Établissement SIRET', etablissement&.siret],
+ ['Établissement siège social', etablissement&.siege_social],
+ ['Établissement NAF', etablissement&.naf],
+ ['Établissement libellé NAF', etablissement&.libelle_naf],
+ ['Établissement Adresse', etablissement&.adresse],
+ ['Établissement numero voie', etablissement&.numero_voie],
+ ['Établissement type voie', etablissement&.type_voie],
+ ['Établissement nom voie', etablissement&.nom_voie],
+ ['Établissement complément adresse', etablissement&.complement_adresse],
+ ['Établissement code postal', etablissement&.code_postal],
+ ['Établissement localité', etablissement&.localite],
+ ['Établissement code INSEE localité', etablissement&.code_insee_localite],
+ ['Entreprise SIREN', etablissement&.entreprise_siren],
+ ['Entreprise capital social', etablissement&.entreprise_capital_social],
+ ['Entreprise numero TVA intracommunautaire', etablissement&.entreprise_numero_tva_intracommunautaire],
+ ['Entreprise forme juridique', etablissement&.entreprise_forme_juridique],
+ ['Entreprise forme juridique code', etablissement&.entreprise_forme_juridique_code],
+ ['Entreprise nom commercial', etablissement&.entreprise_nom_commercial],
+ ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale],
+ ['Entreprise SIRET siège social', etablissement&.entreprise_siret_siege_social],
+ ['Entreprise code effectif entreprise', etablissement&.entreprise_code_effectif_entreprise],
+ ['Entreprise date de création', etablissement&.entreprise_date_creation],
+ ['Entreprise nom', etablissement&.entreprise_nom],
+ ['Entreprise prénom', etablissement&.entreprise_prenom],
+ ['Association RNA', etablissement&.association_rna],
+ ['Association titre', etablissement&.association_titre],
+ ['Association objet', etablissement&.association_objet],
+ ['Association date de création', etablissement&.association_date_creation],
+ ['Association date de déclaration', etablissement&.association_date_declaration],
+ ['Association date de publication', etablissement&.association_date_publication]
+ ]
else
columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
end
diff --git a/app/models/dynamic_smtp_settings_interceptor.rb b/app/models/dynamic_smtp_settings_interceptor.rb
new file mode 100644
index 000000000..885c4b8e9
--- /dev/null
+++ b/app/models/dynamic_smtp_settings_interceptor.rb
@@ -0,0 +1,16 @@
+class DynamicSmtpSettingsInterceptor
+ def self.delivering_email(message)
+ if ENV['SENDINBLUE_BALANCING'] == 'enabled'
+ if rand(0..99) < ENV['SENDINBLUE_BALANCING_VALUE'].to_i
+ message.delivery_method.settings = {
+ user_name: ENV['SENDINBLUE_USER_NAME'],
+ password: ENV['SENDINBLUE_SMTP_KEY'],
+ address: 'smtp-relay.sendinblue.com',
+ domain: 'smtp-relay.sendinblue.com',
+ port: '587',
+ authentication: :cram_md5
+ }
+ end
+ end
+ end
+end
diff --git a/app/models/individual.rb b/app/models/individual.rb
index 32d2e0445..41a6f3963 100644
--- a/app/models/individual.rb
+++ b/app/models/individual.rb
@@ -10,7 +10,7 @@ class Individual < ApplicationRecord
GENDER_FEMALE = 'Mme'
def self.create_from_france_connect(fc_information)
- create(
+ create!(
nom: fc_information.family_name,
prenom: fc_information.given_name,
gender: fc_information.gender == 'female' ? GENDER_FEMALE : GENDER_MALE
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index 6a6d3ecd6..6abe348c7 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -444,7 +444,7 @@ class Procedure < ApplicationRecord
version = options.delete(:version)
if version == 'v2'
options.delete(:tables)
- ProcedureExportV2Service.new(self, dossiers, **options.to_h.symbolize_keys)
+ ProcedureExportV2Service.new(self, dossiers)
else
ProcedureExportService.new(self, dossiers, **options.to_h.symbolize_keys)
end
@@ -595,14 +595,18 @@ class Procedure < ApplicationRecord
def move_type_de_champ_attributes(types_de_champ, type_de_champ, new_index)
old_index = types_de_champ.index(type_de_champ)
- types_de_champ.insert(new_index, types_de_champ.delete_at(old_index))
- .map.with_index do |type_de_champ, index|
- {
- id: type_de_champ.id,
- libelle: type_de_champ.libelle,
- order_place: index
- }
- end
+ if types_de_champ.delete_at(old_index)
+ types_de_champ.insert(new_index, type_de_champ)
+ .map.with_index do |type_de_champ, index|
+ {
+ id: type_de_champ.id,
+ libelle: type_de_champ.libelle,
+ order_place: index
+ }
+ end
+ else
+ []
+ end
end
def before_publish
diff --git a/app/models/types_de_champ/linked_drop_down_list_type_de_champ.rb b/app/models/types_de_champ/linked_drop_down_list_type_de_champ.rb
index 71f0784b1..24410c6f6 100644
--- a/app/models/types_de_champ/linked_drop_down_list_type_de_champ.rb
+++ b/app/models/types_de_champ/linked_drop_down_list_type_de_champ.rb
@@ -53,7 +53,7 @@ class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBas
def check_presence_of_primary_options
if !PRIMARY_PATTERN.match?(drop_down_list.options.second)
- errors.add(libelle, "doit commencer par une entrée de menu primaire de la forme --texte--
")
+ errors.add(libelle.presence || "La liste", "doit commencer par une entrée de menu primaire de la forme --texte--
")
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 126c7adec..41a4984b3 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -49,7 +49,7 @@ class User < ApplicationRecord
def invite_administrateur!(administration_id)
reset_password_token = nil
- if !administrateur.active?
+ if !active?
reset_password_token = set_reset_password_token
end
@@ -92,6 +92,10 @@ class User < ApplicationRecord
"User:#{id}"
end
+ def active?
+ last_sign_in_at.present?
+ end
+
private
def link_invites!
diff --git a/app/services/administrateur_usage_statistics_service.rb b/app/services/administrateur_usage_statistics_service.rb
index b9b38f868..5c85f2a96 100644
--- a/app/services/administrateur_usage_statistics_service.rb
+++ b/app/services/administrateur_usage_statistics_service.rb
@@ -29,7 +29,7 @@ class AdministrateurUsageStatisticsService
result = {
ds_sign_in_count: administrateur.user.sign_in_count,
ds_created_at: administrateur.created_at,
- ds_active: administrateur.active?,
+ ds_active: administrateur.user.active?,
ds_id: administrateur.id,
nb_services: nb_services_by_administrateur_id[administrateur.id],
nb_instructeurs: nb_instructeurs_by_administrateur_id[administrateur.id],
diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb
index f2a192b1b..6d0b1386c 100644
--- a/app/services/procedure_export_service.rb
+++ b/app/services/procedure_export_service.rb
@@ -49,7 +49,7 @@ class ProcedureExportService
:prenom
]
- def initialize(procedure, dossiers, tables: [], ids: nil, since: nil, limit: nil)
+ def initialize(procedure, dossiers, tables: [])
@procedure = procedure
@attributes = ATTRIBUTES.dup
@@ -59,15 +59,6 @@ class ProcedureExportService
end
@dossiers = dossiers.downloadable_sorted
- if ids
- @dossiers = @dossiers.where(id: ids)
- end
- if since
- @dossiers = @dossiers.since(since)
- end
- if limit
- @dossiers = @dossiers.limit(limit)
- end
@dossiers = @dossiers.to_a
@tables = tables.map(&:to_sym)
end
diff --git a/app/services/procedure_export_v2_service.rb b/app/services/procedure_export_v2_service.rb
index 67409b723..eae682c1e 100644
--- a/app/services/procedure_export_v2_service.rb
+++ b/app/services/procedure_export_v2_service.rb
@@ -1,36 +1,27 @@
class ProcedureExportV2Service
attr_reader :dossiers
- def initialize(procedure, dossiers, ids: nil, since: nil, limit: nil)
+ def initialize(procedure, dossiers)
@procedure = procedure
@dossiers = dossiers.downloadable_sorted
- if ids
- @dossiers = @dossiers.where(id: ids)
- end
- if since
- @dossiers = @dossiers.since(since)
- end
- if limit
- @dossiers = @dossiers.limit(limit)
- end
@tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
end
- def to_csv(table = :dossiers)
- SpreadsheetArchitect.to_csv(options_for(table))
+ def to_csv
+ SpreadsheetArchitect.to_csv(options_for(:dossiers, :csv))
end
def to_xlsx
# We recursively build multi page spreadsheet
@tables.reduce(nil) do |package, table|
- SpreadsheetArchitect.to_axlsx_package(options_for(table), package)
+ SpreadsheetArchitect.to_axlsx_package(options_for(table, :xlsx), package)
end.to_stream.read
end
def to_ods
# We recursively build multi page spreadsheet
@tables.reduce(nil) do |spreadsheet, table|
- SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table), spreadsheet)
+ SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table, :ods), spreadsheet)
end.bytes
end
@@ -53,7 +44,7 @@ class ProcedureExportV2Service
[dossier.champs, dossier.champs_private]
.flatten
.filter { |champ| champ.is_a?(Champs::RepetitionChamp) }
- end.group_by(&:libelle)
+ end.group_by(&:libelle_for_export)
end
def champs_repetables_options
@@ -70,21 +61,16 @@ class ProcedureExportV2Service
row_style: { background_color: nil, color: "000000", font_size: 12 }
}
- def sanitize_sheet_name(name)
- ActiveStorage::Filename.new(name.to_s).sanitized.truncate(30)
- end
-
- def options_for(table)
+ def options_for(table, format)
case table
when :dossiers
- { instances: dossiers.to_a, sheet_name: 'Dossiers' }.merge(DEFAULT_STYLES)
+ { instances: dossiers.to_a, sheet_name: 'Dossiers', spreadsheet_columns: :"spreadsheet_columns_#{format}" }.merge(DEFAULT_STYLES)
when :etablissements
{ instances: etablissements.to_a, sheet_name: 'Etablissements' }.merge(DEFAULT_STYLES)
when :avis
{ instances: avis.to_a, sheet_name: 'Avis' }.merge(DEFAULT_STYLES)
when Array
- # We have to truncate the label here as spreadsheets have a (30 char) limit on length.
- { instances: table.last, sheet_name: sanitize_sheet_name(table.first) }.merge(DEFAULT_STYLES)
+ { instances: table.last, sheet_name: table.first }.merge(DEFAULT_STYLES)
end
end
end
diff --git a/app/views/champs/repetition/_show.html.haml b/app/views/champs/repetition/_show.html.haml
index 508593712..8b3240829 100644
--- a/app/views/champs/repetition/_show.html.haml
+++ b/app/views/champs/repetition/_show.html.haml
@@ -1,10 +1,11 @@
- champs = champ.rows.last
-- index = (champ.rows.size - 1) * champs.size
-%div{ class: "row row-#{champs.first.row}" }
- - champs.each.with_index(index) do |champ, index|
- = fields_for "#{attribute}[#{index}]", champ do |form|
- = render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form }
- = form.hidden_field :id
- = form.hidden_field :_destroy, disabled: true
- %button.button.danger.remove-row
- Supprimer
+- if champs.present?
+ - index = (champ.rows.size - 1) * champs.size
+ %div{ class: "row row-#{champs.first.row}" }
+ - champs.each.with_index(index) do |champ, index|
+ = fields_for "#{attribute}[#{index}]", champ do |form|
+ = render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form }
+ = form.hidden_field :id
+ = form.hidden_field :_destroy, disabled: true
+ %button.button.danger.remove-row
+ Supprimer
diff --git a/app/views/commencer/show.html.haml b/app/views/commencer/show.html.haml
index ee3564f92..661b2b4d2 100644
--- a/app/views/commencer/show.html.haml
+++ b/app/views/commencer/show.html.haml
@@ -2,7 +2,7 @@
.commencer.form
- if !user_signed_in?
- %h1 Commencer la démarche
+ %h2.huge-title Commencer la démarche
= link_to commencer_sign_up_path(path: @procedure.path), class: ['button large expand primary'] do
Créer un compte
%span.optional-on-small-screens
@@ -20,7 +20,7 @@
- elsif drafts.count == 1 && not_drafts.count == 0
- dossier = drafts.first
- %h1 Vous avez déjà commencé à remplir un dossier
+ %h2.huge-title Vous avez déjà commencé à remplir un dossier
%p
Il y a #{time_ago_in_words(dossier.created_at)},
vous avez commencé à remplir un dossier sur la démarche « #{dossier.procedure.libelle} ».
@@ -29,7 +29,7 @@
- elsif not_drafts.count == 1
- dossier = not_drafts.first
- %h1 Vous avez déjà déposé un dossier
+ %h2.huge-title Vous avez déjà déposé un dossier
%p
Il y a #{time_ago_in_words(dossier.en_construction_at)},
vous avez déposé un dossier sur la démarche « #{dossier.procedure.libelle} ».
@@ -37,6 +37,6 @@
= link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand']
- else
- %h1 Vous avez déjà des dossiers pour cette démarche
+ %h2.huge-title Vous avez déjà des dossiers pour cette démarche
= link_to 'Voir mes dossiers en cours', dossiers_path, class: ['button large expand primary']
= link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand']
diff --git a/app/views/layouts/_new_header.haml b/app/views/layouts/_new_header.haml
index 440a01b4a..7eae51026 100644
--- a/app/views/layouts/_new_header.haml
+++ b/app/views/layouts/_new_header.haml
@@ -3,7 +3,7 @@
- dossier = controller.try(:dossier_for_help)
- procedure = controller.try(:procedure_for_help)
-.new-header{ class: current_page?(root_path) ? nil : "new-header-with-border" }
+%header.new-header{ class: current_page?(root_path) ? nil : "new-header-with-border" }
.header-inner-content
.flex.align-center
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 7178d975f..8eb3e4004 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -33,8 +33,9 @@
Env Test
= render partial: "layouts/new_header"
- = render partial: "layouts/flash_messages"
- = content_for?(:content) ? yield(:content) : yield
+ %main
+ = render partial: "layouts/flash_messages"
+ = content_for?(:content) ? yield(:content) : yield
- if content_for?(:footer)
= content_for(:footer)
diff --git a/app/views/layouts/commencer/_no_procedure.html.haml b/app/views/layouts/commencer/_no_procedure.html.haml
index 34c054883..15a7c59a6 100644
--- a/app/views/layouts/commencer/_no_procedure.html.haml
+++ b/app/views/layouts/commencer/_no_procedure.html.haml
@@ -1,5 +1,5 @@
.no-procedure
- = image_tag "landing/hero/dematerialiser.svg", class: "paperless-logo"
+ = image_tag "landing/hero/dematerialiser.svg", class: "paperless-logo", alt: "moins de papier"
.baseline.center
%h3 Un outil simple
%p
diff --git a/app/views/layouts/left_panels/_left_panel_admin_procedurescontroller_navbar.html.haml b/app/views/layouts/left_panels/_left_panel_admin_procedurescontroller_navbar.html.haml
index 4f0284677..80a7e9d19 100644
--- a/app/views/layouts/left_panels/_left_panel_admin_procedurescontroller_navbar.html.haml
+++ b/app/views/layouts/left_panels/_left_panel_admin_procedurescontroller_navbar.html.haml
@@ -30,14 +30,14 @@
.procedure-list-element
Administrateurs
- - if !feature_enabled?(:routage)
+ - if !feature_enabled?(:administrateur_routage)
%a#onglet-instructeurs{ href: url_for(admin_procedure_assigns_path(@procedure)) }
.procedure-list-element{ class: ('active' if active == 'Instructeurs') }
Instructeurs
- if @procedure.missing_steps.include?(:instructeurs)
%p.missing-steps (à compléter)
- - if feature_enabled?(:routage)
+ - if feature_enabled?(:administrateur_routage)
%a#onglet-instructeurs{ href: url_for(procedure_groupe_instructeurs_path(@procedure)) }
.procedure-list-element
Groupe d'instructeurs
diff --git a/app/views/root/_footer.html.haml b/app/views/root/_footer.html.haml
index ac7462fec..7a49639b8 100644
--- a/app/views/root/_footer.html.haml
+++ b/app/views/root/_footer.html.haml
@@ -6,22 +6,22 @@
%ul.footer-logos
%li.footer-text
Un service fourni par la
- = link_to "DINSIC", "http://www.modernisation.gouv.fr/"
+ = link_to "DINUM", "http://www.modernisation.gouv.fr/", title: "Direction Interministérielle au Numérique"
%br
et incubé par
- = link_to "beta.gouv.fr", "https://beta.gouv.fr"
+ = link_to "beta.gouv.fr", "https://beta.gouv.fr", title: "le site de Beta.gouv.fr"
%li
- = link_to "http://www.modernisation.gouv.fr/" do
+ = link_to "http://www.modernisation.gouv.fr/", title: "DINUM" do
%span.footer-logo.footer-logo-dinsic{ role: 'img', 'aria-label': 'DINSIC' }
- = link_to "https://beta.gouv.fr" do
+ = link_to "https://beta.gouv.fr", title: "le site de Beta.gouv.fr" do
%span.footer-logo.footer-logo-beta-gouv-fr{ role: 'img', 'aria-label': 'beta.gouv.fr' }
%li.footer-column
%ul.footer-links
%li.footer-link
- = link_to "Newsletter", "https://my.sendinblue.com/users/subscribe/js_id/3s2q1/id/1", :class => "footer-link", :target => "_blank", rel: "noopener"
+ = link_to "Newsletter", "https://my.sendinblue.com/users/subscribe/js_id/3s2q1/id/1", :title => "Notre newsletter", :class => "footer-link", :target => "_blank", rel: "noopener"
%li.footer-link
- = link_to "Nouveautés", "https://github.com/betagouv/demarches-simplifiees.fr/releases", :class => "footer-link"
+ = link_to "Nouveautés", "https://github.com/betagouv/demarches-simplifiees.fr/releases", :class => "footer-link", :title => "Nos nouveautés"
%li.footer-link
= link_to "Statistiques", stats_path, :class => "footer-link", data: { turbolinks: false } # Turbolinks disabled for Chartkick. See Issue #350
%li.footer-link
diff --git a/app/views/root/landing.html.haml b/app/views/root/landing.html.haml
index 72bd6b341..3c72e717c 100644
--- a/app/views/root/landing.html.haml
+++ b/app/views/root/landing.html.haml
@@ -14,13 +14,13 @@
%em.hero-tagline-em en ligne
.hero-illustration
- %img{ :src => image_url("landing/hero/dematerialiser.svg"), alt: "" }
+ %img{ :src => image_url("landing/hero/dematerialiser.svg"), alt: "dématérialisez" }
.landing-panel.usagers-panel
.container
.role-panel-wrapper
.role-panel-30.role-usagers-image
- %img.role-image{ :src => image_url("landing/roles/usagers.svg"), alt: "" }
+ %img.role-image{ :src => image_url("landing/roles/usagers.svg"), alt: "usager" }
.role-panel-70
%h1.role-panel-title Vous souhaitez effectuer une demande auprès d'une administration ?
diff --git a/app/views/shared/_procedure_description.html.haml b/app/views/shared/_procedure_description.html.haml
index 52d7b9460..03713ae51 100644
--- a/app/views/shared/_procedure_description.html.haml
+++ b/app/views/shared/_procedure_description.html.haml
@@ -1,8 +1,8 @@
.procedure-logos
- = image_tag procedure.logo_url
+ = image_tag procedure.logo_url, alt: "logo #{procedure.libelle}"
- if procedure.euro_flag
= image_tag("flag_of_europe.svg", id: 'euro_flag', class: (!procedure.euro_flag ? "hidden" : ""))
-%h2.procedure-title
+%h1.procedure-title
= procedure.libelle
.procedure-description
.procedure-description-body.read-more-enabled.read-more-collapsed
diff --git a/app/views/shared/help/dropdown_items/_email_item.html.haml b/app/views/shared/help/dropdown_items/_email_item.html.haml
index e921003e5..d409ab875 100644
--- a/app/views/shared/help/dropdown_items/_email_item.html.haml
+++ b/app/views/shared/help/dropdown_items/_email_item.html.haml
@@ -2,5 +2,5 @@
= mail_to CONTACT_EMAIL do
%span.icon.mail
.dropdown-description
- %h4.help-dropdown-title Contact technique
+ %span.help-dropdown-title Contact technique
%p Envoyez nous un message à #{CONTACT_EMAIL}.
diff --git a/app/views/shared/help/dropdown_items/_faq_item.html.haml b/app/views/shared/help/dropdown_items/_faq_item.html.haml
index 86f82348b..fd48bb56d 100644
--- a/app/views/shared/help/dropdown_items/_faq_item.html.haml
+++ b/app/views/shared/help/dropdown_items/_faq_item.html.haml
@@ -2,5 +2,5 @@
= link_to FAQ_URL, target: "_blank", rel: "noopener" do
%span.icon.help
.dropdown-description
- %h4.help-dropdown-title Un problème avec le site ?
+ %span.help-dropdown-title Un problème avec le site ?
%p Trouvez votre réponse dans l’aide en ligne.
diff --git a/app/views/shared/help/dropdown_items/_messagerie_item.html.haml b/app/views/shared/help/dropdown_items/_messagerie_item.html.haml
index 2e25a78c8..94d31143d 100644
--- a/app/views/shared/help/dropdown_items/_messagerie_item.html.haml
+++ b/app/views/shared/help/dropdown_items/_messagerie_item.html.haml
@@ -2,5 +2,5 @@
= link_to messagerie_dossier_path(dossier) do
%span.icon.mail
.dropdown-description
- %h4.help-dropdown-title= title
+ %span.help-dropdown-title= title
%p Envoyez directement un message à l’instructeur.
diff --git a/app/views/shared/help/dropdown_items/_service_item.html.haml b/app/views/shared/help/dropdown_items/_service_item.html.haml
index af8624549..c57a0bada 100644
--- a/app/views/shared/help/dropdown_items/_service_item.html.haml
+++ b/app/views/shared/help/dropdown_items/_service_item.html.haml
@@ -1,7 +1,7 @@
%li.help-dropdown-service
%span.icon.person
.dropdown-description
- %h4.help-dropdown-title= title
+ %span.help-dropdown-title= title
.help-dropdown-service-action
%p Contactez directement l’administration :
%p.help-dropdown-service-item
diff --git a/app/views/users/_general_footer_row.html.haml b/app/views/users/_general_footer_row.html.haml
index 8d994718e..987c199e4 100644
--- a/app/views/users/_general_footer_row.html.haml
+++ b/app/views/users/_general_footer_row.html.haml
@@ -1,11 +1,7 @@
-= link_to "Accessibilité", accessibilite_path, :class => "footer-link"
-–
-= link_to "CGU", CGU_URL, :class => "footer-link", :target => "_blank", rel: "noopener noreferrer"
-–
-= link_to "Mentions légales", MENTIONS_LEGALES_URL, :class => "footer-link", :target => "_blank", rel: "noopener noreferrer"
-–
-= link_to 'Documentation', DOC_URL
-–
-= contact_link "Contact technique", class: "footer-link", dossier_id: dossier&.id
-–
-= link_to 'Aide', FAQ_URL
+%ul.footer-row.footer-bottom-line.footer-site-links
+ %li>= link_to "Accessibilité", accessibilite_path
+ %li>= link_to "CGU", CGU_URL, target: "_blank", rel: "noopener noreferrer"
+ %li>= link_to "Mentions légales", MENTIONS_LEGALES_URL, target: "_blank", rel: "noopener noreferrer"
+ %li>= link_to 'Documentation', DOC_URL
+ %li>= contact_link "Contact technique", dossier_id: dossier&.id
+ %li>= link_to 'Aide', FAQ_URL
diff --git a/app/views/users/_procedure_footer.html.haml b/app/views/users/_procedure_footer.html.haml
index 458c6ed00..1286ea584 100644
--- a/app/views/users/_procedure_footer.html.haml
+++ b/app/views/users/_procedure_footer.html.haml
@@ -2,19 +2,19 @@
.container
- service = procedure.service
- if service.present?
- %ul.footer-row.footer-columns
- %li.footer-column
- %h3.footer-header Cette démarche est gérée par :
- %p
+ .footer-row.footer-columns
+ %ul.footer-column
+ %p.footer-header Cette démarche est gérée par :
+ %li
= service.nom
%br
= service.organisme
%br
- = string_to_html(service.adresse)
+ = string_to_html(service.adresse, wrapper_tag = 'span')
- %li.footer-column
- %h3.footer-header Poser une question sur votre dossier :
- %p
+ %ul.footer-column
+ %p.footer-header Poser une question sur votre dossier :
+ %li
- if dossier.present? && dossier.messagerie_available?
Directement
= link_to "par la messagerie", messagerie_dossier_path(dossier)
@@ -22,21 +22,21 @@
Par email :
= link_to service.email, "mailto:#{service.email}"
- %p
+ %li
Par téléphone :
%a{ href: "tel:#{service.telephone}" }= service.telephone
- %p
+ %li
- horaires = "Horaires : #{formatted_horaires(service.horaires)}"
- = simple_format(horaires)
+ = simple_format(horaires, {}, wrapper_tag: 'span')
- politiques = politiques_conservation_de_donnees(procedure)
- if politiques.present?
- %li.footer-column
- %h3.footer-header Conservation des données :
+ %ul.footer-column
+ %p.footer-header Conservation des données :
- politiques.each do |politique|
- %p= politique
+ %li= politique
+
+ = render partial: 'users/general_footer_row', locals: { dossier: dossier }
- .footer-row.footer-bottom-line
- = render partial: 'users/general_footer_row', locals: { dossier: dossier }
diff --git a/app/views/users/dossiers/_index_footer.html.haml b/app/views/users/dossiers/_index_footer.html.haml
index 935c93bbc..16f9f1273 100644
--- a/app/views/users/dossiers/_index_footer.html.haml
+++ b/app/views/users/dossiers/_index_footer.html.haml
@@ -1,4 +1,3 @@
%footer.procedure-footer
.container
- .footer-row.footer-bottom-line
- = render partial: "users/general_footer_row", locals: { dossier: nil }
+ = render partial: "users/general_footer_row", locals: { dossier: nil }
diff --git a/app/views/users/sessions/new.html.haml b/app/views/users/sessions/new.html.haml
index e92e681e2..5cdd40be4 100644
--- a/app/views/users/sessions/new.html.haml
+++ b/app/views/users/sessions/new.html.haml
@@ -3,7 +3,7 @@
.auth-form.sign-in-form
= form_for User.new, url: user_session_path, html: { class: "form" } do |f|
- %h1 Connectez-vous
+ %h2.huge-title Connectez-vous
= f.label :email, "Email"
= f.text_field :email, autofocus: true
diff --git a/bin/setup b/bin/setup
index 3d592bcd5..34110e498 100755
--- a/bin/setup
+++ b/bin/setup
@@ -18,6 +18,8 @@ chdir APP_ROOT do
system('bundle check') || system!('bundle install')
system! 'bin/yarn install'
+ puts "\n== Updating webdrivers =="
+ system! 'RAILS_ENV=test bin/rails webdrivers:chromedriver:update'
puts "\n== Copying sample files =="
unless File.exist?('.env')
diff --git a/bin/update b/bin/update
index c1968201d..04eb642c5 100755
--- a/bin/update
+++ b/bin/update
@@ -18,6 +18,9 @@ chdir APP_ROOT do
system('bundle check') || system!('bundle install')
system! 'bin/yarn install'
+ puts "\n== Updating webdrivers =="
+ system! 'RAILS_ENV=test bin/rails webdrivers:chromedriver:update'
+
puts "\n== Updating database =="
system! 'bin/rails db:migrate'
diff --git a/config/env.example b/config/env.example
index 4f704adab..aa54c288a 100644
--- a/config/env.example
+++ b/config/env.example
@@ -23,7 +23,6 @@ FOG_OPENSTACK_IDENTITY_API_VERSION=""
FOG_OPENSTACK_REGION=""
FOG_DIRECTORY=""
FOG_ENABLED=""
-CARRIERWAVE_CACHE_DIR="/tmp/tps-local-cache"
DS_PROXY_URL=""
FC_PARTICULIER_ID=""
@@ -46,8 +45,14 @@ SENTRY_DSN_JS=""
MATOMO_ENABLED="disabled"
MATOMO_ID="73"
-SENDINBLUE_ENABLED="disabled"
+SENDINBLUE_BALANCING=""
+SENDINBLUE_BALANCING_VALUE=""
+SENDINBLUE_ENABLED=""
SENDINBLUE_CLIENT_KEY=""
+SENDINBLUE_SMTP_KEY=""
+SENDINBLUE_USER_NAME=""
+
+
CRISP_ENABLED="disabled"
CRISP_CLIENT_KEY=""
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 385185b36..718db3436 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -45,14 +45,26 @@ Rails.application.configure do
config.assets.raise_runtime_errors = true
# Action Mailer settings
- config.action_mailer.delivery_method = :letter_opener_web
- # Configure default root URL for generating URLs to routes
- config.action_mailer.default_url_options = {
- host: 'localhost',
- port: 3000
- }
- # Configure default root URL for email assets
- config.action_mailer.asset_host = "http://" + ENV['APP_HOST']
+
+ if ENV['SENDINBLUE_ENABLED'] == 'enabled'
+ config.action_mailer.delivery_method = :smtp
+ config.action_mailer.smtp_settings = {
+ user_name: Rails.application.secrets.sendinblue[:username],
+ password: Rails.application.secrets.sendinblue[:smtp_key],
+ address: 'smtp-relay.sendinblue.com',
+ domain: 'smtp-relay.sendinblue.com',
+ port: '587',
+ authentication: :cram_md5
+ }
+ else
+ config.action_mailer.delivery_method = :letter_opener_web
+ config.action_mailer.default_url_options = {
+ host: 'localhost',
+ port: 3000
+ }
+
+ config.action_mailer.asset_host = "http://" + ENV['APP_HOST']
+ end
Rails.application.routes.default_url_options = {
host: 'localhost',
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 8837fb88b..262c15c4b 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -77,6 +77,16 @@ Rails.application.configure do
port: '2525',
authentication: :cram_md5
}
+ elsif ENV['SENDINBLUE_ENABLED'] == 'enabled'
+ config.action_mailer.delivery_method = :smtp
+ config.action_mailer.smtp_settings = {
+ user_name: Rails.application.secrets.sendinblue[:username],
+ password: Rails.application.secrets.sendinblue[:smtp_key],
+ address: 'smtp-relay.sendinblue.com',
+ domain: 'smtp-relay.sendinblue.com',
+ port: '587',
+ authentication: :cram_md5
+ }
else
config.action_mailer.delivery_method = :mailjet
end
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index bb61fc264..960c3cd3d 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -1,9 +1,8 @@
Rails.application.config.content_security_policy do |policy|
- # En cas de non respect d'une des règles, faire un POST sur cette URL
- if Rails.env.production?
- policy.report_uri "https://demarchessimplifieestest.report-uri.com/r/d/csp/reportOnly"
- else
- policy.report_uri "http://#{ENV['APP_HOST']}/csp/" # ne pas notifier report-uri en dev/test
+ if Rails.env.development?
+ # les CSP ne sont pas appliquées en dev: on notifie cependant une url quelconque de la violation
+ # pour détecter les erreurs lors de l'ajout d'une nouvelle brique externe durant le développement
+ policy.report_uri "http://#{ENV['APP_HOST']}/csp/"
end
# Whitelist image
policy.img_src :self, "*.openstreetmap.org", "static.demarches-simplifiees.fr", "*.cloud.ovh.net", "stats.data.gouv.fr", "*", :data
diff --git a/config/initializers/dynamic_smtp_settings_interceptor.rb b/config/initializers/dynamic_smtp_settings_interceptor.rb
new file mode 100644
index 000000000..a3f8e2d13
--- /dev/null
+++ b/config/initializers/dynamic_smtp_settings_interceptor.rb
@@ -0,0 +1 @@
+ActionMailer::Base.register_interceptor "DynamicSmtpSettingsInterceptor"
diff --git a/config/initializers/graphiql.rb b/config/initializers/graphiql.rb
new file mode 100644
index 000000000..65a8eeb28
--- /dev/null
+++ b/config/initializers/graphiql.rb
@@ -0,0 +1,79 @@
+DEFAULT_QUERY = "# La documentation officielle de la spécification (Anglais) : https://graphql.org/
+# Une introduction aux concepts et raisons d'être de GraphQL (Français) : https://blog.octo.com/graphql-et-pourquoi-faire/
+# Le schema GraphQL de demarches-simplifiees.fr : https://demarches-simplifiees-graphql.netlify.com
+# Le endpoint GraphQL de demarches-simplifiees.fr : https://demarches-simplifiees.fr/api/v2/graphql
+
+query getDemarche($demarcheNumber: Int!) {
+ demarche(number: $demarcheNumber) {
+ id
+ number
+ title
+ champDescriptors {
+ id
+ type
+ label
+ }
+ dossiers(first: 3) {
+ nodes {
+ id
+ number
+ datePassageEnConstruction
+ datePassageEnInstruction
+ dateTraitement
+ usager {
+ email
+ }
+ champs {
+ id
+ label
+ ... on TextChamp {
+ value
+ }
+ ... on DecimalNumberChamp {
+ value
+ }
+ ... on IntegerNumberChamp {
+ value
+ }
+ ... on CheckboxChamp {
+ value
+ }
+ ... on DateChamp {
+ value
+ }
+ ... on DossierLinkChamp {
+ dossier {
+ id
+ }
+ }
+ ... on MultipleDropDownListChamp {
+ values
+ }
+ ... on LinkedDropDownListChamp {
+ primaryValue
+ secondaryValue
+ }
+ ... on PieceJustificativeChamp {
+ url
+ }
+ ... on CarteChamp {
+ geoAreas {
+ source
+ geometry {
+ type
+ coordinates
+ }
+ }
+ }
+ }
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+ }
+}"
+
+GraphiQL::Rails.config.initial_query = DEFAULT_QUERY
+GraphiQL::Rails.config.title = 'demarches-simplifiees.fr'
diff --git a/config/locales/models/commentaire/fr.yml b/config/locales/models/commentaire/fr.yml
index 16b77c012..71ed7e25e 100644
--- a/config/locales/models/commentaire/fr.yml
+++ b/config/locales/models/commentaire/fr.yml
@@ -2,4 +2,5 @@ fr:
activerecord:
attributes:
commentaire:
+ body: 'Votre message'
file: fichier
diff --git a/config/locales/models/geo_area/fr.yml b/config/locales/models/geo_area/fr.yml
new file mode 100644
index 000000000..913e4f0df
--- /dev/null
+++ b/config/locales/models/geo_area/fr.yml
@@ -0,0 +1,8 @@
+fr:
+ activerecord:
+ attributes:
+ geo_area:
+ source:
+ cadastre: Parcelle cadastrale
+ quartier_prioritaire: Quartier prioritaire
+ selection_utilisateur: Sélection utilisateur
diff --git a/config/secrets.yml b/config/secrets.yml
index 7ad675ffe..23c8e5267 100644
--- a/config/secrets.yml
+++ b/config/secrets.yml
@@ -42,8 +42,6 @@ defaults: &defaults
openstack_identity_api_version: "<%= ENV['FOG_OPENSTACK_IDENTITY_API_VERSION'] %>"
openstack_region: <%= ENV['FOG_OPENSTACK_REGION'] %>
directory: <%= ENV['FOG_DIRECTORY'] %>
- carrierwave:
- cache_dir: <%= ENV['CARRIERWAVE_CACHE_DIR'] %>
mailtrap:
username: <%= ENV['MAILTRAP_USERNAME'] %>
password: <%= ENV['MAILTRAP_PASSWORD'] %>
@@ -54,7 +52,9 @@ defaults: &defaults
webhook_secret: <%= ENV['HELPSCOUT_WEBHOOK_SECRET'] %>
sendinblue:
enabled: <%= ENV['SENDINBLUE_ENABLED'] == 'enabled' %>
+ username: <%= ENV['SENDINBLUE_USER_NAME'] %>
client_key: <%= ENV['SENDINBLUE_CLIENT_KEY'] %>
+ smtp_key: <%= ENV['SENDINBLUE_SMTP_KEY'] %>
api_v3_key: <%= ENV['SENDINBLUE_API_V3_KEY'] %>
matomo:
enabled: <%= ENV['MATOMO_ENABLED'] == 'enabled' %>
@@ -82,8 +82,6 @@ test:
key: api_entreprise_test_key
fog:
directory: tps_dev
- carrierwave:
- cache_dir: /tmp/tps-test-cache
pipedrive:
key: pipedrive_test_key
france_connect_particulier:
diff --git a/db/migrate/20191113142816_instructeurs_remove_email.rb b/db/migrate/20191113142816_instructeurs_remove_email.rb
new file mode 100644
index 000000000..ffe93e0be
--- /dev/null
+++ b/db/migrate/20191113142816_instructeurs_remove_email.rb
@@ -0,0 +1,5 @@
+class InstructeursRemoveEmail < ActiveRecord::Migration[5.2]
+ def change
+ remove_column :instructeurs, :email
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 2fb2e05ac..f19d1d49e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_10_24_150452) do
+ActiveRecord::Schema.define(version: 2019_11_13_142816) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -414,12 +414,10 @@ ActiveRecord::Schema.define(version: 2019_10_24_150452) do
end
create_table "instructeurs", id: :serial, force: :cascade do |t|
- t.string "email", default: "", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.text "encrypted_login_token"
t.datetime "login_token_created_at"
- t.index ["email"], name: "index_instructeurs_on_email"
end
create_table "invites", id: :serial, force: :cascade do |t|
diff --git a/lib/tasks/2017_10_30_copy_commentaire_piece_justificative_to_file.rake b/lib/tasks/2017_10_30_copy_commentaire_piece_justificative_to_file.rake
deleted file mode 100644
index 6a721ed00..000000000
--- a/lib/tasks/2017_10_30_copy_commentaire_piece_justificative_to_file.rake
+++ /dev/null
@@ -1,51 +0,0 @@
-require Rails.root.join("lib", "tasks", "task_helper")
-
-namespace :'2017_10_30_copy_commentaire_piece_justificative_to_file' do
- task set: :environment do
- commentaires_to_process = Commentaire.where(file: nil).where.not(piece_justificative_id: nil).reorder(id: :desc)
-
- rake_puts "#{commentaires_to_process.count} commentaires to process..."
-
- commentaires_to_process.each do |c|
- process_commentaire(c)
- end
- end
-
- task fix: :environment do
- commentaires_to_fix = Commentaire.where.not(file: nil).where.not(piece_justificative_id: nil).reorder(id: :desc)
-
- rake_puts "#{commentaires_to_fix.count} commentaires to fix..."
-
- commentaires_to_fix.each do |c|
- process_commentaire(c)
- end
- end
-
- def sanitize_name(name) # from https://github.com/carrierwaveuploader/carrierwave/blob/master/lib/carrierwave/sanitized_file.rb#L323
- name = name.gsub(/[^[:word:]\.\-\+]/, "_")
- name = "_#{name}" if name.match?(/\A\.+\z/)
- name = "unnamed" if name.empty?
- return name.mb_chars.to_s
- end
-
- def process_commentaire(commentaire)
- rake_puts "Processing commentaire #{commentaire.id}"
- if commentaire.piece_justificative.present?
- # https://github.com/carrierwaveuploader/carrierwave#uploading-files-from-a-remote-location
- commentaire.remote_file_url = commentaire.piece_justificative.content_url
-
- if commentaire.piece_justificative.original_filename.present?
- commentaire.file.define_singleton_method(:filename) { sanitize_name(commentaire.piece_justificative.original_filename) }
- end
-
- if commentaire.body.blank?
- commentaire.body = commentaire.piece_justificative.original_filename || "."
- end
-
- commentaire.save
- if commentaire.file.blank?
- rake_puts "Failed to save file for commentaire #{commentaire.id}"
- end
- end
- end
-end
diff --git a/lib/tasks/cloud_storage.rake b/lib/tasks/cloud_storage.rake
deleted file mode 100644
index 2a276ac55..000000000
--- a/lib/tasks/cloud_storage.rake
+++ /dev/null
@@ -1,113 +0,0 @@
-require Rails.root.join("lib", "tasks", "task_helper")
-
-namespace :cloudstorage do
- task init: :environment do
- os_config = (YAML.load_file(Fog.credentials_path))['default']
- @os = OpenStack::Connection.create(
- {
- username: os_config['openstack_username'],
- api_key: os_config['openstack_api_key'],
- auth_method: "password",
- auth_url: "https://auth.cloud.ovh.net/v2.0/",
- authtenant_name: os_config['openstack_tenant'],
- service_type: "object-store",
- region: os_config['openstack_region']
- }
- )
- @cont = @os.container(CarrierWave::Uploader::Base.fog_directory)
- end
-
- desc 'Move local attestations on cloud storage'
- task migrate: :environment do
- puts 'Starting migration'
-
- Rake::Task['cloudstorage:init'].invoke
-
- error_count = 0
- [Cerfa, PieceJustificative, Procedure].each do |c|
- c.all.each do |entry|
- content = (c == Procedure) ? entry.logo : entry.content
- if !(content.current_path.nil? || File.exist?(File.dirname(content.current_path) + '/uploaded'))
- secure_token = SecureRandom.uuid
- filename = "#{entry.class.to_s.underscore}-#{secure_token}#{File.extname(content.current_path)}"
- rake_puts "Uploading #{content.current_path}"
- begin
- @cont.create_object(filename, {}, File.open(content.current_path))
-
- File.open(File.dirname(content.current_path) + '/uploaded', "w+") { |f| f.write(File.basename(content.current_path)) }
- File.open(File.dirname(content.current_path) + '/filename_cloudstorage', "w+") { |f| f.write(filename) }
- File.open(File.dirname(content.current_path) + '/secure_token_cloudstorage', "w+") { |f| f.write(secure_token) }
-
- entry.update_column(c == Procedure ? :logo : :content, filename)
- entry.update_column(c == Procedure ? :logo_secure_token : :content_secure_token, secure_token)
- rescue Errno::ENOENT
- rake_puts "ERROR: #{content.current_path} does not exist!"
- File.open('upload_errors.report', "a+") { |f| f.write(content.current_path) }
- error_count += 1
- end
- else
- if content.current_path.present? && File.exist?(File.dirname(content.current_path) + '/uploaded')
- filename = File.open(File.dirname(content.current_path) + '/filename_cloudstorage', "r").read
- secure_token = File.open(File.dirname(content.current_path) + '/secure_token_cloudstorage', "r").read
-
- entry.update_column(c == Procedure ? :logo : :content, filename)
- entry.update_column(c == Procedure ? :logo_secure_token : :content_secure_token, secure_token)
-
- rake_puts "RESTORE IN DATABASE: #{filename} "
- elsif content.current_path.present?
- rake_puts "Skipping #{content.current_path}"
- end
- end
- end
- end
-
- rake_puts "There were #{error_count} errors while uploading files. See upload_errors.report file for details."
- puts 'Enf of migration'
- end
-
- desc 'Clear documents in tenant and revert file entries in database'
- task :revert do
- Rake::Task['cloudstorage:init'].invoke
-
- [Cerfa, PieceJustificative, Procedure].each do |c|
- c.all.each do |entry|
- content = (c == Procedure) ? entry.logo : entry.content
- if content.current_path.present?
- if File.exist?(File.dirname(content.current_path) + '/uploaded')
- previous_filename = File.read(File.dirname(content.current_path) + '/uploaded')
-
- entry.update_column(c == Procedure ? :logo : :content, previous_filename)
- entry.update_column(c == Procedure ? :logo_secure_token : :content_secure_token, nil)
-
- rake_puts "restoring #{content.current_path} db data to #{previous_filename}"
-
- @cont.delete_object(File.open(File.dirname(content.current_path) + '/filename_cloudstorage', "r").read)
-
- FileUtils.rm(File.dirname(content.current_path) + '/uploaded')
- FileUtils.rm(File.dirname(content.current_path) + '/filename_cloudstorage')
- FileUtils.rm(File.dirname(content.current_path) + '/secure_token_cloudstorage')
- end
- end
- end
- end
- end
-
- desc 'Clear old documents in tenant'
- task :clear do
- Rake::Task['cloudstorage:init'].invoke
-
- @cont.objects.each do |object|
- rake_puts "Removing #{object}"
- @cont.delete_object(object)
- end
- end
-
- task :clear_old_objects do
- Rake::Task['cloudstorage:init'].invoke
-
- @cont.objects_detail.each do |object, details|
- last_modified = Time.zone.parse(details[:last_modified])
- @cont.delete_object(object) if last_modified.utc <= (Time.zone.now - 2.years).utc
- end
- end
-end
diff --git a/spec/controllers/admin/assigns_controller_spec.rb b/spec/controllers/admin/assigns_controller_spec.rb
index 95fb4474a..741937dd8 100644
--- a/spec/controllers/admin/assigns_controller_spec.rb
+++ b/spec/controllers/admin/assigns_controller_spec.rb
@@ -2,19 +2,53 @@ require 'spec_helper'
describe Admin::AssignsController, type: :controller do
let(:admin) { create(:administrateur) }
- let(:procedure) { create :procedure, administrateur: admin }
- let(:instructeur) { create :instructeur, administrateurs: [admin] }
before do
sign_in(admin.user)
end
describe 'GET #show' do
- subject { get :show, params: { procedure_id: procedure.id } }
- it { expect(subject.status).to eq(200) }
+ let(:procedure) { create :procedure, administrateur: admin, instructeurs: [instructeur_assigned_1, instructeur_assigned_2] }
+ let!(:instructeur_assigned_1) { create :instructeur, email: 'instructeur_1@ministere_a.gouv.fr', administrateurs: [admin] }
+ let!(:instructeur_assigned_2) { create :instructeur, email: 'instructeur_2@ministere_b.gouv.fr', administrateurs: [admin] }
+ let!(:instructeur_not_assigned_1) { create :instructeur, email: 'instructeur_3@ministere_a.gouv.fr', administrateurs: [admin] }
+ let!(:instructeur_not_assigned_2) { create :instructeur, email: 'instructeur_4@ministere_b.gouv.fr', administrateurs: [admin] }
+ let(:filter) { nil }
+
+ subject! { get :show, params: { procedure_id: procedure.id, filter: filter } }
+
+ it { expect(response.status).to eq(200) }
+
+ it 'sets the assigned and not assigned instructeurs' do
+ expect(assigns(:instructeurs_assign)).to match_array([instructeur_assigned_1, instructeur_assigned_2])
+ expect(assigns(:instructeurs_not_assign)).to match_array([instructeur_not_assigned_1, instructeur_not_assigned_2])
+ end
+
+ context 'with a search filter' do
+ let(:filter) { '@ministere_a.gouv.fr' }
+
+ it 'filters the unassigned instructeurs' do
+ expect(assigns(:instructeurs_not_assign)).to match_array([instructeur_not_assigned_1])
+ end
+
+ it 'does not filter the assigned instructeurs' do
+ expect(assigns(:instructeurs_assign)).to match_array([instructeur_assigned_1, instructeur_assigned_2])
+ end
+
+ context 'when the filter has spaces or a mixed case' do
+ let(:filter) { ' @ministere_A.gouv.fr ' }
+
+ it 'trims spaces and ignores the case' do
+ expect(assigns(:instructeurs_not_assign)).to match_array([instructeur_not_assigned_1])
+ end
+ end
+ end
end
describe 'PUT #update' do
+ let(:procedure) { create :procedure, administrateur: admin }
+ let(:instructeur) { create :instructeur, administrateurs: [admin] }
+
subject { put :update, params: { instructeur_id: instructeur.id, procedure_id: procedure.id, to: 'assign' } }
it { expect(subject).to redirect_to admin_procedure_assigns_path(procedure_id: procedure.id) }
diff --git a/spec/controllers/api/v2/graphql_controller_spec.rb b/spec/controllers/api/v2/graphql_controller_spec.rb
index 384d85808..eb2721145 100644
--- a/spec/controllers/api/v2/graphql_controller_spec.rb
+++ b/spec/controllers/api/v2/graphql_controller_spec.rb
@@ -12,6 +12,15 @@ describe API::V2::GraphqlController do
create(:commentaire, dossier: dossier, email: 'test@test.com')
dossier
end
+ let(:dossier1) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: 1.day.ago) }
+ let(:dossier2) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: 3.days.ago) }
+ let!(:dossier_brouillon) { create(:dossier, procedure: procedure) }
+ let(:dossiers) { [dossier2, dossier1, dossier] }
+ let(:instructeur) { create(:instructeur, followed_dossiers: dossiers) }
+
+ before do
+ instructeur.assign_to_procedure(procedure)
+ end
let(:query) do
"{
@@ -62,31 +71,65 @@ describe API::V2::GraphqlController do
request.env['HTTP_AUTHORIZATION'] = authorization_header
end
- it "should return demarche" do
- expect(gql_errors).to eq(nil)
- expect(gql_data).to eq(demarche: {
- id: procedure.to_typed_id,
- number: procedure.id.to_s,
- title: procedure.libelle,
- description: procedure.description,
- state: 'brouillon',
- archivedAt: nil,
- createdAt: procedure.created_at.iso8601,
- updatedAt: procedure.updated_at.iso8601,
- groupeInstructeurs: [{ instructeurs: [], label: "défaut" }],
- champDescriptors: procedure.types_de_champ.map do |tdc|
- {
- id: tdc.to_typed_id,
- label: tdc.libelle,
- type: tdc.type_champ,
- description: tdc.description,
- required: tdc.mandatory?
+ context "demarche" do
+ it "should be returned" do
+ expect(gql_errors).to eq(nil)
+ expect(gql_data).to eq(demarche: {
+ id: procedure.to_typed_id,
+ number: procedure.id,
+ title: procedure.libelle,
+ description: procedure.description,
+ state: 'brouillon',
+ archivedAt: nil,
+ createdAt: procedure.created_at.iso8601,
+ updatedAt: procedure.updated_at.iso8601,
+ groupeInstructeurs: [
+ {
+ instructeurs: [{ email: instructeur.email }],
+ label: "défaut"
+ }
+ ],
+ champDescriptors: procedure.types_de_champ.map do |tdc|
+ {
+ id: tdc.to_typed_id,
+ label: tdc.libelle,
+ type: tdc.type_champ,
+ description: tdc.description,
+ required: tdc.mandatory?
+ }
+ end,
+ dossiers: {
+ nodes: dossiers.map { |dossier| { id: dossier.to_typed_id } }
}
- end,
- dossiers: {
- nodes: []
- }
- })
+ })
+ end
+
+ context "filter dossiers" do
+ let(:query) do
+ "{
+ demarche(number: #{procedure.id}) {
+ id
+ number
+ dossiers(createdSince: \"#{2.days.ago.iso8601}\") {
+ nodes {
+ id
+ }
+ }
+ }
+ }"
+ end
+
+ it "should be returned" do
+ expect(gql_errors).to eq(nil)
+ expect(gql_data).to eq(demarche: {
+ id: procedure.to_typed_id,
+ number: procedure.id,
+ dossiers: {
+ nodes: [{ id: dossier1.to_typed_id }, { id: dossier.to_typed_id }]
+ }
+ })
+ end
+ end
end
context "dossier" do
@@ -130,11 +173,11 @@ describe API::V2::GraphqlController do
}"
end
- it "should return dossier" do
+ it "should be returned" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossier: {
id: dossier.to_typed_id,
- number: dossier.id.to_s,
+ number: dossier.id,
state: 'en_construction',
updatedAt: dossier.updated_at.iso8601,
datePassageEnConstruction: dossier.en_construction_at.iso8601,
@@ -146,7 +189,12 @@ describe API::V2::GraphqlController do
id: dossier.user.to_typed_id,
email: dossier.user.email
},
- instructeurs: [],
+ instructeurs: [
+ {
+ id: instructeur.to_typed_id,
+ email: instructeur.email
+ }
+ ],
messages: dossier.commentaires.map do |commentaire|
{
body: commentaire.body,
@@ -166,6 +214,114 @@ describe API::V2::GraphqlController do
expect(gql_data[:dossier][:champs][0][:id]).to eq(dossier.champs[0].type_de_champ.to_typed_id)
end
end
+
+ context "mutations" do
+ describe 'dossierEnvoyerMessage' do
+ context 'success' do
+ let(:query) do
+ "mutation {
+ dossierEnvoyerMessage(input: {
+ dossierId: \"#{dossier.to_typed_id}\",
+ instructeurId: \"#{instructeur.to_typed_id}\",
+ body: \"Bonjour\"
+ }) {
+ message {
+ body
+ }
+ }
+ }"
+ end
+
+ it "should post a message" do
+ expect(gql_errors).to eq(nil)
+
+ expect(gql_data).to eq(dossierEnvoyerMessage: {
+ message: {
+ body: "Bonjour"
+ }
+ })
+ end
+ end
+
+ context 'schema error' do
+ let(:query) do
+ "mutation {
+ dossierEnvoyerMessage(input: {
+ dossierId: \"#{dossier.to_typed_id}\",
+ instructeurId: \"#{instructeur.to_typed_id}\"
+ }) {
+ message {
+ body
+ }
+ }
+ }"
+ end
+
+ it "should fail" do
+ expect(gql_data).to eq(nil)
+ expect(gql_errors).not_to eq(nil)
+ end
+ end
+
+ context 'validation error' do
+ let(:query) do
+ "mutation {
+ dossierEnvoyerMessage(input: {
+ dossierId: \"#{dossier.to_typed_id}\",
+ instructeurId: \"#{instructeur.to_typed_id}\",
+ body: \"\"
+ }) {
+ message {
+ body
+ }
+ errors {
+ message
+ }
+ }
+ }"
+ end
+
+ it "should fail" do
+ expect(gql_errors).to eq(nil)
+ expect(gql_data).to eq(dossierEnvoyerMessage: {
+ errors: [{ message: "Votre message ne peut être vide" }],
+ message: nil
+ })
+ end
+ end
+ end
+
+ context 'createDirectUpload' do
+ let(:query) do
+ "mutation {
+ createDirectUpload(input: {
+ dossierId: \"#{dossier.to_typed_id}\",
+ filename: \"hello.png\",
+ byteSize: 1234,
+ checksum: \"qwerty1234\",
+ contentType: \"image/png\"
+ }) {
+ directUpload {
+ url
+ headers
+ blobId
+ signedBlobId
+ }
+ }
+ }"
+ end
+
+ it "should initiate a direct upload" do
+ expect(gql_errors).to eq(nil)
+
+ data = gql_data[:createDirectUpload][:directUpload]
+ expect(data[:url]).not_to be_nil
+ expect(data[:headers]).not_to be_nil
+ expect(data[:blobId]).not_to be_nil
+ expect(data[:signedBlobId]).not_to be_nil
+ end
+ end
+ end
end
context "when not authenticated" do
@@ -182,5 +338,26 @@ describe API::V2::GraphqlController do
expect(gql_errors).not_to eq(nil)
end
end
+
+ context "mutation" do
+ let(:query) do
+ "mutation {
+ dossierEnvoyerMessage(input: {
+ dossierId: \"#{dossier.to_typed_id}\",
+ instructeurId: \"#{instructeur.to_typed_id}\",
+ body: \"Bonjour\"
+ }) {
+ message {
+ body
+ }
+ }
+ }"
+ end
+
+ it "should return error" do
+ expect(gql_data[:dossierEnvoyerMessage]).to eq(nil)
+ expect(gql_errors).not_to eq(nil)
+ end
+ end
end
end
diff --git a/spec/features/admin/admin_creation_spec.rb b/spec/features/admin/admin_creation_spec.rb
index 166983b64..de719edc9 100644
--- a/spec/features/admin/admin_creation_spec.rb
+++ b/spec/features/admin/admin_creation_spec.rb
@@ -12,7 +12,7 @@ feature 'As an administrateur', js: true do
end
scenario 'I can register' do
- expect(new_admin.reload.active?).to be(false)
+ expect(new_admin.reload.user.active?).to be(false)
confirmation_email = open_email(admin_email)
token_params = confirmation_email.body.match(/token=[^"]+/)
@@ -24,6 +24,6 @@ feature 'As an administrateur', js: true do
expect(page).to have_content 'Mot de passe enregistré'
- expect(new_admin.reload.active?).to be(true)
+ expect(new_admin.reload.user.active?).to be(true)
end
end
diff --git a/spec/features/routing/full_scenario_spec.rb b/spec/features/routing/full_scenario_spec.rb
index 02e338c1a..3eeb141dc 100644
--- a/spec/features/routing/full_scenario_spec.rb
+++ b/spec/features/routing/full_scenario_spec.rb
@@ -7,7 +7,7 @@ feature 'The routing' do
let(:scientifique_user) { create(:user, password: password) }
let(:litteraire_user) { create(:user, password: password) }
- before { Flipper.enable_actor(:routage, administrateur.user) }
+ before { Flipper.enable_actor(:administrateur_routage, administrateur.user) }
scenario 'works' do
login_as administrateur.user, scope: :user
diff --git a/spec/mailers/application_mailer_spec.rb b/spec/mailers/application_mailer_spec.rb
new file mode 100644
index 000000000..9cfa295f9
--- /dev/null
+++ b/spec/mailers/application_mailer_spec.rb
@@ -0,0 +1,20 @@
+RSpec.describe ApplicationMailer, type: :mailer do
+ describe 'dealing with invalid emails' do
+ let(:dossier) { create(:dossier, procedure: build(:simple_procedure)) }
+ subject { DossierMailer.notify_new_draft(dossier) }
+
+ describe 'invalid emails are not sent' do
+ before do
+ allow_any_instance_of(DossierMailer)
+ .to receive(:notify_new_draft)
+ .and_raise(Net::SMTPSyntaxError)
+ end
+
+ it { expect(subject.message).to be_an_instance_of(ActionMailer::Base::NullMail) }
+ end
+
+ describe 'valid emails are sent' do
+ it { expect(subject.message).not_to be_an_instance_of(ActionMailer::Base::NullMail) }
+ end
+ end
+end
diff --git a/spec/models/administrateur_spec.rb b/spec/models/administrateur_spec.rb
index e06c53630..4b9fef1f0 100644
--- a/spec/models/administrateur_spec.rb
+++ b/spec/models/administrateur_spec.rb
@@ -50,22 +50,4 @@ describe Administrateur, type: :model do
# it { expect(subject).to eq([]) }
# end
# end
-
- describe '#active?' do
- let!(:administrateur) { create(:administrateur) }
-
- subject { administrateur.active? }
-
- context 'when the user has never signed in' do
- before { administrateur.user.update(last_sign_in_at: nil) }
-
- it { is_expected.to be false }
- end
-
- context 'when the user has already signed in' do
- before { administrateur.user.update(last_sign_in_at: Time.zone.now) }
-
- it { is_expected.to be true }
- end
- end
end
diff --git a/spec/models/type_de_champ_shared_example.rb b/spec/models/type_de_champ_shared_example.rb
index d0b6e4ecf..a710b4ef0 100644
--- a/spec/models/type_de_champ_shared_example.rb
+++ b/spec/models/type_de_champ_shared_example.rb
@@ -148,4 +148,22 @@ shared_examples 'type_de_champ_spec' do
expect(cloned_procedure.types_de_champ.first.types_de_champ).not_to be_empty
end
end
+
+ describe "linked_drop_down_list" do
+ let(:type_de_champ) { create(:type_de_champ_linked_drop_down_list) }
+
+ it 'should validate without label' do
+ type_de_champ.drop_down_list_value = 'toto'
+ expect(type_de_champ.validate).to be_falsey
+ messages = type_de_champ.errors.full_messages
+ expect(messages.size).to eq(1)
+ expect(messages.first.starts_with?("#{type_de_champ.libelle} doit commencer par")).to be_truthy
+
+ type_de_champ.libelle = ''
+ expect(type_de_champ.validate).to be_falsey
+ messages = type_de_champ.errors.full_messages
+ expect(messages.size).to eq(2)
+ expect(messages.last.starts_with?("La liste doit commencer par")).to be_truthy
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 4ff839507..a24949fbc 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -191,4 +191,22 @@ describe User, type: :model do
it { expect(AdministrationMailer).to have_received(:invite_admin).with(user, nil, administration.id) }
end
end
+
+ describe '#active?' do
+ let!(:user) { create(:user) }
+
+ subject { user.active? }
+
+ context 'when the user has never signed in' do
+ before { user.update(last_sign_in_at: nil) }
+
+ it { is_expected.to be false }
+ end
+
+ context 'when the user has already signed in' do
+ before { user.update(last_sign_in_at: Time.zone.now) }
+
+ it { is_expected.to be true }
+ end
+ end
end
diff --git a/spec/services/procedure_export_v2_service_spec.rb b/spec/services/procedure_export_v2_service_spec.rb
index 5635c0380..d8c30ecd7 100644
--- a/spec/services/procedure_export_v2_service_spec.rb
+++ b/spec/services/procedure_export_v2_service_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+require 'csv'
describe ProcedureExportV2Service do
describe 'to_data' do
@@ -150,6 +151,91 @@ describe ProcedureExportV2Service do
]
end
+ context 'as csv' do
+ subject do
+ Tempfile.create do |f|
+ f << ProcedureExportV2Service.new(procedure, procedure.dossiers).to_csv
+ f.rewind
+ CSV.read(f.path)
+ end
+ end
+
+ let(:nominal_headers) do
+ [
+ "ID",
+ "Email",
+ "Établissement SIRET",
+ "Établissement siège social",
+ "Établissement NAF",
+ "Établissement libellé NAF",
+ "Établissement Adresse",
+ "Établissement numero voie",
+ "Établissement type voie",
+ "Établissement nom voie",
+ "Établissement complément adresse",
+ "Établissement code postal",
+ "Établissement localité",
+ "Établissement code INSEE localité",
+ "Entreprise SIREN",
+ "Entreprise capital social",
+ "Entreprise numero TVA intracommunautaire",
+ "Entreprise forme juridique",
+ "Entreprise forme juridique code",
+ "Entreprise nom commercial",
+ "Entreprise raison sociale",
+ "Entreprise SIRET siège social",
+ "Entreprise code effectif entreprise",
+ "Entreprise date de création",
+ "Entreprise nom",
+ "Entreprise prénom",
+ "Association RNA",
+ "Association titre",
+ "Association objet",
+ "Association date de création",
+ "Association date de déclaration",
+ "Association date de publication",
+ "Archivé",
+ "État du dossier",
+ "Dernière mise à jour le",
+ "Déposé le",
+ "Passé en instruction le",
+ "Traité le",
+ "Motivation de la décision",
+ "Instructeurs",
+ "textarea",
+ "date",
+ "datetime",
+ "number",
+ "decimal_number",
+ "integer_number",
+ "checkbox",
+ "civilite",
+ "email",
+ "phone",
+ "address",
+ "yes_no",
+ "simple_drop_down_list",
+ "multiple_drop_down_list",
+ "linked_drop_down_list",
+ "pays",
+ "regions",
+ "departements",
+ "engagement",
+ "dossier_link",
+ "piece_justificative",
+ "siret",
+ "carte",
+ "text"
+ ]
+ end
+
+ let(:dossiers_sheet_headers) { subject.first }
+
+ it 'should have headers' do
+ expect(dossiers_sheet_headers).to match(nominal_headers)
+ end
+ end
+
it 'should have headers' do
expect(dossiers_sheet.headers).to match(nominal_headers)
@@ -225,7 +311,7 @@ describe ProcedureExportV2Service do
let(:champ_repetition) { dossiers.first.champs.find { |champ| champ.type_champ == 'repetition' } }
it 'should have sheets' do
- expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle])
+ expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle_for_export])
end
it 'should have headers' do
@@ -247,7 +333,18 @@ describe ProcedureExportV2Service do
end
it 'should have valid sheet name' do
- expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', "A - B - C"])
+ expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', "(#{champ_repetition.type_de_champ.stable_id}) A - B - C"])
+ end
+ end
+
+ context 'with non unique labels' do
+ let(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :for_individual, procedure: procedure) }
+ let(:champ_repetition) { dossier.champs.find { |champ| champ.type_champ == 'repetition' } }
+ let(:type_de_champ_repetition) { create(:type_de_champ_repetition, procedure: procedure, libelle: champ_repetition.libelle) }
+ let!(:another_champ_repetition) { create(:champ_repetition, type_de_champ: type_de_champ_repetition, dossier: dossier) }
+
+ it 'should have sheets' do
+ expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle_for_export, another_champ_repetition.libelle_for_export])
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index dd0af845a..655686eb2 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -83,7 +83,7 @@ VCR.configure do |c|
c.hook_into :webmock
c.cassette_library_dir = 'spec/fixtures/cassettes'
c.configure_rspec_metadata!
- c.ignore_hosts 'test.host'
+ c.ignore_hosts 'test.host', 'chromedriver.storage.googleapis.com'
end
DatabaseCleaner.strategy = :transaction