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