diff --git a/.rubocop.yml b/.rubocop.yml index 17ea09c9154..d731af8ae9f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -744,6 +744,7 @@ Rails/CreateTableWithTimestamps: - db/migrate/2017*.rb - db/migrate/2018*.rb - db/migrate/20200630140356_create_traitements.rb + - db/migrate/20230630091637_create_dossier_assignments.rb Rails/Date: Enabled: false diff --git a/Gemfile b/Gemfile index 19a36e41d23..19d93f3a843 100644 --- a/Gemfile +++ b/Gemfile @@ -81,6 +81,7 @@ gem 'rack-attack' gem 'rails-i18n' # Locales par défaut gem 'rake-progressbar', require: false gem 'redcarpet' +gem 'redis' gem 'rexml' # add missing gem due to ruby3 (https://github.com/Shopify/bootsnap/issues/325) gem 'rqrcode' gem 'saml_idp' diff --git a/Gemfile.lock b/Gemfile.lock index f33d836bc5d..9b5d8f2eefe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -188,6 +188,7 @@ GEM coercible (1.0.0) descendants_tracker (~> 0.0.1) concurrent-ruby (1.2.2) + connection_pool (2.4.1) content_disposition (1.0.0) crack (0.4.5) rexml @@ -587,6 +588,10 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) redcarpet (3.6.0) + redis (5.0.6) + redis-client (>= 0.9.0) + redis-client (0.14.1) + connection_pool regexp_parser (2.8.1) request_store (1.5.0) rack (>= 1.4) @@ -922,6 +927,7 @@ DEPENDENCIES rails-i18n rake-progressbar redcarpet + redis rexml rqrcode rspec-rails diff --git a/app/controllers/administrateurs/groupe_instructeurs_controller.rb b/app/controllers/administrateurs/groupe_instructeurs_controller.rb index 6a1baeaee60..101d05c1f22 100644 --- a/app/controllers/administrateurs/groupe_instructeurs_controller.rb +++ b/app/controllers/administrateurs/groupe_instructeurs_controller.rb @@ -200,7 +200,7 @@ def reaffecter target_group = procedure.groupe_instructeurs.find(params[:target_group]) reaffecter_bulk_messages(target_group) groupe_instructeur.dossiers.find_each do |dossier| - dossier.assign_to_groupe_instructeur(target_group, current_administrateur) + dossier.assign_to_groupe_instructeur(target_group, DossierAssignment.modes.fetch(:manual), current_administrateur) end flash[:notice] = "Les dossiers du groupe « #{groupe_instructeur.label} » ont été réaffectés au groupe « #{target_group.label} »." @@ -210,7 +210,7 @@ def reaffecter def reaffecter_all_dossiers_to_defaut_groupe procedure.groupe_instructeurs_but_defaut.each do |gi| gi.dossiers.find_each do |dossier| - dossier.assign_to_groupe_instructeur(procedure.defaut_groupe_instructeur, current_administrateur) + dossier.assign_to_groupe_instructeur(procedure.defaut_groupe_instructeur, DossierAssignment.modes.fetch(:manual), current_administrateur) end end end diff --git a/app/controllers/administrateurs/procedures_controller.rb b/app/controllers/administrateurs/procedures_controller.rb index 23adae47742..b68c2569189 100644 --- a/app/controllers/administrateurs/procedures_controller.rb +++ b/app/controllers/administrateurs/procedures_controller.rb @@ -115,8 +115,6 @@ def show .find(params[:id]) @procedure.validate(:publication) - - @current_administrateur = current_administrateur end def edit diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index fcebd8fd4fc..a7bda002538 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -93,6 +93,7 @@ def personnes_impliquees @avis_emails = dossier.experts.map(&:email) @invites_emails = dossier.invites.map(&:email) @potential_recipients = dossier.groupe_instructeur.instructeurs.reject { |g| g == current_instructeur } + @manual_assignments = dossier.dossier_assignments.manual.includes(:groupe_instructeur, :previous_groupe_instructeur) end def send_to_instructeurs @@ -365,9 +366,7 @@ def reaffecter .procedure .groupe_instructeurs.find(params[:groupe_instructeur_id]) - dossier.assign_to_groupe_instructeur(new_group) - - dossier.update!(forced_groupe_instructeur: true) + dossier.assign_to_groupe_instructeur(new_group, DossierAssignment.modes.fetch(:manual), current_instructeur) flash.notice = t('instructeurs.dossiers.reaffectation', dossier_id: dossier.id, label: new_group.label) redirect_to instructeur_procedure_path(procedure) diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 94e67b7da75..0441f7f5cf8 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -561,8 +561,6 @@ def submit_dossier_and_compute_errors errors += format_errors(errors: @dossier.errors) errors += format_errors(errors: @dossier.check_mandatory_and_visible_champs) - RoutingEngine.compute(@dossier) - errors end diff --git a/app/graphql/mutations/dossier_changer_groupe_instructeur.rb b/app/graphql/mutations/dossier_changer_groupe_instructeur.rb index 7a472b92e33..e9f4cb94784 100644 --- a/app/graphql/mutations/dossier_changer_groupe_instructeur.rb +++ b/app/graphql/mutations/dossier_changer_groupe_instructeur.rb @@ -11,7 +11,7 @@ class DossierChangerGroupeInstructeur < Mutations::BaseMutation field :errors, [Types::ValidationErrorType], null: true def resolve(dossier:, groupe_instructeur:) - dossier.assign_to_groupe_instructeur(groupe_instructeur) + dossier.assign_to_groupe_instructeur(groupe_instructeur, DossierAssignment.modes.fetch(:manual), current_administrateur) { dossier: } end diff --git a/app/lib/api_entreprise/api.rb b/app/lib/api_entreprise/api.rb index f81faf0a038..feced9855e5 100644 --- a/app/lib/api_entreprise/api.rb +++ b/app/lib/api_entreprise/api.rb @@ -91,6 +91,10 @@ def current_status private + def recipient + @procedure&.service && @procedure.service.siret.presence || ENV.fetch('API_ENTREPRISE_DEFAULT_SIRET') + end + def call_with_siret(resource_name, siret_or_siren, user_id: nil) url = make_url(resource_name, siret_or_siren) @@ -157,7 +161,7 @@ def build_params(user_id) def base_params { context: APPLICATION_NAME, - recipient: ENV.fetch('API_ENTREPRISE_DEFAULT_SIRET'), + recipient: recipient, non_diffusables: true } end diff --git a/app/models/administrateur.rb b/app/models/administrateur.rb index ece450a1ae2..2a2386af182 100644 --- a/app/models/administrateur.rb +++ b/app/models/administrateur.rb @@ -139,6 +139,10 @@ def merge(old_admin) i.administrateurs << self i.administrateurs.delete(old_admin) end + + old_admin.api_tokens.where('version >= ?', 3).find_each do |token| + self.api_tokens << token + end end def zones diff --git a/app/models/concerns/dossier_clone_concern.rb b/app/models/concerns/dossier_clone_concern.rb index 5c02ea73136..fc056c8bd06 100644 --- a/app/models/concerns/dossier_clone_concern.rb +++ b/app/models/concerns/dossier_clone_concern.rb @@ -69,7 +69,6 @@ def merge_fork(editing_fork) diff = make_diff(editing_fork) apply_diff(diff) touch(:last_champ_updated_at) - assign_to_groupe_instructeur(editing_fork.groupe_instructeur) end reload update_search_terms_later diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index 15eba801d9c..7839a21043d 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -354,15 +354,18 @@ def parse_tags(text) end def used_tags_and_libelle_for(text) - parse_tags(text).filter_map do |token| - case token - in { tag: tag, id: id } - [id, tag] - in { tag: tag } - [tag] - else - nil - end + # MD5 should be enough and it avoids long key + Rails.cache.fetch(["parse_tags", Digest::MD5.hexdigest(text)], expires_in: 1.day) do + parse_tags(text).filter_map do |token| + case token + in { tag: tag, id: id } + [id, tag] + in { tag: tag } + [tag] + else + nil + end + end end end end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 8bdb7bb777d..e91336284f6 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -155,6 +155,8 @@ def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zon has_one :traitement, -> { order(processed_at: :desc) }, inverse_of: false has_many :dossier_operation_logs, -> { order(:created_at) }, inverse_of: :dossier + has_many :dossier_assignments, -> { order(:assigned_at) }, inverse_of: :dossier, dependent: :destroy + has_one :dossier_assignment, -> { order(assigned_at: :desc) }, inverse_of: false belongs_to :groupe_instructeur, optional: true belongs_to :revision, class_name: 'ProcedureRevision', optional: false @@ -474,7 +476,6 @@ def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zon validates :user, presence: true, if: -> { deleted_user_email_never_send.nil? }, unless: -> { prefilled } validates :individual, presence: true, if: -> { revision.procedure.for_individual? } - validates :groupe_instructeur, presence: true, if: -> { !brouillon? } validates_associated :prefilled_champs_public, on: :prefilling @@ -686,13 +687,17 @@ def show_procedure_state_warning? procedure.discarded? || (brouillon? && !procedure.dossier_can_transition_to_en_construction?) end - def assign_to_groupe_instructeur(groupe_instructeur, author = nil) + def assign_to_groupe_instructeur(groupe_instructeur, mode, author = nil) return if groupe_instructeur.present? && groupe_instructeur.procedure != procedure return if self.groupe_instructeur == groupe_instructeur + previous_groupe_instructeur = self.groupe_instructeur + update!(groupe_instructeur:, groupe_instructeur_updated_at: Time.zone.now) + update!(forced_groupe_instructeur: true) if mode == DossierAssignment.modes.fetch(:manual) if !brouillon? + create_assignment(mode, previous_groupe_instructeur, groupe_instructeur, author&.email) unfollow_stale_instructeurs if author.present? log_dossier_operation(author, :changer_groupe_instructeur, self) @@ -908,6 +913,7 @@ def after_passer_en_construction MailTemplatePresenterService.create_commentaire_for_state(self) NotificationMailer.send_en_construction_notification(self).deliver_later procedure.compute_dossiers_count + RoutingEngine.compute(self) end def after_passer_en_instruction(h) @@ -1319,6 +1325,19 @@ def sva_svr_decision_in_days (sva_svr_decision_on - Date.current).to_i end + def create_assignment(mode, previous_groupe_instructeur, groupe_instructeur, instructeur_email = nil) + DossierAssignment.create!( + dossier_id: self.id, + mode: mode, + previous_groupe_instructeur_id: previous_groupe_instructeur&.id, + groupe_instructeur_id: groupe_instructeur.id, + previous_groupe_instructeur_label: previous_groupe_instructeur&.label, + groupe_instructeur_label: groupe_instructeur.label, + assigned_at: Time.zone.now, + assigned_by: instructeur_email + ) + end + private def create_missing_traitemets diff --git a/app/models/dossier_assignment.rb b/app/models/dossier_assignment.rb new file mode 100644 index 00000000000..46917e188b4 --- /dev/null +++ b/app/models/dossier_assignment.rb @@ -0,0 +1,34 @@ +# == Schema Information +# +# Table name: dossier_assignments +# +# id :bigint not null, primary key +# assigned_at :datetime not null +# assigned_by :string +# groupe_instructeur_label :string +# mode :string not null +# previous_groupe_instructeur_label :string +# dossier_id :bigint not null +# groupe_instructeur_id :bigint +# previous_groupe_instructeur_id :bigint +# +class DossierAssignment < ApplicationRecord + belongs_to :dossier + + belongs_to :groupe_instructeur, optional: true, inverse_of: :assignments + belongs_to :previous_groupe_instructeur, class_name: 'GroupeInstructeur', optional: true, inverse_of: :previous_assignments + + enum mode: { + auto: 'auto', + manual: 'manual' + } + scope :manual, -> { where(mode: :manual) } + + def groupe_instructeur_label + @groupe_instructeur_label ||= groupe_instructeur&.label.presence || read_attribute(:groupe_instructeur_label) + end + + def previous_groupe_instructeur_label + @previous_groupe_instructeur_label ||= previous_groupe_instructeur&.label.presence || read_attribute(:previous_groupe_instructeur_label) + end +end diff --git a/app/models/etablissement.rb b/app/models/etablissement.rb index 79e79feb8d4..b4caffe020c 100644 --- a/app/models/etablissement.rb +++ b/app/models/etablissement.rb @@ -66,6 +66,16 @@ class Etablissement < ApplicationRecord fermé: "fermé" }, _prefix: true + def entreprise_raison_sociale + read_attribute(:entreprise_raison_sociale).presence || raison_sociale_for_ei + end + + def raison_sociale_for_ei + if entreprise_nom || entreprise_prenom + [entreprise_nom, entreprise_prenom].join(' ') + end + end + def search_terms [ entreprise_siren, diff --git a/app/models/groupe_instructeur.rb b/app/models/groupe_instructeur.rb index 0227e645ad6..7acf19561d7 100644 --- a/app/models/groupe_instructeur.rb +++ b/app/models/groupe_instructeur.rb @@ -19,6 +19,8 @@ class GroupeInstructeur < ApplicationRecord has_many :dossiers has_many :deleted_dossiers has_many :batch_operations, through: :dossiers, source: :batch_operations + has_many :assignments, class_name: 'DossierAssignment', dependent: :nullify, inverse_of: :groupe_instructeur + has_many :previous_assignments, class_name: 'DossierAssignment', dependent: :nullify, inverse_of: :previous_groupe_instructeur has_and_belongs_to_many :exports, dependent: :destroy has_and_belongs_to_many :bulk_messages, dependent: :destroy diff --git a/app/models/procedure.rb b/app/models/procedure.rb index d6d705f5270..b8f301462a9 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -300,8 +300,7 @@ def revisions_with_pending_dossiers validates :lien_site_web, presence: true, if: :publiee? validates :lien_notice, url: { no_local: true, allow_blank: true } - validates :lien_dpo, format: { with: Devise.email_regexp, message: "n'est pas valide" }, if: :lien_dpo_email? - validates :lien_dpo, url: { no_local: true, allow_blank: true }, unless: :lien_dpo_email? + validates :lien_dpo, url: { no_local: true, allow_blank: true, accept_email: true } validates :draft_types_de_champ_public, 'types_de_champ/no_empty_block': true, diff --git a/app/models/routing_engine.rb b/app/models/routing_engine.rb index a0a671d33bf..4d9e00c76fc 100644 --- a/app/models/routing_engine.rb +++ b/app/models/routing_engine.rb @@ -5,7 +5,9 @@ def self.compute(dossier) matching_groupe = dossier.procedure.groupe_instructeurs.active.reject(&:invalid_rule?).find do |gi| gi.routing_rule&.compute(dossier.champs) end + matching_groupe ||= dossier.procedure.defaut_groupe_instructeur - dossier.assign_to_groupe_instructeur(matching_groupe) + + dossier.assign_to_groupe_instructeur(matching_groupe, DossierAssignment.modes.fetch(:auto)) end end diff --git a/app/services/archive_uploader.rb b/app/services/archive_uploader.rb index fc644a65e2c..7da5ff246a5 100644 --- a/app/services/archive_uploader.rb +++ b/app/services/archive_uploader.rb @@ -81,10 +81,13 @@ def retryable_syscall_to_custom_uploader(blob) limit_to_retry = 1 begin syscall_to_custom_uploader(blob) - rescue + rescue => e if limit_to_retry > 0 limit_to_retry = limit_to_retry - 1 retry + else + Sentry.set_tags(procedure:) + Sentry.capture_exception(e, extra: { filename: }) end end end diff --git a/app/services/geojson_service.rb b/app/services/geojson_service.rb index d23bc9e97be..47721ac8fa9 100644 --- a/app/services/geojson_service.rb +++ b/app/services/geojson_service.rb @@ -1,7 +1,13 @@ class GeojsonService def self.valid?(json) schemer = JSONSchemer.schema(Rails.root.join('app/schemas/geojson.json')) - schemer.valid?(json) + if schemer.valid?(json) + if ActiveRecord::Base.connection.execute("SELECT 1 as one FROM pg_extension WHERE extname = 'postgis';").count.zero? + true + else + ActiveRecord::Base.connection.exec_query('select ST_IsValid(ST_GeomFromGeoJSON($1)) as valid;', 'ValidateGeoJSON', [json.to_json]).first['valid'] + end + end end def self.to_json_polygon_for_cadastre(coordinates) diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index c24e3590b89..fb2f4df0270 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -14,6 +14,7 @@ def initialize(options) options.reverse_merge!(no_local: false) options.reverse_merge!(public_suffix: false) options.reverse_merge!(accept_array: false) + options.reverse_merge!(accept_email: false) super(options) end @@ -53,15 +54,18 @@ def filtered_options(value) def validate_url(record, attribute, value, message, schemes) uri = Addressable::URI.parse(value) - host = uri && uri.host - scheme = uri && uri.scheme - valid_scheme = host && scheme && schemes.include?(scheme) - valid_no_local = !options.fetch(:no_local) || (host && host.include?('.')) - valid_suffix = !options.fetch(:public_suffix) || (host && PublicSuffix.valid?(host, default_rule: nil)) + unless options.fetch(:accept_email) && uri.path.match?(/^(.+)@(.+)$/) + host = uri && uri.host + scheme = uri && uri.scheme - unless valid_scheme && valid_no_local && valid_suffix - record.errors.add(attribute, message, **filtered_options(value)) + valid_scheme = host && scheme && schemes.include?(scheme) + valid_no_local = !options.fetch(:no_local) || (host && host.include?('.')) + valid_suffix = !options.fetch(:public_suffix) || (host && PublicSuffix.valid?(host, default_rule: nil)) + + unless valid_scheme && valid_no_local && valid_suffix + record.errors.add(attribute, message, **filtered_options(value)) + end end rescue Addressable::URI::InvalidURIError record.errors.add(attribute, message, **filtered_options(value)) diff --git a/app/views/administrateurs/groupe_instructeurs/reaffecter_dossiers.html.haml b/app/views/administrateurs/groupe_instructeurs/reaffecter_dossiers.html.haml index 1d2c5c456b6..d74cccae2f7 100644 --- a/app/views/administrateurs/groupe_instructeurs/reaffecter_dossiers.html.haml +++ b/app/views/administrateurs/groupe_instructeurs/reaffecter_dossiers.html.haml @@ -16,11 +16,11 @@ %th{ colspan: 2 }= t(".existing_groupe", count: @groupes_instructeurs.total_count) %tbody - @groupes_instructeurs.each do |group| - %tr - %td= group.label - %td.actions= button_to 'Réaffecter les dossiers à ce groupe', + .flex.justify-between.align-center.fr-mb-2w + %p.fr-mb-0= group.label + = button_to 'Réaffecter les dossiers à ce groupe', reaffecter_admin_procedure_groupe_instructeur_path(:target_group => group), - { class: 'button', + { class: 'fr-btn fr-btn--secondary fr-btn--sm', data: { confirm: "Êtes-vous sûr de vouloir réaffecter les dossiers du groupe « #{@groupe_instructeur.label} » vers le groupe  « #{group.label} » ?" } } = paginate @groupes_instructeurs, views_prefix: 'shared' diff --git a/app/views/instructeurs/dossiers/_reaffectations_block.html.haml b/app/views/instructeurs/dossiers/_reaffectations_block.html.haml new file mode 100644 index 00000000000..57175a6d4c1 --- /dev/null +++ b/app/views/instructeurs/dossiers/_reaffectations_block.html.haml @@ -0,0 +1,16 @@ +.tab-title Réaffectations +- if manual_assignments.any? + %ul.tab-list + - manual_assignments.each do |assignment| + %li + - assigned_at = l(assignment.assigned_at, format: '%d %B %Y à %R') + - assigned_by = assignment.assigned_by + - if assigned_by.present? + = "Le #{assigned_at}, #{assigned_by} a réaffecté ce dossier du groupe « #{assignment.previous_groupe_instructeur_label} » au groupe « #{assignment.groupe_instructeur_label} »" + - else + = "Le #{assigned_at}, ce dossier a été réaffecté du groupe « #{assignment.previous_groupe_instructeur_label} » au groupe « #{assignment.groupe_instructeur_label} »" + +- elsif dossier.forced_groupe_instructeur + %p.tab-paragraph Ce dossier a été réaffecté au groupe « #{dossier.groupe_instructeur.label} » +- else + %p.tab-paragraph Ce dossier n'a pas été réaffecté diff --git a/app/views/instructeurs/dossiers/personnes_impliquees.html.haml b/app/views/instructeurs/dossiers/personnes_impliquees.html.haml index a55b61b2afc..18067ba0042 100644 --- a/app/views/instructeurs/dossiers/personnes_impliquees.html.haml +++ b/app/views/instructeurs/dossiers/personnes_impliquees.html.haml @@ -16,6 +16,7 @@ = render partial: 'instructeurs/dossiers/decisions_rendues_block', locals: { traitements: @dossier.traitements } + = render partial: 'instructeurs/dossiers/reaffectations_block', locals: { manual_assignments: @manual_assignments, dossier: @dossier } + - if @dossier.archived? && @dossier.archived_at.present? = render partial: 'instructeurs/dossiers/archived_block', locals: @dossier.slice(:archived_by, :archived_at) - diff --git a/app/views/instructeurs/dossiers/reaffectation.html.haml b/app/views/instructeurs/dossiers/reaffectation.html.haml index 116191aa043..5b530759247 100644 --- a/app/views/instructeurs/dossiers/reaffectation.html.haml +++ b/app/views/instructeurs/dossiers/reaffectation.html.haml @@ -7,18 +7,18 @@ .card .card-title Réaffectation du dossier nº #{@dossier.id} du groupe « #{@groupe_instructeur.label} » %p - Vous pouvez réaffecter le dossier nº #{@dossier.id} à l'un des groupes d'instructeurs suivants. + Vous pouvez réaffecter le dossier nº #{@dossier.id} à l’un des groupes d’instructeurs suivants. %table.table.mt-2 %thead %tr %th{ colspan: 2 }= t("instructeurs.dossiers.existing_groupe", count: @groupes_instructeurs.total_count) %tbody - @groupes_instructeurs.each do |group| - %tr - %td= group.label - %td.actions= button_to 'Réaffecter le dossier à ce groupe', + .flex.justify-between.align-center.fr-mb-2w + %p.fr-mb-0= group.label + = button_to 'Réaffecter le dossier à ce groupe', reaffecter_instructeur_dossier_path(procedure_id: @dossier.procedure.id, dossier_id: @dossier.id, groupe_instructeur_id: group.id), - { class: 'button', + { class: 'fr-btn fr-btn--secondary fr-btn--sm', data: { confirm: "Êtes-vous sûr de vouloir réaffecter le dossier nº #{@dossier.id} du groupe « #{@groupe_instructeur.label} » vers le groupe  « #{group.label} » ?" } } = paginate @groupes_instructeurs, views_prefix: 'shared' diff --git a/config/env.example.optional b/config/env.example.optional index 2a905ab1fb0..4f492107632 100644 --- a/config/env.example.optional +++ b/config/env.example.optional @@ -207,3 +207,11 @@ COJO_JWT_RSA_PRIVATE_KEY="" COJO_JWT_ISS="" API_COJO_URL="" + +# Set to `disabled` if you want to diable postgis +POSTGIS_EXTENSION_DISABLED="" + +# Use redis as primary rails cache store, file system otherwise +REDIS_CACHE_URL="" +REDIS_CACHE_SSL="enabled" +REDIS_CACHE_SSL_VERIFY_NONE="enabled" diff --git a/config/environments/development.rb b/config/environments/development.rb index 281a0e67eaa..8860f23b0d8 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -25,7 +25,11 @@ config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true - config.cache_store = :memory_store + if ENV['REDIS_CACHE_URL'].present? + config.cache_store = :redis_cache_store, { url: ENV['REDIS_CACHE_URL'] } + else + config.cache_store = :memory_store + end config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } diff --git a/config/environments/production.rb b/config/environments/production.rb index 75320fad8e0..e4966a97bba 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -61,7 +61,20 @@ # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) # Use a different cache store in production. - # config.cache_store = :mem_cache_store + if ENV['REDIS_CACHE_URL'].present? + redis_options = { url: ENV['REDIS_CACHE_URL'] } + redis_options[:ssl] = (ENV['REDIS_CACHE_SSL'] == 'enabled') + if ENV['REDIS_CACHE_SSL_VERIFY_NONE'] == 'enabled' + redis_options[:ssl_params] = { verify_mode: OpenSSL::SSL::VERIFY_NONE } + end + + redis_options[:error_handler] = -> (method:, returning:, exception:) { + Sentry.capture_exception exception, level: 'warning', + tags: { method: method, returning: returning } + } + + config.cache_store = :redis_cache_store, redis_options + end # Use a real queuing backend for Active Job (and separate queues per environment). config.active_job.queue_adapter = :delayed_job diff --git a/config/initializers/sendinblue.rb b/config/initializers/sendinblue.rb index 379f0b20300..2d73ffb3084 100644 --- a/config/initializers/sendinblue.rb +++ b/config/initializers/sendinblue.rb @@ -10,8 +10,8 @@ class SMTP < ::Mail::SMTP; end ActionMailer::Base.sendinblue_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', + address: 'smtp-relay.brevo.com', + domain: 'smtp-relay.brevo.com', port: '587', authentication: :cram_md5 } diff --git a/config/locales/views/layouts/_account_dropdown.en.yml b/config/locales/views/layouts/_account_dropdown.en.yml index d12216a0697..b9925c6f701 100644 --- a/config/locales/views/layouts/_account_dropdown.en.yml +++ b/config/locales/views/layouts/_account_dropdown.en.yml @@ -16,3 +16,4 @@ en: administrateur: admin expert: expert user: user + guest: guest diff --git a/config/locales/views/layouts/_account_dropdown.fr.yml b/config/locales/views/layouts/_account_dropdown.fr.yml index 74375bfa09a..bbdba1e9eab 100644 --- a/config/locales/views/layouts/_account_dropdown.fr.yml +++ b/config/locales/views/layouts/_account_dropdown.fr.yml @@ -16,3 +16,4 @@ fr: administrateur: administrateur expert: expert user: usager + guest: invité diff --git a/db/migrate/20230630091637_create_dossier_assignments.rb b/db/migrate/20230630091637_create_dossier_assignments.rb new file mode 100644 index 00000000000..6dc77f03f9b --- /dev/null +++ b/db/migrate/20230630091637_create_dossier_assignments.rb @@ -0,0 +1,14 @@ +class CreateDossierAssignments < ActiveRecord::Migration[7.0] + def change + create_table :dossier_assignments do |t| + t.references :dossier, foreign_key: true, null: false + t.string :mode, null: false + t.bigint :groupe_instructeur_id + t.bigint :previous_groupe_instructeur_id + t.string :groupe_instructeur_label + t.string :previous_groupe_instructeur_label + t.string :assigned_by + t.timestamp :assigned_at, null: false + end + end +end diff --git a/db/migrate/20230704093503_enable_postgis.rb b/db/migrate/20230704093503_enable_postgis.rb new file mode 100644 index 00000000000..6b1f8497daa --- /dev/null +++ b/db/migrate/20230704093503_enable_postgis.rb @@ -0,0 +1,7 @@ +class EnablePostgis < ActiveRecord::Migration[7.0] + def change + if ENV['POSTGIS_EXTENSION_DISABLED'] != 'disabled' && ActiveRecord::Base.connection.execute("SELECT 1 as one FROM pg_extension WHERE extname = 'postgis';").count.zero? + enable_extension :postgis + end + end +end diff --git a/db/schema.rb b/db/schema.rb index cb2af3f9a92..d59466caa74 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -308,6 +308,18 @@ t.index ["procedure_id"], name: "index_deleted_dossiers_on_procedure_id" end + create_table "dossier_assignments", force: :cascade do |t| + t.datetime "assigned_at", precision: nil, null: false + t.string "assigned_by" + t.bigint "dossier_id", null: false + t.bigint "groupe_instructeur_id" + t.string "groupe_instructeur_label" + t.string "mode", null: false + t.bigint "previous_groupe_instructeur_id" + t.string "previous_groupe_instructeur_label" + t.index ["dossier_id"], name: "index_dossier_assignments_on_dossier_id" + end + create_table "dossier_batch_operations", force: :cascade do |t| t.bigint "batch_operation_id", null: false t.datetime "created_at", null: false @@ -1041,6 +1053,7 @@ add_foreign_key "commentaires", "dossiers" add_foreign_key "commentaires", "experts" add_foreign_key "commentaires", "instructeurs" + add_foreign_key "dossier_assignments", "dossiers" add_foreign_key "dossier_batch_operations", "batch_operations" add_foreign_key "dossier_batch_operations", "dossiers" add_foreign_key "dossier_corrections", "commentaires" diff --git a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb index e7152e9b5f7..fdaeb450a6c 100644 --- a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb +++ b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb @@ -208,6 +208,9 @@ def reaffecter_url(group) it { expect(response).to redirect_to(admin_procedure_groupe_instructeurs_path(procedure)) } it { expect(gi_1_2.dossiers.last.id).to be(dossier12.id) } it { expect(dossier12.groupe_instructeur.id).to be(gi_1_2.id) } + it { expect(dossier12.dossier_assignment.dossier_id).to be(dossier12.id) } + it { expect(dossier12.dossier_assignment.groupe_instructeur_id).to be(gi_1_2.id) } + it { expect(dossier12.dossier_assignment.assigned_by).to eq(admin.email) } it { expect(bulk_message.groupe_instructeurs).to contain_exactly(gi_1_2, gi_1_3) } end @@ -231,6 +234,30 @@ def reaffecter_url(group) end end + describe '#destroy_all_groups_but_defaut' do + let!(:dossierA) { create(:dossier, :en_construction, :with_individual, procedure: procedure, groupe_instructeur: gi_1_2) } + let!(:dossierB) { create(:dossier, :en_construction, :with_individual, procedure: procedure, groupe_instructeur: gi_1_2) } + + before do + post :destroy_all_groups_but_defaut, + params: { + procedure_id: procedure.id + } + dossierA.reload + dossierB.reload + end + + it do + expect(dossierA.groupe_instructeur.id).to be(procedure.defaut_groupe_instructeur.id) + expect(dossierB.groupe_instructeur.id).to be(procedure.defaut_groupe_instructeur.id) + expect(dossierA.dossier_assignment.dossier_id).to be(dossierA.id) + expect(dossierB.dossier_assignment.dossier_id).to be(dossierB.id) + expect(dossierA.dossier_assignment.groupe_instructeur_id).to be(procedure.defaut_groupe_instructeur.id) + expect(dossierB.dossier_assignment.groupe_instructeur_id).to be(procedure.defaut_groupe_instructeur.id) + expect(dossierA.dossier_assignment.assigned_by).to eq(admin.email) + expect(dossierB.dossier_assignment.assigned_by).to eq(admin.email) + end + end describe '#update' do let(:new_name) { 'nouveau nom du groupe' } let!(:procedure_non_routee) { create(:procedure, :published, :for_individual, administrateurs: [admin]) } diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index 6442d9b2d1f..bd5e2d7e789 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -1179,16 +1179,21 @@ it do expect(response).to have_http_status(:ok) - expect(response.body).to include("Vous pouvez réaffecter le dossier nº #{dossier.id} à l'un des groupes d'instructeurs suivants.") + expect(response.body).to include("Vous pouvez réaffecter le dossier nº #{dossier.id} à l’un des groupes d’instructeurs suivants.") expect(response.body).to include('2 groupes existent') end end describe '#reaffecter' do + let!(:gi_1) { procedure.groupe_instructeurs.first } let!(:gi_2) { GroupeInstructeur.create(label: 'deuxième groupe', procedure: procedure) } - let!(:dossier) { create(:dossier, :en_construction, procedure: procedure, groupe_instructeur: procedure.groupe_instructeurs.first) } + let!(:dossier) { create(:dossier, :en_construction, :with_individual, procedure: procedure, groupe_instructeur: gi_1) } + let!(:new_instructeur) { create(:instructeur) } before do + gi_1.instructeurs << new_instructeur + new_instructeur.followed_dossiers << dossier + post :reaffecter, params: { procedure_id: procedure.id, @@ -1200,8 +1205,39 @@ it do expect(dossier.reload.groupe_instructeur.id).to eq(gi_2.id) expect(dossier.forced_groupe_instructeur).to be_truthy + expect(dossier.followers_instructeurs).to eq [] + expect(dossier.dossier_assignment.previous_groupe_instructeur_id).to eq(gi_1.id) + expect(dossier.dossier_assignment.previous_groupe_instructeur_label).to eq(gi_1.label) + expect(dossier.dossier_assignment.groupe_instructeur_id).to eq(gi_2.id) + expect(dossier.dossier_assignment.groupe_instructeur_label).to eq(gi_2.label) + expect(dossier.dossier_assignment.mode).to eq('manual') + expect(dossier.dossier_assignment.assigned_by).to eq(instructeur.email) expect(response).to redirect_to(instructeur_procedure_path(procedure)) expect(flash.notice).to eq("Le dossier nº #{dossier.id} a été réaffecté au groupe d’instructeurs « deuxième groupe ».") end end + + describe '#personnes_impliquees' do + let!(:gi_1) { procedure.groupe_instructeurs.first } + let!(:gi_2) { GroupeInstructeur.create(label: 'deuxième groupe', procedure: procedure) } + let!(:dossier) { create(:dossier, :en_construction, :with_individual, procedure: procedure, groupe_instructeur: gi_1) } + let!(:new_instructeur) { create(:instructeur) } + + before do + gi_1.instructeurs << new_instructeur + gi_2.instructeurs << instructeur + new_instructeur.followed_dossiers << dossier + dossier.assign_to_groupe_instructeur(gi_2, DossierAssignment.modes.fetch(:manual), new_instructeur) + + get :personnes_impliquees, + params: { + procedure_id: procedure.id, + dossier_id: dossier.id + } + end + + it do + expect(response.body).to include('a réaffecté ce dossier du groupe « défaut » au groupe « deuxième groupe »') + end + end end diff --git a/spec/lib/api_entreprise/api_spec.rb b/spec/lib/api_entreprise/api_spec.rb index e74017b9b36..19a7f85c6de 100644 --- a/spec/lib/api_entreprise/api_spec.rb +++ b/spec/lib/api_entreprise/api_spec.rb @@ -81,6 +81,26 @@ expect(WebMock).to have_requested(:get, /https:\/\/entreprise.api.gouv.fr\/v3\/insee\/sirene\/unites_legales\/#{siren}/) end end + + context 'with a service without siret' do + let(:procedure) { create(:procedure, :with_service) } + let(:dinum_siret) { "13002526500013" } + it 'send default recipient' do + ENV["API_ENTREPRISE_DEFAULT_SIRET"] = dinum_siret + procedure.service.siret = nil + procedure.service.save(validate: false) + subject + expect(WebMock).to have_requested(:get, /https:\/\/entreprise.api.gouv.fr\/v3\/insee\/sirene\/unites_legales\/#{siren}/).with(query: hash_including({ recipient: dinum_siret })) + end + end + + context 'with a service with siret' do + let(:procedure) { create(:procedure, :with_service) } + it 'send default recipient' do + subject + expect(WebMock).to have_requested(:get, /https:\/\/entreprise.api.gouv.fr\/v3\/insee\/sirene\/unites_legales\/#{siren}/).with(query: hash_including({ recipient: procedure.service.siret })) + end + end end end diff --git a/spec/models/administrateur_spec.rb b/spec/models/administrateur_spec.rb index 1686f6fce05..03eaeb8e72b 100644 --- a/spec/models/administrateur_spec.rb +++ b/spec/models/administrateur_spec.rb @@ -166,6 +166,27 @@ end end + context 'when the old admin has an v3 api token' do + let(:old_admin) { create(:administrateur, :with_api_token) } + + it 'transferts the api token' do + token = old_admin.api_tokens.first + subject + expect(new_admin.api_tokens.count).to eq 1 + expect(new_admin.api_tokens.first).to eq token + end + end + + context 'when the old admin has an old api token' do + let(:old_admin) { create(:administrateur, :with_api_token) } + + it 'does not transfer the api token' do + old_admin.api_tokens.first.update(version: 2) + subject + expect(new_admin.api_tokens.count).to eq 0 + end + end + context 'when both admins share an instructeur' do let(:instructeur) { create(:instructeur) } diff --git a/spec/models/concern/dossier_clone_concern_spec.rb b/spec/models/concern/dossier_clone_concern_spec.rb index 8ebe82ef687..4d9f35d42eb 100644 --- a/spec/models/concern/dossier_clone_concern_spec.rb +++ b/spec/models/concern/dossier_clone_concern_spec.rb @@ -221,7 +221,7 @@ context 'with updated groupe instructeur' do before { dossier.update!(groupe_instructeur: create(:groupe_instructeur)) - forked_dossier.assign_to_groupe_instructeur(dossier.procedure.defaut_groupe_instructeur) + forked_dossier.assign_to_groupe_instructeur(dossier.procedure.defaut_groupe_instructeur, DossierAssignment.modes.fetch(:manual)) } it { is_expected.to eq(added: [], updated: [], removed: []) } diff --git a/spec/models/dossier_assignment_spec.rb b/spec/models/dossier_assignment_spec.rb new file mode 100644 index 00000000000..86fe2267699 --- /dev/null +++ b/spec/models/dossier_assignment_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +RSpec.describe DossierAssignment, type: :model do + include Logic + + before { Flipper.enable(:routing_rules, procedure) } + + context 'Assignment from routing engine' do + let(:procedure) do + create(:procedure, + types_de_champ_public: [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }]).tap do |p| + p.groupe_instructeurs.create(label: 'a second group') + p.groupe_instructeurs.create(label: 'a third group') + end + end + + let(:drop_down_tdc) { procedure.draft_revision.types_de_champ.first } + + let(:dossier) { create(:dossier, :en_construction, procedure:).tap { _1.update(groupe_instructeur_id: nil) } } + + before do + RoutingEngine.compute(dossier) + dossier.reload + end + + it 'creates a dossier assignment with right attributes' do + expect(dossier.dossier_assignments.count).to eq 1 + expect(dossier.dossier_assignment.mode).to eq 'auto' + expect(dossier.dossier_assignment.dossier_id).to eq dossier.id + expect(dossier.dossier_assignment.groupe_instructeur_id).to eq dossier.groupe_instructeur.id + expect(dossier.dossier_assignment.groupe_instructeur_label).to eq dossier.groupe_instructeur.label + expect(dossier.dossier_assignment.previous_groupe_instructeur_id).to be nil + expect(dossier.dossier_assignment.previous_groupe_instructeur_label).to be nil + expect(dossier.dossier_assignment.assigned_by).to be nil + end + end +end diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index 4d8e6a625a5..d35bd2dc5db 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -578,12 +578,12 @@ let(:dossier) { create(:dossier, :en_construction, procedure: procedure) } it "can change groupe instructeur" do - dossier.assign_to_groupe_instructeur(new_groupe_instructeur_new_procedure) + dossier.assign_to_groupe_instructeur(new_groupe_instructeur_new_procedure, DossierAssignment.modes.fetch(:auto)) expect(dossier.groupe_instructeur).not_to eq(new_groupe_instructeur_new_procedure) end it "can not change groupe instructeur if new groupe is from another procedure" do - dossier.assign_to_groupe_instructeur(new_groupe_instructeur) + dossier.assign_to_groupe_instructeur(new_groupe_instructeur, DossierAssignment.modes.fetch(:auto)) expect(dossier.groupe_instructeur).to eq(new_groupe_instructeur) end end @@ -603,7 +603,7 @@ it "unfollows stale instructeurs when groupe instructeur change" do instructeur.follow(dossier) instructeur2.follow(dossier) - dossier.reload.assign_to_groupe_instructeur(new_groupe_instructeur, procedure.administrateurs.first) + dossier.reload.assign_to_groupe_instructeur(new_groupe_instructeur, DossierAssignment.modes.fetch(:auto), procedure.administrateurs.first) expect(dossier.reload.followers_instructeurs).not_to include(instructeur) expect(dossier.reload.followers_instructeurs).to include(instructeur2) diff --git a/spec/models/etablissement_spec.rb b/spec/models/etablissement_spec.rb index dce441520b2..a7f38d6cd26 100644 --- a/spec/models/etablissement_spec.rb +++ b/spec/models/etablissement_spec.rb @@ -36,6 +36,30 @@ end end + describe '#entreprise_raison_sociale' do + subject { etablissement.entreprise_raison_sociale } + + context "with nom and prenom" do + context "without raison sociale" do + let(:etablissement) { create(:etablissement, entreprise_raison_sociale: nil, entreprise_prenom: "Stef", entreprise_nom: "Sanseverino") } + + it { is_expected.to eq "Sanseverino Stef" } + end + + context "with raison sociale" do + let(:etablissement) { create(:etablissement, entreprise_raison_sociale: "Sansev Prod", entreprise_prenom: "Stef", entreprise_nom: "Sanseverino") } + + it { is_expected.to eq "Sansev Prod" } + end + end + + context "without nom and prenom" do + let(:etablissement) { create(:etablissement, entreprise_raison_sociale: "ENGIE", entreprise_prenom: nil, entreprise_nom: nil) } + + it { is_expected.to eq "ENGIE" } + end + end + describe '.entreprise_bilans_bdf_to_csv' do let(:etablissement) { build(:etablissement, entreprise_bilans_bdf: bilans) } let(:ordered_headers) { diff --git a/spec/models/geo_area_spec.rb b/spec/models/geo_area_spec.rb index fd66e27d713..524bf425cf3 100644 --- a/spec/models/geo_area_spec.rb +++ b/spec/models/geo_area_spec.rb @@ -57,12 +57,12 @@ it { expect(geo_area.errors).to have_key(:geometry) } end - context.skip 'invalid_right_hand_rule_polygon' do + context 'invalid_right_hand_rule_polygon' do let(:geo_area) { build(:geo_area, :invalid_right_hand_rule_polygon, champ: nil) } it { expect(geo_area.errors).to have_key(:geometry) } end - context.skip 'hourglass_polygon' do + context 'hourglass_polygon' do let(:geo_area) { build(:geo_area, :hourglass_polygon, champ: nil) } it { expect(geo_area.errors).to have_key(:geometry) } end diff --git a/spec/models/instructeur_spec.rb b/spec/models/instructeur_spec.rb index 538660f17f4..7c3d7a0caf3 100644 --- a/spec/models/instructeur_spec.rb +++ b/spec/models/instructeur_spec.rb @@ -212,7 +212,7 @@ context 'when there is a modification on groupe instructeur' do let(:groupe_instructeur) { create(:groupe_instructeur, instructeurs: [instructeur], procedure: dossier.procedure) } - before { dossier.assign_to_groupe_instructeur(groupe_instructeur) } + before { dossier.assign_to_groupe_instructeur(groupe_instructeur, DossierAssignment.modes.fetch(:auto)) } it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false }) } end diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index 0501f14f586..edada8f4117 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -1605,6 +1605,11 @@ let(:lien_notice) { 'www.démarches-simplifiées.fr' } it { expect(procedure.valid?).to be_falsey } end + + context 'when an email' do + let(:lien_notice) { 'test@demarches-simplifiees.fr' } + it { expect(procedure.valid?).to be_falsey } + end end describe 'lien_dpo' do @@ -1630,6 +1635,11 @@ it { expect(procedure.valid?).to be_truthy } end + context 'when valid email with accents' do + let(:lien_dpo) { 'test@démarches-simplifiées.fr' } + it { expect(procedure.valid?).to be_truthy } + end + context 'when not a valid link' do let(:lien_dpo) { 'www.démarches-simplifiées.fr' } it { expect(procedure.valid?).to be_falsey }