) {
+ return s.optional(s.union([s.literal(null), struct]));
+}
+
+const Gon = s.defaulted(
+ s.type({
+ autosave: s.defaulted(
+ s.type({
+ debounce_delay: s.defaulted(s.number(), 0),
+ status_visible_duration: s.defaulted(s.number(), 0)
+ }),
+ {}
+ ),
+ autocomplete: s.defaulted(
+ s.partial(
+ s.type({
+ api_geo_url: s.string(),
+ api_adresse_url: s.string(),
+ api_education_url: s.string()
+ })
+ ),
+ {}
+ ),
+ locale: s.defaulted(s.string(), 'fr'),
+ matomo: s.defaulted(
+ s.type({
+ cookieDomain: s.optional(s.string()),
+ domain: s.optional(s.string()),
+ enabled: s.defaulted(s.boolean(), false),
+ host: s.optional(s.string()),
+ key: nullish(s.union([s.string(), s.number()]))
+ }),
+ {}
+ ),
+ sentry: s.defaulted(
+ s.type({
+ key: nullish(s.string()),
+ enabled: s.defaulted(s.boolean(), false),
+ environment: s.optional(s.string()),
+ user: s.defaulted(s.type({ id: s.string() }), { id: '' }),
+ browser: s.defaulted(s.type({ modern: s.boolean() }), {
+ modern: false
+ }),
+ release: nullish(s.string())
+ }),
+ {}
+ ),
+ crisp: s.defaulted(
+ s.type({
+ key: nullish(s.string()),
+ enabled: s.defaulted(s.boolean(), false),
+ administrateur: s.defaulted(
+ s.type({
+ email: s.string(),
+ DS_SIGN_IN_COUNT: s.number(),
+ DS_NB_DEMARCHES_BROUILLONS: s.number(),
+ DS_NB_DEMARCHES_ACTIVES: s.number(),
+ DS_NB_DEMARCHES_ARCHIVES: s.number(),
+ DS_ID: s.number()
+ }),
+ {
email: '',
DS_SIGN_IN_COUNT: 0,
DS_NB_DEMARCHES_BROUILLONS: 0,
DS_NB_DEMARCHES_ACTIVES: 0,
DS_NB_DEMARCHES_ARCHIVES: 0,
DS_ID: 0
- })
- })
- .default({}),
- defaultQuery: z.string().optional(),
- defaultVariables: z.string().optional()
- })
- .default({});
+ }
+ )
+ }),
+ {}
+ ),
+ defaultQuery: s.optional(s.string()),
+ defaultVariables: s.optional(s.string())
+ }),
+ {}
+);
declare const window: Window & typeof globalThis & { gon: unknown };
export function getConfig() {
- return Gon.parse(window.gon);
+ return s.create(window.gon, Gon);
}
export function show(el: Element | null) {
diff --git a/app/jobs/admin_update_default_zones_job.rb b/app/jobs/admin_update_default_zones_job.rb
index 421526cb4..f09cee59f 100644
--- a/app/jobs/admin_update_default_zones_job.rb
+++ b/app/jobs/admin_update_default_zones_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AdminUpdateDefaultZonesJob < ApplicationJob
def perform(admin)
tchap_hs = APITchap::HsAdapter.new(admin.email).to_hs
diff --git a/app/jobs/api_entreprise/association_job.rb b/app/jobs/api_entreprise/association_job.rb
index 45e2cfb73..9e3ad13a4 100644
--- a/app/jobs/api_entreprise/association_job.rb
+++ b/app/jobs/api_entreprise/association_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::AssociationJob < APIEntreprise::Job
def perform(etablissement_id, procedure_id)
find_etablissement(etablissement_id)
diff --git a/app/jobs/api_entreprise/attestation_fiscale_job.rb b/app/jobs/api_entreprise/attestation_fiscale_job.rb
index 6b01a89f1..fe1db782d 100644
--- a/app/jobs/api_entreprise/attestation_fiscale_job.rb
+++ b/app/jobs/api_entreprise/attestation_fiscale_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::AttestationFiscaleJob < APIEntreprise::Job
def perform(etablissement_id, procedure_id, user_id)
find_etablissement(etablissement_id)
diff --git a/app/jobs/api_entreprise/attestation_sociale_job.rb b/app/jobs/api_entreprise/attestation_sociale_job.rb
index f385a14d3..373cc3b07 100644
--- a/app/jobs/api_entreprise/attestation_sociale_job.rb
+++ b/app/jobs/api_entreprise/attestation_sociale_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::AttestationSocialeJob < APIEntreprise::Job
def perform(etablissement_id, procedure_id)
find_etablissement(etablissement_id)
diff --git a/app/jobs/api_entreprise/bilans_bdf_job.rb b/app/jobs/api_entreprise/bilans_bdf_job.rb
index 9dca78f85..2dad9f4c1 100644
--- a/app/jobs/api_entreprise/bilans_bdf_job.rb
+++ b/app/jobs/api_entreprise/bilans_bdf_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::BilansBdfJob < APIEntreprise::Job
def perform(etablissement_id, procedure_id)
find_etablissement(etablissement_id)
diff --git a/app/jobs/api_entreprise/effectifs_annuels_job.rb b/app/jobs/api_entreprise/effectifs_annuels_job.rb
index e3bcb7908..6154e1ab7 100644
--- a/app/jobs/api_entreprise/effectifs_annuels_job.rb
+++ b/app/jobs/api_entreprise/effectifs_annuels_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::EffectifsAnnuelsJob < APIEntreprise::Job
def perform(etablissement_id, procedure_id, year = default_year)
find_etablissement(etablissement_id)
diff --git a/app/jobs/api_entreprise/effectifs_job.rb b/app/jobs/api_entreprise/effectifs_job.rb
index 40bdc21a2..63e039260 100644
--- a/app/jobs/api_entreprise/effectifs_job.rb
+++ b/app/jobs/api_entreprise/effectifs_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::EffectifsJob < APIEntreprise::Job
def perform(etablissement_id, procedure_id)
etablissement = Etablissement.find(etablissement_id)
diff --git a/app/jobs/api_entreprise/entreprise_job.rb b/app/jobs/api_entreprise/entreprise_job.rb
index 35c0af070..1fe7c37fa 100644
--- a/app/jobs/api_entreprise/entreprise_job.rb
+++ b/app/jobs/api_entreprise/entreprise_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::EntrepriseJob < APIEntreprise::Job
def perform(etablissement_id, procedure_id)
find_etablissement(etablissement_id)
diff --git a/app/jobs/api_entreprise/etablissement_job.rb b/app/jobs/api_entreprise/etablissement_job.rb
new file mode 100644
index 000000000..fac8732f6
--- /dev/null
+++ b/app/jobs/api_entreprise/etablissement_job.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class APIEntreprise::EtablissementJob < APIEntreprise::Job
+ def perform(etablissement_id, procedure_id)
+ find_etablissement(etablissement_id)
+ APIEntrepriseService.update_etablissement_from_degraded_mode(etablissement, procedure_id)
+ end
+end
diff --git a/app/jobs/api_entreprise/exercices_job.rb b/app/jobs/api_entreprise/exercices_job.rb
index de0759a09..0f0afcbe2 100644
--- a/app/jobs/api_entreprise/exercices_job.rb
+++ b/app/jobs/api_entreprise/exercices_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::ExercicesJob < APIEntreprise::Job
rescue_from(APIEntreprise::API::Error::BadFormatRequest) do |exception|
end
diff --git a/app/jobs/api_entreprise/extrait_kbis_job.rb b/app/jobs/api_entreprise/extrait_kbis_job.rb
index 45b80a486..7ebc097a0 100644
--- a/app/jobs/api_entreprise/extrait_kbis_job.rb
+++ b/app/jobs/api_entreprise/extrait_kbis_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::ExtraitKbisJob < APIEntreprise::Job
def perform(etablissement_id, procedure_id)
find_etablissement(etablissement_id)
diff --git a/app/jobs/api_entreprise/job.rb b/app/jobs/api_entreprise/job.rb
index f62a31d69..d228acbe7 100644
--- a/app/jobs/api_entreprise/job.rb
+++ b/app/jobs/api_entreprise/job.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class APIEntreprise::Job < ApplicationJob
DEFAULT_MAX_ATTEMPTS_API_ENTREPRISE_JOBS = 5
- queue_as :api_entreprise
+ queue_as :default
# BadGateway could mean
# - acoss: réessayer ultérieurement
diff --git a/app/jobs/api_entreprise/service_job.rb b/app/jobs/api_entreprise/service_job.rb
index 01e5c62c3..f8b8eaa94 100644
--- a/app/jobs/api_entreprise/service_job.rb
+++ b/app/jobs/api_entreprise/service_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::ServiceJob < APIEntreprise::Job
def perform(service_id)
service = Service.find(service_id)
diff --git a/app/jobs/api_entreprise/tva_job.rb b/app/jobs/api_entreprise/tva_job.rb
index 630409fb0..8573a9d66 100644
--- a/app/jobs/api_entreprise/tva_job.rb
+++ b/app/jobs/api_entreprise/tva_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::TvaJob < APIEntreprise::Job
def perform(etablissement_id, procedure_id)
find_etablissement(etablissement_id)
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
index f9c3a8b34..505ca1586 100644
--- a/app/jobs/application_job.rb
+++ b/app/jobs/application_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationJob < ActiveJob::Base
include ActiveJob::RetryOnTransientErrors
diff --git a/app/jobs/archive_creation_job.rb b/app/jobs/archive_creation_job.rb
index 7a7abbfa2..de4069fcf 100644
--- a/app/jobs/archive_creation_job.rb
+++ b/app/jobs/archive_creation_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ArchiveCreationJob < ApplicationJob
discard_on ActiveRecord::RecordNotFound
diff --git a/app/jobs/auto_archive_procedure_dossiers_job.rb b/app/jobs/auto_archive_procedure_dossiers_job.rb
new file mode 100644
index 000000000..44b3387ab
--- /dev/null
+++ b/app/jobs/auto_archive_procedure_dossiers_job.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AutoArchiveProcedureDossiersJob < ApplicationJob
+ def perform(procedure)
+ procedure
+ .dossiers
+ .state_en_construction
+ .find_each do |d|
+ begin
+ d.passer_automatiquement_en_instruction!
+ rescue StandardError => e
+ Sentry.capture_exception(e, extra: { procedure_id: procedure.id })
+ end
+ end
+ end
+end
diff --git a/app/jobs/batch_operation_enqueue_all_job.rb b/app/jobs/batch_operation_enqueue_all_job.rb
index e01810b93..f99d7a79d 100644
--- a/app/jobs/batch_operation_enqueue_all_job.rb
+++ b/app/jobs/batch_operation_enqueue_all_job.rb
@@ -1,4 +1,8 @@
+# frozen_string_literal: true
+
class BatchOperationEnqueueAllJob < ApplicationJob
+ queue_as :critical
+
def perform(batch_operation)
batch_operation.enqueue_all
end
diff --git a/app/jobs/batch_operation_process_one_job.rb b/app/jobs/batch_operation_process_one_job.rb
index 32870ac57..102b27a76 100644
--- a/app/jobs/batch_operation_process_one_job.rb
+++ b/app/jobs/batch_operation_process_one_job.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class BatchOperationProcessOneJob < ApplicationJob
+ queue_as :critical
retry_on StandardError, attempts: 1 # default 5, for now no retryable behavior
def perform(batch_operation, dossier)
diff --git a/app/jobs/champ_fetch_external_data_job.rb b/app/jobs/champ_fetch_external_data_job.rb
index 648150d9d..134d579e0 100644
--- a/app/jobs/champ_fetch_external_data_job.rb
+++ b/app/jobs/champ_fetch_external_data_job.rb
@@ -1,5 +1,8 @@
+# frozen_string_literal: true
+
class ChampFetchExternalDataJob < ApplicationJob
discard_on ActiveJob::DeserializationError
+ queue_as :critical # ui feedback, asap
include Dry::Monads[:result]
diff --git a/app/jobs/concerns/datagouv_cron_schedulable_concern.rb b/app/jobs/concerns/datagouv_cron_schedulable_concern.rb
index c1d10114a..0394d788a 100644
--- a/app/jobs/concerns/datagouv_cron_schedulable_concern.rb
+++ b/app/jobs/concerns/datagouv_cron_schedulable_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DatagouvCronSchedulableConcern
extend ActiveSupport::Concern
class_methods do
diff --git a/app/jobs/cron/administrateur_activate_before_expiration_job.rb b/app/jobs/cron/administrateur_activate_before_expiration_job.rb
index 0c5779e16..d6c962715 100644
--- a/app/jobs/cron/administrateur_activate_before_expiration_job.rb
+++ b/app/jobs/cron/administrateur_activate_before_expiration_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::AdministrateurActivateBeforeExpirationJob < Cron::CronJob
- self.schedule_expression = "every day at 8 am"
+ self.schedule_expression = "every day at 08:00"
def perform(*args)
Administrateur
diff --git a/app/jobs/cron/auto_archive_procedure_job.rb b/app/jobs/cron/auto_archive_procedure_job.rb
index 9a998cf43..6dd5593fb 100644
--- a/app/jobs/cron/auto_archive_procedure_job.rb
+++ b/app/jobs/cron/auto_archive_procedure_job.rb
@@ -1,17 +1,16 @@
+# frozen_string_literal: true
+
class Cron::AutoArchiveProcedureJob < Cron::CronJob
self.schedule_expression = "every 1 minute"
+ queue_as :critical
def perform(*args)
procedures_to_close.each do |procedure|
# A buggy procedure should NEVER prevent the closing of another procedure
# we therefore exceptionally add a `begin resue` block.
begin
- procedure
- .dossiers
- .state_en_construction
- .find_each(&:passer_automatiquement_en_instruction!)
-
procedure.close!
+ AutoArchiveProcedureDossiersJob.perform_later(procedure)
rescue StandardError => e
Sentry.capture_exception(e, extra: { procedure_id: procedure.id })
end
diff --git a/app/jobs/cron/backfill_siret_degraded_mode_job.rb b/app/jobs/cron/backfill_siret_degraded_mode_job.rb
index 11ac5d0fb..bb80ed637 100644
--- a/app/jobs/cron/backfill_siret_degraded_mode_job.rb
+++ b/app/jobs/cron/backfill_siret_degraded_mode_job.rb
@@ -1,3 +1,6 @@
+# frozen_string_literal: true
+
+# TODO: remove this job in a few days once all failed etablissements are queued as separate jobs
class Cron::BackfillSiretDegradedModeJob < Cron::CronJob
self.schedule_expression = "every 2 hour"
diff --git a/app/jobs/cron/cron_job.rb b/app/jobs/cron/cron_job.rb
index 10a3f4524..7ca5e04e6 100644
--- a/app/jobs/cron/cron_job.rb
+++ b/app/jobs/cron/cron_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::CronJob < ApplicationJob
- queue_as :cron
+ queue_as :default
class_attribute :schedule_expression
class << self
diff --git a/app/jobs/cron/datagouv/account_by_month_job.rb b/app/jobs/cron/datagouv/account_by_month_job.rb
index 187da87db..77a6d0045 100644
--- a/app/jobs/cron/datagouv/account_by_month_job.rb
+++ b/app/jobs/cron/datagouv/account_by_month_job.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Cron::Datagouv::AccountByMonthJob < Cron::CronJob
include DatagouvCronSchedulableConcern
- self.schedule_expression = "every month at 3:00"
+ self.schedule_expression = "every month at 4:30"
FILE_NAME = "nb_comptes_crees_par_mois"
def perform(*args)
diff --git a/app/jobs/cron/datagouv/administrateur_by_month_job.rb b/app/jobs/cron/datagouv/administrateur_by_month_job.rb
index 1391df690..b4cc46846 100644
--- a/app/jobs/cron/datagouv/administrateur_by_month_job.rb
+++ b/app/jobs/cron/datagouv/administrateur_by_month_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::Datagouv::AdministrateurByMonthJob < Cron::CronJob
include DatagouvCronSchedulableConcern
self.schedule_expression = "every month at 3:00"
diff --git a/app/jobs/cron/datagouv/export_and_publish_demarches_publiques_job.rb b/app/jobs/cron/datagouv/export_and_publish_demarches_publiques_job.rb
index c11b8eec4..7218329bb 100644
--- a/app/jobs/cron/datagouv/export_and_publish_demarches_publiques_job.rb
+++ b/app/jobs/cron/datagouv/export_and_publish_demarches_publiques_job.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Cron::Datagouv::ExportAndPublishDemarchesPubliquesJob < Cron::CronJob
include DatagouvCronSchedulableConcern
- self.schedule_expression = "every month at 4:00"
+ self.schedule_expression = "every month at 4:10"
def perform(*args)
gzip_filepath = [
diff --git a/app/jobs/cron/datagouv/file_by_month_job.rb b/app/jobs/cron/datagouv/file_by_month_job.rb
index 7cf7b0357..51a52a4c8 100644
--- a/app/jobs/cron/datagouv/file_by_month_job.rb
+++ b/app/jobs/cron/datagouv/file_by_month_job.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Cron::Datagouv::FileByMonthJob < Cron::CronJob
include DatagouvCronSchedulableConcern
- self.schedule_expression = "every month at 3:00"
+ self.schedule_expression = "every month at 3:15"
FILE_NAME = "nb_dossiers_crees_par_mois"
def perform(*args)
diff --git a/app/jobs/cron/datagouv/file_depose_by_month_job.rb b/app/jobs/cron/datagouv/file_depose_by_month_job.rb
index 4191356e9..dd3d89922 100644
--- a/app/jobs/cron/datagouv/file_depose_by_month_job.rb
+++ b/app/jobs/cron/datagouv/file_depose_by_month_job.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Cron::Datagouv::FileDeposeByMonthJob < Cron::CronJob
include DatagouvCronSchedulableConcern
- self.schedule_expression = "every month at 3:00"
+ self.schedule_expression = "every month at 5:00"
FILE_NAME = "nb_dossiers_deposes_par_mois"
def perform(*args)
diff --git a/app/jobs/cron/datagouv/instructeur_by_month_job.rb b/app/jobs/cron/datagouv/instructeur_by_month_job.rb
index ea98d5dd9..718937a6d 100644
--- a/app/jobs/cron/datagouv/instructeur_by_month_job.rb
+++ b/app/jobs/cron/datagouv/instructeur_by_month_job.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Cron::Datagouv::InstructeurByMonthJob < Cron::CronJob
include DatagouvCronSchedulableConcern
- self.schedule_expression = "every month at 3:00"
+ self.schedule_expression = "every month at 4:00"
FILE_NAME = "nb_instructeurs_crees_par_mois"
def perform(*args)
diff --git a/app/jobs/cron/datagouv/instructeur_connected_by_month_job.rb b/app/jobs/cron/datagouv/instructeur_connected_by_month_job.rb
index cbcb20e08..97319ace6 100644
--- a/app/jobs/cron/datagouv/instructeur_connected_by_month_job.rb
+++ b/app/jobs/cron/datagouv/instructeur_connected_by_month_job.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Cron::Datagouv::InstructeurConnectedByMonthJob < Cron::CronJob
include DatagouvCronSchedulableConcern
- self.schedule_expression = "every month at 3:00"
+ self.schedule_expression = "every month at 4:45"
FILE_NAME = "nb_instructeurs_connectes_par_mois"
def perform(*args)
diff --git a/app/jobs/cron/datagouv/procedure_by_month_job.rb b/app/jobs/cron/datagouv/procedure_by_month_job.rb
index 2a044cf43..7be1725de 100644
--- a/app/jobs/cron/datagouv/procedure_by_month_job.rb
+++ b/app/jobs/cron/datagouv/procedure_by_month_job.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Cron::Datagouv::ProcedureByMonthJob < Cron::CronJob
include DatagouvCronSchedulableConcern
- self.schedule_expression = "every month at 3:00"
+ self.schedule_expression = "every month at 4:15"
FILE_NAME = "nb_procedures_creees_par_mois"
def perform(*args)
diff --git a/app/jobs/cron/datagouv/procedure_closed_by_month_job.rb b/app/jobs/cron/datagouv/procedure_closed_by_month_job.rb
index 315aa7474..b42ebab5a 100644
--- a/app/jobs/cron/datagouv/procedure_closed_by_month_job.rb
+++ b/app/jobs/cron/datagouv/procedure_closed_by_month_job.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Cron::Datagouv::ProcedureClosedByMonthJob < Cron::CronJob
include DatagouvCronSchedulableConcern
- self.schedule_expression = "every month at 3:00"
+ self.schedule_expression = "every month at 4:00"
FILE_NAME = "nb_procedures_closes_par_mois"
def perform(*args)
diff --git a/app/jobs/cron/datagouv/procedure_deleted_by_month_job.rb b/app/jobs/cron/datagouv/procedure_deleted_by_month_job.rb
index 17bdccf63..a52ec15bc 100644
--- a/app/jobs/cron/datagouv/procedure_deleted_by_month_job.rb
+++ b/app/jobs/cron/datagouv/procedure_deleted_by_month_job.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Cron::Datagouv::ProcedureDeletedByMonthJob < Cron::CronJob
include DatagouvCronSchedulableConcern
- self.schedule_expression = "every month at 3:00"
+ self.schedule_expression = "every month at 3:30"
FILE_NAME = "nb_procedures_supprimees_par_mois"
def perform(*args)
diff --git a/app/jobs/cron/datagouv/user_connected_with_france_connect_by_month_job.rb b/app/jobs/cron/datagouv/user_connected_with_france_connect_by_month_job.rb
index a4aea9be7..2938474ce 100644
--- a/app/jobs/cron/datagouv/user_connected_with_france_connect_by_month_job.rb
+++ b/app/jobs/cron/datagouv/user_connected_with_france_connect_by_month_job.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Cron::Datagouv::UserConnectedWithFranceConnectByMonthJob < Cron::CronJob
include DatagouvCronSchedulableConcern
- self.schedule_expression = "every month at 3:00"
+ self.schedule_expression = "every month at 3:45"
FILE_NAME = "nb_utilisateurs_connectes_france_connect_par_mois"
def perform(*args)
diff --git a/app/jobs/cron/discarded_dossiers_deletion_job.rb b/app/jobs/cron/discarded_dossiers_deletion_job.rb
index 9db9623bf..82490e3d4 100644
--- a/app/jobs/cron/discarded_dossiers_deletion_job.rb
+++ b/app/jobs/cron/discarded_dossiers_deletion_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::DiscardedDossiersDeletionJob < Cron::CronJob
- self.schedule_expression = "every day at 2 am"
+ self.schedule_expression = "every day at 02:00"
def perform
Dossier.purge_discarded
diff --git a/app/jobs/cron/discarded_procedures_deletion_job.rb b/app/jobs/cron/discarded_procedures_deletion_job.rb
index d67cde8a9..be740796e 100644
--- a/app/jobs/cron/discarded_procedures_deletion_job.rb
+++ b/app/jobs/cron/discarded_procedures_deletion_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::DiscardedProceduresDeletionJob < Cron::CronJob
- self.schedule_expression = "every day at 1 am"
+ self.schedule_expression = "every day at 00:45"
def perform
Procedure.purge_discarded
diff --git a/app/jobs/cron/dossier_operation_log_move_to_cold_storage_job.rb b/app/jobs/cron/dossier_operation_log_move_to_cold_storage_job.rb
index e4159317f..221f9bdeb 100644
--- a/app/jobs/cron/dossier_operation_log_move_to_cold_storage_job.rb
+++ b/app/jobs/cron/dossier_operation_log_move_to_cold_storage_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::DossierOperationLogMoveToColdStorageJob < Cron::CronJob
- self.schedule_expression = "every day at 1 am"
+ self.schedule_expression = "every day at 23:00"
def perform
DossierOperationLog
diff --git a/app/jobs/cron/enable_procedure_expires_when_termine_enabled_job.rb b/app/jobs/cron/enable_procedure_expires_when_termine_enabled_job.rb
index a61be7d39..1e2d89666 100644
--- a/app/jobs/cron/enable_procedure_expires_when_termine_enabled_job.rb
+++ b/app/jobs/cron/enable_procedure_expires_when_termine_enabled_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::EnableProcedureExpiresWhenTermineEnabledJob < Cron::CronJob
self.schedule_expression = Expired.schedule_at(self)
discard_on StandardError
diff --git a/app/jobs/cron/expired_dossiers_brouillon_deletion_job.rb b/app/jobs/cron/expired_dossiers_brouillon_deletion_job.rb
index cc8812352..35e957ed6 100644
--- a/app/jobs/cron/expired_dossiers_brouillon_deletion_job.rb
+++ b/app/jobs/cron/expired_dossiers_brouillon_deletion_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::ExpiredDossiersBrouillonDeletionJob < Cron::CronJob
self.schedule_expression = Expired.schedule_at(self)
diff --git a/app/jobs/cron/expired_dossiers_en_construction_deletion_job.rb b/app/jobs/cron/expired_dossiers_en_construction_deletion_job.rb
index 36ad0fb96..552592db2 100644
--- a/app/jobs/cron/expired_dossiers_en_construction_deletion_job.rb
+++ b/app/jobs/cron/expired_dossiers_en_construction_deletion_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::ExpiredDossiersEnConstructionDeletionJob < Cron::CronJob
self.schedule_expression = Expired.schedule_at(self)
diff --git a/app/jobs/cron/expired_dossiers_termine_deletion_job.rb b/app/jobs/cron/expired_dossiers_termine_deletion_job.rb
index 3e69cb788..ed4f0f8ee 100644
--- a/app/jobs/cron/expired_dossiers_termine_deletion_job.rb
+++ b/app/jobs/cron/expired_dossiers_termine_deletion_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::ExpiredDossiersTermineDeletionJob < Cron::CronJob
self.schedule_expression = Expired.schedule_at(self)
diff --git a/app/jobs/cron/expired_prefilled_dossiers_deletion_job.rb b/app/jobs/cron/expired_prefilled_dossiers_deletion_job.rb
index d825ba742..f1b6e8c72 100644
--- a/app/jobs/cron/expired_prefilled_dossiers_deletion_job.rb
+++ b/app/jobs/cron/expired_prefilled_dossiers_deletion_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::ExpiredPrefilledDossiersDeletionJob < Cron::CronJob
self.schedule_expression = Expired.schedule_at(self)
diff --git a/app/jobs/cron/expired_users_deletion_job.rb b/app/jobs/cron/expired_users_deletion_job.rb
index 1dd923a81..d43e2f926 100644
--- a/app/jobs/cron/expired_users_deletion_job.rb
+++ b/app/jobs/cron/expired_users_deletion_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::ExpiredUsersDeletionJob < Cron::CronJob
self.schedule_expression = Expired.schedule_at(self)
discard_on StandardError
diff --git a/app/jobs/cron/fix_missing_antivirus_analysis_job.rb b/app/jobs/cron/fix_missing_antivirus_analysis_job.rb
index 52bb90848..5540c6c9e 100644
--- a/app/jobs/cron/fix_missing_antivirus_analysis_job.rb
+++ b/app/jobs/cron/fix_missing_antivirus_analysis_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::FixMissingAntivirusAnalysisJob < Cron::CronJob
- self.schedule_expression = "every day at 2 am"
+ self.schedule_expression = "every day at 01:45"
def perform
ActiveStorage::Blob.where(virus_scan_result: ActiveStorage::VirusScanner::PENDING).find_each do |blob|
diff --git a/app/jobs/cron/instructeur_email_notification_job.rb b/app/jobs/cron/instructeur_email_notification_job.rb
index 756a8b95b..6553629e4 100644
--- a/app/jobs/cron/instructeur_email_notification_job.rb
+++ b/app/jobs/cron/instructeur_email_notification_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::InstructeurEmailNotificationJob < Cron::CronJob
self.schedule_expression = "from monday through friday at 9 am"
diff --git a/app/jobs/cron/notify_draft_not_submitted_job.rb b/app/jobs/cron/notify_draft_not_submitted_job.rb
index 24100930a..702bf2cc2 100644
--- a/app/jobs/cron/notify_draft_not_submitted_job.rb
+++ b/app/jobs/cron/notify_draft_not_submitted_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::NotifyDraftNotSubmittedJob < Cron::CronJob
self.schedule_expression = "from monday through friday at 7 am"
diff --git a/app/jobs/cron/operations_signature_job.rb b/app/jobs/cron/operations_signature_job.rb
index 605cea333..ae08c0db0 100644
--- a/app/jobs/cron/operations_signature_job.rb
+++ b/app/jobs/cron/operations_signature_job.rb
@@ -1,15 +1,20 @@
+# frozen_string_literal: true
+
class Cron::OperationsSignatureJob < Cron::CronJob
- self.schedule_expression = "every day at 6 am"
+ self.schedule_expression = "every day at 06:00"
def perform(*args)
+ start_date = DossierOperationLog.where(bill_signature: nil).order(:executed_at).pick(:executed_at).beginning_of_day
last_midnight = Time.zone.today.beginning_of_day
- operations_by_day = BillSignatureService.grouped_unsigned_operation_until(last_midnight)
- operations_by_day.each do |day, operations|
- begin
- BillSignatureService.sign_operations(operations, day)
- rescue
- raise # let errors show up on Sentry and delayed_jobs
- end
+
+ while start_date < last_midnight
+ operations = DossierOperationLog
+ .select(:id, :digest)
+ .where(executed_at: start_date...start_date.tomorrow, bill_signature: nil)
+
+ BillSignatureService.sign_operations(operations, start_date) if operations.present?
+
+ start_date = start_date.tomorrow
end
end
end
diff --git a/app/jobs/cron/procedure_external_url_check_job.rb b/app/jobs/cron/procedure_external_url_check_job.rb
index ab18c8c37..d81bece90 100644
--- a/app/jobs/cron/procedure_external_url_check_job.rb
+++ b/app/jobs/cron/procedure_external_url_check_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::ProcedureExternalURLCheckJob < Cron::CronJob
- self.schedule_expression = "every week on monday at 1 am"
+ self.schedule_expression = "every week on monday at 01:00"
def perform
Procedure.with_external_urls.find_each { ::ProcedureExternalURLCheckJob.perform_later(_1) }
diff --git a/app/jobs/cron/procedure_process_sva_svr_job.rb b/app/jobs/cron/procedure_process_sva_svr_job.rb
index cda3e8468..30f8c84a6 100644
--- a/app/jobs/cron/procedure_process_sva_svr_job.rb
+++ b/app/jobs/cron/procedure_process_sva_svr_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::ProcedureProcessSVASVRJob < Cron::CronJob
- self.schedule_expression = "every day at 1:00"
+ self.schedule_expression = "every day at 01:15"
def perform
Procedure.sva_svr.find_each do |procedure|
diff --git a/app/jobs/cron/purge_manager_administrateur_sessions_job.rb b/app/jobs/cron/purge_manager_administrateur_sessions_job.rb
index b85714dd5..e204546f1 100644
--- a/app/jobs/cron/purge_manager_administrateur_sessions_job.rb
+++ b/app/jobs/cron/purge_manager_administrateur_sessions_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::PurgeManagerAdministrateurSessionsJob < Cron::CronJob
- self.schedule_expression = "every day at 3 am"
+ self.schedule_expression = "every day at 02:45"
def perform
# TODO: add id column to administrateurs_procedures and use destroy_all
diff --git a/app/jobs/cron/purge_old_email_event_job.rb b/app/jobs/cron/purge_old_email_event_job.rb
index 5de61c5c0..5b22f9094 100644
--- a/app/jobs/cron/purge_old_email_event_job.rb
+++ b/app/jobs/cron/purge_old_email_event_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::PurgeOldEmailEventJob < Cron::CronJob
self.schedule_expression = "every week at 3:00"
diff --git a/app/jobs/cron/purge_old_sib_mails_job.rb b/app/jobs/cron/purge_old_sib_mails_job.rb
index 3ed3685a9..054112cd2 100644
--- a/app/jobs/cron/purge_old_sib_mails_job.rb
+++ b/app/jobs/cron/purge_old_sib_mails_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::PurgeOldSibMailsJob < Cron::CronJob
- self.schedule_expression = "every day at midnight"
+ self.schedule_expression = "every day at 00:15"
def perform
sib = Sendinblue::API.new
diff --git a/app/jobs/cron/purge_stale_archives_job.rb b/app/jobs/cron/purge_stale_archives_job.rb
index 196002d69..c315dc843 100644
--- a/app/jobs/cron/purge_stale_archives_job.rb
+++ b/app/jobs/cron/purge_stale_archives_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::PurgeStaleArchivesJob < Cron::CronJob
self.schedule_expression = "every 5 minutes"
diff --git a/app/jobs/cron/purge_stale_batch_operation_job.rb b/app/jobs/cron/purge_stale_batch_operation_job.rb
index 822553a13..e23baf053 100644
--- a/app/jobs/cron/purge_stale_batch_operation_job.rb
+++ b/app/jobs/cron/purge_stale_batch_operation_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::PurgeStaleBatchOperationJob < Cron::CronJob
self.schedule_expression = "every 5 minutes"
diff --git a/app/jobs/cron/purge_stale_exports_job.rb b/app/jobs/cron/purge_stale_exports_job.rb
index 20462ce0e..a5ab2561e 100644
--- a/app/jobs/cron/purge_stale_exports_job.rb
+++ b/app/jobs/cron/purge_stale_exports_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::PurgeStaleExportsJob < Cron::CronJob
self.schedule_expression = "every 5 minutes"
diff --git a/app/jobs/cron/purge_stale_transfers_job.rb b/app/jobs/cron/purge_stale_transfers_job.rb
index bf54c1c81..6472f8298 100644
--- a/app/jobs/cron/purge_stale_transfers_job.rb
+++ b/app/jobs/cron/purge_stale_transfers_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::PurgeStaleTransfersJob < Cron::CronJob
- self.schedule_expression = "every day at midnight"
+ self.schedule_expression = "every day at 00:00"
def perform
DossierTransfer.destroy_stale
diff --git a/app/jobs/cron/purge_unattached_blobs_job.rb b/app/jobs/cron/purge_unattached_blobs_job.rb
index 218b6f5d2..5a324b8b8 100644
--- a/app/jobs/cron/purge_unattached_blobs_job.rb
+++ b/app/jobs/cron/purge_unattached_blobs_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::PurgeUnattachedBlobsJob < Cron::CronJob
- self.schedule_expression = "every day at midnight"
+ self.schedule_expression = "every day at 00:30"
def perform
# .in_batches { _1.each... } is more efficient in this case that in_batches.each_record or find_each
diff --git a/app/jobs/cron/purge_unused_admin_job.rb b/app/jobs/cron/purge_unused_admin_job.rb
index d58f479d4..c196f6147 100644
--- a/app/jobs/cron/purge_unused_admin_job.rb
+++ b/app/jobs/cron/purge_unused_admin_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::PurgeUnusedAdminJob < Cron::CronJob
- self.schedule_expression = "every monday at 5 am"
+ self.schedule_expression = "every monday at 5:15"
def perform(*args)
Administrateur.unused.destroy_all
diff --git a/app/jobs/cron/release_crashed_export_job.rb b/app/jobs/cron/release_crashed_export_job.rb
index 6a36237a1..23adf0d2f 100644
--- a/app/jobs/cron/release_crashed_export_job.rb
+++ b/app/jobs/cron/release_crashed_export_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::ReleaseCrashedExportJob < Cron::CronJob
self.schedule_expression = "every 10 minute"
SECSCAN_LIMIT = 20_000
diff --git a/app/jobs/cron/send_api_token_expiration_notice_job.rb b/app/jobs/cron/send_api_token_expiration_notice_job.rb
index 48d69c0d8..9c864a8da 100644
--- a/app/jobs/cron/send_api_token_expiration_notice_job.rb
+++ b/app/jobs/cron/send_api_token_expiration_notice_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::SendAPITokenExpirationNoticeJob < Cron::CronJob
- self.schedule_expression = "every day at midnight"
+ self.schedule_expression = "every day at 23:45"
def perform
windows = [
diff --git a/app/jobs/cron/stalled_declarative_procedures_job.rb b/app/jobs/cron/stalled_declarative_procedures_job.rb
index 9e4e60882..7616e19c2 100644
--- a/app/jobs/cron/stalled_declarative_procedures_job.rb
+++ b/app/jobs/cron/stalled_declarative_procedures_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::StalledDeclarativeProceduresJob < Cron::CronJob
self.schedule_expression = "every 10 minutes"
diff --git a/app/jobs/cron/update_stats_job.rb b/app/jobs/cron/update_stats_job.rb
index a5dcd2c43..aedd5792c 100644
--- a/app/jobs/cron/update_stats_job.rb
+++ b/app/jobs/cron/update_stats_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cron::UpdateStatsJob < Cron::CronJob
self.schedule_expression = "every 1 hour"
diff --git a/app/jobs/cron/weekly_overview_job.rb b/app/jobs/cron/weekly_overview_job.rb
index 45408c58d..0524a6eac 100644
--- a/app/jobs/cron/weekly_overview_job.rb
+++ b/app/jobs/cron/weekly_overview_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Cron::WeeklyOverviewJob < Cron::CronJob
- self.schedule_expression = "every monday at 4 am"
+ self.schedule_expression = "every monday at 04:05"
def perform
# Feature flipped to avoid mails in staging due to unprocessed dossier
diff --git a/app/jobs/destroy_record_later_job.rb b/app/jobs/destroy_record_later_job.rb
index 6b9def26f..426c967a7 100644
--- a/app/jobs/destroy_record_later_job.rb
+++ b/app/jobs/destroy_record_later_job.rb
@@ -1,5 +1,8 @@
+# frozen_string_literal: true
+
class DestroyRecordLaterJob < ApplicationJob
discard_on ActiveRecord::RecordNotFound
+ queue_as :low # destroy later, will be done when possible
def perform(record)
record.destroy
diff --git a/app/jobs/dolist_report_job.rb b/app/jobs/dolist_report_job.rb
index 8a7b38efe..60e1be5bf 100644
--- a/app/jobs/dolist_report_job.rb
+++ b/app/jobs/dolist_report_job.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
class DolistReportJob < ApplicationJob
# Consolidate random recent emails dispatched to Dolist with their statuses
# and send a report by email.
+ queue_as :low # reporting will be done asap
def perform(report_to, sample_size = 1000)
events = EmailEvent.dolist.dispatched.where(processed_at: 2.weeks.ago..).order("RANDOM()").limit(sample_size)
diff --git a/app/jobs/dossier_index_search_terms_job.rb b/app/jobs/dossier_index_search_terms_job.rb
new file mode 100644
index 000000000..dc0079a5a
--- /dev/null
+++ b/app/jobs/dossier_index_search_terms_job.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class DossierIndexSearchTermsJob < ApplicationJob
+ queue_as :low
+
+ discard_on ActiveRecord::RecordNotFound
+
+ def perform(dossier)
+ dossier.index_search_terms
+ end
+end
diff --git a/app/jobs/dossier_operation_log_move_to_cold_storage_batch_job.rb b/app/jobs/dossier_operation_log_move_to_cold_storage_batch_job.rb
index 9c6e561bb..d2fb60224 100644
--- a/app/jobs/dossier_operation_log_move_to_cold_storage_batch_job.rb
+++ b/app/jobs/dossier_operation_log_move_to_cold_storage_batch_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class DossierOperationLogMoveToColdStorageBatchJob < ApplicationJob
- queue_as :low_priority
+ queue_as :low
def perform(ids)
DossierOperationLog.where(id: ids)
diff --git a/app/jobs/dossier_rebase_job.rb b/app/jobs/dossier_rebase_job.rb
index 50d1c7d36..2ca0eb8c9 100644
--- a/app/jobs/dossier_rebase_job.rb
+++ b/app/jobs/dossier_rebase_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class DossierRebaseJob < ApplicationJob
- queue_as :low_priority # they are massively enqueued, so don't interfere with others especially antivirus
+ queue_as :low # they are massively enqueued, so don't interfere with others especially antivirus
# If by the time the job runs the Dossier has been deleted, ignore the rebase
discard_on ActiveRecord::RecordNotFound
diff --git a/app/jobs/dossier_update_search_terms_job.rb b/app/jobs/dossier_update_search_terms_job.rb
deleted file mode 100644
index 8b997e3f5..000000000
--- a/app/jobs/dossier_update_search_terms_job.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class DossierUpdateSearchTermsJob < ApplicationJob
- discard_on ActiveRecord::RecordNotFound
-
- def perform(dossier)
- dossier.update_search_terms
- dossier.save!(touch: false)
- end
-end
diff --git a/app/jobs/etablissement_update_job.rb b/app/jobs/etablissement_update_job.rb
index e8eb2b12b..f5eaacba0 100644
--- a/app/jobs/etablissement_update_job.rb
+++ b/app/jobs/etablissement_update_job.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class EtablissementUpdateJob < ApplicationJob
+ queue_as :critical # reporting will be done asap, but no occurence found. maube dead?
def perform(dossier, siret)
begin
etablissement_attributes = APIEntrepriseService.get_etablissement_params_for_siret(siret, dossier.procedure.id)
diff --git a/app/jobs/export_job.rb b/app/jobs/export_job.rb
index 0ea91c844..40eb25d75 100644
--- a/app/jobs/export_job.rb
+++ b/app/jobs/export_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExportJob < ApplicationJob
queue_as :exports
@@ -10,6 +12,8 @@ class ExportJob < ApplicationJob
def perform(export)
return if export.generated?
+ Sentry.set_tags(procedure: export.procedure.id)
+
export.compute_with_safe_stale_for_purge do
export.compute
end
diff --git a/app/jobs/helpscout_create_conversation_job.rb b/app/jobs/helpscout_create_conversation_job.rb
new file mode 100644
index 000000000..8239898c7
--- /dev/null
+++ b/app/jobs/helpscout_create_conversation_job.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+class HelpscoutCreateConversationJob < ApplicationJob
+ queue_as :critical # user feedback is critical
+
+ def max_attempts = 15 # ~10h
+
+ class FileNotScannedYetError < StandardError
+ end
+
+ retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10
+
+ attr_reader :contact_form
+ attr_reader :api
+
+ def perform(contact_form)
+ @contact_form = contact_form
+
+ if contact_form.piece_jointe.attached?
+ raise FileNotScannedYetError if contact_form.piece_jointe.virus_scanner.pending?
+ end
+
+ @api = Helpscout::API.new
+
+ create_conversation
+
+ contact_form.delete
+ rescue StandardError
+ contact_form.delete if executions >= max_attempts
+
+ raise
+ end
+
+ private
+
+ def create_conversation
+ response = api.create_conversation(
+ contact_form.email.presence || contact_form.user.email,
+ contact_form.subject,
+ contact_form.text,
+ safe_blob
+ )
+
+ if response.success?
+ conversation_id = response.headers['Resource-ID']
+
+ if contact_form.phone.present?
+ api.add_phone_number(contact_form.email, contact_form.phone)
+ end
+
+ api.add_tags(conversation_id, contact_form.tags)
+ else
+ fail "Error while creating conversation: #{response.response_code} '#{response.body}'"
+ end
+ end
+
+ def safe_blob
+ return if !contact_form.piece_jointe.virus_scanner&.safe?
+ return if contact_form.piece_jointe.byte_size.zero? # HS don't support empty attachment
+
+ contact_form.piece_jointe
+ end
+end
diff --git a/app/jobs/image_processor_job.rb b/app/jobs/image_processor_job.rb
new file mode 100644
index 000000000..0a4d3ee85
--- /dev/null
+++ b/app/jobs/image_processor_job.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+class ImageProcessorJob < ApplicationJob
+ queue_as :low # thumbnails and watermarks. Execution depends of virus scanner which is more urgent
+
+ class FileNotScannedYetError < StandardError
+ end
+
+ # If by the time the job runs the blob has been deleted, ignore the error
+ discard_on ActiveRecord::RecordNotFound
+ # If the file is deleted during the scan, ignore the error
+ discard_on ActiveStorage::FileNotFoundError
+ discard_on ActiveRecord::InvalidForeignKey
+ # If the file is not an image, not in format we can process or the image is corrupted, ignore the error
+ DISCARDABLE_ERRORS = [
+ 'improper image header',
+ 'width or height exceeds limit',
+ 'attempt to perform an operation not allowed by the security policy',
+ 'no decode delegate for this image format'
+ ]
+ discard_on do |_, error|
+ DISCARDABLE_ERRORS.any? { error.message.match?(_1) }
+ end
+ # If the file is not analyzed or scanned for viruses yet, retry later
+ # (to avoid modifying the file while it is being scanned).
+ retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10
+
+ # Usually invalid image or ImageMagick decoder blocked for this format
+ retry_on MiniMagick::Invalid, attempts: 3
+ retry_on MiniMagick::Error, attempts: 3
+
+ rescue_from ActiveStorage::PreviewError do
+ retry_or_discard
+ end
+
+ def perform(blob)
+ return if blob.nil?
+ raise FileNotScannedYetError if blob.virus_scanner.pending?
+ return if ActiveStorage::Attachment.find_by(blob_id: blob.id)&.record_type == "ActiveStorage::VariantRecord"
+
+ auto_rotate(blob) if ["image/jpeg", "image/jpg"].include?(blob.content_type)
+ uninterlace(blob) if blob.content_type == "image/png"
+ create_representations(blob) if blob.representation_required?
+ add_watermark(blob) if blob.watermark_pending?
+ end
+
+ private
+
+ def auto_rotate(blob)
+ blob.open do |file|
+ Tempfile.create(["rotated", File.extname(file)]) do |output|
+ processed = AutoRotateService.new.process(file, output)
+ return if processed.blank?
+
+ blob.upload(processed) # also update checksum & byte_size accordingly
+ blob.save!
+ end
+ end
+ end
+
+ def uninterlace(blob)
+ blob.open do |file|
+ processed = UninterlaceService.new.process(file)
+ return if processed.blank?
+
+ blob.upload(processed)
+ blob.save!
+ end
+ end
+
+ def create_representations(blob)
+ blob.attachments.each do |attachment|
+ next unless attachment&.representable?
+ attachment.representation(resize_to_limit: [400, 400]).processed
+ if attachment.blob.content_type.in?(RARE_IMAGE_TYPES)
+ attachment.variant(resize_to_limit: [2000, 2000]).processed
+ end
+ if attachment.record.class == ActionText::RichText
+ attachment.variant(resize_to_limit: [1024, 768]).processed
+ end
+ end
+ end
+
+ def add_watermark(blob)
+ return if blob.watermark_done?
+
+ blob.open do |file|
+ Tempfile.create(["watermarked", File.extname(file)]) do |output|
+ processed = WatermarkService.new.process(file, output)
+ return if processed.blank?
+
+ blob.upload(processed) # also update checksum & byte_size accordingly
+ blob.watermarked_at = Time.current
+ blob.save!
+ end
+ end
+ end
+
+ def retry_or_discard
+ if executions < 3
+ retry_job wait: 5.minutes
+ end
+ end
+end
diff --git a/app/jobs/migrations/backfill_dossier_repetition_job.rb b/app/jobs/migrations/backfill_dossier_repetition_job.rb
index 6c03b5dc5..4b19dca61 100644
--- a/app/jobs/migrations/backfill_dossier_repetition_job.rb
+++ b/app/jobs/migrations/backfill_dossier_repetition_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Migrations::BackfillDossierRepetitionJob < ApplicationJob
def perform(dossier_ids)
Dossier.where(id: dossier_ids)
@@ -7,10 +9,10 @@ class Migrations::BackfillDossierRepetitionJob < ApplicationJob
.revision
.types_de_champ
.filter do |type_de_champ|
- type_de_champ.type_champ == 'repetition' && dossier.champs.none? { _1.type_de_champ_id == type_de_champ.id }
+ type_de_champ.type_champ == 'repetition' && dossier.champs.none? { _1.stable_id == type_de_champ.stable_id }
end
.each do |type_de_champ|
- dossier.champs << type_de_champ.champ.build
+ dossier.champs << type_de_champ.build_champ
end
end
end
diff --git a/app/jobs/migrations/backfill_row_id_job.rb b/app/jobs/migrations/backfill_row_id_job.rb
index 30abf408c..daedd6ec8 100644
--- a/app/jobs/migrations/backfill_row_id_job.rb
+++ b/app/jobs/migrations/backfill_row_id_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Migrations::BackfillRowIdJob < ApplicationJob
def perform(batch)
batch.each do |(row_id, champ_ids)|
diff --git a/app/jobs/migrations/backfill_stable_id_job.rb b/app/jobs/migrations/backfill_stable_id_job.rb
index aa23352ff..6cc33fcc9 100644
--- a/app/jobs/migrations/backfill_stable_id_job.rb
+++ b/app/jobs/migrations/backfill_stable_id_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Migrations::BackfillStableIdJob < ApplicationJob
- queue_as :low_priority
+ queue_as :low
DEFAULT_LIMIT = 50_000
diff --git a/app/jobs/migrations/backfill_virus_scan_blobs_job.rb b/app/jobs/migrations/backfill_virus_scan_blobs_job.rb
index f9ba1b002..ca327aff3 100644
--- a/app/jobs/migrations/backfill_virus_scan_blobs_job.rb
+++ b/app/jobs/migrations/backfill_virus_scan_blobs_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Migrations::BackfillVirusScanBlobsJob < ApplicationJob
def perform(batch)
ActiveStorage::Blob.where(id: batch)
diff --git a/app/jobs/migrations/batch_update_datetime_values_job.rb b/app/jobs/migrations/batch_update_datetime_values_job.rb
index 5ef4207aa..fb9b8a39d 100644
--- a/app/jobs/migrations/batch_update_datetime_values_job.rb
+++ b/app/jobs/migrations/batch_update_datetime_values_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Migrations::BatchUpdateDatetimeValuesJob < ApplicationJob
def perform(ids)
Champs::DatetimeChamp.where(id: ids).find_each do |datetime_champ|
diff --git a/app/jobs/migrations/batch_update_pays_values_job.rb b/app/jobs/migrations/batch_update_pays_values_job.rb
index 01add5af8..225bb88e5 100644
--- a/app/jobs/migrations/batch_update_pays_values_job.rb
+++ b/app/jobs/migrations/batch_update_pays_values_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Migrations::BatchUpdatePaysValuesJob < ApplicationJob
UNUSUAL_COUNTRY_NAME_MATCHER = {
"ACORES, MADERE" => "Portugal",
diff --git a/app/jobs/migrations/normalize_communes_job.rb b/app/jobs/migrations/normalize_communes_job.rb
index 62a43771f..2830d5886 100644
--- a/app/jobs/migrations/normalize_communes_job.rb
+++ b/app/jobs/migrations/normalize_communes_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Migrations::NormalizeCommunesJob < ApplicationJob
def perform(ids)
Champs::CommuneChamp.where(id: ids).find_each do |champ|
diff --git a/app/jobs/migrations/normalize_departements_with_empty_external_id_job.rb b/app/jobs/migrations/normalize_departements_with_empty_external_id_job.rb
index 00a8c9e5c..ddb3b7af9 100644
--- a/app/jobs/migrations/normalize_departements_with_empty_external_id_job.rb
+++ b/app/jobs/migrations/normalize_departements_with_empty_external_id_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Migrations::NormalizeDepartementsWithEmptyExternalIdJob < ApplicationJob
def perform(ids)
Champs::DepartementChamp.where(id: ids).find_each do |champ|
diff --git a/app/jobs/migrations/normalize_departements_with_nil_external_id_job.rb b/app/jobs/migrations/normalize_departements_with_nil_external_id_job.rb
index 109ca6c9a..d7cc508e9 100644
--- a/app/jobs/migrations/normalize_departements_with_nil_external_id_job.rb
+++ b/app/jobs/migrations/normalize_departements_with_nil_external_id_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Migrations::NormalizeDepartementsWithNilExternalIdJob < ApplicationJob
def perform(ids)
Champs::DepartementChamp.where(id: ids).find_each do |champ|
diff --git a/app/jobs/migrations/normalize_departements_with_present_external_id_job.rb b/app/jobs/migrations/normalize_departements_with_present_external_id_job.rb
index a024582d2..4547396a6 100644
--- a/app/jobs/migrations/normalize_departements_with_present_external_id_job.rb
+++ b/app/jobs/migrations/normalize_departements_with_present_external_id_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Migrations::NormalizeDepartementsWithPresentExternalIdJob < ApplicationJob
def perform(ids)
Champs::DepartementChamp.where(id: ids).find_each do |champ|
diff --git a/app/jobs/priorized_mail_delivery_job.rb b/app/jobs/priorized_mail_delivery_job.rb
index 8ebe5549c..bf0ea10b7 100644
--- a/app/jobs/priorized_mail_delivery_job.rb
+++ b/app/jobs/priorized_mail_delivery_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PriorizedMailDeliveryJob < ActionMailer::MailDeliveryJob
discard_on ActiveJob::DeserializationError
@@ -11,6 +13,6 @@ class PriorizedMailDeliveryJob < ActionMailer::MailDeliveryJob
end
def custom_queue
- ENV.fetch('BULK_EMAIL_QUEUE') { Rails.application.config.action_mailer.deliver_later_queue_name }
+ 'default'
end
end
diff --git a/app/jobs/procedure_external_url_check_job.rb b/app/jobs/procedure_external_url_check_job.rb
index c0678e882..78a25582d 100644
--- a/app/jobs/procedure_external_url_check_job.rb
+++ b/app/jobs/procedure_external_url_check_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProcedureExternalURLCheckJob < ApplicationJob
def perform(procedure)
procedure.validate
diff --git a/app/jobs/procedure_sva_svr_process_dossier_job.rb b/app/jobs/procedure_sva_svr_process_dossier_job.rb
index 12fb06604..48a8d7634 100644
--- a/app/jobs/procedure_sva_svr_process_dossier_job.rb
+++ b/app/jobs/procedure_sva_svr_process_dossier_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class ProcedureSVASVRProcessDossierJob < ApplicationJob
- queue_as :sva
+ queue_as :critical
def perform(dossier)
dossier.process_sva_svr!
diff --git a/app/jobs/process_stalled_declarative_dossier_job.rb b/app/jobs/process_stalled_declarative_dossier_job.rb
index 1e2481688..aaa49fb70 100644
--- a/app/jobs/process_stalled_declarative_dossier_job.rb
+++ b/app/jobs/process_stalled_declarative_dossier_job.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class ProcessStalledDeclarativeDossierJob < ApplicationJob
+ queue_as :low
def perform(dossier)
return if dossier.declarative_triggered_at.present?
diff --git a/app/jobs/reset_expiring_dossiers_job.rb b/app/jobs/reset_expiring_dossiers_job.rb
index 6ab941bf3..95a87f571 100644
--- a/app/jobs/reset_expiring_dossiers_job.rb
+++ b/app/jobs/reset_expiring_dossiers_job.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class ResetExpiringDossiersJob < ApplicationJob
+ queue_as :low
def perform(procedure)
procedure
.dossiers
diff --git a/app/jobs/send_closing_notification_job.rb b/app/jobs/send_closing_notification_job.rb
index 919b6baa6..f4850b870 100644
--- a/app/jobs/send_closing_notification_job.rb
+++ b/app/jobs/send_closing_notification_job.rb
@@ -1,4 +1,8 @@
+# frozen_string_literal: true
+
class SendClosingNotificationJob < ApplicationJob
+ queue_as :low # no rush on this one
+
def perform(user_ids, content, procedure)
User.where(id: user_ids).find_each do |user|
Expired::MailRateLimiter.new().send_with_delay(UserMailer.notify_after_closing(user, content, @procedure))
diff --git a/app/jobs/sidekiq_again_job.rb b/app/jobs/sidekiq_again_job.rb
deleted file mode 100644
index d819ed936..000000000
--- a/app/jobs/sidekiq_again_job.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class SidekiqAgainJob < ApplicationJob
- self.queue_adapter = :sidekiq
- queue_as :default
-
- def perform(user, with_exception: false)
- if with_exception
- raise 'Nop'
- end
- Sentry.capture_message('this is a message from sidekiq')
- UserMailer.new_account_warning(user).deliver_now
- end
-end
diff --git a/app/jobs/titre_identite_watermark_job.rb b/app/jobs/titre_identite_watermark_job.rb
deleted file mode 100644
index 1b261e291..000000000
--- a/app/jobs/titre_identite_watermark_job.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-class TitreIdentiteWatermarkJob < ApplicationJob
- class FileNotScannedYetError < StandardError
- end
-
- # If by the time the job runs the blob has been deleted, ignore the error
- discard_on ActiveRecord::RecordNotFound
- # If the file is deleted during the scan, ignore the error
- discard_on ActiveStorage::FileNotFoundError
- # If the file is not analyzed or scanned for viruses yet, retry later
- # (to avoid modifying the file while it is being scanned).
- retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10
-
- def perform(blob)
- return if blob.watermark_done?
- raise FileNotScannedYetError if blob.virus_scanner.pending?
-
- blob.open do |file|
- Tempfile.create(["watermarked", File.extname(file)]) do |output|
- processed = WatermarkService.new.process(file, output)
- return if processed.blank?
-
- blob.upload(processed) # also update checksum & byte_size accordingly
- blob.watermarked_at = Time.current
- blob.save!
- end
- end
- end
-end
diff --git a/app/jobs/virus_scanner_job.rb b/app/jobs/virus_scanner_job.rb
index 3d2ea0944..34d20930c 100644
--- a/app/jobs/virus_scanner_job.rb
+++ b/app/jobs/virus_scanner_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class VirusScannerJob < ApplicationJob
# If by the time the job runs the blob has been deleted, ignore the error
discard_on ActiveRecord::RecordNotFound
diff --git a/app/jobs/web_hook_job.rb b/app/jobs/web_hook_job.rb
index 2b9f811c5..40b37c837 100644
--- a/app/jobs/web_hook_job.rb
+++ b/app/jobs/web_hook_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class WebHookJob < ApplicationJob
- queue_as :webhooks_v1
+ queue_as :default
TIMEOUT = 10
diff --git a/app/lib/active_job/application_log_subscriber.rb b/app/lib/active_job/application_log_subscriber.rb
index 23d31f072..c4ee3a23f 100644
--- a/app/lib/active_job/application_log_subscriber.rb
+++ b/app/lib/active_job/application_log_subscriber.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'active_job/logging'
require 'logstash-event'
@@ -33,6 +35,7 @@ class ActiveJob::ApplicationLogSubscriber < ::ActiveJob::LogSubscriber
def process_event(event, type)
data = extract_metadata(event)
data.merge!(extract_exception(event))
+ data[:request_id] = Current.request_id if Current.request_id.present?
case type
when 'enqueue_at'
diff --git a/app/lib/active_job/retry_on_transient_errors.rb b/app/lib/active_job/retry_on_transient_errors.rb
index cf714174a..d75af1e1a 100644
--- a/app/lib/active_job/retry_on_transient_errors.rb
+++ b/app/lib/active_job/retry_on_transient_errors.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveJob::RetryOnTransientErrors
extend ActiveSupport::Concern
diff --git a/app/lib/active_storage/downloadable_file.rb b/app/lib/active_storage/downloadable_file.rb
index b89c20997..13db48d70 100644
--- a/app/lib/active_storage/downloadable_file.rb
+++ b/app/lib/active_storage/downloadable_file.rb
@@ -1,10 +1,17 @@
+# frozen_string_literal: true
+
require 'fog/openstack'
class ActiveStorage::DownloadableFile
- def self.create_list_from_dossiers(dossiers:, user_profile:)
- pj_service = PiecesJustificativesService.new(user_profile:)
+ def self.create_list_from_dossiers(dossiers:, user_profile:, export_template: nil)
+ pj_service = PiecesJustificativesService.new(user_profile:, export_template:)
- pj_service.generate_dossiers_export(dossiers) + pj_service.liste_documents(dossiers)
+ files = []
+ DossierPreloader.new(dossiers).in_batches_with_block do |loaded_dossiers|
+ files += pj_service.generate_dossiers_export(loaded_dossiers) + pj_service.liste_documents(loaded_dossiers)
+ end
+
+ files
end
def self.cleanup_list_from_dossier(files)
diff --git a/app/lib/active_storage/fake_attachment.rb b/app/lib/active_storage/fake_attachment.rb
index f84d8caf2..59358365c 100644
--- a/app/lib/active_storage/fake_attachment.rb
+++ b/app/lib/active_storage/fake_attachment.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ActiveStorage::FakeAttachment < Hashie::Dash
property :filename
property :name
diff --git a/app/lib/active_storage/virus_scanner.rb b/app/lib/active_storage/virus_scanner.rb
index 6e5d26d01..7d78f122d 100644
--- a/app/lib/active_storage/virus_scanner.rb
+++ b/app/lib/active_storage/virus_scanner.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ActiveStorage::VirusScanner
def initialize(blob)
@blob = blob
diff --git a/app/lib/api/client.rb b/app/lib/api/client.rb
index dcf8eab2d..d7f7ea783 100644
--- a/app/lib/api/client.rb
+++ b/app/lib/api/client.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class API::Client
include Dry::Monads[:result]
@@ -17,6 +19,12 @@ class API::Client
timeout: TIMEOUT)
end
handle_response(response, schema:)
+ rescue StandardError => reason
+ if reason.is_a?(URI::InvalidURIError)
+ Failure(Error[:uri, 0, false, reason])
+ else
+ Failure(Error[:error, 0, false, reason])
+ end
end
private
diff --git a/app/lib/api_datagouv/api.rb b/app/lib/api_datagouv/api.rb
index 22fa5cd98..0cf4e11a1 100644
--- a/app/lib/api_datagouv/api.rb
+++ b/app/lib/api_datagouv/api.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIDatagouv::API
class RequestFailed < StandardError
def initialize(url, response)
diff --git a/app/lib/api_education/annuaire_education_adapter.rb b/app/lib/api_education/annuaire_education_adapter.rb
index 492f274d9..0a3186b07 100644
--- a/app/lib/api_education/annuaire_education_adapter.rb
+++ b/app/lib/api_education/annuaire_education_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'json_schemer'
class APIEducation::AnnuaireEducationAdapter
diff --git a/app/lib/api_education/api.rb b/app/lib/api_education/api.rb
index d0ee6bcae..d0bd5c977 100644
--- a/app/lib/api_education/api.rb
+++ b/app/lib/api_education/api.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEducation::API
class ResourceNotFound < StandardError
end
diff --git a/app/lib/api_entreprise/adapter.rb b/app/lib/api_entreprise/adapter.rb
index a79a7f09b..d634c8c76 100644
--- a/app/lib/api_entreprise/adapter.rb
+++ b/app/lib/api_entreprise/adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::Adapter
UNAVAILABLE = 'Donnée indisponible'
diff --git a/app/lib/api_entreprise/api.rb b/app/lib/api_entreprise/api.rb
index ca2d9d7c1..1977b5ee0 100644
--- a/app/lib/api_entreprise/api.rb
+++ b/app/lib/api_entreprise/api.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::API
ENTREPRISE_RESOURCE_NAME = "v3/insee/sirene/unites_legales/%{id}"
ETABLISSEMENT_RESOURCE_NAME = "v3/insee/sirene/etablissements/%{id}"
@@ -125,10 +127,10 @@ class APIEntreprise::API
raise Error::ResourceNotFound.new(response)
elsif response.code == 400
raise Error::BadFormatRequest.new(response)
+ elsif service_unavailable?(response)
+ raise Error::ServiceUnavailable.new(response)
elsif response.code == 502
raise Error::BadGateway.new(response)
- elsif response.code == 503
- raise Error::ServiceUnavailable.new(response)
elsif response.timed_out?
raise Error::TimedOut.new(response)
else
@@ -136,6 +138,19 @@ class APIEntreprise::API
end
end
+ SERVICE_UNAVAILABLE_ERRORS = ["01000", "01001", "01002", "02002", "03002", "28002", "29002", "31002", "34002"]
+ def service_unavailable?(response)
+ if response.code == 502 || response.code == 504
+ parse_response_errors(response).any? { _1.is_a?(Hash) && _1[:code]&.in?(SERVICE_UNAVAILABLE_ERRORS) }
+ end
+ end
+
+ def parse_response_errors(response)
+ JSON.parse(response.body, symbolize_names: true).fetch(:errors, [])
+ rescue JSON::ParserError
+ []
+ end
+
def make_url(resource_name, siret_or_siren = nil)
[API_ENTREPRISE_URL, format(resource_name, id: siret_or_siren)].compact.join("/")
end
diff --git a/app/lib/api_entreprise/api/error.rb b/app/lib/api_entreprise/api/error.rb
index 1178d9b08..c9482720d 100644
--- a/app/lib/api_entreprise/api/error.rb
+++ b/app/lib/api_entreprise/api/error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::API::Error < ::StandardError
def initialize(response)
# use uri to avoid sending token
diff --git a/app/lib/api_entreprise/api/error/bad_format_request.rb b/app/lib/api_entreprise/api/error/bad_format_request.rb
index 147718e0e..0ae77236a 100644
--- a/app/lib/api_entreprise/api/error/bad_format_request.rb
+++ b/app/lib/api_entreprise/api/error/bad_format_request.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::API::Error::BadFormatRequest < APIEntreprise::API::Error
def network_error?
false
diff --git a/app/lib/api_entreprise/api/error/bad_gateway.rb b/app/lib/api_entreprise/api/error/bad_gateway.rb
index 0943c2635..3bc16c842 100644
--- a/app/lib/api_entreprise/api/error/bad_gateway.rb
+++ b/app/lib/api_entreprise/api/error/bad_gateway.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class APIEntreprise::API::Error::BadGateway < APIEntreprise::API::Error
end
diff --git a/app/lib/api_entreprise/api/error/request_failed.rb b/app/lib/api_entreprise/api/error/request_failed.rb
index e99ca8b44..632f3f156 100644
--- a/app/lib/api_entreprise/api/error/request_failed.rb
+++ b/app/lib/api_entreprise/api/error/request_failed.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class APIEntreprise::API::Error::RequestFailed < APIEntreprise::API::Error
end
diff --git a/app/lib/api_entreprise/api/error/resource_not_found.rb b/app/lib/api_entreprise/api/error/resource_not_found.rb
index f6cb118f7..77108ec07 100644
--- a/app/lib/api_entreprise/api/error/resource_not_found.rb
+++ b/app/lib/api_entreprise/api/error/resource_not_found.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::API::Error::ResourceNotFound < APIEntreprise::API::Error
def network_error?
false
diff --git a/app/lib/api_entreprise/api/error/service_unavailable.rb b/app/lib/api_entreprise/api/error/service_unavailable.rb
index 51cdf5254..81140ffd0 100644
--- a/app/lib/api_entreprise/api/error/service_unavailable.rb
+++ b/app/lib/api_entreprise/api/error/service_unavailable.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class APIEntreprise::API::Error::ServiceUnavailable < APIEntreprise::API::Error
end
diff --git a/app/lib/api_entreprise/api/error/timed_out.rb b/app/lib/api_entreprise/api/error/timed_out.rb
index 7e4b27590..53e1d1581 100644
--- a/app/lib/api_entreprise/api/error/timed_out.rb
+++ b/app/lib/api_entreprise/api/error/timed_out.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class APIEntreprise::API::Error::TimedOut < APIEntreprise::API::Error
end
diff --git a/app/lib/api_entreprise/attestation_fiscale_adapter.rb b/app/lib/api_entreprise/attestation_fiscale_adapter.rb
index 13d296170..3051548ca 100644
--- a/app/lib/api_entreprise/attestation_fiscale_adapter.rb
+++ b/app/lib/api_entreprise/attestation_fiscale_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::AttestationFiscaleAdapter < APIEntreprise::Adapter
# Doc métier : https://entreprise.api.gouv.fr/catalogue/dgfip/attestations_fiscales
# Swagger : https://entreprise.api.gouv.fr/developpeurs/openapi#tag/Attestations-sociales-et-fiscales/paths/~1v4~1dgfip~1unites_legales~1%7Bsiren%7D~1attestation_fiscale/get
diff --git a/app/lib/api_entreprise/attestation_sociale_adapter.rb b/app/lib/api_entreprise/attestation_sociale_adapter.rb
index b9a5c47a6..e5e636c77 100644
--- a/app/lib/api_entreprise/attestation_sociale_adapter.rb
+++ b/app/lib/api_entreprise/attestation_sociale_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::AttestationSocialeAdapter < APIEntreprise::Adapter
# Doc métier : https://entreprise.api.gouv.fr/catalogue/urssaf/attestation_vigilance
# Swagger : https://entreprise.api.gouv.fr/developpeurs/openapi#tag/Attestations-sociales-et-fiscales/paths/~1v4~1urssaf~1unites_legales~1%7Bsiren%7D~1attestation_vigilance/get
diff --git a/app/lib/api_entreprise/bilans_bdf_adapter.rb b/app/lib/api_entreprise/bilans_bdf_adapter.rb
index 6d05f2455..5b440ac52 100644
--- a/app/lib/api_entreprise/bilans_bdf_adapter.rb
+++ b/app/lib/api_entreprise/bilans_bdf_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::BilansBdfAdapter < APIEntreprise::Adapter
def initialize(siret, procedure_id)
@siret = siret
diff --git a/app/lib/api_entreprise/effectifs_adapter.rb b/app/lib/api_entreprise/effectifs_adapter.rb
index a8f660fbe..60a14ad75 100644
--- a/app/lib/api_entreprise/effectifs_adapter.rb
+++ b/app/lib/api_entreprise/effectifs_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::EffectifsAdapter < APIEntreprise::Adapter
def initialize(siret, procedure_id, annee, mois)
@siret = siret
diff --git a/app/lib/api_entreprise/effectifs_annuels_adapter.rb b/app/lib/api_entreprise/effectifs_annuels_adapter.rb
index ccfa1e8f9..936c2edd9 100644
--- a/app/lib/api_entreprise/effectifs_annuels_adapter.rb
+++ b/app/lib/api_entreprise/effectifs_annuels_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::EffectifsAnnuelsAdapter < APIEntreprise::Adapter
def initialize(siret, procedure_id, year = default_year)
@siret = siret
diff --git a/app/lib/api_entreprise/entreprise_adapter.rb b/app/lib/api_entreprise/entreprise_adapter.rb
index 41df33da0..d8d7136dc 100644
--- a/app/lib/api_entreprise/entreprise_adapter.rb
+++ b/app/lib/api_entreprise/entreprise_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::EntrepriseAdapter < APIEntreprise::Adapter
# Doc métier : https://entreprise.api.gouv.fr/catalogue/insee/unites_legales
# Swagger : https://entreprise.api.gouv.fr/developpeurs/openapi#tag/Informations-generales/paths/~1v3~1insee~1sirene~1unites_legales~1%7Bsiren%7D/get
diff --git a/app/lib/api_entreprise/etablissement_adapter.rb b/app/lib/api_entreprise/etablissement_adapter.rb
index df9211bfa..5a5be0d1d 100644
--- a/app/lib/api_entreprise/etablissement_adapter.rb
+++ b/app/lib/api_entreprise/etablissement_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::EtablissementAdapter < APIEntreprise::Adapter
# Doc Métier : https://entreprise.api.gouv.fr/catalogue/insee/etablissements
# Swagger : https://entreprise.api.gouv.fr/developpeurs/openapi#tag/Informations-generales/paths/~1v3~1insee~1sirene~1etablissements~1%7Bsiret%7D/get
@@ -23,7 +25,12 @@ class APIEntreprise::EtablissementAdapter < APIEntreprise::Adapter
params.merge!(params[:adresse].slice(*address_attr_to_fetch))
params[:nom_voie] = raw_data[:adresse][:libelle_voie]
params[:code_insee_localite] = raw_data[:adresse][:code_commune]
- params[:localite] = raw_data[:adresse][:libelle_commune]
+ if raw_data[:adresse][:libelle_pays_etranger].present?
+ params[:localite] = raw_data[:adresse][:libelle_commune_etranger]
+ params[:nom_pays] = raw_data[:adresse][:libelle_pays_etranger]
+ else
+ params[:localite] = raw_data[:adresse][:libelle_commune]
+ end
params[:adresse] = adresse_line
params
else
diff --git a/app/lib/api_entreprise/exercices_adapter.rb b/app/lib/api_entreprise/exercices_adapter.rb
index e34d4200c..b42150f65 100644
--- a/app/lib/api_entreprise/exercices_adapter.rb
+++ b/app/lib/api_entreprise/exercices_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::ExercicesAdapter < APIEntreprise::Adapter
# Doc métier : https://entreprise.api.gouv.fr/catalogue/dgfip/chiffres_affaires
# Swagger : https://entreprise.api.gouv.fr/developpeurs/openapi#tag/Informations-financieres/paths/~1v3~1dgfip~1etablissements~1%7Bsiret%7D~1chiffres_affaires/get
diff --git a/app/lib/api_entreprise/extrait_kbis_adapter.rb b/app/lib/api_entreprise/extrait_kbis_adapter.rb
index afa4559b1..bef98ecba 100644
--- a/app/lib/api_entreprise/extrait_kbis_adapter.rb
+++ b/app/lib/api_entreprise/extrait_kbis_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::ExtraitKbisAdapter < APIEntreprise::Adapter
# Doc métier : https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait
# Swagger : https://entreprise.api.gouv.fr/developpeurs/openapi#tag/Informations-generales/paths/~1v3~1infogreffe~1rcs~1unites_legales~1%7Bsiren%7D~1extrait_kbis/get
diff --git a/app/lib/api_entreprise/privileges_adapter.rb b/app/lib/api_entreprise/privileges_adapter.rb
index 7c4d9ad76..a5c61262b 100644
--- a/app/lib/api_entreprise/privileges_adapter.rb
+++ b/app/lib/api_entreprise/privileges_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::PrivilegesAdapter < APIEntreprise::Adapter
def initialize(token)
@token = token
diff --git a/app/lib/api_entreprise/rna_adapter.rb b/app/lib/api_entreprise/rna_adapter.rb
index e4b5d061c..0581fc6f3 100644
--- a/app/lib/api_entreprise/rna_adapter.rb
+++ b/app/lib/api_entreprise/rna_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::RNAAdapter < APIEntreprise::Adapter
# Doc métier : https://entreprise.api.gouv.fr/catalogue/djepva/associations_open_data
# Swagger : https://entreprise.api.gouv.fr/developpeurs/openapi#tag/Informations-generales/paths/~1v4~1djepva~1api-association~1associations~1open_data~1%7Bsiren_or_rna%7D/get
diff --git a/app/lib/api_entreprise/service_adapter.rb b/app/lib/api_entreprise/service_adapter.rb
index a37fa553a..1b586b7f2 100644
--- a/app/lib/api_entreprise/service_adapter.rb
+++ b/app/lib/api_entreprise/service_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::ServiceAdapter < APIEntreprise::EtablissementAdapter
def initialize(siret, service_id)
@siret = siret
diff --git a/app/lib/api_entreprise/tva_adapter.rb b/app/lib/api_entreprise/tva_adapter.rb
index cfe6dfc56..d19fac079 100644
--- a/app/lib/api_entreprise/tva_adapter.rb
+++ b/app/lib/api_entreprise/tva_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntreprise::TvaAdapter < APIEntreprise::Adapter
# Doc métier : https://entreprise.api.gouv.fr/catalogue/commission_europeenne/numero_tva
# Swagger : https://entreprise.api.gouv.fr/developpeurs/openapi#tag/Informations-generales/paths/~1v3~1european_commission~1unites_legales~1%7Bsiren%7D~1numero_tva/get
diff --git a/app/lib/api_particulier/api.rb b/app/lib/api_particulier/api.rb
index 8813f8948..84a1a99e2 100644
--- a/app/lib/api_particulier/api.rb
+++ b/app/lib/api_particulier/api.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIParticulier::API
include APIParticulier::Error
diff --git a/app/lib/api_particulier/cnaf_adapter.rb b/app/lib/api_particulier/cnaf_adapter.rb
index 5377fecaf..26394e9ea 100644
--- a/app/lib/api_particulier/cnaf_adapter.rb
+++ b/app/lib/api_particulier/cnaf_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIParticulier::CnafAdapter
class InvalidSchemaError < ::StandardError
def initialize(errors)
diff --git a/app/lib/api_particulier/dgfip_adapter.rb b/app/lib/api_particulier/dgfip_adapter.rb
index e11cb5535..7b4e13fb3 100644
--- a/app/lib/api_particulier/dgfip_adapter.rb
+++ b/app/lib/api_particulier/dgfip_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIParticulier::DgfipAdapter
class InvalidSchemaError < ::StandardError
def initialize(errors)
diff --git a/app/lib/api_particulier/error.rb b/app/lib/api_particulier/error.rb
index 4224bcad7..61c4dd461 100644
--- a/app/lib/api_particulier/error.rb
+++ b/app/lib/api_particulier/error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module APIParticulier
module Error
class HttpError < ::StandardError
diff --git a/app/lib/api_particulier/mesri_adapter.rb b/app/lib/api_particulier/mesri_adapter.rb
index ea27f4168..85e6a884c 100644
--- a/app/lib/api_particulier/mesri_adapter.rb
+++ b/app/lib/api_particulier/mesri_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIParticulier::MesriAdapter
class InvalidSchemaError < ::StandardError
def initialize(errors)
diff --git a/app/lib/api_particulier/pole_emploi_adapter.rb b/app/lib/api_particulier/pole_emploi_adapter.rb
index 1ecc12cb8..ea96ce375 100644
--- a/app/lib/api_particulier/pole_emploi_adapter.rb
+++ b/app/lib/api_particulier/pole_emploi_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIParticulier::PoleEmploiAdapter
class InvalidSchemaError < ::StandardError
def initialize(errors)
diff --git a/app/lib/api_particulier/services/sources_service.rb b/app/lib/api_particulier/services/sources_service.rb
index 72aa605e1..15ed26f61 100644
--- a/app/lib/api_particulier/services/sources_service.rb
+++ b/app/lib/api_particulier/services/sources_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module APIParticulier
module Services
class SourcesService
diff --git a/app/lib/api_tchap/api.rb b/app/lib/api_tchap/api.rb
index 0dec71d53..de3c64e87 100644
--- a/app/lib/api_tchap/api.rb
+++ b/app/lib/api_tchap/api.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APITchap::API
class ResourceNotFound < StandardError
end
diff --git a/app/lib/api_tchap/hs_adapter.rb b/app/lib/api_tchap/hs_adapter.rb
index cdf2aced2..c99e08e29 100644
--- a/app/lib/api_tchap/hs_adapter.rb
+++ b/app/lib/api_tchap/hs_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APITchap::HsAdapter
def initialize(email)
@email = email
diff --git a/app/lib/asn1/timestamp.rb b/app/lib/asn1/timestamp.rb
index 6dd65e4db..384f00ad6 100644
--- a/app/lib/asn1/timestamp.rb
+++ b/app/lib/asn1/timestamp.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ASN1::Timestamp
## Poor man’s rfc3161 timestamp decoding
# This works, as of 2019-05, for timestamps delivered by the universign POST api.
diff --git a/app/lib/balancer_delivery_method.rb b/app/lib/balancer_delivery_method.rb
index 2f9774af0..c1a1931d4 100644
--- a/app/lib/balancer_delivery_method.rb
+++ b/app/lib/balancer_delivery_method.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# A Mail delivery method that randomly balances the actual delivery between different
# methods.
#
@@ -14,6 +16,7 @@
#
# Be sure to restart your server when you modify this file.
class BalancerDeliveryMethod
+ BYPASS_UNVERIFIED_MAIL_PROTECTION = 'BYPASS_UNVERIFIED_MAIL_PROTECTION'.freeze
FORCE_DELIVERY_METHOD_HEADER = 'X-deliver-with'
# Allows configuring the random number generator used for selecting a delivery method,
# mostly for testing purposes.
@@ -24,6 +27,8 @@ class BalancerDeliveryMethod
end
def deliver!(mail)
+ return if prevent_delivery?(mail)
+
balanced_delivery_method = delivery_method(mail)
ApplicationMailer.wrap_delivery_behavior(mail, balanced_delivery_method)
@@ -40,6 +45,19 @@ class BalancerDeliveryMethod
private
+ def prevent_delivery?(mail)
+ return false if mail[BYPASS_UNVERIFIED_MAIL_PROTECTION].present?
+ return false if mail.to.blank? # bcc list
+
+ user = User.find_by(email: mail.to.first)
+ return user.unverified_email? if user.present?
+
+ individual = Individual.find_by(email: mail.to.first)
+ return individual.unverified_email? if individual.present?
+
+ true
+ end
+
def force_delivery_method?(mail)
@delivery_methods.keys.map(&:to_s).include?(mail[FORCE_DELIVERY_METHOD_HEADER]&.value)
end
diff --git a/app/lib/certigna/api.rb b/app/lib/certigna/api.rb
index e49a4f7e5..8cfaed3f6 100644
--- a/app/lib/certigna/api.rb
+++ b/app/lib/certigna/api.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Certigna::API
## Certigna Timestamp POST API
# the CAfile used to controle the timestamp token is build:
diff --git a/app/lib/code_insee.rb b/app/lib/code_insee.rb
index 358af2279..22044dee0 100644
--- a/app/lib/code_insee.rb
+++ b/app/lib/code_insee.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CodeInsee
def initialize(code_insee)
@code_insee = code_insee
diff --git a/app/lib/data_fixer/champs_phone_invalid.rb b/app/lib/data_fixer/champs_phone_invalid.rb
index 6c6574a11..64c599e9d 100644
--- a/app/lib/data_fixer/champs_phone_invalid.rb
+++ b/app/lib/data_fixer/champs_phone_invalid.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DataFixer::ChampsPhoneInvalid
def self.fix(phones_string)
phone_candidates = phones_string
diff --git a/app/lib/data_fixer/dossier_champs_missing.rb b/app/lib/data_fixer/dossier_champs_missing.rb
deleted file mode 100644
index 64e8eefb1..000000000
--- a/app/lib/data_fixer/dossier_champs_missing.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# some race condition (regarding double submit of dossier.passer_en_construction!) might remove champs
-# until now we haven't decided to push a stronger fix than an UI change
-# so we might have to recreate some deleted champs and notify administration
-class DataFixer::DossierChampsMissing
- def fix
- fixed_on_origin = apply_fix(@original_dossier)
-
- fixed_on_other = Dossier.where(editing_fork_origin_id: @original_dossier.id)
- .map(&method(:apply_fix))
-
- [fixed_on_origin, fixed_on_other.sum].sum
- end
-
- private
-
- attr_reader :original_dossier
-
- def initialize(dossier:)
- @original_dossier = dossier
- end
-
- def apply_fix(dossier)
- added_champs_root = fix_champs_root(dossier)
- added_champs_in_repetition = fix_champs_in_repetition(dossier)
-
- added_champs = added_champs_root + added_champs_in_repetition
- if !added_champs.empty?
- dossier.save!
- log_champs_added(dossier, added_champs)
- added_champs.size
- else
- 0
- end
- end
-
- def fix_champs_root(dossier)
- champs_root, _ = dossier.champs.partition { _1.parent_id.blank? }
- expected_tdcs = dossier.revision.revision_types_de_champ.filter { _1.parent.blank? }.map(&:type_de_champ)
-
- expected_tdcs.filter { !champs_root.map(&:stable_id).include?(_1.stable_id) }
- .map do |missing_tdc|
- champ_root_missing = missing_tdc.build_champ
-
- dossier.champs_public << champ_root_missing
- champ_root_missing
- end
- end
-
- def fix_champs_in_repetition(dossier)
- champs_repetition, _ = dossier.champs.partition(&:repetition?)
-
- champs_repetition.flat_map do |champ_repetition|
- champ_repetition_missing = champ_repetition.rows.flat_map do |row|
- row_id = row.first.row_id
- expected_tdcs = dossier.revision.children_of(champ_repetition.type_de_champ)
- row_tdcs = row.map(&:type_de_champ)
-
- (expected_tdcs - row_tdcs).map do |missing_tdc|
- champ_repetition_missing = missing_tdc.build_champ(row_id: row_id)
- champ_repetition.champs << champ_repetition_missing
- champ_repetition_missing
- end
- end
- end
- end
-
- def log_champs_added(dossier, added_champs)
- app_traces = caller.reject { _1.match?(%r{/ruby/.+/gems/}) }.map { _1.sub(Rails.root.to_s, "") }
-
- payload = {
- message: "DataFixer::DossierChampsMissing",
- dossier_id: dossier.id,
- champs_ids: added_champs.map(&:id).join(","),
- caller: app_traces
- }
-
- logger = Lograge.logger || Rails.logger
-
- logger.info payload.to_json
- end
-end
diff --git a/app/lib/database/migration_helpers.rb b/app/lib/database/migration_helpers.rb
index e0a224d83..33b44533d 100644
--- a/app/lib/database/migration_helpers.rb
+++ b/app/lib/database/migration_helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Some of this file is lifted from Gitlab's `lib/gitlab/database/migration_helpers.rb`
# Copyright (c) 2011-present GitLab B.V.
diff --git a/app/lib/dolist/api.rb b/app/lib/dolist/api.rb
index 87e401a0a..8713e93bf 100644
--- a/app/lib/dolist/api.rb
+++ b/app/lib/dolist/api.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "support/jsv"
module Dolist
@@ -59,61 +61,57 @@ module Dolist
end
def send_email(mail)
- if mail.attachments.any? { !_1.inline? }
- return send_email_with_attachment(mail)
- end
-
body = { "TransactionalSending": prepare_mail_body(mail) }
url = format_url(EMAIL_SENDING_TRANSACTIONAL)
post(url, body.to_json)
end
- def send_email_with_attachment(mail)
- uri = URI(format_url(EMAIL_SENDING_TRANSACTIONAL_ATTACHMENT))
+ # def send_email_with_attachment(mail)
+ # uri = URI(format_url(EMAIL_SENDING_TRANSACTIONAL_ATTACHMENT))
- request = Net::HTTP::Post.new(uri)
+ # request = Net::HTTP::Post.new(uri)
- default_headers.each do |key, value|
- next if key.to_s == "Content-Type"
- request[key] = value
- end
+ # default_headers.each do |key, value|
+ # next if key.to_s == "Content-Type"
+ # request[key] = value
+ # end
- boundary = "---011000010111000001101001" # any random string not present in the body
- request.content_type = "multipart/form-data; boundary=#{boundary}"
+ # boundary = "---011000010111000001101001" # any random string not present in the body
+ # request.content_type = "multipart/form-data; boundary=#{boundary}"
- body = "--#{boundary}\r\n"
+ # body = "--#{boundary}\r\n"
- base64_files(mail.attachments).each do |file|
- body << "Content-Disposition: form-data; name=\"#{file.field_name}\"; filename=\"#{file.filename}\"\r\n"
- body << "Content-Type: #{file.mime_type}\r\n"
- body << "\r\n"
- body << file.content
- body << "\r\n"
- end
+ # base64_files(mail.attachments).each do |file|
+ # body << "Content-Disposition: form-data; name=\"#{file.field_name}\"; filename=\"#{file.filename}\"\r\n"
+ # body << "Content-Type: #{file.mime_type}\r\n"
+ # body << "\r\n"
+ # body << file.content
+ # body << "\r\n"
+ # end
- body << "\r\n--#{boundary}\r\n"
- body << "Content-Disposition: form-data; name=\"TransactionalSending\"\r\n"
- body << "Content-Type: text/plain; charset=utf-8\r\n"
- body << "\r\n"
- body << prepare_mail_body(mail).to_jsv
+ # body << "\r\n--#{boundary}\r\n"
+ # body << "Content-Disposition: form-data; name=\"TransactionalSending\"\r\n"
+ # body << "Content-Type: text/plain; charset=utf-8\r\n"
+ # body << "\r\n"
+ # body << prepare_mail_body(mail).to_jsv
- body << "\r\n--#{boundary}--\r\n"
- body << "\r\n"
+ # body << "\r\n--#{boundary}--\r\n"
+ # body << "\r\n"
- request.body = body
+ # request.body = body
- http = Net::HTTP.new(uri.host, uri.port)
- http.use_ssl = true
+ # http = Net::HTTP.new(uri.host, uri.port)
+ # http.use_ssl = true
- response = http.request(request)
+ # response = http.request(request)
- if response.body.empty?
- fail "Dolist API returned an empty response"
- else
- JSON.parse(response.body)
- end
- end
+ # if response.body.empty?
+ # fail "Dolist API returned an empty response"
+ # else
+ # JSON.parse(response.body)
+ # end
+ # end
def sent_mails(email_address)
contact_id = fetch_contact_id(email_address)
@@ -190,9 +188,11 @@ module Dolist
format(base, account_id: account_id)
end
- def sender_id
- Rails.cache.fetch("dolist_api_sender_id", expires_in: 1.hour) do
- senders.dig("ItemList", 0, "Sender", "ID")
+ def sender_id(domain)
+ if domain == "demarches.gouv.fr"
+ Rails.application.secrets.dolist[:gouv_sender_id]
+ else
+ Rails.application.secrets.dolist[:default_sender_id]
end
end
@@ -267,7 +267,7 @@ module Dolist
"Message": {
"Name": mail['X-Dolist-Message-Name'].value,
"Subject": mail.subject,
- "SenderID": sender_id,
+ "SenderID": sender_id(mail.from_address.domain),
"ForceHttp": false, # ForceHttp : force le tracking http non sécurisé (True/False).
"Format": "html",
"DisableOpenTracking": true, # DisableOpenTracking : désactivation du tracking d'ouverture (True/False).
diff --git a/app/lib/dolist/api_sender.rb b/app/lib/dolist/api_sender.rb
index 7177df39b..89b2d6977 100644
--- a/app/lib/dolist/api_sender.rb
+++ b/app/lib/dolist/api_sender.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Dolist
class APISender
def initialize(mail); end
diff --git a/app/lib/dolist/base64_file.rb b/app/lib/dolist/base64_file.rb
index 8afcf2024..d449a1f92 100644
--- a/app/lib/dolist/base64_file.rb
+++ b/app/lib/dolist/base64_file.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Dolist
Base64File = Struct.new(:field_name, :filename, :mime_type, :content, keyword_init: true)
end
diff --git a/app/lib/dolist/ignorable_error.rb b/app/lib/dolist/ignorable_error.rb
index 18ae5d64a..711fead59 100644
--- a/app/lib/dolist/ignorable_error.rb
+++ b/app/lib/dolist/ignorable_error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Dolist
class IgnorableError < StandardError; end
end
diff --git a/app/lib/dossier_with_reference_date.rb b/app/lib/dossier_with_reference_date.rb
index 49c7b2645..c2c0aaa6c 100644
--- a/app/lib/dossier_with_reference_date.rb
+++ b/app/lib/dossier_with_reference_date.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossierWithReferenceDate
def self.assign(dossier, state: nil, reference_date: nil)
created_at = reference_date.presence || default_created_at(dossier)
diff --git a/app/lib/download_manager/parallel_download_queue.rb b/app/lib/download_manager/parallel_download_queue.rb
index 16dcbd762..ebab592cd 100644
--- a/app/lib/download_manager/parallel_download_queue.rb
+++ b/app/lib/download_manager/parallel_download_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DownloadManager
class ParallelDownloadQueue
DOWNLOAD_MAX_PARALLEL = ENV.fetch('DOWNLOAD_MAX_PARALLEL') { 10 }
diff --git a/app/lib/download_manager/procedure_attachments_export.rb b/app/lib/download_manager/procedure_attachments_export.rb
index 3eef6ad82..bdba09b5f 100644
--- a/app/lib/download_manager/procedure_attachments_export.rb
+++ b/app/lib/download_manager/procedure_attachments_export.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DownloadManager
class ProcedureAttachmentsExport
delegate :destination, to: :@queue
diff --git a/app/lib/email_checker.rb b/app/lib/email_checker.rb
new file mode 100644
index 000000000..efa9be6ee
--- /dev/null
+++ b/app/lib/email_checker.rb
@@ -0,0 +1,656 @@
+# frozen_string_literal: true
+
+class EmailChecker
+ # Extracted 100 most used domain on our users table [june 2024]
+ # + all .gouv.fr domain on our users table
+ # + all .ac-xxx on our users table
+ KNOWN_DOMAINS = [
+ 'gmail.com',
+ 'hotmail.fr',
+ 'orange.fr',
+ 'yahoo.fr',
+ 'hotmail.com',
+ 'outlook.fr',
+ 'wanadoo.fr',
+ 'free.fr',
+ 'yahoo.com',
+ 'icloud.com',
+ 'laposte.net',
+ 'live.fr',
+ 'sfr.fr',
+ 'outlook.com',
+ 'neuf.fr',
+ 'aol.com',
+ 'bbox.fr',
+ 'msn.com',
+ 'me.com',
+ 'gmx.fr',
+ 'protonmail.com',
+ 'club-internet.fr',
+ 'live.com',
+ 'ymail.com',
+ 'ars.sante.fr',
+ 'mail.ru',
+ 'cegetel.net',
+ 'numericable.fr',
+ 'aliceadsl.fr',
+ 'comcast.net',
+ 'assurance-maladie.fr',
+ 'mac.com',
+ 'naver.com',
+ 'airbus.com',
+ 'justice.fr',
+ 'pole-emploi.fr',
+ 'educagri.fr',
+ 'aphp.fr',
+ 'netcourrier.com',
+ 'dbmail.com',
+ 'aol.fr',
+ 'qq.com',
+ 'hotmail.co.uk',
+ 'yahoo.co.uk',
+ 'proxima-mail.fr',
+ 'yahoo.com.br',
+ 'sciencespo.fr',
+ 'gmx.com',
+ 'etu.univ-st-etienne.fr',
+ 'yahoo.ca',
+ '163.com',
+ 'francetravail.fr',
+ 'mail.pf',
+ 'nantesmetropole.fr',
+ 'hotmail.it',
+ 'sbcglobal.net',
+ 'noos.fr',
+ 'ird.fr',
+ 'safrangroup.com',
+ 'croix-rouge.fr',
+ 'eiffage.com',
+ 'veolia.com',
+ 'notaires.fr',
+ 'nordnet.fr',
+ 'videotron.ca',
+ 'paris.fr',
+ 'lilo.org',
+ 'mfr.asso.fr',
+ 'yopmail.com',
+ 'ukr.net',
+ 'onf.fr',
+ 'stellantis.com',
+ '9online.fr',
+ 'atmp50.fr',
+ 'engie.com',
+ 'libertysurf.fr',
+ 'mailo.com',
+ 'auchan.fr',
+ 'verizon.net',
+ 'rocketmail.com',
+ 'mpsa.com',
+ 'entrepreneur.fr',
+ 'googlemail.com',
+ 'arcelormittal.com',
+ 'groupe-sos.org',
+ 'proton.me',
+ 'att.net',
+ 'pm.me',
+ 'orange.com',
+ 'abv.bg',
+ 'yahoo.es',
+ 'creditmutuel.fr',
+ 'yandex.ru',
+ 'essec.edu',
+ 'urssaf.fr',
+ 'bpifrance.fr',
+ 'uol.com.br',
+ 'suez.com',
+ 'univ-st-etienne.fr',
+ 'korian.fr',
+ 'developpement-durable.gouv.fr',
+ 'modernisation.gouv.fr',
+ 'social.gouv.fr',
+ 'emploi.gouv.fr',
+ 'agriculture.gouv.fr',
+ 'intradef.gouv.fr',
+ 'interieur.gouv.fr',
+ 'oise.gouv.fr',
+ 'direccte.gouv.fr',
+ 'culture.gouv.fr',
+ 'pas-de-calais.gouv.fr',
+ 'finances.gouv.fr',
+ 'drieets.gouv.fr',
+ 'drjscs.gouv.fr',
+ 'sg.social.gouv.fr',
+ 'martinique.pref.gouv.fr',
+ 'beta.gouv.fr',
+ 'dieccte.gouv.fr',
+ 'cotes-darmor.gouv.fr',
+ 'vosges.gouv.fr',
+ 'developppement-durable.gouv.fr',
+ 'mayenne.gouv.fr',
+ 'aviation-civile.gouv.fr',
+ 'data.gouv.fr',
+ 'recherche.gouv.fr',
+ 'sante.gouv.fr',
+ 'paris-idf.gouv.fr',
+ 'guyane.gouv.fr',
+ 'douane.finances.gouv.fr',
+ 'cget.gouv.fr',
+ 'herault.gouv.fr',
+ 'loire-atlantique.gouv.fr',
+ 'manche.gouv.fr',
+ 'seine-maritime.gouv.fr',
+ 'dgccrf.finances.gouv.fr',
+ 'tarn-et-garonne.gouv.fr',
+ 'dila.gouv.fr',
+ 'diplomatie.gouv.fr',
+ 'haut-rhin.gouv.fr',
+ 'nord.gouv.fr',
+ 'bouches-du-rhone.gouv.fr',
+ 'alpes-de-haute-provence.gouv.fr',
+ 'hautes-alpes.gouv.fr',
+ 'alpes-maritimes.gouv.fr',
+ 'var.gouv.fr',
+ 'vaucluse.gouv.fr',
+ 'rhone.gouv.fr',
+ 'occitanie.gouv.fr',
+ 'ille-et-vilaine.gouv.fr',
+ 'finistere.gouv.fr',
+ 'aisne.gouv.fr',
+ 'indre.gouv.fr',
+ 'yvelines.gouv.fr',
+ 'bas-rhin.gouv.fr',
+ 'landes.gouv.fr',
+ 'haute-marne.gouv.fr',
+ 'correze.gouv.fr',
+ 'val-doise.gouv.fr',
+ 'seine-et-marne.gouv.fr',
+ 'essonne.gouv.fr',
+ 'calvados.gouv.fr',
+ 'charente-maritime.gouv.fr',
+ 'corse-du-sud.gouv.fr',
+ 'gironde.gouv.fr',
+ 'haute-corse.gouv.fr',
+ 'morbihan.gouv.fr',
+ 'pyrenees-atlantiques.gouv.fr',
+ 'pyrenees-orientales.gouv.fr',
+ 'somme.gouv.fr',
+ 'vendee.gouv.fr',
+ 'dgtresor.gouv.fr',
+ 'marne.gouv.fr',
+ 'auvergne-rhone-alpes.gouv.fr',
+ 'meurthe-et-moselle.gouv.fr',
+ 'pm.gouv.fr',
+ 'oncfs.gouv.fr',
+ 'orne.gouv.fr',
+ 'charente.gouv.fr',
+ 'travail.gouv.fr',
+ 'gard.gouv.fr',
+ 'maine-et-loire.gouv.fr',
+ 'moselle.gouv.fr',
+ 'outre-mer.gouv.fr',
+ 'jscs.gouv.fr',
+ 'haute-garonne.gouv.fr',
+ 'vienne.gouv.fr',
+ 'dordogne.gouv.fr',
+ 'eure.gouv.fr',
+ 'meuse.gouv.fr',
+ 'savoie.gouv.fr',
+ 'doubs.gouv.fr',
+ 'bfc.gouv.fr',
+ 'education.gouv.fr',
+ 'ariege.gouv.fr',
+ 'normandie.gouv.fr',
+ 'gendarmerie.interieur.gouv.fr',
+ 'ain.gouv.fr',
+ 'ardennes.gouv.fr',
+ 'drome.gouv.fr',
+ 'bretagne.gouv.fr',
+ 'paca.gouv.fr',
+ 'haute-saone.gouv.fr',
+ 'lot.gouv.fr',
+ 'dgfip.finances.gouv.fr',
+ 'aveyron.gouv.fr',
+ 'gers.gouv.fr',
+ 'tarn.gouv.fr',
+ 'aude.gouv.fr',
+ 'lozere.gouv.fr',
+ 'hautes-pyrenees.gouv.fr',
+ 'jeunesse-sports.gouv.fr',
+ 'alpes.maritimes.gouv.fr',
+ 'dreets.gouv.fr',
+ 'justice.gouv.fr',
+ 'sports.gouv.fr',
+ 'nouvelle-aquitaine.gouv.fr',
+ 'jura.gouv.fr',
+ 'haute-savoie.gouv.fr',
+ 'creuse.gouv.fr',
+ 'creps-poitiers.sports.gouv.fr',
+ 'equipement-agriculture.gouv.fr',
+ 'ira-metz.gouv.fr',
+ 'loire.gouv.fr',
+ 'defense.gouv.fr',
+ 'paris.gouv.fr',
+ 'ensm.sports.gouv.fr',
+ 'isere.gouv.fr',
+ 'haute-loire.gouv.fr',
+ 'cantal.gouv.fr',
+ 'lot-et-garonne.gouv.fr',
+ 'reunion.pref.gouv.fr',
+ 'loiret.gouv.fr',
+ 'indre-et-loire.gouv.fr',
+ 'eleve.ira-metz.gouv.fr',
+ 'deux-sevres.gouv.fr',
+ 'inao.gouv.fr',
+ 'franceconnect.gouv.fr',
+ 'essone.gouv.fr',
+ 'workinfrance.beta.gouv.fr',
+ 'seine-saint-denis.gouv.fr',
+ 'val-de-marne.gouv.fr',
+ 'morbihan.pref.gouv.fr',
+ 'externes.justice.gouv.fr',
+ 'haute-vienne.gouv.fr',
+ 'territoire-de-belfort.gouv.fr',
+ 'creps-reunion.sports.gouv.fr',
+ 'creps-centre.sports.gouv.fr',
+ 'creps-rhonealpes.sports.gouv.fr',
+ 'creps-montpellier.sports.gouv.fr',
+ 'nord.pref.gouv.fr',
+ 'charente-maritime.pref.gouv.fr',
+ 'cher.gouv.fr',
+ 'cote-dor.gouv.fr',
+ 'ssi.gouv.fr',
+ 'ira.gouv.fr',
+ 'pays-de-la-loire.gouv.fr',
+ 'loir-et-cher.gouv.fr',
+ 'saone-et-loire.gouv.fr',
+ 'enseignementsup.gouv.fr',
+ 'eure-et-loir.gouv.fr',
+ 'yonne.gouv.fr',
+ 'guadeloupe.pref.gouv.fr',
+ 'centre-val-de-loire.gouv.fr',
+ 'entreprise.api.gouv.fr',
+ 'grand-est.gouv.fr',
+ 'sarthe.gouv.fr',
+ 'sarthe.pref.gouv.fr',
+ 'puy-de-dome.gouv.fr',
+ 'externes.sante.gouv.fr',
+ 'allier.gouv.fr',
+ 'aube.gouv.fr',
+ 'nievre.gouv.fr',
+ 'ardeche.gouv.fr',
+ 'api.gouv.fr',
+ 'hauts-de-seine.gouv.fr',
+ 'hauts-de-france.gouv.fr',
+ 'temp-beta.gouv.fr',
+ 'def.gouv.fr',
+ 'particulier.api.gouv.fr',
+ 'ira-lille.gouv.fr',
+ 'haute-saone.pref.gouv.fr',
+ 'yvelines.pref.gouv.fr',
+ 'sgg.pm.gouv.fr',
+ 'anah.gouv.fr',
+ 'corse.gouv.fr',
+ 'mayenne.pref.gouv.fr',
+ 'cote-dor.pref.gouv.fr',
+ 'guyane.pref.gouv.fr',
+ 'ira-nantes.gouv.fr',
+ 'igas.gouv.fr',
+ 'tarn.pref.gouv.fr',
+ 'martinique.gouv.fr',
+ 'creps-paca.sports.gouv.fr',
+ 'ofb.gouv.fr',
+ 'loir-et-cher.pref.gouv.fr',
+ 'indre-et-loire.pref.gouv.fr',
+ 'polynesie-francaise.pref.gouv.fr',
+ 'scl.finances.gouv.fr',
+ 'numerique.gouv.fr',
+ 'cantal.pref.gouv.fr',
+ 'territoire-de-belfort.pref.gouv.fr',
+ 'creps-wattignies.sports.gouv.fr',
+ 'vienne.pref.gouv.fr',
+ 'ardennes.pref.gouv.fr',
+ 'creps-strasbourg.sports.gouv.fr',
+ 'creps-dijon.sports.gouv.fr',
+ 'ara.gouv.fr',
+ 'sgdsn.gouv.fr',
+ 'pays-de-la-loire.pref.gouv.fr',
+ 'anct.gouv.fr',
+ 'creps-pap.sports.gouv.fr',
+ 'sgae.gouv.fr',
+ 'esnm.sports.gouv.fr',
+ 'nouvelle-caledonie.gouv.fr',
+ 'deets.gouv.fr',
+ 'mayotte.gouv.fr',
+ 'creps-bordeaux.sports.gouv.fr',
+ 'civs.gouv.fr',
+ 'iga.interieur.gouv.fr',
+ 'cab.travail.gouv.fr',
+ 'ira-bastia.gouv.fr',
+ 'ira-lyon.gouv.fr',
+ 'creps-lorraine.sports.gouv.fr',
+ 'dihal.gouv.fr',
+ 'ofpra.gouv.fr',
+ 'mayotte.pref.gouv.fr',
+ 'strategie.gouv.fr',
+ 'territoires.gouv.fr',
+ 'dgcl.gouv.fr',
+ 'doubs.pref.gouv.fr',
+ 'service-civique.gouv.fr',
+ 'maine-et-loire.pref.gouv.fr',
+ 'envsn.sports.gouv.fr',
+ 'wallis-et-futuna.pref.gouv.fr',
+ 'gendarmerie.defense.gouv.fr',
+ 'anlci.gouv.fr',
+ 'cabinets.finances.gouv.fr',
+ 'seine-maritime.pref.gouv.fr',
+ 'promo46.ira-metz.gouv.fr',
+ 'aisne.pref.gouv.fr',
+ 'sportsdenature.gouv.fr',
+ 'loire-atlantique.pref.gouv.fr',
+ 'aude.pref.gouv.fr',
+ 'premier-ministre.gouv.fr',
+ 'igf.finances.gouv.fr',
+ 'eleves.ira-bastia.gouv.fr',
+ 'igesr.gouv.fr',
+ 'alpc.gouv.fr',
+ 'externes.emploi.gouv.fr',
+ 'prestataire.finances.gouv.fr',
+ 'gironde.pref.gouv.fr',
+ 'premar-atlantique.gouv.fr',
+ 'creps-toulouse.sports.gouv.fr',
+ 'guadeloupe.gouv.fr',
+ 'cybermalveillance.gouv.fr',
+ 'dicod.defense.gouv.fr',
+ 'creps-vichy.sports.gouv.fr',
+ 'aft.gouv.fr',
+ 'equipement.gouv.fr',
+ 'academie.defense.gouv.fr',
+ 'aube.pref.gouv.fr',
+ 'seine-et-marne.pref.gouv.fr',
+ 'pyrenees-orientales.pref.gouv.fr',
+ 'haute-garonne.pref.gouv.fr',
+ 'haut-rhin.pref.gouv.fr',
+ 'seine-saint-denis.pref.gouv.fr',
+ 'dcstep.gouv.fr',
+ 'promo47.ira-metz.gouv.fr',
+ 'trackdechets.beta.gouv.fr',
+ 'val-de-marne.pref.gouv.fr',
+ 'fabrique.social.gouv.fr',
+ 'agrasc.gouv.fr',
+ 'indre.pref.gouv.fr',
+ 'tarn-et-garonne.pref.gouv.fr',
+ 'corse.pref.gouv.fr',
+ 'bas-rhin.pref.gouv.fr',
+ 'inclusion.beta.gouv.fr',
+ 'hauts-de-seine.pref.gouv.fr',
+ 'loiret.pref.gouv.fr',
+ 'essonne.pref.gouv.fr',
+ 'territoires-industrie.gouv.fr',
+ 'spm975.gouv.fr',
+ 'saint-barth-saint-martin.gouv.fr',
+ 'judiciaire.interieur.gouv.fr',
+ 'mer.gouv.fr',
+ 'premar-manche.gouv.fr',
+ 'haute-normandie.pref.gouv.fr',
+ 'prestataire.modernisation.gouv.fr',
+ 'covoiturage.beta.gouv.fr',
+ 'promo48.ira-metz.gouv.fr',
+ 'france-services.gouv.fr',
+ 'ddets.gouv.fr',
+ 'afa.gouv.fr',
+ 'externes.social.gouv.fr',
+ 'vosges.pref.gouv.fr',
+ 'reunion.gouv.fr',
+ 'rhone.pref.gouv.fr',
+ 'alpes-maritimes.pref.gouv.fr',
+ 'gard.pref.gouv.fr',
+ 'oise.pref.gouv.fr',
+ 'creps-reims.sports.gouv.fr',
+ 'bouches-du-rhone.pref.gouv.fr',
+ 'esante.gouv.fr',
+ 'rhone-alpes.pref.gouv.fr',
+ 'finistere.pref.gouv.fr',
+ 'ops-bss.defense.gouv.fr',
+ 'orne.pref.gouv.fr',
+ 'transformation.gouv.fr',
+ 'cbcm.social.gouv.fr',
+ 'recosante.beta.gouv.fr',
+ 'pas-de-calais.pref.gouv.fr',
+ 'promo49.ira-metz.gouv.fr',
+ 'paca.pref.gouv.fr',
+ 'meurthe-et-moselle.pref.gouv.fr',
+ 'externes.sg.social.gouv.fr',
+ 'puy-de-dome.pref.gouv.fr',
+ 'academie.def.gouv.fr',
+ 'tarn.gouv.frd81intranet.ddcspp.tarn.gouv.fr',
+ 'agriculture-equipement.gouv.fr',
+ 'creps-idf.sports.gouv.fr',
+ 'eleve.ira-nantes.gouv.fr',
+ 'cohesion-territoires.gouv.fr',
+ 'ariege.pref.gouv.fr',
+ 'pyrenees-atlantiques.pref.gouv.fr',
+ 'hautes-pyrenees.pref.gouv.fr',
+ 'lot-et-garonne.pref.gouv.fr',
+ 'loire.pref.gouv.fr',
+ 'info-routiere.gouv.fr',
+ 'diges.gouv.fr',
+ 'insp.gouv.fr',
+ 'creps-pdl.sports.gouv.fr',
+ 'ddc.social.gouv.fr',
+ 'eleve.insp.gouv.fr',
+ 'val-doise.pref.gouv.fr',
+ 'montsaintmichel.gouv.fr',
+ 'st-cyr.terre-net.defense.gouv.fr',
+ '.finances.gouv.fr',
+ 'logement.gouv.fr',
+ 'cotes-darmor.pref.gouv.fr',
+ 'marne.pref.gouv.fr',
+ 'herault.pref.gouv.fr',
+ 'viennne.gouv.fr',
+ 'landes.pref.gouv.fr',
+ 'moselle.pref.gouv.fr',
+ 'saone-et-loire.pref.gouv.fr',
+ 'bmpm.gouv.fr',
+ 'ecologie-territoires.gouv.fr',
+ 'nievre.pref.gouv.fr',
+ 'hautes-pyrénées.gouv.fr',
+ 'gic.gouv.fr',
+ 'industrie.gouv.fr',
+ 'lot.pref.gouv.fr',
+ 'plan.gouv.fr',
+ 'internet.gouv.fr',
+ 'mesads.beta.gouv.fr',
+ 'gers.pref.gouv.fr',
+ 'dordogne.pref.gouv.fr',
+ 'somme.pref.gouv.fr',
+ 'datasubvention.beta.gouv.fr',
+ 'anc.gouv.fr',
+ 'premar-mediterranee.gouv.fr',
+ 'ille-et-vilaine.pref.gouv.fr',
+ 'eure-et-loir.pref.gouv.fr',
+ 'prestataires.pm.gouv.fr',
+ 'snu.gouv.fr',
+ 'code.gouv.fr',
+ 'alsace.pref.gouv.fr',
+ 'haute-vienne.pref.gouv.fr',
+ 'yonne.pref.gouv.fr',
+ 'bretagne.pref.gouv.fr',
+ 'mastere.insp.gouv.fr',
+ 'cada.pm.gouv.fr',
+ 'creuse.pref.gouv.fr',
+ 'ecologie.gouv.fr',
+ 'midi-pyrenees.pref.gouv.fr',
+ 'promo54.ira-metz.gouv.fr',
+ 'var.pref.gouv.fr',
+ 'alpes-de-haute-provence.pref.gouv.fr',
+ 'mail.numerique.gouv.fr',
+ 'france-identite.gouv.fr',
+ 'transport.data.gouv.fr',
+ 'allier.pref.gouv.fr',
+ 'dilhal.gouv.fr',
+ 'ardeche.pref.gouv.fr',
+ 'haute-corse.pref.gouv.fr',
+ 'intérieur.gouv.fr',
+ 'ddfip.gouv.fr',
+ 'calvados.pref.gouv.fr',
+ 'territoir-de-belfort.gouv.fr',
+ 'nor.gouv.fr',
+ 'creps-occitanie.sports.gouv.fr',
+ 'developpement-durabe.gouv.fr',
+ 'educ.nat.gouv.fr',
+ 'developpement-duable.gouv.fr',
+ 'dgfip.finanes.gouv.fr',
+ 'loire-atlantqieu.gouv.fr',
+ 'promo55.ira-metz.gouv.fr',
+ 'haute-saône.gouv.fr',
+ 'developpement.durable.gouv.fr',
+ 'dreet.gouv.fr',
+ 'miprof.gouv.fr',
+ 'pref.guyane.gouv.fr',
+ 'developpement.gouv.fr',
+ 'gendamrerie.interieur.gouv.fr',
+ 'pyrenees-atlantique.gouv.fr',
+ 'apprentissage.beta.gouv.fr',
+ 'yveliens.gouv.fr',
+ 'justiice.gouv.fr',
+ 'cutlure.gouv.fr',
+ 'aidantsconnect.beta.gouv.fr',
+ 'developpement-durbale.gouv.fr',
+ 'sine-et-marne.gouv.fr',
+ 'sociale.gouv.fr',
+ 'develeoppement-durable.gouv.fr',
+ 'draaf.gouv.fr',
+ 'drets.gouv.fr',
+ 'ancli.gouv.fr',
+ 'finistrere.gouv.fr',
+ 'bourgogne.pref.gouv.fr',
+ 'ac-polynesie.pf',
+ 'ac-lille.fr',
+ 'ac-nantes.fr',
+ 'ac-martinique.fr',
+ 'ac-creteil.fr',
+ 'ac-toulouse.fr',
+ 'ac-amiensfr',
+ 'ac-amiens.fr',
+ 'ac-rennes.fr',
+ 'ac-strasbourg.fr',
+ 'ac-lyon.fr',
+ 'ac-versailles.fr',
+ 'ac-audit.fr',
+ 'ac-rouen.fr',
+ 'ac-reunion.fr',
+ 'ac-poitiers.fr',
+ 'ac-caen.fr',
+ 'ac-montpellier.fr',
+ 'ac-paris.fr',
+ 'ac-besancon.fr',
+ 'ac-nancy-metz.fr',
+ 'ac-aix-marseille.fr',
+ 'ac-grenoble.fr',
+ 'ac-corse.fr',
+ 'ac-nice.fr',
+ 'ac-orleans-tours.fr',
+ 'ac-guadeloupe.fr',
+ 'ac-reims.fr',
+ 'ac-mayotte.fr',
+ 'ac-clermont.fr',
+ 'ac-bordeaux.fr',
+ 'ac-limoges.fr',
+ 'ac-normandie.fr',
+ 'ac-dijon.fr',
+ 'ac-guyane.fr',
+ 'ac-transports.fr',
+ 'ac-arpajonnais.com',
+ 'ac-cned.fr',
+ 'ac-nettoyage.com',
+ 'ac-architectes.fr',
+ 'ac-ajaccio.corsica',
+ 'ac-noumea.nc',
+ 'ac-spm.fr',
+ 'ac-versailes.fr',
+ 'ac-polynesie.fr',
+ 'ac-experts.fr',
+ 'ac-creteil.com',
+ 'ac-smart-relocation.com',
+ 'ac-ec.pro',
+ 'ac-sas.fr',
+ 'ac-derma.de',
+ 'ac-or.com',
+ 'ac-baugeois.fr',
+ 'ac-5.ru',
+ 'ac-arles.fr',
+ 'ac-holding.net',
+ 'ac-mb.fr',
+ 'ac-wf.wf',
+ 'ac-brest-finistere.fr',
+ 'ac-leman.com',
+ 'ac-darboussier.fr',
+ 'ac-si.fr',
+ 'ac-bordeau.fr',
+ 'ac-gatinais.com',
+ 'ac-cheminots.fr',
+ 'ac-seyssinet.com',
+ 'ac-cannes.fr',
+ 'ac-prev.com',
+ 'ac-sologne.fr',
+ 'ac-rennes',
+ 'ac-courbevoie.com',
+ 'ac-ce.fr',
+ 'ac-architecte.fr',
+ 'ac-tions.org',
+ 'ac-pm.fr',
+ 'ac-avocats.com',
+ 'ac-talents-rh.com',
+ 'ac-louis.com',
+ 'ac-internet.fr',
+ 'ac-toulouse.com',
+ 'ac-escial.fr',
+ 'ac-environnement.com',
+ 'ac-academie.fr',
+ 'ac-poiters.fr',
+ 'ac-bordeux.fr',
+ 'ac-verseilles.fr',
+ 'ac-ais-marseille.fr',
+ 'ac-horizon.fr',
+ 'ac-bordeaux.ft',
+ 'ac-toulouses.fr',
+ 'ac-toulous.fr'
+ ].freeze
+
+ def self.check(email:)
+ return { success: false } if email.blank?
+
+ parsed_email = Mail::Address.new(EmailSanitizableConcern::EmailSanitizer.sanitize(email))
+ return { success: false } if parsed_email.domain.blank?
+
+ return { success: true } if KNOWN_DOMAINS.any? { _1 == parsed_email.domain }
+
+ similar_domains = closest_domains(domain: parsed_email.domain)
+ return { success: true } if similar_domains.empty?
+
+ { success: true, suggestions: suggestions(parsed_email:, similar_domains:) }
+ rescue Mail::Field::IncompleteParseError
+ return { success: false }
+ end
+
+ private
+
+ def self.closest_domains(domain:)
+ KNOWN_DOMAINS.filter do |known_domain|
+ close_by_distance_of(domain, known_domain, distance: 1) ||
+ with_same_chars_and_close_by_distance_of(domain, known_domain, distance: 2)
+ end
+ end
+
+ def self.close_by_distance_of(a, b, distance:)
+ String::Similarity.levenshtein_distance(a, b) == distance
+ end
+
+ def self.with_same_chars_and_close_by_distance_of(a, b, distance:)
+ close_by_distance_of(a, b, distance: 2) && a.chars.sort == b.chars.sort
+ end
+
+ def self.suggestions(parsed_email:, similar_domains:)
+ similar_domains.map { Mail::Address.new("#{parsed_email.local}@#{_1}").to_s }
+ end
+end
diff --git a/app/lib/helpscout/api.rb b/app/lib/helpscout/api.rb
index c0538c7a9..bf5e9ed8b 100644
--- a/app/lib/helpscout/api.rb
+++ b/app/lib/helpscout/api.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Helpscout::API
MAILBOXES = 'mailboxes'
CONVERSATIONS = 'conversations'
@@ -7,6 +9,10 @@ class Helpscout::API
PHONES = 'phones'
OAUTH2_TOKEN = 'oauth2/token'
+ RATELIMIT_KEY = "helpscout-rate-limit-remaining"
+
+ class RateLimitError < StandardError; end;
+
def ready?
required_secrets = [
Rails.application.secrets.helpscout[:mailbox_id],
@@ -22,7 +28,7 @@ class Helpscout::API
})
end
- def create_conversation(email, subject, text, file)
+ def create_conversation(email, subject, text, blob)
body = {
subject: subject,
customer: customer(email),
@@ -34,7 +40,7 @@ class Helpscout::API
type: 'customer',
customer: customer(email),
text: text,
- attachments: attachments(file)
+ attachments: attachments(blob)
}
]
}.compact
@@ -42,6 +48,53 @@ class Helpscout::API
call_api(:post, CONVERSATIONS, body)
end
+ def list_old_conversations(status, before, page: 1)
+ body = {
+ page:,
+ status:, # active, open, closed, pending, spam. "all" does not work
+ query: "(
+ modifiedAt:[* TO #{before.iso8601}]
+ )",
+ sortField: "modifiedAt",
+ sortOrder: "desc"
+ }
+
+ response = call_api(:get, "#{CONVERSATIONS}?#{body.to_query}")
+ if !response.success?
+ raise StandardError, "Error while listing conversations: #{response.response_code} '#{response.body}'"
+ end
+
+ body = parse_response_body(response)
+ [body[:_embedded][:conversations], body[:page]]
+ end
+
+ def list_old_customers(before, page: 1)
+ body = {
+ page:,
+ query: "(
+ modifiedAt:[* TO #{before.iso8601}]
+ )",
+ sortField: "modifiedAt",
+ sortOrder: "desc"
+ }
+
+ response = call_api(:get, "#{CUSTOMERS}?#{body.to_query}")
+ if !response.success?
+ raise StandardError, "Error while listing customers: #{response.response_code} '#{response.body}'"
+ end
+
+ body = parse_response_body(response)
+ [body[:_embedded][:customers], body[:page]]
+ end
+
+ def delete_conversation(conversation_id)
+ call_api(:delete, "#{CONVERSATIONS}/#{conversation_id}")
+ end
+
+ def delete_customer(customer_id)
+ call_api(:delete, "#{CUSTOMERS}/#{customer_id}")
+ end
+
def add_phone_number(email, phone)
query = CGI.escape("(email:#{email})")
response = call_api(:get, "#{CUSTOMERS}?mailbox=#{user_support_mailbox_id}&query=#{query}")
@@ -76,13 +129,13 @@ class Helpscout::API
private
- def attachments(file)
- if file.present?
+ def attachments(blob)
+ if blob.present?
[
{
- fileName: file.original_filename,
- mimeType: file.content_type,
- data: Base64.strict_encode64(file.read)
+ fileName: blob.filename,
+ mimeType: blob.content_type,
+ data: Base64.strict_encode64(blob.download)
}
]
else
@@ -129,6 +182,17 @@ class Helpscout::API
body: body.to_json,
headers: headers
})
+ when :delete
+ Typhoeus.delete(url, {
+ body: body.to_json,
+ headers: headers
+ })
+ end.tap do |response|
+ Rails.cache.write(RATELIMIT_KEY, response.headers["X-Ratelimit-Remaining-Minute"], expires_in: 1.minute)
+
+ if response.response_code.to_i == 429
+ raise RateLimitError
+ end
end
end
diff --git a/app/lib/helpscout/form_adapter.rb b/app/lib/helpscout/form_adapter.rb
deleted file mode 100644
index 03c168f08..000000000
--- a/app/lib/helpscout/form_adapter.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-class Helpscout::FormAdapter
- attr_reader :params
-
- def self.options
- [
- [I18n.t(:question, scope: [:support, :index, TYPE_INFO]), TYPE_INFO, I18n.t("links.common.faq.contacter_service_en_charge_url")],
- [I18n.t(:question, scope: [:support, :index, TYPE_PERDU]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL],
- [I18n.t(:question, scope: [:support, :index, TYPE_INSTRUCTION]), TYPE_INSTRUCTION, I18n.t("links.common.faq.ou_en_est_mon_dossier_url")],
- [I18n.t(:question, scope: [:support, :index, TYPE_AMELIORATION]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL],
- [I18n.t(:question, scope: [:support, :index, TYPE_AUTRE]), TYPE_AUTRE]
- ]
- end
-
- def self.admin_options
- [
- [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_QUESTION], app_name: Current.application_name), ADMIN_TYPE_QUESTION],
- [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_RDV], app_name: Current.application_name), ADMIN_TYPE_RDV],
- [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_SOUCIS], app_name: Current.application_name), ADMIN_TYPE_SOUCIS],
- [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_PRODUIT]), ADMIN_TYPE_PRODUIT],
- [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_DEMANDE_COMPTE]), ADMIN_TYPE_DEMANDE_COMPTE],
- [I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_AUTRE]), ADMIN_TYPE_AUTRE]
- ]
- end
-
- def initialize(params = {}, api = nil)
- @params = params
- @api = api || Helpscout::API.new
- end
-
- TYPE_INFO = 'procedure_info'
- TYPE_PERDU = 'lost_user'
- TYPE_INSTRUCTION = 'instruction_info'
- TYPE_AMELIORATION = 'product'
- TYPE_AUTRE = 'other'
-
- ADMIN_TYPE_RDV = 'admin demande rdv'
- ADMIN_TYPE_QUESTION = 'admin question'
- ADMIN_TYPE_SOUCIS = 'admin soucis'
- ADMIN_TYPE_PRODUIT = 'admin suggestion produit'
- ADMIN_TYPE_DEMANDE_COMPTE = 'admin demande compte'
- ADMIN_TYPE_AUTRE = 'admin autre'
-
- def send_form
- conversation_id = create_conversation
-
- if conversation_id.present?
- add_tags(conversation_id)
- true
- else
- false
- end
- end
-
- private
-
- def add_tags(conversation_id)
- @api.add_tags(conversation_id, tags)
- end
-
- def tags
- (params[:tags].presence || []) + ['contact form']
- end
-
- def create_conversation
- response = @api.create_conversation(
- params[:email],
- params[:subject],
- params[:text],
- params[:file]
- )
-
- if response.success?
- if params[:phone].present?
- @api.add_phone_number(params[:email], params[:phone])
- end
- response.headers['Resource-ID']
- end
- end
-end
diff --git a/app/lib/helpscout/user_conversations_adapter.rb b/app/lib/helpscout/user_conversations_adapter.rb
index 12ed81dc1..6ba171a52 100644
--- a/app/lib/helpscout/user_conversations_adapter.rb
+++ b/app/lib/helpscout/user_conversations_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Fetch and compute monthly reports about the users conversations on Helpscout
class Helpscout::UserConversationsAdapter
def initialize(from, to)
diff --git a/app/lib/recovery/align_champ_with_dossier_revision.rb b/app/lib/recovery/align_champ_with_dossier_revision.rb
deleted file mode 100644
index 8fdba8dc9..000000000
--- a/app/lib/recovery/align_champ_with_dossier_revision.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-class Recovery::AlignChampWithDossierRevision
- def initialize(dossiers, progress: nil)
- @dossiers = dossiers
- @progress = progress
- @logs = []
- end
-
- attr_reader :logs
-
- def run(destroy_extra_champs: false)
- @logs = []
- bad_dossier_ids = find_broken_dossier_ids
-
- Dossier
- .where(id: bad_dossier_ids)
- .includes(:procedure, champs: { type_de_champ: :revisions })
- .find_each do |dossier|
- bad_champs = dossier.champs.filter { !dossier.revision_id.in?(_1.type_de_champ.revisions.ids) }
- bad_champs.each do |champ|
- type_de_champ = dossier.revision.types_de_champ.find { _1.stable_id == champ.stable_id }
- state = {
- champ_id: champ.id,
- champ_type_de_champ_id: champ.type_de_champ_id,
- dossier_id: dossier.id,
- dossier_revision_id: dossier.revision_id,
- procedure_id: dossier.procedure.id
- }
- if type_de_champ.present?
- logs << state.merge(status: :updated, type_de_champ_id: type_de_champ.id)
- champ.update_column(:type_de_champ_id, type_de_champ.id)
- else
- logs << state.merge(status: :not_found)
- champ.destroy! if destroy_extra_champs
- end
- end
- end
- end
-
- def find_broken_dossier_ids
- bad_dossier_ids = []
-
- @dossiers.in_batches(of: 15_000) do |dossiers|
- dossier_ids_revision_ids = dossiers.pluck(:id, :revision_id)
- dossier_ids = dossier_ids_revision_ids.map(&:first)
- dossier_ids_type_de_champ_ids = Champ.where(dossier_id: dossier_ids).pluck(:dossier_id, :type_de_champ_id)
- type_de_champ_ids = dossier_ids_type_de_champ_ids.map(&:second).uniq
- revision_ids_by_type_de_champ_id = ProcedureRevisionTypeDeChamp
- .where(type_de_champ_id: type_de_champ_ids)
- .pluck(:type_de_champ_id, :revision_id)
- .group_by(&:first).transform_values { _1.map(&:second).uniq }
-
- type_de_champ_ids_by_dossier_id = dossier_ids_type_de_champ_ids
- .group_by(&:first)
- .transform_values { _1.map(&:second).uniq }
-
- bad_dossier_ids += dossier_ids_revision_ids.filter do |(dossier_id, revision_id)|
- type_de_champ_ids_by_dossier_id.fetch(dossier_id, []).any? do |type_de_champ_id|
- !revision_id.in?(revision_ids_by_type_de_champ_id.fetch(type_de_champ_id, []))
- end
- end.map(&:first)
-
- @progress.inc(dossiers.count) if @progress
- end
-
- @progress.finish if @progress
-
- bad_dossier_ids
- end
-end
diff --git a/app/lib/recovery/exporter.rb b/app/lib/recovery/exporter.rb
index 4781896f1..cf05482bc 100644
--- a/app/lib/recovery/exporter.rb
+++ b/app/lib/recovery/exporter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Recovery
class Exporter
FILE_PATH = Rails.root.join('lib', 'data', 'export.dump')
@@ -10,7 +12,7 @@ module Recovery
:invites,
:traitements,
:transfer_logs,
- commentaires: { piece_jointe_attachment: :blob },
+ commentaires: { piece_jointe_attachments: :blob },
avis: { introduction_file_attachment: :blob, piece_justificative_file_attachment: :blob },
dossier_operation_logs: { serialized_attachment: :blob },
attestation: { pdf_attachment: :blob },
@@ -18,7 +20,7 @@ module Recovery
etablissement: :exercices,
revision: :procedure)
@dossiers = DossierPreloader.new(dossier_with_data,
- includes_for_dossier: [:geo_areas, etablissement: :exercices],
+ includes_for_champ: [:geo_areas, etablissement: :exercices],
includes_for_etablissement: [:exercices]).all
@file_path = file_path
end
diff --git a/app/lib/recovery/importer.rb b/app/lib/recovery/importer.rb
index 0edc4b6a7..92c7dbaad 100644
--- a/app/lib/recovery/importer.rb
+++ b/app/lib/recovery/importer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Recovery
class Importer
attr_reader :dossiers
@@ -112,9 +114,12 @@ module Recovery
end
end
- def import(pj)
- ActiveStorage::Blob.insert(pj.blob.attributes)
- ActiveStorage::Attachment.insert(pj.attributes)
+ def import(pjs)
+ attachments = pjs.respond_to?(:each) ? pjs : [pjs]
+ attachments.each do |pj|
+ ActiveStorage::Blob.insert(pj.blob.attributes)
+ ActiveStorage::Attachment.insert(pj.attributes)
+ end
end
end
end
diff --git a/app/lib/recovery/revision_exporter.rb b/app/lib/recovery/revision_exporter.rb
index fdbbfb40d..b14df75e1 100644
--- a/app/lib/recovery/revision_exporter.rb
+++ b/app/lib/recovery/revision_exporter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Recovery
class RevisionExporter
FILE_PATH = Rails.root.join('lib', 'data', 'revision', 'export.dump')
diff --git a/app/lib/recovery/revision_importer.rb b/app/lib/recovery/revision_importer.rb
index ab13d7040..8ba441d22 100644
--- a/app/lib/recovery/revision_importer.rb
+++ b/app/lib/recovery/revision_importer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Recovery
class RevisionImporter
attr_reader :revisions
diff --git a/app/lib/redcarpet/bare_renderer.rb b/app/lib/redcarpet/bare_renderer.rb
index d8944374c..6989a6ebf 100644
--- a/app/lib/redcarpet/bare_renderer.rb
+++ b/app/lib/redcarpet/bare_renderer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Redcarpet
class BareRenderer < Redcarpet::Render::HTML
include ActionView::Helpers::TagHelper
@@ -33,7 +35,7 @@ module Redcarpet
when :url
link(link, nil, link)
when :email
- # NOTE: As of Redcarpet 3.6.0, autolinking email containing is broken https://github.com/vmg/redcarpet/issues/402
+ # NOTE: As of Redcarpet 3.6.0, autolinking email containing underscore is broken https://github.com/vmg/redcarpet/issues/402
content_tag(:a, link, { href: "mailto:#{link}" })
else
link
diff --git a/app/lib/redcarpet/trusted_renderer.rb b/app/lib/redcarpet/trusted_renderer.rb
new file mode 100644
index 000000000..bdfa95d83
--- /dev/null
+++ b/app/lib/redcarpet/trusted_renderer.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Redcarpet
+ class TrustedRenderer < Redcarpet::Render::HTML
+ include ActionView::Helpers::TagHelper
+ include Sprockets::Rails::Helper
+ include ApplicationHelper
+
+ attr_reader :view_context
+
+ def initialize(view_context, extensions = {})
+ @view_context = view_context
+
+ super extensions
+ end
+
+ def link(href, title, content)
+ html_options = {
+ href: href
+ }
+
+ unless href.starts_with?('/')
+ html_options.merge!(title: new_tab_suffix(content), **external_link_attributes)
+ end
+
+ content_tag(:a, content, html_options, false)
+ end
+
+ def autolink(link, link_type)
+ case link_type
+ when :url
+ link(link, nil, link)
+ when :email
+ # NOTE: As of Redcarpet 3.6.0, autolinking email containing underscore is broken https://github.com/vmg/redcarpet/issues/402
+ content_tag(:a, link, { href: "mailto:#{link}" })
+ end
+ end
+
+ def image(link, title, alt_text)
+ # Extrait les attributs personnalisés s'ils existent sous la forme { aria-hidden=true } dans les []
+ custom_attributes = {}
+ if alt_text =~ /\s*\{(.+)\}$/
+ attr_string = Regexp.last_match(1)
+ alt_text = alt_text.sub(/\s*\{.+\}$/, '').strip
+ attr_string.split(' ').each do |attr|
+ key, value = attr.split('=')
+ custom_attributes[key.strip] = value.strip.delete('"')
+ end
+ end
+
+ # Combine les attributs standard et personnalisés
+ image_options = {
+ alt: alt_text,
+ title:,
+ loading: :lazy
+ }.merge(custom_attributes)
+
+ view_context.image_tag(link, image_options)
+ end
+
+ # rubocop:disable Rails/OutputSafety
+ def block_quote(raw_html)
+ if raw_html =~ /^\[!(INFO|WARNING)\]\n/
+ state = Regexp.last_match(1).downcase.to_sym
+ content = raw_html.sub(/^
\[!(?:INFO|WARNING)\]\n/, '
')
+ component = Dsfr::AlertComponent.new(state:, heading_level: "h2", extra_class_names: "fr-my-3w")
+ component.render_in(view_context) do |c|
+ c.with_body { content.html_safe }
+ end
+ else
+ view_context.content_tag(:blockquote, raw_html.html_safe)
+ end
+ end
+ # rubocop:enable Rails/OutputSafety
+ end
+end
diff --git a/app/lib/sanitizers/mail_scrubber.rb b/app/lib/sanitizers/mail_scrubber.rb
index 89a1eeaf5..4a1da83b2 100644
--- a/app/lib/sanitizers/mail_scrubber.rb
+++ b/app/lib/sanitizers/mail_scrubber.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Sanitizers
class MailScrubber < Rails::Html::PermitScrubber
def initialize
diff --git a/app/lib/sendinblue/api.rb b/app/lib/sendinblue/api.rb
index 522e96d34..ce5b92a0d 100644
--- a/app/lib/sendinblue/api.rb
+++ b/app/lib/sendinblue/api.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Sendinblue::API
def self.new_properly_configured!
api = self.new
diff --git a/app/lib/sent_mail.rb b/app/lib/sent_mail.rb
index 828dd62e1..a08e47e9d 100644
--- a/app/lib/sent_mail.rb
+++ b/app/lib/sent_mail.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Represent an email sent using an external API
class SentMail < Struct.new(:from, :to, :subject, :delivered_at, :status, :service_name, :external_url, keyword_init: true)
end
diff --git a/app/lib/typhoeus/cache/successful_requests_rails_cache.rb b/app/lib/typhoeus/cache/successful_requests_rails_cache.rb
index 5d95f6539..98be17b6f 100644
--- a/app/lib/typhoeus/cache/successful_requests_rails_cache.rb
+++ b/app/lib/typhoeus/cache/successful_requests_rails_cache.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Typhoeus
module Cache
# Cache successful Typhoeus requests in the Rails cache
diff --git a/app/lib/universign/api.rb b/app/lib/universign/api.rb
index eeb987ca1..6e273b4be 100644
--- a/app/lib/universign/api.rb
+++ b/app/lib/universign/api.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Universign::API
## Universign Timestamp POST API
# Official documentation is at https://help.universign.com/hc/fr/articles/360000898965-Guide-d-intégration-horodatage
diff --git a/app/mailers/administrateur_mailer.rb b/app/mailers/administrateur_mailer.rb
index 0ed0d6598..f3fdf264d 100644
--- a/app/mailers/administrateur_mailer.rb
+++ b/app/mailers/administrateur_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Preview all emails at http://localhost:3000/rails/mailers/administrateur_mailer
class AdministrateurMailer < ApplicationMailer
layout 'mailers/layout'
@@ -8,6 +10,8 @@ class AdministrateurMailer < ApplicationMailer
@expiration_date = @user.reset_password_sent_at + Devise.reset_password_within
@subject = "N'oubliez pas d’activer votre compte administrateur"
+ bypass_unverified_mail_protection!
+
mail(to: user.email,
subject: @subject,
reply_to: CONTACT_EMAIL)
diff --git a/app/mailers/administration_mailer.rb b/app/mailers/administration_mailer.rb
index baa26e321..e1dc84ae9 100644
--- a/app/mailers/administration_mailer.rb
+++ b/app/mailers/administration_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Preview all emails at http://localhost:3000/rails/mailers/administration_mailer
class AdministrationMailer < ApplicationMailer
layout 'mailers/layout'
@@ -8,6 +10,8 @@ class AdministrationMailer < ApplicationMailer
@author_name = "Équipe de #{APPLICATION_NAME}"
subject = "Activez votre compte administrateur"
+ bypass_unverified_mail_protection!
+
mail(to: user.email,
subject: subject,
reply_to: CONTACT_EMAIL)
@@ -16,6 +20,8 @@ class AdministrationMailer < ApplicationMailer
def refuse_admin(admin_email)
subject = "Votre demande de compte a été refusée"
+ bypass_unverified_mail_protection!
+
mail(to: admin_email,
subject: subject,
reply_to: CONTACT_EMAIL)
diff --git a/app/mailers/api_token_mailer.rb b/app/mailers/api_token_mailer.rb
index 0ab3f6dea..f9f906bc2 100644
--- a/app/mailers/api_token_mailer.rb
+++ b/app/mailers/api_token_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Preview all emails at http://localhost:3000/rails/mailers/api_token_mailer
class APITokenMailer < ApplicationMailer
helper MailerHelper
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index cb5124753..48c6d8180 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationMailer < ActionMailer::Base
include MailerDefaultsConfigurableConcern
include MailerDolistConcern
diff --git a/app/mailers/avis_mailer.rb b/app/mailers/avis_mailer.rb
index 1efa8116f..d3018920b 100644
--- a/app/mailers/avis_mailer.rb
+++ b/app/mailers/avis_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Preview all emails at http://localhost:3000/rails/mailers/avis_mailer
class AvisMailer < ApplicationMailer
helper MailerHelper
@@ -20,6 +22,25 @@ class AvisMailer < ApplicationMailer
end
end
+ def avis_invitation_and_confirm_email(user, token, avis, targeted_user_link = nil) # ensure re-entrance if existing AvisMailer.avis_invitation in queue
+ if avis.dossier.visible_by_administration?
+ targeted_user_link = avis.targeted_user_links
+ .find_or_create_by(target_context: 'avis',
+ target_model_type: Avis.name,
+ target_model_id: avis.id,
+ user: avis.expert.user)
+ email = user.email
+ @token = token
+ @avis = avis
+ @url = targeted_user_link_url(targeted_user_link)
+ subject = "Donnez votre avis sur le dossier nº #{@avis.dossier.id} (#{@avis.dossier.procedure.libelle})"
+
+ bypass_unverified_mail_protection!
+
+ mail(to: email, subject: subject)
+ end
+ end
+
# i18n-tasks-use t("avis_mailer.#{action}.subject")
def notify_new_commentaire_to_expert(dossier, avis, expert)
I18n.with_locale(dossier.user_locale) do
diff --git a/app/mailers/concerns/balanced_delivery_concern.rb b/app/mailers/concerns/balanced_delivery_concern.rb
index 486aadac4..c2974f92b 100644
--- a/app/mailers/concerns/balanced_delivery_concern.rb
+++ b/app/mailers/concerns/balanced_delivery_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BalancedDeliveryConcern
extend ActiveSupport::Concern
@@ -8,6 +10,10 @@ module BalancedDeliveryConcern
self.class.critical_email?(action_name)
end
+ def bypass_unverified_mail_protection!
+ headers[BalancerDeliveryMethod::BYPASS_UNVERIFIED_MAIL_PROTECTION] = true
+ end
+
private
def forced_delivery_provider?
diff --git a/app/mailers/concerns/mailer_dolist_concern.rb b/app/mailers/concerns/mailer_dolist_concern.rb
index 6db7c74d7..33681ca12 100644
--- a/app/mailers/concerns/mailer_dolist_concern.rb
+++ b/app/mailers/concerns/mailer_dolist_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MailerDolistConcern
extend ActiveSupport::Concern
diff --git a/app/mailers/concerns/mailer_monitoring_concern.rb b/app/mailers/concerns/mailer_monitoring_concern.rb
index dca983bcc..0c8b12455 100644
--- a/app/mailers/concerns/mailer_monitoring_concern.rb
+++ b/app/mailers/concerns/mailer_monitoring_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MailerMonitoringConcern
extend ActiveSupport::Concern
diff --git a/app/mailers/concerns/priority_delivery_concern.rb b/app/mailers/concerns/priority_delivery_concern.rb
index f8e68dd6f..21d9f4b7c 100644
--- a/app/mailers/concerns/priority_delivery_concern.rb
+++ b/app/mailers/concerns/priority_delivery_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module PriorityDeliveryConcern
extend ActiveSupport::Concern
included do
diff --git a/app/mailers/devise_user_mailer.rb b/app/mailers/devise_user_mailer.rb
index d9acb25c3..bfeb738c5 100644
--- a/app/mailers/devise_user_mailer.rb
+++ b/app/mailers/devise_user_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Preview all emails at http://localhost:3000/rails/mailers/devise_user_mailer
class DeviseUserMailer < Devise::Mailer
helper :application # gives access to all helpers defined within `application_helper`.
@@ -34,11 +36,19 @@ class DeviseUserMailer < Devise::Mailer
@procedure = opts[:procedure_after_confirmation] || nil
@prefill_token = opts[:prefill_token]
+ bypass_unverified_mail_protection!
+
I18n.with_locale(record.locale) do
super
end
end
+ def reset_password_instructions(record, token, opts = {})
+ bypass_unverified_mail_protection!
+
+ super
+ end
+
def self.critical_email?(action_name)
true
end
diff --git a/app/mailers/dossier_mailer.rb b/app/mailers/dossier_mailer.rb
index 1064af740..aa4fb081d 100644
--- a/app/mailers/dossier_mailer.rb
+++ b/app/mailers/dossier_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Preview all emails at http://localhost:3000/rails/mailers/dossier_mailer
class DossierMailer < ApplicationMailer
class AbortDeliveryError < StandardError; end
@@ -129,32 +131,32 @@ class DossierMailer < ApplicationMailer
mail(to: to_email, subject: @subject)
end
- def notify_deletion_to_administration(deleted_dossier, to_email)
+ def notify_deletion_to_administration(hidden_dossier, to_email)
configure_defaults_for_email(to_email)
- @subject = default_i18n_subject(dossier_id: deleted_dossier.dossier_id)
- @deleted_dossier = deleted_dossier
+ @subject = default_i18n_subject(dossier_id: hidden_dossier.id)
+ @hidden_dossier = hidden_dossier
mail(to: to_email, subject: @subject)
end
- def notify_automatic_deletion_to_user(deleted_dossiers, to_email)
+ def notify_automatic_deletion_to_user(hidden_dossiers, to_email)
configure_defaults_for_email(to_email)
- I18n.with_locale(deleted_dossiers.first.user_locale) do
- @state = deleted_dossiers.first.state
- @subject = default_i18n_subject(count: deleted_dossiers.size)
- @deleted_dossiers = deleted_dossiers
+ I18n.with_locale(hidden_dossiers.first.user_locale) do
+ @state = hidden_dossiers.first.state
+ @subject = default_i18n_subject(count: hidden_dossiers.size)
+ @hidden_dossiers = hidden_dossiers
mail(to: to_email, subject: @subject)
end
end
- def notify_automatic_deletion_to_administration(deleted_dossiers, to_email)
+ def notify_automatic_deletion_to_administration(hidden_dossiers, to_email)
configure_defaults_for_email(to_email)
- @subject = default_i18n_subject(count: deleted_dossiers.size)
- @deleted_dossiers = deleted_dossiers
+ @subject = default_i18n_subject(count: hidden_dossiers.size)
+ @hidden_dossiers = hidden_dossiers
mail(to: to_email, subject: @subject)
end
@@ -204,6 +206,8 @@ class DossierMailer < ApplicationMailer
def notify_transfer
@transfer = params[:dossier_transfer]
+ @user = User.find_by(email: @transfer.email)
+
configure_defaults_for_email(@transfer.email)
I18n.with_locale(@transfer.user_locale) do
diff --git a/app/mailers/expert_mailer.rb b/app/mailers/expert_mailer.rb
index de871bea4..64dd4bd43 100644
--- a/app/mailers/expert_mailer.rb
+++ b/app/mailers/expert_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExpertMailer < ApplicationMailer
helper MailerHelper
layout 'mailers/layout'
diff --git a/app/mailers/groupe_gestionnaire_mailer.rb b/app/mailers/groupe_gestionnaire_mailer.rb
index 1dc5f54bd..defbf2a91 100644
--- a/app/mailers/groupe_gestionnaire_mailer.rb
+++ b/app/mailers/groupe_gestionnaire_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GroupeGestionnaireMailer < ApplicationMailer
helper MailerHelper
layout 'mailers/layout'
diff --git a/app/mailers/groupe_instructeur_mailer.rb b/app/mailers/groupe_instructeur_mailer.rb
index 76f0e917c..9c00a8011 100644
--- a/app/mailers/groupe_instructeur_mailer.rb
+++ b/app/mailers/groupe_instructeur_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GroupeInstructeurMailer < ApplicationMailer
layout 'mailers/layout'
@@ -20,9 +22,9 @@ class GroupeInstructeurMailer < ApplicationMailer
@current_instructeur_email = current_instructeur_email
subject = if group.procedure.groupe_instructeurs.many?
- "Vous avez été ajouté(e) au groupe \"#{group.label}\" de la démarche \"#{group.procedure.libelle}\""
+ "Vous avez été ajouté(e) au groupe « #{group.label} » de la démarche « #{group.procedure.libelle} »"
else
- "Vous avez été affecté(e) à la démarche \"#{group.procedure.libelle}\""
+ "Vous avez été affecté(e) à la démarche « #{group.procedure.libelle} »"
end
mail(bcc: added_instructeur_emails, subject: subject)
diff --git a/app/mailers/instructeur_mailer.rb b/app/mailers/instructeur_mailer.rb
index f2d4f6239..7e971b9bd 100644
--- a/app/mailers/instructeur_mailer.rb
+++ b/app/mailers/instructeur_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Preview all emails at http://localhost:3000/rails/mailers/instructeur_mailer
class InstructeurMailer < ApplicationMailer
helper MailerHelper
@@ -34,6 +36,8 @@ class InstructeurMailer < ApplicationMailer
@login_token = login_token
subject = "Connexion sécurisée à #{Current.application_name}"
+ bypass_unverified_mail_protection!
+
mail(to: instructeur.email, subject: subject)
end
@@ -47,4 +51,21 @@ class InstructeurMailer < ApplicationMailer
def self.critical_email?(action_name)
action_name == "send_login_token"
end
+
+ def confirm_and_notify_added_instructeur(instructeur, group, current_instructeur_email)
+ @instructeur = instructeur
+ @group = group
+ @current_instructeur_email = current_instructeur_email
+ @reset_password_token = instructeur.user.send(:set_reset_password_token)
+
+ subject = if group.procedure.groupe_instructeurs.many?
+ "Vous avez été ajouté(e) au groupe \"#{group.label}\" de la démarche \"#{group.procedure.libelle}\""
+ else
+ "Vous avez été affecté(e) à la démarche \"#{group.procedure.libelle}\""
+ end
+
+ bypass_unverified_mail_protection!
+
+ mail(to: instructeur.email, subject: subject)
+ end
end
diff --git a/app/mailers/invite_mailer.rb b/app/mailers/invite_mailer.rb
index a769d5458..f94eb30b8 100644
--- a/app/mailers/invite_mailer.rb
+++ b/app/mailers/invite_mailer.rb
@@ -1,8 +1,12 @@
+# frozen_string_literal: true
+
# Preview all emails at http://localhost:3000/rails/mailers/invite_mailer
class InviteMailer < ApplicationMailer
layout 'mailers/layout'
def invite_user(invite)
+ bypass_unverified_mail_protection!
+
subject = "Participez à l'élaboration d’un dossier"
targeted_user_link = invite.targeted_user_link || invite.create_targeted_user_link(target_context: 'invite',
target_model: invite,
@@ -14,6 +18,8 @@ class InviteMailer < ApplicationMailer
end
def invite_guest(invite)
+ bypass_unverified_mail_protection!
+
subject = "#{invite.email_sender} vous invite à consulter un dossier"
targeted_user_link = invite.targeted_user_link || invite.create_targeted_user_link(target_context: 'invite',
target_model: invite)
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
index 4459e741d..230553d13 100644
--- a/app/mailers/notification_mailer.rb
+++ b/app/mailers/notification_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Preview all emails at http://localhost:3000/rails/mailers/notification_mailer
# A Notification is attached as a Comment to the relevant discussion,
@@ -8,6 +10,7 @@
class NotificationMailer < ApplicationMailer
before_action :set_dossier, except: [:send_notification_for_tiers, :send_accuse_lecture_notification]
before_action :set_services_publics_plus, only: :send_notification
+ before_action :set_jdma, only: :send_notification
helper ServiceHelper
helper MailerHelper
@@ -88,6 +91,12 @@ class NotificationMailer < ApplicationMailer
@services_publics_plus_url = ENV['SERVICES_PUBLICS_PLUS_URL'].presence
end
+ def set_jdma
+ if params[:state] == Dossier.states.fetch(:en_construction) && @dossier.procedure.monavis_embed
+ @jdma_html = @dossier.procedure.monavis_embed_html_source("email")
+ end
+ end
+
def set_dossier
@dossier = params[:dossier]
configure_defaults_for_user(@dossier.user)
diff --git a/app/mailers/phishing_alert_mailer.rb b/app/mailers/phishing_alert_mailer.rb
new file mode 100644
index 000000000..d2b8a6281
--- /dev/null
+++ b/app/mailers/phishing_alert_mailer.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class PhishingAlertMailer < ApplicationMailer
+ helper MailerHelper
+
+ layout 'mailers/layout'
+
+ def notify(user)
+ @user = user
+ @subject = "Détection d'une possible usurpation de votre compte"
+
+ mail(to: user.email, subject: @subject)
+ end
+
+ def self.critical_email?(action_name) = false
+end
diff --git a/app/mailers/preactivate_users_mailer.rb b/app/mailers/preactivate_users_mailer.rb
index d957b2fba..015293be1 100644
--- a/app/mailers/preactivate_users_mailer.rb
+++ b/app/mailers/preactivate_users_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PreactivateUsersMailer < ApplicationMailer
layout 'mailers/layout'
diff --git a/app/mailers/resend_attestation_mailer.rb b/app/mailers/resend_attestation_mailer.rb
index 395eb8062..07600dea5 100644
--- a/app/mailers/resend_attestation_mailer.rb
+++ b/app/mailers/resend_attestation_mailer.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
class ResendAttestationMailer < ApplicationMailer
include Rails.application.routes.url_helpers
def resend_attestation(dossier)
- to = dossier.user.email
+ to = dossier.user_email_for(:notification)
subject = "Nouvelle attestation pour votre dossier nº #{dossier.id}"
mail(to: to, subject: subject, body: body(dossier))
diff --git a/app/mailers/super_admin_mailer.rb b/app/mailers/super_admin_mailer.rb
index edbb050b6..6789ee3bc 100644
--- a/app/mailers/super_admin_mailer.rb
+++ b/app/mailers/super_admin_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SuperAdminMailer < ApplicationMailer
def dolist_report(to, csv_path)
attachments["dolist_report.csv"] = File.read(csv_path)
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index f45892699..9a0a3afe1 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailer < ApplicationMailer
helper MailerHelper
@@ -34,6 +36,12 @@ class UserMailer < ApplicationMailer
mail(to: email, subject: @subject)
end
+ def custom_confirmation_instructions(user, token)
+ @user = user
+ @token = token
+ mail(to: @user.email, subject: 'Confirmez votre email')
+ end
+
def invite_instructeur(user, reset_password_token)
@reset_password_token = reset_password_token
@user = user
@@ -41,6 +49,37 @@ class UserMailer < ApplicationMailer
configure_defaults_for_user(user)
+ bypass_unverified_mail_protection!
+
+ mail(to: user.email,
+ subject: subject,
+ reply_to: Current.contact_email)
+ end
+
+ def invite_tiers(user, token, dossier)
+ @token = token
+ @user = user
+ @dossier = dossier
+ subject = "Vérification de votre mail"
+
+ configure_defaults_for_user(user)
+
+ bypass_unverified_mail_protection!
+
+ mail(to: user.email,
+ subject: subject,
+ reply_to: Current.contact_email)
+ end
+
+ def resend_confirmation_email(user, token)
+ @token = token
+ @user = user
+ subject = "Vérification de votre mail"
+
+ configure_defaults_for_user(user)
+
+ bypass_unverified_mail_protection!
+
mail(to: user.email,
subject: subject,
reply_to: Current.contact_email)
@@ -54,6 +93,8 @@ class UserMailer < ApplicationMailer
configure_defaults_for_user(user)
+ bypass_unverified_mail_protection!
+
mail(to: user.email,
subject: subject,
reply_to: Current.contact_email)
@@ -90,7 +131,7 @@ class UserMailer < ApplicationMailer
def notify_after_closing(user, content, procedure = nil)
@user = user
- @subject = "Clôture d'une démarche sur Démarches simplifiées"
+ @subject = "Clôture d'une démarche sur #{Current.application_name}"
@procedure = procedure
@content = content
@@ -104,7 +145,8 @@ class UserMailer < ApplicationMailer
'france_connect_merge_confirmation',
"new_account_warning",
"ask_for_merge",
- "invite_instructeur"
+ "invite_instructeur",
+ "custom_confirmation_instructions"
].include?(action_name)
end
end
diff --git a/app/models/KeyableModel.rb b/app/models/KeyableModel.rb
index 846bde92b..ea9dd7563 100644
--- a/app/models/KeyableModel.rb
+++ b/app/models/KeyableModel.rb
@@ -1 +1,3 @@
+# frozen_string_literal: true
+
KeyableModel = Struct.new(:model_name, :to_key, :param_key, keyword_init: true)
diff --git a/app/models/address_proxy.rb b/app/models/address_proxy.rb
new file mode 100644
index 000000000..e8463b5d6
--- /dev/null
+++ b/app/models/address_proxy.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+class AddressProxy
+ ADDRESS_PARTS = [
+ :street_address,
+ :city_name,
+ :postal_code,
+ :city_code,
+ :departement_name,
+ :departement_code,
+ :region_name,
+ :region_code
+ ]
+
+ class ChampAddressPresenter
+ ADDRESS_PARTS.each do |address_part|
+ define_method(address_part) do
+ @data[address_part]
+ end
+ end
+
+ def initialize(champ)
+ @data = champ.value_json&.with_indifferent_access || {}
+ end
+ end
+
+ class EtablissementAddressPresenter
+ attr_reader(*ADDRESS_PARTS)
+
+ def initialize(etablissement)
+ @street_address = [etablissement.numero_voie, etablissement.type_voie, etablissement.nom_voie].compact.join(" ")
+ @city_name = etablissement.localite
+ @postal_code = etablissement.code_postal
+ @city_code = etablissement.code_insee_localite
+ if @postal_code
+ @departement_name = APIGeoService.departement_name_by_postal_code(@postal_code)
+ @departement_code = APIGeoService.departement_code(@departement_name)
+ @region_code = APIGeoService.region_code_by_departement(@departement_code)
+ @region_name = APIGeoService.region_name(@region_code)
+ else # adresse without postal_code, ex:
+ @departement_name, @departement_code, @region_code, @region_name = nil
+ end
+ end
+ end
+
+ delegate(*ADDRESS_PARTS, to: :@presenter)
+
+ def initialize(champ_or_etablissement)
+ @presenter = make(champ_or_etablissement)
+ end
+
+ def make(champ_or_etablissement)
+ case champ_or_etablissement
+ when Champ then ChampAddressPresenter.new(champ_or_etablissement)
+ when Etablissement then EtablissementAddressPresenter.new(champ_or_etablissement)
+ else raise NotImplementedError("Unsupported address from #{champ_or_etablissement.class.name}")
+ end
+ end
+end
diff --git a/app/models/administrateur.rb b/app/models/administrateur.rb
index 48b38840c..6913d469d 100644
--- a/app/models/administrateur.rb
+++ b/app/models/administrateur.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Administrateur < ApplicationRecord
include UserFindByConcern
UNUSED_ADMIN_THRESHOLD = ENV.fetch('UNUSED_ADMIN_THRESHOLD') { 6 }.to_i.months
diff --git a/app/models/administrateurs_instructeur.rb b/app/models/administrateurs_instructeur.rb
index 099f1bbc9..3e611bc7a 100644
--- a/app/models/administrateurs_instructeur.rb
+++ b/app/models/administrateurs_instructeur.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AdministrateursInstructeur < ApplicationRecord
belongs_to :administrateur
belongs_to :instructeur
diff --git a/app/models/administrateurs_procedure.rb b/app/models/administrateurs_procedure.rb
index 084a18781..4147f81ef 100644
--- a/app/models/administrateurs_procedure.rb
+++ b/app/models/administrateurs_procedure.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AdministrateursProcedure < ApplicationRecord
belongs_to :administrateur
belongs_to :procedure
diff --git a/app/models/agent_connect_information.rb b/app/models/agent_connect_information.rb
index fd341b4a6..e3321c37c 100644
--- a/app/models/agent_connect_information.rb
+++ b/app/models/agent_connect_information.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AgentConnectInformation < ApplicationRecord
belongs_to :instructeur
end
diff --git a/app/models/api_entreprise_token.rb b/app/models/api_entreprise_token.rb
index 0ebf50c2c..b3c1e5b7b 100644
--- a/app/models/api_entreprise_token.rb
+++ b/app/models/api_entreprise_token.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntrepriseToken
TokenError = Class.new(StandardError)
@@ -15,6 +17,10 @@ class APIEntrepriseToken
decoded_token.key?("exp") && decoded_token["exp"] <= Time.zone.now.to_i
end
+ def expiration
+ decoded_token.key?("exp") && Time.zone.at(decoded_token["exp"])
+ end
+
def role?(role)
roles.include?(role)
end
diff --git a/app/models/api_token.rb b/app/models/api_token.rb
index 9183c67c7..069990caa 100644
--- a/app/models/api_token.rb
+++ b/app/models/api_token.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIToken < ApplicationRecord
include ActiveRecord::SecureToken
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 1a770302e..c4690ad80 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
diff --git a/app/models/archive.rb b/app/models/archive.rb
index e64c96a0b..a04710756 100644
--- a/app/models/archive.rb
+++ b/app/models/archive.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# == Schema Information
class Archive < ApplicationRecord
include TransientModelsWithPurgeableJobConcern
diff --git a/app/models/assign_to.rb b/app/models/assign_to.rb
index a2f178235..19d1fd847 100644
--- a/app/models/assign_to.rb
+++ b/app/models/assign_to.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AssignTo < ApplicationRecord
belongs_to :instructeur, optional: false
belongs_to :groupe_instructeur, optional: false
@@ -8,28 +10,33 @@ class AssignTo < ApplicationRecord
def procedure_presentation_or_default_and_errors
errors = reset_procedure_presentation_if_invalid
+
if self.procedure_presentation.nil?
- self.procedure_presentation = build_procedure_presentation
- self.procedure_presentation.save if procedure_presentation.valid? && !procedure_presentation.persisted?
+ self.procedure_presentation = create_procedure_presentation!
end
+
[self.procedure_presentation, errors]
end
private
def reset_procedure_presentation_if_invalid
- if procedure_presentation&.invalid?
- # This is a last defense against invalid `ProcedurePresentation`s persistently
- # hindering instructeurs. Whenever this gets triggered, it means that there is
- # a bug somewhere else that we need to fix.
+ errors = begin
+ procedure_presentation.errors if procedure_presentation&.invalid?
+ rescue ActiveRecord::RecordNotFound => e
+ errors = ActiveModel::Errors.new(self)
+ errors.add(:procedure_presentation, e.message)
+ errors
+ end
- errors = procedure_presentation.errors
+ if errors.present?
Sentry.capture_message(
"Destroying invalid ProcedurePresentation",
- extra: { procedure_presentation: procedure_presentation.as_json }
+ extra: { procedure_presentation_id: procedure_presentation.id, errors: errors.full_messages }
)
self.procedure_presentation = nil
- errors
end
+
+ errors
end
end
diff --git a/app/models/attestation.rb b/app/models/attestation.rb
index 1bb486633..dad64b744 100644
--- a/app/models/attestation.rb
+++ b/app/models/attestation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Attestation < ApplicationRecord
belongs_to :dossier, optional: false
diff --git a/app/models/attestation_template.rb b/app/models/attestation_template.rb
index eea88355c..8aba5d352 100644
--- a/app/models/attestation_template.rb
+++ b/app/models/attestation_template.rb
@@ -1,12 +1,19 @@
+# frozen_string_literal: true
+
class AttestationTemplate < ApplicationRecord
include ActionView::Helpers::NumberHelper
include TagsSubstitutionConcern
- belongs_to :procedure, inverse_of: :attestation_template_v2
+ belongs_to :procedure, inverse_of: :attestation_template
has_one_attached :logo
has_one_attached :signature
+ enum state: {
+ draft: 'draft',
+ published: 'published'
+ }
+
validates :title, tags: true, if: -> { procedure.present? && version == 1 }
validates :body, tags: true, if: -> { procedure.present? && version == 1 }
validates :json_body, tags: true, if: -> { procedure.present? && version == 2 }
@@ -67,9 +74,10 @@ class AttestationTemplate < ApplicationRecord
}.freeze
def attestation_for(dossier)
- attestation = Attestation.new(title: replace_tags(title, dossier, escape: false))
+ attestation = Attestation.new
+ attestation.title = replace_tags(title, dossier, escape: false) if version == 1
attestation.pdf.attach(
- io: build_pdf(dossier),
+ io: StringIO.new(build_pdf(dossier)),
filename: "attestation-dossier-#{dossier.id}.pdf",
content_type: 'application/pdf',
# we don't want to run virus scanner on this file
@@ -79,26 +87,27 @@ class AttestationTemplate < ApplicationRecord
end
def unspecified_champs_for_dossier(dossier)
- champs_by_stable_id = dossier.champs_for_revision(root: true).index_by { "tdc#{_1.stable_id}" }
+ types_de_champ_by_tag_id = dossier.revision.types_de_champ.index_by { "tdc#{_1.stable_id}" }
used_tags.filter_map do |used_tag|
- corresponding_champ = champs_by_stable_id[used_tag]
+ corresponding_type_de_champ = types_de_champ_by_tag_id[used_tag]
- if corresponding_champ && corresponding_champ.blank?
- corresponding_champ
+ if corresponding_type_de_champ && dossier.project_champ(corresponding_type_de_champ, nil).blank?
+ corresponding_type_de_champ
end
end
end
def dup
- attestation_template = AttestationTemplate.new(title: title, body: body, footer: footer, activated: activated)
+ attestation_template = super
ClonePiecesJustificativesService.clone_attachments(self, attestation_template)
attestation_template
end
def logo_url
if logo.attached?
- Rails.application.routes.url_helpers.url_for(logo)
+ logo_variant = logo.variant(resize_to_limit: [400, 400])
+ logo_variant.key.present? ? logo_variant.processed.url : Rails.application.routes.url_helpers.url_for(logo)
end
end
@@ -179,7 +188,7 @@ class AttestationTemplate < ApplicationRecord
if dossier.present?
# 2x faster this way than with `replace_tags` which would reparse text
- used_tags = tiptap.used_tags_and_libelle_for(json.deep_symbolize_keys)
+ used_tags = TiptapService.used_tags_and_libelle_for(json.deep_symbolize_keys)
substitutions = tags_substitutions(used_tags, dossier, escape: false)
body = tiptap.to_html(json, substitutions)
@@ -202,17 +211,41 @@ class AttestationTemplate < ApplicationRecord
end
def used_tags
- used_tags_for(title) + used_tags_for(body)
+ if version == 2
+ json = json_body&.deep_symbolize_keys
+ TiptapService.used_tags_and_libelle_for(json.deep_symbolize_keys).map(&:first)
+ else
+ used_tags_for(title) + used_tags_for(body)
+ end
end
def build_pdf(dossier)
+ if version == 2
+ build_v2_pdf(dossier)
+ else
+ build_v1_pdf(dossier)
+ end
+ end
+
+ def build_v1_pdf(dossier)
attestation = render_attributes_for(dossier: dossier)
- attestation_view = ApplicationController.render(
+ ApplicationController.render(
template: 'administrateurs/attestation_templates/show',
formats: :pdf,
assigns: { attestation: attestation }
)
+ end
- StringIO.new(attestation_view)
+ def build_v2_pdf(dossier)
+ body = render_attributes_for(dossier:).fetch(:body)
+
+ html = ApplicationController.render(
+ template: '/administrateurs/attestation_template_v2s/show',
+ formats: [:html],
+ layout: 'attestation',
+ assigns: { attestation_template: self, body: body }
+ )
+
+ WeasyprintService.generate_pdf(html, { procedure_id: procedure.id, dossier_id: dossier.id })
end
end
diff --git a/app/models/avis.rb b/app/models/avis.rb
index 04f240270..4fb8d9ac3 100644
--- a/app/models/avis.rb
+++ b/app/models/avis.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Avis < ApplicationRecord
include EmailSanitizableConcern
diff --git a/app/models/batch_operation.rb b/app/models/batch_operation.rb
index 07e3c113d..b00d8b919 100644
--- a/app/models/batch_operation.rb
+++ b/app/models/batch_operation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BatchOperation < ApplicationRecord
enum operation: {
accepter: 'accepter',
@@ -82,13 +84,13 @@ class BatchOperation < ApplicationRecord
when BatchOperation.operations.fetch(:desarchiver)
dossier.desarchiver!
when BatchOperation.operations.fetch(:passer_en_instruction)
- dossier.passer_en_instruction(instructeur: instructeur)
+ dossier.passer_en_instruction!(instructeur: instructeur)
when BatchOperation.operations.fetch(:accepter)
- dossier.accepter(instructeur: instructeur, motivation: motivation, justificatif: justificatif_motivation)
+ dossier.accepter!(instructeur: instructeur, motivation: motivation, justificatif: justificatif_motivation)
when BatchOperation.operations.fetch(:refuser)
- dossier.refuser(instructeur: instructeur, motivation: motivation, justificatif: justificatif_motivation)
+ dossier.refuser!(instructeur: instructeur, motivation: motivation, justificatif: justificatif_motivation)
when BatchOperation.operations.fetch(:classer_sans_suite)
- dossier.classer_sans_suite(instructeur: instructeur, motivation: motivation, justificatif: justificatif_motivation)
+ dossier.classer_sans_suite!(instructeur: instructeur, motivation: motivation, justificatif: justificatif_motivation)
when BatchOperation.operations.fetch(:follow)
instructeur.follow(dossier)
when BatchOperation.operations.fetch(:repousser_expiration)
diff --git a/app/models/bill_signature.rb b/app/models/bill_signature.rb
index e5778a912..3a17ab4fd 100644
--- a/app/models/bill_signature.rb
+++ b/app/models/bill_signature.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BillSignature < ApplicationRecord
has_many :dossier_operation_logs
diff --git a/app/models/bulk_message.rb b/app/models/bulk_message.rb
index 5ec93da52..40d91d195 100644
--- a/app/models/bulk_message.rb
+++ b/app/models/bulk_message.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BulkMessage < ApplicationRecord
belongs_to :instructeur
belongs_to :procedure
diff --git a/app/models/champ.rb b/app/models/champ.rb
index b3672d9f0..e58dd4fb9 100644
--- a/app/models/champ.rb
+++ b/app/models/champ.rb
@@ -1,27 +1,35 @@
+# frozen_string_literal: true
+
class Champ < ApplicationRecord
include ChampConditionalConcern
include ChampsValidateConcern
+ self.ignored_columns += [:type_de_champ_id, :parent_id]
+
+ attr_readonly :stable_id
+
belongs_to :dossier, inverse_of: false, touch: true, optional: false
- belongs_to :type_de_champ, inverse_of: :champ, optional: false
- belongs_to :parent, class_name: 'Champ', optional: true
has_many_attached :piece_justificative_file
# We declare champ specific relationships (Champs::CarteChamp, Champs::SiretChamp and Champs::RepetitionChamp)
# here because otherwise we can't easily use includes in our queries.
has_many :geo_areas, -> { order(:created_at) }, dependent: :destroy, inverse_of: :champ
belongs_to :etablissement, optional: true, dependent: :destroy
- has_many :champs, foreign_key: :parent_id, inverse_of: :parent
delegate :procedure, to: :dossier
+ def type_de_champ
+ @type_de_champ ||= dossier.revision
+ .types_de_champ
+ .find(-> { raise "Type De Champ #{stable_id} not found in Revision #{dossier.revision_id}" }) { _1.stable_id == stable_id }
+ end
+
delegate :libelle,
:type_champ,
:description,
- :drop_down_list_options,
+ :drop_down_options,
:drop_down_other?,
- :drop_down_list_options?,
- :drop_down_list_enabled_non_empty_options,
+ :drop_down_options_with_other,
:drop_down_secondary_libelle,
:drop_down_secondary_description,
:collapsible_explanation_enabled?,
@@ -30,51 +38,28 @@ class Champ < ApplicationRecord
:current_section_level,
:exclude_from_export?,
:exclude_from_view?,
- :repetition?,
- :block?,
- :dossier_link?,
- :departement?,
- :region?,
- :textarea?,
- :titre_identite?,
- :header_section?,
- :checkbox?,
- :simple_drop_down_list?,
- :linked_drop_down_list?,
:non_fillable?,
:fillable?,
- :cnaf?,
- :dgfip?,
- :pole_emploi?,
- :mesri?,
- :rna?,
- :siret?,
- :carte?,
- :datetime?,
:mandatory?,
:prefillable?,
:refresh_after_update?,
:character_limit?,
:character_limit,
- :yes_no?,
:expression_reguliere,
:expression_reguliere_exemple_text,
:expression_reguliere_error_message,
to: :type_de_champ
+ delegate(*TypeDeChamp.type_champs.values.map { "#{_1}?".to_sym }, to: :type_de_champ)
+ delegate :piece_justificative_or_titre_identite?, :any_drop_down_list?, to: :type_de_champ
+
delegate :to_typed_id, :to_typed_id_for_query, to: :type_de_champ, prefix: true
delegate :revision, to: :dossier, prefix: true
- delegate :used_by_routing_rules?, to: :type_de_champ
scope :updated_since?, -> (date) { where('champs.updated_at > ?', date) }
- scope :public_only, -> { where(private: false) }
- scope :private_only, -> { where(private: true) }
- scope :root, -> { where(parent_id: nil) }
scope :prefilled, -> { where(prefilled: true) }
- before_create :set_dossier_id, if: :needs_dossier_id?
- before_validation :set_dossier_id, if: :needs_dossier_id?
before_save :cleanup_if_empty
before_save :normalize
after_update_commit :fetch_external_data_later
@@ -84,7 +69,7 @@ class Champ < ApplicationRecord
end
def child?
- parent_id.present?
+ row_id.present?
end
# used for the `required` html attribute
@@ -95,11 +80,15 @@ class Champ < ApplicationRecord
end
def mandatory_blank?
- mandatory? && blank?
+ type_de_champ.mandatory_blank?(self)
end
def blank?
- value.blank?
+ type_de_champ.champ_blank?(self)
+ end
+
+ def used_by_routing_rules?
+ procedure.used_by_routing_rules?(type_de_champ)
end
def search_terms
@@ -107,23 +96,15 @@ class Champ < ApplicationRecord
end
def to_s
- value.present? ? value.to_s : ''
+ type_de_champ.champ_value(self)
end
- def for_export(path = :value)
- path == :value ? value.presence : nil
+ def last_write_type_champ
+ TypeDeChamp::CHAMP_TYPE_TO_TYPE_CHAMP.fetch(type)
end
- def for_api
- value
- end
-
- def for_api_v2
- to_s
- end
-
- def for_tag(path = :value)
- path == :value && value.present? ? value.to_s : ''
+ def is_type?(type_champ)
+ last_write_type_champ == type_champ
end
def main_value_name
@@ -174,13 +155,13 @@ class Champ < ApplicationRecord
# However the field index makes it difficult to render a single field, independent from the ordering of the others.
#
# Luckily, this is only used to make the name unique, but the actual value is ignored when Rails parses nested
- # attributes. So instead of the field index, this method uses the champ id; which gives us an independent and
+ # attributes. So instead of the field index, this method uses the champ public_id; which gives us an independent and
# predictable input name.
def input_name
if private?
- "dossier[champs_private_attributes][#{id}]"
+ "dossier[champs_private_attributes][#{public_id}]"
else
- "dossier[champs_public_attributes][#{id}]"
+ "dossier[champs_public_attributes][#{public_id}]"
end
end
@@ -221,7 +202,7 @@ class Champ < ApplicationRecord
end
def clone(fork = false)
- champ_attributes = [:parent_id, :private, :row_id, :type, :type_de_champ_id, :stable_id, :stream]
+ champ_attributes = [:private, :row_id, :type, :stable_id, :stream]
value_attributes = fork || !private? ? [:value, :value_json, :data, :external_id] : []
relationships = fork || !private? ? [:etablissement, :geo_areas] : []
@@ -230,7 +211,7 @@ class Champ < ApplicationRecord
kopy.write_attribute(:stable_id, original.stable_id)
kopy.write_attribute(:stream, 'main')
end
- ClonePiecesJustificativesService.clone_attachments(original, kopy)
+ ClonePiecesJustificativesService.clone_attachments(original, kopy) if fork || !private?
end
end
@@ -251,15 +232,7 @@ class Champ < ApplicationRecord
end
def html_id
- "champ-#{public_id}"
- end
-
- def needs_dossier_id?
- !dossier_id && parent_id
- end
-
- def set_dossier_id
- self.dossier_id = parent.dossier_id
+ type_de_champ.html_id(row_id)
end
def cleanup_if_empty
@@ -279,7 +252,7 @@ class Champ < ApplicationRecord
return if value.nil?
return if value.present? && !value.include?("\u0000")
- self.value = value.delete("\u0000")
+ write_attribute(:value, value.delete("\u0000"))
end
class NotImplemented < ::StandardError
diff --git a/app/models/champ_presentations/base_presentation.rb b/app/models/champ_presentations/base_presentation.rb
new file mode 100644
index 000000000..6b043219b
--- /dev/null
+++ b/app/models/champ_presentations/base_presentation.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ChampPresentations
+ class BasePresentation
+ def block_level?
+ true
+ end
+ end
+end
diff --git a/app/models/champ_presentations/multiple_drop_down_list_presentation.rb b/app/models/champ_presentations/multiple_drop_down_list_presentation.rb
new file mode 100644
index 000000000..8660b06a2
--- /dev/null
+++ b/app/models/champ_presentations/multiple_drop_down_list_presentation.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class ChampPresentations::MultipleDropDownListPresentation < ChampPresentations::BasePresentation
+ attr_reader :selected_options
+
+ def initialize(selected_options)
+ @selected_options = selected_options
+ end
+
+ def to_s
+ selected_options.join(', ')
+ end
+
+ def to_tiptap_node
+ {
+ type: 'bulletList',
+ content: selected_options.map do |text|
+ {
+ type: 'listItem',
+ content: [
+ {
+ type: 'paragraph',
+ content: [
+ {
+ type: 'text',
+ text: text
+ }
+ ]
+ }
+ ]
+ }
+ end
+ }
+ end
+end
diff --git a/app/models/champ_presentations/repetition_presentation.rb b/app/models/champ_presentations/repetition_presentation.rb
new file mode 100644
index 000000000..000f74430
--- /dev/null
+++ b/app/models/champ_presentations/repetition_presentation.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+class ChampPresentations::RepetitionPresentation < ChampPresentations::BasePresentation
+ attr_reader :libelle
+ attr_reader :rows
+
+ def initialize(libelle, rows)
+ @libelle = libelle
+ @rows = rows
+ end
+
+ def to_s
+ ([libelle] + rows.map do |champs|
+ champs.map do |champ|
+ "#{champ.libelle} : #{champ}"
+ end.join("\n")
+ end).join("\n\n")
+ end
+
+ def to_tiptap_node
+ {
+ type: 'orderedList',
+ attrs: { class: 'tdc-repetition' },
+ content: rows.map do |champs|
+ {
+ type: 'listItem',
+ content: [
+ {
+ type: 'descriptionList',
+ content: champs.map do |champ|
+ [
+ {
+ type: 'descriptionTerm',
+ attrs: champ.blank? ? { class: 'invisible' } : nil, # still render libelle so width & alignment are preserved
+ content: [
+ {
+ type: 'text',
+ text: champ.libelle
+ }
+ ]
+ }.compact,
+ {
+ type: 'descriptionDetails',
+ content: [
+ {
+ type: 'text',
+ text: champ.to_s
+ }
+ ]
+ }
+ ]
+ end.flatten
+ }
+ ]
+ }
+ end
+ }
+ end
+end
diff --git a/app/models/champs/address_champ.rb b/app/models/champs/address_champ.rb
index 751b9844a..f0be9ab33 100644
--- a/app/models/champs/address_champ.rb
+++ b/app/models/champs/address_champ.rb
@@ -1,12 +1,10 @@
+# frozen_string_literal: true
+
class Champs::AddressChamp < Champs::TextChamp
def full_address?
data.present?
end
- def feature
- data.to_json if full_address?
- end
-
def feature=(value)
if value.blank?
self.data = nil
@@ -22,6 +20,14 @@ class Champs::AddressChamp < Champs::TextChamp
self.data = nil
end
+ def selected_items
+ if value.present?
+ [{ value:, label: value, data: full_address? ? data : nil }]
+ else
+ []
+ end
+ end
+
def address
full_address? ? data : nil
end
@@ -38,36 +44,6 @@ class Champs::AddressChamp < Champs::TextChamp
end
end
- def to_s
- address_label.presence || ''
- end
-
- def for_tag(path = :value)
- case path
- when :value
- address_label
- when :departement
- departement_code_and_name || ''
- when :commune
- commune_name || ''
- end
- end
-
- def for_export(path = :value)
- case path
- when :value
- value.present? ? address_label : nil
- when :departement
- departement_code_and_name
- when :commune
- commune_name
- end
- end
-
- def for_api
- address_label
- end
-
def code_departement
if full_address?
address.fetch('department_code')
diff --git a/app/models/champs/annuaire_education_champ.rb b/app/models/champs/annuaire_education_champ.rb
index e691b707b..3d5534e6d 100644
--- a/app/models/champs/annuaire_education_champ.rb
+++ b/app/models/champs/annuaire_education_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::AnnuaireEducationChamp < Champs::TextChamp
def fetch_external_data?
true
@@ -7,14 +9,11 @@ class Champs::AnnuaireEducationChamp < Champs::TextChamp
APIEducation::AnnuaireEducationAdapter.new(external_id).to_params
end
- def update_with_external_data!(data:)
- if data&.is_a?(Hash) && data['nom_etablissement'].present? && data['nom_commune'].present? && data['identifiant_de_l_etablissement'].present?
- update!(
- data: data,
- value: "#{data['nom_etablissement']}, #{data['nom_commune']} (#{data['identifiant_de_l_etablissement']})"
- )
+ def selected_items
+ if external_id.present?
+ [{ value: external_id, label: value }]
else
- update!(data: data)
+ []
end
end
end
diff --git a/app/models/champs/boolean_champ.rb b/app/models/champs/boolean_champ.rb
index 745a3536d..1d4f8f63c 100644
--- a/app/models/champs/boolean_champ.rb
+++ b/app/models/champs/boolean_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::BooleanChamp < Champ
TRUE_VALUE = 'true'
FALSE_VALUE = 'false'
@@ -17,28 +19,8 @@ class Champs::BooleanChamp < Champ
end
end
- def to_s
- processed_value
- end
-
- def for_tag(path = :value)
- processed_value
- end
-
- def for_export(path = :value)
- processed_value
- end
-
- def for_api_v2
- true? ? 'true' : 'false'
- end
-
private
- def processed_value
- true? ? 'Oui' : 'Non'
- end
-
def set_value_to_nil
self.value = nil
end
diff --git a/app/models/champs/carte_champ.rb b/app/models/champs/carte_champ.rb
index c19d2d913..d5eb51fb2 100644
--- a/app/models/champs/carte_champ.rb
+++ b/app/models/champs/carte_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::CarteChamp < Champ
# Default map location. Center of the World, ahm, France...
DEFAULT_LON = 2.428462
@@ -14,8 +16,10 @@ class Champs::CarteChamp < Champ
# We are not using scopes here as we want to access
# the following collections on unsaved records.
def cadastres
- geo_areas.filter do |area|
- area.source == GeoArea.sources.fetch(:cadastre)
+ if cadastres?
+ geo_areas.filter { _1.source == GeoArea.sources.fetch(:cadastre) }
+ else
+ []
end
end
@@ -83,18 +87,6 @@ class Champs::CarteChamp < Champ
end
end
- def for_api
- nil
- end
-
- def for_export(path = :value)
- geo_areas.map(&:label).join("\n")
- end
-
- def blank?
- geo_areas.blank?
- end
-
private
def selection_utilisateur_legacy_geometry
diff --git a/app/models/champs/checkbox_champ.rb b/app/models/champs/checkbox_champ.rb
index f58bc76d0..111eaeb00 100644
--- a/app/models/champs/checkbox_champ.rb
+++ b/app/models/champs/checkbox_champ.rb
@@ -1,12 +1,6 @@
+# frozen_string_literal: true
+
class Champs::CheckboxChamp < Champs::BooleanChamp
- def for_export(path = :value)
- true? ? 'on' : 'off'
- end
-
- def mandatory_blank?
- mandatory? && (blank? || !true?)
- end
-
def legend_label?
false
end
@@ -15,11 +9,6 @@ class Champs::CheckboxChamp < Champs::BooleanChamp
[[I18n.t('activerecord.attributes.type_de_champ.type_champs.checkbox_true'), true], [I18n.t('activerecord.attributes.type_de_champ.type_champs.checkbox_false'), false]]
end
- # TODO remove when normalize_checkbox_values is over
- def true?
- value_with_legacy == TRUE_VALUE
- end
-
def html_label?
false
end
@@ -27,11 +16,4 @@ class Champs::CheckboxChamp < Champs::BooleanChamp
def single_checkbox?
true
end
-
- private
-
- # TODO remove when normalize_checkbox_values is over
- def value_with_legacy
- value == 'on' ? TRUE_VALUE : value
- end
end
diff --git a/app/models/champs/civilite_champ.rb b/app/models/champs/civilite_champ.rb
index 6e7be24f9..7a1f2a254 100644
--- a/app/models/champs/civilite_champ.rb
+++ b/app/models/champs/civilite_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::CiviliteChamp < Champ
validates :value, inclusion: ["M.", "Mme"], allow_nil: true, allow_blank: false, if: :validate_champ_value_or_prefill?
diff --git a/app/models/champs/cnaf_champ.rb b/app/models/champs/cnaf_champ.rb
index e3bd048e9..c0cdc9383 100644
--- a/app/models/champs/cnaf_champ.rb
+++ b/app/models/champs/cnaf_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::CnafChamp < Champs::TextChamp
# see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/cnaf-input-validation.middleware.ts
@@ -6,10 +8,6 @@ class Champs::CnafChamp < Champs::TextChamp
store_accessor :value_json, :numero_allocataire, :code_postal
- def blank?
- external_id.nil?
- end
-
def fetch_external_data?
true
end
diff --git a/app/models/champs/cojo_champ.rb b/app/models/champs/cojo_champ.rb
index be2ab2311..a770521eb 100644
--- a/app/models/champs/cojo_champ.rb
+++ b/app/models/champs/cojo_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::COJOChamp < Champ
store_accessor :value_json, :accreditation_number, :accreditation_birthdate
store_accessor :data, :accreditation_success, :accreditation_first_name, :accreditation_last_name
@@ -18,10 +20,6 @@ class Champs::COJOChamp < Champ
accreditation_success == false
end
- def blank?
- accreditation_success != true
- end
-
def fetch_external_data?
true
end
@@ -34,10 +32,6 @@ class Champs::COJOChamp < Champ
COJOService.new.(accreditation_number:, accreditation_birthdate:)
end
- def to_s
- "#{accreditation_number} – #{accreditation_birthdate}"
- end
-
def accreditation_number_input_id
"#{input_id}-accreditation_number"
end
@@ -54,7 +48,7 @@ class Champs::COJOChamp < Champ
def update_external_id
if accreditation_number_changed? || accreditation_birthdate_changed?
- if accreditation_number.present? && accreditation_birthdate.present? && /\A\d+\z/.match?(accreditation_number)
+ if accreditation_number.present? && accreditation_birthdate.present? && /\A[\d-]+\z/.match?(accreditation_number)
self.external_id = { accreditation_number:, accreditation_birthdate: }.to_json
else
self.external_id = nil
diff --git a/app/models/champs/commune_champ.rb b/app/models/champs/commune_champ.rb
index 8dc7c9903..a1e69ba48 100644
--- a/app/models/champs/commune_champ.rb
+++ b/app/models/champs/commune_champ.rb
@@ -1,28 +1,11 @@
+# frozen_string_literal: true
+
class Champs::CommuneChamp < Champs::TextChamp
store_accessor :value_json, :code_departement, :code_postal, :code_region
before_save :on_codes_change, if: :should_refresh_after_code_change?
- def for_export(path = :value)
- case path
- when :value
- to_s
- when :departement
- departement_code_and_name || ''
- when :code
- code || ''
- end
- end
-
- def for_tag(path = :value)
- case path
- when :value
- to_s
- when :departement
- departement_code_and_name || ''
- when :code
- code || ''
- end
- end
+ validates :external_id, presence: true, if: -> { validate_champ_value_or_prefill? && value.present? }
+ after_validation :instrument_external_id_error, if: -> { errors.include?(:external_id) }
def departement_name
APIGeoService.departement_name(code_departement)
@@ -50,26 +33,47 @@ class Champs::CommuneChamp < Champs::TextChamp
code_postal.present?
end
- def code_postal=(value)
- super(value&.gsub(/[[:space:]]/, ''))
- end
-
alias postal_code code_postal
def name
APIGeoService.safely_normalize_city_name(code_departement, code, safe_to_s)
end
- def to_s
- code_postal? ? "#{name} (#{code_postal})" : name
- end
-
def code
external_id
end
def selected
- code
+ code? ? "#{code}-#{code_postal}" : nil
+ end
+
+ def selected_items
+ if code?
+ [{ label: to_s, value: selected }]
+ else
+ []
+ end
+ end
+
+ def code=(code)
+ if code.blank?
+ self.code_departement = nil
+ self.code_postal = nil
+ self.external_id = nil
+ self.value = nil
+ elsif code.match?(/-/)
+ codes = code.split('-')
+ self.external_id = codes.first
+ self.code_postal = codes.second
+ else
+ self.external_id = code
+ end
+ end
+
+ private
+
+ def safe_to_s
+ value.present? ? value.to_s : ''
end
def communes
@@ -80,12 +84,6 @@ class Champs::CommuneChamp < Champs::TextChamp
end
end
- private
-
- def safe_to_s
- value.present? ? value.to_s : ''
- end
-
def on_codes_change
return if !code?
@@ -106,4 +104,11 @@ class Champs::CommuneChamp < Champs::TextChamp
def should_refresh_after_code_change?
!departement? || code_postal_changed? || external_id_changed?
end
+
+ def instrument_external_id_error
+ Sentry.capture_message(
+ "Commune with value and no external id Edge case reached",
+ extra: { request_id: Current.request_id }
+ )
+ end
end
diff --git a/app/models/champs/date_champ.rb b/app/models/champs/date_champ.rb
index 4a9d1a215..5f6f447f4 100644
--- a/app/models/champs/date_champ.rb
+++ b/app/models/champs/date_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::DateChamp < Champ
before_validation :convert_to_iso8601, unless: -> { validation_context == :prefill }
validate :iso_8601
@@ -6,16 +8,6 @@ class Champs::DateChamp < Champ
# Text search is pretty useless for dates so we’re not including these champs
end
- def to_s
- value.present? ? I18n.l(Time.zone.parse(value), format: '%d %B %Y') : ""
- rescue ArgumentError
- value.presence || "" # old dossiers can have not parseable dates
- end
-
- def for_tag(path = :value)
- to_s if path == :value
- end
-
private
def convert_to_iso8601
diff --git a/app/models/champs/datetime_champ.rb b/app/models/champs/datetime_champ.rb
index 93983b5dd..2627fcde0 100644
--- a/app/models/champs/datetime_champ.rb
+++ b/app/models/champs/datetime_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::DatetimeChamp < Champ
before_validation :convert_to_iso8601, unless: -> { validation_context == :prefill }
validate :iso_8601
@@ -6,14 +8,6 @@ class Champs::DatetimeChamp < Champ
# Text search is pretty useless for datetimes so we’re not including these champs
end
- def to_s
- value.present? ? I18n.l(Time.zone.parse(value)) : ""
- end
-
- def for_tag(path = :value)
- value.present? ? I18n.l(Time.zone.parse(value)) : ""
- end
-
private
def convert_to_iso8601
diff --git a/app/models/champs/decimal_number_champ.rb b/app/models/champs/decimal_number_champ.rb
index 856c92a4c..04e1d9af5 100644
--- a/app/models/champs/decimal_number_champ.rb
+++ b/app/models/champs/decimal_number_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::DecimalNumberChamp < Champ
before_validation :format_value
@@ -17,25 +19,11 @@ class Champs::DecimalNumberChamp < Champ
}
}, if: :validate_champ_value_or_prefill?
- def for_export(path = :value)
- processed_value
- end
-
- def for_api
- processed_value
- end
-
private
def format_value
return if value.blank?
- self.value = value.tr(",", ".")
- end
-
- def processed_value
- return unless valid_champ_value?
-
- value&.to_f
+ self.value = value.tr(",", ".").gsub(/[[:space:]]/, "")
end
end
diff --git a/app/models/champs/departement_champ.rb b/app/models/champs/departement_champ.rb
index d599e6249..b6a84f421 100644
--- a/app/models/champs/departement_champ.rb
+++ b/app/models/champs/departement_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::DepartementChamp < Champs::TextChamp
store_accessor :value_json, :code_region
@@ -5,36 +7,6 @@ class Champs::DepartementChamp < Champs::TextChamp
validate :external_id_in_departement_codes, if: -> { validate_champ_value_or_prefill? && !external_id.nil? }
before_save :store_code_region
- def for_export(path = :value)
- case path
- when :code
- code
- when :value
- name
- end
- end
-
- def to_s
- formatted_value
- end
-
- def for_tag(path = :value)
- case path
- when :code
- code
- when :value
- formatted_value
- end
- end
-
- def for_api
- formatted_value
- end
-
- def for_api_v2
- formatted_value.tr('–', '-')
- end
-
def selected
code
end
@@ -71,10 +43,6 @@ class Champs::DepartementChamp < Champs::TextChamp
private
- def formatted_value
- blank? ? "" : "#{code} – #{name}"
- end
-
def value_in_departement_names
return if value.in?(APIGeoService.departements.pluck(:name))
diff --git a/app/models/champs/dgfip_champ.rb b/app/models/champs/dgfip_champ.rb
index fa5107343..c64e55b9a 100644
--- a/app/models/champs/dgfip_champ.rb
+++ b/app/models/champs/dgfip_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::DgfipChamp < Champs::TextChamp
# see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/dgfip-input-validation.middleware.ts
validates :numero_fiscal, format: { with: /\A\w{13,14}\z/ }, if: -> { reference_avis.present? && validate_champ_value_or_prefill? }
@@ -5,10 +7,6 @@ class Champs::DgfipChamp < Champs::TextChamp
store_accessor :value_json, :numero_fiscal, :reference_avis
- def blank?
- external_id.nil?
- end
-
def fetch_external_data?
true
end
diff --git a/app/models/champs/dossier_link_champ.rb b/app/models/champs/dossier_link_champ.rb
index ba73ddce6..110cb3ce4 100644
--- a/app/models/champs/dossier_link_champ.rb
+++ b/app/models/champs/dossier_link_champ.rb
@@ -1,8 +1,17 @@
+# frozen_string_literal: true
+
class Champs::DossierLinkChamp < Champ
validate :value_integerable, if: -> { value.present? }, on: :prefill
+ validate :dossier_exists, if: -> { validate_champ_value? && !value.nil? }
private
+ def dossier_exists
+ if mandatory? && !Dossier.exists?(value)
+ errors.add(:value, :not_found)
+ end
+ end
+
def value_integerable
Integer(value)
rescue ArgumentError
diff --git a/app/models/champs/drop_down_list_champ.rb b/app/models/champs/drop_down_list_champ.rb
index 6cdd094d5..4c5306f6c 100644
--- a/app/models/champs/drop_down_list_champ.rb
+++ b/app/models/champs/drop_down_list_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::DropDownListChamp < Champ
store_accessor :value_json, :other
THRESHOLD_NB_OPTIONS_AS_RADIO = 5
@@ -7,15 +9,11 @@ class Champs::DropDownListChamp < Champ
validate :value_is_in_options, if: -> { !(value.blank? || drop_down_other?) && validate_champ_value_or_prefill? }
def render_as_radios?
- enabled_non_empty_options.size <= THRESHOLD_NB_OPTIONS_AS_RADIO
+ drop_down_options.size <= THRESHOLD_NB_OPTIONS_AS_RADIO
end
def render_as_combobox?
- enabled_non_empty_options.size >= THRESHOLD_NB_OPTIONS_AS_AUTOCOMPLETE
- end
-
- def options?
- drop_down_list_options?
+ drop_down_options.size >= THRESHOLD_NB_OPTIONS_AS_AUTOCOMPLETE
end
def html_label?
@@ -30,12 +28,8 @@ class Champs::DropDownListChamp < Champ
other? ? OTHER : value
end
- def enabled_non_empty_options(other: false)
- drop_down_list_enabled_non_empty_options(other:)
- end
-
def other?
- drop_down_other? && (other || (value.present? && enabled_non_empty_options.exclude?(value)))
+ drop_down_other? && (other || (value.present? && drop_down_options.exclude?(value)))
end
def value=(value)
@@ -62,18 +56,10 @@ class Champs::DropDownListChamp < Champ
options.include?(value)
end
- def remove_option(options, touch = false)
- if touch
- update(value: nil)
- else
- update_column(:value, nil)
- end
- end
-
private
def value_is_in_options
- return if enabled_non_empty_options.include?(value)
+ return if drop_down_options.include?(value)
errors.add(:value, :not_in_options)
end
diff --git a/app/models/champs/email_champ.rb b/app/models/champs/email_champ.rb
index 1564e5ba0..6a1f50306 100644
--- a/app/models/champs/email_champ.rb
+++ b/app/models/champs/email_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::EmailChamp < Champs::TextChamp
include EmailSanitizableConcern
before_validation -> { sanitize_email(:value) }
diff --git a/app/models/champs/engagement_juridique_champ.rb b/app/models/champs/engagement_juridique_champ.rb
index a9241eb04..31118063a 100644
--- a/app/models/champs/engagement_juridique_champ.rb
+++ b/app/models/champs/engagement_juridique_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::EngagementJuridiqueChamp < Champ
# cf: https://communaute.chorus-pro.gouv.fr/documentation/creer-un-engagement/#1522314752186-a34f3662-0644b5d1-16c22add-8ea097de-3a0a
validates_with ExpressionReguliereValidator,
diff --git a/app/models/champs/epci_champ.rb b/app/models/champs/epci_champ.rb
index 6f11b462a..bdedee60a 100644
--- a/app/models/champs/epci_champ.rb
+++ b/app/models/champs/epci_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::EpciChamp < Champs::TextChamp
store_accessor :value_json, :code_departement, :code_region
before_validation :on_departement_change
@@ -7,28 +9,6 @@ class Champs::EpciChamp < Champs::TextChamp
validate :external_id_in_departement_epci_codes, if: -> { !(code_departement.nil? || external_id.nil?) && validate_champ_value_or_prefill? }
validate :value_in_departement_epci_names, if: -> { !(code_departement.nil? || external_id.nil? || value.nil?) && validate_champ_value_or_prefill? }
- def for_export(path = :value)
- case path
- when :value
- value
- when :code
- code
- when :departement
- departement_code_and_name
- end
- end
-
- def for_tag(path = :value)
- case path
- when :value
- value
- when :code
- code
- when :departement
- departement_code_and_name
- end
- end
-
def departement_name
APIGeoService.departement_name(code_departement)
end
diff --git a/app/models/champs/explication_champ.rb b/app/models/champs/explication_champ.rb
index d15bd2e1d..1e38b8174 100644
--- a/app/models/champs/explication_champ.rb
+++ b/app/models/champs/explication_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::ExplicationChamp < Champs::TextChamp
def search_terms
# The user cannot enter any information here so it doesn’t make much sense to search
diff --git a/app/models/champs/expression_reguliere_champ.rb b/app/models/champs/expression_reguliere_champ.rb
index c8bc48008..9fb44375f 100644
--- a/app/models/champs/expression_reguliere_champ.rb
+++ b/app/models/champs/expression_reguliere_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::ExpressionReguliereChamp < Champ
validates_with ExpressionReguliereValidator, if: :validate_champ_value_or_prefill?
end
diff --git a/app/models/champs/header_section_champ.rb b/app/models/champs/header_section_champ.rb
index d1b4d21d1..a01c57949 100644
--- a/app/models/champs/header_section_champ.rb
+++ b/app/models/champs/header_section_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::HeaderSectionChamp < Champ
def search_terms
# The user cannot enter any information here so it doesn’t make much sense to search
diff --git a/app/models/champs/iban_champ.rb b/app/models/champs/iban_champ.rb
index 0d17f86e7..cc26eafa8 100644
--- a/app/models/champs/iban_champ.rb
+++ b/app/models/champs/iban_champ.rb
@@ -1,15 +1,9 @@
+# frozen_string_literal: true
+
class Champs::IbanChamp < Champ
validates_with IbanValidator, if: :validate_champ_value_or_prefill?
after_validation :format_iban
- def for_api
- to_s.gsub(/\s+/, '')
- end
-
- def for_api_v2
- for_api
- end
-
private
def format_iban
diff --git a/app/models/champs/integer_number_champ.rb b/app/models/champs/integer_number_champ.rb
index 69f95adc4..0456ba1b6 100644
--- a/app/models/champs/integer_number_champ.rb
+++ b/app/models/champs/integer_number_champ.rb
@@ -1,4 +1,8 @@
+# frozen_string_literal: true
+
class Champs::IntegerNumberChamp < Champ
+ before_validation :format_value
+
validates :value, numericality: {
only_integer: true,
allow_nil: true,
@@ -9,19 +13,9 @@ class Champs::IntegerNumberChamp < Champ
}
}, if: :validate_champ_value_or_prefill?
- def for_export(path = :value)
- processed_value
- end
+ def format_value
+ return if value.blank?
- def for_api
- processed_value
- end
-
- private
-
- def processed_value
- return unless valid_champ_value?
-
- value&.to_i
+ self.value = value.gsub(/[[:space:]]/, "")
end
end
diff --git a/app/models/champs/linked_drop_down_list_champ.rb b/app/models/champs/linked_drop_down_list_champ.rb
index 6a1edcaef..9254cfca0 100644
--- a/app/models/champs/linked_drop_down_list_champ.rb
+++ b/app/models/champs/linked_drop_down_list_champ.rb
@@ -1,23 +1,21 @@
+# frozen_string_literal: true
+
class Champs::LinkedDropDownListChamp < Champ
delegate :primary_options, :secondary_options, to: :type_de_champ
- def options?
- drop_down_list_options?
- end
-
def primary_value
- if value.present?
- JSON.parse(value)[0]
- else
+ if type_de_champ.champ_blank?(self)
''
+ else
+ JSON.parse(value)[0]
end
end
def secondary_value
- if value.present?
- JSON.parse(value)[1]
- else
+ if type_de_champ.champ_blank?(self)
''
+ else
+ JSON.parse(value)[1]
end
end
@@ -37,41 +35,6 @@ class Champs::LinkedDropDownListChamp < Champ
:primary_value
end
- def to_s
- value.present? ? [primary_value, secondary_value].filter(&:present?).join(' / ') : ""
- end
-
- def for_tag(path = :value)
- case path
- when :primary
- primary_value
- when :secondary
- secondary_value
- when :value
- value.present? ? [primary_value, secondary_value].filter(&:present?).join(' / ') : ""
- end
- end
-
- def for_export(path = :value)
- case path
- when :primary
- primary_value
- when :secondary
- secondary_value
- when :value
- value.present? ? "#{primary_value || ''};#{secondary_value || ''}" : nil
- end
- end
-
- def for_api
- value.present? ? { primary: primary_value, secondary: secondary_value } : nil
- end
-
- def blank?
- primary_value.blank? ||
- (has_secondary_options_for_primary? && secondary_value.blank?)
- end
-
def search_terms
[primary_value, secondary_value]
end
@@ -84,14 +47,6 @@ class Champs::LinkedDropDownListChamp < Champ
options.include?(primary_value) || options.include?(secondary_value)
end
- def remove_option(options, touch = false)
- if touch
- update(value: nil)
- else
- update_column(:value, nil)
- end
- end
-
private
def pack_value(primary, secondary)
diff --git a/app/models/champs/mesri_champ.rb b/app/models/champs/mesri_champ.rb
index 145f251a3..2d8bc2114 100644
--- a/app/models/champs/mesri_champ.rb
+++ b/app/models/champs/mesri_champ.rb
@@ -1,11 +1,9 @@
+# frozen_string_literal: true
+
class Champs::MesriChamp < Champs::TextChamp
# see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/mesri-input-validation.middleware.ts
store_accessor :value_json, :ine
- def blank?
- external_id.nil?
- end
-
def fetch_external_data?
true
end
diff --git a/app/models/champs/multiple_drop_down_list_champ.rb b/app/models/champs/multiple_drop_down_list_champ.rb
index 60e8be104..46bbabfb8 100644
--- a/app/models/champs/multiple_drop_down_list_champ.rb
+++ b/app/models/champs/multiple_drop_down_list_champ.rb
@@ -1,14 +1,8 @@
+# frozen_string_literal: true
+
class Champs::MultipleDropDownListChamp < Champ
validate :values_are_in_options, if: -> { value.present? && validate_champ_value_or_prefill? }
- def options?
- drop_down_list_options?
- end
-
- def enabled_non_empty_options
- drop_down_list_enabled_non_empty_options
- end
-
THRESHOLD_NB_OPTIONS_AS_CHECKBOX = 5
def search_terms
@@ -19,20 +13,8 @@ class Champs::MultipleDropDownListChamp < Champ
value.blank? ? [] : JSON.parse(value)
end
- def to_s
- selected_options.join(', ')
- end
-
- def for_tag(path = :value)
- selected_options.join(', ')
- end
-
- def for_export(path = :value)
- value.present? ? selected_options.join(', ') : nil
- end
-
def render_as_checkboxes?
- enabled_non_empty_options.size <= THRESHOLD_NB_OPTIONS_AS_CHECKBOX
+ drop_down_options.size <= THRESHOLD_NB_OPTIONS_AS_CHECKBOX
end
def html_label?
@@ -47,25 +29,12 @@ class Champs::MultipleDropDownListChamp < Champ
render_as_checkboxes?
end
- def blank?
- selected_options.blank?
- end
-
def in?(options)
(selected_options - options).size != selected_options.size
end
- def remove_option(options, touch = false)
- value = (selected_options - options).to_json
- if touch
- update(value:)
- else
- update_columns(value:)
- end
- end
-
def focusable_input_id
- render_as_checkboxes? ? checkbox_id(enabled_non_empty_options.first) : input_id
+ render_as_checkboxes? ? checkbox_id(drop_down_options.first) : input_id
end
def checkbox_id(value)
@@ -81,11 +50,11 @@ class Champs::MultipleDropDownListChamp < Champ
end
def unselected_options
- enabled_non_empty_options - selected_options
+ drop_down_options - selected_options
end
def value=(value)
- return super(nil) if value.nil?
+ return super(nil) if value.blank?
values = if value.is_a?(Array)
value
@@ -107,7 +76,7 @@ class Champs::MultipleDropDownListChamp < Champ
def values_are_in_options
json = selected_options.compact_blank
return if json.empty?
- return if (json - enabled_non_empty_options).empty?
+ return if (json - drop_down_options).empty?
errors.add(:value, :not_in_options)
end
diff --git a/app/models/champs/number_champ.rb b/app/models/champs/number_champ.rb
index 8903284e2..9bd625a6d 100644
--- a/app/models/champs/number_champ.rb
+++ b/app/models/champs/number_champ.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class Champs::NumberChamp < Champ
end
diff --git a/app/models/champs/pays_champ.rb b/app/models/champs/pays_champ.rb
index 4dc5c0a6d..a1a6e3d37 100644
--- a/app/models/champs/pays_champ.rb
+++ b/app/models/champs/pays_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::PaysChamp < Champs::TextChamp
with_options if: :validate_champ_value? do
validates :external_id, inclusion: APIGeoService.countries.pluck(:code), allow_nil: true, allow_blank: false
@@ -11,28 +13,6 @@ class Champs::PaysChamp < Champs::TextChamp
validates :value, inclusion: APIGeoService.countries.pluck(:name), allow_nil: false, allow_blank: false
end
- def for_export(path = :value)
- case path
- when :code
- code
- when :value
- name
- end
- end
-
- def to_s
- name
- end
-
- def for_tag(path = :value)
- case path
- when :code
- code
- when :value
- name
- end
- end
-
def selected
code || value
end
diff --git a/app/models/champs/phone_champ.rb b/app/models/champs/phone_champ.rb
index e22e70682..a29dc7f19 100644
--- a/app/models/champs/phone_champ.rb
+++ b/app/models/champs/phone_champ.rb
@@ -1,41 +1,11 @@
+# frozen_string_literal: true
+
class Champs::PhoneChamp < Champs::TextChamp
- # We want to allow:
- # * international (e164) phone numbers
- # * “french format” (ten digits with a leading 0)
- # * DROM numbers
- #
- # However, we need to special-case some ten-digit numbers,
- # because the ARCEP assigns some blocks of "O6 XX XX XX XX" numbers to DROM operators.
- # Guadeloupe | GP | +590 | 0690XXXXXX, 0691XXXXXX
- # Guyane | GF | +594 | 0694XXXXXX
- # Martinique | MQ | +596 | 0696XXXXXX, 0697XXXXXX
- # Réunion | RE | +262 | 0692XXXXXX, 0693XXXXXX
- # Mayotte | YT | +262 | 0692XXXXXX, 0693XXXXXX
- # Nouvelle-Calédonie | NC | +687 |
- # Polynésie française | PF | +689 | 40XXXXXX, 45XXXXXX, 87XXXXXX, 88XXXXXX, 89XXXXXX
- #
- # Cf: Plan national de numérotation téléphonique,
- # https://www.arcep.fr/uploads/tx_gsavis/05-1085.pdf “Numéros mobiles à 10 chiffres”, page 6
- #
- # See issue #6996.
- DEFAULT_COUNTRY_CODES = [:FR, :GP, :GF, :MQ, :RE, :YT, :NC, :PF].freeze
validates :value,
phone: {
possible: true,
allow_blank: true,
message: I18n.t(:not_a_phone, scope: 'activerecord.errors.messages')
},
- if: -> { !Phonelib.valid_for_countries?(value, DEFAULT_COUNTRY_CODES) && validate_champ_value_or_prefill? }
-
- def to_s
- return '' if value.blank?
-
- if Phonelib.valid_for_countries?(value, DEFAULT_COUNTRY_CODES)
- Phonelib.parse_for_countries(value, DEFAULT_COUNTRY_CODES).full_national
- else
- # When he phone number is possible for the default countries, but not strictly valid,
- # `full_national` could mess up the formatting. In this case just return the original.
- value
- end
- end
+ if: -> { !Phonelib.valid_for_countries?(value, TypesDeChamp::PhoneTypeDeChamp::DEFAULT_COUNTRY_CODES) && validate_champ_value_or_prefill? }
end
diff --git a/app/models/champs/piece_justificative_champ.rb b/app/models/champs/piece_justificative_champ.rb
index 0708460e2..b282aee35 100644
--- a/app/models/champs/piece_justificative_champ.rb
+++ b/app/models/champs/piece_justificative_champ.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
class Champs::PieceJustificativeChamp < Champ
FILE_MAX_SIZE = 200.megabytes
+ has_many_attached :piece_justificative_file
+
# TODO: if: -> { validate_champ_value? || validation_context == :prefill }
validates :piece_justificative_file,
size: { less_than: FILE_MAX_SIZE },
@@ -17,28 +21,4 @@ class Champs::PieceJustificativeChamp < Champ
def search_terms
# We don’t know how to search inside documents yet
end
-
- def mandatory_blank?
- mandatory? && !piece_justificative_file.attached?
- end
-
- def blank?
- piece_justificative_file.blank?
- end
-
- def for_export(path = :value)
- piece_justificative_file.map { _1.filename.to_s }.join(', ')
- end
-
- def for_api
- return nil unless piece_justificative_file.attached?
-
- # API v1 don't support multiple PJ
- attachment = piece_justificative_file.first
- return nil if attachment.nil?
-
- if attachment.virus_scanner.safe? || attachment.virus_scanner.pending?
- attachment.url
- end
- end
end
diff --git a/app/models/champs/pole_emploi_champ.rb b/app/models/champs/pole_emploi_champ.rb
index eec697860..5a5d917f4 100644
--- a/app/models/champs/pole_emploi_champ.rb
+++ b/app/models/champs/pole_emploi_champ.rb
@@ -1,11 +1,9 @@
+# frozen_string_literal: true
+
class Champs::PoleEmploiChamp < Champs::TextChamp
# see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/pole-emploi-input-validation.middleware.ts
store_accessor :value_json, :identifiant
- def blank?
- external_id.nil?
- end
-
def fetch_external_data?
true
end
diff --git a/app/models/champs/region_champ.rb b/app/models/champs/region_champ.rb
index 04fab515c..9bbff77b6 100644
--- a/app/models/champs/region_champ.rb
+++ b/app/models/champs/region_champ.rb
@@ -1,25 +1,9 @@
+# frozen_string_literal: true
+
class Champs::RegionChamp < Champs::TextChamp
validate :value_in_region_names, if: -> { !value.nil? && validate_champ_value_or_prefill? }
validate :external_id_in_region_codes, if: -> { !external_id.nil? && validate_champ_value_or_prefill? }
- def for_export(path = :value)
- case path
- when :value
- name
- when :code
- code
- end
- end
-
- def for_tag(path = :value)
- case path
- when :value
- name
- when :code
- code
- end
- end
-
def selected
code
end
diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb
index 6bb189c49..0e9e8ef64 100644
--- a/app/models/champs/repetition_champ.rb
+++ b/app/models/champs/repetition_champ.rb
@@ -1,52 +1,32 @@
+# frozen_string_literal: true
+
class Champs::RepetitionChamp < Champ
- accepts_nested_attributes_for :champs
+ delegate :libelle_for_export, to: :type_de_champ
def rows
- dossier
- .champs_for_revision(scope: type_de_champ)
- .group_by(&:row_id)
- .sort
- .map(&:second)
+ dossier.project_rows_for(type_de_champ)
end
def row_ids
- rows.map { _1.first.row_id }
+ dossier.repetition_row_ids(type_de_champ)
end
- def add_row(revision)
- added_champs = []
- transaction do
- row_id = ULID.generate
- revision.children_of(type_de_champ).each do |type_de_champ|
- added_champs << type_de_champ.build_champ(row_id:)
- end
- self.champs << added_champs
- end
- added_champs
+ def add_row(updated_by:)
+ dossier.repetition_add_row(type_de_champ, updated_by:)
end
- def blank?
- champs.empty?
+ def remove_row(row_id, updated_by:)
+ dossier.repetition_remove_row(type_de_champ, row_id, updated_by:)
+ end
+
+ def focusable_input_id
+ rows.last&.first&.focusable_input_id
end
def search_terms
# The user cannot enter any information here so it doesn’t make much sense to search
end
- def for_tag(path = :value)
- ([libelle] + rows.map do |champs|
- champs.map do |champ|
- "#{champ.libelle} : #{champ}"
- end.join("\n")
- end).join("\n\n")
- end
-
- def rows_for_export
- row_ids.map.with_index(1) do |row_id, index|
- Champs::RepetitionChamp::Row.new(index:, row_id:, dossier:)
- end
- end
-
class Row < Hashie::Dash
property :index
property :row_id
@@ -60,11 +40,11 @@ class Champs::RepetitionChamp < Champ
self[attribute]
end
- def spreadsheet_columns(types_de_champ)
+ def spreadsheet_columns(types_de_champ, export_template: nil, format:)
[
['Dossier ID', :dossier_id],
['Ligne', :index]
- ] + dossier.champs_for_export(types_de_champ, row_id)
+ ] + dossier.champ_values_for_export(types_de_champ, row_id:, export_template:, format:)
end
end
end
diff --git a/app/models/champs/rna_champ.rb b/app/models/champs/rna_champ.rb
index 5905d35fa..b738922d8 100644
--- a/app/models/champs/rna_champ.rb
+++ b/app/models/champs/rna_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::RNAChamp < Champ
include RNAChampAssociationFetchableConcern
@@ -11,12 +13,12 @@ class Champs::RNAChamp < Champ
data&.dig("association_titre")
end
- def identifier
- title.present? ? "#{value} (#{title})" : value
+ def update_with_external_data!(data:)
+ update!(data:, value_json: extract_value_json(data:))
end
- def for_export(path = :value)
- identifier
+ def identifier
+ title.present? ? "#{value} (#{title})" : value
end
def search_terms
@@ -43,4 +45,11 @@ class Champs::RNAChamp < Champ
city_code: address["code_insee"]
}.with_indifferent_access
end
+
+ private
+
+ def extract_value_json(data:)
+ h = APIGeoService.parse_rna_address(data['adresse'])
+ h.merge(title: data['association_titre'])
+ end
end
diff --git a/app/models/champs/rnf_champ.rb b/app/models/champs/rnf_champ.rb
index cb0de4e8d..0dab98707 100644
--- a/app/models/champs/rnf_champ.rb
+++ b/app/models/champs/rnf_champ.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
class Champs::RNFChamp < Champ
- store_accessor :data, :title, :email, :phone, :createdAt, :updatedAt, :dissolvedAt, :address, :status
+ store_accessor :data, :title, :email, :phone, :createdAt, :updatedAt, :dissolvedAt, :address
def rnf_id
- external_id
+ external_id&.gsub(/[[:space:]]/, '')
end
def value
@@ -13,6 +15,10 @@ class Champs::RNFChamp < Champ
RNFService.new.(rnf_id:)
end
+ def update_with_external_data!(data:)
+ update!(data:, value_json: extract_value_json(data:))
+ end
+
def fetch_external_data?
true
end
@@ -21,40 +27,6 @@ class Champs::RNFChamp < Champ
true
end
- def blank?
- rnf_id.blank?
- end
-
- def for_export(path = :value)
- case path
- when :value
- rnf_id
- when :departement
- departement_code_and_name
- when :code_insee
- commune&.fetch(:code)
- when :address
- full_address
- when :nom
- title
- end
- end
-
- def for_tag(path = :value)
- case path
- when :value
- rnf_id
- when :departement
- departement_code_and_name || ''
- when :code_insee
- commune&.fetch(:code) || ''
- when :address
- full_address || ''
- when :nom
- title || ''
- end
- end
-
def code_departement
address.present? && address['departmentCode']
end
@@ -137,4 +109,11 @@ class Champs::RNFChamp < Champ
address['label']
end
end
+
+ private
+
+ def extract_value_json(data:)
+ h = APIGeoService.parse_rnf_address(data[:address])
+ h.merge(title: data[:title])
+ end
end
diff --git a/app/models/champs/siret_champ.rb b/app/models/champs/siret_champ.rb
index 54c350544..f124d218d 100644
--- a/app/models/champs/siret_champ.rb
+++ b/app/models/champs/siret_champ.rb
@@ -1,11 +1,9 @@
+# frozen_string_literal: true
+
class Champs::SiretChamp < Champ
include SiretChampEtablissementFetchableConcern
def search_terms
etablissement.present? ? etablissement.search_terms : [value]
end
-
- def mandatory_blank?
- mandatory? && Siret.new(siret: value).invalid?
- end
end
diff --git a/app/models/champs/text_champ.rb b/app/models/champs/text_champ.rb
index 8e24252ce..ee99e1819 100644
--- a/app/models/champs/text_champ.rb
+++ b/app/models/champs/text_champ.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class Champs::TextChamp < Champ
end
diff --git a/app/models/champs/textarea_champ.rb b/app/models/champs/textarea_champ.rb
index abcb2644e..9b62f415c 100644
--- a/app/models/champs/textarea_champ.rb
+++ b/app/models/champs/textarea_champ.rb
@@ -1,8 +1,6 @@
-class Champs::TextareaChamp < Champs::TextChamp
- def for_export(path = :value)
- value.present? ? ActionView::Base.full_sanitizer.sanitize(value) : nil
- end
+# frozen_string_literal: true
+class Champs::TextareaChamp < Champs::TextChamp
def remaining_characters
character_limit_base - character_count if character_count >= character_limit_threshold_75
end
diff --git a/app/models/champs/titre_identite_champ.rb b/app/models/champs/titre_identite_champ.rb
index ae57491bd..790259f7c 100644
--- a/app/models/champs/titre_identite_champ.rb
+++ b/app/models/champs/titre_identite_champ.rb
@@ -1,6 +1,11 @@
+# frozen_string_literal: true
+
class Champs::TitreIdentiteChamp < Champ
FILE_MAX_SIZE = 20.megabytes
ACCEPTED_FORMATS = ['image/png', 'image/jpeg']
+
+ has_many_attached :piece_justificative_file
+
# TODO: if: -> { validate_champ_value? || validation_context == :prefill }
validates :piece_justificative_file, content_type: ACCEPTED_FORMATS, size: { less_than: FILE_MAX_SIZE }
@@ -11,20 +16,4 @@ class Champs::TitreIdentiteChamp < Champ
def search_terms
# We don’t know how to search inside documents yet
end
-
- def mandatory_blank?
- mandatory? && !piece_justificative_file.attached?
- end
-
- def blank?
- piece_justificative_file.blank?
- end
-
- def for_export(path = :value)
- piece_justificative_file.attached? ? "présent" : "absent"
- end
-
- def for_api
- nil
- end
end
diff --git a/app/models/champs/yes_no_champ.rb b/app/models/champs/yes_no_champ.rb
index 8f9763811..80b5a499e 100644
--- a/app/models/champs/yes_no_champ.rb
+++ b/app/models/champs/yes_no_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Champs::YesNoChamp < Champs::BooleanChamp
def legend_label?
true
diff --git a/app/models/chorus_configuration.rb b/app/models/chorus_configuration.rb
index e4042eccd..f2f4a4db4 100644
--- a/app/models/chorus_configuration.rb
+++ b/app/models/chorus_configuration.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ChorusConfiguration
include ActiveModel::Model
include ActiveModel::Attributes
diff --git a/app/models/column.rb b/app/models/column.rb
new file mode 100644
index 000000000..b02feda10
--- /dev/null
+++ b/app/models/column.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+class Column
+ # include validations to enable procedure_presentation.validate_associate,
+ # which enforces the deserialization of columns in the displayed_columns attribute
+ # and raises an error if a column is not found
+ include ActiveModel::Validations
+
+ TYPE_DE_CHAMP_TABLE = 'type_de_champ'
+
+ attr_reader :table, :column, :label, :type, :filterable, :displayable
+ attr_accessor :options_for_select
+
+ def initialize(procedure_id:, table:, column:, label: nil, type: :text, filterable: true, displayable: true, options_for_select: [])
+ @procedure_id = procedure_id
+ @table = table
+ @column = column
+ @label = label || I18n.t(column, scope: [:activerecord, :attributes, :procedure_presentation, :fields, table])
+ @type = type
+ @filterable = filterable
+ @displayable = displayable
+ @options_for_select = options_for_select
+ end
+
+ # the id is a String to be used in forms
+ def id = h_id.to_json
+
+ # the h_id is a Hash and hold enough information to find the column
+ # in the ColumnType class, aka be able to do the h_id -> column conversion
+ def h_id = { procedure_id: @procedure_id, column_id: }
+
+ def ==(other) = h_id == other.h_id # using h_id instead of id to avoid inversion of keys
+
+ def notifications? = [table, column] == ['notifications', 'notifications']
+ def dossier_id? = [table, column] == ['self', 'id']
+ def dossier_state? = [table, column] == ['self', 'state']
+ def groupe_instructeur? = [table, column] == ['groupe_instructeur', 'id']
+ def dossier_labels? = [table, column] == ['dossier_labels', 'label_id']
+ def type_de_champ? = table == TYPE_DE_CHAMP_TABLE
+
+ def self.find(h_id)
+ begin
+ procedure = Procedure.with_discarded.find(h_id[:procedure_id])
+ rescue ActiveRecord::RecordNotFound
+ raise ActiveRecord::RecordNotFound.new("Column: unable to find procedure #{h_id[:procedure_id]} from h_id #{h_id}")
+ end
+
+ procedure.find_column(h_id: h_id)
+ end
+
+ def dossier_column? = false
+ def champ_column? = false
+
+ private
+
+ def column_id = "#{table}/#{column}"
+end
diff --git a/app/models/columns/attached_many_column.rb b/app/models/columns/attached_many_column.rb
new file mode 100644
index 000000000..439f7adc5
--- /dev/null
+++ b/app/models/columns/attached_many_column.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Columns::AttachedManyColumn < Columns::ChampColumn
+ private
+
+ def typed_value(champ)
+ champ.piece_justificative_file.to_a
+ end
+end
diff --git a/app/models/columns/champ_column.rb b/app/models/columns/champ_column.rb
new file mode 100644
index 000000000..2d3859774
--- /dev/null
+++ b/app/models/columns/champ_column.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+class Columns::ChampColumn < Column
+ attr_reader :stable_id, :tdc_type
+
+ def initialize(procedure_id:, label:, stable_id:, tdc_type:, displayable: true, filterable: true, type: :text, options_for_select: [])
+ @stable_id = stable_id
+ @tdc_type = tdc_type
+ column = tdc_type.in?(['departements', 'regions']) ? :external_id : :value
+
+ super(
+ procedure_id:,
+ table: 'type_de_champ',
+ column:,
+ label:,
+ type:,
+ displayable:,
+ filterable:,
+ options_for_select:
+ )
+ end
+
+ def value(champ)
+ return if champ.nil?
+
+ # nominal case
+ if champ.is_type?(@tdc_type)
+ typed_value(champ)
+ else
+ cast_value(champ)
+ end
+ end
+
+ def filtered_ids(dossiers, search_terms)
+ relation = dossiers.with_type_de_champ(stable_id)
+
+ if type == :enum
+ relation.where(champs: { column => search_terms }).ids
+ elsif type == :enums
+ # in a multiple drop down list, the value are stored as '["v1", "v2"]'
+ quoted_search_terms = search_terms.map { %{"#{_1}"} }
+ relation.filter_ilike(:champs, column, quoted_search_terms).ids
+ else
+ relation.filter_ilike(:champs, column, search_terms).ids
+ end
+ end
+
+ def champ_column? = true
+
+ private
+
+ def column_id = "type_de_champ/#{stable_id}"
+
+ def string_value(champ) = champ.public_send(column)
+
+ def typed_value(champ)
+ value = string_value(champ)
+
+ return if value.blank?
+
+ case type
+ when :boolean
+ parse_boolean(value)
+ when :integer
+ value.to_i
+ when :decimal
+ value.to_f
+ when :datetime
+ parse_datetime(value)
+ when :date
+ parse_datetime(value)&.to_date
+ when :enums
+ parse_enums(value)
+ else
+ value
+ end
+ end
+
+ def cast_value(champ)
+ value = string_value(champ)
+
+ return if value.blank?
+
+ case [champ.last_write_type_champ, @tdc_type]
+ when ['integer_number', 'decimal_number'] # recast numbers automatically
+ value.to_f
+ when ['decimal_number', 'integer_number'] # may lose some data, but who cares ?
+ value.to_i
+ when ['integer_number', 'text'], ['decimal_number', 'text'] # number to text
+ value
+ when ['drop_down_list', 'multiple_drop_down_list'] # single list can become multi
+ [value]
+ when ['drop_down_list', 'text'] # single list can become text
+ value
+ when ['multiple_drop_down_list', 'drop_down_list'] # multi list can become single
+ parse_enums(value).first
+ when ['multiple_drop_down_list', 'text'] # single list can become text
+ parse_enums(value).join(', ')
+ when ['date', 'datetime'] # date <=> datetime
+ parse_datetime(value)&.to_datetime
+ when ['datetime', 'date'] # may lose some data, but who cares ?
+ parse_datetime(value)&.to_date
+ else
+ nil
+ end
+ end
+
+ def parse_boolean(value)
+ case value
+ when 'true', 'on', '1'
+ true
+ when 'false'
+ false
+ end
+ end
+
+ def parse_enums(value) = JSON.parse(value) rescue nil
+
+ def parse_datetime(value) = Time.zone.parse(value) rescue nil
+end
diff --git a/app/models/columns/dossier_column.rb b/app/models/columns/dossier_column.rb
new file mode 100644
index 000000000..82730b2a9
--- /dev/null
+++ b/app/models/columns/dossier_column.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class Columns::DossierColumn < Column
+ def value(dossier)
+ case table
+ when 'self'
+ dossier.public_send(column)
+ when 'etablissement'
+ dossier.etablissement.public_send(column)
+ when 'individual'
+ dossier.individual.public_send(column)
+ when 'groupe_instructeur'
+ dossier.groupe_instructeur.label
+ when 'followers_instructeurs'
+ dossier.followers_instructeurs.map(&:email).join(' ')
+ end
+ end
+
+ def dossier_column? = true
+end
diff --git a/app/models/columns/json_path_column.rb b/app/models/columns/json_path_column.rb
new file mode 100644
index 000000000..a60d5d872
--- /dev/null
+++ b/app/models/columns/json_path_column.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class Columns::JSONPathColumn < Columns::ChampColumn
+ attr_reader :jsonpath
+
+ def initialize(procedure_id:, label:, stable_id:, tdc_type:, jsonpath:, options_for_select: [], displayable:, type: :text)
+ @jsonpath = quote_string(jsonpath)
+
+ super(
+ procedure_id:,
+ label:,
+ stable_id:,
+ tdc_type:,
+ displayable:,
+ type:,
+ options_for_select:
+ )
+ end
+
+ def filtered_ids(dossiers, search_terms)
+ value = quote_string(search_terms.join('|'))
+
+ condition = %{champs.value_json @? '#{jsonpath} ? (@ like_regex "#{value}" flag "i")'}
+
+ dossiers.with_type_de_champ(stable_id)
+ .where(condition)
+ .ids
+ end
+
+ private
+
+ def column_id = "type_de_champ/#{stable_id}-#{jsonpath}"
+
+ def typed_value(champ)
+ champ.value_json&.dig(*jsonpath.split('.')[1..])
+ end
+
+ def quote_string(string) = ActiveRecord::Base.connection.quote_string(string)
+end
diff --git a/app/models/columns/linked_drop_down_column.rb b/app/models/columns/linked_drop_down_column.rb
new file mode 100644
index 000000000..c7611168e
--- /dev/null
+++ b/app/models/columns/linked_drop_down_column.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+class Columns::LinkedDropDownColumn < Columns::ChampColumn
+ attr_reader :path
+
+ def initialize(procedure_id:, label:, stable_id:, tdc_type:, path:, options_for_select: [], displayable:, type: :text)
+ @path = path
+
+ super(
+ procedure_id:,
+ label:,
+ stable_id:,
+ tdc_type:,
+ displayable:,
+ type:,
+ options_for_select:
+ )
+ end
+
+ def filtered_ids(dossiers, search_terms)
+ relation = dossiers.with_type_de_champ(@stable_id)
+
+ case path
+ when :value
+ search_terms.flat_map do |search_term|
+ # when looking for "section 1 / option A",
+ # the value must contain both "section 1" and "option A"
+ primary, *secondary = search_term.split(%r{[[:space:]]*/[[:space:]]*})
+ safe_terms = [primary, *secondary].map { "%#{safe_like(_1)}%" }
+
+ relation.where("champs.value ILIKE ALL (ARRAY[?])", safe_terms).ids
+ end.uniq
+ when :primary
+ primary_terms = search_terms.map { |term| %{["#{safe_like(term)}","%"]} }
+
+ relation.where("champs.value ILIKE ANY (array[?])", primary_terms).ids
+ when :secondary
+ secondary_terms = search_terms.map { |term| %{["%","#{safe_like(term)}"]} }
+
+ relation.where("champs.value ILIKE ANY (array[?])", secondary_terms).ids
+ end
+ end
+
+ private
+
+ def column_id = "type_de_champ/#{stable_id}.#{path}"
+
+ def typed_value(champ)
+ primary_value, secondary_value = unpack_values(champ.value)
+ case path
+ when :value
+ "#{primary_value} / #{secondary_value}"
+ when :primary
+ primary_value
+ when :secondary
+ secondary_value
+ end
+ end
+
+ def unpack_values(value)
+ JSON.parse(value)
+ rescue JSON::ParserError
+ []
+ end
+
+ def safe_like(q) = ActiveRecord::Base.sanitize_sql_like(q)
+end
diff --git a/app/models/columns/piece_justificative_column.rb b/app/models/columns/piece_justificative_column.rb
new file mode 100644
index 000000000..6b29ae35b
--- /dev/null
+++ b/app/models/columns/piece_justificative_column.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Columns::PieceJustificativeColumn < Column
+ private
+
+ def typed_value(champ)
+ champ.piece_justificative_file.map { _1.blob.filename.to_s }.join(', ')
+ end
+end
diff --git a/app/models/commentaire.rb b/app/models/commentaire.rb
index cab033348..6570c9094 100644
--- a/app/models/commentaire.rb
+++ b/app/models/commentaire.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Commentaire < ApplicationRecord
include Discard::Model
belongs_to :dossier, inverse_of: :commentaires, touch: true, optional: false
@@ -7,7 +9,7 @@ class Commentaire < ApplicationRecord
validate :messagerie_available?, on: :create, unless: -> { dossier.brouillon? }
- has_one_attached :piece_jointe
+ has_many_attached :piece_jointe
validates :body, presence: { message: "ne peut être vide" }, unless: :discarded?
@@ -37,7 +39,7 @@ class Commentaire < ApplicationRecord
def redacted_email
if sent_by_instructeur?
- if dossier.procedure.feature_enabled?(:hide_instructeur_email)
+ if dossier.procedure.hide_instructeurs_email?
"Instructeur n° #{instructeur.id}"
else
instructeur.email.split('@').first
@@ -67,12 +69,6 @@ class Commentaire < ApplicationRecord
sent_by?(connected_user) && (sent_by_instructeur? || sent_by_expert?) && !discarded?
end
- def file_url
- if piece_jointe.attached? && piece_jointe.virus_scanner.safe?
- Rails.application.routes.url_helpers.url_for(piece_jointe)
- end
- end
-
def soft_delete!
transaction do
discard!
@@ -80,7 +76,7 @@ class Commentaire < ApplicationRecord
update! body: ''
end
- piece_jointe.purge_later if piece_jointe.attached?
+ piece_jointe.each(&:purge_later) if piece_jointe.attached?
end
def flagged_pending_correction?
diff --git a/app/models/commentaire_groupe_gestionnaire.rb b/app/models/commentaire_groupe_gestionnaire.rb
index f6602bef9..2a944d2e0 100644
--- a/app/models/commentaire_groupe_gestionnaire.rb
+++ b/app/models/commentaire_groupe_gestionnaire.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CommentaireGroupeGestionnaire < ApplicationRecord
include Discard::Model
belongs_to :groupe_gestionnaire
diff --git a/app/models/concerns/addressable_column_concern.rb b/app/models/concerns/addressable_column_concern.rb
new file mode 100644
index 000000000..b3a6edf73
--- /dev/null
+++ b/app/models/concerns/addressable_column_concern.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module AddressableColumnConcern
+ extend ActiveSupport::Concern
+
+ included do
+ def addressable_columns(procedure:, displayable: true, prefix: nil)
+ [
+ ["code postal (5 chiffres)", '$.postal_code', :text, []],
+ ["commune", '$.city_name', :text, []],
+ ["département", '$.departement_code', :enum, APIGeoService.departement_options],
+ ["region", '$.region_name', :enum, APIGeoService.region_options]
+ ].map do |(label, jsonpath, type, options_for_select)|
+ Columns::JSONPathColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: "#{libelle_with_prefix(prefix)} – #{label}",
+ jsonpath:,
+ displayable:,
+ options_for_select:,
+ type:
+ )
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/api_entreprise_token_concern.rb b/app/models/concerns/api_entreprise_token_concern.rb
new file mode 100644
index 000000000..3750d35ef
--- /dev/null
+++ b/app/models/concerns/api_entreprise_token_concern.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module APIEntrepriseTokenConcern
+ extend ActiveSupport::Concern
+
+ SOON_TO_EXPIRE_DELAY = 1.month
+
+ included do
+ validates :api_entreprise_token, jwt_token: true, allow_blank: true
+
+ before_save :set_api_entreprise_token_expires_at, if: :will_save_change_to_api_entreprise_token?
+
+ def api_entreprise_role?(role)
+ APIEntrepriseToken.new(api_entreprise_token).role?(role)
+ end
+
+ def api_entreprise_token
+ self[:api_entreprise_token].presence || Rails.application.secrets.api_entreprise[:key]
+ end
+
+ def api_entreprise_token_expired_or_expires_soon?
+ api_entreprise_token_expires_at && api_entreprise_token_expires_at <= SOON_TO_EXPIRE_DELAY.from_now
+ end
+
+ def has_api_entreprise_token?
+ self[:api_entreprise_token].present?
+ end
+
+ def set_api_entreprise_token_expires_at
+ self.api_entreprise_token_expires_at = has_api_entreprise_token? ? APIEntrepriseToken.new(api_entreprise_token).expiration : nil
+ end
+ end
+end
diff --git a/app/models/concerns/attachment_image_processor_concern.rb b/app/models/concerns/attachment_image_processor_concern.rb
new file mode 100644
index 000000000..1457122a5
--- /dev/null
+++ b/app/models/concerns/attachment_image_processor_concern.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# Run a virus scan on all attachments after they are analyzed.
+#
+# We're using a class extension to ensure that all attachments get scanned,
+# regardless on how they were created. This could be an ActiveStorage::Analyzer,
+# but as of Rails 6.1 only the first matching analyzer is ever run on
+# a blob (and we may want to analyze the dimension of a picture as well
+# as scanning it).
+module AttachmentImageProcessorConcern
+ extend ActiveSupport::Concern
+
+ included do
+ after_create_commit :process_image
+ end
+
+ private
+
+ def process_image
+ return if blob.nil?
+ return if blob.attachments.size != 1
+ return if blob.attachments.any? { _1.record_type == "Export" }
+ return if !blob.content_type.in?(PROCESSABLE_TYPES)
+ return if blob.byte_size.zero? # some empty files may be considered as image depending on filename
+
+ ImageProcessorJob.perform_later(blob)
+ end
+end
diff --git a/app/models/concerns/attachment_titre_identite_watermark_concern.rb b/app/models/concerns/attachment_titre_identite_watermark_concern.rb
index 2091a850d..05aadb7c8 100644
--- a/app/models/concerns/attachment_titre_identite_watermark_concern.rb
+++ b/app/models/concerns/attachment_titre_identite_watermark_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Request a watermark on files attached to a `Champs::TitreIdentiteChamp`.
#
# We're using a class extension here, but we could as well have a periodic
diff --git a/app/models/concerns/attachment_virus_scanner_concern.rb b/app/models/concerns/attachment_virus_scanner_concern.rb
index 832d15a98..43e9e4f26 100644
--- a/app/models/concerns/attachment_virus_scanner_concern.rb
+++ b/app/models/concerns/attachment_virus_scanner_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Run a virus scan on all attachments after they are analyzed.
#
# We're using a class extension to ensure that all attachments get scanned,
diff --git a/app/models/concerns/blob_image_processor_concern.rb b/app/models/concerns/blob_image_processor_concern.rb
new file mode 100644
index 000000000..7e07b17dc
--- /dev/null
+++ b/app/models/concerns/blob_image_processor_concern.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module BlobImageProcessorConcern
+ def watermark_pending?
+ watermark_required? && !watermark_done?
+ end
+
+ def watermark_done?
+ watermarked_at.present?
+ end
+
+ def representation_required?
+ from_champ? || from_messagerie? || logo? || from_action_text? || from_avis? || from_justificatif_motivation?
+ end
+
+ private
+
+ def from_champ?
+ attachments.any? { _1.record.class == Champs::TitreIdentiteChamp || _1.record.class == Champs::PieceJustificativeChamp }
+ end
+
+ def from_messagerie?
+ attachments.any? { _1.record.class == Commentaire }
+ end
+
+ def logo?
+ attachments.any? { _1.name == 'logo' }
+ end
+
+ def from_action_text?
+ attachments.any? { _1.record.class == ActionText::RichText }
+ end
+
+ def from_avis?
+ attachments.any? { _1.record.class == Avis }
+ end
+
+ def watermark_required?
+ attachments.any? { _1.record.class == Champs::TitreIdentiteChamp }
+ end
+
+ def from_justificatif_motivation?
+ attachments.any? { _1.name == 'justificatif_motivation' }
+ end
+end
diff --git a/app/models/concerns/blob_signed_id_concern.rb b/app/models/concerns/blob_signed_id_concern.rb
index f12b2616d..e05e71697 100644
--- a/app/models/concerns/blob_signed_id_concern.rb
+++ b/app/models/concerns/blob_signed_id_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BlobSignedIdConcern
extend ActiveSupport::Concern
diff --git a/app/models/concerns/blob_titre_identite_watermark_concern.rb b/app/models/concerns/blob_titre_identite_watermark_concern.rb
deleted file mode 100644
index 152c512f9..000000000
--- a/app/models/concerns/blob_titre_identite_watermark_concern.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module BlobTitreIdentiteWatermarkConcern
- def watermark_pending?
- watermark_required? && !watermark_done?
- end
-
- def watermark_done?
- watermarked_at.present?
- end
-
- def watermark_later
- if watermark_pending?
- TitreIdentiteWatermarkJob.perform_later(self)
- end
- end
-
- private
-
- def watermark_required?
- attachments.any? { _1.record.class == Champs::TitreIdentiteChamp }
- end
-end
diff --git a/app/models/concerns/blob_virus_scanner_concern.rb b/app/models/concerns/blob_virus_scanner_concern.rb
index dd5fbdb51..f4d45309a 100644
--- a/app/models/concerns/blob_virus_scanner_concern.rb
+++ b/app/models/concerns/blob_virus_scanner_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module BlobVirusScannerConcern
extend ActiveSupport::Concern
diff --git a/app/models/concerns/champ_conditional_concern.rb b/app/models/concerns/champ_conditional_concern.rb
index 9e6559be9..91fbd97be 100644
--- a/app/models/concerns/champ_conditional_concern.rb
+++ b/app/models/concerns/champ_conditional_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ChampConditionalConcern
extend ActiveSupport::Concern
@@ -21,10 +23,14 @@ module ChampConditionalConcern
end
end
+ def reset_visible # recompute after a dossier update
+ remove_instance_variable :@visible if instance_variable_defined? :@visible
+ end
+
private
def champs_for_condition
- dossier.champs.filter { _1.row_id.nil? || _1.row_id == row_id }
+ dossier.filled_champs.filter { _1.row_id.nil? || _1.row_id == row_id }
end
end
end
diff --git a/app/models/concerns/champs_validate_concern.rb b/app/models/concerns/champs_validate_concern.rb
index e9f040908..ac8d0e1aa 100644
--- a/app/models/concerns/champs_validate_concern.rb
+++ b/app/models/concerns/champs_validate_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ChampsValidateConcern
extend ActiveSupport::Concern
@@ -12,13 +14,11 @@ module ChampsValidateConcern
private
def validate_champ_value?
- return false unless visible?
-
case validation_context
when :champs_public_value
- public?
+ public? && in_dossier_revision? && visible?
when :champs_private_value
- private?
+ private? && in_dossier_revision? && visible?
else
false
end
@@ -27,5 +27,9 @@ module ChampsValidateConcern
def validate_champ_value_or_prefill?
validate_champ_value? || validation_context == :prefill
end
+
+ def in_dossier_revision?
+ dossier.revision.types_de_champ.any? { _1.stable_id == stable_id } && is_type?(type_de_champ.type_champ)
+ end
end
end
diff --git a/app/models/concerns/columns_concern.rb b/app/models/concerns/columns_concern.rb
new file mode 100644
index 000000000..d845bd7d7
--- /dev/null
+++ b/app/models/concerns/columns_concern.rb
@@ -0,0 +1,176 @@
+# frozen_string_literal: true
+
+module ColumnsConcern
+ extend ActiveSupport::Concern
+
+ included do
+ # we cannot use column.id ( == { procedure_id, column_id }.to_json)
+ # as the order of the keys is not guaranteed
+ # instead, we are using h_id == { procedure_id:, column_id: }
+ # another way to find a column is to look for its label
+ def find_column(h_id: nil, label: nil)
+ column = columns.find { _1.h_id == h_id } if h_id.present?
+ column = columns.find { _1.label == label } if label.present?
+
+ # TODO: to remove after linked_drop_down column column_id migration
+ if column.nil? && h_id.is_a?(Hash) && h_id[:column_id].present?
+ h_id[:column_id].gsub!('->', '.')
+
+ column = columns.find { _1.h_id == h_id }
+ end
+
+ raise ActiveRecord::RecordNotFound.new("Column: unable to find h_id: #{h_id} or label: #{label} for procedure_id #{id}") if column.nil?
+
+ column
+ end
+
+ def columns
+ Current.procedure_columns ||= {}
+
+ Current.procedure_columns[id] ||= begin
+ columns = dossier_columns
+ columns.concat(standard_columns)
+ columns.concat(individual_columns) if for_individual
+ columns.concat(moral_columns) if !for_individual
+ columns.concat(procedure_chorus_columns) if chorusable? && chorus_configuration.complete?
+ columns.concat(types_de_champ_columns)
+ end
+ end
+
+ def usager_columns_for_export
+ columns = [dossier_id_column, user_email_for_display_column, user_france_connected_column]
+ columns.concat(individual_columns) if for_individual
+ columns.concat(moral_columns) if !for_individual
+ columns.concat(procedure_chorus_columns) if chorusable? && chorus_configuration.complete?
+
+ # ensure the columns exist in main list
+ # otherwise, they will be found by the find_column method
+ columns.filter { _1.id.in?(self.columns.map(&:id)) }
+ end
+
+ def dossier_columns_for_export
+ columns = [dossier_state_column, dossier_archived_column]
+ columns.concat(dossier_dates_columns)
+ columns.concat([dossier_motivation_column])
+ columns.concat(sva_svr_columns(for_export: true)) if sva_svr_enabled?
+ columns.concat([groupe_instructeurs_id_column, followers_instructeurs_email_column])
+
+ # ensure the columns exist in main list
+ # otherwise, they will be found by the find_column method
+ columns.filter { _1.id.in?(self.columns.map(&:id)) }
+ end
+
+ def dossier_id_column = dossier_col(table: 'self', column: 'id', type: :integer)
+
+ def dossier_state_column
+ options_for_select = I18n.t('instructeurs.dossiers.filterable_state').map(&:to_a).map(&:reverse)
+
+ dossier_col(table: 'self', column: 'state', type: :enum, options_for_select:, displayable: false)
+ end
+
+ def notifications_column = dossier_col(table: 'notifications', column: 'notifications', label: "notifications", filterable: false)
+
+ def sva_svr_columns(for_export: false)
+ scope = [:activerecord, :attributes, :procedure_presentation, :fields, :self]
+
+ columns = [
+ dossier_col(table: 'self', column: 'sva_svr_decision_on', type: :date,
+ label: I18n.t("#{sva_svr_decision}_decision_on", scope:, type: sva_svr_configuration.human_decision))
+ ]
+ if !for_export
+ columns << dossier_col(table: 'self', column: 'sva_svr_decision_before', type: :date, displayable: false,
+ label: I18n.t("#{sva_svr_decision}_decision_before", scope:))
+ end
+ columns
+ end
+
+ def default_sorted_column
+ SortedColumn.new(column: notifications_column, order: 'desc')
+ end
+
+ def default_displayed_columns = [email_column]
+
+ private
+
+ def groupe_instructeurs_id_column = dossier_col(table: 'groupe_instructeur', column: 'id', type: :enum)
+
+ def followers_instructeurs_email_column = dossier_col(table: 'followers_instructeurs', column: 'email')
+
+ def dossier_archived_column = dossier_col(table: 'self', column: 'archived', type: :boolean, displayable: false, filterable: false);
+
+ def dossier_motivation_column = dossier_col(table: 'self', column: 'motivation', type: :text, displayable: false, filterable: false);
+
+ def user_email_for_display_column = dossier_col(table: 'self', column: 'user_email_for_display', filterable: false, displayable: false)
+
+ def user_france_connected_column = dossier_col(table: 'self', column: 'user_from_france_connect?', type: :boolean, filterable: false, displayable: false)
+
+ def dossier_labels_column = dossier_col(table: 'dossier_labels', column: 'label_id', type: :enum, options_for_select: labels.map { [_1.name, _1.id] })
+
+ def procedure_chorus_columns
+ ['domaine_fonctionnel', 'referentiel_prog', 'centre_de_cout']
+ .map { |column| dossier_col(table: 'procedure', column:, displayable: false, filterable: false) }
+ end
+
+ def dossier_non_displayable_dates_columns
+ ['updated_since', 'depose_since', 'en_construction_since', 'en_instruction_since', 'processed_since']
+ .map { |column| dossier_col(table: 'self', column:, type: :date, displayable: false) }
+ end
+
+ def dossier_dates_columns
+ ['created_at', 'updated_at', 'last_champ_updated_at', 'depose_at', 'en_construction_at', 'en_instruction_at', 'processed_at']
+ .map { |column| dossier_col(table: 'self', column:, type: :datetime) }
+ end
+
+ def email_column
+ dossier_col(table: 'user', column: 'email')
+ end
+
+ def dossier_columns
+ columns = [dossier_id_column, notifications_column]
+ columns.concat([dossier_state_column])
+ columns.concat([dossier_archived_column])
+ columns.concat(dossier_dates_columns)
+ columns.concat([dossier_motivation_column])
+ columns.concat(sva_svr_columns(for_export: false)) if sva_svr_enabled?
+ columns.concat(dossier_non_displayable_dates_columns)
+ end
+
+ def standard_columns
+ [
+ email_column,
+ user_email_for_display_column,
+ followers_instructeurs_email_column,
+ groupe_instructeurs_id_column,
+ dossier_col(table: 'avis', column: 'question_answer', filterable: false),
+ user_france_connected_column,
+ dossier_labels_column
+ ]
+ end
+
+ def individual_columns
+ ['gender', 'nom', 'prenom'].map { |column| dossier_col(table: 'individual', column:) }
+ .concat ['mandataire_last_name', 'mandataire_first_name'].map { |column| dossier_col(table: 'self', column:) }
+ .concat ['for_tiers'].map { |column| dossier_col(table: 'self', column:, type: :boolean) }
+ end
+
+ def moral_columns
+ etablissements = ['entreprise_forme_juridique', 'entreprise_siren', 'entreprise_nom_commercial', 'entreprise_raison_sociale', 'entreprise_siret_siege_social']
+ .map { |column| dossier_col(table: 'etablissement', column:) }
+
+ etablissement_dates = ['entreprise_date_creation'].map { |column| dossier_col(table: 'etablissement', column:, type: :date) }
+
+ for_export = ["siege_social", "naf", "adresse", "numero_voie", "type_voie", "nom_voie", "complement_adresse", "localite", "code_insee_localite", "entreprise_siren", "entreprise_capital_social", "entreprise_numero_tva_intracommunautaire", "entreprise_forme_juridique_code", "entreprise_code_effectif_entreprise", "entreprise_etat_administratif", "entreprise_nom", "entreprise_prenom", "association_rna", "association_titre", "association_objet", "association_date_creation", "association_date_declaration", "association_date_publication"]
+ .map { |column| dossier_col(table: 'etablissement', column:, displayable: false, filterable: false) }
+
+ other = ['siret', 'libelle_naf', 'code_postal'].map { |column| dossier_col(table: 'etablissement', column:) }
+
+ [etablissements, etablissement_dates, other, for_export].flatten
+ end
+
+ def types_de_champ_columns
+ all_revisions_types_de_champ.flat_map { _1.columns(procedure: self) }
+ end
+
+ def dossier_col(**args) = Columns::DossierColumn.new(**(args.merge(procedure_id: id)))
+ end
+end
diff --git a/app/models/concerns/domain_migratable_concern.rb b/app/models/concerns/domain_migratable_concern.rb
index 65be5f723..034314c06 100644
--- a/app/models/concerns/domain_migratable_concern.rb
+++ b/app/models/concerns/domain_migratable_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DomainMigratableConcern
extend ActiveSupport::Concern
diff --git a/app/models/concerns/dossier_champs_concern.rb b/app/models/concerns/dossier_champs_concern.rb
new file mode 100644
index 000000000..f6e9baf0a
--- /dev/null
+++ b/app/models/concerns/dossier_champs_concern.rb
@@ -0,0 +1,208 @@
+# frozen_string_literal: true
+
+module DossierChampsConcern
+ extend ActiveSupport::Concern
+
+ def project_champ(type_de_champ, row_id)
+ check_valid_row_id?(type_de_champ, row_id)
+ champ = champs_by_public_id[type_de_champ.public_id(row_id)]
+ if champ.nil? || !champ.is_type?(type_de_champ.type_champ)
+ value = type_de_champ.champ_blank?(champ) ? nil : champ.value
+ updated_at = champ&.updated_at || depose_at || created_at
+ rebased_at = champ&.rebased_at
+ type_de_champ.build_champ(dossier: self, row_id:, updated_at:, rebased_at:, value:)
+ else
+ champ
+ end
+ end
+
+ def project_champs_public
+ @project_champs_public ||= revision.types_de_champ_public.map { project_champ(_1, nil) }
+ end
+
+ def project_champs_private
+ @project_champs_private ||= revision.types_de_champ_private.map { project_champ(_1, nil) }
+ end
+
+ def filled_champs_public
+ @filled_champs_public ||= project_champs_public.flat_map do |champ|
+ if champ.repetition?
+ champ.rows.flatten.filter { _1.persisted? && _1.fillable? }
+ elsif champ.persisted? && champ.fillable?
+ champ
+ else
+ []
+ end
+ end
+ end
+
+ def filled_champs_private
+ @filled_champs_private ||= project_champs_private.flat_map do |champ|
+ if champ.repetition?
+ champ.rows.flatten.filter { _1.persisted? && _1.fillable? }
+ elsif champ.persisted? && champ.fillable?
+ champ
+ else
+ []
+ end
+ end
+ end
+
+ def filled_champs
+ filled_champs_public + filled_champs_private
+ end
+
+ def project_rows_for(type_de_champ)
+ return [] if !type_de_champ.repetition?
+
+ children = revision.children_of(type_de_champ)
+ row_ids = repetition_row_ids(type_de_champ)
+
+ row_ids.map do |row_id|
+ children.map { project_champ(_1, row_id) }
+ end
+ end
+
+ def find_type_de_champ_by_stable_id(stable_id, scope = nil)
+ case scope
+ when :public
+ revision.types_de_champ.public_only
+ when :private
+ revision.types_de_champ.private_only
+ else
+ revision.types_de_champ
+ end.find_by!(stable_id:)
+ end
+
+ def champs_for_prefill(stable_ids)
+ revision
+ .types_de_champ
+ .filter { _1.stable_id.in?(stable_ids) }
+ .filter { !_1.child?(revision) }
+ .map { _1.repetition? ? project_champ(_1, nil) : champ_for_update(_1, nil, updated_by: nil) }
+ end
+
+ def champ_value_for_tag(type_de_champ, path = :value)
+ champ = filled_champ(type_de_champ, nil)
+ type_de_champ.champ_value_for_tag(champ, path)
+ end
+
+ def champ_for_update(type_de_champ, row_id, updated_by:)
+ champ, attributes = champ_with_attributes_for_update(type_de_champ, row_id, updated_by:)
+ champ.assign_attributes(attributes)
+ champ
+ end
+
+ def update_champs_attributes(attributes, scope, updated_by:)
+ champs_attributes = attributes.to_h.map do |public_id, attributes|
+ champ_attributes_by_public_id(public_id, attributes, scope, updated_by:)
+ end
+
+ assign_attributes(champs_attributes:)
+ end
+
+ def repetition_rows_for_export(type_de_champ)
+ repetition_row_ids(type_de_champ).map.with_index(1) do |row_id, index|
+ Champs::RepetitionChamp::Row.new(index:, row_id:, dossier: self)
+ end
+ end
+
+ def repetition_row_ids(type_de_champ)
+ return [] if !type_de_champ.repetition?
+
+ stable_ids = revision.children_of(type_de_champ).map(&:stable_id)
+ champs.filter { _1.stable_id.in?(stable_ids) && _1.row_id.present? }
+ .map(&:row_id)
+ .uniq
+ .sort
+ end
+
+ def repetition_add_row(type_de_champ, updated_by:)
+ raise "Can't add row to non-repetition type de champ" if !type_de_champ.repetition?
+
+ row_id = ULID.generate
+ types_de_champ = revision.children_of(type_de_champ)
+ self.champs += types_de_champ.map { _1.build_champ(row_id:, updated_by:) }
+ reload_champs_cache
+ row_id
+ end
+
+ def repetition_remove_row(type_de_champ, row_id, updated_by:)
+ raise "Can't remove row from non-repetition type de champ" if !type_de_champ.repetition?
+
+ champs.where(row_id:).destroy_all
+ reload_champs_cache
+ end
+
+ def reload
+ super.tap { reset_champs_cache }
+ end
+
+ private
+
+ def champs_by_public_id
+ @champs_by_public_id ||= champs.sort_by(&:id).index_by(&:public_id)
+ end
+
+ def filled_champ(type_de_champ, row_id)
+ champ = champs_by_public_id[type_de_champ.public_id(row_id)]
+ if type_de_champ.champ_blank?(champ) || !champ.visible?
+ nil
+ else
+ champ
+ end
+ end
+
+ def champ_attributes_by_public_id(public_id, attributes, scope, updated_by:)
+ stable_id, row_id = public_id.split('-')
+ type_de_champ = find_type_de_champ_by_stable_id(stable_id, scope)
+ champ_with_attributes_for_update(type_de_champ, row_id, updated_by:).last.merge(attributes)
+ end
+
+ def champ_with_attributes_for_update(type_de_champ, row_id, updated_by:)
+ check_valid_row_id?(type_de_champ, row_id)
+ attributes = type_de_champ.params_for_champ
+ # TODO: Once we have the right index in place, we should change this to use `create_or_find_by` instead of `find_or_create_by`
+ champ = champs
+ .create_with(**attributes)
+ .find_or_create_by!(stable_id: type_de_champ.stable_id, row_id:)
+
+ attributes[:id] = champ.id
+ attributes[:updated_by] = updated_by
+
+ # Needed when a revision change the champ type in this case, we reset the champ data
+ if champ.type != attributes[:type]
+ attributes[:value] = nil
+ attributes[:value_json] = nil
+ attributes[:external_id] = nil
+ attributes[:data] = nil
+ end
+
+ reset_champs_cache
+
+ [champ, attributes]
+ end
+
+ def check_valid_row_id?(type_de_champ, row_id)
+ if type_de_champ.child?(revision)
+ if row_id.blank?
+ raise "type_de_champ #{type_de_champ.stable_id} in revision #{revision_id} must have a row_id because it is part of a repetition"
+ end
+ elsif row_id.present? && type_de_champ.in_revision?(revision)
+ raise "type_de_champ #{type_de_champ.stable_id} in revision #{revision_id} can not have a row_id because it is not part of a repetition"
+ end
+ end
+
+ def reset_champs_cache
+ @champs_by_public_id = nil
+ @filled_champs_public = nil
+ @filled_champs_private = nil
+ @project_champs_public = nil
+ @project_champs_private = nil
+ end
+
+ def reload_champs_cache
+ champs.reload if persisted?
+ reset_champs_cache
+ end
+end
diff --git a/app/models/concerns/dossier_clone_concern.rb b/app/models/concerns/dossier_clone_concern.rb
index 537afd8d4..193561fe4 100644
--- a/app/models/concerns/dossier_clone_concern.rb
+++ b/app/models/concerns/dossier_clone_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DossierCloneConcern
extend ActiveSupport::Concern
@@ -42,10 +44,10 @@ module DossierCloneConcern
end
def make_diff(editing_fork)
- origin_champs_index = champs_for_revision(scope: :public).index_by(&:public_id)
- forked_champs_index = editing_fork.champs_for_revision(scope: :public).index_by(&:public_id)
+ origin_champs_index = project_champs_public_all.index_by(&:public_id)
+ forked_champs_index = editing_fork.project_champs_public_all.index_by(&:public_id)
updated_champs_index = editing_fork
- .champs_for_revision(scope: :public)
+ .project_champs_public_all
.filter { _1.updated_at > editing_fork.created_at }
.index_by(&:public_id)
@@ -69,9 +71,10 @@ module DossierCloneConcern
diff = make_diff(editing_fork)
apply_diff(diff)
touch(:last_champ_updated_at)
+ touch(:last_champ_piece_jointe_updated_at) if diff[:updated].any? { |c| c.class.in?([Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp]) }
end
reload
- update_search_terms_later
+ index_search_terms_later
editing_fork.destroy_editing_fork!
end
@@ -80,7 +83,7 @@ module DossierCloneConcern
dossier_attributes += [:groupe_instructeur_id] if fork
relationships = [:individual, :etablissement]
- cloned_champs = champs_for_revision
+ cloned_champs = champs
.index_by(&:id)
.transform_values { [_1, _1.clone(fork)] }
@@ -98,7 +101,6 @@ module DossierCloneConcern
kopy.state = Dossier.states.fetch(:brouillon)
kopy.champs = cloned_champs.values.map do |(_, champ)|
champ.dossier = kopy
- champ.parent = cloned_champs[champ.parent_id].second if champ.child?
champ
end
end
@@ -120,6 +122,7 @@ module DossierCloneConcern
end
end
+ cloned_dossier.index_search_terms_later if !fork
cloned_dossier.reload
end
@@ -146,35 +149,34 @@ module DossierCloneConcern
end
def apply_diff(diff)
- champs_index = (champs_for_revision(scope: :public) + diff[:added]).index_by(&:public_id)
+ champs_added = diff[:added].filter(&:persisted?)
+ champs_updated = diff[:updated].filter(&:persisted?)
+ champs_removed = diff[:removed].filter(&:persisted?)
- diff[:added].each do |champ|
- if champ.child?
- champ.update_columns(dossier_id: id, parent_id: champs_index.fetch(champ.parent.public_id).id)
- else
+ champs_added.each { _1.update_column(:dossier_id, id) }
+
+ if champs_updated.present?
+ champs_index = filled_champs_public.index_by(&:public_id)
+ champs_updated.each do |champ|
+ champs_index[champ.public_id]&.destroy!
champ.update_column(:dossier_id, id)
end
end
- champs_to_remove = []
- diff[:updated].each do |champ|
- old_champ = champs_index.fetch(champ.public_id)
- champs_to_remove << old_champ
+ champs_removed.each(&:destroy!)
+ end
- if champ.child?
- # we need to do that in order to avoid a foreign key constraint
- old_champ.update(row_id: nil)
- champ.update_columns(dossier_id: id, parent_id: champs_index.fetch(champ.parent.public_id).id)
+ protected
+
+ # This is a temporary method that is only used by diff/merge algorithm. Once it's gone, this method should be removed.
+ def project_champs_public_all
+ revision.types_de_champ_public.flat_map do |type_de_champ|
+ champ = project_champ(type_de_champ, nil)
+ if type_de_champ.repetition?
+ [champ] + project_rows_for(type_de_champ).flatten
else
- champ.update_column(:dossier_id, id)
+ champ
end
end
-
- champs_to_remove += diff[:removed]
- children_champs_to_remove, root_champs_to_remove = champs_to_remove.partition(&:child?)
-
- children_champs_to_remove.each(&:destroy!)
- Champ.where(parent_id: root_champs_to_remove.map(&:id)).destroy_all
- root_champs_to_remove.each(&:destroy!)
end
end
diff --git a/app/models/concerns/dossier_correctable_concern.rb b/app/models/concerns/dossier_correctable_concern.rb
index 0e5876985..04d8f6c7f 100644
--- a/app/models/concerns/dossier_correctable_concern.rb
+++ b/app/models/concerns/dossier_correctable_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DossierCorrectableConcern
extend ActiveSupport::Concern
diff --git a/app/models/concerns/dossier_empty_concern.rb b/app/models/concerns/dossier_empty_concern.rb
new file mode 100644
index 000000000..af2acbbab
--- /dev/null
+++ b/app/models/concerns/dossier_empty_concern.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module DossierEmptyConcern
+ extend ActiveSupport::Concern
+
+ included do
+ scope :empty_brouillon, -> (created_at) do
+ dossiers_ids = Dossier.brouillon.where(created_at:).ids
+
+ dossiers_with_value = Dossier.select('id').includes(:champs)
+ .where.not(champs: { value: nil })
+ .where(id: dossiers_ids)
+
+ dossier_with_geo_areas = Dossier.select('id').includes(champs: :geo_areas)
+ .where.not(geo_areas: { id: nil })
+ .where(id: dossiers_ids)
+
+ dossier_with_pj = Dossier.select('id')
+ .joins(champs: :piece_justificative_file_attachments)
+ .where(id: dossiers_ids)
+
+ brouillon
+ .where.not(id: dossiers_with_value)
+ .where.not(id: dossier_with_geo_areas)
+ .where.not(id: dossier_with_pj)
+ .where(id: dossiers_ids)
+ end
+ end
+end
diff --git a/app/models/concerns/dossier_export_concern.rb b/app/models/concerns/dossier_export_concern.rb
new file mode 100644
index 000000000..887b48207
--- /dev/null
+++ b/app/models/concerns/dossier_export_concern.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+module DossierExportConcern
+ extend ActiveSupport::Concern
+
+ def spreadsheet_columns_csv(types_de_champ:, export_template: nil)
+ spreadsheet_columns(with_etablissement: true, types_de_champ:, export_template:, format: :csv)
+ end
+
+ def spreadsheet_columns_xlsx(types_de_champ:, export_template: nil)
+ spreadsheet_columns(types_de_champ:, export_template:, format: :xlsx)
+ end
+
+ def spreadsheet_columns_ods(types_de_champ:, export_template: nil)
+ spreadsheet_columns(types_de_champ:, export_template:, format: :ods)
+ end
+
+ def champ_values_for_export(types_de_champ, row_id: nil, export_template: nil, format:)
+ types_de_champ.flat_map do |type_de_champ|
+ champ = filled_champ(type_de_champ, row_id)
+ if export_template.present?
+ export_template
+ .columns_for_stable_id(type_de_champ.stable_id)
+ .map { |exported_column| exported_column.libelle_with_value(champ, format:) }
+ else
+ type_de_champ.libelles_for_export.map do |(libelle, path)|
+ [libelle, type_de_champ.champ_value_for_export(champ, path)]
+ end
+ end
+ end
+ end
+
+ def spreadsheet_columns(types_de_champ:, with_etablissement: false, export_template: nil, format: nil)
+ dossier_values_for_export(with_etablissement:, export_template:, format:) + champ_values_for_export(types_de_champ, export_template:, format:)
+ end
+
+ private
+
+ def dossier_values_for_export(with_etablissement: false, export_template: nil, format:)
+ if export_template.present?
+ return export_template.dossier_exported_columns.map { _1.libelle_with_value(self, format:) }
+ end
+
+ columns = [
+ ['ID', id.to_s],
+ ['Email', user_email_for(:display)],
+ ['FranceConnect ?', user_from_france_connect?]
+ ]
+
+ if procedure.for_individual?
+ columns += [
+ ['Civilité', individual&.gender],
+ ['Nom', individual&.nom],
+ ['Prénom', individual&.prenom],
+ ['Dépôt pour un tiers', :for_tiers],
+ ['Nom du mandataire', :mandataire_last_name],
+ ['Prénom du mandataire', :mandataire_first_name]
+ ]
+ if procedure.ask_birthday
+ columns += [['Date de naissance', individual&.birthdate]]
+ end
+ 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 état administratif', etablissement&.entreprise_etat_administratif],
+ ['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
+ if procedure.chorusable? && procedure.chorus_configuration.complete?
+ columns += [
+ ['Domaine Fonctionnel', procedure.chorus_configuration.domaine_fonctionnel&.fetch("code") { '' }],
+ ['Référentiel De Programmation', procedure.chorus_configuration.referentiel_de_programmation&.fetch("code") { '' }],
+ ['Centre De Coût', procedure.chorus_configuration.centre_de_cout&.fetch("code") { '' }]
+ ]
+ end
+ columns += [
+ ['Archivé', :archived],
+ ['État du dossier', Dossier.human_attribute_name("state.#{state}")],
+ ['Dernière mise à jour le', :updated_at],
+ ['Dernière mise à jour du dossier le', :last_champ_updated_at],
+ ['Déposé le', :depose_at],
+ ['Passé en instruction le', :en_instruction_at],
+ procedure.sva_svr_enabled? ? ["Date décision #{procedure.sva_svr_configuration.human_decision}", :sva_svr_decision_on] : nil,
+ ['Traité le', :processed_at],
+ ['Motivation de la décision', :motivation],
+ ['Instructeurs', followers_instructeurs.map(&:email).join(' ')]
+ ].compact
+
+ if procedure.routing_enabled?
+ columns << ['Groupe instructeur', groupe_instructeur.label]
+ end
+
+ columns
+ end
+end
diff --git a/app/models/concerns/dossier_filtering_concern.rb b/app/models/concerns/dossier_filtering_concern.rb
index 8c46a11e8..7883ae700 100644
--- a/app/models/concerns/dossier_filtering_concern.rb
+++ b/app/models/concerns/dossier_filtering_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DossierFilteringConcern
extend ActiveSupport::Concern
@@ -26,10 +28,13 @@ module DossierFilteringConcern
end
}
- scope :filter_ilike, lambda { |table, column, values|
- table_column = ProcedurePresentation.sanitized_column(table, column)
- q = Array.new(values.count, "(#{table_column} ILIKE ?)").join(' OR ')
- where(q, *(values.map { |value| "%#{value}%" }))
+ scope :filter_ilike, lambda { |table, column, search_terms|
+ safe_quoted_terms = search_terms.map { "%#{sanitize_sql_like(_1)}%" }
+ table_column = DossierFilterService.sanitized_column(table, column)
+
+ where("#{table_column} LIKE ANY (ARRAY[?])", safe_quoted_terms)
}
+
+ def sanitize_sql_like(q) = ActiveRecord::Base.sanitize_sql_like(q)
end
end
diff --git a/app/models/concerns/dossier_prefillable_concern.rb b/app/models/concerns/dossier_prefillable_concern.rb
index bbcfdc8e1..d0c21a2bf 100644
--- a/app/models/concerns/dossier_prefillable_concern.rb
+++ b/app/models/concerns/dossier_prefillable_concern.rb
@@ -13,8 +13,4 @@ module DossierPrefillableConcern
assign_attributes(attributes)
save(validate: false)
end
-
- def find_champs_by_stable_ids(stable_ids)
- champs.joins(:type_de_champ).where(types_de_champ: { stable_id: stable_ids.compact.uniq })
- end
end
diff --git a/app/models/concerns/dossier_rebase_concern.rb b/app/models/concerns/dossier_rebase_concern.rb
index dd6395dc2..675bd6117 100644
--- a/app/models/concerns/dossier_rebase_concern.rb
+++ b/app/models/concerns/dossier_rebase_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DossierRebaseConcern
extend ActiveSupport::Concern
@@ -22,7 +24,7 @@ module DossierRebaseConcern
end
def pending_changes
- procedure.published_revision.present? ? revision.compare(procedure.published_revision) : []
+ procedure.published_revision.present? ? revision.compare_types_de_champ(procedure.published_revision) : []
end
def can_rebase_mandatory_change?(stable_id)
@@ -50,7 +52,7 @@ module DossierRebaseConcern
# index published types de champ coordinates by stable_id
target_coordinates_by_stable_id = target_revision
.revision_types_de_champ
- .includes(:type_de_champ, :parent)
+ .includes(:parent)
.index_by(&:stable_id)
changes_by_op = pending_changes
@@ -58,96 +60,45 @@ module DossierRebaseConcern
.tap { _1.default = [] }
champs_by_stable_id = champs
- .includes(:type_de_champ)
.group_by(&:stable_id)
.transform_values { Champ.where(id: _1) }
.tap { _1.default = Champ.none }
- # add champ
- changes_by_op[:add]
- .map { target_coordinates_by_stable_id[_1.stable_id] }
- # add parent champs first so we can then add children
- .sort_by { _1.child? ? 1 : 0 }
- .each { add_new_champs_for_revision(_1) }
-
# remove champ
- children_champ, root_champ = changes_by_op[:remove].partition(&:child?)
- children_champ.each { champs_by_stable_id[_1.stable_id].destroy_all }
- root_champ.each { champs_by_stable_id[_1.stable_id].destroy_all }
+ changes_by_op[:remove].each { champs_by_stable_id[_1.stable_id].destroy_all }
# update champ
- changes_by_op[:update].each { apply(_1, champs_by_stable_id[_1.stable_id]) }
-
- # due to repetition tdc clone on update or erase
- # we must reassign tdc to the latest version
- champs_by_stable_id.each do |stable_id, champs|
- if target_coordinates_by_stable_id[stable_id].present? && champs.present?
- champs.update_all(type_de_champ_id: target_coordinates_by_stable_id[stable_id].type_de_champ_id)
- end
- end
+ changes_by_op[:update].each { champs_by_stable_id[_1.stable_id].update_all(rebased_at: Time.zone.now) }
# update dossier revision
update_column(:revision_id, target_revision.id)
- end
- def apply(change, champs)
- case change.attribute
- when :type_champ
- champs.each { purge_piece_justificative_file(_1) }
- GeoArea.where(champ: champs).destroy_all
- Etablissement.where(champ: champs).destroy_all
- champs.update_all(type: "Champs::#{change.to.classify}Champ",
- value: nil,
- value_json: nil,
- external_id: nil,
- data: nil,
- rebased_at: Time.zone.now)
- when :drop_down_options
- # we are removing options, we need to remove the value if it contains one of the removed options
- removed_options = change.from - change.to
- if removed_options.present? && champs.any? { _1.in?(removed_options) }
- champs.filter { _1.in?(removed_options) }.each do
- _1.remove_option(removed_options)
- _1.update_column(:rebased_at, Time.zone.now)
- end
- end
- when :carte_layers
- # if we are removing cadastres layer, we need to remove cadastre geo areas
- if change.from.include?(:cadastres) && !change.to.include?(:cadastres)
- champs.filter { _1.cadastres.present? }.each do
- _1.cadastres.each(&:destroy)
- _1.update_column(:rebased_at, Time.zone.now)
- end
- end
- else
- champs.update_all(rebased_at: Time.zone.now)
- end
+ # add champ (after changing dossier revision to avoid errors)
+ changes_by_op[:add]
+ .map { target_coordinates_by_stable_id[_1.stable_id] }
+ .each { add_new_champs_for_revision(_1) }
end
def add_new_champs_for_revision(target_coordinate)
if target_coordinate.child?
- # If this type de champ is a child, we create a new champ for each row of the parent
- parent_stable_id = target_coordinate.parent.stable_id
+ row_ids = repetition_row_ids(target_coordinate.parent.type_de_champ)
- champs.filter { _1.stable_id == parent_stable_id }.each do |champ_repetition|
- if champ_repetition.champs.present?
- champ_repetition.champs.map(&:row_id).uniq.each do |row_id|
- champs << create_champ(target_coordinate, champ_repetition, row_id:)
- end
- elsif champ_repetition.mandatory?
- champs << create_champ(target_coordinate, champ_repetition, row_id: ULID.generate)
+ if row_ids.present?
+ row_ids.each do |row_id|
+ create_champ(target_coordinate, row_id:)
end
+ elsif target_coordinate.parent.mandatory?
+ create_champ(target_coordinate, row_id: ULID.generate)
end
else
- create_champ(target_coordinate, self)
+ create_champ(target_coordinate)
end
end
- def create_champ(target_coordinate, parent, row_id: nil)
- target_coordinate
+ def create_champ(target_coordinate, row_id: nil)
+ self.champs << target_coordinate
.type_de_champ
.build_champ(rebased_at: Time.zone.now, row_id:)
- .tap { parent.champs << _1 }
end
def purge_piece_justificative_file(champ)
diff --git a/app/models/concerns/dossier_searchable_concern.rb b/app/models/concerns/dossier_searchable_concern.rb
index 1bb9ef424..13f65a602 100644
--- a/app/models/concerns/dossier_searchable_concern.rb
+++ b/app/models/concerns/dossier_searchable_concern.rb
@@ -1,23 +1,40 @@
+# frozen_string_literal: true
+
module DossierSearchableConcern
extend ActiveSupport::Concern
included do
- before_save :update_search_terms
+ after_commit :index_search_terms_later, if: -> { previously_new_record? || user_previously_changed? || mandataire_first_name_previously_changed? || mandataire_last_name_previously_changed? }
- def update_search_terms
- self.search_terms = [
+ SEARCH_TERMS_DEBOUNCE = 5.minutes
+
+ kredis_flag :debounce_index_search_terms_flag
+
+ def index_search_terms
+ DossierPreloader.load_one(self)
+
+ search_terms = [
user&.email,
- *champs_public.flat_map(&:search_terms),
+ *project_champs_public.flat_map(&:search_terms),
*etablissement&.search_terms,
individual&.nom,
- individual&.prenom
+ individual&.prenom,
+ mandataire_first_name,
+ mandataire_last_name
].compact_blank.join(' ')
- self.private_search_terms = champs_private.flat_map(&:search_terms).compact_blank.join(' ')
+ private_search_terms = project_champs_private.flat_map(&:search_terms).compact_blank.join(' ')
+
+ sql = "UPDATE dossiers SET search_terms = :search_terms, private_search_terms = :private_search_terms WHERE id = :id"
+ sanitized_sql = self.class.sanitize_sql_array([sql, search_terms:, private_search_terms:, id:])
+ self.class.connection.execute(sanitized_sql)
end
- def update_search_terms_later
- DossierUpdateSearchTermsJob.perform_later(self)
+ def index_search_terms_later
+ return if debounce_index_search_terms_flag.marked?
+
+ debounce_index_search_terms_flag.mark(expires_in: SEARCH_TERMS_DEBOUNCE)
+ DossierIndexSearchTermsJob.set(wait: SEARCH_TERMS_DEBOUNCE).perform_later(self)
end
end
end
diff --git a/app/models/concerns/dossier_sections_concern.rb b/app/models/concerns/dossier_sections_concern.rb
index d49a1e2d7..c4644dba3 100644
--- a/app/models/concerns/dossier_sections_concern.rb
+++ b/app/models/concerns/dossier_sections_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DossierSectionsConcern
extend ActiveSupport::Concern
@@ -17,7 +19,7 @@ module DossierSectionsConcern
end
def auto_numbering_section_headers_for?(type_de_champ)
- return false if revision.child?(type_de_champ)
+ return false if type_de_champ.child?(revision)
sections_for(type_de_champ)&.none? { _1.libelle =~ /^\d/ }
end
diff --git a/app/models/concerns/dossier_state_concern.rb b/app/models/concerns/dossier_state_concern.rb
index 1c99ffb65..7b60de6ad 100644
--- a/app/models/concerns/dossier_state_concern.rb
+++ b/app/models/concerns/dossier_state_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module DossierStateConcern
extend ActiveSupport::Concern
@@ -13,11 +15,14 @@ module DossierStateConcern
MailTemplatePresenterService.create_commentaire_for_state(self, Dossier.states.fetch(:en_construction))
procedure.compute_dossiers_count
+
+ index_search_terms_later
end
def after_commit_passer_en_construction
NotificationMailer.send_en_construction_notification(self).deliver_later
NotificationMailer.send_notification_for_tiers(self).deliver_later if self.for_tiers?
+ remove_piece_justificative_file_not_visible!
end
def after_passer_en_instruction(h)
@@ -60,7 +65,6 @@ module DossierStateConcern
MailTemplatePresenterService.create_commentaire_for_state(self, Dossier.states.fetch(:en_instruction))
if procedure.sva_svr_enabled?
- # TODO: handle serialization errors when SIRET demandeur was not completed
log_automatic_dossier_operation(:passer_en_instruction, self)
else
log_automatic_dossier_operation(:passer_en_instruction)
diff --git a/app/models/concerns/email_sanitizable_concern.rb b/app/models/concerns/email_sanitizable_concern.rb
index d143c7c7d..605d10519 100644
--- a/app/models/concerns/email_sanitizable_concern.rb
+++ b/app/models/concerns/email_sanitizable_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module EmailSanitizableConcern
extend ActiveSupport::Concern
@@ -8,6 +10,26 @@ module EmailSanitizableConcern
end
end
+ def generate_emails_suggestions_message(suggestions)
+ return if suggestions.empty?
+
+ typo_list = suggestions.map(&:first).join(', ')
+ verification_link = view_context.link_to("vérifier l’orthographe", "#maybe_typos_errors")
+
+ "Attention, nous pensons avoir identifié une faute de frappe dans les invitations : #{typo_list}. Veuillez #{verification_link} des invitations."
+ end
+
+ def check_if_typo(emails)
+ emails = emails.map { EmailSanitizer.sanitize(_1) }
+ @maybe_typos, no_suggestions = emails
+ .map { |email| [email, EmailChecker.check(email:)[:suggestions]&.first] }
+ .partition { _1[1].present? }
+
+ emails = no_suggestions.map(&:first)
+ emails << EmailSanitizer.sanitize(params['final_email']) if params['final_email'].present?
+ emails
+ end
+
class EmailSanitizer
def self.sanitize(value)
value.gsub(/[[:space:]]/, ' ').strip.downcase
diff --git a/app/models/concerns/encryptable_concern.rb b/app/models/concerns/encryptable_concern.rb
index ebb9ff09c..8fd653ae9 100644
--- a/app/models/concerns/encryptable_concern.rb
+++ b/app/models/concerns/encryptable_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module EncryptableConcern
extend ActiveSupport::Concern
diff --git a/app/models/concerns/initiation_procedure_concern.rb b/app/models/concerns/initiation_procedure_concern.rb
index 089379b39..7f67c2481 100644
--- a/app/models/concerns/initiation_procedure_concern.rb
+++ b/app/models/concerns/initiation_procedure_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module InitiationProcedureConcern
extend ActiveSupport::Concern
@@ -28,7 +30,7 @@ module InitiationProcedureConcern
telephone: '1234',
horaires: 'de 9 h à 18 h',
adresse: 'adresse',
- siret: '35600082800018',
+ siret: Service::SIRET_TEST,
etablissement_infos: { adresse: "75 rue du Louvre\n75002\nPARIS\nFRANCE" },
etablissement_lat: 48.87,
etablissement_lng: 2.34,
diff --git a/app/models/concerns/mail_template_concern.rb b/app/models/concerns/mail_template_concern.rb
index 4310a5ce4..0325d7239 100644
--- a/app/models/concerns/mail_template_concern.rb
+++ b/app/models/concerns/mail_template_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MailTemplateConcern
extend ActiveSupport::Concern
@@ -48,6 +50,6 @@ module MailTemplateConcern
end
def dossier_tags
- TagsSubstitutionConcern::DOSSIER_TAGS + TagsSubstitutionConcern::DOSSIER_TAGS_FOR_MAIL
+ super + TagsSubstitutionConcern::DOSSIER_TAGS_FOR_MAIL
end
end
diff --git a/app/models/concerns/password_complexity_concern.rb b/app/models/concerns/password_complexity_concern.rb
index bbcef17fc..47f053d45 100644
--- a/app/models/concerns/password_complexity_concern.rb
+++ b/app/models/concerns/password_complexity_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module PasswordComplexityConcern
extend ActiveSupport::Concern
diff --git a/app/models/concerns/pieces_jointes_list_concern.rb b/app/models/concerns/pieces_jointes_list_concern.rb
new file mode 100644
index 000000000..5fa84792d
--- /dev/null
+++ b/app/models/concerns/pieces_jointes_list_concern.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module PiecesJointesListConcern
+ extend ActiveSupport::Concern
+
+ included do
+ def public_wrapped_partionned_pjs
+ pieces_jointes(public_only: true, wrap_with_parent: true)
+ .partition { |(pj, _)| pj.condition.nil? }
+ end
+
+ def exportables_pieces_jointes
+ pieces_jointes(exclude_titre_identite: true)
+ end
+
+ def exportables_pieces_jointes_for_all_versions
+ pieces_jointes(
+ exclude_titre_identite: true,
+ revision: revisions
+ ).sort_by { - _1.id }.uniq(&:stable_id)
+ end
+
+ def outdated_exportables_pieces_jointes
+ exportables_pieces_jointes_for_all_versions - exportables_pieces_jointes
+ end
+
+ private
+
+ def pieces_jointes(
+ exclude_titre_identite: false,
+ public_only: false,
+ wrap_with_parent: false,
+ revision: active_revision
+ )
+ coordinates = ProcedureRevisionTypeDeChamp.where(revision:)
+ .includes(:type_de_champ, revision_types_de_champ: :type_de_champ)
+
+ coordinates = coordinates.public_only if public_only
+
+ type_champ = ['piece_justificative']
+ type_champ << 'titre_identite' if !exclude_titre_identite
+
+ coordinates = coordinates.where(types_de_champ: { type_champ: })
+
+ return coordinates.map(&:type_de_champ) if !wrap_with_parent
+
+ # we want pj in the form of [[pj1], [pj2, repetition], [pj3, repetition]]
+ coordinates
+ .map { |c| c.child? ? [c, c.parent] : [c] }
+ .map { |a| a.map(&:type_de_champ) }
+ end
+ end
+end
diff --git a/app/models/concerns/prefillable_from_service_public_concern.rb b/app/models/concerns/prefillable_from_service_public_concern.rb
new file mode 100644
index 000000000..bcf4f8475
--- /dev/null
+++ b/app/models/concerns/prefillable_from_service_public_concern.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module PrefillableFromServicePublicConcern
+ extend ActiveSupport::Concern
+
+ included do
+ def prefill_from_siret
+ future_sp = Concurrent::Future.execute { AnnuaireServicePublicService.new.call(siret:) }
+ future_api_ent = Concurrent::Future.execute { APIRechercheEntreprisesService.new.call(siret:) }
+
+ result_sp = future_sp.value!
+ result_api_ent = future_api_ent.value!
+
+ prefill_from_service_public(result_sp)
+ prefill_from_api_entreprise(result_api_ent)
+
+ [result_sp, result_api_ent]
+ end
+
+ private
+
+ def prefill_from_service_public(result)
+ case result
+ in Dry::Monads::Success(data)
+ self.nom = data[:nom] if nom.blank?
+ self.email = data[:adresse_courriel] if email.blank?
+ self.telephone = data[:telephone]&.first&.dig("valeur") if telephone.blank?
+ self.horaires = denormalize_plage_ouverture(data[:plage_ouverture]) if horaires.blank?
+ self.adresse = APIGeoService.inline_service_public_address(data[:adresse]&.first) if adresse.blank?
+ else
+ # NOOP
+ end
+ end
+
+ def prefill_from_api_entreprise(result)
+ case result
+ in Dry::Monads::Success(data)
+ self.type_organisme = detect_type_organisme(data) if type_organisme.blank?
+ self.nom = data[:nom_complet] if nom.blank?
+ self.adresse = data.dig(:siege, :geo_adresse) if adresse.blank?
+ else
+ # NOOP
+ end
+ end
+
+ def denormalize_plage_ouverture(data)
+ return if data.blank?
+
+ data.map do |range|
+ day_range = range.values_at('nom_jour_debut', 'nom_jour_fin').uniq.join(' au ')
+
+ hours_range = (1..2).each_with_object([]) do |i, hours|
+ start_hour = range["valeur_heure_debut_#{i}"]
+ end_hour = range["valeur_heure_fin_#{i}"]
+
+ if start_hour.present? && end_hour.present?
+ hours << "de #{format_time(start_hour)} à #{format_time(end_hour)}"
+ end
+ end
+
+ result = day_range
+ result += " : #{hours_range.join(' et ')}" if hours_range.present?
+ result += " (#{range['commentaire']})" if range['commentaire'].present?
+ result
+ end.join("\n")
+ end
+
+ def detect_type_organisme(data)
+ # Cf https://recherche-entreprises.api.gouv.fr/docs/#tag/Recherche-textuelle/paths/~1search/get
+ type = if data.dig(:complements, :collectivite_territoriale).present?
+ :collectivite_territoriale
+ elsif data.dig(:complements, :est_association)
+ :association
+ elsif data[:section_activite_principale] == "P"
+ :etablissement_enseignement
+ elsif data[:nom_complet].match?(/MINISTERE|MINISTERIEL/)
+ :administration_centrale
+ else # we can't differentiate between operateur d'état, administration centrale and service déconcentré de l'état, set the most frequent
+ :service_deconcentre_de_l_etat
+ end
+
+ Service.type_organismes[type]
+ end
+
+ def format_time(str_time)
+ Time.zone
+ .parse(str_time)
+ .strftime("%-H:%M")
+ end
+ end
+end
diff --git a/app/models/concerns/procedure_chorus_concern.rb b/app/models/concerns/procedure_chorus_concern.rb
index d940dfafd..1d638c43a 100644
--- a/app/models/concerns/procedure_chorus_concern.rb
+++ b/app/models/concerns/procedure_chorus_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ProcedureChorusConcern
extend ActiveSupport::Concern
diff --git a/app/models/concerns/procedure_groupe_instructeur_api_hack_concern.rb b/app/models/concerns/procedure_groupe_instructeur_api_hack_concern.rb
index cb89bbd6e..86af559bb 100644
--- a/app/models/concerns/procedure_groupe_instructeur_api_hack_concern.rb
+++ b/app/models/concerns/procedure_groupe_instructeur_api_hack_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ProcedureGroupeInstructeurAPIHackConcern
extend ActiveSupport::Concern
diff --git a/app/models/concerns/procedure_publish_concern.rb b/app/models/concerns/procedure_publish_concern.rb
new file mode 100644
index 000000000..85e0fe48c
--- /dev/null
+++ b/app/models/concerns/procedure_publish_concern.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+module ProcedurePublishConcern
+ extend ActiveSupport::Concern
+
+ def publish_or_reopen!(administrateur)
+ Procedure.transaction do
+ if brouillon?
+ reset!
+ end
+
+ other_procedure = other_procedure_with_path(path)
+ if other_procedure.present? && administrateur.owns?(other_procedure)
+ other_procedure.unpublish!
+ publish!(other_procedure.canonical_procedure || other_procedure)
+ else
+ publish!
+ end
+ end
+ end
+
+ def publish_revision!
+ reset!
+
+ transaction { publish_new_revision }
+
+ dossiers
+ .state_not_termine
+ .find_each(&:rebase_later)
+ end
+
+ def reset_draft_revision!
+ if published_revision.present? && draft_changed?
+ reset!
+ transaction do
+ draft_revision.types_de_champ.filter(&:only_present_on_draft?).each(&:destroy)
+ draft_revision.update(dossier_submitted_message: nil)
+ draft_revision.destroy
+ update!(draft_revision: create_new_revision(published_revision))
+ end
+ end
+ end
+
+ def reset!
+ if !locked? || draft_changed?
+ dossier_ids_to_destroy = draft_revision.dossiers.ids
+ if dossier_ids_to_destroy.present?
+ Rails.logger.info("Resetting #{dossier_ids_to_destroy.size} dossiers on procedure #{id}: #{dossier_ids_to_destroy}")
+ draft_revision.dossiers.destroy_all
+ end
+ end
+ end
+
+ def before_publish
+ assign_attributes(closed_at: nil, unpublished_at: nil)
+ end
+
+ def after_publish(canonical_procedure = nil)
+ self.canonical_procedure = canonical_procedure
+ touch(:published_at)
+ publish_new_revision
+ end
+
+ def after_republish(canonical_procedure = nil)
+ touch(:published_at)
+ end
+
+ def after_close
+ touch(:closed_at)
+ end
+
+ def after_unpublish
+ touch(:unpublished_at)
+ end
+
+ def create_new_revision(revision = nil)
+ transaction do
+ new_revision = (revision || draft_revision)
+ .deep_clone(include: [:revision_types_de_champ])
+ .tap { |revision| revision.published_at = nil }
+ .tap(&:save!)
+
+ move_new_children_to_new_parent_coordinate(new_revision)
+
+ # they are not aware of the new tdcs
+ new_revision.types_de_champ_public.reset
+ new_revision.types_de_champ_private.reset
+
+ new_revision
+ end
+ end
+
+ private
+
+ def publish_new_revision
+ cleanup_types_de_champ_options!
+ cleanup_types_de_champ_children!
+ self.published_revision = draft_revision
+ self.draft_revision = create_new_revision
+ save!(context: :publication)
+ published_revision.touch(:published_at)
+ end
+
+ def move_new_children_to_new_parent_coordinate(new_draft)
+ children = new_draft.revision_types_de_champ
+ .includes(parent: :type_de_champ)
+ .where.not(parent_id: nil)
+ coordinates_by_stable_id = new_draft.revision_types_de_champ
+ .includes(:type_de_champ)
+ .index_by(&:stable_id)
+
+ children.each do |child|
+ child.update!(parent: coordinates_by_stable_id.fetch(child.parent.stable_id))
+ end
+ new_draft.reload
+ end
+
+ def cleanup_types_de_champ_options!
+ draft_revision.types_de_champ.each do |type_de_champ|
+ type_de_champ.update!(options: type_de_champ.clean_options)
+ end
+ end
+
+ def cleanup_types_de_champ_children!
+ draft_revision.types_de_champ.reject(&:repetition?).each do |type_de_champ|
+ draft_revision.remove_children_of(type_de_champ)
+ end
+ end
+end
diff --git a/app/models/concerns/procedure_stats_concern.rb b/app/models/concerns/procedure_stats_concern.rb
index 6fdacd7d6..a842213d4 100644
--- a/app/models/concerns/procedure_stats_concern.rb
+++ b/app/models/concerns/procedure_stats_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ProcedureStatsConcern
extend ActiveSupport::Concern
diff --git a/app/models/concerns/procedure_sva_svr_concern.rb b/app/models/concerns/procedure_sva_svr_concern.rb
index b0a92cfd4..09c3316f9 100644
--- a/app/models/concerns/procedure_sva_svr_concern.rb
+++ b/app/models/concerns/procedure_sva_svr_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ProcedureSVASVRConcern
extend ActiveSupport::Concern
diff --git a/app/models/concerns/rna_champ_association_fetchable_concern.rb b/app/models/concerns/rna_champ_association_fetchable_concern.rb
index 1a17691dc..8a5f322e8 100644
--- a/app/models/concerns/rna_champ_association_fetchable_concern.rb
+++ b/app/models/concerns/rna_champ_association_fetchable_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RNAChampAssociationFetchableConcern
extend ActiveSupport::Concern
@@ -10,10 +12,14 @@ module RNAChampAssociationFetchableConcern
return clear_association!(:invalid) unless valid_champ_value?
return clear_association!(:not_found) if (data = APIEntreprise::RNAAdapter.new(rna, procedure_id).to_params).blank?
- update!(data: data)
- rescue APIEntreprise::API::Error => error
- error_key = :network_error if error.try(:network_error?) && !APIEntrepriseService.api_djepva_up?
- clear_association!(error_key)
+ update_with_external_data!(data:)
+ rescue APIEntreprise::API::Error, APIEntrepriseToken::TokenError => error
+ if APIEntrepriseService.service_unavailable_error?(error, target: :djepva)
+ clear_association!(:network_error)
+ else
+ Sentry.capture_exception(error, extra: { dossier_id:, rna: })
+ clear_association!(nil)
+ end
end
private
diff --git a/app/models/concerns/siret_champ_etablissement_fetchable_concern.rb b/app/models/concerns/siret_champ_etablissement_fetchable_concern.rb
index 333d58ad8..e6dbba3e3 100644
--- a/app/models/concerns/siret_champ_etablissement_fetchable_concern.rb
+++ b/app/models/concerns/siret_champ_etablissement_fetchable_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SiretChampEtablissementFetchableConcern
extend ActiveSupport::Concern
@@ -9,17 +11,16 @@ module SiretChampEtablissementFetchableConcern
return clear_etablissement!(:invalid_checksum) if invalid_because?(siret, :checksum) # i18n-tasks-use t('errors.messages.invalid_siret_checksum')
return clear_etablissement!(:not_found) unless (etablissement = APIEntrepriseService.create_etablissement(self, siret, user&.id)) # i18n-tasks-use t('errors.messages.siret_not_found')
- update!(etablissement: etablissement)
- rescue => error
- if error.try(:network_error?) && !APIEntrepriseService.api_insee_up?
- # TODO: notify ops
+ update!(etablissement:)
+ rescue APIEntreprise::API::Error, APIEntrepriseToken::TokenError => error
+ if APIEntrepriseService.service_unavailable_error?(error, target: :insee)
update!(
etablissement: APIEntrepriseService.create_etablissement_as_degraded_mode(self, siret, user.id)
)
@etablissement_fetch_error_key = :api_entreprise_down
false
else
- Sentry.capture_exception(error, extra: { dossier_id: dossier_id, siret: siret })
+ Sentry.capture_exception(error, extra: { dossier_id:, siret: })
clear_etablissement!(:network_error) # i18n-tasks-use t('errors.messages.siret_network_error')
end
end
diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb
index 8bdc51705..d24f945e8 100644
--- a/app/models/concerns/tags_substitution_concern.rb
+++ b/app/models/concerns/tags_substitution_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TagsSubstitutionConcern
extend ActiveSupport::Concern
@@ -10,7 +12,7 @@ module TagsSubstitutionConcern
extend self
def parse(io)
- doc.parse io
+ doc.parse(+io) # parsby mutates the StringIO during parsing!
end
def self.normalize(str)
@@ -61,6 +63,15 @@ module TagsSubstitutionConcern
end
end
+ DOSSIER_ID_TAG = {
+ id: 'dossier_number',
+ label: 'numéro du dossier',
+ libelle: 'numéro du dossier',
+ description: '',
+ lambda: -> (d) { d.id },
+ available_for_states: Dossier::SOUMIS
+ }
+
DOSSIER_TAGS = [
{
id: 'dossier_motivation',
@@ -91,6 +102,13 @@ module TagsSubstitutionConcern
lambda: -> (d) { format_date(d.processed_at) },
available_for_states: Dossier::TERMINE
},
+ {
+ id: 'dossier_last_champ_updated_at',
+ libelle: 'date de mise à jour',
+ description: 'Date de dernière mise à jour d’un champ du dossier',
+ lambda: -> (d) { format_date(d.last_champ_updated_at) },
+ available_for_states: Dossier::SOUMIS
+ },
{
id: 'dossier_procedure_libelle',
libelle: 'libellé démarche',
@@ -98,13 +116,6 @@ module TagsSubstitutionConcern
lambda: -> (d) { d.procedure.libelle },
available_for_states: Dossier::SOUMIS
},
- {
- id: 'dossier_number',
- libelle: 'numéro du dossier',
- description: '',
- target: :id,
- available_for_states: Dossier::SOUMIS
- },
{
id: 'dossier_service_name',
libelle: 'nom du service',
@@ -112,7 +123,7 @@ module TagsSubstitutionConcern
lambda: -> (d) { d.procedure.organisation_name || '' },
available_for_states: Dossier::SOUMIS
}
- ]
+ ].push(DOSSIER_ID_TAG)
DOSSIER_TAGS_FOR_MAIL = [
{
@@ -147,26 +158,34 @@ module TagsSubstitutionConcern
}
]
+ DOSSIER_SVA_SVR_DECISION_DATE_TAG = {
+ id: 'dossier_sva_svr_decision_on',
+ libelle: 'date prévisionnelle SVA/SVR',
+ description: 'Date prévisionnelle de décision automatique par le SVA/SVR',
+ lambda: -> (d) { format_date(d.sva_svr_decision_on) },
+ available_for_states: Dossier.states.fetch(:en_instruction)
+ }
+
INDIVIDUAL_TAGS = [
{
id: 'individual_gender',
libelle: 'civilité',
description: 'M., Mme',
- target: :gender,
+ lambda: -> (d) { d.individual&.gender },
available_for_states: Dossier::SOUMIS
},
{
id: 'individual_last_name',
libelle: 'nom',
description: "nom de l'usager",
- target: :nom,
+ lambda: -> (d) { d.individual&.nom },
available_for_states: Dossier::SOUMIS
},
{
id: 'individual_first_name',
libelle: 'prénom',
description: "prénom de l'usager",
- target: :prenom,
+ lambda: -> (d) { d.individual&.prenom },
available_for_states: Dossier::SOUMIS
}
]
@@ -176,35 +195,35 @@ module TagsSubstitutionConcern
id: 'entreprise_siren',
libelle: 'SIREN',
description: '',
- target: :siren,
+ lambda: -> (d) { d.etablissement&.entreprise&.siren },
available_for_states: Dossier::SOUMIS
},
{
id: 'entreprise_numero_tva_intracommunautaire',
libelle: 'numéro de TVA intracommunautaire',
description: '',
- target: :numero_tva_intracommunautaire,
+ lambda: -> (d) { d.etablissement&.entreprise&.numero_tva_intracommunautaire },
available_for_states: Dossier::SOUMIS
},
{
id: 'entreprise_siret_siege_social',
libelle: 'SIRET du siège social',
description: '',
- target: :siret_siege_social,
+ lambda: -> (d) { d.etablissement&.entreprise&.siret_siege_social },
available_for_states: Dossier::SOUMIS
},
{
id: 'entreprise_raison_sociale',
libelle: 'raison sociale',
description: '',
- target: :raison_sociale,
+ lambda: -> (d) { d.etablissement&.entreprise&.raison_sociale },
available_for_states: Dossier::SOUMIS
},
{
id: 'entreprise_adresse',
libelle: 'adresse',
description: '',
- target: :inline_adresse,
+ lambda: -> (d) { d.etablissement&.entreprise&.inline_adresse },
available_for_states: Dossier::SOUMIS
}
]
@@ -255,7 +274,7 @@ module TagsSubstitutionConcern
def used_type_de_champ_tags(text_or_tiptap)
used_tags =
if text_or_tiptap.respond_to?(:deconstruct_keys) # hash pattern matching
- TiptapService.new.used_tags_and_libelle_for(text_or_tiptap.deep_symbolize_keys)
+ TiptapService.used_tags_and_libelle_for(text_or_tiptap.deep_symbolize_keys)
else
used_tags_and_libelle_for(text_or_tiptap.to_s)
end
@@ -273,7 +292,7 @@ module TagsSubstitutionConcern
used_tags_and_libelle_for(text).map { _1.first.nil? ? _1.second : _1.first }
end
- def tags_substitutions(tags_and_libelles, dossier, escape: true)
+ def tags_substitutions(tags_and_libelles, dossier, escape: true, memoize: false)
# NOTE:
# - tags_and_libelles est un simple Set de couples (tag_id, libelle) (pas la même structure que dans replace_tags)
# - dans `replace_tags`, on fait référence à des tags avec ou sans id, mais pas ici,
@@ -281,20 +300,20 @@ module TagsSubstitutionConcern
@escape_unsafe_tags = escape
- flat_tags = tags_and_datas_list(dossier).each_with_object({}) do |(tags, data), result|
- next if data.nil?
-
- valid_tags = tags_for_dossier_state(tags)
-
- valid_tags.each do |tag|
- result[tag[:id]] = [tag, data]
- end
+ flat_tags = if memoize && @flat_tags.present?
+ @flat_tags
+ else
+ available_tags(dossier)
+ .flatten
+ .then { tags_for_dossier_state(_1) }
+ .index_by { _1[:id] }
end
+ @flat_tags = flat_tags if memoize
+
tags_and_libelles.each_with_object({}) do |(tag_id, libelle), substitutions|
- substitutions[tag_id] = case flat_tags[tag_id]
- in tag, data
- replace_tag(tag, data)
+ substitutions[tag_id] = if flat_tags[tag_id].present?
+ replace_tag(flat_tags[tag_id], dossier)
else # champ not in dossier, for example during preview on draft revision
libelle
end
@@ -305,7 +324,8 @@ module TagsSubstitutionConcern
def format_date(date)
if date.present?
- date.strftime('%d/%m/%Y')
+ format = defined?(self.class::FORMAT_DATE) ? self.class::FORMAT_DATE : '%d/%m/%Y'
+ date.strftime(format)
else
''
end
@@ -323,7 +343,13 @@ module TagsSubstitutionConcern
def dossier_tags
# Overridden by MailTemplateConcern
- DOSSIER_TAGS
+ DOSSIER_TAGS + contextual_dossier_tags
+ end
+
+ def contextual_dossier_tags
+ tags = []
+ tags << DOSSIER_SVA_SVR_DECISION_DATE_TAG if respond_to?(:procedure) && procedure.sva_svr_enabled?
+ tags
end
def tags_for_dossier_state(tags)
@@ -370,8 +396,8 @@ module TagsSubstitutionConcern
tokens = parse_tags(text)
- tags_and_datas = tags_and_datas_list(dossier).filter_map do |(tags, data)|
- data && [tags_for_dossier_state(tags).index_by { _1[:id] }, data]
+ tags_and_datas = available_tags(dossier).filter_map do |tags|
+ dossier && [tags_for_dossier_state(tags).index_by { _1[:id] }, dossier]
end
tags_and_datas.reduce(tokens) do |tokens, (tags, data)|
@@ -397,12 +423,8 @@ module TagsSubstitutionConcern
end.join('')
end
- def replace_tag(tag, data)
- value = if tag.key?(:target)
- data.public_send(tag[:target])
- else
- instance_exec(data, &tag[:lambda])
- end
+ def replace_tag(tag, dossier)
+ value = instance_exec(dossier, &tag[:lambda])
if escape_unsafe_tags? && tag.fetch(:escapable, true)
escape_once(value)
@@ -449,14 +471,14 @@ module TagsSubstitutionConcern
end
end
- def tags_and_datas_list(dossier)
+ def available_tags(dossier)
[
- [champ_public_tags(dossier:), dossier],
- [champ_private_tags(dossier:), dossier],
- [dossier_tags, dossier],
- [ROUTAGE_TAGS, dossier],
- [INDIVIDUAL_TAGS, dossier.individual],
- [ENTREPRISE_TAGS, dossier.etablissement&.entreprise]
+ champ_public_tags(dossier:),
+ champ_private_tags(dossier:),
+ dossier_tags,
+ ROUTAGE_TAGS,
+ INDIVIDUAL_TAGS,
+ ENTREPRISE_TAGS
]
end
end
diff --git a/app/models/concerns/transient_models_with_purgeable_job_concern.rb b/app/models/concerns/transient_models_with_purgeable_job_concern.rb
index 72e5d1c64..04b5bd765 100644
--- a/app/models/concerns/transient_models_with_purgeable_job_concern.rb
+++ b/app/models/concerns/transient_models_with_purgeable_job_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Archive and Export models are generated in background
# those models being are destroy after an expiration period
# but, it might take more time to process than the expiration period
diff --git a/app/models/concerns/treeable_concern.rb b/app/models/concerns/treeable_concern.rb
index 174a54304..c7df40a64 100644
--- a/app/models/concerns/treeable_concern.rb
+++ b/app/models/concerns/treeable_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TreeableConcern
extend ActiveSupport::Concern
diff --git a/app/models/concerns/trusted_device_concern.rb b/app/models/concerns/trusted_device_concern.rb
index 2aa895893..5edbc776d 100644
--- a/app/models/concerns/trusted_device_concern.rb
+++ b/app/models/concerns/trusted_device_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TrustedDeviceConcern
extend ActiveSupport::Concern
@@ -8,7 +10,8 @@ module TrustedDeviceConcern
cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] = {
value: JSON.generate({ created_at: start_at }),
expires: start_at + TRUSTED_DEVICE_PERIOD,
- httponly: true
+ httponly: true,
+ secure: Rails.env.production?
}
end
diff --git a/app/models/concerns/user_find_by_concern.rb b/app/models/concerns/user_find_by_concern.rb
index 7f3290695..6efb7f14d 100644
--- a/app/models/concerns/user_find_by_concern.rb
+++ b/app/models/concerns/user_find_by_concern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module UserFindByConcern
extend ActiveSupport::Concern
diff --git a/app/models/condition_form.rb b/app/models/condition_form.rb
index 14e5b4dc9..7b6aca161 100644
--- a/app/models/condition_form.rb
+++ b/app/models/condition_form.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ConditionForm
include ActiveModel::Model
include Logic
diff --git a/app/models/contact_form.rb b/app/models/contact_form.rb
new file mode 100644
index 000000000..82b79feec
--- /dev/null
+++ b/app/models/contact_form.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+class ContactForm < ApplicationRecord
+ attr_reader :options
+
+ belongs_to :user, optional: true
+
+ after_initialize :set_options
+ before_validation :normalize_strings
+ before_validation :sanitize_email
+ before_save :add_default_tags
+
+ validates :email, presence: true, strict_email: true, if: :require_email?
+ validates :subject, presence: true
+ validates :text, presence: true
+ validates :question_type, presence: true
+
+ has_one_attached :piece_jointe
+
+ TYPE_INFO = 'procedure_info'
+ TYPE_PERDU = 'lost_user'
+ TYPE_INSTRUCTION = 'instruction_info'
+ TYPE_AMELIORATION = 'product'
+ TYPE_AUTRE = 'other'
+
+ ADMIN_TYPE_RDV = 'admin_demande_rdv'
+ ADMIN_TYPE_QUESTION = 'admin_question'
+ ADMIN_TYPE_SOUCIS = 'admin_soucis'
+ ADMIN_TYPE_PRODUIT = 'admin_suggestion_produit'
+ ADMIN_TYPE_DEMANDE_COMPTE = 'admin_demande_compte'
+ ADMIN_TYPE_AUTRE = 'admin_autre'
+
+ def self.default_options
+ [
+ [I18n.t(:question, scope: [:contact, :index, TYPE_INFO]), TYPE_INFO, I18n.t("links.common.faq.contacter_service_en_charge_url")],
+ [I18n.t(:question, scope: [:contact, :index, TYPE_PERDU]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL],
+ [I18n.t(:question, scope: [:contact, :index, TYPE_INSTRUCTION]), TYPE_INSTRUCTION, I18n.t("links.common.faq.ou_en_est_mon_dossier_url")],
+ [I18n.t(:question, scope: [:contact, :index, TYPE_AMELIORATION]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL],
+ [I18n.t(:question, scope: [:contact, :index, TYPE_AUTRE]), TYPE_AUTRE]
+ ]
+ end
+
+ def self.admin_options
+ [
+ [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_QUESTION], app_name: Current.application_name), ADMIN_TYPE_QUESTION],
+ [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_RDV], app_name: Current.application_name), ADMIN_TYPE_RDV],
+ [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_SOUCIS], app_name: Current.application_name), ADMIN_TYPE_SOUCIS],
+ [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_PRODUIT]), ADMIN_TYPE_PRODUIT],
+ [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_DEMANDE_COMPTE]), ADMIN_TYPE_DEMANDE_COMPTE],
+ [I18n.t(:question, scope: [:contact, :admin, ADMIN_TYPE_AUTRE]), ADMIN_TYPE_AUTRE]
+ ]
+ end
+
+ def for_admin=(value)
+ super(value)
+ set_options
+ end
+
+ def create_conversation_later
+ HelpscoutCreateConversationJob.perform_later(self)
+ end
+
+ def require_email? = user.blank?
+
+ private
+
+ def normalize_strings
+ self.subject = subject&.strip
+ self.text = text&.strip
+ end
+
+ def sanitize_email
+ self.email = EmailSanitizableConcern::EmailSanitizer.sanitize(email) if email.present?
+ end
+
+ def add_default_tags
+ self.tags = tags.push('contact form', question_type).uniq
+ end
+
+ def set_options
+ @options = for_admin? ? self.class.admin_options : self.class.default_options
+ end
+end
diff --git a/app/models/contact_information.rb b/app/models/contact_information.rb
index 803e06c87..047de49e8 100644
--- a/app/models/contact_information.rb
+++ b/app/models/contact_information.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ContactInformation < ApplicationRecord
include EmailSanitizableConcern
diff --git a/app/models/current.rb b/app/models/current.rb
index 4b18adc10..77045cfff 100644
--- a/app/models/current.rb
+++ b/app/models/current.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Current < ActiveSupport::CurrentAttributes
attribute :application_base_url
attribute :application_name
@@ -7,4 +9,5 @@ class Current < ActiveSupport::CurrentAttributes
attribute :no_reply_email
attribute :request_id
attribute :user
+ attribute :procedure_columns
end
diff --git a/app/models/current_confirmation.rb b/app/models/current_confirmation.rb
index ce9853ee2..0530845fe 100644
--- a/app/models/current_confirmation.rb
+++ b/app/models/current_confirmation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CurrentConfirmation < ActiveSupport::CurrentAttributes
attribute :procedure_after_confirmation
attribute :prefill_token
diff --git a/app/models/deleted_dossier.rb b/app/models/deleted_dossier.rb
index b8caead0e..e9bd22c68 100644
--- a/app/models/deleted_dossier.rb
+++ b/app/models/deleted_dossier.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DeletedDossier < ApplicationRecord
belongs_to :procedure, -> { with_discarded }, inverse_of: :deleted_dossiers, optional: false
belongs_to :groupe_instructeur, inverse_of: :deleted_dossiers, optional: true
diff --git a/app/models/demande.rb b/app/models/demande.rb
index 132282727..8d45ce19f 100644
--- a/app/models/demande.rb
+++ b/app/models/demande.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Demande
def self.model_name
self
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index d2aafd943..3d4b9ab9b 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Dossier < ApplicationRecord
- self.ignored_columns += [:re_instructed_at]
+ self.ignored_columns += [:search_terms, :private_search_terms]
include DossierCloneConcern
include DossierCorrectableConcern
@@ -9,6 +11,9 @@ class Dossier < ApplicationRecord
include DossierSearchableConcern
include DossierSectionsConcern
include DossierStateConcern
+ include DossierChampsConcern
+ include DossierEmptyConcern
+ include DossierExportConcern
enum state: {
brouillon: 'brouillon',
@@ -42,25 +47,16 @@ class Dossier < ApplicationRecord
has_one_attached :justificatif_motivation
- has_many :champs
- # We have to remove champs in a particular order - champs with a reference to a parent have to be
- # removed first, otherwise we get a foreign key constraint error.
- has_many :champs_to_destroy, -> { order(:parent_id) }, class_name: 'Champ', inverse_of: false, dependent: :destroy
- has_many :champs_public, -> { root.public_only }, class_name: 'Champ', inverse_of: false
- has_many :champs_private, -> { root.private_only }, class_name: 'Champ', inverse_of: false
- has_many :champs_public_all, -> { public_only }, class_name: 'Champ', inverse_of: false
- has_many :champs_private_all, -> { private_only }, class_name: 'Champ', inverse_of: false
- has_many :prefilled_champs_public, -> { root.public_only.prefilled }, class_name: 'Champ', inverse_of: false
-
+ has_many :champs, dependent: :destroy
has_many :commentaires, inverse_of: :dossier, dependent: :destroy
- has_many :preloaded_commentaires, -> { includes(:dossier_correction, piece_jointe_attachment: :blob) }, class_name: 'Commentaire', inverse_of: :dossier
+ has_many :preloaded_commentaires, -> { includes(:dossier_correction, piece_jointe_attachments: :blob) }, class_name: 'Commentaire', inverse_of: :dossier
has_many :invites, dependent: :destroy
has_many :follows, -> { active }, inverse_of: :dossier
has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier
has_many :followers_instructeurs, through: :follows, source: :instructeur
has_many :previous_followers_instructeurs, -> { distinct }, through: :previous_follows, source: :instructeur
- has_many :avis, inverse_of: :dossier, dependent: :destroy
+ has_many :avis, -> { order(:created_at) }, inverse_of: :dossier, dependent: :destroy
has_many :experts, through: :avis
has_many :traitements, -> { order(:processed_at) }, inverse_of: :dossier, dependent: :destroy do
def passer_en_construction(instructeur: nil, processed_at: Time.zone.now)
@@ -138,14 +134,12 @@ class Dossier < ApplicationRecord
belongs_to :transfer, class_name: 'DossierTransfer', foreign_key: 'dossier_transfer_id', optional: true, inverse_of: :dossiers
has_many :transfer_logs, class_name: 'DossierTransferLog', dependent: :destroy
+ has_many :dossier_labels, dependent: :destroy
+ has_many :labels, through: :dossier_labels
after_destroy_commit :log_destroy
accepts_nested_attributes_for :champs
- accepts_nested_attributes_for :champs_public
- accepts_nested_attributes_for :champs_private
- accepts_nested_attributes_for :champs_public_all
- accepts_nested_attributes_for :champs_private_all
accepts_nested_attributes_for :individual
include AASM
@@ -159,7 +153,7 @@ class Dossier < ApplicationRecord
state :sans_suite
event :passer_en_construction, after: :after_passer_en_construction, after_commit: :after_commit_passer_en_construction do
- transitions from: :brouillon, to: :en_construction
+ transitions from: :brouillon, to: :en_construction, guard: :can_passer_en_construction?
end
event :passer_en_instruction, after: :after_passer_en_instruction, after_commit: :after_commit_passer_en_instruction do
@@ -224,10 +218,12 @@ class Dossier < ApplicationRecord
scope :prefilled, -> { where(prefilled: true) }
scope :hidden_by_user, -> { where.not(hidden_by_user_at: nil) }
scope :hidden_by_administration, -> { where.not(hidden_by_administration_at: nil) }
- scope :visible_by_user, -> { where(for_procedure_preview: false).where(hidden_by_user_at: nil, editing_fork_origin_id: nil) }
+ scope :hidden_by_expired, -> { where.not(hidden_by_expired_at: nil) }
+ scope :visible_by_user, -> { where(for_procedure_preview: false, hidden_by_user_at: nil, editing_fork_origin_id: nil, hidden_by_expired_at: nil) }
scope :visible_by_administration, -> {
state_not_brouillon
.where(hidden_by_administration_at: nil)
+ .where(hidden_by_expired_at: nil)
.merge(visible_by_user.or(state_not_en_construction))
}
scope :visible_by_user_or_administration, -> { visible_by_user.or(visible_by_administration) }
@@ -245,10 +241,7 @@ class Dossier < ApplicationRecord
scope :hidden_by_administration_since, -> (since) { where('dossiers.hidden_by_administration_at IS NOT NULL AND dossiers.hidden_by_administration_at >= ?', since) }
scope :hidden_since, -> (since) { hidden_by_user_since(since).or(hidden_by_administration_since(since)) }
- scope :with_type_de_champ, -> (stable_id) {
- joins('INNER JOIN champs ON champs.dossier_id = dossiers.id INNER JOIN types_de_champ ON types_de_champ.id = champs.type_de_champ_id')
- .where(types_de_champ: { stable_id: })
- }
+ scope :with_type_de_champ, -> (stable_id) { joins(:champs).where(champs: { stream: 'main', stable_id: }) }
scope :all_state, -> { not_archived.state_not_brouillon }
scope :en_construction, -> { not_archived.state_en_construction }
@@ -272,35 +265,16 @@ class Dossier < ApplicationRecord
scope :en_cours, -> { not_archived.state_en_construction_ou_instruction }
scope :without_followers, -> { where.missing(:follows) }
scope :with_followers, -> { left_outer_joins(:follows).where.not(follows: { id: nil }) }
- scope :with_champs, -> {
- includes(champs_public: [
- :type_de_champ,
- :geo_areas,
- piece_justificative_file_attachments: :blob,
- champs: [:type_de_champ, piece_justificative_file_attachments: :blob]
- ])
- }
-
scope :brouillons_recently_updated, -> { updated_since(2.days.ago).state_brouillon.order_by_updated_at }
- scope :with_annotations, -> {
- includes(champs_private: [
- :type_de_champ,
- :geo_areas,
- piece_justificative_file_attachments: :blob,
- champs: [:type_de_champ, piece_justificative_file_attachments: :blob]
- ])
- }
scope :for_api, -> {
- with_champs
- .with_annotations
- .includes(commentaires: { piece_jointe_attachment: :blob },
- justificatif_motivation_attachment: :blob,
- attestation: [],
- avis: { piece_justificative_file_attachment: :blob },
- traitement: [],
- etablissement: [],
- individual: [],
- user: [])
+ includes(commentaires: { piece_jointe_attachments: :blob },
+ justificatif_motivation_attachment: :blob,
+ attestation: [],
+ avis: { piece_justificative_file_attachment: :blob },
+ traitement: [],
+ etablissement: [],
+ individual: [],
+ user: [])
}
scope :with_notifiable_procedure, -> (opts = { notify_on_closed: false }) do
@@ -371,12 +345,12 @@ class Dossier < ApplicationRecord
scope :without_brouillon_expiration_notice_sent, -> { where(brouillon_close_to_expiration_notice_sent_at: nil) }
scope :without_en_construction_expiration_notice_sent, -> { where(en_construction_close_to_expiration_notice_sent_at: nil) }
scope :without_termine_expiration_notice_sent, -> { where(termine_close_to_expiration_notice_sent_at: nil) }
-
scope :deleted_by_user_expired, -> { where('dossiers.hidden_by_user_at < ?', 1.week.ago) }
scope :deleted_by_administration_expired, -> { where('dossiers.hidden_by_administration_at < ?', 1.week.ago) }
- scope :en_brouillon_expired_to_delete, -> { state_brouillon.deleted_by_user_expired }
- scope :en_construction_expired_to_delete, -> { state_en_construction.deleted_by_user_expired }
- scope :termine_expired_to_delete, -> { state_termine.deleted_by_user_expired.deleted_by_administration_expired }
+ scope :deleted_by_automatic_expired, -> { where('dossiers.hidden_by_expired_at < ?', 1.week.ago) }
+ scope :en_brouillon_expired_to_delete, -> { state_brouillon.deleted_by_user_expired.or(state_brouillon.deleted_by_automatic_expired) }
+ scope :en_construction_expired_to_delete, -> { state_en_construction.deleted_by_user_expired.or(state_en_construction.deleted_by_automatic_expired) }
+ scope :termine_expired_to_delete, -> { state_termine.deleted_by_user_expired.deleted_by_administration_expired.or(state_termine.deleted_by_automatic_expired) }
scope :brouillon_near_procedure_closing_date, -> do
# select users who have submitted dossier for the given 'procedures.id'
@@ -394,7 +368,7 @@ class Dossier < ApplicationRecord
.where.not(user: users_who_submitted)
end
- scope :for_api_v2, -> { includes(:attestation_template, revision: [procedure: [:administrateurs]], etablissement: [], individual: [], traitement: []) }
+ scope :for_api_v2, -> { includes(:attestation_template, revision: [procedure: [:administrateurs]], etablissement: [], individual: [], traitement: [], procedure: [], user: [:france_connect_informations]) }
scope :with_notifications, -> do
joins(:follows)
@@ -403,7 +377,10 @@ class Dossier < ApplicationRecord
' OR groupe_instructeur_updated_at > follows.demande_seen_at' \
' OR last_champ_private_updated_at > follows.annotations_privees_seen_at' \
' OR last_avis_updated_at > follows.avis_seen_at' \
- ' OR last_commentaire_updated_at > follows.messagerie_seen_at')
+ ' OR last_commentaire_updated_at > follows.messagerie_seen_at' \
+ ' OR last_commentaire_piece_jointe_updated_at > follows.pieces_jointes_seen_at' \
+ ' OR last_champ_piece_jointe_updated_at > follows.pieces_jointes_seen_at' \
+ ' OR last_avis_piece_jointe_updated_at > follows.pieces_jointes_seen_at')
.distinct
end
@@ -422,8 +399,8 @@ class Dossier < ApplicationRecord
visible_by_administration.termine
when 'tous'
visible_by_administration.all_state
- when 'supprimes_recemment'
- hidden_by_administration.termine
+ when 'supprimes'
+ hidden_by_administration.state_termine.or(hidden_by_expired)
when 'archives'
visible_by_administration.archived
when 'expirant'
@@ -435,7 +412,6 @@ class Dossier < ApplicationRecord
delegate :siret, :siren, to: :etablissement, allow_nil: true
delegate :france_connected_with_one_identity?, to: :user, allow_nil: true
- before_save :build_default_champs_for_new_dossier, if: Proc.new { revision_id_was.nil? && parent_dossier_id.nil? && editing_fork_origin_id.nil? }
after_save :send_web_hook
@@ -445,8 +421,6 @@ class Dossier < ApplicationRecord
validates :mandataire_last_name, presence: true, if: :for_tiers?
validates :for_tiers, inclusion: { in: [true, false] }, if: -> { revision&.procedure&.for_individual? }
- validates_associated :prefilled_champs_public, on: :champs_public_value
-
def types_de_champ_public
types_de_champ
end
@@ -481,6 +455,10 @@ class Dossier < ApplicationRecord
end
end
+ def user_email_for_display
+ user_email_for(:display)
+ end
+
def expiration_started?
[
brouillon_close_to_expiration_notice_sent_at,
@@ -495,29 +473,9 @@ class Dossier < ApplicationRecord
end
end
- def build_default_champs_for_new_dossier
- revision.build_champs_public.each do |champ|
- champs_public << champ
- end
- revision.build_champs_private.each do |champ|
- champs_private << champ
- end
- champs_public.filter { _1.repetition? && _1.mandatory? }.each do |champ|
- champ.add_row(revision)
- end
- champs_private.filter(&:repetition?).each do |champ|
- champ.add_row(revision)
- end
- end
-
- def build_default_individual
- if procedure.for_individual? && individual.blank?
- self.individual = if france_connected_with_one_identity?
- Individual.from_france_connect(user.france_connect_informations.first)
- else
- Individual.new
- end
- end
+ def build_default_values
+ build_default_individual
+ build_default_champs
end
def en_construction_ou_instruction?
@@ -561,8 +519,18 @@ class Dossier < ApplicationRecord
false
end
+ def blocked_with_pending_correction?
+ procedure.feature_enabled?(:blocking_pending_correction) && pending_correction?
+ end
+
+ def can_passer_en_construction?
+ return true if !revision.ineligibilite_enabled || !revision.ineligibilite_rules
+
+ !revision.ineligibilite_rules.compute(filled_champs_public)
+ end
+
def can_passer_en_instruction?
- return false if procedure.feature_enabled?(:blocking_pending_correction) && pending_correction?
+ return false if blocked_with_pending_correction?
true
end
@@ -598,13 +566,17 @@ class Dossier < ApplicationRecord
termine? || reason == :procedure_removed
end
+ def can_be_deleted_by_automatic?(reason)
+ reason == :expired && !en_instruction?
+ end
+
def can_terminer_automatiquement_by_sva_svr?
sva_svr_decision_triggered_at.nil? && !pending_correction? && (sva_svr_decision_on.today? || sva_svr_decision_on.past?)
end
def any_etablissement_as_degraded_mode?
return true if etablissement&.as_degraded_mode?
- return true if champs_for_revision(scope: :public).any? { _1.etablissement&.as_degraded_mode? }
+ return true if filled_champs_public.any? { _1.etablissement&.as_degraded_mode? }
false
end
@@ -643,7 +615,12 @@ class Dossier < ApplicationRecord
def close_to_expiration?
return false if en_instruction?
- expiration_notification_date < Time.zone.now
+ expiration_notification_date < Time.zone.now && Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks.ago < expiration_notification_date
+ end
+
+ def has_expired?
+ return false if en_instruction?
+ expiration_notification_date < Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks.ago
end
def after_notification_expiration_date
@@ -675,6 +652,12 @@ class Dossier < ApplicationRecord
termine_close_to_expiration_notice_sent_at: nil)
end
+ def extend_conservation_and_restore(conservation_extension, author)
+ extend_conservation(conservation_extension)
+ update(hidden_by_expired_at: nil, hidden_by_reason: nil)
+ restore(author)
+ end
+
def show_procedure_state_warning?
procedure.discarded? || (brouillon? && !procedure.dossier_can_transition_to_en_construction?)
end
@@ -764,6 +747,10 @@ class Dossier < ApplicationRecord
!procedure.brouillon? && !brouillon?
end
+ def hidden_by_expired?
+ hidden_by_expired_at.present?
+ end
+
def hidden_by_user?
hidden_by_user_at.present?
end
@@ -818,37 +805,32 @@ class Dossier < ApplicationRecord
end
end
- def expired_keep_track_and_destroy!
- transaction do
- DeletedDossier.create_from_dossier(self, :expired)
- log_automatic_dossier_operation(:supprimer, self)
- dossier_operation_logs.purge_discarded
- destroy!
- end
- true
- rescue
- false
- end
-
- def author_is_user(author)
+ def is_user?(author)
author.is_a?(User)
end
- def author_is_administration(author)
+ def is_administration?(author)
author.is_a?(Instructeur) || author.is_a?(Administrateur) || author.is_a?(SuperAdmin)
end
+ def is_automatic?(author)
+ author == :automatic
+ end
+
def hide_and_keep_track!(author, reason)
transaction do
- if author_is_administration(author) && can_be_deleted_by_administration?(reason)
+ if is_administration?(author) && can_be_deleted_by_administration?(reason)
update(hidden_by_administration_at: Time.zone.now, hidden_by_reason: reason)
- elsif author_is_user(author) && can_be_deleted_by_user?
+ log_dossier_operation(author, :supprimer, self)
+ elsif is_user?(author) && can_be_deleted_by_user?
update(hidden_by_user_at: Time.zone.now, dossier_transfer_id: nil, hidden_by_reason: reason)
+ log_dossier_operation(author, :supprimer, self)
+ elsif is_automatic?(author) && can_be_deleted_by_automatic?(reason)
+ update(hidden_by_expired_at: Time.zone.now, hidden_by_reason: reason)
+ log_automatic_dossier_operation(:supprimer, self)
else
raise "Unauthorized dossier hide attempt Dossier##{id} by #{author} for reason #{reason}"
end
-
- log_dossier_operation(author, :supprimer, self)
end
if en_construction? && !hidden_by_administration?
@@ -861,14 +843,18 @@ class Dossier < ApplicationRecord
def restore(author)
transaction do
- if author_is_administration(author)
+ if is_administration?(author)
update(hidden_by_administration_at: nil)
- elsif author_is_user(author)
+ elsif is_user?(author)
update(hidden_by_user_at: nil)
end
if !hidden_by_user? && !hidden_by_administration?
update(hidden_by_reason: nil)
+ elsif hidden_by_user?
+ update(hidden_by_reason: :user_request)
+ elsif hidden_by_administration?
+ update(hidden_by_reason: :instructeur_request)
end
log_dossier_operation(author, :restaurer, self)
@@ -887,6 +873,7 @@ class Dossier < ApplicationRecord
resolve_pending_correction!
process_sva_svr!
+ remove_piece_justificative_file_not_visible!
end
def process_declarative!
@@ -924,132 +911,42 @@ class Dossier < ApplicationRecord
end
def remove_titres_identite!
- champs_public.filter(&:titre_identite?).map(&:piece_justificative_file).each(&:purge_later)
+ champs.filter(&:titre_identite?).map(&:piece_justificative_file).each(&:purge_later)
+ end
+
+ def remove_piece_justificative_file_not_visible!
+ champs.each do |champ|
+ next unless champ.piece_justificative_file.attached?
+ next if champ.visible?
+
+ champ.piece_justificative_file.purge_later
+ end
end
def check_mandatory_and_visible_champs
- champs_for_revision(scope: :public)
- .filter { _1.child? ? _1.parent.visible? : true }
- .filter(&:visible?)
- .filter(&:mandatory_blank?)
- .map do |champ|
- champ.errors.add(:value, :missing)
+ project_champs_public.filter(&:visible?).each do |champ|
+ if champ.mandatory_blank?
+ error = champ.errors.add(:value, :missing)
+ errors.import(error)
end
+ if champ.repetition?
+ champ.rows.each do |champs|
+ champs.filter(&:visible?).filter(&:mandatory_blank?).each do |champ|
+ error = champ.errors.add(:value, :missing)
+ errors.import(error)
+ end
+ end
+ end
+ end
+ errors
end
def demander_un_avis!(avis)
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end
- def spreadsheet_columns_csv(types_de_champ:)
- spreadsheet_columns(with_etablissement: true, types_de_champ: types_de_champ)
- end
-
- def spreadsheet_columns_xlsx(types_de_champ:)
- spreadsheet_columns(types_de_champ: types_de_champ)
- end
-
- def spreadsheet_columns_ods(types_de_champ:)
- spreadsheet_columns(types_de_champ: types_de_champ)
- end
-
- def spreadsheet_columns(with_etablissement: false, types_de_champ:)
- columns = [
- ['ID', id.to_s],
- ['Email', user_email_for(:display)],
- ['FranceConnect ?', user_from_france_connect?]
- ]
-
- if procedure.for_individual?
- columns += [
- ['Civilité', individual&.gender],
- ['Nom', individual&.nom],
- ['Prénom', individual&.prenom],
- ['Dépôt pour un tiers', :for_tiers],
- ['Nom du mandataire', :mandataire_last_name],
- ['Prénom du mandataire', :mandataire_first_name]
- ]
- if procedure.ask_birthday
- columns += [['Date de naissance', individual&.birthdate]]
- end
- 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 état administratif', etablissement&.entreprise_etat_administratif],
- ['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
-
- if procedure.chorusable? && procedure.chorus_configuration.complete?
- columns += [
- ['Domaine Fonctionnel', procedure.chorus_configuration.domaine_fonctionnel&.fetch("code") { '' }],
- ['Référentiel De Programmation', procedure.chorus_configuration.referentiel_de_programmation&.fetch("code") { '' }],
- ['Centre De Coût', procedure.chorus_configuration.centre_de_cout&.fetch("code") { '' }]
- ]
- end
- columns += [
- ['Archivé', :archived],
- ['État du dossier', Dossier.human_attribute_name("state.#{state}")],
- ['Dernière mise à jour le', :updated_at],
- ['Dernière mise à jour du dossier le', :last_champ_updated_at],
- ['Déposé le', :depose_at],
- ['Passé en instruction le', :en_instruction_at],
- procedure.sva_svr_enabled? ? ["Date décision #{procedure.sva_svr_configuration.human_decision}", :sva_svr_decision_on] : nil,
- ['Traité le', :processed_at],
- ['Motivation de la décision', :motivation],
- ['Instructeurs', followers_instructeurs.map(&:email).join(' ')]
- ].compact
-
- if procedure.routing_enabled?
- columns << ['Groupe instructeur', groupe_instructeur.label]
- end
- columns + champs_for_export(types_de_champ)
- end
-
- # Get all the champs values for the types de champ in the final list.
- # Dossier might not have corresponding champ – display nil.
- # To do so, we build a virtual champ when there is no value so we can call for_export with all indexes
- def champs_for_export(types_de_champ, row_id = nil)
- types_de_champ.flat_map do |type_de_champ|
- champ = champ_for_export(type_de_champ, row_id)
- type_de_champ.libelles_for_export.map do |(libelle, path)|
- [libelle, champ&.for_export(path)]
- end
- end
- end
-
def linked_dossiers_for(instructeur_or_expert)
- dossier_ids = champs_for_revision.filter(&:dossier_link?).filter_map(&:value)
+ dossier_ids = filled_champs.filter(&:dossier_link?).filter_map(&:value)
instructeur_or_expert.dossiers.where(id: dossier_ids)
end
@@ -1058,7 +955,7 @@ class Dossier < ApplicationRecord
end
def geo_data?
- GeoArea.exists?(champ_id: champs_for_revision)
+ GeoArea.exists?(champ_id: filled_champs)
end
def to_feature_collection
@@ -1070,13 +967,6 @@ class Dossier < ApplicationRecord
}
end
- def self.to_feature_collection
- {
- type: 'FeatureCollection',
- features: GeoArea.joins(:champ).where(champ: { dossier: ids }).map(&:to_feature)
- }
- end
-
def log_api_entreprise_job_exception(exception)
exceptions = self.api_entreprise_job_exceptions ||= []
exceptions << exception.inspect
@@ -1141,43 +1031,8 @@ class Dossier < ApplicationRecord
user.france_connected_with_one_identity?
end
- def champs_for_revision(scope: nil, root: false)
- champs_index = champs.group_by(&:stable_id)
- # Due to some bad data we can have multiple copies of the same champ. Ignore extra copy.
- .transform_values { _1.sort_by(&:id).uniq(&:row_id) }
-
- if scope.is_a?(TypeDeChamp)
- revision
- .children_of(scope)
- .flat_map { champs_index[_1.stable_id] || [] }
- .filter(&:child?) # TODO: remove once bad data (child champ without a row id) is cleaned
- else
- revision
- .types_de_champ_for(scope:, root:)
- .flat_map { champs_index[_1.stable_id] || [] }
- end
- end
-
def has_annotations?
- revision.revision_types_de_champ_private.present?
- end
-
- def project_champ(type_de_champ, row_id)
- champ = champs_by_public_id[type_de_champ.public_id(row_id)]
- if champ.nil?
- type_de_champ.build_champ(dossier: self, row_id:)
- else
- champ
- end
- end
-
- def champ_for_export(type_de_champ, row_id)
- champ = champs_by_public_id[type_de_champ.public_id(row_id)]
- if champ.blank? || !champ.visible?
- nil
- else
- champ
- end
+ revision.types_de_champ_private.present?
end
def hide_info_with_accuse_lecture?
@@ -1188,10 +1043,45 @@ class Dossier < ApplicationRecord
procedure.accuse_lecture? && termine?
end
+ def track_can_passer_en_construction
+ if !revision.ineligibilite_enabled
+ yield
+ [true, true] # without eligibilite rules, we never reach dossier.champs.visible?, don't cache anything
+ else
+ from = can_passer_en_construction? # with eligibilite rules, self.champ[x].visible is cached by passing thru conditions checks
+ yield
+ champs.map(&:reset_visible) # we must reset self.champs[x].visible?, because an update occurred and we should re-evaluate champs[x] visibility
+ [from, can_passer_en_construction?]
+ end
+ end
+
private
- def champs_by_public_id
- @champs_by_public_id ||= champs.sort_by(&:id).index_by(&:public_id)
+ def build_default_champs
+ build_default_champs_for(revision.types_de_champ_public) if !champs.any?(&:public?)
+ build_default_champs_for(revision.types_de_champ_private) if !champs.any?(&:private?)
+ end
+
+ def build_default_champs_for(types_de_champ)
+ self.champs << types_de_champ.flat_map do |type_de_champ|
+ champ = type_de_champ.build_champ(dossier: self)
+ if type_de_champ.repetition? && (type_de_champ.private? || type_de_champ.mandatory?)
+ row_id = ULID.generate
+ [champ] + revision.children_of(type_de_champ).map { _1.build_champ(dossier: self, row_id:) }
+ else
+ champ
+ end
+ end
+ end
+
+ def build_default_individual
+ if procedure.for_individual? && individual.blank?
+ self.individual = if france_connected_with_one_identity?
+ Individual.from_france_connect(user.france_connect_informations.first)
+ else
+ Individual.new
+ end
+ end
end
def create_missing_traitemets
@@ -1213,7 +1103,7 @@ class Dossier < ApplicationRecord
end
def geo_areas
- champs_for_revision.flat_map(&:geo_areas)
+ filled_champs.flat_map(&:geo_areas)
end
def bounding_box
diff --git a/app/models/dossier_assignment.rb b/app/models/dossier_assignment.rb
index 239a218d4..e08c8ea25 100644
--- a/app/models/dossier_assignment.rb
+++ b/app/models/dossier_assignment.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossierAssignment < ApplicationRecord
belongs_to :dossier
diff --git a/app/models/dossier_batch_operation.rb b/app/models/dossier_batch_operation.rb
index ca5582d22..a807be83f 100644
--- a/app/models/dossier_batch_operation.rb
+++ b/app/models/dossier_batch_operation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossierBatchOperation < ApplicationRecord
belongs_to :dossier
belongs_to :batch_operation
diff --git a/app/models/dossier_correction.rb b/app/models/dossier_correction.rb
index 2f442a385..6347832d7 100644
--- a/app/models/dossier_correction.rb
+++ b/app/models/dossier_correction.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossierCorrection < ApplicationRecord
belongs_to :dossier
belongs_to :commentaire
diff --git a/app/models/dossier_label.rb b/app/models/dossier_label.rb
new file mode 100644
index 000000000..6e9cc96f5
--- /dev/null
+++ b/app/models/dossier_label.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class DossierLabel < ApplicationRecord
+ belongs_to :dossier
+ belongs_to :label
+end
diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb
index 902626a85..5deba8cc4 100644
--- a/app/models/dossier_operation_log.rb
+++ b/app/models/dossier_operation_log.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossierOperationLog < ApplicationRecord
enum operation: {
changer_groupe_instructeur: 'changer_groupe_instructeur',
@@ -41,7 +43,9 @@ class DossierOperationLog < ApplicationRecord
def self.purge_discarded
not_deletion.destroy_all
- with_data.each(&:move_to_cold_storage!)
+
+ supprimer.map { _1.serialized.purge_later }
+ supprimer.update_all(data: nil)
end
def self.create_and_serialize(params)
diff --git a/app/models/dossier_preloader.rb b/app/models/dossier_preloader.rb
index 2f541ad93..920ef00bf 100644
--- a/app/models/dossier_preloader.rb
+++ b/app/models/dossier_preloader.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
class DossierPreloader
DEFAULT_BATCH_SIZE = 2000
- def initialize(dossiers, includes_for_dossier: [], includes_for_etablissement: [])
+ def initialize(dossiers, includes_for_champ: [], includes_for_etablissement: [])
@dossiers = dossiers
@includes_for_etablissement = includes_for_etablissement
- @includes_for_dossier = includes_for_dossier
+ @includes_for_champ = includes_for_champ
end
def in_batches(size = DEFAULT_BATCH_SIZE)
@@ -13,6 +15,16 @@ class DossierPreloader
dossiers
end
+ def in_batches_with_block(size = DEFAULT_BATCH_SIZE, &block)
+ @dossiers.in_batches(of: size) do |batch|
+ data = Dossier.where(id: batch.ids).includes(:individual, :traitement, :etablissement, user: :france_connect_informations, avis: :expert, commentaires: [:instructeur, :expert])
+
+ dossiers = data.to_a
+ load_dossiers(dossiers)
+ yield(dossiers)
+ end
+ end
+
def all(pj_template: false)
dossiers = @dossiers.to_a
load_dossiers(dossiers, pj_template:)
@@ -20,55 +32,39 @@ class DossierPreloader
end
def self.load_one(dossier, pj_template: false)
- DossierPreloader.new([dossier]).all(pj_template: pj_template).first
+ DossierPreloader.new([dossier]).all(pj_template:).first
end
private
- # returns: { revision_id : { type_de_champ_id : position } }
- def positions
- @positions ||= ProcedureRevisionTypeDeChamp
- .where(revision_id: @dossiers.pluck(:revision_id).uniq)
- .select(:revision_id, :type_de_champ_id, :position)
- .group_by(&:revision_id)
- .transform_values do |coordinates|
- coordinates.index_by(&:type_de_champ_id).transform_values(&:position)
- end
+ def revisions(pj_template: false)
+ @revisions ||= ProcedureRevision.where(id: @dossiers.pluck(:revision_id).uniq)
+ .includes(procedure: [], revision_types_de_champ: { parent: :type_de_champ }, types_de_champ_public: [], types_de_champ_private: [], types_de_champ: pj_template ? { piece_justificative_template_attachment: :blob } : [])
+ .index_by(&:id)
end
def load_dossiers(dossiers, pj_template: false)
- to_include = @includes_for_dossier.dup
+ to_include = @includes_for_champ.dup
to_include << [piece_justificative_file_attachments: :blob]
- if pj_template
- to_include << { type_de_champ: { piece_justificative_template_attachment: :blob } }
- else
- to_include << :type_de_champ
- end
-
all_champs = Champ
.includes(to_include)
.where(dossier_id: dossiers)
.to_a
- load_etablissements(all_champs)
-
- children_champs, root_champs = all_champs.partition(&:child?)
- champs_by_dossier = root_champs.group_by(&:dossier_id)
- champs_by_dossier_by_parent = children_champs
- .group_by(&:dossier_id)
- .transform_values do |champs|
- champs.group_by(&:parent_id)
- end
+ champs_by_dossier = all_champs.group_by(&:dossier_id)
dossiers.each do |dossier|
- load_dossier(dossier, champs_by_dossier[dossier.id] || [], champs_by_dossier_by_parent[dossier.id] || {})
+ load_dossier(dossier, champs_by_dossier[dossier.id] || [], pj_template:)
end
+
+ load_etablissements(all_champs)
end
def load_etablissements(champs)
to_include = @includes_for_etablissement.dup
- champs_siret = champs.filter(&:siret?)
+ # `champs.siret?` will delegate to type_de_champ; this is not what we want here
+ champs_siret = champs.filter { _1.type == 'Champs::SiretChamp' }
etablissements_by_id = Etablissement.includes(to_include).where(id: champs_siret.map(&:etablissement_id).compact).index_by(&:id)
champs_siret.each do |champ|
etablissement = etablissements_by_id[champ.etablissement_id]
@@ -79,14 +75,16 @@ class DossierPreloader
end
end
- def load_dossier(dossier, champs, children_by_parent = {})
- champs_public, champs_private = champs.partition(&:public?)
+ def load_dossier(dossier, champs, pj_template: false)
+ revision = revisions(pj_template:)[dossier.revision_id]
+ if revision.present?
+ dossier.association(:revision).target = revision
+ end
+ dossier.association(:champs).target = champs
- dossier.association(:champs).target = []
- dossier.association(:champs_public_all).target = []
- dossier.association(:champs_private_all).target = []
- load_champs(dossier, :champs_public, champs_public, dossier, children_by_parent)
- load_champs(dossier, :champs_private, champs_private, dossier, children_by_parent)
+ champs.each do |champ|
+ champ.association(:dossier).target = dossier
+ end
# We need to do this because of the check on `Etablissement#champ` in
# `Etablissement#libelle_for_export`. By assigning `nil` to `target` we mark association
@@ -94,40 +92,7 @@ class DossierPreloader
if dossier.etablissement
dossier.etablissement.association(:champ).target = nil
end
- end
- def load_champs(parent, name, champs, dossier, children_by_parent)
- if champs.empty?
- parent.association(name).target = [] # tells to Rails association has been loaded
- return
- end
-
- champs.each do |champ|
- champ.association(:dossier).target = dossier
-
- if parent.is_a?(Champ)
- champ.association(:parent).target = parent
- end
- end
-
- dossier.association(:champs).target += champs
-
- if champs.first.public?
- dossier.association(:champs_public_all).target += champs
- else
- dossier.association(:champs_private_all).target += champs
- end
-
- parent.association(name).target = champs
- .filter { positions[dossier.revision_id][_1.type_de_champ_id].present? }
- .sort_by { [_1.row_id, positions[dossier.revision_id][_1.type_de_champ_id]] }
-
- # Load children champs
- champs.filter(&:block?).each do |parent_champ|
- champs = children_by_parent[parent_champ.id] || []
- parent_champ.association(:dossier).target = dossier
-
- load_champs(parent_champ, :champs, champs, dossier, children_by_parent)
- end
+ dossier.send(:reset_champs_cache)
end
end
diff --git a/app/models/dossier_submitted_message.rb b/app/models/dossier_submitted_message.rb
index c87e7c10b..358e7df1d 100644
--- a/app/models/dossier_submitted_message.rb
+++ b/app/models/dossier_submitted_message.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossierSubmittedMessage < ApplicationRecord
has_many :revisions, class_name: 'ProcedureRevision', inverse_of: :dossier_submitted_message, dependent: :nullify
end
diff --git a/app/models/dossier_transfer.rb b/app/models/dossier_transfer.rb
index 0783247fa..31e0448ca 100644
--- a/app/models/dossier_transfer.rb
+++ b/app/models/dossier_transfer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossierTransfer < ApplicationRecord
include EmailSanitizableConcern
has_many :dossiers, dependent: :nullify
@@ -28,7 +30,7 @@ class DossierTransfer < ApplicationRecord
DossierTransferLog.create(transfer.dossiers.map do |dossier|
{
dossier: dossier,
- from: dossier.user.email,
+ from: dossier.user_email_for(:notification),
from_support: transfer.from_support,
to: transfer.email
}
diff --git a/app/models/dossier_transfer_log.rb b/app/models/dossier_transfer_log.rb
index 574a7bfb0..53eb88b57 100644
--- a/app/models/dossier_transfer_log.rb
+++ b/app/models/dossier_transfer_log.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossierTransferLog < ApplicationRecord
belongs_to :dossier
end
diff --git a/app/models/dossiers_filter.rb b/app/models/dossiers_filter.rb
index c542c3cac..68e732a99 100644
--- a/app/models/dossiers_filter.rb
+++ b/app/models/dossiers_filter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossiersFilter
attr_reader :user, :params
diff --git a/app/models/dubious_procedure.rb b/app/models/dubious_procedure.rb
index 016456c29..792b1526d 100644
--- a/app/models/dubious_procedure.rb
+++ b/app/models/dubious_procedure.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DubiousProcedure
extend ActiveModel::Naming
extend ActiveModel::Translation
@@ -17,11 +19,11 @@ class DubiousProcedure
end
def self.all
- procedures_with_forbidden_tdcs_sql = TypeDeChamp
- .joins(:procedure)
+ procedures_with_forbidden_tdcs_sql = ProcedureRevisionTypeDeChamp
+ .joins(:procedure, :type_de_champ)
.select("string_agg(types_de_champ.libelle, ' - ') as dubious_champs, procedures.id as procedure_id, procedures.libelle as procedure_libelle, procedures.aasm_state as procedure_aasm_state, procedures.hidden_at_as_template as procedure_hidden_at_as_template")
.where("unaccent(types_de_champ.libelle) ~* unaccent(?)", forbidden_regexp)
- .where(type_champ: [TypeDeChamp.type_champs.fetch(:text), TypeDeChamp.type_champs.fetch(:textarea)])
+ .where(types_de_champ: { type_champ: [TypeDeChamp.type_champs.fetch(:text), TypeDeChamp.type_champs.fetch(:textarea)] })
.where(procedures: { closed_at: nil, whitelisted_at: nil })
.group("procedures.id")
.order("procedures.id asc")
diff --git a/app/models/email_event.rb b/app/models/email_event.rb
index 4831adbd6..a9a161f0f 100644
--- a/app/models/email_event.rb
+++ b/app/models/email_event.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmailEvent < ApplicationRecord
RETENTION_DURATION = 1.month
diff --git a/app/models/entreprise.rb b/app/models/entreprise.rb
index c1f4a077d..a017301e2 100644
--- a/app/models/entreprise.rb
+++ b/app/models/entreprise.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Entreprise < Hashie::Dash
def read_attribute_for_serialization(attribute)
self[attribute]
diff --git a/app/models/etablissement.rb b/app/models/etablissement.rb
index 811a0c0a9..ba5232598 100644
--- a/app/models/etablissement.rb
+++ b/app/models/etablissement.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Etablissement < ApplicationRecord
belongs_to :dossier, optional: true
@@ -17,6 +19,8 @@ class Etablissement < ApplicationRecord
fermé: "fermé"
}, _prefix: true
+ after_commit -> { dossier&.index_search_terms_later }
+
def entreprise_raison_sociale
read_attribute(:entreprise_raison_sociale).presence || raison_sociale_for_ei
end
@@ -48,7 +52,8 @@ class Etablissement < ApplicationRecord
adresse,
code_postal,
localite,
- code_insee_localite
+ code_insee_localite,
+ nom_pays
]
end
diff --git a/app/models/exercice.rb b/app/models/exercice.rb
index 12169a663..b03ef9599 100644
--- a/app/models/exercice.rb
+++ b/app/models/exercice.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Exercice < ApplicationRecord
belongs_to :etablissement, optional: false
diff --git a/app/models/expert.rb b/app/models/expert.rb
index 53a08bd90..c15ada1f6 100644
--- a/app/models/expert.rb
+++ b/app/models/expert.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Expert < ApplicationRecord
belongs_to :user
has_many :experts_procedures
diff --git a/app/models/experts_procedure.rb b/app/models/experts_procedure.rb
index 41c58594f..1a6813bde 100644
--- a/app/models/experts_procedure.rb
+++ b/app/models/experts_procedure.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExpertsProcedure < ApplicationRecord
belongs_to :expert
belongs_to :procedure
diff --git a/app/models/export.rb b/app/models/export.rb
index 3a7a1ac34..7cce92ca9 100644
--- a/app/models/export.rb
+++ b/app/models/export.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
class Export < ApplicationRecord
include TransientModelsWithPurgeableJobConcern
+ self.ignored_columns += ["procedure_presentation_snapshot"]
+
MAX_DUREE_CONSERVATION_EXPORT = 32.hours
MAX_DUREE_GENERATION = 16.hours
@@ -22,7 +26,7 @@ class Export < ApplicationRecord
suivis: 'suivis',
traites: 'traites',
tous: 'tous',
- supprimes_recemment: 'supprimes_recemment',
+ supprimes: 'supprimes',
archives: 'archives',
expirant: 'expirant'
}
@@ -31,9 +35,13 @@ class Export < ApplicationRecord
belongs_to :procedure_presentation, optional: true
belongs_to :instructeur, optional: true
belongs_to :user_profile, polymorphic: true, optional: true
+ belongs_to :export_template, optional: true
has_one_attached :file
+ attribute :sorted_column, :sorted_column
+ attribute :filtered_columns, :filtered_column, array: true
+
validates :format, :groupe_instructeurs, :key, presence: true
scope :ante_chronological, -> { order(updated_at: :desc) }
@@ -53,7 +61,6 @@ class Export < ApplicationRecord
def compute
self.dossiers_count = dossiers_for_export.count
- load_snapshot!
file.attach(blob.signed_id) # attaching a blob directly might run identify/virus scanner and wipe it
end
@@ -62,16 +69,16 @@ class Export < ApplicationRecord
time_span_type == Export.time_span_types.fetch(:monthly) ? 30.days.ago : nil
end
- def filtered?
- procedure_presentation_id.present?
- end
+ def self.find_or_create_fresh_export(format, groupe_instructeurs, user_profile, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil, export_template: nil)
+ filtered_columns = Array.wrap(procedure_presentation&.filters_for(statut))
+ sorted_column = procedure_presentation&.sorted_column
- def self.find_or_create_fresh_export(format, groupe_instructeurs, user_profile, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil)
attributes = {
format:,
+ export_template:,
time_span_type:,
statut:,
- key: generate_cache_key(groupe_instructeurs.map(&:id), procedure_presentation)
+ key: generate_cache_key(groupe_instructeurs.map(&:id), filtered_columns, sorted_column)
}
recent_export = pending
@@ -83,36 +90,30 @@ class Export < ApplicationRecord
create!(**attributes, groupe_instructeurs:,
user_profile:,
- procedure_presentation:,
- procedure_presentation_snapshot: procedure_presentation&.snapshot)
+ filtered_columns:,
+ sorted_column:)
end
def self.for_groupe_instructeurs(groupe_instructeurs_ids)
joins(:groupe_instructeurs).where(groupe_instructeurs: groupe_instructeurs_ids).distinct(:id)
end
- def self.by_key(groupe_instructeurs_ids, procedure_presentation)
- where(key: [
- generate_cache_key(groupe_instructeurs_ids),
- generate_cache_key(groupe_instructeurs_ids, procedure_presentation)
- ])
+ def self.by_key(groupe_instructeurs_ids)
+ where(key: generate_cache_key(groupe_instructeurs_ids))
end
- def self.generate_cache_key(groupe_instructeurs_ids, procedure_presentation = nil)
- if procedure_presentation.present?
- [
- groupe_instructeurs_ids.sort.join('-'),
- procedure_presentation.id,
- Digest::MD5.hexdigest(procedure_presentation.snapshot.slice(:filters, :sort).to_s)
- ].join('--')
- else
- groupe_instructeurs_ids.sort.join('-')
- end
+ def self.generate_cache_key(groupe_instructeurs_ids, filtered_columns = [], sorted_column = nil)
+ columns_key = ([sorted_column] + filtered_columns).compact.map(&:id).sort.join
+
+ [
+ groupe_instructeurs_ids.sort.join('-'),
+ Digest::MD5.hexdigest(columns_key)
+ ].join('--')
end
def count
return dossiers_count if !dossiers_count.nil? # export generated
- return dossiers_for_export.count if procedure_presentation_id.present?
+ return dossiers_for_export.count if built_from_procedure_presentation?
nil
end
@@ -121,23 +122,21 @@ class Export < ApplicationRecord
groupe_instructeurs.first.procedure
end
- private
-
- def load_snapshot!
- if procedure_presentation_snapshot.present?
- procedure_presentation.attributes = procedure_presentation_snapshot
- end
+ def built_from_procedure_presentation?
+ sorted_column.present? # hack has we know that procedure_presentation always has a sorted_column
end
+ private
+
def dossiers_for_export
@dossiers_for_export ||= begin
dossiers = Dossier.where(groupe_instructeur: groupe_instructeurs)
if since.present?
dossiers.visible_by_administration.where('dossiers.depose_at > ?', since)
- elsif procedure_presentation.present?
- filtered_sorted_ids = procedure_presentation
- .filtered_sorted_ids(dossiers, statut)
+ elsif filtered_columns.present? || sorted_column.present?
+ instructeur = instructeur_from(user_profile)
+ filtered_sorted_ids = DossierFilterService.filtered_sorted_ids(dossiers, statut, filtered_columns, sorted_column, instructeur)
dossiers.where(id: filtered_sorted_ids)
else
@@ -146,8 +145,17 @@ class Export < ApplicationRecord
end
end
+ def instructeur_from(user_profile)
+ case user_profile
+ when Administrateur
+ user_profile.instructeur
+ when Instructeur
+ user_profile
+ end
+ end
+
def blob
- service = ProcedureExportService.new(procedure, dossiers_for_export, user_profile)
+ service = ProcedureExportService.new(procedure, dossiers_for_export, user_profile, export_template)
case format.to_sym
when :csv
diff --git a/app/models/export_item.rb b/app/models/export_item.rb
new file mode 100644
index 000000000..b0fb012ef
--- /dev/null
+++ b/app/models/export_item.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+class ExportItem
+ include TagsSubstitutionConcern
+ DOSSIER_STATE = Dossier.states.fetch(:en_construction)
+ FORMAT_DATE = "%Y-%m-%d".freeze
+
+ attr_reader :template, :enabled, :stable_id
+
+ def initialize(template:, enabled: true, stable_id: nil)
+ @template, @enabled, @stable_id = template, enabled, stable_id
+ end
+
+ def self.default(prefix:, enabled: true, stable_id: nil)
+ new(template: prefix_dossier_id(prefix), enabled:, stable_id:)
+ end
+
+ def self.default_pj(tdc)
+ default(prefix: tdc.libelle_as_filename, enabled: false, stable_id: tdc.stable_id)
+ end
+
+ def enabled? = enabled
+
+ def template_json = template.to_json
+
+ def template_string = TiptapService.new.to_texts_and_tags(template)
+
+ def path(dossier, attachment: nil, row_index: nil, index: nil)
+ used_tags = TiptapService.used_tags_and_libelle_for(template)
+ substitutions = tags_substitutions(used_tags, dossier, escape: false, memoize: true)
+ substitutions['original-filename'] = attachment.filename.base if attachment
+
+ TiptapService.new.to_texts_and_tags(template, substitutions) + suffix(attachment, row_index, index)
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ template == other.template &&
+ enabled == other.enabled &&
+ stable_id == other.stable_id
+ end
+
+ private
+
+ def self.prefix_dossier_id(prefix)
+ {
+ type: "doc",
+ content: [
+ {
+ type: "paragraph",
+ content: [
+ { text: "#{prefix}-", type: "text" },
+ { type: "mention", attrs: DOSSIER_ID_TAG.slice(:id, :label) }
+ ]
+ }
+ ]
+ }
+ end
+
+ def suffix(attachment, row_index, index)
+ suffix = ""
+ suffix += "-#{add_one_and_pad(row_index)}" if row_index.present?
+ suffix += "-#{add_one_and_pad(index)}" if index.present?
+ suffix += attachment.filename.extension_with_delimiter if attachment
+
+ suffix
+ end
+
+ def add_one_and_pad(number)
+ (number + 1).to_s.rjust(2, '0') if number.present?
+ end
+end
diff --git a/app/models/export_template.rb b/app/models/export_template.rb
new file mode 100644
index 000000000..0328e16b4
--- /dev/null
+++ b/app/models/export_template.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+class ExportTemplate < ApplicationRecord
+ include TagsSubstitutionConcern
+
+ self.ignored_columns += ["content"]
+
+ belongs_to :groupe_instructeur
+ has_one :procedure, through: :groupe_instructeur
+ has_many :exports, dependent: :nullify
+
+ enum kind: { zip: 'zip', csv: 'csv', xlsx: 'xlsx', ods: 'ods' }, _prefix: :template
+
+ attribute :dossier_folder, :export_item
+ attribute :export_pdf, :export_item
+ attribute :pjs, :export_item, array: true
+
+ attribute :exported_columns, :exported_column, array: true
+
+ before_validation :ensure_pjs_are_legit
+
+ validates_with ExportTemplateValidator
+
+ DOSSIER_STATE = Dossier.states.fetch(:en_construction)
+
+ # when a pj has been added to a revision, it will not be present in the previous pjs
+ # a default value is provided.
+ def pj(tdc)
+ pjs.find { _1.stable_id == tdc.stable_id } || ExportItem.default_pj(tdc)
+ end
+
+ def self.default(name: nil, kind: 'zip', groupe_instructeur:)
+ # TODO: remove default values for tabular export
+ dossier_folder = ExportItem.default(prefix: 'dossier')
+ export_pdf = ExportItem.default(prefix: 'export')
+ pjs = groupe_instructeur.procedure.exportables_pieces_jointes.map { |tdc| ExportItem.default_pj(tdc) }
+
+ new(name:, kind:, groupe_instructeur:, dossier_folder:, export_pdf:, pjs:)
+ end
+
+ def tabular?
+ kind != 'zip'
+ end
+
+ def tags
+ tags_categorized.slice(:individual, :etablissement, :dossier).values.flatten
+ end
+
+ def pj_tags
+ tags.push(
+ libelle: 'nom original du fichier',
+ id: 'original-filename'
+ )
+ end
+
+ def attachment_path(dossier, attachment, index: 0, row_index: nil, champ: nil)
+ file_path = if attachment.name == 'pdf_export_for_instructeur'
+ export_pdf.path(dossier, attachment:)
+ elsif attachment.record_type == 'Champ' && pj(champ.type_de_champ).enabled?
+ pj(champ.type_de_champ).path(dossier, attachment:, index:, row_index:)
+ else
+ nil
+ end
+
+ File.join(dossier_folder.path(dossier), file_path) if file_path.present?
+ end
+
+ def dossier_exported_columns = exported_columns.filter { _1.column.dossier_column? }
+
+ def columns_for_stable_id(stable_id)
+ exported_columns
+ .filter { _1.column.champ_column? }
+ .filter { _1.column.stable_id == stable_id }
+ end
+
+ def in_export?(exported_column)
+ @template_exported_columns ||= exported_columns.map(&:column)
+ @template_exported_columns.include?(exported_column.column)
+ end
+
+ private
+
+ def ensure_pjs_are_legit
+ legitimate_pj_stable_ids = procedure.exportables_pieces_jointes_for_all_versions.map(&:stable_id)
+
+ self.pjs = pjs.filter { _1.stable_id.in?(legitimate_pj_stable_ids) }
+ end
+end
diff --git a/app/models/exported_column.rb b/app/models/exported_column.rb
new file mode 100644
index 000000000..651407a71
--- /dev/null
+++ b/app/models/exported_column.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class ExportedColumn
+ attr_reader :column, :libelle
+
+ def initialize(column:, libelle:)
+ @column = column
+ @libelle = libelle
+ end
+
+ def id = { id: column.id, libelle: }.to_json
+
+ def libelle_with_value(champ_or_dossier, format:)
+ [libelle, ExportedColumnFormatter.format(column:, champ_or_dossier:, format:), spreadsheet_architect_type]
+ end
+
+ def spreadsheet_architect_type
+ case @column.type
+ when :boolean
+ :boolean
+ when :decimal, :integer
+ :float
+ when :datetime
+ :time
+ when :date
+ :date
+ else
+ :string
+ end
+ end
+end
diff --git a/app/models/filtered_column.rb b/app/models/filtered_column.rb
new file mode 100644
index 000000000..c861b7338
--- /dev/null
+++ b/app/models/filtered_column.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+class FilteredColumn
+ include ActiveModel::Validations
+
+ FILTERS_VALUE_MAX_LENGTH = 4048
+ # https://www.postgresql.org/docs/current/datatype-numeric.html
+ PG_INTEGER_MAX_VALUE = 2147483647
+
+ attr_reader :column, :filter
+
+ delegate :label, to: :column
+
+ validate :check_filter_max_length
+ validate :check_filter_max_integer
+ validates :filter, presence: {
+ message: -> (object, _data) { "Le filtre « #{object.label} » ne peut pas être vide" }
+ }
+
+ def initialize(column:, filter:)
+ @column = column
+ @filter = filter
+ end
+
+ def ==(other)
+ other&.column == column && other.filter == filter
+ end
+
+ def id
+ column.h_id.merge(filter:).sort.to_json
+ end
+
+ private
+
+ def check_filter_max_length
+ if @filter.present? && @filter.length > FILTERS_VALUE_MAX_LENGTH
+ errors.add(
+ :base,
+ "Le filtre « #{label} » est trop long (maximum: #{FILTERS_VALUE_MAX_LENGTH} caractères)"
+ )
+ end
+ end
+
+ def check_filter_max_integer
+ if @column.column == 'id' && @filter.to_i > PG_INTEGER_MAX_VALUE
+ errors.add(:base, "Le filtre « #{label} » n'est pas un numéro de dossier possible")
+ end
+ end
+end
diff --git a/app/models/follow.rb b/app/models/follow.rb
index 1ec50e371..7c2dcaed2 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class Follow < ApplicationRecord
belongs_to :instructeur, optional: false
- belongs_to :dossier, optional: false
+ belongs_to :dossier, optional: false, touch: true
validates :instructeur_id, uniqueness: { scope: [:dossier_id, :unfollowed_at] }
@@ -16,5 +18,6 @@ class Follow < ApplicationRecord
self.annotations_privees_seen_at ||= Time.zone.now
self.avis_seen_at ||= Time.zone.now
self.messagerie_seen_at ||= Time.zone.now
+ self.pieces_jointes_seen_at ||= Time.zone.now
end
end
diff --git a/app/models/follow_commentaire_groupe_gestionnaire.rb b/app/models/follow_commentaire_groupe_gestionnaire.rb
index 2179c326b..8f0f02da6 100644
--- a/app/models/follow_commentaire_groupe_gestionnaire.rb
+++ b/app/models/follow_commentaire_groupe_gestionnaire.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class FollowCommentaireGroupeGestionnaire < ApplicationRecord
belongs_to :gestionnaire
belongs_to :groupe_gestionnaire
diff --git a/app/models/france_connect_information.rb b/app/models/france_connect_information.rb
index 81ea02f7a..82fad522c 100644
--- a/app/models/france_connect_information.rb
+++ b/app/models/france_connect_information.rb
@@ -1,27 +1,42 @@
+# frozen_string_literal: true
+
class FranceConnectInformation < ApplicationRecord
MERGE_VALIDITY = 15.minutes
+ CONFIRMATION_EMAIL_VALIDITY = 2.days
belongs_to :user, optional: true
validates :france_connect_particulier_id, presence: true, allow_blank: false, allow_nil: false
- def associate_user!(email)
+ def safely_associate_user!(email)
begin
user = User.create!(
email: email.downcase,
password: Devise.friendly_token[0, 20],
confirmed_at: Time.zone.now
)
- user.after_confirmation
rescue ActiveRecord::RecordNotUnique
- # ignore this exception because we check before is user is nil.
+ # ignore this exception because we check before if user is nil.
# exception can be raised in race conditions, when FranceConnect calls callback 2 times.
# At the 2nd call, user is nil but exception is raised at the creation of the user
# because the first call has already created a user
end
+ clean_tokens_and_requested_email
update_attribute('user_id', user.id)
- touch # needed to update updated_at column
+ save!
+ end
+
+ def safely_update_user(user:)
+ self.user = user
+ clean_tokens_and_requested_email
+ save!
+ end
+
+ def send_custom_confirmation_instructions
+ token = SecureRandom.hex(10)
+ user.update!(confirmation_token: token, confirmation_sent_at: Time.zone.now)
+ UserMailer.custom_confirmation_instructions(user, token).deliver_later
end
def create_merge_token!
@@ -46,14 +61,18 @@ class FranceConnectInformation < ApplicationRecord
(MERGE_VALIDITY.ago < email_merge_token_created_at) && user_id.nil?
end
- def delete_merge_token!
- update(merge_token: nil, merge_token_created_at: nil)
- end
-
def delete_email_merge_token!
update(email_merge_token: nil, email_merge_token_created_at: nil)
end
+ def clean_tokens_and_requested_email
+ self.merge_token = nil
+ self.merge_token_created_at = nil
+ self.email_merge_token = nil
+ self.email_merge_token_created_at = nil
+ self.requested_email = nil
+ end
+
def full_name
[given_name, family_name].compact.join(" ")
end
diff --git a/app/models/france_connect_particulier_client.rb b/app/models/france_connect_particulier_client.rb
index 6c8248f8d..df9b10bbb 100644
--- a/app/models/france_connect_particulier_client.rb
+++ b/app/models/france_connect_particulier_client.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class FranceConnectParticulierClient < OpenIDConnect::Client
def initialize(code = nil)
config = FRANCE_CONNECT[:particulier].deep_dup
diff --git a/app/models/geo_area.rb b/app/models/geo_area.rb
index e34669759..6e847abac 100644
--- a/app/models/geo_area.rb
+++ b/app/models/geo_area.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GeoArea < ApplicationRecord
include ActionView::Helpers::NumberHelper
belongs_to :champ, optional: false
@@ -63,7 +65,7 @@ class GeoArea < ApplicationRecord
def label
case source
when GeoArea.sources.fetch(:cadastre)
- I18n.t("cadastre", scope: 'geo_area.label', numero: numero, prefixe: prefixe, section: section, surface: surface.round, commune: commune)
+ I18n.t("cadastre", scope: 'geo_area.label', numero: numero, prefixe: prefixe, section: section, surface: surface&.round, commune: commune)
when GeoArea.sources.fetch(:selection_utilisateur)
if polygon?
if area > 0
diff --git a/app/models/gestionnaire.rb b/app/models/gestionnaire.rb
index c0589ce4e..758cddd80 100644
--- a/app/models/gestionnaire.rb
+++ b/app/models/gestionnaire.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Gestionnaire < ApplicationRecord
include UserFindByConcern
has_and_belongs_to_many :groupe_gestionnaires
diff --git a/app/models/groupe_gestionnaire.rb b/app/models/groupe_gestionnaire.rb
index f9f521da0..f893132ba 100644
--- a/app/models/groupe_gestionnaire.rb
+++ b/app/models/groupe_gestionnaire.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GroupeGestionnaire < ApplicationRecord
has_many :administrateurs
has_many :commentaire_groupe_gestionnaires
diff --git a/app/models/groupe_instructeur.rb b/app/models/groupe_instructeur.rb
index b0b165b1e..b717090f8 100644
--- a/app/models/groupe_instructeur.rb
+++ b/app/models/groupe_instructeur.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GroupeInstructeur < ApplicationRecord
include Logic
DEFAUT_LABEL = 'défaut'
@@ -9,6 +11,7 @@ class GroupeInstructeur < ApplicationRecord
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_many :export_templates, dependent: :destroy
has_and_belongs_to_many :exports, dependent: :destroy
has_one :defaut_procedure, -> { with_discarded }, class_name: 'Procedure', foreign_key: :defaut_groupe_instructeur_id, dependent: :nullify, inverse_of: :defaut_groupe_instructeur
@@ -57,7 +60,6 @@ class GroupeInstructeur < ApplicationRecord
if not_found_emails.present?
instructeurs_to_add += not_found_emails.map do |email|
user = User.create_or_promote_to_instructeur(email, SecureRandom.hex, administrateurs: procedure.administrateurs)
- user.invite!
user.instructeur
end
end
diff --git a/app/models/individual.rb b/app/models/individual.rb
index c3d92b9eb..9c1c5315b 100644
--- a/app/models/individual.rb
+++ b/app/models/individual.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Individual < ApplicationRecord
enum notification_method: {
email: 'email',
@@ -15,7 +17,10 @@ class Individual < ApplicationRecord
if: -> { dossier.for_tiers? },
on: :update
- validates :email, presence: true, if: -> { dossier.for_tiers? && self.email? }, on: :update
+ validates :email, strict_email: true, presence: true, if: -> { dossier.for_tiers? && self.email? }, on: :update
+ validate :email_different_from_mandataire, on: :update
+
+ after_commit -> { dossier.index_search_terms_later }, if: -> { nom_previously_changed? || prenom_previously_changed? }
GENDER_MALE = "M."
GENDER_FEMALE = 'Mme'
@@ -27,4 +32,12 @@ class Individual < ApplicationRecord
gender: fc_information.gender == 'female' ? GENDER_FEMALE : GENDER_MALE
)
end
+
+ def unverified_email? = !email_verified_at?
+
+ def email_different_from_mandataire
+ if email.present? && email.casecmp?(dossier.user.email)
+ errors.add(:email, :must_be_different_from_mandataire)
+ end
+ end
end
diff --git a/app/models/instructeur.rb b/app/models/instructeur.rb
index 707e4da98..b84e2c3b7 100644
--- a/app/models/instructeur.rb
+++ b/app/models/instructeur.rb
@@ -1,6 +1,6 @@
-class Instructeur < ApplicationRecord
- self.ignored_columns += [:agent_connect_id]
+# frozen_string_literal: true
+class Instructeur < ApplicationRecord
include UserFindByConcern
has_and_belongs_to_many :administrateurs
@@ -14,6 +14,7 @@ class Instructeur < ApplicationRecord
has_many :batch_operations, dependent: :nullify
has_many :assign_to_with_email_notifications, -> { with_email_notifications }, class_name: 'AssignTo', inverse_of: :instructeur
has_many :groupe_instructeur_with_email_notifications, through: :assign_to_with_email_notifications, source: :groupe_instructeur
+ has_many :export_templates, through: :groupe_instructeurs
has_many :commentaires, inverse_of: :instructeur, dependent: :nullify
has_many :dossiers, -> { state_not_brouillon }, through: :unordered_groupe_instructeurs
@@ -124,10 +125,11 @@ class Instructeur < ApplicationRecord
annotations_privees = dossier.last_champ_private_updated_at&.>(follow.annotations_privees_seen_at) || false
avis_notif = dossier.last_avis_updated_at&.>(follow.avis_seen_at) || false
messagerie = dossier.last_commentaire_updated_at&.>(follow.messagerie_seen_at) || false
+ pieces_jointes = dossier.last_champ_piece_jointe_updated_at&.>(follow.pieces_jointes_seen_at) || dossier.last_commentaire_piece_jointe_updated_at&.>(follow.pieces_jointes_seen_at) || dossier.last_avis_piece_jointe_updated_at&.>(follow.pieces_jointes_seen_at) || false
- annotations_hash(demande, annotations_privees, avis_notif, messagerie)
+ annotations_hash(demande, annotations_privees, avis_notif, messagerie, pieces_jointes)
else
- annotations_hash(false, false, false, false)
+ annotations_hash(false, false, false, false, false)
end
end
@@ -211,6 +213,11 @@ class Instructeur < ApplicationRecord
trusted_device_token&.token_young?
end
+ def should_receive_email_activation?
+ # if was recently created or received an activation email more than 7 days ago
+ previously_new_record? || user.reset_password_sent_at.nil? || user.reset_password_sent_at < Devise.reset_password_within.ago
+ end
+
def can_be_deleted?
user.administrateur.nil? && procedures.all? { |p| p.defaut_groupe_instructeur.instructeurs.count > 1 }
end
@@ -226,18 +233,18 @@ class Instructeur < ApplicationRecord
def dossiers_count_summary(groupe_instructeur_ids)
query = <<~EOF
SELECT
- COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND not archived AND dossiers.state in ('en_construction', 'en_instruction') AND follows.id IS NULL) AS a_suivre,
- COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND not archived AND dossiers.state in ('en_construction', 'en_instruction') AND follows.instructeur_id = :instructeur_id) AS suivis,
- COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND not archived AND dossiers.state in ('accepte', 'refuse', 'sans_suite')) AS traites,
- COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND not archived) AS tous,
- COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND archived) AS archives,
- COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NOT NULL AND not archived AND dossiers.state in ('accepte', 'refuse', 'sans_suite')) AS supprimes_recemment,
- COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND procedures.procedure_expires_when_termine_enabled
+ COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND dossiers.hidden_by_expired_at IS NULL AND not archived AND dossiers.state in ('en_construction', 'en_instruction') AND follows.id IS NULL) AS a_suivre,
+ COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND dossiers.hidden_by_expired_at IS NULL AND not archived AND dossiers.state in ('en_construction', 'en_instruction') AND follows.instructeur_id = :instructeur_id) AS suivis,
+ COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND dossiers.hidden_by_expired_at IS NULL AND not archived AND dossiers.state in ('accepte', 'refuse', 'sans_suite')) AS traites,
+ COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND dossiers.hidden_by_expired_at IS NULL AND not archived) AS tous,
+ COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND dossiers.hidden_by_expired_at IS NULL AND archived) AS archives,
+ COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NOT NULL AND not archived OR dossiers.hidden_by_expired_at IS NOT NULL) AS supprimes,
+ COUNT(DISTINCT dossiers.id) FILTER (where dossiers.hidden_by_administration_at IS NULL AND dossiers.hidden_by_expired_at IS NULL AND procedures.procedure_expires_when_termine_enabled
AND (
dossiers.state in ('accepte', 'refuse', 'sans_suite')
AND dossiers.processed_at + dossiers.conservation_extension + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now
) OR (
- dossiers.state in ('en_construction')
+ dossiers.state in ('en_construction') AND dossiers.hidden_by_expired_at IS NULL
AND dossiers.en_construction_at + dossiers.conservation_extension + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now
)
) AS expirant
@@ -302,14 +309,19 @@ class Instructeur < ApplicationRecord
agent_connect_information.order(updated_at: :desc).first
end
+ def export_templates_for(procedure)
+ procedure.export_templates.where(groupe_instructeur: groupe_instructeurs).order(:name)
+ end
+
private
- def annotations_hash(demande, annotations_privees, avis, messagerie)
+ def annotations_hash(demande, annotations_privees, avis, messagerie, pieces_jointes)
{
demande: demande,
annotations_privees: annotations_privees,
avis: avis,
- messagerie: messagerie
+ messagerie: messagerie,
+ pieces_jointes: pieces_jointes
}
end
diff --git a/app/models/invite.rb b/app/models/invite.rb
index 3d96fc475..de4412e53 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Invite < ApplicationRecord
include EmailSanitizableConcern
diff --git a/app/models/label.rb b/app/models/label.rb
new file mode 100644
index 000000000..aaeeebf0d
--- /dev/null
+++ b/app/models/label.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class Label < ApplicationRecord
+ belongs_to :procedure
+ has_many :dossier_labels, dependent: :destroy
+
+ NAME_MAX_LENGTH = 30
+ GENERIC_LABELS = [
+ { name: 'À examiner', color: 'purple_glycine' },
+ { name: 'À relancer', color: 'green_tilleul_verveine' },
+ { name: 'Complet', color: 'green_emeraude' },
+ { name: 'À signer', color: 'blue_ecume' },
+ { name: 'Urgent', color: 'pink_macaron' }
+ ]
+
+ enum color: {
+ green_tilleul_verveine: "green-tilleul-verveine",
+ green_bourgeon: "green-bourgeon",
+ green_emeraude: "green-emeraude",
+ green_menthe: "green-menthe",
+ blue_ecume: "blue-ecume",
+ purple_glycine: "purple-glycine",
+ pink_macaron: "pink-macaron",
+ yellow_tournesol: "yellow-tournesol",
+ brown_cafe_creme: "brown-cafe-creme",
+ beige_gris_galet: "beige-gris-galet"
+ }
+
+ validates :name, :color, presence: true
+ validates :name, length: { maximum: NAME_MAX_LENGTH }
+
+ def self.class_name(color)
+ Label.colors.fetch(color.underscore)
+ end
+end
diff --git a/app/models/label_model.rb b/app/models/label_model.rb
index 4fb427394..49bb7fe06 100644
--- a/app/models/label_model.rb
+++ b/app/models/label_model.rb
@@ -1 +1,3 @@
+# frozen_string_literal: true
+
LabelModel = Struct.new(:id, :label, keyword_init: true)
diff --git a/app/models/logic.rb b/app/models/logic.rb
index 8eced2658..7325b29f9 100644
--- a/app/models/logic.rb
+++ b/app/models/logic.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Logic
def self.from_h(h)
class_from_name(h['term']).from_h(h)
@@ -42,7 +44,7 @@ module Logic
operator_class = EmptyOperator
in [:enum, _]
operator_class = Eq
- in [:commune_enum, _] | [:epci_enum, _]
+ in [:commune_enum, _] | [:epci_enum, _] | [:address, _]
operator_class = InDepartementOperator
in [:departement_enum, _]
operator_class = Eq
@@ -59,7 +61,7 @@ module Logic
Constant.new(true)
when :empty
Empty.new
- when :enum, :enums, :commune_enum, :epci_enum, :departement_enum
+ when :enum, :enums, :commune_enum, :epci_enum, :departement_enum, :address
Constant.new(left.options(type_de_champs).first.second)
when :number
Constant.new(0)
@@ -73,7 +75,7 @@ module Logic
case [left.type(type_de_champs), right.type(type_de_champs)]
in [a, ^a] # syntax for same type
true
- in [:enum, :string] | [:enums, :string] | [:commune_enum, :string] | [:epci_enum, :string] | [:departement_enum, :string]
+ in [:enum, :string] | [:enums, :string] | [:commune_enum, :string] | [:epci_enum, :string] | [:departement_enum, :string] | [:address, :string]
true
else
false
diff --git a/app/models/logic/and.rb b/app/models/logic/and.rb
index 51537235f..5bb97329c 100644
--- a/app/models/logic/and.rb
+++ b/app/models/logic/and.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::And < Logic::NAryOperator
attr_reader :operands
diff --git a/app/models/logic/binary_operator.rb b/app/models/logic/binary_operator.rb
index 812fa0605..019836923 100644
--- a/app/models/logic/binary_operator.rb
+++ b/app/models/logic/binary_operator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::BinaryOperator < Logic::Term
attr_reader :left, :right
diff --git a/app/models/logic/champ_value.rb b/app/models/logic/champ_value.rb
index efb46a55a..1d9c9858e 100644
--- a/app/models/logic/champ_value.rb
+++ b/app/models/logic/champ_value.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::ChampValue < Logic::Term
MANAGED_TYPE_DE_CHAMP = TypeDeChamp.type_champs.slice(
:yes_no,
@@ -6,12 +8,19 @@ class Logic::ChampValue < Logic::Term
:decimal_number,
:drop_down_list,
:multiple_drop_down_list,
+ :address,
:communes,
:epci,
:departements,
- :regions
+ :regions,
+ :pays
)
+ MANAGED_TYPE_DE_CHAMP_BY_CATEGORY = MANAGED_TYPE_DE_CHAMP.keys.map(&:to_sym)
+ .each_with_object(Hash.new { |h, k| h[k] = [] }) do |type, h|
+ h[TypeDeChamp::TYPE_DE_CHAMP_TO_CATEGORIE[type]] << type
+ end
+
CHAMP_VALUE_TYPE = {
boolean: :boolean, # from yes_no or checkbox champ
number: :number, # from integer or decimal number champ
@@ -19,6 +28,7 @@ class Logic::ChampValue < Logic::Term
commune_enum: :commune_enum,
epci_enum: :epci_enum,
departement_enum: :departement_enum,
+ address: :address,
enums: :enums, # multiple choice from a dropdownlist (multipledropdownlist)
empty: :empty,
unmanaged: :unmanaged
@@ -41,25 +51,25 @@ class Logic::ChampValue < Logic::Term
return nil if !targeted_champ.visible?
return nil if targeted_champ.blank? & !targeted_champ.drop_down_other?
- # on dépense 22ms ici, à cause du map, mais on doit pouvoir passer par un champ type
case targeted_champ.type
when "Champs::YesNoChamp",
"Champs::CheckboxChamp"
targeted_champ.true?
when "Champs::IntegerNumberChamp", "Champs::DecimalNumberChamp"
- targeted_champ.for_api
+ # TODO expose raw typed value of champs
+ targeted_champ.type_de_champ.champ_value_for_api(targeted_champ, version: 1)
when "Champs::DropDownListChamp"
targeted_champ.selected
when "Champs::MultipleDropDownListChamp"
targeted_champ.selected_options
- when "Champs::RegionChamp"
+ when "Champs::RegionChamp", "Champs::PaysChamp"
targeted_champ.code
when "Champs::DepartementChamp"
{
value: targeted_champ.code,
code_region: targeted_champ.code_region
}
- when "Champs::CommuneChamp", "Champs::EpciChamp"
+ when "Champs::CommuneChamp", "Champs::EpciChamp", "Champs::AddressChamp"
{
code_departement: targeted_champ.code_departement,
code_region: targeted_champ.code_region
@@ -77,7 +87,7 @@ class Logic::ChampValue < Logic::Term
when MANAGED_TYPE_DE_CHAMP.fetch(:integer_number), MANAGED_TYPE_DE_CHAMP.fetch(:decimal_number)
CHAMP_VALUE_TYPE.fetch(:number)
when MANAGED_TYPE_DE_CHAMP.fetch(:drop_down_list),
- MANAGED_TYPE_DE_CHAMP.fetch(:regions)
+ MANAGED_TYPE_DE_CHAMP.fetch(:regions), MANAGED_TYPE_DE_CHAMP.fetch(:pays)
CHAMP_VALUE_TYPE.fetch(:enum)
when MANAGED_TYPE_DE_CHAMP.fetch(:communes)
CHAMP_VALUE_TYPE.fetch(:commune_enum)
@@ -85,6 +95,8 @@ class Logic::ChampValue < Logic::Term
CHAMP_VALUE_TYPE.fetch(:epci_enum)
when MANAGED_TYPE_DE_CHAMP.fetch(:departements)
CHAMP_VALUE_TYPE.fetch(:departement_enum)
+ when MANAGED_TYPE_DE_CHAMP.fetch(:address)
+ CHAMP_VALUE_TYPE.fetch(:address)
when MANAGED_TYPE_DE_CHAMP.fetch(:multiple_drop_down_list)
CHAMP_VALUE_TYPE.fetch(:enums)
else
@@ -119,11 +131,13 @@ class Logic::ChampValue < Logic::Term
tdc = type_de_champ(type_de_champs)
if operator_name.in?([Logic::InRegionOperator.name, Logic::NotInRegionOperator.name]) || tdc.type_champ == MANAGED_TYPE_DE_CHAMP.fetch(:regions)
- APIGeoService.regions.map { ["#{_1[:code]} – #{_1[:name]}", _1[:code]] }
- elsif operator_name.in?([Logic::InDepartementOperator.name, Logic::NotInDepartementOperator.name]) || tdc.type_champ.in?([MANAGED_TYPE_DE_CHAMP.fetch(:communes), MANAGED_TYPE_DE_CHAMP.fetch(:epci), MANAGED_TYPE_DE_CHAMP.fetch(:departements)])
- APIGeoService.departements.map { ["#{_1[:code]} – #{_1[:name]}", _1[:code]] }
+ APIGeoService.region_options
+ elsif operator_name.in?([Logic::InDepartementOperator.name, Logic::NotInDepartementOperator.name]) || tdc.type_champ.in?([MANAGED_TYPE_DE_CHAMP.fetch(:communes), MANAGED_TYPE_DE_CHAMP.fetch(:epci), MANAGED_TYPE_DE_CHAMP.fetch(:departements), MANAGED_TYPE_DE_CHAMP.fetch(:address)])
+ APIGeoService.departement_options
+ elsif tdc.type_champ == MANAGED_TYPE_DE_CHAMP.fetch(:pays)
+ APIGeoService.countries.map { ["#{_1[:name]} – #{_1[:code]}", _1[:code]] }
else
- tdc.drop_down_list_enabled_non_empty_options(other: true).map { _1.is_a?(Array) ? _1 : [_1, _1] }
+ tdc.drop_down_options_with_other.map { _1.is_a?(Array) ? _1 : [_1, _1] }
end
end
diff --git a/app/models/logic/constant.rb b/app/models/logic/constant.rb
index e1042d580..bce68c2c7 100644
--- a/app/models/logic/constant.rb
+++ b/app/models/logic/constant.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::Constant < Logic::Term
attr_reader :value
diff --git a/app/models/logic/empty.rb b/app/models/logic/empty.rb
index 605af6368..071dd8f4d 100644
--- a/app/models/logic/empty.rb
+++ b/app/models/logic/empty.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::Empty < Logic::Term
def sources
[]
diff --git a/app/models/logic/empty_operator.rb b/app/models/logic/empty_operator.rb
index 5315f0b87..27938d42a 100644
--- a/app/models/logic/empty_operator.rb
+++ b/app/models/logic/empty_operator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::EmptyOperator < Logic::BinaryOperator
def to_s(_type_de_champs = []) = I18n.t('logic.empty_operator')
diff --git a/app/models/logic/eq.rb b/app/models/logic/eq.rb
index da07341ce..ba050d72d 100644
--- a/app/models/logic/eq.rb
+++ b/app/models/logic/eq.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::Eq < Logic::BinaryOperator
def operation = :==
diff --git a/app/models/logic/exclude_operator.rb b/app/models/logic/exclude_operator.rb
index 8addb7d0a..78ae7e038 100644
--- a/app/models/logic/exclude_operator.rb
+++ b/app/models/logic/exclude_operator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::ExcludeOperator < Logic::IncludeOperator
def operation = :exclude?
end
diff --git a/app/models/logic/greater_than.rb b/app/models/logic/greater_than.rb
index 2ba94af0f..1039c49bb 100644
--- a/app/models/logic/greater_than.rb
+++ b/app/models/logic/greater_than.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::GreaterThan < Logic::BinaryOperator
def operation = :>
end
diff --git a/app/models/logic/greater_than_eq.rb b/app/models/logic/greater_than_eq.rb
index d452bceef..df594abff 100644
--- a/app/models/logic/greater_than_eq.rb
+++ b/app/models/logic/greater_than_eq.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::GreaterThanEq < Logic::BinaryOperator
def operation = :>=
end
diff --git a/app/models/logic/in_departement_operator.rb b/app/models/logic/in_departement_operator.rb
index ee9532b36..280ed3ce6 100644
--- a/app/models/logic/in_departement_operator.rb
+++ b/app/models/logic/in_departement_operator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::InDepartementOperator < Logic::BinaryOperator
def operation
:est_dans_le_departement
diff --git a/app/models/logic/in_region_operator.rb b/app/models/logic/in_region_operator.rb
index 54625e987..0cccd0ed8 100644
--- a/app/models/logic/in_region_operator.rb
+++ b/app/models/logic/in_region_operator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::InRegionOperator < Logic::BinaryOperator
def operation
:est_dans_la_region
diff --git a/app/models/logic/include_operator.rb b/app/models/logic/include_operator.rb
index 2e1f05c57..a76d7e15f 100644
--- a/app/models/logic/include_operator.rb
+++ b/app/models/logic/include_operator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::IncludeOperator < Logic::BinaryOperator
def operation = :include?
diff --git a/app/models/logic/less_than.rb b/app/models/logic/less_than.rb
index b39282ee7..cf12f0402 100644
--- a/app/models/logic/less_than.rb
+++ b/app/models/logic/less_than.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::LessThan < Logic::BinaryOperator
def operation = :<
end
diff --git a/app/models/logic/less_than_eq.rb b/app/models/logic/less_than_eq.rb
index 79e4c7308..b1180d1e2 100644
--- a/app/models/logic/less_than_eq.rb
+++ b/app/models/logic/less_than_eq.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::LessThanEq < Logic::BinaryOperator
def operation = :<=
end
diff --git a/app/models/logic/n_ary_operator.rb b/app/models/logic/n_ary_operator.rb
index 02bc0cf42..c7006a082 100644
--- a/app/models/logic/n_ary_operator.rb
+++ b/app/models/logic/n_ary_operator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::NAryOperator < Logic::Term
attr_reader :operands
diff --git a/app/models/logic/not_eq.rb b/app/models/logic/not_eq.rb
index b11ec1c92..d1ff226b9 100644
--- a/app/models/logic/not_eq.rb
+++ b/app/models/logic/not_eq.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::NotEq < Logic::Eq
def operation = :!=
end
diff --git a/app/models/logic/not_in_departement_operator.rb b/app/models/logic/not_in_departement_operator.rb
index 472ef6c18..5c3053a0c 100644
--- a/app/models/logic/not_in_departement_operator.rb
+++ b/app/models/logic/not_in_departement_operator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::NotInDepartementOperator < Logic::InDepartementOperator
def operation
:n_est_pas_dans_le_departement
diff --git a/app/models/logic/not_in_region_operator.rb b/app/models/logic/not_in_region_operator.rb
index 3bdac4d85..179203d21 100644
--- a/app/models/logic/not_in_region_operator.rb
+++ b/app/models/logic/not_in_region_operator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::NotInRegionOperator < Logic::InRegionOperator
def operation
:n_est_pas_dans_la_region
diff --git a/app/models/logic/or.rb b/app/models/logic/or.rb
index a0e2dfeae..bd886a813 100644
--- a/app/models/logic/or.rb
+++ b/app/models/logic/or.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::Or < Logic::NAryOperator
attr_reader :operands
diff --git a/app/models/logic/term.rb b/app/models/logic/term.rb
index 3cbed35a6..86489a806 100644
--- a/app/models/logic/term.rb
+++ b/app/models/logic/term.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Logic::Term
def to_json
to_h.to_json
diff --git a/app/models/mails/closed_mail.rb b/app/models/mails/closed_mail.rb
index 9e6ba017d..f155760ac 100644
--- a/app/models/mails/closed_mail.rb
+++ b/app/models/mails/closed_mail.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# == Schema Information
#
# Table name: closed_mails
diff --git a/app/models/mails/initiated_mail.rb b/app/models/mails/initiated_mail.rb
index 9611ef125..a55edeab3 100644
--- a/app/models/mails/initiated_mail.rb
+++ b/app/models/mails/initiated_mail.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# == Schema Information
#
# Table name: initiated_mails
diff --git a/app/models/mails/re_instructed_mail.rb b/app/models/mails/re_instructed_mail.rb
index 0a7dc35e0..d8fc4549b 100644
--- a/app/models/mails/re_instructed_mail.rb
+++ b/app/models/mails/re_instructed_mail.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# == Schema Information
#
# Table name: re_instructed_mails
diff --git a/app/models/mails/received_mail.rb b/app/models/mails/received_mail.rb
index 69242d60d..cb137a699 100644
--- a/app/models/mails/received_mail.rb
+++ b/app/models/mails/received_mail.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# == Schema Information
#
# Table name: received_mails
diff --git a/app/models/mails/refused_mail.rb b/app/models/mails/refused_mail.rb
index eff138c4f..565656084 100644
--- a/app/models/mails/refused_mail.rb
+++ b/app/models/mails/refused_mail.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# == Schema Information
#
# Table name: refused_mails
diff --git a/app/models/mails/without_continuation_mail.rb b/app/models/mails/without_continuation_mail.rb
index bc9c71843..00b991d3a 100644
--- a/app/models/mails/without_continuation_mail.rb
+++ b/app/models/mails/without_continuation_mail.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# == Schema Information
#
# Table name: without_continuation_mails
diff --git a/app/models/map_filter.rb b/app/models/map_filter.rb
index 06837bdb1..fb6c5510b 100644
--- a/app/models/map_filter.rb
+++ b/app/models/map_filter.rb
@@ -1,34 +1,23 @@
-class MapFilter
- # https://api.rubyonrails.org/v7.1.1/classes/ActiveModel/Errors.html
+# frozen_string_literal: true
- include ActiveModel::Conversion
- extend ActiveModel::Translation
- extend ActiveModel::Naming
+class MapFilter
+ include ActiveModel::Model
+ include ActiveModel::Attributes
LEGEND = {
- nb_demarches: { 'nothing': -1, 'small': 20, 'medium': 50, 'large': 100, 'xlarge': 500 },
- nb_dossiers: { 'nothing': -1, 'small': 500, 'medium': 2000, 'large': 10000, 'xlarge': 50000 }
- }
+ "nb_demarches" => { 'nothing': -1, 'small': 20, 'medium': 50, 'large': 100, 'xlarge': 500 },
+ "nb_dossiers" => { 'nothing': -1, 'small': 500, 'medium': 2000, 'large': 10000, 'xlarge': 50000 }
+ }.freeze
+
+ YEARS_INTERVAL = 2018..Date.current.year
attr_accessor :stats
- attr_reader :errors
- def initialize(params)
- @params = params[:map_filter]&.permit(:kind, :year) || {}
- @errors = ActiveModel::Errors.new(self)
- end
+ attribute :year, :integer
+ validates :year, numericality: { only_integer: true, greater_than_or_equal_to: YEARS_INTERVAL.begin, less_than_or_equal_to: YEARS_INTERVAL.end }
- def persisted?
- false
- end
-
- def kind
- @params[:kind]&.to_sym || :nb_demarches
- end
-
- def year
- @params[:year].presence
- end
+ attribute :kind, default: "nb_demarches"
+ validates :kind, inclusion: { in: LEGEND.keys }
def kind_buttons
LEGEND.keys.map do
@@ -41,7 +30,7 @@ class MapFilter
end
def css_class_for_departement(departement)
- if kind == :nb_demarches
+ if kind == "nb_demarches"
kind_legend_keys.reverse.find do
nb_demarches_for_departement(departement) > LEGEND[kind][_1]
end
diff --git a/app/models/merge_log.rb b/app/models/merge_log.rb
index 5e4a14ade..6575cd62a 100644
--- a/app/models/merge_log.rb
+++ b/app/models/merge_log.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class MergeLog < ApplicationRecord
belongs_to :user
end
diff --git a/app/models/module_api_carto.rb b/app/models/module_api_carto.rb
index ffc6d8dd4..fe4b07805 100644
--- a/app/models/module_api_carto.rb
+++ b/app/models/module_api_carto.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ModuleAPICarto < ApplicationRecord
belongs_to :procedure, optional: false
end
diff --git a/app/models/null_zone.rb b/app/models/null_zone.rb
index 9ce32eacf..4349d2424 100644
--- a/app/models/null_zone.rb
+++ b/app/models/null_zone.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NullZone
include ActiveModel::Model
ReflectionAssociation = Struct.new(:class_name)
diff --git a/app/models/outdated_procedure.rb b/app/models/outdated_procedure.rb
index b74295060..6d8e08690 100644
--- a/app/models/outdated_procedure.rb
+++ b/app/models/outdated_procedure.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class OutdatedProcedure
extend ActiveModel::Naming
extend ActiveModel::Translation
diff --git a/app/models/path_rewrite.rb b/app/models/path_rewrite.rb
new file mode 100644
index 000000000..7cb983ca8
--- /dev/null
+++ b/app/models/path_rewrite.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class PathRewrite < ApplicationRecord
+end
diff --git a/app/models/prefill_champs.rb b/app/models/prefill_champs.rb
index f013c96d4..a55fa7d9b 100644
--- a/app/models/prefill_champs.rb
+++ b/app/models/prefill_champs.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PrefillChamps
attr_reader :dossier, :params
@@ -23,7 +25,7 @@ class PrefillChamps
.to_h
dossier
- .find_champs_by_stable_ids(value_by_stable_id.keys)
+ .champs_for_prefill(value_by_stable_id.keys)
.map { |champ| [champ, value_by_stable_id[champ.stable_id]] }
.map { |champ, value| PrefillValue.new(champ:, value:, dossier:) }
end
diff --git a/app/models/prefill_description.rb b/app/models/prefill_description.rb
index 58853f600..4da4e3479 100644
--- a/app/models/prefill_description.rb
+++ b/app/models/prefill_description.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PrefillDescription < SimpleDelegator
include Rails.application.routes.url_helpers
diff --git a/app/models/prefill_identity.rb b/app/models/prefill_identity.rb
index c4419b094..bbf66ed25 100644
--- a/app/models/prefill_identity.rb
+++ b/app/models/prefill_identity.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PrefillIdentity
attr_reader :dossier, :params
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index 827f417d5..90344edc9 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -1,20 +1,19 @@
+# frozen_string_literal: true
+
class Procedure < ApplicationRecord
+ include APIEntrepriseTokenConcern
include ProcedureStatsConcern
include EncryptableConcern
include InitiationProcedureConcern
include ProcedureGroupeInstructeurAPIHackConcern
include ProcedureSVASVRConcern
include ProcedureChorusConcern
+ include ProcedurePublishConcern
+ include PiecesJointesListConcern
+ include ColumnsConcern
include Discard::Model
self.discard_column = :hidden_at
- self.ignored_columns += [
- :direction,
- :durees_conservation_required,
- :cerfa_flag,
- :test_started_at,
- :lien_demarche
- ]
default_scope -> { kept }
@@ -49,9 +48,9 @@ class Procedure < ApplicationRecord
has_one :module_api_carto, dependent: :destroy
has_many :attestation_templates, dependent: :destroy
has_one :attestation_template_v1, -> { AttestationTemplate.v1 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure
- has_one :attestation_template_v2, -> { AttestationTemplate.v2 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure
+ has_many :attestation_templates_v2, -> { AttestationTemplate.v2 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure
- has_one :attestation_template, -> { order(Arel.sql("CASE WHEN version = '1' THEN 0 ELSE 1 END")) }, dependent: :destroy, inverse_of: :procedure
+ has_one :attestation_template, -> { published }, dependent: :destroy, inverse_of: :procedure
belongs_to :parent_procedure, class_name: 'Procedure', optional: true
belongs_to :canonical_procedure, class_name: 'Procedure', optional: true
@@ -59,8 +58,10 @@ class Procedure < ApplicationRecord
belongs_to :service, optional: true
belongs_to :zone, optional: true
has_and_belongs_to_many :zones
+ has_and_belongs_to_many :procedure_tags
has_many :bulk_messages, dependent: :destroy
+ has_many :labels, dependent: :destroy
def active_dossier_submitted_message
published_dossier_submitted_message || draft_dossier_submitted_message
@@ -70,10 +71,11 @@ class Procedure < ApplicationRecord
brouillon? ? draft_revision : published_revision
end
- def types_de_champ_for_procedure_presentation(parent = nil)
+ def all_revisions_types_de_champ(parent: nil, with_header_section: false)
+ types_de_champ_scope = with_header_section ? TypeDeChamp.with_header_section : TypeDeChamp.fillable
if brouillon?
if parent.nil?
- TypeDeChamp.fillable
+ types_de_champ_scope
.joins(:revision_types_de_champ)
.where(revision_types_de_champ: { revision_id: draft_revision_id, parent_id: nil })
.order(:private, :position)
@@ -81,45 +83,15 @@ class Procedure < ApplicationRecord
draft_revision.children_of(parent)
end
else
- # all published revisions
- revision_ids = revisions.ids - [draft_revision_id]
- # fetch all parent types de champ
- parent_ids = if parent.present?
- ProcedureRevisionTypeDeChamp
- .where(revision_id: revision_ids)
- .joins(:type_de_champ)
- .where(type_de_champ: { stable_id: parent.stable_id })
- .ids
- end
-
- # fetch all type_de_champ.stable_id for all the revisions expect draft
- # and for each stable_id take the bigger (more recent) type_de_champ.id
- recent_ids = TypeDeChamp
- .fillable
- .joins(:revision_types_de_champ)
- .where(revision_types_de_champ: { revision_id: revision_ids, parent_id: parent_ids })
- .group(:stable_id).select('MAX(types_de_champ.id)')
-
- # fetch the more recent procedure_revision_types_de_champ
- # which includes recents_ids
- recents_prtdc = ProcedureRevisionTypeDeChamp
- .where(type_de_champ_id: recent_ids)
- .where.not(revision_id: draft_revision_id)
- .group(:type_de_champ_id)
- .select('MAX(id)')
-
- TypeDeChamp
- .joins(:revision_types_de_champ)
- .where(revision_types_de_champ: { id: recents_prtdc }).then do |relation|
- if feature_enabled?(:export_order_by_revision) # Fonds Verts, en attente d'exports personnalisables
- relation.order(:private, 'revision_types_de_champ.revision_id': :desc, position: :asc)
- else
- relation.order(:private, :position, 'revision_types_de_champ.revision_id': :desc)
- end
- end
+ cache_key = ['all_revisions_types_de_champ', published_revision, parent, with_header_section].compact
+ Rails.cache.fetch(cache_key, expires_in: 1.month) { published_revisions_types_de_champ(parent:, with_header_section:) }
end
end
+ def types_de_champ_for_procedure_export
+ all_revisions_types_de_champ.not_repetition
+ end
+
def types_de_champ_for_tags
TypeDeChamp
.fillable
@@ -150,9 +122,10 @@ class Procedure < ApplicationRecord
end
has_many :administrateurs_procedures, dependent: :delete_all
- has_many :administrateurs, through: :administrateurs_procedures, after_remove: -> (procedure, _admin) { procedure.validate! }
+ has_many :administrateurs, through: :administrateurs_procedures, before_remove: :check_administrateur_minimal_presence
has_many :groupe_instructeurs, -> { order(:label) }, inverse_of: :procedure, dependent: :destroy
has_many :instructeurs, through: :groupe_instructeurs
+ has_many :export_templates, through: :groupe_instructeurs
has_many :active_groupe_instructeurs, -> { active }, class_name: 'GroupeInstructeur', inverse_of: false
has_many :closed_groupe_instructeurs, -> { closed }, class_name: 'GroupeInstructeur', inverse_of: false
@@ -171,9 +144,7 @@ class Procedure < ApplicationRecord
belongs_to :defaut_groupe_instructeur, class_name: 'GroupeInstructeur', inverse_of: false, optional: true
- has_one_attached :logo do |attachable|
- attachable.variant :email, resize_to_limit: [450, 450]
- end
+ has_one_attached :logo
has_one_attached :notice
has_one_attached :deliberation
@@ -234,20 +205,6 @@ class Procedure < ApplicationRecord
includes(:draft_revision, :published_revision, administrateurs: :user)
}
- scope :for_download, -> {
- includes(
- :groupe_instructeurs,
- dossiers: {
- champs_public: [
- piece_justificative_file_attachments: :blob,
- champs: [
- piece_justificative_file_attachments: :blob
- ]
- ]
- }
- )
- }
-
validates :libelle, presence: true, allow_blank: false, allow_nil: false
validates :description, presence: true, allow_blank: false, allow_nil: false
validates :administrateurs, presence: true
@@ -257,13 +214,19 @@ class Procedure < ApplicationRecord
validates :lien_dpo, url: { no_local: true, allow_blank: true, accept_email: true }
validates :draft_types_de_champ_public,
+ 'types_de_champ/condition': true,
+ 'types_de_champ/expression_reguliere': true,
+ 'types_de_champ/header_section_consistency': true,
'types_de_champ/no_empty_block': true,
'types_de_champ/no_empty_drop_down': true,
- on: :publication
+ on: [:types_de_champ_public_editor, :publication]
+
validates :draft_types_de_champ_private,
+ 'types_de_champ/condition': true,
+ 'types_de_champ/header_section_consistency': true,
'types_de_champ/no_empty_block': true,
'types_de_champ/no_empty_drop_down': true,
- on: :publication
+ on: [:types_de_champ_private_editor, :publication]
validate :check_juridique, on: [:create, :publication]
@@ -285,7 +248,7 @@ class Procedure < ApplicationRecord
validates_with MonAvisEmbedValidator
- validates_associated :draft_revision, on: :publication
+ validate :validates_associated_draft_revision_with_context
validates_associated :initiated_mail, on: :publication
validates_associated :received_mail, on: :publication
validates_associated :closed_mail, on: :publication
@@ -325,7 +288,6 @@ class Procedure < ApplicationRecord
size: { less_than: LOGO_MAX_SIZE },
if: -> { new_record? || created_at > Date.new(2020, 11, 13) }
- validates :api_entreprise_token, jwt_token: true, allow_blank: true
validates :api_particulier_token, format: { with: /\A[A-Za-z0-9\-_=.]{15,}\z/ }, allow_blank: true
validate :validate_auto_archive_on_in_the_future, if: :will_save_change_to_auto_archive_on?
@@ -359,36 +321,16 @@ class Procedure < ApplicationRecord
end
end
+ def check_administrateur_minimal_presence(_object)
+ if self.administrateurs.count <= 1
+ raise ActiveRecord::RecordNotDestroyed.new("Cannot remove the last administrateur of procedure #{self.libelle} (#{self.id})")
+ end
+ end
+
def dossiers_close_to_expiration
dossiers.close_to_expiration.count
end
- def publish_or_reopen!(administrateur)
- Procedure.transaction do
- if brouillon?
- reset!
- end
-
- other_procedure = other_procedure_with_path(path)
- if other_procedure.present? && administrateur.owns?(other_procedure)
- other_procedure.unpublish!
- publish!(other_procedure.canonical_procedure || other_procedure)
- else
- publish!
- end
- end
- end
-
- def reset!
- if !locked? || draft_changed?
- dossier_ids_to_destroy = draft_revision.dossiers.ids
- if dossier_ids_to_destroy.present?
- Rails.logger.info("Resetting #{dossier_ids_to_destroy.size} dossiers on procedure #{id}: #{dossier_ids_to_destroy}")
- draft_revision.dossiers.destroy_all
- end
- end
- end
-
def suggested_path(administrateur)
if path_customized?
return path
@@ -423,11 +365,15 @@ class Procedure < ApplicationRecord
def draft_changed?
preload_draft_and_published_revisions
- !brouillon? && published_revision.different_from?(draft_revision) && revision_changes.present?
+ !brouillon? && (types_de_champ_revision_changes.present? || ineligibilite_rules_revision_changes.present?)
end
- def revision_changes
- published_revision.compare(draft_revision)
+ def types_de_champ_revision_changes
+ published_revision.compare_types_de_champ(draft_revision)
+ end
+
+ def ineligibilite_rules_revision_changes
+ published_revision.compare_ineligibilite_rules(draft_revision)
end
def preload_draft_and_published_revisions
@@ -550,6 +496,9 @@ class Procedure < ApplicationRecord
procedure.closing_details = nil
procedure.closing_notification_brouillon = false
procedure.closing_notification_en_cours = false
+ procedure.template = false
+ procedure.monavis_embed = nil
+ procedure.labels = labels.map(&:dup)
if !procedure.valid?
procedure.errors.attribute_names.each do |attribute|
@@ -649,14 +598,6 @@ class Procedure < ApplicationRecord
end
end
- def self.default_sort
- {
- 'table' => 'self',
- 'column' => 'id',
- 'order' => 'desc'
- }
- end
-
def whitelist!
touch(:whitelisted_at)
end
@@ -689,6 +630,10 @@ class Procedure < ApplicationRecord
result << :service
end
+ if service_siret_test?
+ result << :service
+ end
+
if missing_instructeurs?
result << :instructeurs
end
@@ -702,7 +647,8 @@ class Procedure < ApplicationRecord
def logo_url
if logo.attached?
- Rails.application.routes.url_helpers.url_for(logo)
+ logo_variant = logo.variant(resize_to_limit: [400, 400])
+ logo_variant.key.present? ? logo_variant.processed.url : Rails.application.routes.url_helpers.url_for(logo)
else
ActionController::Base.helpers.image_url(PROCEDURE_DEFAULT_LOGO_SRC)
end
@@ -720,6 +666,10 @@ class Procedure < ApplicationRecord
end
end
+ def service_siret_test?
+ service&.siret == Service::SIRET_TEST
+ end
+
def revised?
revisions.size > 2
end
@@ -738,7 +688,7 @@ class Procedure < ApplicationRecord
end
def routing_champs
- active_revision.types_de_champ_public.filter(&:used_by_routing_rules?).map(&:libelle)
+ active_revision.revision_types_de_champ_public.filter(&:used_by_routing_rules?).map(&:libelle)
end
def can_be_deleted_by_administrateur?
@@ -789,35 +739,6 @@ class Procedure < ApplicationRecord
"Procedure;#{id}"
end
- def api_entreprise_role?(role)
- APIEntrepriseToken.new(api_entreprise_token).role?(role)
- end
-
- def api_entreprise_token
- self[:api_entreprise_token].presence || Rails.application.secrets.api_entreprise[:key]
- end
-
- def api_entreprise_token_expired?
- APIEntrepriseToken.new(api_entreprise_token).expired?
- end
-
- def create_new_revision(revision = nil)
- transaction do
- new_revision = (revision || draft_revision)
- .deep_clone(include: [:revision_types_de_champ])
- .tap { |revision| revision.published_at = nil }
- .tap(&:save!)
-
- move_new_children_to_new_parent_coordinate(new_revision)
-
- # they are not aware of the new tdcs
- new_revision.types_de_champ_public.reset
- new_revision.types_de_champ_private.reset
-
- new_revision
- end
- end
-
def average_dossier_weight
if dossiers.termine.any?
dossiers_sample = dossiers.termine.limit(100)
@@ -832,31 +753,6 @@ class Procedure < ApplicationRecord
end
end
- def publish_revision!
- reset!
- transaction do
- self.published_revision = draft_revision
- self.draft_revision = create_new_revision
- save!(context: :publication)
- published_revision.touch(:published_at)
- end
- dossiers
- .state_not_termine
- .find_each(&:rebase_later)
- end
-
- def reset_draft_revision!
- if published_revision.present? && draft_changed?
- reset!
- transaction do
- draft_revision.types_de_champ.filter(&:only_present_on_draft?).each(&:destroy)
- draft_revision.update(dossier_submitted_message: nil)
- draft_revision.destroy
- update!(draft_revision: create_new_revision(published_revision))
- end
- end
- end
-
def cnaf_enabled?
api_particulier_sources['cnaf'].present?
end
@@ -895,45 +791,6 @@ class Procedure < ApplicationRecord
end
end
- def move_new_children_to_new_parent_coordinate(new_draft)
- children = new_draft.revision_types_de_champ
- .includes(parent: :type_de_champ)
- .where.not(parent_id: nil)
- coordinates_by_stable_id = new_draft.revision_types_de_champ
- .includes(:type_de_champ)
- .index_by(&:stable_id)
-
- children.each do |child|
- child.update!(parent: coordinates_by_stable_id.fetch(child.parent.stable_id))
- end
- new_draft.reload
- end
-
- def before_publish
- assign_attributes(closed_at: nil, unpublished_at: nil)
- end
-
- def after_publish(canonical_procedure = nil)
- self.canonical_procedure = canonical_procedure
- self.published_revision = draft_revision
- self.draft_revision = create_new_revision
- save!(context: :publication)
- touch(:published_at)
- published_revision.touch(:published_at)
- end
-
- def after_republish(canonical_procedure = nil)
- touch(:published_at)
- end
-
- def after_close
- touch(:closed_at)
- end
-
- def after_unpublish
- touch(:unpublished_at)
- end
-
def update_juridique_required
self.juridique_required ||= (cadre_juridique.present? || deliberation.attached?)
true
@@ -967,8 +824,14 @@ class Procedure < ApplicationRecord
end
end
- def stable_ids_used_by_routing_rules
- @stable_ids_used_by_routing_rules ||= groupe_instructeurs.flat_map { _1.routing_rule&.sources }.compact
+ def create_generic_labels
+ Label::GENERIC_LABELS.each do |label|
+ Label.create(name: label[:name], color: label[:color], procedure_id: self.id)
+ end
+ end
+
+ def used_by_routing_rules?(type_de_champ)
+ type_de_champ.stable_id.in?(stable_ids_used_by_routing_rules)
end
# We need this to unfuck administrate + aasm
@@ -980,22 +843,6 @@ class Procedure < ApplicationRecord
end
end
- def pieces_jointes_list?
- pieces_jointes_list_without_conditionnal.present? || pieces_jointes_list_with_conditionnal.present?
- end
-
- def pieces_jointes_list_without_conditionnal
- pieces_jointes_list do |base_scope|
- base_scope.where(types_de_champ: { condition: nil })
- end
- end
-
- def pieces_jointes_list_with_conditionnal
- pieces_jointes_list do |base_scope|
- base_scope.where.not(types_de_champ: { condition: nil })
- end
- end
-
def toggle_routing
update!(routing_enabled: self.groupe_instructeurs.active.many?)
end
@@ -1004,16 +851,14 @@ class Procedure < ApplicationRecord
lien_dpo.present? && lien_dpo.match?(/@/)
end
- def header_sections
- draft_revision.revision_types_de_champ_public.filter { _1.type_de_champ.header_section? }
- end
-
def dossier_for_preview(user)
# Try to use a preview or a dossier filled by current user
- dossiers.where(for_procedure_preview: true).or(dossiers.not_brouillon)
+ dossiers.where(for_procedure_preview: true).or(dossiers.visible_by_administration)
.order(Arel.sql("CASE WHEN user_id = #{user.id} THEN 1 ELSE 0 END DESC,
CASE WHEN state = 'accepte' THEN 1 ELSE 0 END DESC,
- CASE WHEN for_procedure_preview = True THEN 1 ELSE 0 END DESC")) \
+ CASE WHEN state = 'brouillon' THEN 0 ELSE 1 END DESC,
+ CASE WHEN for_procedure_preview = True THEN 1 ELSE 0 END DESC,
+ id DESC")) \
.first
end
@@ -1021,22 +866,60 @@ class Procedure < ApplicationRecord
update!(closing_reason: nil, closing_details: nil, replaced_by_procedure_id: nil, closing_notification_brouillon: false, closing_notification_en_cours: false)
end
+ def monavis_embed_html_source(source)
+ monavis_embed.gsub('nd_source=button', "nd_source=#{source}").gsub(' ['notifications'],
- 'self' => ['id', 'state']
- }
-
- TABLE = 'table'
- COLUMN = 'column'
- ORDER = 'order'
-
- SLASH = '/'
- TYPE_DE_CHAMP = 'type_de_champ'
- TYPE_DE_CHAMP_PRIVATE = 'type_de_champ_private'
-
- FILTERS_VALUE_MAX_LENGTH = 100
+ self.ignored_columns += ["displayed_fields", "filters", "sort"]
belongs_to :assign_to, optional: false
has_many :exports, dependent: :destroy
delegate :procedure, :instructeur, to: :assign_to
- validate :check_allowed_displayed_fields
- validate :check_allowed_sort_column
- validate :check_allowed_sort_order
- validate :check_allowed_filter_columns
- validate :check_filters_max_length
+ attribute :displayed_columns, :column, array: true
- def self_fields
- [
- field_hash('self', 'created_at', type: :date),
- field_hash('self', 'updated_at', type: :date),
- field_hash('self', 'depose_at', type: :date),
- field_hash('self', 'en_construction_at', type: :date),
- field_hash('self', 'en_instruction_at', type: :date),
- field_hash('self', 'processed_at', type: :date),
- *sva_svr_fields(for_filters: true),
- field_hash('self', 'updated_since', type: :date, virtual: true),
- field_hash('self', 'depose_since', type: :date, virtual: true),
- field_hash('self', 'en_construction_since', type: :date, virtual: true),
- field_hash('self', 'en_instruction_since', type: :date, virtual: true),
- field_hash('self', 'processed_since', type: :date, virtual: true),
- field_hash('self', 'state', type: :enum, scope: 'instructeurs.dossiers.filterable_state', virtual: true)
- ].compact_blank
+ attribute :sorted_column, :sorted_column
+ def sorted_column = super || procedure.default_sorted_column # Dummy override to set default value
+
+ attribute :a_suivre_filters, :filtered_column, array: true
+ attribute :suivis_filters, :filtered_column, array: true
+ attribute :traites_filters, :filtered_column, array: true
+ attribute :tous_filters, :filtered_column, array: true
+ attribute :supprimes_filters, :filtered_column, array: true
+ attribute :supprimes_recemment_filters, :filtered_column, array: true
+ attribute :expirant_filters, :filtered_column, array: true
+ attribute :archives_filters, :filtered_column, array: true
+
+ before_create { self.displayed_columns = procedure.default_displayed_columns }
+
+ validates_associated :displayed_columns, :sorted_column, :a_suivre_filters, :suivis_filters,
+ :traites_filters, :tous_filters, :supprimes_filters, :expirant_filters, :archives_filters
+
+ def filters_for(statut)
+ send(filters_name_for(statut))
end
- def fields
- fields = self_fields
-
- fields.push(
- field_hash('user', 'email', type: :text),
- field_hash('followers_instructeurs', 'email', type: :text),
- field_hash('groupe_instructeur', 'id', type: :enum),
- field_hash('avis', 'question_answer', filterable: false)
- )
-
- if procedure.for_individual
- fields.push(
- field_hash("individual", "prenom", type: :text),
- field_hash("individual", "nom", type: :text),
- field_hash("individual", "gender", type: :text)
- )
- end
-
- if !procedure.for_individual
- fields.push(
- field_hash('etablissement', 'entreprise_siren', type: :text),
- field_hash('etablissement', 'entreprise_forme_juridique', type: :text),
- field_hash('etablissement', 'entreprise_nom_commercial', type: :text),
- field_hash('etablissement', 'entreprise_raison_sociale', type: :text),
- field_hash('etablissement', 'entreprise_siret_siege_social', type: :text),
- field_hash('etablissement', 'entreprise_date_creation', type: :date)
- )
-
- fields.push(
- field_hash('etablissement', 'siret', type: :text),
- field_hash('etablissement', 'libelle_naf', type: :text),
- field_hash('etablissement', 'code_postal', type: :text)
- )
- end
-
- fields.concat(procedure.types_de_champ_for_procedure_presentation
- .pluck(:type_champ, :libelle, :private, :stable_id)
- .reject { |(type_champ)| type_champ == TypeDeChamp.type_champs.fetch(:repetition) }
- .map do |(type_champ, libelle, is_private, stable_id)|
- if is_private
- field_hash_for_type_de_champ_private(type_champ, libelle, stable_id)
- else
- field_hash_for_type_de_champ_public(type_champ, libelle, stable_id)
- end
- end)
-
- fields
- end
-
- def displayable_fields_for_select
- [
- fields.reject { |field| field['virtual'] }
- .map { |field| [field['label'], field_id(field)] },
- displayed_fields.map { |field| field_id(field) }
- ]
- end
-
- def filterable_fields_options
- fields.filter_map do |field|
- next if field['filterable'] == false
-
- [field['label'], field_id(field)]
- end
- end
+ def filters_name_for(statut) = statut.tr('-', '_').then { "#{_1}_filters" }
def displayed_fields_for_headers
- [
- field_hash('self', 'id', classname: 'number-col'),
- *displayed_fields,
- field_hash('self', 'state', classname: 'state-col'),
- *sva_svr_fields
+ columns = [
+ procedure.dossier_id_column,
+ *displayed_columns,
+ procedure.dossier_state_column
]
- end
-
- def sva_svr_fields(for_filters: false)
- return if !procedure.sva_svr_enabled?
-
- i18n_scope = [:activerecord, :attributes, :procedure_presentation, :fields, :self]
-
- fields = []
- fields << field_hash('self', 'sva_svr_decision_on',
- type: :date,
- label: I18n.t("#{procedure.sva_svr_decision}_decision_on", scope: i18n_scope),
- classname: for_filters ? '' : 'sva-col')
-
- if for_filters
- fields << field_hash('self', 'sva_svr_decision_before',
- label: I18n.t("#{procedure.sva_svr_decision}_decision_before", scope: i18n_scope),
- type: :date, virtual: true)
- end
-
- fields
- end
-
- def sorted_ids(dossiers, count)
- table, column, order = sort.values_at(TABLE, COLUMN, 'order')
-
- case table
- when 'notifications'
- dossiers_id_with_notification = dossiers.merge(instructeur.followed_dossiers).with_notifications.ids
- if order == 'desc'
- dossiers_id_with_notification +
- (dossiers.order('dossiers.updated_at desc').ids - dossiers_id_with_notification)
- else
- (dossiers.order('dossiers.updated_at asc').ids - dossiers_id_with_notification) +
- dossiers_id_with_notification
- end
- when TYPE_DE_CHAMP
- ids = dossiers
- .with_type_de_champ(column)
- .order("champs.value #{order}")
- .pluck(:id)
- if ids.size != count
- rest = dossiers.where.not(id: ids).order(id: order).pluck(:id)
- order == 'asc' ? ids + rest : rest + ids
- else
- ids
- end
- when TYPE_DE_CHAMP_PRIVATE
- ids = dossiers
- .with_type_de_champ(column)
- .order("champs.value #{order}")
- .pluck(:id)
- if ids.size != count
- rest = dossiers.where.not(id: ids).order(id: order).pluck(:id)
- order == 'asc' ? ids + rest : rest + ids
- else
- ids
- end
- when 'followers_instructeurs'
- assert_supported_column(table, column)
- # LEFT OUTER JOIN allows to keep dossiers without assigned instructeurs yet
- dossiers
- .includes(:followers_instructeurs)
- .joins('LEFT OUTER JOIN users instructeurs_users ON instructeurs_users.id = instructeurs.user_id')
- .order("instructeurs_users.email #{order}")
- .pluck(:id)
- .uniq
- when 'avis'
- dossiers.includes(table)
- .order("#{self.class.sanitized_column(table, column)} #{order}")
- .pluck(:id)
- .uniq
- when 'self', 'user', 'individual', 'etablissement', 'groupe_instructeur'
- (table == 'self' ? dossiers : dossiers.includes(table))
- .order("#{self.class.sanitized_column(table, column)} #{order}")
- .pluck(:id)
- end
- end
-
- def filtered_ids(dossiers, statut)
- filters.fetch(statut)
- .group_by { |filter| filter.values_at(TABLE, COLUMN) }
- .map do |(table, column), filters|
- values = filters.pluck('value')
- value_column = filters.pluck('value_column').compact.first || :value
- case table
- when 'self'
- field = self_fields.find { |h| h['column'] == column }
- if field['type'] == :date
- dates = values
- .filter_map { |v| Time.zone.parse(v).beginning_of_day rescue nil }
-
- dossiers.filter_by_datetimes(column, dates)
- elsif field['column'] == "state" && values.include?("pending_correction")
- dossiers.joins(:corrections).where(corrections: DossierCorrection.pending)
- elsif field['column'] == "state" && values.include?("en_construction")
- dossiers.where("dossiers.#{column} IN (?)", values).includes(:corrections).where.not(corrections: DossierCorrection.pending)
- else
- dossiers.where("dossiers.#{column} IN (?)", values)
- end
- when TYPE_DE_CHAMP
- dossiers.with_type_de_champ(column)
- .filter_ilike(:champs, value_column, values)
- when TYPE_DE_CHAMP_PRIVATE
- dossiers.with_type_de_champ(column)
- .filter_ilike(:champs, value_column, values)
- when 'etablissement'
- if column == 'entreprise_date_creation'
- dates = values
- .filter_map { |v| v.to_date rescue nil }
-
- dossiers
- .includes(table)
- .where(table.pluralize => { column => dates })
- else
- dossiers
- .includes(table)
- .filter_ilike(table, column, values)
- end
- when 'followers_instructeurs'
- assert_supported_column(table, column)
- dossiers
- .includes(:followers_instructeurs)
- .joins('INNER JOIN users instructeurs_users ON instructeurs_users.id = instructeurs.user_id')
- .filter_ilike('instructeurs_users', :email, values)
- when 'user', 'individual', 'avis'
- dossiers
- .includes(table)
- .filter_ilike(table, column, values)
- when 'groupe_instructeur'
- assert_supported_column(table, column)
- if column == 'label'
- dossiers
- .joins(:groupe_instructeur)
- .filter_ilike(table, column, values)
- else
- dossiers
- .joins(:groupe_instructeur)
- .where(groupe_instructeur_id: values)
- end
- end.pluck(:id)
- end.reduce(:&)
- end
-
- def filtered_sorted_ids(dossiers, statut, count: nil)
- dossiers_by_statut = dossiers.by_statut(statut, instructeur)
- dossiers_sorted_ids = self.sorted_ids(dossiers_by_statut, count || dossiers_by_statut.size)
-
- if filters[statut].present?
- dossiers_sorted_ids.intersection(filtered_ids(dossiers_by_statut, statut))
- else
- dossiers_sorted_ids
- end
- end
-
- def human_value_for_filter(filter)
- if [TYPE_DE_CHAMP, TYPE_DE_CHAMP_PRIVATE].include?(filter[TABLE])
- find_type_de_champ(filter[COLUMN]).dynamic_type.filter_to_human(filter['value'])
- elsif filter['column'] == 'state'
- if filter['value'] == 'pending_correction'
- Dossier.human_attribute_name("pending_correction.for_instructeur")
- else
- Dossier.human_attribute_name("state.#{filter['value']}")
- end
- elsif filter['table'] == 'groupe_instructeur' && filter['column'] == 'id'
- instructeur.groupe_instructeurs
- .find { _1.id == filter['value'].to_i }&.label || filter['value']
- else
- field = find_field(filter[TABLE], filter[COLUMN])
-
- if field["type"] == :date
- parsed_date = safe_parse_date(filter['value'])
-
- return parsed_date.present? ? I18n.l(parsed_date) : nil
- end
-
- filter['value']
- end
- end
-
- def safe_parse_date(string)
- Date.parse(string)
- rescue Date::Error
- nil
- end
-
- def add_filter(statut, field, value)
- if value.present?
- table, column = field.split(SLASH)
- label, value_column = find_field(table, column).values_at('label', 'value_column')
-
- case table
- when TYPE_DE_CHAMP, TYPE_DE_CHAMP_PRIVATE
- value = find_type_de_champ(column).dynamic_type.human_to_filter(value)
- end
-
- updated_filters = filters.dup
- updated_filters[statut] << {
- 'label' => label,
- TABLE => table,
- COLUMN => column,
- 'value_column' => value_column,
- 'value' => value
- }
-
- update(filters: updated_filters)
- end
- end
-
- def remove_filter(statut, field, value)
- table, column = field.split(SLASH)
-
- updated_filters = filters.dup
- updated_filters[statut] = filters[statut].reject do |filter|
- filter.values_at(TABLE, COLUMN, 'value') == [table, column, value]
- end
-
- update!(filters: updated_filters)
- end
-
- def update_displayed_fields(values)
- if values.nil?
- values = []
- end
-
- fields = values.map { |value| find_field(*value.split(SLASH)) }
-
- update!(displayed_fields: fields)
-
- if !values.include?(field_id(sort))
- update!(sort: Procedure.default_sort)
- end
- end
-
- def update_sort(table, column, order)
- update!(sort: {
- TABLE => table,
- COLUMN => column,
- ORDER => order.presence || opposite_order_for(table, column)
- })
- end
-
- def opposite_order_for(table, column)
- if sort.values_at(TABLE, COLUMN) == [table, column]
- sort['order'] == 'asc' ? 'desc' : 'asc'
- elsif [table, column] == ["notifications", "notifications"]
- 'desc' # default order for notifications
- else
- 'asc'
- end
- end
-
- def snapshot
- slice(:filters, :sort, :displayed_fields)
- end
-
- def field_type(field_id)
- find_field(*field_id.split(SLASH))['type']
- end
-
- def field_enum(field_id)
- field = find_field(*field_id.split(SLASH))
- if field['scope'].present?
- I18n.t(field['scope']).map(&:to_a).map(&:reverse)
- elsif field['table'] == 'groupe_instructeur'
- instructeur.groupe_instructeurs.filter_map do
- if _1.procedure_id == procedure.id
- [_1.label, _1.id]
- end
- end
- else
- find_type_de_champ(field['column']).options_for_select
- end
- end
-
- def sortable?(field)
- sort['table'] == field['table'] && sort['column'] == field['column']
- end
-
- def aria_sort(order, field)
- if sortable?(field)
- if order == 'asc'
- { "aria-sort": "ascending" }
- elsif order == 'desc'
- { "aria-sort": "descending" }
- end
- else
- {}
- end
- end
-
- private
-
- def field_id(field)
- field.values_at(TABLE, COLUMN).join(SLASH)
- end
-
- def find_field(table, column)
- fields.find { |field| field.values_at(TABLE, COLUMN) == [table, column] }
- end
-
- def find_type_de_champ(column)
- TypeDeChamp
- .joins(:revision_types_de_champ)
- .where(revision_types_de_champ: { revision_id: procedure.revisions })
- .order(created_at: :desc)
- .find_by(stable_id: column)
- end
-
- def check_allowed_displayed_fields
- displayed_fields.each do |field|
- check_allowed_field(:displayed_fields, field)
- end
- end
-
- def check_allowed_sort_column
- check_allowed_field(:sort, sort, EXTRA_SORT_COLUMNS)
- end
-
- def check_allowed_sort_order
- order = sort['order']
- if !["asc", "desc"].include?(order)
- errors.add(:sort, "#{order} n’est pas une ordre permis")
- end
- end
-
- def check_allowed_filter_columns
- filters.each do |key, columns|
- return true if key == 'migrated'
- columns.each do |column|
- check_allowed_field(:filters, column)
- end
- end
- end
-
- def check_allowed_field(kind, field, extra_columns = {})
- table, column = field.values_at(TABLE, COLUMN)
- if !valid_column?(table, column, extra_columns)
- errors.add(kind, "#{table}.#{column} n’est pas une colonne permise")
- end
- end
-
- def check_filters_max_length
- filters.values.flatten.each do |filter|
- next if !filter.is_a?(Hash)
- next if filter['value']&.length.to_i <= FILTERS_VALUE_MAX_LENGTH
-
- errors.add(:base, "Le filtre #{filter['label']} est trop long (maximum: #{FILTERS_VALUE_MAX_LENGTH} caractères)")
- end
- end
-
- def field_hash(table, column, label: nil, classname: '', virtual: false, type: :text, scope: '', value_column: :value, filterable: true)
- {
- 'label' => label || I18n.t(column, scope: [:activerecord, :attributes, :procedure_presentation, :fields, table]),
- TABLE => table,
- COLUMN => column,
- 'classname' => classname,
- 'virtual' => virtual,
- 'type' => type,
- 'scope' => scope,
- 'value_column' => value_column,
- 'filterable' => filterable
- }
- end
-
- def field_hash_for_type_de_champ_public(type_champ, libelle, stable_id)
- field_hash(TYPE_DE_CHAMP, stable_id.to_s,
- label: libelle,
- type: TypeDeChamp.filter_hash_type(type_champ),
- value_column: TypeDeChamp.filter_hash_value_column(type_champ))
- end
-
- def field_hash_for_type_de_champ_private(type_champ, libelle, stable_id)
- field_hash(TYPE_DE_CHAMP_PRIVATE, stable_id.to_s,
- label: libelle,
- type: TypeDeChamp.filter_hash_type(type_champ),
- value_column: TypeDeChamp.filter_hash_value_column(type_champ))
- end
-
- def valid_column?(table, column, extra_columns = {})
- valid_columns_for_table(table).include?(column) ||
- extra_columns[table]&.include?(column)
- end
-
- def valid_columns_for_table(table)
- @column_whitelist ||= fields
- .group_by { |field| field[TABLE] }
- .transform_values { |fields| Set.new(fields.pluck(COLUMN)) }
-
- @column_whitelist[table] || []
- end
-
- def self.sanitized_column(association, column)
- table = if association == 'self'
- Dossier.table_name
- elsif (association_reflection = Dossier.reflect_on_association(association))
- association_reflection.klass.table_name
- else
- # Allow filtering on a joined table alias (which doesn’t exist
- # in the ActiveRecord domain).
- association
- end
-
- [table, column]
- .map { |name| ActiveRecord::Base.connection.quote_column_name(name) }
- .join('.')
- end
-
- def assert_supported_column(table, column)
- if table == 'followers_instructeurs' && column != 'email'
- raise ArgumentError, 'Table `followers_instructeurs` only supports the `email` column.'
- end
- if table == 'groupe_instructeur' && (column != 'label' && column != 'id')
- raise ArgumentError, 'Table `groupe_instructeur` only supports the `label` or `id` column.'
- end
+ columns.concat(procedure.sva_svr_columns.filter(&:displayable)) if procedure.sva_svr_enabled?
+ columns
end
end
diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb
index c8daa1bec..6ed3cce75 100644
--- a/app/models/procedure_revision.rb
+++ b/app/models/procedure_revision.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class ProcedureRevision < ApplicationRecord
+ include Logic
self.implicit_order_column = :created_at
belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false
belongs_to :dossier_submitted_message, inverse_of: :revisions, optional: true, dependent: :destroy
@@ -17,21 +20,22 @@ class ProcedureRevision < ApplicationRecord
scope :ordered, -> { order(:created_at) }
- validate :conditions_are_valid?
- validate :header_sections_are_valid?
- validate :expressions_regulieres_are_valid?
+ validates :ineligibilite_message, presence: true, if: -> { ineligibilite_enabled? }
delegate :path, to: :procedure, prefix: true
- def build_champs_public
- # reload: it can be out of sync in test if some tdcs are added wihtout using add_tdc
- types_de_champ_public.reload.map(&:build_champ)
- end
+ validate :ineligibilite_rules_are_valid?,
+ on: [:ineligibilite_rules_editor, :publication]
+ validates :ineligibilite_message,
+ presence: true,
+ if: -> { ineligibilite_enabled? },
+ on: [:ineligibilite_rules_editor, :publication]
+ validates :ineligibilite_rules,
+ presence: true,
+ if: -> { ineligibilite_enabled? },
+ on: [:ineligibilite_rules_editor, :publication]
- def build_champs_private
- # reload: it can be out of sync in test if some tdcs are added wihtout using add_tdc
- types_de_champ_private.reload.map(&:build_champ)
- end
+ serialize :ineligibilite_rules, LogicSerializer
def add_type_de_champ(params)
parent_stable_id = params.delete(:parent_stable_id)
@@ -140,42 +144,39 @@ class ProcedureRevision < ApplicationRecord
!draft?
end
- def different_from?(revision)
- revision_types_de_champ != revision.revision_types_de_champ
- end
-
- def compare(revision)
+ def compare_types_de_champ(revision)
changes = []
changes += compare_revision_types_de_champ(revision_types_de_champ, revision.revision_types_de_champ)
changes
end
+ def compare_ineligibilite_rules(revision)
+ changes = []
+ changes += compare_revision_ineligibilite_rules(revision)
+ changes
+ end
+
def dossier_for_preview(user)
dossier = Dossier
.create_with(autorisation_donnees: true)
.find_or_initialize_by(revision: self, user: user, for_procedure_preview: true, state: Dossier.states.fetch(:brouillon))
if dossier.new_record?
- dossier.build_default_individual
+ dossier.build_default_values
dossier.save!
end
dossier
end
- def types_de_champ_for(scope: nil, root: false)
- # We return an unordered collection
- return types_de_champ if !root && scope.nil?
- return types_de_champ.filter { scope == :public ? _1.public? : _1.private? } if !root
-
- # We return an ordered collection
+ def types_de_champ_for(scope: nil)
case scope
when :public
- types_de_champ_public
+ types_de_champ.filter(&:public?)
when :private
- types_de_champ_private
+ types_de_champ.filter(&:private?)
else
- types_de_champ_public + types_de_champ_private
+ types_de_champ
end
end
@@ -203,11 +204,6 @@ class ProcedureRevision < ApplicationRecord
.find { _1.type_de_champ_id == tdc.id }.parent&.type_de_champ
end
- def child?(tdc)
- revision_types_de_champ
- .find { _1.type_de_champ_id == tdc.id }.child?
- end
-
def remove_children_of(tdc)
children_of(tdc).each do |child|
remove_type_de_champ(child.stable_id)
@@ -234,7 +230,7 @@ class ProcedureRevision < ApplicationRecord
end
def coordinate_for(tdc)
- revision_types_de_champ.find_by!(type_de_champ: tdc)
+ revision_types_de_champ.find { _1.stable_id == tdc.stable_id }
end
def carte?
@@ -251,8 +247,12 @@ class ProcedureRevision < ApplicationRecord
[coordinate, coordinate&.type_de_champ]
end
- def routable_types_de_champ
- types_de_champ_public.filter(&:routable?)
+ def simple_routable_types_de_champ
+ types_de_champ_public.filter(&:simple_routable?)
+ end
+
+ def conditionable_types_de_champ
+ types_de_champ_for(scope: :public).filter(&:conditionable?)
end
private
@@ -322,6 +322,29 @@ class ProcedureRevision < ApplicationRecord
end
end
+ def compare_revision_ineligibilite_rules(new_revision)
+ from_ineligibilite_rules = ineligibilite_rules
+ to_ineligibilite_rules = new_revision.ineligibilite_rules
+ changes = []
+
+ if from_ineligibilite_rules.present? && to_ineligibilite_rules.blank?
+ changes << ProcedureRevisionChange::RemoveEligibiliteRuleChange
+ end
+ if from_ineligibilite_rules.blank? && to_ineligibilite_rules.present?
+ changes << ProcedureRevisionChange::AddEligibiliteRuleChange
+ end
+ if from_ineligibilite_rules != to_ineligibilite_rules
+ changes << ProcedureRevisionChange::UpdateEligibiliteRuleChange
+ end
+ if ineligibilite_message != new_revision.ineligibilite_message
+ changes << ProcedureRevisionChange::UpdateEligibiliteMessageChange
+ end
+ if ineligibilite_enabled != new_revision.ineligibilite_enabled
+ changes << (new_revision.ineligibilite_enabled ? ProcedureRevisionChange::EligibiliteEnabledChange : ProcedureRevisionChange::EligibiliteDisabledChange)
+ end
+ changes.map { _1.new(self, new_revision) }
+ end
+
def compare_type_de_champ(from_type_de_champ, to_type_de_champ, from_coordinates, to_coordinates)
changes = []
if from_type_de_champ.type_champ != to_type_de_champ.type_champ
@@ -368,12 +391,12 @@ class ProcedureRevision < ApplicationRecord
to_type_de_champ.condition&.to_s(to_coordinates.map(&:type_de_champ)))
end
- if to_type_de_champ.drop_down_list?
- if from_type_de_champ.drop_down_list_options != to_type_de_champ.drop_down_list_options
+ if to_type_de_champ.any_drop_down_list?
+ if from_type_de_champ.drop_down_options != to_type_de_champ.drop_down_options
changes << ProcedureRevisionChange::UpdateChamp.new(from_type_de_champ,
:drop_down_options,
- from_type_de_champ.drop_down_list_options,
- to_type_de_champ.drop_down_list_options)
+ from_type_de_champ.drop_down_options,
+ to_type_de_champ.drop_down_options)
end
if to_type_de_champ.linked_drop_down_list?
if from_type_de_champ.drop_down_secondary_libelle != to_type_de_champ.drop_down_secondary_libelle
@@ -402,7 +425,7 @@ class ProcedureRevision < ApplicationRecord
from_type_de_champ.carte_optional_layers,
to_type_de_champ.carte_optional_layers)
end
- elsif to_type_de_champ.piece_justificative?
+ elsif to_type_de_champ.piece_justificative_or_titre_identite?
if from_type_de_champ.checksum_for_attachment(:piece_justificative_template) != to_type_de_champ.checksum_for_attachment(:piece_justificative_template)
changes << ProcedureRevisionChange::UpdateChamp.new(from_type_de_champ,
:piece_justificative_template,
@@ -417,7 +440,7 @@ class ProcedureRevision < ApplicationRecord
to_type_de_champ.filename_for_attachement(:notice_explicative))
end
elsif to_type_de_champ.textarea?
- if from_type_de_champ.character_limit != to_type_de_champ.character_limit
+ if from_type_de_champ.character_limit.presence != to_type_de_champ.character_limit.presence
changes << ProcedureRevisionChange::UpdateChamp.new(from_type_de_champ,
:character_limit,
from_type_de_champ.character_limit,
@@ -446,6 +469,13 @@ class ProcedureRevision < ApplicationRecord
changes
end
+ def ineligibilite_rules_are_valid?
+ if ineligibilite_rules
+ ineligibilite_rules.errors(types_de_champ_for(scope: :public).to_a)
+ .each { errors.add(:ineligibilite_rules, :invalid) }
+ end
+ end
+
def replace_type_de_champ_by_clone(coordinate)
cloned_type_de_champ = coordinate.type_de_champ.deep_clone do |original, kopy|
ClonePiecesJustificativesService.clone_attachments(original, kopy)
@@ -453,48 +483,4 @@ class ProcedureRevision < ApplicationRecord
coordinate.update!(type_de_champ: cloned_type_de_champ)
cloned_type_de_champ
end
-
- def conditions_are_valid?
- public_tdcs = types_de_champ_public.to_a
- .flat_map { _1.repetition? ? children_of(_1) : _1 }
-
- public_tdcs
- .map.with_index
- .filter_map { |tdc, i| tdc.condition? ? [tdc, i] : nil }
- .map do |tdc, i|
- [tdc, tdc.condition.errors(public_tdcs.take(i))]
- end
- .filter { |_tdc, errors| errors.present? }
- .each { |tdc, message| errors.add(:condition, message, type_de_champ: tdc) }
- end
-
- def header_sections_are_valid?
- public_tdcs = types_de_champ_public.to_a
-
- root_tdcs_errors = errors_for_header_sections_order(public_tdcs)
- repetition_tdcs_errors = public_tdcs
- .filter_map { _1.repetition? ? children_of(_1) : nil }
- .map { errors_for_header_sections_order(_1) }
-
- repetition_tdcs_errors + root_tdcs_errors
- end
-
- def expressions_regulieres_are_valid?
- types_de_champ_public.to_a
- .flat_map { _1.repetition? ? children_of(_1) : _1 }
- .each do |tdc|
- if tdc.expression_reguliere? && tdc.invalid_regexp?
- errors.add(:expression_reguliere, type_de_champ: tdc)
- end
- end
- end
-
- def errors_for_header_sections_order(tdcs)
- tdcs
- .map.with_index
- .filter_map { |tdc, i| tdc.header_section? ? [tdc, i] : nil }
- .map { |tdc, i| [tdc, tdc.check_coherent_header_level(tdcs.take(i))] }
- .filter { |_tdc, errors| errors.present? }
- .each { |tdc, message| errors.add(:header_section, message, type_de_champ: tdc) }
- end
end
diff --git a/app/models/procedure_revision_change.rb b/app/models/procedure_revision_change.rb
index fc412cc26..ddd52a7d0 100644
--- a/app/models/procedure_revision_change.rb
+++ b/app/models/procedure_revision_change.rb
@@ -1,17 +1,21 @@
+# frozen_string_literal: true
+
class ProcedureRevisionChange
- attr_reader :type_de_champ
- def initialize(type_de_champ)
- @type_de_champ = type_de_champ
+ class TypeDeChange
+ attr_reader :type_de_champ
+ def initialize(type_de_champ)
+ @type_de_champ = type_de_champ
+ end
+
+ def label = @type_de_champ.libelle
+ def stable_id = @type_de_champ.stable_id
+ def private? = @type_de_champ.private?
+ def child? = @type_de_champ.child?
+
+ def to_h = { op:, stable_id:, label:, private: private? }
end
- def label = @type_de_champ.libelle
- def stable_id = @type_de_champ.stable_id
- def private? = @type_de_champ.private?
- def child? = @type_de_champ.child?
-
- def to_h = { op:, stable_id:, label:, private: private? }
-
- class AddChamp < ProcedureRevisionChange
+ class AddChamp < TypeDeChange
def initialize(type_de_champ)
super(type_de_champ)
end
@@ -23,7 +27,7 @@ class ProcedureRevisionChange
def to_h = super.merge(mandatory: mandatory?)
end
- class RemoveChamp < ProcedureRevisionChange
+ class RemoveChamp < TypeDeChange
def initialize(type_de_champ)
super(type_de_champ)
end
@@ -32,7 +36,7 @@ class ProcedureRevisionChange
def can_rebase?(dossier = nil) = true
end
- class MoveChamp < ProcedureRevisionChange
+ class MoveChamp < TypeDeChange
attr_reader :from, :to
def initialize(type_de_champ, from, to)
@@ -46,7 +50,7 @@ class ProcedureRevisionChange
def to_h = super.merge(from:, to:)
end
- class UpdateChamp < ProcedureRevisionChange
+ class UpdateChamp < TypeDeChange
attr_reader :attribute, :from, :to
def initialize(type_de_champ, attribute, from, to)
@@ -75,4 +79,48 @@ class ProcedureRevisionChange
end
end
end
+
+ class EligibiliteRulesChange
+ attr_reader :previous_revision, :new_revision
+ def initialize(previous_revision, new_revision)
+ @previous_revision = previous_revision
+ @new_revision = new_revision
+ @previous_ineligibilite_rules = @previous_revision.ineligibilite_rules
+ @new_ineligibilite_rules = @new_revision.ineligibilite_rules
+ end
+
+ def i18n_params
+ {
+ previous_condition: @previous_ineligibilite_rules&.to_s(previous_revision.types_de_champ.filter { @previous_ineligibilite_rules.sources.include? _1.stable_id }),
+ new_condition: @new_ineligibilite_rules&.to_s(new_revision.types_de_champ.filter { @new_ineligibilite_rules.sources.include? _1.stable_id })
+ }
+ end
+ end
+
+ class AddEligibiliteRuleChange < EligibiliteRulesChange
+ def op = :add
+ end
+
+ class RemoveEligibiliteRuleChange < EligibiliteRulesChange
+ def op = :remove
+ end
+
+ class UpdateEligibiliteRuleChange < EligibiliteRulesChange
+ def op = :update
+ end
+
+ class EligibiliteEnabledChange < EligibiliteRulesChange
+ def op = :enabled
+ def i18n_params = {}
+ end
+
+ class EligibiliteDisabledChange < EligibiliteRulesChange
+ def op = :disabled
+ def i18n_params = {}
+ end
+
+ class UpdateEligibiliteMessageChange < EligibiliteRulesChange
+ def op = :message_updated
+ def i18n_params = { ineligibilite_message: @new_revision.ineligibilite_message }
+ end
end
diff --git a/app/models/procedure_revision_preloader.rb b/app/models/procedure_revision_preloader.rb
index bd85517ba..a5c35dac6 100644
--- a/app/models/procedure_revision_preloader.rb
+++ b/app/models/procedure_revision_preloader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProcedureRevisionPreloader
def initialize(revisions)
@revisions = revisions
diff --git a/app/models/procedure_revision_type_de_champ.rb b/app/models/procedure_revision_type_de_champ.rb
index c4842da20..3963ae630 100644
--- a/app/models/procedure_revision_type_de_champ.rb
+++ b/app/models/procedure_revision_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProcedureRevisionTypeDeChamp < ApplicationRecord
belongs_to :revision, class_name: 'ProcedureRevision'
belongs_to :type_de_champ
@@ -11,7 +13,7 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
scope :public_only, -> { joins(:type_de_champ).where(types_de_champ: { private: false }) }
scope :private_only, -> { joins(:type_de_champ).where(types_de_champ: { private: true }) }
- delegate :stable_id, :libelle, :description, :type_champ, :mandatory?, :private?, :to_typed_id, to: :type_de_champ
+ delegate :stable_id, :libelle, :description, :type_champ, :header_section?, :mandatory?, :private?, :to_typed_id, to: :type_de_champ
def child?
parent_id.present?
@@ -30,8 +32,8 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
end
def siblings
- if parent_id.present?
- revision.revision_types_de_champ.where(parent_id: parent_id).ordered
+ if child?
+ revision.revision_types_de_champ.where(parent_id:).ordered
elsif private?
revision.revision_types_de_champ_private
else
@@ -73,6 +75,10 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
end
def used_by_routing_rules?
- stable_id.in?(procedure.stable_ids_used_by_routing_rules)
+ procedure.used_by_routing_rules?(type_de_champ)
+ end
+
+ def used_by_ineligibilite_rules?
+ revision.ineligibilite_enabled? && stable_id.in?(revision.ineligibilite_rules&.sources || [])
end
end
diff --git a/app/models/procedure_tag.rb b/app/models/procedure_tag.rb
new file mode 100644
index 000000000..37d770b43
--- /dev/null
+++ b/app/models/procedure_tag.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class ProcedureTag < ApplicationRecord
+ has_and_belongs_to_many :procedures
+
+ validates :name, presence: true, uniqueness: { case_sensitive: false }
+end
diff --git a/app/models/procedures_filter.rb b/app/models/procedures_filter.rb
index c590cd355..a993494e8 100644
--- a/app/models/procedures_filter.rb
+++ b/app/models/procedures_filter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProceduresFilter
attr_reader :admin, :params
diff --git a/app/models/published_procedure.rb b/app/models/published_procedure.rb
new file mode 100644
index 000000000..3f55757a4
--- /dev/null
+++ b/app/models/published_procedure.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class PublishedProcedure
+ extend ActiveModel::Naming
+ extend ActiveModel::Translation
+end
diff --git a/app/models/release_note.rb b/app/models/release_note.rb
index a2a48f1f1..11a5a3ce3 100644
--- a/app/models/release_note.rb
+++ b/app/models/release_note.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ReleaseNote < ApplicationRecord
has_rich_text :body
diff --git a/app/models/routing_engine.rb b/app/models/routing_engine.rb
index 040becb72..6f2ceafab 100644
--- a/app/models/routing_engine.rb
+++ b/app/models/routing_engine.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module RoutingEngine
def self.compute(dossier, assignment_mode: DossierAssignment.modes.fetch(:auto))
return if dossier.forced_groupe_instructeur
matching_groupe = dossier.procedure.groupe_instructeurs.active.reject(&:invalid_rule?).find do |gi|
- gi.routing_rule&.compute(dossier.champs)
+ gi.routing_rule&.compute(dossier.filled_champs)
end
matching_groupe ||= dossier.procedure.defaut_groupe_instructeur
diff --git a/app/models/safe_mailer.rb b/app/models/safe_mailer.rb
index 9ccc83aef..668c57c66 100644
--- a/app/models/safe_mailer.rb
+++ b/app/models/safe_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SafeMailer < ApplicationRecord
before_create do
raise if SafeMailer.count == 1
diff --git a/app/models/service.rb b/app/models/service.rb
index 25f37ad3e..487bc15af 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -1,9 +1,15 @@
+# frozen_string_literal: true
+
class Service < ApplicationRecord
+ include PrefillableFromServicePublicConcern
+
has_many :procedures
belongs_to :administrateur, optional: false
scope :ordered, -> { order(nom: :asc) }
+ SIRET_TEST = '35600082800018'
+
enum type_organisme: {
administration_centrale: 'administration_centrale',
association: 'association',
@@ -18,6 +24,7 @@ class Service < ApplicationRecord
validates :nom, uniqueness: { scope: :administrateur, message: 'existe déjà' }
validates :organisme, presence: { message: 'doit être renseigné' }, allow_nil: false
validates :siret, siret_format: true
+ validates :siret, comparison: { other_than: SIRET_TEST, message: "n'est pas valide" }, on: :update
validates :type_organisme, presence: { message: 'doit être renseigné' }, allow_nil: false
validates :email, presence: { message: 'doit être renseigné' }, allow_nil: false
validates :telephone, phone: { possible: true, allow_blank: true }
diff --git a/app/models/siret.rb b/app/models/siret.rb
index 6aff5363c..8562fdf64 100644
--- a/app/models/siret.rb
+++ b/app/models/siret.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Siret
include ActiveModel::Model
include ActiveModel::Validations::Callbacks
diff --git a/app/models/sorted_column.rb b/app/models/sorted_column.rb
new file mode 100644
index 000000000..469c07559
--- /dev/null
+++ b/app/models/sorted_column.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class SortedColumn
+ # include validations to enable procedure_presentation.validate_associate,
+ # which enforces the deserialization of columns in the sorted_column attribute
+ # and raises an error if a column is not found
+ include ActiveModel::Validations
+
+ attr_reader :column, :order
+
+ def initialize(column:, order:)
+ @column = column
+ @order = order
+ end
+
+ def ascending? = @order == 'asc'
+
+ def opposite_order = ascending? ? 'desc' : 'asc'
+
+ def ==(other)
+ other&.column == column && other.order == order
+ end
+
+ def sort_by_notifications?
+ @column.notifications? && @order == 'desc'
+ end
+
+ def id
+ column.h_id.merge(order:).sort.to_json
+ end
+end
diff --git a/app/models/stat.rb b/app/models/stat.rb
index d4cd331a2..9cfc9f63c 100644
--- a/app/models/stat.rb
+++ b/app/models/stat.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Stat < ApplicationRecord
class << self
def update_stats
diff --git a/app/models/super_admin.rb b/app/models/super_admin.rb
index d6fa4a51f..b5c948b55 100644
--- a/app/models/super_admin.rb
+++ b/app/models/super_admin.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SuperAdmin < ApplicationRecord
include PasswordComplexityConcern
diff --git a/app/models/sva_svr_configuration.rb b/app/models/sva_svr_configuration.rb
index 24b7edc56..fc5fc61a9 100644
--- a/app/models/sva_svr_configuration.rb
+++ b/app/models/sva_svr_configuration.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SVASVRConfiguration
include ActiveModel::Model
include ActiveModel::Attributes
diff --git a/app/models/targeted_user_link.rb b/app/models/targeted_user_link.rb
index 2d3d53660..398a8b512 100644
--- a/app/models/targeted_user_link.rb
+++ b/app/models/targeted_user_link.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TargetedUserLink < ApplicationRecord
belongs_to :user, optional: true
belongs_to :target_model, polymorphic: true, optional: false
@@ -31,9 +33,11 @@ class TargetedUserLink < ApplicationRecord
url_helper.invite_path(invite, params: { email: invite.email })
when "avis"
avis = target_model
- avis.expert.user.active? ?
- url_helper.expert_avis_path(avis.procedure, avis) :
+ if avis.expert.user.active?
+ url_helper.expert_avis_path(avis.procedure, avis)
+ else
url_helper.sign_up_expert_avis_path(avis.procedure, avis, email: avis.expert.email)
+ end
end
end
diff --git a/app/models/team_account.rb b/app/models/team_account.rb
index 048019de0..4c705c81c 100644
--- a/app/models/team_account.rb
+++ b/app/models/team_account.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TeamAccount
extend ActiveModel::Naming
extend ActiveModel::Translation
diff --git a/app/models/traitement.rb b/app/models/traitement.rb
index efff0bbfd..0a3929b57 100644
--- a/app/models/traitement.rb
+++ b/app/models/traitement.rb
@@ -1,6 +1,7 @@
+# frozen_string_literal: true
+
class Traitement < ApplicationRecord
belongs_to :dossier, optional: false
-
scope :en_construction, -> { where(state: Dossier.states.fetch(:en_construction)) }
scope :en_instruction, -> { where(state: Dossier.states.fetch(:en_instruction)) }
scope :termine, -> { where(state: Dossier::TERMINE) }
diff --git a/app/models/trusted_device_token.rb b/app/models/trusted_device_token.rb
index ddb132961..0e02feab1 100644
--- a/app/models/trusted_device_token.rb
+++ b/app/models/trusted_device_token.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TrustedDeviceToken < ApplicationRecord
LOGIN_TOKEN_VALIDITY = 1.week
LOGIN_TOKEN_YOUTH = 15.minutes
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index b4ff3ac51..b380dc9fe 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -1,6 +1,6 @@
-class TypeDeChamp < ApplicationRecord
- self.ignored_columns += [:migrated_parent, :revision_id, :parent_id, :order_place]
+# frozen_string_literal: true
+class TypeDeChamp < ApplicationRecord
FILE_MAX_SIZE = 200.megabytes
FEATURE_FLAGS = {
engagement_juridique: :engagement_juridique_type_de_champ,
@@ -24,7 +24,6 @@ class TypeDeChamp < ApplicationRecord
TYPE_DE_CHAMP_TO_CATEGORIE = {
engagement_juridique: REFERENTIEL_EXTERNE,
-
header_section: STRUCTURE,
repetition: STRUCTURE,
dossier_link: STRUCTURE,
@@ -66,7 +65,7 @@ class TypeDeChamp < ApplicationRecord
expression_reguliere: STANDARD
}
- enum type_champs: {
+ enum type_champ: {
engagement_juridique: 'engagement_juridique',
header_section: 'header_section',
@@ -110,12 +109,14 @@ class TypeDeChamp < ApplicationRecord
expression_reguliere: 'expression_reguliere'
}
- ROUTABLE_TYPES = [
+ SIMPLE_ROUTABLE_TYPES = [
type_champs.fetch(:drop_down_list),
type_champs.fetch(:communes),
type_champs.fetch(:departements),
type_champs.fetch(:regions),
- type_champs.fetch(:epci)
+ type_champs.fetch(:pays),
+ type_champs.fetch(:epci),
+ type_champs.fetch(:address)
]
PRIVATE_ONLY_TYPES = [
@@ -140,13 +141,9 @@ class TypeDeChamp < ApplicationRecord
:header_section_level
has_many :revision_types_de_champ, -> { revision_ordered }, class_name: 'ProcedureRevisionTypeDeChamp', dependent: :destroy, inverse_of: :type_de_champ
- has_one :revision_type_de_champ, -> { revision_ordered }, class_name: 'ProcedureRevisionTypeDeChamp', inverse_of: false
has_many :revisions, -> { ordered }, through: :revision_types_de_champ
- has_one :revision, through: :revision_type_de_champ
- has_one :procedure, through: :revision
- delegate :estimated_fill_duration, :estimated_read_duration, :tags_for_template, :libelles_for_export, :libelle_for_export, :primary_options, :secondary_options, to: :dynamic_type
- delegate :used_by_routing_rules?, to: :revision_type_de_champ
+ delegate :estimated_fill_duration, :estimated_read_duration, :tags_for_template, :libelles_for_export, :libelle_for_export, :primary_options, :secondary_options, :columns, to: :dynamic_type
class WithIndifferentAccess
def self.load(options)
@@ -173,22 +170,13 @@ class TypeDeChamp < ApplicationRecord
scope :not_repetition, -> { where.not(type_champ: type_champs.fetch(:repetition)) }
scope :not_condition, -> { where(condition: nil) }
scope :fillable, -> { where.not(type_champ: [type_champs.fetch(:header_section), type_champs.fetch(:explication)]) }
+ scope :with_header_section, -> { where.not(type_champ: TypeDeChamp.type_champs[:explication]) }
scope :dubious, -> {
where("unaccent(types_de_champ.libelle) ~* unaccent(?)", DubiousProcedure.forbidden_regexp)
.where(type_champ: [TypeDeChamp.type_champs.fetch(:text), TypeDeChamp.type_champs.fetch(:textarea)])
}
- has_many :champ, inverse_of: :type_de_champ, dependent: :destroy do
- def build(params = {})
- super(params.merge(proxy_association.owner.params_for_champ))
- end
-
- def create(params = {})
- super(params.merge(proxy_association.owner.params_for_champ))
- end
- end
-
has_one_attached :piece_justificative_template
validates :piece_justificative_template, size: { less_than: FILE_MAX_SIZE }, on: :update
validates :piece_justificative_template, content_type: AUTHORIZED_CONTENT_TYPES, on: :update
@@ -219,13 +207,8 @@ class TypeDeChamp < ApplicationRecord
before_validation :check_mandatory
before_validation :normalize_libelle
- before_save :remove_piece_justificative_template, if: -> { type_champ_changed? }
- before_validation :remove_drop_down_list, if: -> { type_champ_changed? }
- before_save :remove_block, if: -> { type_champ_changed? }
-
- after_save if: -> { @remove_piece_justificative_template } do
- piece_justificative_template.purge_later
- end
+ before_save :remove_attachment, if: -> { type_champ_changed? }
+ before_validation :set_drop_down_list_options, if: -> { type_champ_changed? }
def valid?(context = nil)
super
@@ -250,18 +233,18 @@ class TypeDeChamp < ApplicationRecord
def params_for_champ
{
private: private?,
- type: "Champs::#{type_champ.classify}Champ",
+ type: self.class.type_champ_to_champ_class_name(type_champ),
stable_id:,
stream: 'main'
}
end
def build_champ(params = {})
- champ.build(params)
+ self.class.type_champ_to_champ_class_name(type_champ).constantize.new(params_for_champ.merge(params))
end
def check_mandatory
- if non_fillable?
+ if non_fillable? || private?
self.mandatory = false
else
true
@@ -307,10 +290,8 @@ class TypeDeChamp < ApplicationRecord
TypeDeChamp.type_champs.fetch(:repetition),
TypeDeChamp.type_champs.fetch(:multiple_drop_down_list),
TypeDeChamp.type_champs.fetch(:epci),
- TypeDeChamp.type_champs.fetch(:annuaire_education),
TypeDeChamp.type_champs.fetch(:dossier_link),
- TypeDeChamp.type_champs.fetch(:siret),
- TypeDeChamp.type_champs.fetch(:rna)
+ TypeDeChamp.type_champs.fetch(:siret)
])
end
@@ -342,129 +323,20 @@ class TypeDeChamp < ApplicationRecord
])
end
- def self.is_choice_type_from(type_champ)
- return false if type_champ == TypeDeChamp.type_champs.fetch(:linked_drop_down_list) # To remove when we stop using linked_drop_down_list
- TYPE_DE_CHAMP_TO_CATEGORIE[type_champ.to_sym] == CHOICE || type_champ.in?([TypeDeChamp.type_champs.fetch(:departements), TypeDeChamp.type_champs.fetch(:regions)])
- end
-
- def drop_down_list?
- type_champ.in?([
- TypeDeChamp.type_champs.fetch(:drop_down_list),
- TypeDeChamp.type_champs.fetch(:multiple_drop_down_list),
- TypeDeChamp.type_champs.fetch(:linked_drop_down_list)
- ])
- end
-
- def simple_drop_down_list?
- type_champ == TypeDeChamp.type_champs.fetch(:drop_down_list)
- end
-
- def multiple_drop_down_list?
- type_champ == TypeDeChamp.type_champs.fetch(:multiple_drop_down_list)
- end
-
- def linked_drop_down_list?
- type_champ == TypeDeChamp.type_champs.fetch(:linked_drop_down_list)
- end
-
- def yes_no?
- type_champ == TypeDeChamp.type_champs.fetch(:yes_no)
- end
-
- def block?
- type_champ == TypeDeChamp.type_champs.fetch(:repetition)
- end
-
- def header_section?
- type_champ == TypeDeChamp.type_champs.fetch(:header_section)
- end
-
def exclude_from_view?
type_champ == TypeDeChamp.type_champs.fetch(:explication)
end
- def explication?
- type_champ == TypeDeChamp.type_champs.fetch(:explication)
- end
-
- def repetition?
- type_champ == TypeDeChamp.type_champs.fetch(:repetition)
- end
-
- def dossier_link?
- type_champ == TypeDeChamp.type_champs.fetch(:dossier_link)
- end
-
- def siret?
- type_champ == TypeDeChamp.type_champs.fetch(:siret)
- end
-
- def piece_justificative?
- type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative) || type_champ == TypeDeChamp.type_champs.fetch(:titre_identite)
- end
-
- def legacy_number?
- type_champ == TypeDeChamp.type_champs.fetch(:number)
- end
-
- def textarea?
- type_champ == TypeDeChamp.type_champs.fetch(:textarea)
- end
-
- def titre_identite?
- type_champ == TypeDeChamp.type_champs.fetch(:titre_identite)
- end
-
- def carte?
- type_champ == TypeDeChamp.type_champs.fetch(:carte)
- end
-
- def cnaf?
- type_champ == TypeDeChamp.type_champs.fetch(:cnaf)
- end
-
- def rna?
- type_champ == TypeDeChamp.type_champs.fetch(:rna)
- end
-
- def dgfip?
- type_champ == TypeDeChamp.type_champs.fetch(:dgfip)
- end
-
- def pole_emploi?
- type_champ == TypeDeChamp.type_champs.fetch(:pole_emploi)
- end
-
- def departement?
- type_champ == TypeDeChamp.type_champs.fetch(:departements)
- end
-
- def region?
- type_champ == TypeDeChamp.type_champs.fetch(:regions)
- end
-
- def mesri?
- type_champ == TypeDeChamp.type_champs.fetch(:mesri)
- end
-
- def datetime?
- type_champ == TypeDeChamp.type_champs.fetch(:datetime)
- end
-
- def checkbox?
- type_champ == TypeDeChamp.type_champs.fetch(:checkbox)
- end
-
- def expression_reguliere?
- type_champ == TypeDeChamp.type_champs.fetch(:expression_reguliere)
- end
-
def public?
!private?
end
- def self.type_champ_to_class_name(type_champ)
- "TypesDeChamp::#{type_champ.classify}TypeDeChamp"
+ def in_revision?(revision)
+ revision.types_de_champ.any? { _1.stable_id == stable_id }
+ end
+
+ def child?(revision)
+ revision.revision_types_de_champ.find { _1.stable_id == stable_id }&.child?
end
def filename_for_attachement(attachment_sym)
@@ -481,16 +353,20 @@ class TypeDeChamp < ApplicationRecord
end
end
- def drop_down_list_value
- if drop_down_list_options.present?
- drop_down_list_options.reject(&:empty?).join("\r\n")
- else
- ''
- end
+ def drop_down_options
+ Array.wrap(super)
end
- def drop_down_list_value=(value)
- self.drop_down_options = parse_drop_down_list_value(value)
+ def drop_down_options_from_text=(text)
+ self.drop_down_options = text.to_s.lines.map(&:strip).reject(&:empty?)
+ end
+
+ def drop_down_options_with_other
+ if drop_down_other?
+ drop_down_options + [[I18n.t('shared.champs.drop_down_list.other'), Champs::DropDownListChamp::OTHER]]
+ else
+ drop_down_options
+ end
end
def header_section_level_value
@@ -509,15 +385,15 @@ class TypeDeChamp < ApplicationRecord
end
def check_coherent_header_level(upper_tdcs)
- errs = []
previous_level = previous_section_level(upper_tdcs)
-
current_level = header_section_level_value.to_i
+
difference = current_level - previous_level
if current_level > previous_level && difference != 1
- errs << I18n.t('activerecord.errors.type_de_champ.attributes.header_section_level.gap_error', level: current_level - previous_level - 1)
+ I18n.t('activerecord.errors.type_de_champ.attributes.header_section_level.gap_error', level: current_level - previous_level - 1)
+ else
+ nil
end
- errs
end
def current_section_level(revision)
@@ -528,6 +404,7 @@ class TypeDeChamp < ApplicationRecord
def level_for_revision(revision)
rtdc = revision.revision_types_de_champ.find { |rtdc| rtdc.stable_id == stable_id }
+
if rtdc.child?
header_section_level_value.to_i + rtdc.parent.type_de_champ.current_section_level(revision)
elsif header_section_level_value
@@ -537,57 +414,40 @@ class TypeDeChamp < ApplicationRecord
end
end
- def self.filter_hash_type(type_champ)
- if is_choice_type_from(type_champ)
+ def self.column_type(type_champ)
+ case type_champ
+ when type_champs.fetch(:datetime)
+ :datetime
+ when type_champs.fetch(:date)
+ :date
+ when type_champs.fetch(:integer_number)
+ :integer
+ when type_champs.fetch(:decimal_number)
+ :decimal
+ when type_champs.fetch(:multiple_drop_down_list)
+ :enums
+ when type_champs.fetch(:drop_down_list), type_champs.fetch(:departements), type_champs.fetch(:regions)
:enum
+ when type_champs.fetch(:checkbox), type_champs.fetch(:yes_no)
+ :boolean
+ when type_champs.fetch(:titre_identite), type_champs.fetch(:piece_justificative)
+ :attachements
else
:text
end
end
- def self.filter_hash_value_column(type_champ)
- if type_champ.in?([TypeDeChamp.type_champs.fetch(:departements), TypeDeChamp.type_champs.fetch(:regions)])
- :external_id
- else
- :value
- end
- end
-
def options_for_select
- if departement?
- APIGeoService.departements.map { ["#{_1[:code]} – #{_1[:name]}", _1[:code]] }
- elsif region?
- APIGeoService.regions.map { [_1[:name], _1[:code]] }
- elsif choice_type?
- if drop_down_list?
- drop_down_list_enabled_non_empty_options
- elsif yes_no?
- Champs::YesNoChamp.options
- elsif checkbox?
- Champs::CheckboxChamp.options
- end
- end
- end
-
- def drop_down_list_options?
- drop_down_list_options.any?
- end
-
- def drop_down_list_options
- drop_down_options.presence || []
- end
-
- def drop_down_list_disabled_options
- drop_down_list_options.filter { |v| (v =~ /^--.*--$/).present? }
- end
-
- def drop_down_list_enabled_non_empty_options(other: false)
- list_options = (drop_down_list_options - drop_down_list_disabled_options).reject(&:empty?)
-
- if other && drop_down_other?
- list_options + [[I18n.t('shared.champs.drop_down_list.other'), Champs::DropDownListChamp::OTHER]]
- else
- list_options
+ if departements?
+ APIGeoService.departement_options
+ elsif regions?
+ APIGeoService.region_options
+ elsif any_drop_down_list?
+ drop_down_options
+ elsif yes_no?
+ Champs::YesNoChamp.options
+ elsif checkbox?
+ Champs::CheckboxChamp.options
end
end
@@ -645,8 +505,7 @@ class TypeDeChamp < ApplicationRecord
# We should refresh all champs after update except for champs using react or custom refresh
# logic (RNA, SIRET, etc.)
case type_champ
- when type_champs.fetch(:annuaire_education),
- type_champs.fetch(:carte),
+ when type_champs.fetch(:carte),
type_champs.fetch(:piece_justificative),
type_champs.fetch(:titre_identite),
type_champs.fetch(:rna),
@@ -657,8 +516,29 @@ class TypeDeChamp < ApplicationRecord
end
end
- def routable?
- type_champ.in?(ROUTABLE_TYPES)
+ def simple_routable?
+ type_champ.in?(SIMPLE_ROUTABLE_TYPES)
+ end
+
+ def conditionable?
+ Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(type_champ)
+ end
+
+ def self.humanized_conditionable_types_by_category
+ Logic::ChampValue::MANAGED_TYPE_DE_CHAMP_BY_CATEGORY
+ .map { |_, v| v.map { "« #{I18n.t(_1, scope: [:activerecord, :attributes, :type_de_champ, :type_champs])} »" } }
+ end
+
+ def self.humanized_simple_routable_types_by_category
+ Logic::ChampValue::MANAGED_TYPE_DE_CHAMP_BY_CATEGORY
+ .map { |_, v| v.filter_map { "« #{I18n.t(_1, scope: [:activerecord, :attributes, :type_de_champ, :type_champs])} »" if _1.to_s.in?(SIMPLE_ROUTABLE_TYPES) } }
+ .reject(&:empty?)
+ end
+
+ def self.humanized_custom_routable_types_by_category
+ Logic::ChampValue::MANAGED_TYPE_DE_CHAMP_BY_CATEGORY
+ .map { |_, v| v.filter_map { "« #{I18n.t(_1, scope: [:activerecord, :attributes, :type_de_champ, :type_champs])} »" if !_1.to_s.in?(SIMPLE_ROUTABLE_TYPES) } }
+ .reject(&:empty?)
end
def invalid_regexp?
@@ -687,13 +567,129 @@ class TypeDeChamp < ApplicationRecord
end
end
+ def libelle_as_filename
+ libelle.gsub(/[[:space:]]+/, ' ')
+ .truncate(30, omission: '', separator: ' ')
+ .parameterize
+ end
+
+ OPTS_BY_TYPE = {
+ type_champs.fetch(:header_section) => [:header_section_level],
+ type_champs.fetch(:explication) => [:collapsible_explanation_enabled, :collapsible_explanation_text],
+ type_champs.fetch(:textarea) => [:character_limit],
+ type_champs.fetch(:carte) => TypesDeChamp::CarteTypeDeChamp::LAYERS,
+ type_champs.fetch(:drop_down_list) => [:drop_down_other, :drop_down_options],
+ type_champs.fetch(:multiple_drop_down_list) => [:drop_down_options],
+ type_champs.fetch(:linked_drop_down_list) => [:drop_down_options, :drop_down_secondary_libelle, :drop_down_secondary_description],
+ type_champs.fetch(:piece_justificative) => [:old_pj, :skip_pj_validation, :skip_content_type_pj_validation],
+ type_champs.fetch(:titre_identite) => [:old_pj, :skip_pj_validation, :skip_content_type_pj_validation],
+ type_champs.fetch(:expression_reguliere) => [:expression_reguliere, :expression_reguliere_error_message, :expression_reguliere_exemple_text]
+ }
+
+ def clean_options
+ kept_keys = OPTS_BY_TYPE.fetch(type_champ.to_s) { [] }
+ options.slice(*kept_keys.map(&:to_s))
+ end
+
+ def champ_value(champ)
+ if champ_blank?(champ)
+ dynamic_type.champ_default_value
+ else
+ dynamic_type.champ_value(champ)
+ end
+ end
+
+ def champ_value_for_api(champ, version: 2)
+ if champ_blank?(champ)
+ dynamic_type.champ_default_api_value(version)
+ else
+ dynamic_type.champ_value_for_api(champ, version:)
+ end
+ end
+
+ def champ_value_for_export(champ, path = :value)
+ if champ_blank?(champ)
+ dynamic_type.champ_default_export_value(path)
+ else
+ dynamic_type.champ_value_for_export(champ, path)
+ end
+ end
+
+ def champ_value_for_tag(champ, path = :value)
+ if champ_blank?(champ)
+ ''
+ else
+ dynamic_type.champ_value_for_tag(champ, path)
+ end
+ end
+
+ def champ_blank?(champ)
+ # no champ
+ return true if champ.nil?
+ # type de champ on the revision changed
+ if champ.is_type?(type_champ) || castable_on_change?(champ.last_write_type_champ, type_champ)
+ dynamic_type.champ_blank?(champ)
+ else
+ true
+ end
+ end
+
+ def mandatory_blank?(champ)
+ # no champ
+ return true if champ.nil?
+ # type de champ on the revision changed
+ if champ.is_type?(type_champ) || castable_on_change?(champ.last_write_type_champ, type_champ)
+ mandatory? && dynamic_type.champ_blank_or_invalid?(champ)
+ else
+ true
+ end
+ end
+
+ def html_id(row_id = nil)
+ "champ-#{public_id(row_id)}"
+ end
+
+ class << self
+ def type_champ_to_champ_class_name(type_champ)
+ "Champs::#{type_champ.classify}Champ"
+ end
+
+ def type_champ_to_class_name(type_champ)
+ "TypesDeChamp::#{type_champ.classify}TypeDeChamp"
+ end
+ end
+
+ CHAMP_TYPE_TO_TYPE_CHAMP = type_champs.values.map { [type_champ_to_champ_class_name(_1), _1] }.to_h
+
+ def piece_justificative_or_titre_identite?
+ type_champ.in?([
+ TypeDeChamp.type_champs.fetch(:piece_justificative),
+ TypeDeChamp.type_champs.fetch(:titre_identite)
+ ])
+ end
+
+ def any_drop_down_list?
+ type_champ.in?([
+ TypeDeChamp.type_champs.fetch(:drop_down_list),
+ TypeDeChamp.type_champs.fetch(:multiple_drop_down_list),
+ TypeDeChamp.type_champs.fetch(:linked_drop_down_list)
+ ])
+ end
+
private
- DEFAULT_EMPTY = ['']
- def parse_drop_down_list_value(value)
- value = value ? value.split("\r\n").map(&:strip).join("\r\n") : ''
- result = value.split(/[\r\n]|[\r]|[\n]|[\n\r]/).reject(&:empty?)
- result.blank? ? [] : DEFAULT_EMPTY + result
+ def castable_on_change?(from_type, to_type)
+ case [from_type, to_type]
+ when ['integer_number', 'decimal_number'], # recast numbers automatically
+ ['decimal_number', 'integer_number'], # may lose some data, but who cares ?
+ ['text', 'textarea'], # allow short text to long text
+ ['drop_down_list', 'multiple_drop_down_list'], # single list can become multi
+ ['date', 'datetime'], # date <=> datetime
+ ['datetime', 'date'] # may lose some data, but who cares ?
+ true
+ else
+ false
+ end
end
def populate_stable_id
@@ -702,29 +698,19 @@ class TypeDeChamp < ApplicationRecord
end
end
- def remove_piece_justificative_template
- if !piece_justificative? && piece_justificative_template.attached?
- @remove_piece_justificative_template = true
+ def remove_attachment
+ if !piece_justificative_or_titre_identite? && piece_justificative_template.attached?
+ piece_justificative_template.purge_later
+ elsif !explication? && notice_explicative.attached?
+ notice_explicative.purge_later
end
end
- def remove_drop_down_list
- if !drop_down_list?
- self.drop_down_options = nil
- elsif !drop_down_options_changed?
- self.drop_down_options = if linked_drop_down_list?
- ['', '--Fromage--', 'bleu de sassenage', 'picodon', '--Dessert--', 'éclair', 'tarte aux pommes']
- else
- ['', 'Premier choix', 'Deuxième choix']
- end
- end
- end
-
- def remove_block
- if !block? && procedure.present?
- procedure
- .draft_revision # action occurs only on draft
- .remove_children_of(self)
+ def set_drop_down_list_options
+ if (drop_down_list? || multiple_drop_down_list?) && drop_down_options.empty?
+ self.drop_down_options = ['Fromage', 'Dessert']
+ elsif linked_drop_down_list? && drop_down_options.none?(/^--.*--$/)
+ self.drop_down_options = ['--Fromage--', 'bleu de sassenage', 'picodon', '--Dessert--', 'éclair', 'tarte aux pommes']
end
end
diff --git a/app/models/types_de_champ/address_type_de_champ.rb b/app/models/types_de_champ/address_type_de_champ.rb
index 710ca5f96..5173b4712 100644
--- a/app/models/types_de_champ/address_type_de_champ.rb
+++ b/app/models/types_de_champ/address_type_de_champ.rb
@@ -1,9 +1,41 @@
+# frozen_string_literal: true
+
class TypesDeChamp::AddressTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def libelles_for_export
path = paths.first
[[path[:libelle], path[:path]]]
end
+ def champ_value(champ)
+ champ.address_label.presence || ''
+ end
+
+ def champ_value_for_api(champ, version: 2)
+ champ_value(champ)
+ end
+
+ def champ_value_for_tag(champ, path = :value)
+ case path
+ when :value
+ champ_value(champ)
+ when :departement
+ champ.departement_code_and_name || ''
+ when :commune
+ champ.commune_name || ''
+ end
+ end
+
+ def champ_value_for_export(champ, path = :value)
+ case path
+ when :value
+ champ_value(champ)
+ when :departement
+ champ.departement_code_and_name
+ when :commune
+ champ.commune_name
+ end
+ end
+
private
def paths
diff --git a/app/models/types_de_champ/annuaire_education_type_de_champ.rb b/app/models/types_de_champ/annuaire_education_type_de_champ.rb
index bce005c58..657d961b2 100644
--- a/app/models/types_de_champ/annuaire_education_type_de_champ.rb
+++ b/app/models/types_de_champ/annuaire_education_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::AnnuaireEducationTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
diff --git a/app/models/types_de_champ/carte_type_de_champ.rb b/app/models/types_de_champ/carte_type_de_champ.rb
index 054914bcf..fb7822353 100644
--- a/app/models/types_de_champ/carte_type_de_champ.rb
+++ b/app/models/types_de_champ/carte_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::CarteTypeDeChamp < TypesDeChamp::TypeDeChampBase
LAYERS = [
:unesco,
@@ -17,4 +19,18 @@ class TypesDeChamp::CarteTypeDeChamp < TypesDeChamp::TypeDeChampBase
end
def tags_for_template = [].freeze
+
+ def champ_value_for_api(champ, version: 2)
+ nil
+ end
+
+ def champ_value_for_export(champ, path = :value)
+ champ.geo_areas.map(&:label).join("\n")
+ end
+
+ def champ_blank?(champ) = champ.geo_areas.blank?
+
+ def columns(procedure:, displayable: true, prefix: nil)
+ []
+ end
end
diff --git a/app/models/types_de_champ/checkbox_type_de_champ.rb b/app/models/types_de_champ/checkbox_type_de_champ.rb
index aa60db19d..9cf47a847 100644
--- a/app/models/types_de_champ/checkbox_type_de_champ.rb
+++ b/app/models/types_de_champ/checkbox_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::CheckboxTypeDeChamp < TypesDeChamp::TypeDeChampBase
def filter_to_human(filter_value)
if filter_value == "true"
@@ -8,4 +10,44 @@ class TypesDeChamp::CheckboxTypeDeChamp < TypesDeChamp::TypeDeChampBase
filter_value
end
end
+
+ def champ_value(champ)
+ champ_value_true?(champ) ? 'Oui' : 'Non'
+ end
+
+ def champ_value_for_export(champ, path = :value)
+ champ_value_true?(champ) ? 'on' : 'off'
+ end
+
+ def champ_value_for_api(champ, version: 2)
+ case version
+ when 2
+ champ_value_true?(champ).to_s
+ else
+ super
+ end
+ end
+
+ def champ_default_value
+ 'Non'
+ end
+
+ def champ_default_export_value(path = :value)
+ 'off'
+ end
+
+ def champ_default_api_value(version = 2)
+ case version
+ when 2
+ 'false'
+ else
+ nil
+ end
+ end
+
+ def champ_blank_or_invalid?(champ) = !champ_value_true?(champ)
+
+ private
+
+ def champ_value_true?(champ) = champ.value == 'true'
end
diff --git a/app/models/types_de_champ/civilite_type_de_champ.rb b/app/models/types_de_champ/civilite_type_de_champ.rb
index 508c14f06..648cdaadc 100644
--- a/app/models/types_de_champ/civilite_type_de_champ.rb
+++ b/app/models/types_de_champ/civilite_type_de_champ.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class TypesDeChamp::CiviliteTypeDeChamp < TypesDeChamp::TypeDeChampBase
end
diff --git a/app/models/types_de_champ/cnaf_type_de_champ.rb b/app/models/types_de_champ/cnaf_type_de_champ.rb
index cd62bbfab..9a6d1b58e 100644
--- a/app/models/types_de_champ/cnaf_type_de_champ.rb
+++ b/app/models/types_de_champ/cnaf_type_de_champ.rb
@@ -1,5 +1,9 @@
+# frozen_string_literal: true
+
class TypesDeChamp::CnafTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
+
+ def champ_blank?(champ) = champ.external_id.blank?
end
diff --git a/app/models/types_de_champ/cojo_type_de_champ.rb b/app/models/types_de_champ/cojo_type_de_champ.rb
index 2747a34c7..5d65a80ff 100644
--- a/app/models/types_de_champ/cojo_type_de_champ.rb
+++ b/app/models/types_de_champ/cojo_type_de_champ.rb
@@ -1,2 +1,9 @@
+# frozen_string_literal: true
+
class TypesDeChamp::COJOTypeDeChamp < TypesDeChamp::TextTypeDeChamp
+ def champ_value(champ)
+ "#{champ.accreditation_number} – #{champ.accreditation_birthdate}"
+ end
+
+ def champ_blank?(champ) = champ.accreditation_success != true
end
diff --git a/app/models/types_de_champ/commune_type_de_champ.rb b/app/models/types_de_champ/commune_type_de_champ.rb
index 69be05959..2f3d639df 100644
--- a/app/models/types_de_champ/commune_type_de_champ.rb
+++ b/app/models/types_de_champ/commune_type_de_champ.rb
@@ -1,4 +1,57 @@
+# frozen_string_literal: true
+
class TypesDeChamp::CommuneTypeDeChamp < TypesDeChamp::TypeDeChampBase
+ def champ_value_for_export(champ, path = :value)
+ case path
+ when :value
+ champ_value(champ)
+ when :departement
+ champ.departement_code_and_name || ''
+ when :code
+ champ.code || ''
+ end
+ end
+
+ def champ_value_for_tag(champ, path = :value)
+ case path
+ when :value
+ champ_value(champ)
+ when :departement
+ champ.departement_code_and_name || ''
+ when :code
+ champ.code || ''
+ end
+ end
+
+ def champ_value(champ)
+ champ.code_postal? ? "#{champ.name} (#{champ.code_postal})" : champ.name
+ end
+
+ def columns(procedure:, displayable: true, prefix: nil)
+ super.concat(
+ [
+ Columns::JSONPathColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: "#{libelle_with_prefix(prefix)} - code postal (5 chiffres)",
+ jsonpath: '$.code_postal',
+ displayable:,
+ type: :text
+ ),
+ Columns::JSONPathColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: "#{libelle_with_prefix(prefix)} - département",
+ jsonpath: '$.code_departement',
+ displayable:,
+ type: :number
+ )
+ ]
+ )
+ end
+
private
def paths
diff --git a/app/models/types_de_champ/date_type_de_champ.rb b/app/models/types_de_champ/date_type_de_champ.rb
index 65b0d5fc7..b332e34d7 100644
--- a/app/models/types_de_champ/date_type_de_champ.rb
+++ b/app/models/types_de_champ/date_type_de_champ.rb
@@ -1,2 +1,9 @@
+# frozen_string_literal: true
+
class TypesDeChamp::DateTypeDeChamp < TypesDeChamp::TypeDeChampBase
+ def champ_value(champ)
+ I18n.l(Time.zone.parse(champ.value), format: '%d %B %Y')
+ rescue ArgumentError
+ champ.value.presence || "" # old dossiers can have not parseable dates
+ end
end
diff --git a/app/models/types_de_champ/datetime_type_de_champ.rb b/app/models/types_de_champ/datetime_type_de_champ.rb
index 2ccfec18f..e74423665 100644
--- a/app/models/types_de_champ/datetime_type_de_champ.rb
+++ b/app/models/types_de_champ/datetime_type_de_champ.rb
@@ -1,2 +1,7 @@
+# frozen_string_literal: true
+
class TypesDeChamp::DatetimeTypeDeChamp < TypesDeChamp::TypeDeChampBase
+ def champ_value(champ)
+ I18n.l(Time.zone.parse(champ.value))
+ end
end
diff --git a/app/models/types_de_champ/decimal_number_type_de_champ.rb b/app/models/types_de_champ/decimal_number_type_de_champ.rb
index 9a1363317..baf62f2b1 100644
--- a/app/models/types_de_champ/decimal_number_type_de_champ.rb
+++ b/app/models/types_de_champ/decimal_number_type_de_champ.rb
@@ -1,2 +1,26 @@
+# frozen_string_literal: true
+
class TypesDeChamp::DecimalNumberTypeDeChamp < TypesDeChamp::TypeDeChampBase
+ def champ_value_for_export(champ, path = :value)
+ champ_formatted_value(champ)
+ end
+
+ def champ_value_for_api(champ, version: 2)
+ case version
+ when 1
+ champ_formatted_value(champ)
+ else
+ super
+ end
+ end
+
+ def champ_default_export_value(path = :value)
+ 0
+ end
+
+ private
+
+ def champ_formatted_value(champ)
+ champ.value&.to_f
+ end
end
diff --git a/app/models/types_de_champ/departement_type_de_champ.rb b/app/models/types_de_champ/departement_type_de_champ.rb
index 334286f78..cf296bac0 100644
--- a/app/models/types_de_champ/departement_type_de_champ.rb
+++ b/app/models/types_de_champ/departement_type_de_champ.rb
@@ -1,8 +1,41 @@
+# frozen_string_literal: true
+
class TypesDeChamp::DepartementTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def filter_to_human(filter_value)
APIGeoService.departement_name(filter_value).presence || filter_value
end
+ def champ_value(champ)
+ "#{champ.code} – #{champ.name}"
+ end
+
+ def champ_value_for_export(champ, path = :value)
+ case path
+ when :code
+ champ.code
+ when :value
+ champ.name
+ end
+ end
+
+ def champ_value_for_tag(champ, path = :value)
+ case path
+ when :code
+ champ.code
+ when :value
+ champ_value(champ)
+ end
+ end
+
+ def champ_value_for_api(champ, version: 2)
+ case version
+ when 2
+ champ_value(champ).tr('–', '-')
+ else
+ champ_value(champ)
+ end
+ end
+
private
def paths
diff --git a/app/models/types_de_champ/dgfip_type_de_champ.rb b/app/models/types_de_champ/dgfip_type_de_champ.rb
index 666d80ef2..7c20a7c7d 100644
--- a/app/models/types_de_champ/dgfip_type_de_champ.rb
+++ b/app/models/types_de_champ/dgfip_type_de_champ.rb
@@ -1,5 +1,9 @@
+# frozen_string_literal: true
+
class TypesDeChamp::DgfipTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
+
+ def champ_blank?(champ) = champ.external_id.blank?
end
diff --git a/app/models/types_de_champ/dossier_link_type_de_champ.rb b/app/models/types_de_champ/dossier_link_type_de_champ.rb
index 5c91a820f..a208fe4eb 100644
--- a/app/models/types_de_champ/dossier_link_type_de_champ.rb
+++ b/app/models/types_de_champ/dossier_link_type_de_champ.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class TypesDeChamp::DossierLinkTypeDeChamp < TypesDeChamp::TypeDeChampBase
end
diff --git a/app/models/types_de_champ/drop_down_list_type_de_champ.rb b/app/models/types_de_champ/drop_down_list_type_de_champ.rb
index c930f3ff7..eee0c03b8 100644
--- a/app/models/types_de_champ/drop_down_list_type_de_champ.rb
+++ b/app/models/types_de_champ/drop_down_list_type_de_champ.rb
@@ -1,2 +1,17 @@
+# frozen_string_literal: true
+
class TypesDeChamp::DropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBase
+ def champ_blank?(champ)
+ super || !champ_value_in_options?(champ)
+ end
+
+ private
+
+ def champ_value_in_options?(champ)
+ champ_with_other_value?(champ) || drop_down_options.include?(champ.value)
+ end
+
+ def champ_with_other_value?(champ)
+ drop_down_other? && champ.value_json&.fetch('other', false)
+ end
end
diff --git a/app/models/types_de_champ/email_type_de_champ.rb b/app/models/types_de_champ/email_type_de_champ.rb
index 022330d3c..7a9ff0bfc 100644
--- a/app/models/types_de_champ/email_type_de_champ.rb
+++ b/app/models/types_de_champ/email_type_de_champ.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class TypesDeChamp::EmailTypeDeChamp < TypesDeChamp::TextTypeDeChamp
end
diff --git a/app/models/types_de_champ/engagement_juridique_type_de_champ.rb b/app/models/types_de_champ/engagement_juridique_type_de_champ.rb
index bd076d2c8..c93da053f 100644
--- a/app/models/types_de_champ/engagement_juridique_type_de_champ.rb
+++ b/app/models/types_de_champ/engagement_juridique_type_de_champ.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class TypesDeChamp::EngagementJuridiqueTypeDeChamp < TypesDeChamp::TypeDeChampBase
end
diff --git a/app/models/types_de_champ/epci_type_de_champ.rb b/app/models/types_de_champ/epci_type_de_champ.rb
index 1ef14d66e..5fcc09ad4 100644
--- a/app/models/types_de_champ/epci_type_de_champ.rb
+++ b/app/models/types_de_champ/epci_type_de_champ.rb
@@ -1,4 +1,28 @@
+# frozen_string_literal: true
+
class TypesDeChamp::EpciTypeDeChamp < TypesDeChamp::TextTypeDeChamp
+ def champ_value_for_export(champ, path = :value)
+ case path
+ when :value
+ champ_value(champ)
+ when :code
+ champ.code
+ when :departement
+ champ.departement_code_and_name
+ end
+ end
+
+ def champ_value_for_tag(champ, path = :value)
+ case path
+ when :value
+ champ_value(champ)
+ when :code
+ champ.code
+ when :departement
+ champ.departement_code_and_name
+ end
+ end
+
private
def paths
diff --git a/app/models/types_de_champ/explication_type_de_champ.rb b/app/models/types_de_champ/explication_type_de_champ.rb
index fd27efb55..837c1239f 100644
--- a/app/models/types_de_champ/explication_type_de_champ.rb
+++ b/app/models/types_de_champ/explication_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::ExplicationTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def tags_for_template = [].freeze
end
diff --git a/app/models/types_de_champ/expression_reguliere_type_de_champ.rb b/app/models/types_de_champ/expression_reguliere_type_de_champ.rb
index 9dcbe14af..f46829581 100644
--- a/app/models/types_de_champ/expression_reguliere_type_de_champ.rb
+++ b/app/models/types_de_champ/expression_reguliere_type_de_champ.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class TypesDeChamp::ExpressionReguliereTypeDeChamp < TypesDeChamp::TypeDeChampBase
end
diff --git a/app/models/types_de_champ/header_section_type_de_champ.rb b/app/models/types_de_champ/header_section_type_de_champ.rb
index ba596abbc..5214c18f5 100644
--- a/app/models/types_de_champ/header_section_type_de_champ.rb
+++ b/app/models/types_de_champ/header_section_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::HeaderSectionTypeDeChamp < TypesDeChamp::TypeDeChampBase
def tags_for_template = [].freeze
end
diff --git a/app/models/types_de_champ/iban_type_de_champ.rb b/app/models/types_de_champ/iban_type_de_champ.rb
index 209e4b899..a5a683f25 100644
--- a/app/models/types_de_champ/iban_type_de_champ.rb
+++ b/app/models/types_de_champ/iban_type_de_champ.rb
@@ -1,5 +1,11 @@
+# frozen_string_literal: true
+
class TypesDeChamp::IbanTypeDeChamp < TypesDeChamp::TypeDeChampBase
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
+
+ def champ_value_for_api(champ, version: 2)
+ champ_value(champ).gsub(/\s+/, '')
+ end
end
diff --git a/app/models/types_de_champ/integer_number_type_de_champ.rb b/app/models/types_de_champ/integer_number_type_de_champ.rb
index 0eda2c3d5..5a7670a83 100644
--- a/app/models/types_de_champ/integer_number_type_de_champ.rb
+++ b/app/models/types_de_champ/integer_number_type_de_champ.rb
@@ -1,2 +1,26 @@
+# frozen_string_literal: true
+
class TypesDeChamp::IntegerNumberTypeDeChamp < TypesDeChamp::TypeDeChampBase
+ def champ_value_for_export(champ, path = :value)
+ champ_formatted_value(champ)
+ end
+
+ def champ_value_for_api(champ, version: 2)
+ case version
+ when 1
+ champ_formatted_value(champ)
+ else
+ super
+ end
+ end
+
+ def champ_default_export_value(path = :value)
+ 0
+ end
+
+ private
+
+ def champ_formatted_value(champ)
+ champ.value&.to_i
+ end
end
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 e5b0b56a3..cecb2699b 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
@@ -1,7 +1,8 @@
+# frozen_string_literal: true
+
class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBase
PRIMARY_PATTERN = /^--(.*)--$/
- delegate :drop_down_list_options, to: :@type_de_champ
validate :check_presence_of_primary_options
def libelles_for_export
@@ -9,11 +10,6 @@ class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBas
[[path[:libelle], path[:path]]]
end
- def add_blank_option_when_not_mandatory(options)
- return options if mandatory
- options.unshift('')
- end
-
def primary_options
primary_options = unpack_options.map(&:first)
if primary_options.present?
@@ -30,8 +26,107 @@ class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBas
secondary_options
end
+ def champ_value(champ)
+ [primary_value(champ), secondary_value(champ)].filter(&:present?).join(' / ')
+ end
+
+ def champ_value_for_tag(champ, path = :value)
+ case path
+ when :primary
+ primary_value(champ)
+ when :secondary
+ secondary_value(champ)
+ when :value
+ champ_value(champ)
+ end
+ end
+
+ def champ_value_for_export(champ, path = :value)
+ case path
+ when :primary
+ primary_value(champ)
+ when :secondary
+ secondary_value(champ)
+ when :value
+ "#{primary_value(champ) || ''};#{secondary_value(champ) || ''}"
+ end
+ end
+
+ def champ_value_for_api(champ, version: 2)
+ case version
+ when 1
+ { primary: primary_value(champ), secondary: secondary_value(champ) }
+ else
+ super
+ end
+ end
+
+ def champ_blank?(champ)
+ primary_value(champ).blank? && secondary_value(champ).blank?
+ end
+
+ def champ_blank_or_invalid?(champ)
+ primary_value(champ).blank? ||
+ (has_secondary_options_for_primary?(champ) && secondary_value(champ).blank?)
+ end
+
+ def columns(procedure:, displayable: true, prefix: nil)
+ [
+ Columns::LinkedDropDownColumn.new(
+ procedure_id: procedure.id,
+ label: libelle_with_prefix(prefix),
+ stable_id:,
+ tdc_type: type_champ,
+ type: :text,
+ path: :value,
+ displayable:
+ ),
+ Columns::LinkedDropDownColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: "#{libelle_with_prefix(prefix)} (Primaire)",
+ type: :enum,
+ path: :primary,
+ displayable: false,
+ options_for_select: primary_options
+ ),
+ Columns::LinkedDropDownColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: "#{libelle_with_prefix(prefix)} (Secondaire)",
+ type: :enum,
+ path: :secondary,
+ displayable: false,
+ options_for_select: secondary_options.values.flatten.uniq.sort
+ )
+ ]
+ end
+
private
+ def add_blank_option_when_not_mandatory(options)
+ return options if mandatory
+ options.unshift('')
+ end
+
+ def primary_value(champ) = unpack_value(champ.value, 0, primary_options)
+ def secondary_value(champ) = unpack_value(champ.value, 1, secondary_options.values.flatten)
+
+ def unpack_value(value, index, options)
+ value&.then do
+ unpacked_value = JSON.parse(_1)[index]
+ unpacked_value if options.include?(unpacked_value)
+ rescue
+ nil
+ end
+ end
+
+ def has_secondary_options_for_primary?(champ)
+ primary_value(champ).present? && secondary_options[primary_value(champ)]&.any?(&:present?)
+ end
+
def paths
paths = super
paths.push({
@@ -50,8 +145,8 @@ class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBas
end
def unpack_options
- _, *options = drop_down_list_options
- chunked = options.slice_before(PRIMARY_PATTERN)
+ chunked = drop_down_options.slice_before(PRIMARY_PATTERN)
+
chunked.map do |chunk|
primary, *secondary = chunk
secondary = add_blank_option_when_not_mandatory(secondary)
@@ -60,7 +155,7 @@ class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBas
end
def check_presence_of_primary_options
- if !PRIMARY_PATTERN.match?(drop_down_list_options.second)
+ if !PRIMARY_PATTERN.match?(drop_down_options.first)
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/types_de_champ/mesri_type_de_champ.rb b/app/models/types_de_champ/mesri_type_de_champ.rb
index ed616875f..ff0584800 100644
--- a/app/models/types_de_champ/mesri_type_de_champ.rb
+++ b/app/models/types_de_champ/mesri_type_de_champ.rb
@@ -1,5 +1,9 @@
+# frozen_string_literal: true
+
class TypesDeChamp::MesriTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
+
+ def champ_blank?(champ) = champ.external_id.blank?
end
diff --git a/app/models/types_de_champ/multiple_drop_down_list_type_de_champ.rb b/app/models/types_de_champ/multiple_drop_down_list_type_de_champ.rb
index f5028c302..bbca8ee52 100644
--- a/app/models/types_de_champ/multiple_drop_down_list_type_de_champ.rb
+++ b/app/models/types_de_champ/multiple_drop_down_list_type_de_champ.rb
@@ -1,2 +1,31 @@
+# frozen_string_literal: true
+
class TypesDeChamp::MultipleDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBase
+ def champ_value(champ)
+ selected_options(champ).join(', ')
+ end
+
+ def champ_value_for_tag(champ, path = :value)
+ ChampPresentations::MultipleDropDownListPresentation.new(selected_options(champ))
+ end
+
+ def champ_value_for_export(champ, path = :value)
+ champ_value(champ)
+ end
+
+ def champ_blank?(champ) = selected_options(champ).blank?
+
+ private
+
+ def selected_options(champ)
+ return [] if champ.value.blank?
+
+ if champ.is_type?(TypeDeChamp.type_champs.fetch(:drop_down_list))
+ [champ.value]
+ else
+ JSON.parse(champ.value)
+ end.filter { drop_down_options.include?(_1) }
+ rescue JSON::ParserError
+ []
+ end
end
diff --git a/app/models/types_de_champ/number_type_de_champ.rb b/app/models/types_de_champ/number_type_de_champ.rb
index 9ff857f15..df32d878e 100644
--- a/app/models/types_de_champ/number_type_de_champ.rb
+++ b/app/models/types_de_champ/number_type_de_champ.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class TypesDeChamp::NumberTypeDeChamp < TypesDeChamp::TypeDeChampBase
end
diff --git a/app/models/types_de_champ/pays_type_de_champ.rb b/app/models/types_de_champ/pays_type_de_champ.rb
index 43a041e76..40d60e1b3 100644
--- a/app/models/types_de_champ/pays_type_de_champ.rb
+++ b/app/models/types_de_champ/pays_type_de_champ.rb
@@ -1,4 +1,32 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PaysTypeDeChamp < TypesDeChamp::TextTypeDeChamp
+ def champ_value(champ)
+ champ.name
+ end
+
+ def champ_value_for_export(champ, path = :value)
+ case path
+ when :value
+ champ_value(champ)
+ when :code
+ champ.code
+ end
+ end
+
+ def champ_value_for_tag(champ, path = :value)
+ case path
+ when :value
+ champ_value(champ)
+ when :code
+ champ.code
+ end
+ end
+
+ def champ_blank?(champ)
+ champ.value.blank? && champ.external_id.blank?
+ end
+
private
def paths
diff --git a/app/models/types_de_champ/phone_type_de_champ.rb b/app/models/types_de_champ/phone_type_de_champ.rb
index 6cd42ac46..7d95156b3 100644
--- a/app/models/types_de_champ/phone_type_de_champ.rb
+++ b/app/models/types_de_champ/phone_type_de_champ.rb
@@ -1,2 +1,34 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PhoneTypeDeChamp < TypesDeChamp::TextTypeDeChamp
+ # We want to allow:
+ # * international (e164) phone numbers
+ # * “french format” (ten digits with a leading 0)
+ # * DROM numbers
+ #
+ # However, we need to special-case some ten-digit numbers,
+ # because the ARCEP assigns some blocks of "O6 XX XX XX XX" numbers to DROM operators.
+ # Guadeloupe | GP | +590 | 0690XXXXXX, 0691XXXXXX
+ # Guyane | GF | +594 | 0694XXXXXX
+ # Martinique | MQ | +596 | 0696XXXXXX, 0697XXXXXX
+ # Réunion | RE | +262 | 0692XXXXXX, 0693XXXXXX
+ # Mayotte | YT | +262 | 0692XXXXXX, 0693XXXXXX
+ # Nouvelle-Calédonie | NC | +687 |
+ # Polynésie française | PF | +689 | 40XXXXXX, 45XXXXXX, 87XXXXXX, 88XXXXXX, 89XXXXXX
+ #
+ # Cf: Plan national de numérotation téléphonique,
+ # https://www.arcep.fr/uploads/tx_gsavis/05-1085.pdf “Numéros mobiles à 10 chiffres”, page 6
+ #
+ # See issue #6996.
+ DEFAULT_COUNTRY_CODES = [:FR, :GP, :GF, :MQ, :RE, :YT, :NC, :PF].freeze
+
+ def champ_value(champ)
+ if Phonelib.valid_for_countries?(champ.value, DEFAULT_COUNTRY_CODES)
+ Phonelib.parse_for_countries(champ.value, DEFAULT_COUNTRY_CODES).full_national
+ else
+ # When he phone number is possible for the default countries, but not strictly valid,
+ # `full_national` could mess up the formatting. In this case just return the original.
+ champ.value
+ end
+ end
end
diff --git a/app/models/types_de_champ/piece_justificative_type_de_champ.rb b/app/models/types_de_champ/piece_justificative_type_de_champ.rb
index b10a261b7..3fef517d0 100644
--- a/app/models/types_de_champ/piece_justificative_type_de_champ.rb
+++ b/app/models/types_de_champ/piece_justificative_type_de_champ.rb
@@ -1,7 +1,41 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PieceJustificativeTypeDeChamp < TypesDeChamp::TypeDeChampBase
def estimated_fill_duration(revision)
FILL_DURATION_LONG
end
def tags_for_template = [].freeze
+
+ def champ_value_for_export(champ, path = :value)
+ champ.piece_justificative_file.map { _1.filename.to_s }.join(', ')
+ end
+
+ def champ_value_for_api(champ, version: 2)
+ return if version == 2
+
+ # API v1 don't support multiple PJ
+ attachment = champ.piece_justificative_file.first
+ return if attachment.nil?
+
+ if attachment.virus_scanner.safe? || attachment.virus_scanner.pending?
+ attachment.url
+ end
+ end
+
+ def champ_blank?(champ) = champ.piece_justificative_file.blank?
+
+ def columns(procedure:, displayable: true, prefix: nil)
+ [
+ Columns::AttachedManyColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: libelle_with_prefix(prefix),
+ type: TypeDeChamp.column_type(type_champ),
+ displayable: false,
+ filterable: false
+ )
+ ]
+ end
end
diff --git a/app/models/types_de_champ/pole_emploi_type_de_champ.rb b/app/models/types_de_champ/pole_emploi_type_de_champ.rb
index 508f72e20..e41113f6a 100644
--- a/app/models/types_de_champ/pole_emploi_type_de_champ.rb
+++ b/app/models/types_de_champ/pole_emploi_type_de_champ.rb
@@ -1,5 +1,9 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PoleEmploiTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
+
+ def champ_blank?(champ) = champ.external_id.blank?
end
diff --git a/app/models/types_de_champ/prefill_annuaire_education_type_de_champ.rb b/app/models/types_de_champ/prefill_annuaire_education_type_de_champ.rb
deleted file mode 100644
index 7790458d9..000000000
--- a/app/models/types_de_champ/prefill_annuaire_education_type_de_champ.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-class TypesDeChamp::PrefillAnnuaireEducationTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp
- def to_assignable_attributes(champ, value)
- return nil if value.blank?
-
- {
- id: champ.id,
- external_id: value,
- value: value
- }
- end
-end
diff --git a/app/models/types_de_champ/prefill_commune_type_de_champ.rb b/app/models/types_de_champ/prefill_commune_type_de_champ.rb
index 524fade1b..122db27f2 100644
--- a/app/models/types_de_champ/prefill_commune_type_de_champ.rb
+++ b/app/models/types_de_champ/prefill_commune_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PrefillCommuneTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp
def all_possible_values
[]
diff --git a/app/models/types_de_champ/prefill_departement_type_de_champ.rb b/app/models/types_de_champ/prefill_departement_type_de_champ.rb
index 4c92cad1d..703db7d4b 100644
--- a/app/models/types_de_champ/prefill_departement_type_de_champ.rb
+++ b/app/models/types_de_champ/prefill_departement_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PrefillDepartementTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp
def all_possible_values
departements.map { |departement| "#{departement[:code]} (#{departement[:name]})" }
diff --git a/app/models/types_de_champ/prefill_drop_down_list_type_de_champ.rb b/app/models/types_de_champ/prefill_drop_down_list_type_de_champ.rb
index 6930905e7..65702131a 100644
--- a/app/models/types_de_champ/prefill_drop_down_list_type_de_champ.rb
+++ b/app/models/types_de_champ/prefill_drop_down_list_type_de_champ.rb
@@ -1,12 +1,11 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PrefillDropDownListTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp
def all_possible_values
if drop_down_other?
- drop_down_list_enabled_non_empty_options.insert(
- 0,
- I18n.t("views.prefill_descriptions.edit.possible_values.drop_down_list_other_html")
- )
+ [I18n.t("views.prefill_descriptions.edit.possible_values.drop_down_list_other_html")] + drop_down_options
else
- drop_down_list_enabled_non_empty_options
+ drop_down_options
end
end
diff --git a/app/models/types_de_champ/prefill_epci_type_de_champ.rb b/app/models/types_de_champ/prefill_epci_type_de_champ.rb
index f50c2e35b..8e26c5ad5 100644
--- a/app/models/types_de_champ/prefill_epci_type_de_champ.rb
+++ b/app/models/types_de_champ/prefill_epci_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PrefillEpciTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp
def all_possible_values
departements.map do |departement|
diff --git a/app/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ.rb b/app/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ.rb
index 17bea90a4..c958204cd 100644
--- a/app/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ.rb
+++ b/app/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PrefillMultipleDropDownListTypeDeChamp < TypesDeChamp::PrefillDropDownListTypeDeChamp
def example_value
return nil if all_possible_values.empty?
diff --git a/app/models/types_de_champ/prefill_pays_type_de_champ.rb b/app/models/types_de_champ/prefill_pays_type_de_champ.rb
index 9c638c904..1a49c8fc9 100644
--- a/app/models/types_de_champ/prefill_pays_type_de_champ.rb
+++ b/app/models/types_de_champ/prefill_pays_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PrefillPaysTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp
def all_possible_values
countries.map { |country| "#{country[:code]} (#{country[:name]})" }
diff --git a/app/models/types_de_champ/prefill_region_type_de_champ.rb b/app/models/types_de_champ/prefill_region_type_de_champ.rb
index ae9d0501a..fb983ef1c 100644
--- a/app/models/types_de_champ/prefill_region_type_de_champ.rb
+++ b/app/models/types_de_champ/prefill_region_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PrefillRegionTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp
def all_possible_values
regions.map { |region| "#{region[:code]} (#{region[:name]})" }
diff --git a/app/models/types_de_champ/prefill_repetition_type_de_champ.rb b/app/models/types_de_champ/prefill_repetition_type_de_champ.rb
index 7ff8076ed..5af53c178 100644
--- a/app/models/types_de_champ/prefill_repetition_type_de_champ.rb
+++ b/app/models/types_de_champ/prefill_repetition_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PrefillRepetitionTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp
include ActionView::Helpers::UrlHelper
include ApplicationHelper
@@ -54,14 +56,16 @@ class TypesDeChamp::PrefillRepetitionTypeDeChamp < TypesDeChamp::PrefillTypeDeCh
def to_assignable_attributes
return unless repetition.is_a?(Hash)
- row = champ.rows[index] || champ.add_row(champ.dossier_revision)
+ row_id = champ.row_ids[index] || champ.add_row(updated_by: nil)
repetition.map do |key, value|
next unless key.is_a?(String) && key.starts_with?("champ_")
- subchamp = row.find { |champ| champ.stable_id == Champ.stable_id_from_typed_id(key) }
- next unless subchamp
+ stable_id = Champ.stable_id_from_typed_id(key)
+ type_de_champ = revision.types_de_champ.find { _1.stable_id == stable_id }
+ next unless type_de_champ
+ subchamp = champ.dossier.champ_for_update(type_de_champ, row_id, updated_by: nil)
TypesDeChamp::PrefillTypeDeChamp.build(subchamp.type_de_champ, revision).to_assignable_attributes(subchamp, value)
end.compact
end
diff --git a/app/models/types_de_champ/prefill_type_de_champ.rb b/app/models/types_de_champ/prefill_type_de_champ.rb
index 52b3a7315..1917f7b55 100644
--- a/app/models/types_de_champ/prefill_type_de_champ.rb
+++ b/app/models/types_de_champ/prefill_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::PrefillTypeDeChamp < SimpleDelegator
include ActionView::Helpers::UrlHelper
include ApplicationHelper
@@ -29,8 +31,6 @@ class TypesDeChamp::PrefillTypeDeChamp < SimpleDelegator
TypesDeChamp::PrefillAddressTypeDeChamp.new(type_de_champ, revision)
when TypeDeChamp.type_champs.fetch(:epci)
TypesDeChamp::PrefillEpciTypeDeChamp.new(type_de_champ, revision)
- when TypeDeChamp.type_champs.fetch(:annuaire_education)
- TypesDeChamp::PrefillAnnuaireEducationTypeDeChamp.new(type_de_champ, revision)
else
new(type_de_champ, revision)
end
@@ -72,7 +72,7 @@ class TypesDeChamp::PrefillTypeDeChamp < SimpleDelegator
link_to(
I18n.t("views.prefill_descriptions.edit.possible_values.link.text"),
- Rails.application.routes.url_helpers.prefill_type_de_champ_path(revision.procedure_path, self),
+ Rails.application.routes.url_helpers.prefill_type_de_champ_path(@revision.procedure_path, self),
title: new_tab_suffix(I18n.t("views.prefill_descriptions.edit.possible_values.link.title")),
**external_link_attributes
)
diff --git a/app/models/types_de_champ/region_type_de_champ.rb b/app/models/types_de_champ/region_type_de_champ.rb
index 015614aa9..ad31c7710 100644
--- a/app/models/types_de_champ/region_type_de_champ.rb
+++ b/app/models/types_de_champ/region_type_de_champ.rb
@@ -1,8 +1,32 @@
+# frozen_string_literal: true
+
class TypesDeChamp::RegionTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def filter_to_human(filter_value)
APIGeoService.region_name(filter_value).presence || filter_value
end
+ def champ_value(champ)
+ champ.name
+ end
+
+ def champ_value_for_export(champ, path = :value)
+ case path
+ when :value
+ champ_value(champ)
+ when :code
+ champ.code
+ end
+ end
+
+ def champ_value_for_tag(champ, path = :value)
+ case path
+ when :value
+ champ_value(champ)
+ when :code
+ champ.code
+ end
+ end
+
private
def paths
diff --git a/app/models/types_de_champ/repetition_type_de_champ.rb b/app/models/types_de_champ/repetition_type_de_champ.rb
index b6184011d..7470162fd 100644
--- a/app/models/types_de_champ/repetition_type_de_champ.rb
+++ b/app/models/types_de_champ/repetition_type_de_champ.rb
@@ -1,4 +1,11 @@
+# frozen_string_literal: true
+
class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
+ def champ_value_for_tag(champ, path = :value)
+ return nil if path != :value
+ ChampPresentations::RepetitionPresentation.new(libelle, champ.dossier.project_rows_for(@type_de_champ))
+ end
+
def estimated_fill_duration(revision)
estimated_rows_in_repetition = 2.5
@@ -17,4 +24,14 @@ class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
# /\*?[] are invalid Excel worksheet characters
ActiveStorage::Filename.new(str.delete('[]*?')).sanitized
end
+
+ def columns(procedure:, displayable: nil, prefix: nil)
+ prefix = prefix.present? ? "(#{prefix} #{libelle})" : libelle
+
+ procedure
+ .all_revisions_types_de_champ(parent: @type_de_champ)
+ .flat_map { _1.columns(procedure:, displayable: false, prefix:) }
+ end
+
+ def champ_blank?(champ) = champ.dossier.repetition_row_ids(@type_de_champ).blank?
end
diff --git a/app/models/types_de_champ/rna_type_de_champ.rb b/app/models/types_de_champ/rna_type_de_champ.rb
index 01bf96815..e6c2ca915 100644
--- a/app/models/types_de_champ/rna_type_de_champ.rb
+++ b/app/models/types_de_champ/rna_type_de_champ.rb
@@ -1,5 +1,29 @@
+# frozen_string_literal: true
+
class TypesDeChamp::RNATypeDeChamp < TypesDeChamp::TypeDeChampBase
+ include AddressableColumnConcern
+
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
+
+ def champ_value_for_export(champ, path = :value)
+ champ.identifier
+ end
+
+ def columns(procedure:, displayable: true, prefix: nil)
+ super
+ .concat(addressable_columns(procedure:, displayable:, prefix:))
+ .concat([
+ Columns::JSONPathColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: "#{libelle_with_prefix(prefix)} – Titre au répertoire national des associations",
+ type: :text,
+ jsonpath: '$.title',
+ displayable:
+ )
+ ])
+ end
end
diff --git a/app/models/types_de_champ/rnf_type_de_champ.rb b/app/models/types_de_champ/rnf_type_de_champ.rb
index d25e02a46..e92989af3 100644
--- a/app/models/types_de_champ/rnf_type_de_champ.rb
+++ b/app/models/types_de_champ/rnf_type_de_champ.rb
@@ -1,4 +1,56 @@
+# frozen_string_literal: true
+
class TypesDeChamp::RNFTypeDeChamp < TypesDeChamp::TextTypeDeChamp
+ include AddressableColumnConcern
+
+ def champ_value_for_export(champ, path = :value)
+ case path
+ when :value
+ champ.rnf_id
+ when :departement
+ champ.departement_code_and_name
+ when :code_insee
+ champ.commune&.fetch(:code)
+ when :address
+ champ.full_address
+ when :nom
+ champ.title
+ end
+ end
+
+ def champ_value_for_tag(champ, path = :value)
+ case path
+ when :value
+ champ.rnf_id
+ when :departement
+ champ.departement_code_and_name || ''
+ when :code_insee
+ champ.commune&.fetch(:code) || ''
+ when :address
+ champ.full_address || ''
+ when :nom
+ champ.title || ''
+ end
+ end
+
+ def champ_blank?(champ) = champ.external_id.blank?
+
+ def columns(procedure:, displayable: true, prefix: nil)
+ super
+ .concat(addressable_columns(procedure:, displayable:, prefix:))
+ .concat([
+ Columns::JSONPathColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: "#{libelle_with_prefix(prefix)} – Titre au répertoire national des fondations ",
+ type: :text,
+ jsonpath: '$.title',
+ displayable:
+ )
+ ])
+ end
+
private
def paths
diff --git a/app/models/types_de_champ/siret_type_de_champ.rb b/app/models/types_de_champ/siret_type_de_champ.rb
index 26b653cf6..5786a071f 100644
--- a/app/models/types_de_champ/siret_type_de_champ.rb
+++ b/app/models/types_de_champ/siret_type_de_champ.rb
@@ -1,5 +1,15 @@
+# frozen_string_literal: true
+
class TypesDeChamp::SiretTypeDeChamp < TypesDeChamp::TypeDeChampBase
+ include AddressableColumnConcern
+
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
+
+ def champ_blank_or_invalid?(champ) = Siret.new(siret: champ.value).invalid?
+
+ def columns(procedure:, displayable: true, prefix: nil)
+ super.concat(addressable_columns(procedure:, displayable:, prefix:))
+ end
end
diff --git a/app/models/types_de_champ/text_type_de_champ.rb b/app/models/types_de_champ/text_type_de_champ.rb
index 437a1ef8a..50b1e2c0d 100644
--- a/app/models/types_de_champ/text_type_de_champ.rb
+++ b/app/models/types_de_champ/text_type_de_champ.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class TypesDeChamp::TextTypeDeChamp < TypesDeChamp::TypeDeChampBase
end
diff --git a/app/models/types_de_champ/textarea_type_de_champ.rb b/app/models/types_de_champ/textarea_type_de_champ.rb
index 3c92afb1e..d16309498 100644
--- a/app/models/types_de_champ/textarea_type_de_champ.rb
+++ b/app/models/types_de_champ/textarea_type_de_champ.rb
@@ -1,5 +1,11 @@
+# frozen_string_literal: true
+
class TypesDeChamp::TextareaTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
+
+ def champ_value_for_export(champ, path = :value)
+ ActionView::Base.full_sanitizer.sanitize(champ.value)
+ end
end
diff --git a/app/models/types_de_champ/titre_identite_type_de_champ.rb b/app/models/types_de_champ/titre_identite_type_de_champ.rb
index 36ff0cd52..ed929dec4 100644
--- a/app/models/types_de_champ/titre_identite_type_de_champ.rb
+++ b/app/models/types_de_champ/titre_identite_type_de_champ.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypesDeChamp::TitreIdentiteTypeDeChamp < TypesDeChamp::TypeDeChampBase
FRANCE_CONNECT = 'france_connect'
PIECE_JUSTIFICATIVE = 'piece_justificative'
@@ -7,4 +9,32 @@ class TypesDeChamp::TitreIdentiteTypeDeChamp < TypesDeChamp::TypeDeChampBase
end
def tags_for_template = [].freeze
+
+ def champ_value_for_export(champ, path = :value)
+ champ.piece_justificative_file.attached? ? "présent" : "absent"
+ end
+
+ def champ_value_for_api(champ, version: 2)
+ nil
+ end
+
+ def champ_default_export_value(path = :value)
+ "absent"
+ end
+
+ def champ_blank?(champ) = champ.piece_justificative_file.blank?
+
+ def columns(procedure:, displayable: nil, prefix: nil)
+ [
+ Columns::AttachedManyColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: libelle_with_prefix(prefix),
+ type: TypeDeChamp.column_type(type_champ),
+ displayable: false,
+ filterable: false
+ )
+ ]
+ end
end
diff --git a/app/models/types_de_champ/type_de_champ_base.rb b/app/models/types_de_champ/type_de_champ_base.rb
index db0d38d9b..86ab644f5 100644
--- a/app/models/types_de_champ/type_de_champ_base.rb
+++ b/app/models/types_de_champ/type_de_champ_base.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class TypesDeChamp::TypeDeChampBase
include ActiveModel::Validations
- delegate :description, :libelle, :mandatory, :mandatory?, :stable_id, :fillable?, :public?, to: :@type_de_champ
+ delegate :description, :libelle, :mandatory, :mandatory?, :stable_id, :fillable?, :public?, :type_champ, :options_for_select, :drop_down_options, :drop_down_other?, to: :@type_de_champ
FILL_DURATION_SHORT = 10.seconds
FILL_DURATION_MEDIUM = 1.minute
@@ -13,12 +15,12 @@ class TypesDeChamp::TypeDeChampBase
end
def tags_for_template
- tdc = @type_de_champ
+ type_de_champ = @type_de_champ
paths.map do |path|
path.merge(
libelle: TagsSubstitutionConcern::TagsParser.normalize(path[:libelle]),
id: path[:path] == :value ? "tdc#{stable_id}" : "tdc#{stable_id}/#{path[:path]}",
- lambda: -> (dossier) { dossier.project_champ(tdc, nil).for_tag(path[:path]) }
+ lambda: -> (dossier) { dossier.champ_value_for_tag(type_de_champ, path[:path]) }
)
end
end
@@ -52,12 +54,71 @@ class TypesDeChamp::TypeDeChampBase
filter_value
end
- def human_to_filter(human_value)
- human_value
+ def champ_value(champ)
+ champ.value.present? ? champ.value.to_s : champ_default_value
+ end
+
+ def champ_value_for_api(champ, version: 2)
+ case version
+ when 2
+ champ_value(champ)
+ else
+ champ.value.presence || champ_default_api_value(version)
+ end
+ end
+
+ def champ_value_for_export(champ, path = :value)
+ path == :value ? champ.value.presence : champ_default_export_value(path)
+ end
+
+ def champ_value_for_tag(champ, path = :value)
+ path == :value ? champ_value(champ) : nil
+ end
+
+ def champ_default_value
+ ''
+ end
+
+ def champ_default_export_value(path = :value)
+ nil
+ end
+
+ def champ_default_api_value(version = 2)
+ case version
+ when 2
+ ''
+ else
+ nil
+ end
+ end
+
+ def champ_blank?(champ) = champ.value.blank?
+ def champ_blank_or_invalid?(champ) = champ_blank?(champ)
+
+ def columns(procedure:, displayable: true, prefix: nil)
+ if fillable?
+ [
+ Columns::ChampColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: libelle_with_prefix(prefix),
+ type: TypeDeChamp.column_type(type_champ),
+ displayable:,
+ options_for_select:
+ )
+ ]
+ else
+ []
+ end
end
private
+ def libelle_with_prefix(prefix)
+ [prefix, libelle].compact.join(' – ')
+ end
+
def paths
[
{
diff --git a/app/models/types_de_champ/yes_no_type_de_champ.rb b/app/models/types_de_champ/yes_no_type_de_champ.rb
index 25dae97bc..1821ce4cb 100644
--- a/app/models/types_de_champ/yes_no_type_de_champ.rb
+++ b/app/models/types_de_champ/yes_no_type_de_champ.rb
@@ -1,4 +1,6 @@
-class TypesDeChamp::YesNoTypeDeChamp < TypesDeChamp::CheckboxTypeDeChamp
+# frozen_string_literal: true
+
+class TypesDeChamp::YesNoTypeDeChamp < TypesDeChamp::TypeDeChampBase
def filter_to_human(filter_value)
if filter_value == "true"
I18n.t('activerecord.attributes.type_de_champ.type_champs.yes_no_true')
@@ -9,14 +11,34 @@ class TypesDeChamp::YesNoTypeDeChamp < TypesDeChamp::CheckboxTypeDeChamp
end
end
- def human_to_filter(human_value)
- human_value.downcase!
- if human_value == "oui"
- "true"
- elsif human_value == "non"
- "false"
+ def champ_value(champ)
+ champ_value_true?(champ) ? 'Oui' : 'Non'
+ end
+
+ def champ_value_for_export(champ, path = :value)
+ champ_value_true?(champ) ? 'Oui' : 'Non'
+ end
+
+ def champ_value_for_api(champ, version: 2)
+ case version
+ when 2
+ champ_value_true?(champ).to_s
else
- human_value
+ super
end
end
+
+ def champ_default_value
+ ''
+ end
+
+ def champ_default_export_value(path = :value)
+ ''
+ end
+
+ private
+
+ def champ_value_true?(champ)
+ champ.value == 'true'
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 3a9aebfb2..e6a659e43 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class User < ApplicationRecord
include DomainMigratableConcern
include EmailSanitizableConcern
@@ -42,10 +44,6 @@ class User < ApplicationRecord
# plug our custom validation a la devise (same options) https://github.com/heartcombo/devise/blob/main/lib/devise/models/validatable.rb#L30
validates :email, strict_email: true, allow_blank: true, if: :devise_will_save_change_to_email?
- def validate_password_complexity?
- administrateur?
- end
-
# Override of Devise::Models::Confirmable#send_confirmation_instructions
def send_confirmation_instructions
unless @raw_confirmation_token
@@ -78,10 +76,28 @@ class User < ApplicationRecord
owns?(dossier) || invite?(dossier)
end
- def invite!
+ def invite_instructeur!
UserMailer.invite_instructeur(self, set_reset_password_token).deliver_later
end
+ def invite_tiers!(dossier)
+ token = SecureRandom.hex(10)
+ self.update!(confirmation_token: token, confirmation_sent_at: Time.zone.now)
+ UserMailer.invite_tiers(self, token, dossier).deliver_later
+ end
+
+ def invite_expert_and_send_avis!(avis)
+ token = SecureRandom.hex(10)
+ self.update!(confirmation_token: token, confirmation_sent_at: Time.zone.now)
+ AvisMailer.avis_invitation_and_confirm_email(self, token, avis).deliver_later
+ end
+
+ def resend_confirmation_email!
+ token = SecureRandom.hex(10)
+ self.update!(confirmation_token: token, confirmation_sent_at: Time.zone.now)
+ UserMailer.resend_confirmation_email(self, token).deliver_later
+ end
+
def invite_gestionnaire!(groupe_gestionnaire)
UserMailer.invite_gestionnaire(self, set_reset_password_token, groupe_gestionnaire).deliver_later
end
@@ -96,10 +112,16 @@ class User < ApplicationRecord
AdministrateurMailer.activate_before_expiration(self, reset_password_token).deliver_later
end
- def self.create_or_promote_to_instructeur(email, password, administrateurs: [])
- user = User
- .create_with(password: password, confirmed_at: Time.zone.now)
- .find_or_create_by(email: email)
+ def self.create_or_promote_to_instructeur(email, password, administrateurs: [], agent_connect: false)
+ if agent_connect
+ user = User
+ .create_with(password: password, confirmed_at: Time.zone.now, email_verified_at: Time.zone.now)
+ .find_or_create_by(email: email)
+ else
+ user = User
+ .create_with(password: password, confirmed_at: Time.zone.now)
+ .find_or_create_by(email: email)
+ end
if user.valid?
if user.instructeur.nil?
@@ -123,6 +145,17 @@ class User < ApplicationRecord
user
end
+ def self.create_or_promote_to_tiers(email, password, dossier = nil)
+ user = User
+ .create_with(password: password, confirmed_at: Time.zone.now)
+ .find_or_create_by(email: email)
+
+ if user.valid? && user.unverified_email?
+ user.invite_tiers!(dossier)
+ end
+ user
+ end
+
def self.create_or_promote_to_administrateur(email, password)
user = User.create_or_promote_to_instructeur(email, password)
@@ -140,10 +173,8 @@ class User < ApplicationRecord
.create_with(password: password, confirmed_at: Time.zone.now)
.find_or_create_by(email: email)
- if user.valid?
- if user.expert.nil?
- user.create_expert!
- end
+ if user.valid? && user.expert.nil?
+ user.create_expert!
end
user
@@ -250,12 +281,8 @@ class User < ApplicationRecord
end
def ask_for_merge(requested_user)
- if update(requested_merge_into: requested_user)
- UserMailer.ask_for_merge(self, requested_user.email).deliver_later
- return true
- else
- return false
- end
+ update!(requested_merge_into: requested_user, unconfirmed_email: nil)
+ UserMailer.ask_for_merge(self, requested_user.email).deliver_later
end
def send_devise_notification(notification, *args)
@@ -266,6 +293,8 @@ class User < ApplicationRecord
super && blocked_at.nil?
end
+ def unverified_email? = !email_verified_at?
+
private
def does_not_merge_on_self
diff --git a/app/models/zone.rb b/app/models/zone.rb
index 20829bc81..64f288b63 100644
--- a/app/models/zone.rb
+++ b/app/models/zone.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Zone < ApplicationRecord
validates :acronym, presence: true, uniqueness: true
has_many :labels, -> { order(designated_on: :desc) }, class_name: 'ZoneLabel', inverse_of: :zone
diff --git a/app/models/zone_label.rb b/app/models/zone_label.rb
index 926488b75..1ddf1f32a 100644
--- a/app/models/zone_label.rb
+++ b/app/models/zone_label.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ZoneLabel < ApplicationRecord
belongs_to :zone
end
diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb
index dfebff858..e8e816cc3 100644
--- a/app/policies/application_policy.rb
+++ b/app/policies/application_policy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationPolicy
attr_reader :user, :record
diff --git a/app/policies/champ_policy.rb b/app/policies/dossier_policy.rb
similarity index 64%
rename from app/policies/champ_policy.rb
rename to app/policies/dossier_policy.rb
index 1376abe40..fff3aa1d0 100644
--- a/app/policies/champ_policy.rb
+++ b/app/policies/dossier_policy.rb
@@ -1,7 +1,9 @@
-class ChampPolicy < ApplicationPolicy
- # Scope for WRITING to a champ.
+# frozen_string_literal: true
+
+class DossierPolicy < ApplicationPolicy
+ # Scope for WRITING to a dossier.
#
- # (If the need for a scope to READ a champ emerges, we can implement another scope
+ # (If the need for a scope to READ a dossier emerges, we can implement another scope
# in this file, following this example: https://github.com/varvet/pundit/issues/368#issuecomment-196111115)
class Scope < ApplicationScope
def resolve
@@ -11,33 +13,29 @@ class ChampPolicy < ApplicationPolicy
# The join must be the same for all elements of the WHERE clause.
#
- # NB: here we want to do `.left_outer_joins(dossier: [:invites, { :groupe_instructeur: :instructeurs }]))`,
+ # NB: here we want to do `.left_outer_joins(:invites, { :groupe_instructeur: :instructeurs })`,
# but for some reasons ActiveRecord <= 5.2 generates bogus SQL. Hence the manual version of it below.
joined_scope = scope
- .joins('LEFT OUTER JOIN dossiers ON dossiers.id = champs.dossier_id')
.joins('LEFT OUTER JOIN invites ON invites.dossier_id = dossiers.id OR invites.dossier_id = dossiers.editing_fork_origin_id')
.joins('LEFT OUTER JOIN groupe_instructeurs ON groupe_instructeurs.id = dossiers.groupe_instructeur_id')
.joins('LEFT OUTER JOIN assign_tos ON assign_tos.groupe_instructeur_id = groupe_instructeurs.id')
.joins('LEFT OUTER JOIN instructeurs ON instructeurs.id = assign_tos.instructeur_id')
# Users can access public champs on their own dossiers.
- resolved_scope = joined_scope
- .where('dossiers.user_id': user.id, private: false)
+ resolved_scope = joined_scope.where(user_id: user.id)
# Invited users can access public champs on dossiers they are invited to
- invite_clause = joined_scope
- .where('invites.user_id': user.id, private: false)
+ invite_clause = joined_scope.where('invites.user_id': user.id)
resolved_scope = resolved_scope.or(invite_clause)
if instructeur.present?
# Additionnaly, instructeurs can access private champs
# on dossiers they are allowed to instruct.
- instructeur_clause = joined_scope
- .where('instructeurs.id': instructeur.id, private: true)
+ instructeur_clause = joined_scope.where('instructeurs.id': instructeur.id)
resolved_scope = resolved_scope.or(instructeur_clause)
end
- resolved_scope.or(joined_scope.where('dossiers.for_procedure_preview': true))
+ resolved_scope.or(joined_scope.where(for_procedure_preview: true))
end
end
end
diff --git a/app/policies/type_de_champ_policy.rb b/app/policies/type_de_champ_policy.rb
deleted file mode 100644
index 98bd57ec6..000000000
--- a/app/policies/type_de_champ_policy.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class TypeDeChampPolicy < ApplicationPolicy
- class Scope < ApplicationScope
- def resolve
- if administrateur.present?
- scope
- .joins(procedure: [:administrateurs])
- .where({ administrateurs: { id: administrateur.id } })
- else
- scope.none
- end
- end
- end
-end
diff --git a/app/schemas/rnf.json b/app/schemas/rnf.json
index 866821090..b17244751 100644
--- a/app/schemas/rnf.json
+++ b/app/schemas/rnf.json
@@ -30,7 +30,6 @@
"regionName":{ "type": "string" },
"regionCode":{ "type": "string" }
},
- "status": { "type": ["string", "null"] },
"persons": { "type": "array" }
}
}
diff --git a/app/schemas/service-public.json b/app/schemas/service-public.json
new file mode 100644
index 000000000..f5ceb71c5
--- /dev/null
+++ b/app/schemas/service-public.json
@@ -0,0 +1,76 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://demarches-simplifiees.fr/service-public.schema.json",
+ "title": "Service Public",
+ "type": "object",
+ "properties": {
+ "total_count": {
+ "type": "integer"
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "plage_ouverture": { "type": ["string", "null"] },
+ "site_internet": { "type": ["string", "null"] },
+ "copyright": { "type": ["string", "null"] },
+ "siren": { "type": ["string", "null"] },
+ "ancien_code_pivot": { "type": ["string", "null"] },
+ "reseau_social": { "type": ["string", "null"] },
+ "texte_reference": { "type": ["string", "null"] },
+ "partenaire": { "type": ["string", "null"] },
+ "telecopie": { "type": ["string", "null"] },
+ "nom": { "type": "string" },
+ "siret": { "type": ["string", "null"] },
+ "itm_identifiant": { "type": ["string", "null"] },
+ "sigle": { "type": ["string", "null"] },
+ "affectation_personne": { "type": ["string", "null"] },
+ "date_modification": { "type": "string" },
+ "adresse_courriel": { "type": ["string", "null"] },
+ "service_disponible": { "type": ["string", "null"] },
+ "organigramme": { "type": ["string", "null"] },
+ "pivot": { "type": ["string", "null"] },
+ "partenaire_identifiant": { "type": ["string", "null"] },
+ "ancien_identifiant": { "type": ["string", "null"] },
+ "id": { "type": "string" },
+ "ancien_nom": { "type": ["string", "null"] },
+ "commentaire_plage_ouverture": { "type": ["string", "null"] },
+ "annuaire": { "type": ["string", "null"] },
+ "tchat": { "type": ["string", "null"] },
+ "hierarchie": { "type": ["string", "null"] },
+ "categorie": { "type": "string" },
+ "sve": { "type": ["string", "null"] },
+ "telephone_accessible": { "type": ["string", "null"] },
+ "application_mobile": { "type": ["string", "null"] },
+ "version_type": { "type": "string" },
+ "type_repertoire": { "type": ["string", "null"] },
+ "telephone": { "type": ["string", "null"] },
+ "version_etat_modification": { "type": ["string", "null"] },
+ "date_creation": { "type": "string" },
+ "partenaire_date_modification": { "type": ["string", "null"] },
+ "mission": { "type": ["string", "null"] },
+ "formulaire_contact": { "type": ["string", "null"] },
+ "version_source": { "type": ["string", "null"] },
+ "type_organisme": { "type": ["string", "null"] },
+ "code_insee_commune": { "type": ["string", "null"] },
+ "statut_de_diffusion": { "type": ["string", "null"] },
+ "adresse": { "type": ["string", "null"] },
+ "url_service_public": { "type": ["string", "null"] },
+ "information_complementaire": { "type": ["string", "null"] },
+ "date_diffusion": { "type": ["string", "null"] }
+ },
+ "required": [
+ "id",
+ "nom",
+ "categorie",
+ "adresse",
+ "adresse_courriel",
+ "telephone",
+ "plage_ouverture"
+ ]
+ }
+ }
+ },
+ "required": ["total_count", "results"]
+}
diff --git a/app/serializers/avis_serializer.rb b/app/serializers/avis_serializer.rb
index a1212c0a7..94b116da8 100644
--- a/app/serializers/avis_serializer.rb
+++ b/app/serializers/avis_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AvisSerializer < ActiveModel::Serializer
attributes :answer,
:introduction,
diff --git a/app/serializers/champ_serializer.rb b/app/serializers/champ_serializer.rb
index 003f611a5..4ba762f05 100644
--- a/app/serializers/champ_serializer.rb
+++ b/app/serializers/champ_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ChampSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
@@ -15,7 +17,7 @@ class ChampSerializer < ActiveModel::Serializer
when GeoArea
object.geometry
else
- object.for_api
+ object.type_de_champ.champ_value_for_api(object, version: 1)
end
end
@@ -46,11 +48,7 @@ class ChampSerializer < ActiveModel::Serializer
end
def rows
- object.dossier
- .champs_for_revision(scope: object.type_de_champ)
- .group_by(&:row_id)
- .values
- .map.with_index(1) { |champs, index| Row.new(index:, champs:) }
+ object.rows.map.with_index(1) { |champs, index| Row.new(index:, champs:) }
end
def include_etablissement?
diff --git a/app/serializers/commentaire_serializer.rb b/app/serializers/commentaire_serializer.rb
index 3cc79c8a2..96d2b74c5 100644
--- a/app/serializers/commentaire_serializer.rb
+++ b/app/serializers/commentaire_serializer.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
class CommentaireSerializer < ActiveModel::Serializer
attributes :email,
:body,
:created_at,
+ :piece_jointe_attachments,
:attachment
def created_at
@@ -9,6 +12,10 @@ class CommentaireSerializer < ActiveModel::Serializer
end
def attachment
- object.file_url
+ piece_jointe = object.piece_jointe_attachments.first
+
+ if piece_jointe&.virus_scanner&.safe?
+ Rails.application.routes.url_helpers.url_for(piece_jointe)
+ end
end
end
diff --git a/app/serializers/dossier_serializer.rb b/app/serializers/dossier_serializer.rb
index 81e783bba..33bc5e75c 100644
--- a/app/serializers/dossier_serializer.rb
+++ b/app/serializers/dossier_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossierSerializer < ActiveModel::Serializer
include DossierHelper
@@ -30,7 +32,7 @@ class DossierSerializer < ActiveModel::Serializer
has_many :champs, serializer: ChampSerializer
def champs
- champs = object.champs_public.reject { |c| c.type_de_champ.old_pj.present? }
+ champs = object.project_champs_public.reject { |c| c.type_de_champ.old_pj.present? }
if object.expose_legacy_carto_api?
champ_carte = champs.find do |champ|
@@ -50,16 +52,20 @@ class DossierSerializer < ActiveModel::Serializer
champs
end
+ def champs_private
+ object.project_champs_private
+ end
+
def cerfa
[]
end
def pieces_justificatives
- object.champs_public.filter { |champ| champ.type_de_champ.old_pj }.map do |champ|
+ object.project_champs_public.filter { |champ| champ.type_de_champ.old_pj }.map do |champ|
{
created_at: champ.created_at&.in_time_zone('UTC'),
type_de_piece_justificative_id: champ.type_de_champ.old_pj[:stable_id],
- content_url: champ.for_api,
+ content_url: champ.type_de_champ.champ_value_for_api(champ, version: 1),
user: champ.dossier.user
}
end.flatten
diff --git a/app/serializers/dossiers_serializer.rb b/app/serializers/dossiers_serializer.rb
index d94e1ba6e..80961600f 100644
--- a/app/serializers/dossiers_serializer.rb
+++ b/app/serializers/dossiers_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossiersSerializer < ActiveModel::Serializer
include DossierHelper
diff --git a/app/serializers/entreprise_serializer.rb b/app/serializers/entreprise_serializer.rb
index 602f9e69b..c4b6a2b46 100644
--- a/app/serializers/entreprise_serializer.rb
+++ b/app/serializers/entreprise_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EntrepriseSerializer < ActiveModel::Serializer
attributes :siren,
:capital_social,
diff --git a/app/serializers/etablissement_serializer.rb b/app/serializers/etablissement_serializer.rb
index 6cd091859..2e4c96146 100644
--- a/app/serializers/etablissement_serializer.rb
+++ b/app/serializers/etablissement_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EtablissementSerializer < ActiveModel::Serializer
attributes :siret,
:siege_social,
diff --git a/app/serializers/geo_area_serializer.rb b/app/serializers/geo_area_serializer.rb
index 39fb6cfd3..5620ca29b 100644
--- a/app/serializers/geo_area_serializer.rb
+++ b/app/serializers/geo_area_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GeoAreaSerializer < ActiveModel::Serializer
attributes :geometry, :source, :geo_reference_id
diff --git a/app/serializers/individual_serializer.rb b/app/serializers/individual_serializer.rb
index 0ef0f4207..3f85b860a 100644
--- a/app/serializers/individual_serializer.rb
+++ b/app/serializers/individual_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class IndividualSerializer < ActiveModel::Serializer
attribute :gender, key: :civilite
attributes :nom, :prenom
diff --git a/app/serializers/logic_serializer.rb b/app/serializers/logic_serializer.rb
index 90cec89af..44f21b45b 100644
--- a/app/serializers/logic_serializer.rb
+++ b/app/serializers/logic_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LogicSerializer
def self.load(logic)
if logic.present?
diff --git a/app/serializers/module_api_carto_serializer.rb b/app/serializers/module_api_carto_serializer.rb
index bbc19de16..3fde0635d 100644
--- a/app/serializers/module_api_carto_serializer.rb
+++ b/app/serializers/module_api_carto_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ModuleAPICartoSerializer < ActiveModel::Serializer
attributes :use_api_carto, :cadastre
end
diff --git a/app/serializers/procedure_serializer.rb b/app/serializers/procedure_serializer.rb
index 2659a58a5..556192f0d 100644
--- a/app/serializers/procedure_serializer.rb
+++ b/app/serializers/procedure_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProcedureSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
diff --git a/app/serializers/row_serializer.rb b/app/serializers/row_serializer.rb
index b7aa2affd..d257fc55a 100644
--- a/app/serializers/row_serializer.rb
+++ b/app/serializers/row_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RowSerializer < ActiveModel::Serializer
has_many :champs, serializer: ChampSerializer
diff --git a/app/serializers/service_serializer.rb b/app/serializers/service_serializer.rb
index 12586202c..d8e3b4f26 100644
--- a/app/serializers/service_serializer.rb
+++ b/app/serializers/service_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ServiceSerializer < ActiveModel::Serializer
attributes :id, :email
attribute :nom, key: :name
diff --git a/app/serializers/type_de_champ_serializer.rb b/app/serializers/type_de_champ_serializer.rb
index bf07af980..d08be2694 100644
--- a/app/serializers/type_de_champ_serializer.rb
+++ b/app/serializers/type_de_champ_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TypeDeChampSerializer < ActiveModel::Serializer
attributes :id,
:libelle,
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index 4665c89d7..99d9b6126 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UserSerializer < ActiveModel::Serializer
attributes :email
end
diff --git a/app/services/administrateur_deletion_service.rb b/app/services/administrateur_deletion_service.rb
index 75d2ee458..f13b0b291 100644
--- a/app/services/administrateur_deletion_service.rb
+++ b/app/services/administrateur_deletion_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AdministrateurDeletionService
include Dry::Monads[:result]
diff --git a/app/services/agent_connect_service.rb b/app/services/agent_connect_service.rb
index b0d9de66f..6b2dc6ba1 100644
--- a/app/services/agent_connect_service.rb
+++ b/app/services/agent_connect_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AgentConnectService
include OpenIDConnect
@@ -12,10 +14,12 @@ class AgentConnectService
nonce = SecureRandom.hex(16)
uri = client.authorization_uri(
- scope: [:openid, :email, :given_name, :usual_name, :organizational_unit, :belonging_population, :siret],
+ scope: [:openid, :email, :given_name, :usual_name, :organizational_unit, :belonging_population, :siret, :idp_id],
state:,
nonce:,
- acr_values: 'eidas1'
+ acr_values: 'eidas1',
+ claims: { id_token: { amr: { essential: true } } }.to_json,
+ prompt: :login
)
[uri, state, nonce]
@@ -30,7 +34,15 @@ class AgentConnectService
id_token = ResponseObject::IdToken.decode(access_token.id_token, conf[:jwks])
id_token.verify!(conf.merge(nonce: nonce))
- [access_token.userinfo!.raw_attributes, access_token.id_token]
+ amr = id_token.amr.present? ? JSON.parse(id_token.amr) : []
+
+ [access_token.userinfo!.raw_attributes, access_token.id_token, amr]
+ end
+
+ def self.logout_url(id_token, host_with_port:)
+ app_logout = Rails.application.routes.url_helpers.logout_url(host: host_with_port)
+ h = { id_token_hint: id_token, post_logout_redirect_uri: app_logout }
+ "#{AGENT_CONNECT[:end_session_endpoint]}?#{h.to_query}"
end
private
diff --git a/app/services/annuaire_service_public_service.rb b/app/services/annuaire_service_public_service.rb
new file mode 100644
index 000000000..7c5728fd8
--- /dev/null
+++ b/app/services/annuaire_service_public_service.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+class AnnuaireServicePublicService
+ include Dry::Monads[:result]
+
+ def call(siret:)
+ result = API::Client.new.call(url: url(siret), schema:, timeout: 1.second)
+
+ case result
+ in Success(body:)
+ result = body[:results].first
+
+ if result.present?
+ Success(
+ result.slice(:nom, :adresse, :adresse_courriel).merge(
+ telephone: maybe_json_parse(result[:telephone]),
+ plage_ouverture: maybe_json_parse(result[:plage_ouverture]),
+ adresse: maybe_json_parse(result[:adresse])
+ )
+ )
+ else
+ Failure(API::Client::Error[:not_found, 404, false, "No result found for this SIRET."])
+ end
+ in Failure(code:, reason:) if code.in?(401..403)
+ Sentry.capture_message("#{self.class.name}: #{reason} code: #{code}", extra: { siret: })
+ Failure(API::Client::Error[:unauthorized, code, false, reason])
+ in Failure(type: :schema, code:, reason:)
+ reason.errors[0].first
+ Sentry.capture_exception(reason, extra: { siret:, code: })
+
+ Failure(API::Client::Error[:schema, code, false, reason])
+ else
+ result
+ end
+ end
+
+ private
+
+ def schema
+ JSONSchemer.schema(Rails.root.join('app/schemas/service-public.json'))
+ end
+
+ def url(siret)
+ "https://api-lannuaire.service-public.fr/api/explore/v2.1/catalog/datasets/api-lannuaire-administration/records?where=siret:#{siret}"
+ end
+
+ def maybe_json_parse(value)
+ return nil if value.blank?
+
+ JSON.parse(value)
+ end
+end
diff --git a/app/services/api_bretagne_service.rb b/app/services/api_bretagne_service.rb
index 1eb319c24..765788dfa 100644
--- a/app/services/api_bretagne_service.rb
+++ b/app/services/api_bretagne_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIBretagneService
include Dry::Monads[:result]
HOST = 'https://api.databretagne.fr'
diff --git a/app/services/api_entreprise_service.rb b/app/services/api_entreprise_service.rb
index 188f6d23d..f73ccbcb0 100644
--- a/app/services/api_entreprise_service.rb
+++ b/app/services/api_entreprise_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIEntrepriseService
class << self
# create etablissement with EtablissementAdapter
@@ -21,6 +23,10 @@ class APIEntrepriseService
etablissement = dossier_or_champ.build_etablissement(etablissement_params)
etablissement.save!
+ if dossier_or_champ.is_a?(Champ)
+ dossier_or_champ.update!(value_json: APIGeoService.parse_etablissement_address(etablissement))
+ end
+
perform_later_fetch_jobs(etablissement, procedure_id, user_id)
etablissement
@@ -43,15 +49,25 @@ class APIEntrepriseService
return nil if etablissement_params.empty?
etablissement.update!(etablissement_params)
+
+ if etablissement.champ.present?
+ etablissement.champ.update!(value_json: APIGeoService.parse_etablissement_address(etablissement))
+ end
+
+ etablissement
end
def perform_later_fetch_jobs(etablissement, procedure_id, user_id, wait: nil)
- [
+ jobs = [
APIEntreprise::EntrepriseJob, APIEntreprise::ExtraitKbisJob, APIEntreprise::TvaJob,
APIEntreprise::AssociationJob, APIEntreprise::ExercicesJob,
APIEntreprise::EffectifsJob, APIEntreprise::EffectifsAnnuelsJob, APIEntreprise::AttestationSocialeJob,
APIEntreprise::BilansBdfJob
- ].each do |job|
+ ]
+ if etablissement.as_degraded_mode?
+ jobs << APIEntreprise::EtablissementJob
+ end
+ jobs.each do |job|
job.set(wait:).perform_later(etablissement.id, procedure_id)
end
@@ -67,6 +83,13 @@ class APIEntrepriseService
api_up?("https://entreprise.api.gouv.fr/ping/djepva/api-association")
end
+ def service_unavailable_error?(error, target:)
+ return false if !error.try(:network_error?)
+ return true if target == :insee && !APIEntrepriseService.api_insee_up?
+ return true if target == :djepva && !APIEntrepriseService.api_djepva_up?
+ error.is_a?(APIEntreprise::API::Error::ServiceUnavailable)
+ end
+
private
def api_up?(url)
diff --git a/app/services/api_geo_service.rb b/app/services/api_geo_service.rb
index f1ed8c228..19931cc19 100644
--- a/app/services/api_geo_service.rb
+++ b/app/services/api_geo_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIGeoService
class << self
def countries(locale: I18n.locale)
@@ -25,6 +27,8 @@ class APIGeoService
get_from_api_geo(:regions).sort_by { I18n.transliterate(_1[:name]) }
end
+ def region_options = regions.map { [_1[:name], _1[:code]] }
+
def region_name(code)
regions.find { _1[:code] == code }&.dig(:name)
end
@@ -40,13 +44,21 @@ class APIGeoService
end
def departements
- [{ code: '99', name: 'Etranger' }] + get_from_api_geo(:departements).sort_by { _1[:code] }
+ ([{ code: '99', name: 'Etranger' }] + get_from_api_geo(:departements)).sort_by { _1[:code] }
+ end
+
+ def departement_options
+ departements.map { ["#{_1[:code]} – #{_1[:name]}", _1[:code]] }
end
def departement_name(code)
departements.find { _1[:code] == code }&.dig(:name)
end
+ def departement_name_by_postal_code(postal_code)
+ APIGeoService.departement_name(postal_code[0..2]) || APIGeoService.departement_name(postal_code[0..1])
+ end
+
def departement_code(name)
return if name.nil?
departements.find { _1[:name] == name }&.dig(:code)
@@ -78,6 +90,14 @@ class APIGeoService
communes(departement_code).find { _1[:code] == code }&.dig(:name)
end
+ def commune_by_name_or_postal_code(query)
+ if postal_code?(query)
+ fetch_by_postal_code(query)
+ else
+ fetch_by_name(query)
+ end
+ end
+
def commune_code(departement_code, name)
communes(departement_code).find { _1[:name] == name }&.dig(:code)
end
@@ -122,14 +142,154 @@ class APIGeoService
}.merge(territory)
end
+ def parse_rna_address(address)
+ postal_code = address[:code_postal]
+ city_name_fallback = address[:commune]
+ city_code = address[:code_insee]
+ department_code, region_code = if postal_code.present? && city_code.present?
+ commune = communes_by_postal_code(postal_code).find { _1[:code] == city_code }
+ if commune.present?
+ [commune[:departement_code], commune[:region_code]]
+ else
+ []
+ end
+ end
+
+ department_name = departement_name(department_code)
+ {
+ street_number: address[:numero_voie],
+ street_name: address[:libelle_voie],
+ street_address: address[:libelle_voie].present? ? [address[:numero_voie], address[:type_voie], address[:libelle_voie]].compact.join(' ') : nil,
+ postal_code: postal_code.presence || '',
+ city_name: safely_normalize_city_name(department_code, city_code, city_name_fallback),
+ city_code: city_code.presence || '',
+ departement_code: department_code,
+ department_code:,
+ departement_name: department_name,
+ department_name:,
+ region_code:,
+ region_name: region_name(region_code)
+ }
+ end
+
+ def parse_rnf_address(address)
+ postal_code = address[:postalCode]
+ city_name_fallback = address[:cityName]
+ city_code = address[:cityCode]
+ department_code, region_code = if postal_code.present? && city_code.present?
+ commune = communes_by_postal_code(postal_code).find { _1[:code] == city_code }
+ if commune.present?
+ [commune[:departement_code], commune[:region_code]]
+ else
+ []
+ end
+ end
+ department_name = departement_name(department_code)
+
+ {
+ street_number: address[:streetNumber],
+ street_name: address[:streetName],
+ street_address: address[:streetAddress],
+ postal_code: postal_code.presence || '',
+ city_name: safely_normalize_city_name(department_code, city_code, city_name_fallback),
+ city_code: city_code.presence || '',
+ departement_code: department_code,
+ department_code:,
+ departement_name: department_name,
+ department_name:,
+ region_code:,
+ region_name: region_name(region_code)
+ }
+ end
+
+ def parse_etablissement_address(etablissement)
+ postal_code = etablissement.code_postal
+ city_name_fallback = etablissement.localite.presence || ''
+ city_code = etablissement.code_insee_localite
+ department_code, region_code = if postal_code.present? && city_code.present?
+ commune = communes_by_postal_code(postal_code).find { _1[:code] == city_code }
+ if commune.present?
+ [commune[:departement_code], commune[:region_code]]
+ else
+ []
+ end
+ end
+
+ department_name = departement_name(department_code)
+
+ {
+ street_number: etablissement.numero_voie,
+ street_name: etablissement.nom_voie,
+ street_address: etablissement.nom_voie.present? ? [etablissement.numero_voie, etablissement.type_voie, etablissement.nom_voie].compact.join(' ') : nil,
+ postal_code: postal_code.presence || '',
+ city_name: safely_normalize_city_name(department_code, city_code, city_name_fallback),
+ city_code: city_code.presence || '',
+ departement_code: department_code,
+ department_code:,
+ departement_name: department_name,
+ department_name:,
+ region_code:,
+ region_name: region_name(region_code)
+ }
+ end
+
def safely_normalize_city_name(department_code, city_code, fallback)
return fallback if department_code.blank? || city_code.blank?
commune_name(department_code, city_code) || fallback
end
+ def format_commune_response(results, with_combined_code)
+ results.reject(&method(:code_metropole?)).flat_map do |result|
+ item = {
+ name: result[:nom].tr("'", '’'),
+ code: result[:code],
+ epci_code: result[:codeEpci],
+ departement_code: result[:codeDepartement]
+ }.compact
+
+ if result[:codesPostaux].present?
+ result[:codesPostaux].map { item.merge(postal_code: _1) }
+ else
+ [item]
+ end.map do |item|
+ if with_combined_code.present?
+ {
+ label: "#{item[:name]} (#{item[:postal_code]})",
+ value: "#{item[:code]}-#{item[:postal_code]}"
+ }
+ else
+ {
+ label: "#{item[:name]} (#{item[:postal_code]})",
+ value: item[:code],
+ data: item[:postal_code]
+ }
+ end
+ end
+ end
+ end
+
+ def inline_service_public_address(address_data)
+ return nil if address_data.blank?
+
+ components = [
+ address_data['numero_voie'],
+ address_data['complement1'],
+ address_data['complement2'],
+ address_data['service_distribution'],
+ address_data['code_postal'],
+ address_data['nom_commune']
+ ].compact_blank
+
+ components.join(' ')
+ end
+
private
+ def code_metropole?(result)
+ result[:code].in?(['75056', '13055', '69123'])
+ end
+
def communes_by_postal_code_map
Rails.cache.fetch('api_geo_communes', expires_in: 1.day, version: 3) do
departements
@@ -164,6 +324,28 @@ class APIGeoService
private
+ def fetch_by_name(name)
+ Typhoeus.get("#{API_GEO_URL}/communes", params: {
+ type: 'commune-actuelle,arrondissement-municipal',
+ nom: name,
+ boost: 'population',
+ limit: 100
+ }, timeout: 3)
+ end
+
+ def fetch_by_postal_code(postal_code)
+ Typhoeus.get("#{API_GEO_URL}/communes", params: {
+ type: 'commune-actuelle,arrondissement-municipal',
+ codePostal: postal_code,
+ boost: 'population',
+ limit: 50
+ }, timeout: 3)
+ end
+
+ def postal_code?(string)
+ string.match?(/\A[-+]?\d+\z/) ? true : false
+ end
+
def ban_address_schema
JSONSchemer.schema(Rails.root.join('app/schemas/adresse-ban.json'))
end
diff --git a/app/services/api_recherche_entreprises_service.rb b/app/services/api_recherche_entreprises_service.rb
index f40287907..fe1cf177e 100644
--- a/app/services/api_recherche_entreprises_service.rb
+++ b/app/services/api_recherche_entreprises_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class APIRechercheEntreprisesService
include Dry::Monads[:result]
diff --git a/app/services/archive_uploader.rb b/app/services/archive_uploader.rb
index a1ad9023d..673c36b31 100644
--- a/app/services/archive_uploader.rb
+++ b/app/services/archive_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ArchiveUploader
# see: https://docs.ovh.com/fr/storage/pcs/capabilities-and-limitations/#max_file_size-5368709122-5gb
# officialy it's 5Gb. but let's avoid to reach the exact spot of the limit
diff --git a/app/services/auto_rotate_service.rb b/app/services/auto_rotate_service.rb
new file mode 100644
index 000000000..66e389e1b
--- /dev/null
+++ b/app/services/auto_rotate_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class AutoRotateService
+ def process(file, output)
+ auto_rotate_image(file, output)
+ end
+
+ private
+
+ def auto_rotate_image(file, output)
+ image = MiniMagick::Image.new(file.to_path)
+
+ return nil if !image.valid?
+
+ case image["%[orientation]"]
+ when 'LeftBottom'
+ rotate_image(file, output, 90)
+ when 'BottomRight'
+ rotate_image(file, output, 180)
+ when 'RightTop'
+ rotate_image(file, output, 270)
+ else
+ nil
+ end
+ end
+
+ def rotate_image(file, output, degree)
+ MiniMagick::Tool::Convert.new do |convert|
+ convert << file.to_path
+ convert.rotate(degree)
+ convert.auto_orient
+ convert << output.to_path
+ end
+ output
+ end
+end
diff --git a/app/services/bill_signature_service.rb b/app/services/bill_signature_service.rb
index 7badb3399..663310dcf 100644
--- a/app/services/bill_signature_service.rb
+++ b/app/services/bill_signature_service.rb
@@ -1,12 +1,6 @@
-class BillSignatureService
- def self.grouped_unsigned_operation_until(date)
- date = date.in_time_zone
- unsigned_operations = DossierOperationLog
- .where(bill_signature: nil)
- .where('executed_at < ?', date)
- unsigned_operations.group_by { |e| e.executed_at.to_date }
- end
+# frozen_string_literal: true
+class BillSignatureService
def self.sign_operations(operations, day)
return unless Certigna::API.enabled?
bill = BillSignature.build_with_operations(operations, day)
diff --git a/app/services/browser_support.rb b/app/services/browser_support.rb
index 399c2bb6d..482264151 100644
--- a/app/services/browser_support.rb
+++ b/app/services/browser_support.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BrowserSupport
def self.supported?(browser)
[
diff --git a/app/services/clamav_service.rb b/app/services/clamav_service.rb
index 5b7032a55..19ac22139 100644
--- a/app/services/clamav_service.rb
+++ b/app/services/clamav_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClamavService
def self.safe_file?(file_path)
return true if !Rails.configuration.x.clamav.enabled
diff --git a/app/services/clone_pieces_justificatives_service.rb b/app/services/clone_pieces_justificatives_service.rb
index d014b1c1c..18375d318 100644
--- a/app/services/clone_pieces_justificatives_service.rb
+++ b/app/services/clone_pieces_justificatives_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClonePiecesJustificativesService
def self.clone_attachments(original, kopy)
case original
diff --git a/app/services/cojo_service.rb b/app/services/cojo_service.rb
index 1c95ffc07..93c5c813d 100644
--- a/app/services/cojo_service.rb
+++ b/app/services/cojo_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class COJOService
include Dry::Monads[:result]
diff --git a/app/services/commentaire_service.rb b/app/services/commentaire_service.rb
index 5fb2e9821..a69772927 100644
--- a/app/services/commentaire_service.rb
+++ b/app/services/commentaire_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CommentaireService
def self.create(sender, dossier, params)
save(dossier, prepare_params(sender, params))
diff --git a/app/services/demarches_publiques_export_service.rb b/app/services/demarches_publiques_export_service.rb
index d210bc2c0..3a735b031 100644
--- a/app/services/demarches_publiques_export_service.rb
+++ b/app/services/demarches_publiques_export_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DemarchesPubliquesExportService
attr_reader :gzip_filename
diff --git a/app/services/dossier_filter_service.rb b/app/services/dossier_filter_service.rb
new file mode 100644
index 000000000..ce32fefc2
--- /dev/null
+++ b/app/services/dossier_filter_service.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+class DossierFilterService
+ TYPE_DE_CHAMP = 'type_de_champ'
+
+ def self.filtered_sorted_ids(dossiers, statut, filters, sorted_column, instructeur, count: nil)
+ dossiers_by_statut = dossiers.by_statut(statut, instructeur)
+ dossiers_sorted_ids = self.sorted_ids(dossiers_by_statut, sorted_column, instructeur, count || dossiers_by_statut.size)
+
+ if filters.present?
+ dossiers_sorted_ids.intersection(filtered_ids(dossiers_by_statut, filters))
+ else
+ dossiers_sorted_ids
+ end
+ end
+
+ private
+
+ def self.sorted_ids(dossiers, sorted_column, instructeur, count)
+ table = sorted_column.column.table
+ column = sorted_column.column.column
+ order = sorted_column.order
+
+ case table
+ when 'notifications'
+ dossiers_id_with_notification = dossiers.merge(instructeur.followed_dossiers).with_notifications.ids
+ if order == 'desc'
+ dossiers_id_with_notification +
+ (dossiers.order('dossiers.updated_at desc').ids - dossiers_id_with_notification)
+ else
+ (dossiers.order('dossiers.updated_at asc').ids - dossiers_id_with_notification) +
+ dossiers_id_with_notification
+ end
+ when TYPE_DE_CHAMP
+ stable_id = sorted_column.column.stable_id
+ ids = dossiers
+ .with_type_de_champ(stable_id)
+ .order("champs.value #{order}")
+ .pluck(:id)
+ if ids.size != count
+ rest = dossiers.where.not(id: ids).order(id: order).pluck(:id)
+ order == 'asc' ? ids + rest : rest + ids
+ else
+ ids
+ end
+ when 'followers_instructeurs'
+ assert_supported_column(table, column)
+ # LEFT OUTER JOIN allows to keep dossiers without assigned instructeurs yet
+ dossiers
+ .includes(:followers_instructeurs)
+ .joins('LEFT OUTER JOIN users instructeurs_users ON instructeurs_users.id = instructeurs.user_id')
+ .order("instructeurs_users.email #{order}")
+ .pluck(:id)
+ .uniq
+ when 'avis'
+ dossiers.includes(table)
+ .order("#{sanitized_column(table, column)} #{order}")
+ .pluck(:id)
+ .uniq
+ when 'dossier_labels'
+ dossiers.includes(:labels)
+ .order("labels.name #{order}")
+ .pluck(:id)
+ .uniq
+ when 'self', 'user', 'individual', 'etablissement', 'groupe_instructeur'
+ (table == 'self' ? dossiers : dossiers.includes(table))
+ .order("#{sanitized_column(table, column)} #{order}")
+ .pluck(:id)
+ end
+ end
+
+ def self.filtered_ids(dossiers, filters)
+ filters
+ .group_by { |filter| filter.column.then { [_1.table, _1.column] } }
+ .map do |(table, column), filters_for_column|
+ values = filters_for_column.map(&:filter)
+ filtered_column = filters_for_column.first.column
+
+ if filtered_column.respond_to?(:filtered_ids)
+ filtered_column.filtered_ids(dossiers, values)
+ else
+ case table
+ when 'self'
+ if filtered_column.type == :date || filtered_column.type == :datetime
+ dates = values
+ .filter_map { |v| Time.zone.parse(v).beginning_of_day rescue nil }
+
+ dossiers.filter_by_datetimes(column, dates)
+ elsif filtered_column.column == "state" && values.include?("pending_correction")
+ dossiers.joins(:corrections).where(corrections: DossierCorrection.pending)
+ elsif filtered_column.column == "state" && values.include?("en_construction")
+ dossiers.where("dossiers.#{column} IN (?)", values).includes(:corrections).where.not(corrections: DossierCorrection.pending)
+ else
+ dossiers.where("dossiers.#{column} IN (?)", values)
+ end
+ when 'etablissement'
+ if column == 'entreprise_date_creation'
+ dates = values
+ .filter_map { |v| v.to_date rescue nil }
+
+ dossiers
+ .includes(table)
+ .where(table.pluralize => { column => dates })
+ else
+ dossiers
+ .includes(table)
+ .filter_ilike(table, column, values)
+ end
+ when 'followers_instructeurs'
+ assert_supported_column(table, column)
+ dossiers
+ .includes(:followers_instructeurs)
+ .joins('INNER JOIN users instructeurs_users ON instructeurs_users.id = instructeurs.user_id')
+ .filter_ilike('instructeurs_users', :email, values) # ilike OK, user may want to search by *@domain
+ when 'user', 'individual' # user_columns: [email], individual_columns: ['nom', 'prenom', 'gender']
+ dossiers
+ .includes(table)
+ .filter_ilike(table, column, values) # ilike or where column == 'value' are both valid, we opted for ilike
+ when 'dossier_labels'
+ assert_supported_column(table, column)
+ dossiers
+ .joins(:dossier_labels)
+ .where(dossier_labels: { label_id: values })
+ when 'groupe_instructeur'
+ assert_supported_column(table, column)
+
+ dossiers
+ .joins(:groupe_instructeur)
+ .where(groupe_instructeur_id: values)
+ end.pluck(:id)
+ end
+ end.reduce(:&)
+ end
+
+ def self.sanitized_column(association, column)
+ table = if association == 'self'
+ Dossier.table_name
+ elsif (association_reflection = Dossier.reflect_on_association(association))
+ association_reflection.klass.table_name
+ else
+ # Allow filtering on a joined table alias (which doesn’t exist
+ # in the ActiveRecord domain).
+ association
+ end
+
+ [table, column]
+ .map { |name| ActiveRecord::Base.connection.quote_column_name(name) }
+ .join('.')
+ end
+
+ def self.assert_supported_column(table, column)
+ if table == 'followers_instructeurs' && column != 'email'
+ raise ArgumentError, 'Table `followers_instructeurs` only supports the `email` column.'
+ end
+ if table == 'groupe_instructeur' && (column != 'label' && column != 'id')
+ raise ArgumentError, 'Table `groupe_instructeur` only supports the `label` or `id` column.'
+ end
+ end
+end
diff --git a/app/services/dossier_projection_service.rb b/app/services/dossier_projection_service.rb
index 8727f07a5..3c3f2a438 100644
--- a/app/services/dossier_projection_service.rb
+++ b/app/services/dossier_projection_service.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class DossierProjectionService
- class DossierProjection < Struct.new(:dossier_id, :state, :archived, :hidden_by_user_at, :hidden_by_administration_at, :for_tiers, :prenom, :nom, :batch_operation_id, :sva_svr_decision_on, :corrections, :columns) do
+ class DossierProjection < Struct.new(:dossier_id, :state, :archived, :hidden_by_user_at, :hidden_by_administration_at, :hidden_by_reason, :for_tiers, :prenom, :nom, :batch_operation_id, :sva_svr_decision_on, :corrections, :columns) do
def pending_correction?
return false if corrections.blank?
@@ -25,6 +27,7 @@ class DossierProjectionService
TABLE = 'table'
COLUMN = 'column'
+ STABLE_ID = 'stable_id'
# Returns [DossierProjection(dossier, columns)] ordered by dossiers_ids
# and the columns orderd by fields.
@@ -38,34 +41,48 @@ class DossierProjectionService
# Those hashes are needed because:
# - the order of the intermediary query results are unknown
# - some values can be missing (if a revision added or removed them)
- def self.project(dossiers_ids, fields)
+ def self.project(dossiers_ids, columns)
+ fields = columns.map do |c|
+ if c.is_a?(Columns::ChampColumn)
+ { TABLE => c.table, STABLE_ID => c.stable_id, original_column: c }
+ else
+ { TABLE => c.table, COLUMN => c.column }
+ end
+ end
+ champ_value = champ_value_formatter(dossiers_ids, fields)
+
state_field = { TABLE => 'self', COLUMN => 'state' }
archived_field = { TABLE => 'self', COLUMN => 'archived' }
batch_operation_field = { TABLE => 'self', COLUMN => 'batch_operation_id' }
hidden_by_user_at_field = { TABLE => 'self', COLUMN => 'hidden_by_user_at' }
hidden_by_administration_at_field = { TABLE => 'self', COLUMN => 'hidden_by_administration_at' }
+ hidden_by_reason_field = { TABLE => 'self', COLUMN => 'hidden_by_reason' }
for_tiers_field = { TABLE => 'self', COLUMN => 'for_tiers' }
individual_first_name = { TABLE => 'individual', COLUMN => 'prenom' }
individual_last_name = { TABLE => 'individual', COLUMN => 'nom' }
sva_svr_decision_on_field = { TABLE => 'self', COLUMN => 'sva_svr_decision_on' }
dossier_corrections = { TABLE => 'dossier_corrections', COLUMN => 'resolved_at' }
- ([state_field, archived_field, sva_svr_decision_on_field, hidden_by_user_at_field, hidden_by_administration_at_field, for_tiers_field, individual_first_name, individual_last_name, batch_operation_field, dossier_corrections] + fields) # the view needs state and archived dossier attributes
+
+ ([state_field, archived_field, sva_svr_decision_on_field, hidden_by_user_at_field, hidden_by_administration_at_field, hidden_by_reason_field, for_tiers_field, individual_first_name, individual_last_name, batch_operation_field, dossier_corrections] + fields)
.each { |f| f[:id_value_h] = {} }
.group_by { |f| f[TABLE] } # one query per table
.each do |table, fields|
case table
- when 'type_de_champ', 'type_de_champ_private'
+ when 'type_de_champ'
Champ
- .includes(:type_de_champ)
.where(
- types_de_champ: { stable_id: fields.map { |f| f[COLUMN] } },
+ stable_id: fields.map { |f| f[STABLE_ID] },
dossier_id: dossiers_ids
)
- .select(:dossier_id, :value, :type_de_champ_id, 'types_de_champ.stable_id', :type, :external_id, :data, :value_json) # we cannot pluck :value, as we need the champ.to_s method
+ .select(:dossier_id, :value, :stable_id, :type, :external_id, :data, :value_json) # we cannot pluck :value, as we need the champ.to_s method
.group_by(&:stable_id) # the champs are redispatched to their respective fields
.map do |stable_id, champs|
- field = fields.find { |f| f[COLUMN] == stable_id.to_s }
- field[:id_value_h] = champs.to_h { |c| [c.dossier_id, c.to_s] }
+ fields
+ .filter { |f| f[STABLE_ID] == stable_id }
+ .each do |field|
+ column = field[:original_column]
+ field[:id_value_h] = champs.to_h { [_1.dossier_id, column.is_a?(Columns::JSONPathColumn) ? column.value(_1) : champ_value.(_1)] }
+ end
end
when 'self'
Dossier
@@ -73,10 +90,11 @@ class DossierProjectionService
.pluck(:id, *fields.map { |f| f[COLUMN].to_sym })
.each do |id, *columns|
fields.zip(columns).each do |field, value|
- if [state_field, archived_field, hidden_by_user_at_field, hidden_by_administration_at_field, for_tiers_field, batch_operation_field, sva_svr_decision_on_field].include?(field)
- field[:id_value_h][id] = value
+ # SVA must remain a date: in other column we compute remaining delay with it
+ field[:id_value_h][id] = if value.respond_to?(:strftime) && field != sva_svr_decision_on_field
+ I18n.l(value.to_date)
else
- field[:id_value_h][id] = value&.strftime('%d/%m/%Y') # other fields are datetime
+ value
end
end
end
@@ -116,6 +134,18 @@ class DossierProjectionService
fields[0][:id_value_h] = id_value_h
+ when 'dossier_labels'
+ columns = fields.map { _1[COLUMN].to_sym }
+
+ id_value_h =
+ DossierLabel
+ .includes(:label)
+ .where(dossier_id: dossiers_ids)
+ .pluck('dossier_id, labels.name, labels.color')
+ .group_by { |dossier_id, _| dossier_id }
+
+ fields[0][:id_value_h] = id_value_h.transform_values { |v| { value: v, type: :label } }
+
when 'procedure'
Dossier
.joins(:procedure)
@@ -150,6 +180,7 @@ class DossierProjectionService
archived_field[:id_value_h][dossier_id],
hidden_by_user_at_field[:id_value_h][dossier_id],
hidden_by_administration_at_field[:id_value_h][dossier_id],
+ hidden_by_reason_field[:id_value_h][dossier_id],
for_tiers_field[:id_value_h][dossier_id],
individual_first_name[:id_value_h][dossier_id],
individual_last_name[:id_value_h][dossier_id],
@@ -160,4 +191,24 @@ class DossierProjectionService
)
end
end
+
+ class << self
+ private
+
+ def champ_value_formatter(dossiers_ids, fields)
+ stable_ids = fields.filter { _1[TABLE].in?(['type_de_champ']) }.map { _1[STABLE_ID] }
+ revision_ids_by_dossier_ids = Dossier.where(id: dossiers_ids).pluck(:id, :revision_id).to_h
+ stable_ids_and_types_de_champ_by_revision_ids = ProcedureRevisionTypeDeChamp.includes(:type_de_champ)
+ .where(revision_id: revision_ids_by_dossier_ids.values.uniq, type_de_champ: { stable_id: stable_ids })
+ .map { [_1.revision_id, _1.type_de_champ] }
+ .group_by(&:first)
+ .transform_values { _1.map { |_, type_de_champ| [type_de_champ.stable_id, type_de_champ] }.to_h }
+ stable_ids_and_types_de_champ_by_dossier_ids = revision_ids_by_dossier_ids.transform_values { stable_ids_and_types_de_champ_by_revision_ids[_1] }.compact
+ -> (champ) {
+ type_de_champ = stable_ids_and_types_de_champ_by_dossier_ids
+ .fetch(champ.dossier_id, {})[champ.stable_id]
+ type_de_champ&.champ_value(champ)
+ }
+ end
+ end
end
diff --git a/app/services/dossier_search_service.rb b/app/services/dossier_search_service.rb
index 7afdad5f6..dc046ed92 100644
--- a/app/services/dossier_search_service.rb
+++ b/app/services/dossier_search_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DossierSearchService
def self.matching_dossiers(dossiers, search_terms, with_annotations = false)
if dossiers.nil?
diff --git a/app/services/downloadable_file_service.rb b/app/services/downloadable_file_service.rb
index 97909578b..3a8519600 100644
--- a/app/services/downloadable_file_service.rb
+++ b/app/services/downloadable_file_service.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
class DownloadableFileService
ARCHIVE_CREATION_DIR = ENV.fetch('ARCHIVE_CREATION_DIR') { '/tmp' }
+ EXPORT_DIRNAME = 'export'
def self.download_and_zip(procedure, attachments, filename, &block)
Dir.mktmpdir(nil, ARCHIVE_CREATION_DIR) do |tmp_dir|
- export_dir = File.join(tmp_dir, filename)
+ export_dir = File.join(tmp_dir, EXPORT_DIRNAME)
zip_path = File.join(ARCHIVE_CREATION_DIR, "#{filename}.zip")
begin
@@ -15,7 +18,7 @@ class DownloadableFileService
Dir.chdir(tmp_dir) do
File.delete(zip_path) if File.exist?(zip_path)
- system 'zip', '-0', '-r', zip_path, filename
+ system 'zip', '-0', '-r', zip_path, EXPORT_DIRNAME
end
yield(zip_path)
ensure
diff --git a/app/services/email_delivering_interceptor.rb b/app/services/email_delivering_interceptor.rb
index e4a7648d6..b864a200a 100644
--- a/app/services/email_delivering_interceptor.rb
+++ b/app/services/email_delivering_interceptor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmailDeliveringInterceptor
def self.delivering_email(message)
EmailEvent.create_from_message!(message, status: "pending")
diff --git a/app/services/email_delivery_observer.rb b/app/services/email_delivery_observer.rb
index c297dcf85..a80fab94d 100644
--- a/app/services/email_delivery_observer.rb
+++ b/app/services/email_delivery_observer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmailDeliveryObserver
def self.delivered_email(message)
EmailEvent.create_from_message!(message, status: "dispatched")
diff --git a/app/services/encryption_service.rb b/app/services/encryption_service.rb
index 448ddcc7a..3d0dbb92e 100644
--- a/app/services/encryption_service.rb
+++ b/app/services/encryption_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EncryptionService
def initialize
len = ActiveSupport::MessageEncryptor.key_len
@@ -5,6 +7,10 @@ class EncryptionService
password = Rails.application.secrets.secret_key_base
key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, len)
@encryptor = ActiveSupport::MessageEncryptor.new(key)
+
+ # Remove after all encrypted attributes have been rotated.
+ legacy_key = ActiveSupport::KeyGenerator.new(password, hash_digest_class: OpenSSL::Digest::SHA1).generate_key(salt, len)
+ @encryptor.rotate legacy_key
end
def encrypt(value)
diff --git a/app/services/expired.rb b/app/services/expired.rb
index b6f4a0210..11d79c76d 100644
--- a/app/services/expired.rb
+++ b/app/services/expired.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Expired
# User is considered inactive after two years of idleness regarding
# when he does not have a dossier en instruction
@@ -23,7 +25,7 @@ module Expired
when 'Cron::ExpiredPrefilledDossiersDeletionJob'
"every day at 3 am"
when 'Cron::ExpiredDossiersTermineDeletionJob'
- "every day at 7 am"
+ "every day at 1 am"
when 'Cron::ExpiredDossiersBrouillonDeletionJob'
"every day at 10 pm"
when 'Cron::ExpiredUsersDeletionJob'
diff --git a/app/services/expired/dossiers_deletion_service.rb b/app/services/expired/dossiers_deletion_service.rb
index 9d4bfddb7..62e5d8a76 100644
--- a/app/services/expired/dossiers_deletion_service.rb
+++ b/app/services/expired/dossiers_deletion_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Expired::DossiersDeletionService < Expired::MailRateLimiter
def process_expired_dossiers_brouillon
send_brouillon_expiration_notices
@@ -93,27 +95,26 @@ class Expired::DossiersDeletionService < Expired::MailRateLimiter
administration_notifications = group_by_fonctionnaire_email(dossiers_to_remove)
.map { |(email, dossiers)| [email, dossiers.map(&:id)] }
- deleted_dossier_ids = []
+ hidden_dossier_ids = []
dossiers_to_remove.find_each do |dossier|
- if dossier.expired_keep_track_and_destroy!
- deleted_dossier_ids << dossier.id
- end
+ dossier.hide_and_keep_track!(:automatic, :expired)
+ hidden_dossier_ids << dossier.id
end
user_notifications.each do |(email, dossier_ids)|
- dossier_ids = dossier_ids.intersection(deleted_dossier_ids)
+ dossier_ids = dossier_ids.intersection(hidden_dossier_ids)
if dossier_ids.present?
mail = DossierMailer.notify_automatic_deletion_to_user(
- DeletedDossier.where(dossier_id: dossier_ids).to_a,
+ Dossier.where(id: dossier_ids).to_a,
email
)
send_with_delay(mail)
end
end
administration_notifications.each do |(email, dossier_ids)|
- dossier_ids = dossier_ids.intersection(deleted_dossier_ids)
+ dossier_ids = dossier_ids.intersection(hidden_dossier_ids)
if dossier_ids.present?
mail = DossierMailer.notify_automatic_deletion_to_administration(
- DeletedDossier.where(dossier_id: dossier_ids).to_a,
+ Dossier.where(id: dossier_ids).to_a,
email
)
send_with_delay(mail)
diff --git a/app/services/expired/mail_rate_limiter.rb b/app/services/expired/mail_rate_limiter.rb
index fba9f625b..e91d6f918 100644
--- a/app/services/expired/mail_rate_limiter.rb
+++ b/app/services/expired/mail_rate_limiter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Expired::MailRateLimiter
attr_reader :delay, :current_window
diff --git a/app/services/expired/users_deletion_service.rb b/app/services/expired/users_deletion_service.rb
index f8e448422..24a994420 100644
--- a/app/services/expired/users_deletion_service.rb
+++ b/app/services/expired/users_deletion_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Expired::UsersDeletionService < Expired::MailRateLimiter
def process_expired
# we are working on two dataset because we apply two incompatible join on the same query
diff --git a/app/services/exported_column_formatter.rb b/app/services/exported_column_formatter.rb
new file mode 100644
index 000000000..824b26d29
--- /dev/null
+++ b/app/services/exported_column_formatter.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+class ExportedColumnFormatter
+ def self.format(column:, champ_or_dossier:, format:)
+ return if champ_or_dossier.nil?
+
+ raw_value = column.value(champ_or_dossier)
+
+ case column.type
+ when :boolean
+ format_boolean(column:, raw_value:, format:)
+ when :attachements
+ format_attachments(column:, raw_value:)
+ when :enum
+ format_enum(column:, raw_value:)
+ when :enums
+ format_enums(column:, raw_values: raw_value)
+ else
+ raw_value
+ end
+ end
+
+ private
+
+ def self.format_boolean(column:, raw_value:, format:)
+ if format == :ods
+ raw_value ? 1 : 0
+ else
+ raw_value
+ end
+ end
+
+ def self.format_attachments(column:, raw_value:)
+ case column.tdc_type
+ when TypeDeChamp.type_champs[:titre_identite]
+ raw_value.present? ? 'présent' : 'absent'
+ when TypeDeChamp.type_champs[:piece_justificative]
+ raw_value.map { _1.blob.filename }.join(", ")
+ end
+ end
+
+ def self.format_enums(column:, raw_values:)
+ raw_values.map { format_enum(column:, raw_value: _1) }.join(', ')
+ end
+
+ def self.format_enum(column:, raw_value:)
+ # options for select store ["trad", :enum_value]
+ selected_option = column.options_for_select.find { _1[1].to_s == raw_value }
+
+ selected_option ? selected_option.first : raw_value
+ end
+end
diff --git a/app/services/falsify_opendata_service.rb b/app/services/falsify_opendata_service.rb
index e1afca40b..cc1e3c7d7 100644
--- a/app/services/falsify_opendata_service.rb
+++ b/app/services/falsify_opendata_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class FalsifyOpendataService
def self.call(lines)
errors = []
diff --git a/app/services/faqs_loader_service.rb b/app/services/faqs_loader_service.rb
new file mode 100644
index 000000000..be2434d83
--- /dev/null
+++ b/app/services/faqs_loader_service.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+class FAQsLoaderService
+ PATH = Rails.root.join('doc', 'faqs').freeze
+ ORDER = ['usager', 'instructeur', 'administrateur'].freeze
+
+ attr_reader :substitutions
+
+ def initialize(substitutions)
+ @substitutions = substitutions
+
+ @faqs_by_path ||= Rails.cache.fetch(["faqs_data", ApplicationVersion.current, substitutions], expires_in: 1.day) do
+ load_faqs
+ end
+ end
+
+ def find(path)
+ Rails.cache.fetch(["faq", path, ApplicationVersion.current, substitutions], expires_in: 1.day) do
+ file_path = @faqs_by_path.fetch(path).fetch(:file_path)
+
+ parse_with_substitutions(file_path)
+ end
+ end
+
+ def faqs_for_category(category)
+ @faqs_by_path.values
+ .filter { |faq| faq[:category] == category }
+ .group_by { |faq| faq[:subcategory] }
+ end
+
+ def all
+ @faqs_by_path.values
+ .group_by { |faq| faq.fetch(:category) }
+ .sort_by { |category, _| ORDER.index(category) || ORDER.size }
+ .to_h
+ .transform_values do |faqs|
+ faqs.group_by { |faq| faq.fetch(:subcategory) }
+ end
+ end
+
+ private
+
+ def load_faqs
+ Dir.glob("#{PATH}/**/*.md").each_with_object({}) do |file_path, faqs_by_path|
+ parsed = parse_with_substitutions(file_path)
+ front_matter = parsed.front_matter.symbolize_keys
+
+ faq_data = front_matter.slice(:slug, :title, :category, :subcategory, :locale, :keywords).merge(file_path: file_path)
+
+ path = front_matter.fetch(:category) + '/' + front_matter.fetch(:slug)
+ faqs_by_path[path] = faq_data
+ end
+ end
+
+ # Substitute all string before front matter parser so metadata are also substituted.
+ # using standard ruby formatting, ie => `%{my_var} % { my_var: 'value' }`
+ # We have to escape % chars not used for substitutions, ie. not preceeded by {
+ def parse_with_substitutions(file_path)
+ substituted_content = File.read(file_path).gsub(/%(?!{)/, '%%') % substitutions
+
+ FrontMatterParser::Parser.new(:md).call(substituted_content)
+ end
+end
diff --git a/app/services/france_connect_service.rb b/app/services/france_connect_service.rb
index 31b2491c4..5da4be924 100644
--- a/app/services/france_connect_service.rb
+++ b/app/services/france_connect_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class FranceConnectService
def self.enabled?
ENV.fetch("FRANCE_CONNECT_ENABLED", "enabled") == "enabled"
diff --git a/app/services/generate_open_data_csv_service.rb b/app/services/generate_open_data_csv_service.rb
index 2511d1fed..6f111c87f 100644
--- a/app/services/generate_open_data_csv_service.rb
+++ b/app/services/generate_open_data_csv_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GenerateOpenDataCsvService
def self.save_csv_to_tmp(file_name, data)
f = Tempfile.create(["#{file_name}_#{date_last_month}", '.csv'], 'tmp')
diff --git a/app/services/geojson_service.rb b/app/services/geojson_service.rb
index 47721ac8f..49132b154 100644
--- a/app/services/geojson_service.rb
+++ b/app/services/geojson_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GeojsonService
def self.valid?(json)
schemer = JSONSchemer.schema(Rails.root.join('app/schemas/geojson.json'))
diff --git a/app/services/instructeurs_import_service.rb b/app/services/instructeurs_import_service.rb
index 2a98f7ee9..cf2c95cf3 100644
--- a/app/services/instructeurs_import_service.rb
+++ b/app/services/instructeurs_import_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class InstructeursImportService
def self.import_groupes(procedure, groupes_emails)
groupes_emails, error_groupe_emails = groupes_emails.partition { _1['groupe'].present? }
diff --git a/app/services/ip_service.rb b/app/services/ip_service.rb
index 8338698cc..327de4e37 100644
--- a/app/services/ip_service.rb
+++ b/app/services/ip_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class IPService
class << self
def ip_trusted?(ip)
diff --git a/app/services/mail_template_presenter_service.rb b/app/services/mail_template_presenter_service.rb
index b0e9db3bb..82ff6bff0 100644
--- a/app/services/mail_template_presenter_service.rb
+++ b/app/services/mail_template_presenter_service.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
class MailTemplatePresenterService
include ActionView::Helpers::SanitizeHelper
include ActionView::Helpers::TextHelper
def self.create_commentaire_for_state(dossier, state)
if dossier.procedure.accuse_lecture? && Dossier::TERMINE.include?(state)
- CommentaireService.create!(CONTACT_EMAIL, dossier, body: I18n.t('layouts.mailers.accuse_lecture.commentaire_html', service: dossier.procedure.service.nom))
+ CommentaireService.create!(CONTACT_EMAIL, dossier, body: I18n.t('layouts.mailers.accuse_lecture.commentaire_html', service: dossier.procedure.service&.nom))
else
service = new(dossier, state)
body = ["[#{service.safe_subject}]
", service.safe_body].join('')
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 0c17c884e..1976855e6 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NotificationService
class << self
SPREAD_DURATION = 2.hours
diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb
index 6c5648597..cdd79e99c 100644
--- a/app/services/pieces_justificatives_service.rb
+++ b/app/services/pieces_justificatives_service.rb
@@ -1,45 +1,40 @@
+# frozen_string_literal: true
+
class PiecesJustificativesService
- def initialize(user_profile:)
+ def initialize(user_profile:, export_template:)
@user_profile = user_profile
+ @export_template = export_template
end
def liste_documents(dossiers)
- bill_ids = []
+ docs = pjs_for_champs(dossiers) +
+ pjs_for_commentaires(dossiers) +
+ pjs_for_dossier(dossiers) +
+ pjs_for_avis(dossiers)
- docs = dossiers.in_batches.flat_map do |batch|
- pjs = pjs_for_champs(batch) +
- pjs_for_commentaires(batch) +
- pjs_for_dossier(batch) +
- pjs_for_avis(batch)
+ # we do not export bills no more with the new export system
+ # the bills have never been properly understood by the users
+ # their export is now deprecated
+ if liste_documents_allows?(:with_bills) && @export_template.nil?
+ # some bills are shared among operations
+ # so first, all the bill_ids are fetched
+ operation_logs, some_bill_ids = operation_logs_and_signature_ids(dossiers)
- if liste_documents_allows?(:with_bills)
- # some bills are shared among operations
- # so first, all the bill_ids are fetched
- operation_logs, some_bill_ids = operation_logs_and_signature_ids(batch)
+ docs += operation_logs
- pjs += operation_logs
- bill_ids += some_bill_ids
- end
-
- pjs
- end
-
- if liste_documents_allows?(:with_bills)
# then the bills are retrieved without duplication
- docs += signatures(bill_ids.uniq)
+ docs += signatures(some_bill_ids.uniq)
end
- docs
+ docs.filter { |_attachment, path| path.present? }
end
- def generate_dossiers_export(dossiers)
+ def generate_dossiers_export(dossiers) # TODO: renommer generate_dossier_export sans s
return [] if dossiers.empty?
pdfs = []
procedure = dossiers.first.procedure
- dossiers = dossiers.includes(:individual, :traitement, :etablissement, user: :france_connect_informations, avis: :expert, commentaires: [:instructeur, :expert])
- dossiers = DossierPreloader.new(dossiers).in_batches
dossiers.each do |dossier|
dossier.association(:procedure).target = procedure
@@ -49,16 +44,19 @@ class PiecesJustificativesService
acls: acl_for_dossier_export(procedure),
dossier: dossier
})
-
a = ActiveStorage::FakeAttachment.new(
file: StringIO.new(pdf),
- filename: "export-#{dossier.id}.pdf",
+ filename: ActiveStorage::Filename.new("export-#{dossier.id}.pdf"),
name: 'pdf_export_for_instructeur',
id: dossier.id,
created_at: dossier.updated_at
)
- pdfs << ActiveStorage::DownloadableFile.pj_and_path(dossier.id, a)
+ if @export_template
+ pdfs << [a, @export_template.attachment_path(dossier, a)]
+ else
+ pdfs << ActiveStorage::DownloadableFile.pj_and_path(dossier.id, a)
+ end
end
pdfs
@@ -137,31 +135,27 @@ class PiecesJustificativesService
end
def pjs_for_champs(dossiers)
- champs = Champ
- .joins(:piece_justificative_file_attachments)
- .where(type: "Champs::PieceJustificativeChamp", dossier: dossiers)
+ champs = liste_documents_allows?(:with_champs_private) ? dossiers.flat_map(&:filled_champs) : dossiers.flat_map(&:filled_champs_public)
+ champs = champs.filter { _1.piece_justificative? && _1.is_type?(_1.type_de_champ.type_champ) }
- if !liste_documents_allows?(:with_champs_private)
- champs = champs.where(private: false)
- end
+ champs_id_row_index = compute_champ_id_row_index(champs)
- champ_id_dossier_id = champs
- .pluck(:id, :dossier_id)
- .to_h
+ champs.flat_map do |champ|
+ champ.piece_justificative_file_attachments.filter { |a| safe_attachment(a) }.map.with_index do |attachment, index|
+ row_index = champs_id_row_index[champ.id]
- ActiveStorage::Attachment
- .includes(:blob)
- .where(record_type: "Champ", record_id: champ_id_dossier_id.keys)
- .filter { |a| safe_attachment(a) }
- .map do |a|
- dossier_id = champ_id_dossier_id[a.record_id]
- ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ if @export_template
+ [attachment, @export_template.attachment_path(champ.dossier, attachment, index:, row_index:, champ:)]
+ else
+ ActiveStorage::DownloadableFile.pj_and_path(champ.dossier_id, attachment)
+ end
end
+ end
end
def pjs_for_commentaires(dossiers)
commentaire_id_dossier_id = Commentaire
- .joins(:piece_jointe_attachment)
+ .joins(:piece_jointe_attachments)
.where(dossier: dossiers)
.pluck(:id, :dossier_id)
.to_h
@@ -172,7 +166,12 @@ class PiecesJustificativesService
.filter { |a| safe_attachment(a) }
.map do |a|
dossier_id = commentaire_id_dossier_id[a.record_id]
- ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ if @export_template
+ dossier = dossiers.find { _1.id == dossier_id }
+ [a, @export_template.attachment_path(dossier, a)]
+ else
+ ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ end
end
end
@@ -193,7 +192,12 @@ class PiecesJustificativesService
.where(record_type: "Etablissement", record_id: etablissement_id_dossier_id.keys)
.map do |a|
dossier_id = etablissement_id_dossier_id[a.record_id]
- ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ if @export_template
+ dossier = dossiers.find { _1.id == dossier_id }
+ [a, @export_template.attachment_path(dossier, a)]
+ else
+ ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ end
end
end
@@ -204,7 +208,12 @@ class PiecesJustificativesService
.filter { |a| safe_attachment(a) }
.map do |a|
dossier_id = a.record_id
- ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ if @export_template
+ dossier = dossiers.find { _1.id == dossier_id }
+ [a, @export_template.attachment_path(dossier, a)]
+ else
+ ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ end
end
end
@@ -220,7 +229,12 @@ class PiecesJustificativesService
.where(record_type: "Attestation", record_id: attestation_id_dossier_id.keys)
.map do |a|
dossier_id = attestation_id_dossier_id[a.record_id]
- ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ if @export_template
+ dossier = dossiers.find { _1.id == dossier_id }
+ [a, @export_template.attachment_path(dossier, a)]
+ else
+ ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ end
end
end
@@ -244,7 +258,12 @@ class PiecesJustificativesService
.filter { |a| safe_attachment(a) }
.map do |a|
dossier_id = avis_ids_dossier_id[a.record_id]
- ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ if @export_template
+ dossier = dossiers.find { _1.id == dossier_id }
+ [a, @export_template.attachment_path(dossier, a)]
+ else
+ ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ end
end
end
@@ -300,4 +319,26 @@ class PiecesJustificativesService
.blob
.virus_scan_result == ActiveStorage::VirusScanner::SAFE
end
+
+ # given
+ # repet_0 (stable_id: r0)
+ # # row_0
+ # # # pj_champ_0 (stable_id: 0)
+ # # row_1
+ # # # pj_champ_1 (stable_id: 0)
+ # repet_1 (stable_id: r1)
+ # # row_0
+ # # # pj_champ_2 (stable_id: 1)
+ # # # pj_champ_3 (stable_id: 2)
+ # # row_1
+ # # # pj_champ_4 (stable_id: 1)
+ # # # pj_champ_5 (stable_id: 2)
+ # it returns { pj_0.id => 0, pj_1.id => 1, pj_2.id => 0, pj_3.id => 0, pj_4.id => 1, pj_5.id => 1 }
+ def compute_champ_id_row_index(champs)
+ champs.filter(&:child?).group_by(&:dossier_id).values.each_with_object({}) do |children_for_dossier, hash|
+ children_for_dossier.group_by(&:stable_id).values.each do |champs_for_stable_id|
+ champs_for_stable_id.sort_by(&:row_id).each.with_index { |c, index| hash[c.id] = index }
+ end
+ end
+ end
end
diff --git a/app/services/procedure_archive_service.rb b/app/services/procedure_archive_service.rb
index 3ad452ebf..34b73f215 100644
--- a/app/services/procedure_archive_service.rb
+++ b/app/services/procedure_archive_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'tempfile'
class ProcedureArchiveService
diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb
index 75eab89e8..181352d5c 100644
--- a/app/services/procedure_export_service.rb
+++ b/app/services/procedure_export_service.rb
@@ -1,10 +1,13 @@
+# frozen_string_literal: true
+
class ProcedureExportService
attr_reader :procedure, :dossiers
- def initialize(procedure, dossiers, user_profile)
+ def initialize(procedure, dossiers, user_profile, export_template)
@procedure = procedure
@dossiers = dossiers
@user_profile = user_profile
+ @export_template = export_template
end
def to_csv
@@ -15,7 +18,7 @@ class ProcedureExportService
def to_xlsx
@dossiers = @dossiers.downloadable_sorted_batch
- tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
+ tables = [:dossiers, :etablissements, :avis] + champs_repetables_options(format: :xlsx)
# We recursively build multi page spreadsheet
io = tables.reduce(nil) do |package, table|
@@ -26,7 +29,7 @@ class ProcedureExportService
def to_ods
@dossiers = @dossiers.downloadable_sorted_batch
- tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
+ tables = [:dossiers, :etablissements, :avis] + champs_repetables_options(format: :ods)
# We recursively build multi page spreadsheet
io = StringIO.new(tables.reduce(nil) do |spreadsheet, table|
@@ -36,7 +39,7 @@ class ProcedureExportService
end
def to_zip
- attachments = ActiveStorage::DownloadableFile.create_list_from_dossiers(dossiers:, user_profile: @user_profile)
+ attachments = ActiveStorage::DownloadableFile.create_list_from_dossiers(dossiers:, user_profile: @user_profile, export_template: @export_template)
DownloadableFileService.download_and_zip(procedure, attachments, base_filename) do |zip_filepath|
ArchiveUploader.new(procedure: procedure, filename: filename(:zip), filepath: zip_filepath).blob
@@ -44,7 +47,9 @@ class ProcedureExportService
end
def to_geo_json
- io = StringIO.new(dossiers.to_feature_collection.to_json)
+ champs_carte = dossiers.flat_map { _1.filled_champs.filter(&:carte?) }
+ features = GeoArea.where(champ_id: champs_carte).map(&:to_feature)
+ io = StringIO.new({ type: 'FeatureCollection', features: }.to_json)
create_blob(io, :json)
end
@@ -89,32 +94,28 @@ class ProcedureExportService
end
def etablissements
- @etablissements ||= dossiers.flat_map do |dossier|
- dossier.champs.filter { _1.is_a?(Champs::SiretChamp) }
- end.filter_map(&:etablissement) + dossiers.filter_map(&:etablissement)
+ @etablissements ||= dossiers
+ .flat_map { _1.filled_champs.filter(&:siret?) }
+ .filter_map(&:etablissement) + dossiers.filter_map(&:etablissement)
end
def avis
@avis ||= dossiers.flat_map(&:avis)
end
- def champs_repetables_options
- champs_by_stable_id = dossiers
- .flat_map { _1.champs.filter(&:repetition?) }
- .group_by(&:stable_id)
-
+ def champs_repetables_options(format:)
procedure
- .types_de_champ_for_procedure_presentation
+ .all_revisions_types_de_champ
.repetition
.filter_map do |type_de_champ_repetition|
- types_de_champ = procedure.types_de_champ_for_procedure_presentation(type_de_champ_repetition).to_a
- rows = champs_by_stable_id.fetch(type_de_champ_repetition.stable_id, []).flat_map(&:rows_for_export)
+ types_de_champ = procedure.all_revisions_types_de_champ(parent: type_de_champ_repetition).to_a
+ rows = dossiers.flat_map { _1.repetition_rows_for_export(type_de_champ_repetition) }
if types_de_champ.present? && rows.present?
{
sheet_name: type_de_champ_repetition.libelle_for_export,
instances: rows,
- spreadsheet_columns: Proc.new { |instance| instance.spreadsheet_columns(types_de_champ) }
+ spreadsheet_columns: Proc.new { |instance| instance.spreadsheet_columns(types_de_champ, export_template: @export_template, format:) }
}
end
end
@@ -148,10 +149,10 @@ class ProcedureExportService
end
def spreadsheet_columns(format)
- types_de_champ = procedure.types_de_champ_for_procedure_presentation.not_repetition.to_a
+ types_de_champ = procedure.types_de_champ_for_procedure_export.to_a
Proc.new do |instance|
- instance.send(:"spreadsheet_columns_#{format}", types_de_champ: types_de_champ)
+ instance.send(:"spreadsheet_columns_#{format}", types_de_champ: types_de_champ, export_template: @export_template)
end
end
end
diff --git a/app/services/recovery_service.rb b/app/services/recovery_service.rb
index 9efa4c6e5..57e9674f5 100644
--- a/app/services/recovery_service.rb
+++ b/app/services/recovery_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RecoveryService
def self.recoverable_procedures(previous_user:, siret:)
return [] if previous_user.nil?
diff --git a/app/services/rnf_service.rb b/app/services/rnf_service.rb
index c58e41719..b3663f75a 100644
--- a/app/services/rnf_service.rb
+++ b/app/services/rnf_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RNFService
include Dry::Monads[:result]
diff --git a/app/services/serializer_service.rb b/app/services/serializer_service.rb
index 6f97841c1..8c25f6b76 100644
--- a/app/services/serializer_service.rb
+++ b/app/services/serializer_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SerializerService
def self.dossier(dossier)
Sentry.with_scope do |scope|
@@ -164,6 +166,7 @@ class SerializerService
demandeur {
...PersonnePhysiqueFragment
...PersonneMoraleFragment
+ ...PersonneMoraleIncompleteFragment
}
motivation
motivationAttachment {
@@ -309,6 +312,10 @@ class SerializerService
}
}
+ fragment PersonneMoraleIncompleteFragment on PersonneMoraleIncomplete {
+ siret
+ }
+
fragment AddressFragment on Address {
label
type
diff --git a/app/services/staging_auth_service.rb b/app/services/staging_auth_service.rb
index 085ec497c..9235d73a3 100644
--- a/app/services/staging_auth_service.rb
+++ b/app/services/staging_auth_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StagingAuthService
def self.authenticate(username, password)
if enabled?
diff --git a/app/services/sva_svr_decision_date_calculator_service.rb b/app/services/sva_svr_decision_date_calculator_service.rb
index c17fd8347..ec8c9e829 100644
--- a/app/services/sva_svr_decision_date_calculator_service.rb
+++ b/app/services/sva_svr_decision_date_calculator_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SVASVRDecisionDateCalculatorService
attr_reader :dossier, :procedure, :unit, :period, :resume_method
@@ -57,7 +59,7 @@ class SVASVRDecisionDateCalculatorService
end
def latest_correction_date
- correction_date dossier.corrections.max_by(&:resolved_at)
+ correction_date dossier.corrections.max_by { _1.resolved_at || Time.current }
end
def calculate_correction_delay(start_date)
diff --git a/app/services/tiptap_service.rb b/app/services/tiptap_service.rb
index ce372d39f..c63ee7f0a 100644
--- a/app/services/tiptap_service.rb
+++ b/app/services/tiptap_service.rb
@@ -1,12 +1,8 @@
+# frozen_string_literal: true
+
class TiptapService
- def to_html(node, substitutions = {})
- return '' if node.nil?
-
- children(node[:content], substitutions, 0)
- end
-
# NOTE: node must be deep symbolized keys
- def used_tags_and_libelle_for(node, tags = Set.new)
+ def self.used_tags_and_libelle_for(node, tags = Set.new)
case node
in type: 'mention', attrs: { id:, label: }, **rest
tags << [id, label]
@@ -19,12 +15,45 @@ class TiptapService
tags
end
+ def to_html(node, substitutions = {})
+ return '' if node.nil?
+
+ children(node[:content], substitutions, 0).gsub('', '')
+ end
+
+ def to_texts_and_tags(node, substitutions = {})
+ return '' if node.nil?
+
+ children_texts_and_tags(node[:content], substitutions)
+ end
+
private
def initialize
@body_started = false
end
+ def children_texts_and_tags(content, substitutions)
+ content.map { node_to_texts_and_tags(_1, substitutions) }.join
+ end
+
+ def node_to_texts_and_tags(node, substitutions)
+ case node
+ in type: 'paragraph', content:
+ children_texts_and_tags(content, substitutions)
+ in type: 'paragraph' # empty paragraph
+ ''
+ in type: 'text', text:
+ text.strip
+ in type: 'mention', attrs: { id:, label: }
+ if substitutions.present?
+ substitutions.fetch(id) { "--#{id}--" }
+ else
+ "#{label}"
+ end
+ end
+ end
+
def children(content, substitutions, level)
content.map { node_to_html(_1, substitutions, level) }.join
end
@@ -50,10 +79,16 @@ class TiptapService
"#{children(content, substitutions, level + 1)}"
in type: 'bulletList', content:
"#{children(content, substitutions, level + 1)}
"
- in type: 'orderedList', content:
- "#{children(content, substitutions, level + 1)}
"
+ in type: 'orderedList', content:, **rest
+ "#{children(content, substitutions, level + 1)}
"
in type: 'listItem', content:
"#{children(content, substitutions, level + 1)}"
+ in type: 'descriptionList', content:
+ "#{children(content, substitutions, level + 1)}
"
+ in type: 'descriptionTerm', content:, **rest
+ "#{children(content, substitutions, level + 1)}"
+ in type: 'descriptionDetails', content:
+ "#{children(content, substitutions, level + 1)}"
in type: 'text', text:, **rest
if rest[:marks].present?
apply_marks(text, rest[:marks])
@@ -61,7 +96,12 @@ class TiptapService
text
end
in type: 'mention', attrs: { id: }, **rest
- text = substitutions.fetch(id) { "--#{id}--" }
+ text_or_presentation = substitutions.fetch(id) { "--#{id}--" }
+ text = if text_or_presentation.respond_to?(:to_tiptap_node)
+ handle_presentation_node(text_or_presentation, substitutions, level + 1)
+ else
+ text_or_presentation
+ end
if rest[:marks].present?
apply_marks(text, rest[:marks])
@@ -73,6 +113,16 @@ class TiptapService
end
end
+ def handle_presentation_node(presentation, substitutions, level)
+ node = presentation.to_tiptap_node
+ content = node_to_html(node, substitutions, level)
+ if presentation.block_level?
+ "
#{content}"
+ else
+ content
+ end
+ end
+
def text_align(attrs)
if attrs.present? && attrs[:textAlign].present?
" style=\"text-align: #{attrs[:textAlign]}\""
@@ -81,6 +131,12 @@ class TiptapService
end
end
+ def class_list(attrs)
+ if attrs.present? && attrs[:class].present?
+ " class=\"#{attrs[:class]}\""
+ end
+ end
+
def apply_marks(text, marks)
marks.reduce(text) do |text, mark|
case mark
diff --git a/app/services/uninterlace_service.rb b/app/services/uninterlace_service.rb
new file mode 100644
index 000000000..72b51b63e
--- /dev/null
+++ b/app/services/uninterlace_service.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class UninterlaceService
+ def process(file)
+ uninterlace_png(file)
+ end
+
+ private
+
+ def uninterlace_png(uploaded_file)
+ if interlaced?(uploaded_file.to_path)
+ chunky_img = ChunkyPNG::Image.from_io(uploaded_file.to_io)
+ chunky_img.save(uploaded_file.to_path, interlace: false)
+ uploaded_file.reopen(uploaded_file.to_path, 'rb')
+ end
+ uploaded_file
+ end
+
+ def interlaced?(png_path)
+ return false if png_path.blank?
+ begin
+ png = MiniMagick::Image.open(png_path)
+ rescue MiniMagick::Invalid
+ return false
+ end
+ png.data["interlace"] != "None"
+ end
+end
diff --git a/app/services/watermark_service.rb b/app/services/watermark_service.rb
index a3412ccb1..671881a05 100644
--- a/app/services/watermark_service.rb
+++ b/app/services/watermark_service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class WatermarkService
POINTSIZE = 20
KERNING = 1.2
@@ -7,7 +9,7 @@ class WatermarkService
attr_reader :text
attr_reader :text_length
- def initialize(text = Current.application_name)
+ def initialize(text = APPLICATION_NAME)
@text = " #{text} " # give more space around each occurence
@text_length = @text.length
end
diff --git a/app/services/weasyprint_service.rb b/app/services/weasyprint_service.rb
new file mode 100644
index 000000000..d0ab2944b
--- /dev/null
+++ b/app/services/weasyprint_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class WeasyprintService
+ def self.generate_pdf(html, options = {})
+ headers = {
+ 'Content-Type' => 'application/json',
+ 'X-Request-Id' => Current.request_id
+ }
+
+ body = {
+ html:,
+ upstream_context: options
+ }.to_json
+
+ response = Typhoeus.post(WEASYPRINT_URL, headers:, body:)
+
+ if response.success?
+ response.body
+ else
+ raise StandardError, "PDF Generation failed: #{response.code} #{response.status_message}"
+ end
+ end
+end
diff --git a/app/services/zxcvbn_service.rb b/app/services/zxcvbn_service.rb
index 1930b66fe..1b35e3055 100644
--- a/app/services/zxcvbn_service.rb
+++ b/app/services/zxcvbn_service.rb
@@ -1,51 +1,18 @@
+# frozen_string_literal: true
+
class ZxcvbnService
@tester_mutex = Mutex.new
- class << self
- # Returns an Zxcvbn instance cached between classes instances and between threads.
- #
- # The tester weights ~20 Mo, and we'd like to save some memory – so rather
- # that storing it in a per-thread accessor, we prefer to use a mutex
- # to cache it between threads.
- def tester
- @tester_mutex.synchronize do
- @tester ||= build_tester
- end
- end
-
- private
-
- # Returns a fully initializer tester from the on-disk dictionary.
- #
- # This is slow: loading and parsing the dictionary may take around 1s.
- def build_tester
- dictionaries = YAML.safe_load(Rails.root.join("config", "initializers", "zxcvbn_dictionnaries.yaml").read)
-
- tester = Zxcvbn::Tester.new
- tester.add_word_lists(dictionaries)
- tester
+ # Returns an Zxcvbn instance cached between classes instances and between threads.
+ #
+ # The tester weights ~20 Mo, and we'd like to save some memory – so rather
+ # that storing it in a per-thread accessor, we prefer to use a mutex
+ # to cache it between threads.
+ def self.tester
+ @tester_mutex.synchronize do
+ @tester ||= Zxcvbn::Tester.new
end
end
- def initialize(password)
- @password = password
- end
-
- def complexity
- wxcvbn = compute_zxcvbn
- score = wxcvbn.score
- length = @password.blank? ? 0 : @password.length
- vulnerabilities = wxcvbn.match_sequence.map { |m| m.matched_word.nil? ? m.token : m.matched_word }.filter { |s| s.length > 2 }.join(', ')
- [score, vulnerabilities, length]
- end
-
- def score
- compute_zxcvbn.score
- end
-
- private
-
- def compute_zxcvbn
- self.class.tester.test(@password)
- end
+ def self.complexity(password)= tester.test(password.to_s).score
end
diff --git a/app/tasks/maintenance/backfill_bulk_messages_with_procedure_id_task.rb b/app/tasks/maintenance/backfill_bulk_messages_with_procedure_id_task.rb
index 952c36b09..b7fb63d16 100644
--- a/app/tasks/maintenance/backfill_bulk_messages_with_procedure_id_task.rb
+++ b/app/tasks/maintenance/backfill_bulk_messages_with_procedure_id_task.rb
@@ -2,6 +2,9 @@
module Maintenance
class BackfillBulkMessagesWithProcedureIdTask < MaintenanceTasks::Task
+ # Périmètre: envoi d’un email groupé aux usagers ayant dossiers en brouillon.
+ # Change la manière dont ces messages sont liés aux démarches.
+ # 2024-03-12-01 PR #10071
def collection
BulkMessage
.where(procedure: nil)
diff --git a/app/tasks/maintenance/backfill_city_name_task.rb b/app/tasks/maintenance/backfill_city_name_task.rb
index e22aa403e..636a65e76 100644
--- a/app/tasks/maintenance/backfill_city_name_task.rb
+++ b/app/tasks/maintenance/backfill_city_name_task.rb
@@ -2,6 +2,9 @@
module Maintenance
class BackfillCityNameTask < MaintenanceTasks::Task
+ # corrige des données du champ adresse suite à un bug
+ # introduit pendant quelques jours début mars
+ # 2024-04-09-02 PR #10290
attribute :champ_ids, :string
validates :champ_ids, presence: true
diff --git a/app/tasks/maintenance/backfill_cloned_champs_private_piece_justificatives_task.rb b/app/tasks/maintenance/backfill_cloned_champs_private_piece_justificatives_task.rb
new file mode 100644
index 000000000..a042c8db9
--- /dev/null
+++ b/app/tasks/maintenance/backfill_cloned_champs_private_piece_justificatives_task.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class BackfillClonedChampsPrivatePieceJustificativesTask < MaintenanceTasks::Task
+ # Supprime les PJ d’annotations privées
+ # qui étaient conservées par erreur lorsqu’un dossier était cloné
+ # 2024-05-27-01 PR #10435
+ def collection
+ Dossier.en_brouillon.where.not(parent_dossier_id: nil)
+ end
+
+ def process(cloned_dossier)
+ cloned_dossier.project_champs_private
+ .filter { checkable_pj?(_1, cloned_dossier) }
+ .map do |cloned_champ|
+ parent_champ = cloned_dossier.parent_dossier
+ .project_champs_private
+ .find { _1.stable_id == cloned_champ.stable_id }
+
+ next if !parent_champ
+
+ parent_blob_ids = parent_champ.piece_justificative_file.map(&:blob_id)
+ cloned_blob_ids = cloned_champ.piece_justificative_file.map(&:blob_id)
+
+ if parent_blob_ids.sort == cloned_blob_ids.sort
+ cloned_champ.piece_justificative_file.detach
+ end
+ end
+ end
+
+ def checkable_pj?(champ, dossier)
+ return false if champ.type != "Champs::PieceJustificativeChamp"
+ return false if !champ.piece_justificative_file.attached?
+ true
+ end
+ end
+end
diff --git a/app/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task.rb b/app/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task.rb
index 49981e6ed..88525d1f9 100644
--- a/app/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task.rb
+++ b/app/tasks/maintenance/backfill_closing_reason_in_closed_procedures_task.rb
@@ -2,6 +2,9 @@
module Maintenance
class BackfillClosingReasonInClosedProceduresTask < MaintenanceTasks::Task
+ # Remet les messages de cloture d'une démarche proprement (sinon affichage KO).
+ # Suite de UpdateClosingReasonIfNoReplacedByIdTask
+ # 2024-05-27-01 PR #9930
def collection
Procedure
.with_discarded
diff --git a/app/tasks/maintenance/backfill_commune_code_from_name_task.rb b/app/tasks/maintenance/backfill_commune_code_from_name_task.rb
index d5092a16d..b2285a602 100644
--- a/app/tasks/maintenance/backfill_commune_code_from_name_task.rb
+++ b/app/tasks/maintenance/backfill_commune_code_from_name_task.rb
@@ -2,11 +2,15 @@
module Maintenance
class BackfillCommuneCodeFromNameTask < MaintenanceTasks::Task
- attribute :champ_ids, :string
- validates :champ_ids, presence: true
+ # corrige structure champs commune pour une démarche donnée. Suite à un bug ?
+ # 2024-05-31-01 PR #10469
+
+ attribute :procedure_id, :string
+ validates :procedure_id, presence: true
def collection
- Champ.where(id: champ_ids.split(',').map(&:strip).map(&:to_i))
+ procedure = Procedure.find(procedure_id.strip.to_i)
+ Champs::CommuneChamp.where(dossier_id: procedure.dossiers.not_brouillon)
end
def process(champ)
@@ -14,11 +18,11 @@ module Maintenance
return if champ.external_id.present?
return if champ.value.blank?
- data = champ.data
- return if data.blank?
- return if data['code_departement'].blank?
+ value_json = champ.value_json
+ return if value_json.blank?
+ return if value_json['code_departement'].blank?
- external_id = APIGeoService.commune_code(data['code_departement'], champ.value)
+ external_id = APIGeoService.commune_code(value_json['code_departement'], champ.value)
if external_id.present?
champ.update(external_id:)
diff --git a/app/tasks/maintenance/backfill_departement_services_task.rb b/app/tasks/maintenance/backfill_departement_services_task.rb
index cb75cee97..e2da6bc11 100644
--- a/app/tasks/maintenance/backfill_departement_services_task.rb
+++ b/app/tasks/maintenance/backfill_departement_services_task.rb
@@ -2,6 +2,9 @@
module Maintenance
class BackfillDepartementServicesTask < MaintenanceTasks::Task
+ # Fait le lien service – département pour permettre
+ # le filtrage des démarches par département
+ # 2023-10-30-01 PR #9647
def collection
Service.where.not(etablissement_infos: nil)
end
diff --git a/app/tasks/maintenance/backfill_depose_at_on_deleted_dossiers_task.rb b/app/tasks/maintenance/backfill_depose_at_on_deleted_dossiers_task.rb
index 2f8eb6178..389eddef3 100644
--- a/app/tasks/maintenance/backfill_depose_at_on_deleted_dossiers_task.rb
+++ b/app/tasks/maintenance/backfill_depose_at_on_deleted_dossiers_task.rb
@@ -2,6 +2,8 @@
module Maintenance
class BackfillDeposeAtOnDeletedDossiersTask < MaintenanceTasks::Task
+ # Améliore les stats à propos des dates de dépôts pour les dossiers supprimés
+ # 2024-04-05-01 PR #10259
def collection
DeletedDossier.where(depose_at: nil)
end
diff --git a/app/tasks/maintenance/backfill_effectif_annuel_annee_task.rb b/app/tasks/maintenance/backfill_effectif_annuel_annee_task.rb
index ce52d05f9..65d6d4a1e 100644
--- a/app/tasks/maintenance/backfill_effectif_annuel_annee_task.rb
+++ b/app/tasks/maintenance/backfill_effectif_annuel_annee_task.rb
@@ -2,6 +2,8 @@
module Maintenance
class BackfillEffectifAnnuelAnneeTask < MaintenanceTasks::Task
+ # API entreprise: rattrape les informations d'effectif
+ # 2024-05-27-01 PR #10053
def collection
Etablissement.where.not(entreprise_effectif_annuel: nil).where(entreprise_effectif_annuel_annee: nil)
end
diff --git a/app/tasks/maintenance/backfill_invalid_dossiers_for_tiers_task.rb b/app/tasks/maintenance/backfill_invalid_dossiers_for_tiers_task.rb
new file mode 100644
index 000000000..1af3f814a
--- /dev/null
+++ b/app/tasks/maintenance/backfill_invalid_dossiers_for_tiers_task.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class BackfillInvalidDossiersForTiersTask < MaintenanceTasks::Task
+ # Corrige les dossiers declarés pour un tiers mais sans avoir renseigné les infos du tiers
+ # 2024-05-22-01
+ def collection
+ Dossier.where(for_tiers: true).where(mandataire_first_name: nil)
+ end
+
+ def process(element)
+ element.update_column(:for_tiers, false)
+ end
+ end
+end
diff --git a/app/tasks/maintenance/backfill_labels_for_procedures_task.rb b/app/tasks/maintenance/backfill_labels_for_procedures_task.rb
new file mode 100644
index 000000000..b207454f1
--- /dev/null
+++ b/app/tasks/maintenance/backfill_labels_for_procedures_task.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class BackfillLabelsForProceduresTask < MaintenanceTasks::Task
+ # Cette tâche permet de créer un jeu de labels génériques pour les anciennes procédures
+ # Plus d'informations sur l'implémentation des labels ici : https://github.com/demarches-simplifiees/demarches-simplifiees.fr/issues/9787
+ # 2024-10-15
+
+ include RunnableOnDeployConcern
+
+ run_on_first_deploy
+
+ def collection
+ Procedure
+ .includes(:labels)
+ .where(labels: { id: nil })
+ end
+
+ def process(procedure)
+ Label::GENERIC_LABELS.each do |label|
+ Label.create(name: label[:name], color: label[:color], procedure_id: procedure.id)
+ end
+ end
+ end
+end
diff --git a/app/tasks/maintenance/clean_header_section_options_task.rb b/app/tasks/maintenance/clean_header_section_options_task.rb
new file mode 100644
index 000000000..7bcd250bb
--- /dev/null
+++ b/app/tasks/maintenance/clean_header_section_options_task.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class CleanHeaderSectionOptionsTask < MaintenanceTasks::Task
+ # In the rest of PR 10713
+ # TypeDeChamp options may contain options which are not consistent with the
+ # type_champ (e.g. a ‘header_section’ TypeDeChamp which has a
+ # drop_down_options key/value in its options).
+ # The aim here is to clean up the options so that only those wich are useful
+ # for the type_champ in question.
+
+ def collection
+ TypeDeChamp
+ .where(type_champ: 'header_section')
+ .where.not(options: {})
+ .where.not("(SELECT COUNT(*) FROM jsonb_each_text(options)) = 1 AND options ? 'header_section_level'")
+ end
+
+ def process(tdc)
+ tdc.update(options: tdc.options.slice(:header_section_level))
+ end
+ end
+end
diff --git a/app/tasks/maintenance/clean_invalid_procedure_presentation_task.rb b/app/tasks/maintenance/clean_invalid_procedure_presentation_task.rb
new file mode 100644
index 000000000..669c6d3bf
--- /dev/null
+++ b/app/tasks/maintenance/clean_invalid_procedure_presentation_task.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Maintenance
+ # PR: 10774
+ # why: postgres does not support integer greater than FilteredColumn::PG_INTEGER_MAX_VALUE)
+ # it occures when user copypaste the dossier id twice (like missed copy paste,paste)
+ # once this huge integer is saved on procedure presentation, page with this filter can't be loaded
+ # when: run this migration when it appears in your maintenance tasks list, this file fix the data and we added some validations too
+ class CleanInvalidProcedurePresentationTask < MaintenanceTasks::Task
+ def collection
+ ProcedurePresentation.all
+ end
+
+ def process(element)
+ element.filters = element.filters.transform_values do |filters_by_status|
+ filters_by_status.reject do |filter|
+ filter.is_a?(Hash) &&
+ filter['column'] == 'id' &&
+ (filter['value']&.to_i&. >= FilteredColumn::PG_INTEGER_MAX_VALUE)
+ end
+ end
+ element.save
+ end
+
+ def count
+ # Optionally, define the number of rows that will be iterated over
+ # This is used to track the task's progress
+ end
+ end
+end
diff --git a/app/tasks/maintenance/concerns/runnable_on_deploy_concern.rb b/app/tasks/maintenance/concerns/runnable_on_deploy_concern.rb
new file mode 100644
index 000000000..f324cef29
--- /dev/null
+++ b/app/tasks/maintenance/concerns/runnable_on_deploy_concern.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Maintenance
+ module RunnableOnDeployConcern
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def run_on_first_deploy
+ @run_on_first_deploy = true
+ end
+
+ def run_on_deploy?
+ return false unless @run_on_first_deploy
+
+ task = MaintenanceTasks::TaskDataShow.new(name)
+
+ return false if task.completed_runs.not_errored.any?
+ return false if task.active_runs.any?
+
+ true
+ end
+ end
+ end
+end
diff --git a/app/tasks/maintenance/concerns/statements_helpers_concern.rb b/app/tasks/maintenance/concerns/statements_helpers_concern.rb
new file mode 100644
index 000000000..658c07b1b
--- /dev/null
+++ b/app/tasks/maintenance/concerns/statements_helpers_concern.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Maintenance
+ module StatementsHelpersConcern
+ extend ActiveSupport::Concern
+
+ included do
+ # Execute block in transaction with a local statement timeout.
+ # A value of 0 disable the timeout.
+ #
+ # Example:
+ # def collection
+ # with_statement_timeout("5min") do
+ # Dossier.all
+ # end
+ # end
+ def with_statement_timeout(timeout)
+ ApplicationRecord.transaction do
+ ApplicationRecord.connection.execute("SET LOCAL statement_timeout = '#{timeout}'")
+ yield
+ end
+ end
+ end
+ end
+end
diff --git a/app/tasks/maintenance/copy_super_admin_otp_secret_to_rails7_encrypted_attr_task.rb b/app/tasks/maintenance/copy_super_admin_otp_secret_to_rails7_encrypted_attr_task.rb
new file mode 100644
index 000000000..396aa946d
--- /dev/null
+++ b/app/tasks/maintenance/copy_super_admin_otp_secret_to_rails7_encrypted_attr_task.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class CopySuperAdminOtpSecretToRails7EncryptedAttrTask < MaintenanceTasks::Task
+ # Cette tâche finalise la mise à niveau vers devies-two-factor 5
+ # qui utilise les encrypted attributes de Rails 7.
+ # Elle copie les secrets OTP des super admins vers la nouvelle colonne
+ # avant une suppression plus tard des anciennes colonnes.
+ # Plus d'informations : https://github.com/devise-two-factor/devise-two-factor/blob/main/UPGRADING.md
+ # Introduit 2024-08-29, https://github.com/demarches-simplifiees/demarches-simplifiees.fr/pull/10722
+ def collection
+ SuperAdmin.all
+ end
+
+ def process(super_admin)
+ # From https://github.com/devise-two-factor/devise-two-factor/blob/main/UPGRADING.md
+ otp_secret = super_admin.otp_secret # read from otp_secret column, fall back to legacy columns if new column is empty
+ # This is NOOP when otp_secret column has already the same value
+ super_admin.update!(otp_secret: otp_secret)
+ end
+
+ def count
+ SuperAdmin.count
+ end
+ end
+end
diff --git a/app/tasks/maintenance/create_previews_for_pj_of_latest_dossiers_task.rb b/app/tasks/maintenance/create_previews_for_pj_of_latest_dossiers_task.rb
new file mode 100644
index 000000000..37e1d2b7b
--- /dev/null
+++ b/app/tasks/maintenance/create_previews_for_pj_of_latest_dossiers_task.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class CreatePreviewsForPjOfLatestDossiersTask < MaintenanceTasks::Task
+ # Génère les vignettes de PJ existantes pour les dossiers déposés entre 2 dates (facultatif)
+ # Elles sont affichées dans le nouvel onglet "Pièces jointes" des instructeurs.
+ # 2024-07-11-01
+ attribute :start_text, :string
+ validates :start_text, presence: true
+
+ attribute :end_text, :string
+ validates :end_text, presence: true
+
+ def collection
+ start_date = DateTime.parse(start_text)
+ end_date = DateTime.parse(end_text)
+
+ Dossier
+ .state_en_construction_ou_instruction
+ .where(depose_at: start_date..end_date)
+ end
+
+ def process(dossier)
+ champ_ids = Champ
+ .where(dossier_id: dossier)
+ .where(type: ["Champs::PieceJustificativeChamp", 'Champs::TitreIdentiteChamp'])
+ .ids
+
+ attachments = ActiveStorage::Attachment
+ .where(record_id: champ_ids)
+
+ attachments.each do |attachment|
+ next if !(attachment.previewable? && attachment.representation_required?)
+ attachment.preview(resize_to_limit: [400, 400]).processed unless attachment.preview(resize_to_limit: [400, 400]).image.attached?
+ rescue MiniMagick::Error, ActiveStorage::Error
+ end
+ end
+ end
+end
diff --git a/app/tasks/maintenance/create_previews_for_pjs_from_messagerie_task.rb b/app/tasks/maintenance/create_previews_for_pjs_from_messagerie_task.rb
new file mode 100644
index 000000000..b75c530b9
--- /dev/null
+++ b/app/tasks/maintenance/create_previews_for_pjs_from_messagerie_task.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class CreatePreviewsForPjsFromMessagerieTask < MaintenanceTasks::Task
+ attribute :start_text, :string
+ validates :start_text, presence: true
+
+ attribute :end_text, :string
+ validates :end_text, presence: true
+
+ def collection
+ start_date = DateTime.parse(start_text)
+ end_date = DateTime.parse(end_text)
+
+ Dossier
+ .state_en_construction_ou_instruction
+ .where(depose_at: start_date..end_date)
+ end
+
+ def process(dossier)
+ commentaire_ids = Commentaire
+ .where(dossier_id: dossier)
+ .pluck(:id)
+
+ attachments = ActiveStorage::Attachment
+ .where(record_id: commentaire_ids)
+
+ attachments.each do |attachment|
+ next if !(attachment.previewable? && attachment.representation_required?)
+ attachment.preview(resize_to_limit: [400, 400]).processed unless attachment.preview(resize_to_limit: [400, 400]).image.attached?
+ rescue MiniMagick::Error, ActiveStorage::Error
+ end
+ end
+ end
+end
diff --git a/app/tasks/maintenance/create_procedure_tags_task.rb b/app/tasks/maintenance/create_procedure_tags_task.rb
new file mode 100644
index 000000000..7b8c7c1ab
--- /dev/null
+++ b/app/tasks/maintenance/create_procedure_tags_task.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+# this task is used to create the procedure_tags and backfill the procedures that have the tag in their tags array
+
+module Maintenance
+ class CreateProcedureTagsTask < MaintenanceTasks::Task
+ include RunnableOnDeployConcern
+ include StatementsHelpersConcern
+ run_on_first_deploy
+
+ def collection
+ [
+ "Aap",
+ "Accompagnement",
+ "Action sociale",
+ "Adeli",
+ "Affectation",
+ "Agrément",
+ "Agriculture",
+ "agroécologie",
+ "Aide aux entreprises",
+ "Aide financière",
+ "Appel à manifestation d'intérêt",
+ "AMI",
+ "Animaux",
+ "Appel à projets",
+ "Association",
+ "Auto-école",
+ "Autorisation",
+ "Autorisation d'exercer",
+ "Bilan",
+ "Biodiversité",
+ "Candidature",
+ "Cerfa",
+ "Chasse",
+ "Cinéma",
+ "Cmg",
+ "Collectivé territoriale",
+ "Collège",
+ "Convention",
+ "Covid",
+ "Culture",
+ "Dérogation",
+ "Diplôme",
+ "Drone",
+ "DSDEN",
+ "Eau",
+ "Ecoles",
+ "Education",
+ "Elections",
+ "Energie",
+ "Enseignant",
+ "ENT",
+ "Environnement",
+ "Étrangers",
+ "Formation",
+ "FPRNM",
+ "Funéraire",
+ "Handicap",
+ "Hygiène",
+ "Industrie",
+ "innovation",
+ "Inscription",
+ "Logement",
+ "Lycée",
+ "Manifestation",
+ "Médicament",
+ "Micro-crèche",
+ "MODELE DS",
+ "Numérique",
+ "Permis",
+ "Pompiers",
+ "Préfecture",
+ "Professionels de santé",
+ "Recrutement",
+ "Rh",
+ "Santé",
+ "Scolaire",
+ "SDIS",
+ "Sécurité",
+ "Sécurité routière",
+ "Sécurité sociale",
+ "Séjour",
+ "Service civique",
+ "Subvention",
+ "Supérieur",
+ "Taxi",
+ "Télétravail",
+ "Tirs",
+ "Transition écologique",
+ "Transport",
+ "Travail",
+ "Université",
+ "Urbanisme"
+ ]
+ end
+
+ def process(tag)
+ procedure_tag = ProcedureTag.find_or_create_by(name: tag)
+
+ Procedure.where("? ILIKE ANY(tags)", tag).find_each(batch_size: 500) do |procedure|
+ procedure.procedure_tags << procedure_tag unless procedure.procedure_tags.include?(procedure_tag)
+ end
+ end
+
+ def count
+ collection.size
+ end
+ end
+end
diff --git a/app/tasks/maintenance/create_variants_for_pj_of_latest_dossiers_task.rb b/app/tasks/maintenance/create_variants_for_pj_of_latest_dossiers_task.rb
new file mode 100644
index 000000000..2fa1bd2a4
--- /dev/null
+++ b/app/tasks/maintenance/create_variants_for_pj_of_latest_dossiers_task.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class CreateVariantsForPjOfLatestDossiersTask < MaintenanceTasks::Task
+ # Génère les vignettes de fichiers PDF pour les dossiers déposés entre 2 dates (facultatif)
+ # Elles sont affichées dans le nouvel onglet "Pièces jointes" des instructeurs.
+ # 2024-07-11-01
+ attribute :start_text, :string
+ validates :start_text, presence: true
+
+ attribute :end_text, :string
+ validates :end_text, presence: true
+
+ def collection
+ start_date = DateTime.parse(start_text)
+ end_date = DateTime.parse(end_text)
+
+ Dossier
+ .state_en_construction_ou_instruction
+ .where(depose_at: start_date..end_date)
+ end
+
+ def process(dossier)
+ champ_ids = Champ
+ .where(dossier_id: dossier)
+ .where(type: ["Champs::PieceJustificativeChamp", 'Champs::TitreIdentiteChamp'])
+ .ids
+
+ attachments = ActiveStorage::Attachment
+ .where(record_id: champ_ids)
+
+ attachments.each do |attachment|
+ next if !(attachment.variable? && attachment.representation_required?)
+ attachment.variant(resize_to_limit: [400, 400]).processed if attachment.variant(resize_to_limit: [400, 400]).key.nil?
+ if attachment.blob.content_type.in?(RARE_IMAGE_TYPES) && attachment.variant(resize_to_limit: [2000, 2000]).key.nil?
+ attachment.variant(resize_to_limit: [2000, 2000]).processed
+ end
+ rescue MiniMagick::Error, ActiveStorage::Error
+ end
+ end
+ end
+end
diff --git a/app/tasks/maintenance/create_variants_for_pjs_from_messagerie__task.rb b/app/tasks/maintenance/create_variants_for_pjs_from_messagerie__task.rb
new file mode 100644
index 000000000..fa1eb30d2
--- /dev/null
+++ b/app/tasks/maintenance/create_variants_for_pjs_from_messagerie__task.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class CreateVariantsForPjsFromMessagerieTask < MaintenanceTasks::Task
+ attribute :start_text, :string
+ validates :start_text, presence: true
+
+ attribute :end_text, :string
+ validates :end_text, presence: true
+
+ def collection
+ start_date = DateTime.parse(start_text)
+ end_date = DateTime.parse(end_text)
+
+ Dossier
+ .state_en_construction_ou_instruction
+ .where(depose_at: start_date..end_date)
+ end
+
+ def process(dossier)
+ commentaire_ids = Commentaire
+ .where(dossier_id: dossier)
+ .pluck(:id)
+
+ attachments = ActiveStorage::Attachment
+ .where(record_id: commentaire_ids)
+
+ attachments.each do |attachment|
+ next if !(attachment.variable? && attachment.representation_required?)
+ attachment.variant(resize_to_limit: [400, 400]).processed if attachment.variant(resize_to_limit: [400, 400]).key.nil?
+ if attachment.blob.content_type.in?(RARE_IMAGE_TYPES) && attachment.variant(resize_to_limit: [2000, 2000]).key.nil?
+ attachment.variant(resize_to_limit: [2000, 2000]).processed
+ end
+ rescue MiniMagick::Error, ActiveStorage::Error
+ end
+ end
+ end
+end
diff --git a/app/tasks/maintenance/delete_draft_revision_type_de_champs_task.rb b/app/tasks/maintenance/delete_draft_revision_type_de_champs_task.rb
index 95f577403..cc08a519d 100644
--- a/app/tasks/maintenance/delete_draft_revision_type_de_champs_task.rb
+++ b/app/tasks/maintenance/delete_draft_revision_type_de_champs_task.rb
@@ -2,10 +2,10 @@
module Maintenance
class DeleteDraftRevisionTypeDeChampsTask < MaintenanceTasks::Task
- csv_collection
-
+ # Modifie le form d’une démarche à partir d’un CSV (dev spécifique Fonds Verts).
# See UpdateDraftRevisionTypeDeChampsTask for more information
- # Just add delete_flag with "true" to effectively remove the type de champ from the draft.
+ # Just add delete_flag with "true" in CSV to effectively remove the type de champ from the draft.
+ csv_collection
def process(row)
return unless row["delete_flag"] == "true"
diff --git a/app/tasks/maintenance/destroy_incomplete_bulk_messages_task.rb b/app/tasks/maintenance/destroy_incomplete_bulk_messages_task.rb
index 32d1bef43..f0c96cbc8 100644
--- a/app/tasks/maintenance/destroy_incomplete_bulk_messages_task.rb
+++ b/app/tasks/maintenance/destroy_incomplete_bulk_messages_task.rb
@@ -2,6 +2,10 @@
module Maintenance
class DestroyIncompleteBulkMessagesTask < MaintenanceTasks::Task
+ # Périmètre: envoi d’un email groupé aux usagers ayant dossiers en brouillon.
+ # Change la manière dont ces messages sont liés aux démarches.
+ # Suite de BackfillBulkMessagesWithProcedureIdTask
+ # 2024-03-12-01 PR #10071
def collection
BulkMessage.where(procedure: nil).where.missing(:groupe_instructeurs)
end
diff --git a/app/tasks/maintenance/destroy_procedure_without_administrateur_and_without_dossier_task.rb b/app/tasks/maintenance/destroy_procedure_without_administrateur_and_without_dossier_task.rb
index 70860af82..29768d104 100644
--- a/app/tasks/maintenance/destroy_procedure_without_administrateur_and_without_dossier_task.rb
+++ b/app/tasks/maintenance/destroy_procedure_without_administrateur_and_without_dossier_task.rb
@@ -2,6 +2,8 @@
module Maintenance
class DestroyProcedureWithoutAdministrateurAndWithoutDossierTask < MaintenanceTasks::Task
+ # suppression de procédures closes sans admin et sans dossier
+ # 2024-03-18-01 PR #10125
def collection
Procedure.with_discarded.where.missing(:administrateurs, :dossiers)
end
diff --git a/app/tasks/maintenance/disable_remaining_invalid_mon_avis_task.rb b/app/tasks/maintenance/disable_remaining_invalid_mon_avis_task.rb
index 71a78696d..b0bef2c77 100644
--- a/app/tasks/maintenance/disable_remaining_invalid_mon_avis_task.rb
+++ b/app/tasks/maintenance/disable_remaining_invalid_mon_avis_task.rb
@@ -2,6 +2,8 @@
module Maintenance
class DisableRemainingInvalidMonAvisTask < MaintenanceTasks::Task
+ # Supprime les codes d’intégration « mon avis » invalides
+ # 2024-03-18-01 PR #10120
def collection
# rubocop:disable DS/Unscoped
Procedure.unscoped.where.not(monavis_embed: nil)
diff --git a/app/tasks/maintenance/fix_champs_commune_having_value_but_not_external_id_task.rb b/app/tasks/maintenance/fix_champs_commune_having_value_but_not_external_id_task.rb
new file mode 100644
index 000000000..3520075ac
--- /dev/null
+++ b/app/tasks/maintenance/fix_champs_commune_having_value_but_not_external_id_task.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module Maintenance
+ # some of our Champs::CommuneChamp had been corrupted, ie: missing external_id
+ # this tasks fix this issue
+ class FixChampsCommuneHavingValueButNotExternalIdTask < MaintenanceTasks::Task
+ DEFAULT_INSTRUCTEUR_EMAIL = ENV.fetch('DEFAULT_INSTRUCTEUR_EMAIL') { CONTACT_EMAIL }
+
+ def collection
+ Champs::CommuneChamp.select(:id, :value, :external_id)
+ end
+
+ def process(champ)
+ return if !(champ.value.present? && champ.external_id.blank?)
+ champ.reload
+ return if !fixable?(champ)
+
+ response = APIGeoService.commune_by_name_or_postal_code(champ.value)
+ if !response.success?
+ notify("Strange case of existing commune not requestable", champ)
+ else
+ results = JSON.parse(response.body, symbolize_names: true)
+ formated_results = APIGeoService.format_commune_response(results, true)
+ case formated_results.size
+ when 1
+ champ.code = formated_results.first[:value]
+ champ.save!
+ else # otherwise, we can't find the expected departement
+ if champ.dossier.en_construction?
+ champ.code_departement = nil
+ champ.code_postal = nil
+ champ.external_id = nil
+ champ.value = nil
+ champ.save(validate: false)
+
+ ask_user_correction(champ)
+ end
+ end
+ end
+ end
+
+ def count
+ # osf, count is not an option
+ end
+
+ private
+
+ def ask_user_correction(champ)
+ dossier = champ.dossier
+
+ commentaire = CommentaireService.build(current_instructeur, dossier, { body: "Suite à un problème technique, Veuillez re-remplir le champs : #{champ.libelle}" })
+ dossier.flag_as_pending_correction!(commentaire, :incomplete)
+ end
+
+ def current_instructeur
+ user = User.find_by(email: DEFAULT_INSTRUCTEUR_EMAIL)
+ user ||= User.create(email: DEFAULT_INSTRUCTEUR_EMAIL,
+ password: Random.srand,
+ confirmed_at: Time.zone.now,
+ email_verified_at: Time.zone.now)
+ instructeur = user.instructeur
+ instructeur ||= user.create_instructeur!
+
+ instructeur
+ end
+
+ def fixable?(champ)
+ champ.dossier.en_instruction? || champ.dossier.en_construction?
+ end
+
+ def notify(message, champ) = Sentry.capture_message(message, extra: { champ: })
+ end
+end
diff --git a/app/tasks/maintenance/fix_decimal_number_with_spaces_task.rb b/app/tasks/maintenance/fix_decimal_number_with_spaces_task.rb
new file mode 100644
index 000000000..499093a4b
--- /dev/null
+++ b/app/tasks/maintenance/fix_decimal_number_with_spaces_task.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class FixDecimalNumberWithSpacesTask < MaintenanceTasks::Task
+ # normalise les champs nombres en y supprimant les éventuels espaces
+ # 2024-07-01-01 PR #10554
+
+ ANY_SPACES = /[[:space:]]/
+ def collection
+ Champs::DecimalNumberChamp.where.not(value: nil)
+ end
+
+ def process(element)
+ if element.value.present? && ANY_SPACES.match?(element.value)
+ element.update_column(:value, element.value.gsub(ANY_SPACES, ''))
+ end
+ end
+
+ def count
+ # not really interested in counting because it raises PG Statement timeout
+ end
+ end
+end
diff --git a/app/tasks/maintenance/fix_duree_conservation_greater_than_max_duree_conservation_task.rb b/app/tasks/maintenance/fix_duree_conservation_greater_than_max_duree_conservation_task.rb
index 4d3618dbf..adcea8687 100644
--- a/app/tasks/maintenance/fix_duree_conservation_greater_than_max_duree_conservation_task.rb
+++ b/app/tasks/maintenance/fix_duree_conservation_greater_than_max_duree_conservation_task.rb
@@ -2,6 +2,10 @@
module Maintenance
class FixDureeConservationGreaterThanMaxDureeConservationTask < MaintenanceTasks::Task
+ # Corrige la durée de conservation des dossiers :
+ # pour toutes les démarches dont la durée de conservation est supérieure
+ # à celle de l'instance, on prend la durée max de DS (12 mois)
+ # 2024-05-27-01 PR #10107
def collection
Procedure.where('duree_conservation_dossiers_dans_ds > max_duree_conservation_dossiers_dans_ds')
end
diff --git a/app/tasks/maintenance/fix_missing_champs_task.rb b/app/tasks/maintenance/fix_missing_champs_task.rb
deleted file mode 100644
index c7c92eb1a..000000000
--- a/app/tasks/maintenance/fix_missing_champs_task.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-# bundle exec maintenance_tasks perform Maintenance::FixMissingChampsTask --arguments procedure_ids:id1,id2,id3
-module Maintenance
- class FixMissingChampsTask < MaintenanceTasks::Task
- attribute :procedure_ids, array: true, default: []
-
- def collection
- Dossier.joins(:procedure).where(procedure: { id: procedure_ids }).in_batches
- end
-
- def process(dossiers)
- # rubocop:disable Rails/FindEach
- DossierPreloader.new(dossiers).all.each do |dossier|
- # rubocop:enable Rails/FindEach
- maybe_fixable = [dossier, dossier.editing_forks.first].compact.any? { _1.champs.size < _1.revision.types_de_champ.size }
- if maybe_fixable
- DataFixer::DossierChampsMissing.new(dossier:).fix
- end
- end
- end
- end
-end
diff --git a/app/tasks/maintenance/fix_open_procedures_with_closing_reason_task.rb b/app/tasks/maintenance/fix_open_procedures_with_closing_reason_task.rb
index daf619542..711e454ff 100644
--- a/app/tasks/maintenance/fix_open_procedures_with_closing_reason_task.rb
+++ b/app/tasks/maintenance/fix_open_procedures_with_closing_reason_task.rb
@@ -2,6 +2,8 @@
module Maintenance
class FixOpenProceduresWithClosingReasonTask < MaintenanceTasks::Task
+ # Corrige des démarches avec un motif de fermerture alors qu’elles ont été publiées
+ # 2024-05-27-01 PR #10181
def collection
Procedure
.with_discarded
diff --git a/app/tasks/maintenance/helpscout_delete_old_conversations_task.rb b/app/tasks/maintenance/helpscout_delete_old_conversations_task.rb
new file mode 100644
index 000000000..a8e37a874
--- /dev/null
+++ b/app/tasks/maintenance/helpscout_delete_old_conversations_task.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class HelpscoutDeleteOldConversationsTask < MaintenanceTasks::Task
+ # Delete Helpscout conversations not modified in the last 2 years, given a status.
+ # In order to delete all conversations, this task must be invoked 4 times
+ # for the 4 status: active, closed, spam, pending.
+ # Respects the Helpscount API rate limit (200 calls per minute).
+
+ attribute :status, :string # active, closed, spam, or pending
+ validates :status, presence: true
+
+ MODIFIED_BEFORE = 2.years.freeze
+
+ throttle_on(backoff: 1.minute) do
+ limit = Rails.cache.read(Helpscout::API::RATELIMIT_KEY)
+ limit.present? && limit.to_i <= 26 # check is made before each process but not before listing each page. External activity can affect the rate limit.
+ end
+
+ def count
+ _conversations, pagination = api.list_old_conversations(status, modified_before)
+
+ pagination[:totalElements]
+ end
+
+ # Because conversations are deleted progressively,
+ # ignore cursor and always pick the first page
+ def enumerator_builder(cursor:)
+ Enumerator.new do |yielder|
+ loop do
+ conversations, pagination = api.list_old_conversations(status, modified_before)
+ conversations.each do |conversation|
+ yielder.yield(conversation[:id], nil) # don't care about cursor parameter
+ end
+
+ # "number" is the current page (always 1 in our case)
+ # iterate until there are no remaining pages
+ break if pagination[:totalPages] == 0 || pagination[:totalPages] == pagination[:number]
+ end
+ end
+ end
+
+ def process(conversation_id)
+ @api.delete_conversation(conversation_id)
+ end
+
+ private
+
+ def api
+ @api ||= Helpscout::API.new
+ end
+
+ def modified_before
+ MODIFIED_BEFORE.ago.utc.beginning_of_day
+ end
+ end
+end
diff --git a/app/tasks/maintenance/helpscout_delete_old_customers_task.rb b/app/tasks/maintenance/helpscout_delete_old_customers_task.rb
new file mode 100644
index 000000000..32e241435
--- /dev/null
+++ b/app/tasks/maintenance/helpscout_delete_old_customers_task.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class HelpscoutDeleteOldCustomersTask < MaintenanceTasks::Task
+ # Delete Helpscout customers not seen in the last 2 years
+ # with any conversations, and any data related with GPDR compliance.
+ # Respects the Helpscout API rate limit (200 calls per minute).
+
+ MODIFIED_BEFORE = 2.years.freeze
+
+ throttle_on(backoff: 1.minute) do
+ limit = Rails.cache.read(Helpscout::API::RATELIMIT_KEY)
+ limit.present? && limit.to_i <= 26 # check is made before each process but not before listing each page. External activity can affect the rate limit.
+ end
+
+ def count
+ _customers, pagination = api.list_old_customers(modified_before)
+
+ pagination[:totalElements]
+ end
+
+ # Because customers are deleted progressively,
+ # ignore cursor and always pick the first page
+ def enumerator_builder(cursor:)
+ Enumerator.new do |yielder|
+ loop do
+ customers, pagination = api.list_old_customers(modified_before)
+ customers.each do |customer|
+ yielder.yield(customer[:id], nil) # don't care about cursor parameter
+ end
+
+ # "number" is the current page (always 1 in our case)
+ # iterate until there are no remaining pages
+ break if pagination[:totalPages] == 0 || pagination[:totalPages] == pagination[:number]
+ end
+ end
+ end
+
+ def process(customer_id)
+ api.delete_customer(customer_id)
+ rescue Helpscout::API::RateLimitError # despite throttle and counter, race conditions sometimes lead to rate limit hit
+ sleep 1.minute
+ retry
+ end
+
+ private
+
+ def api
+ @api ||= Helpscout::API.new
+ end
+
+ def modified_before
+ MODIFIED_BEFORE.ago.utc.beginning_of_day
+ end
+ end
+end
diff --git a/app/tasks/maintenance/hotfix_former_procedure_presentation_naming_task.rb b/app/tasks/maintenance/hotfix_former_procedure_presentation_naming_task.rb
new file mode 100644
index 000000000..3d3a6f9af
--- /dev/null
+++ b/app/tasks/maintenance/hotfix_former_procedure_presentation_naming_task.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Maintenance
+ # a previous commit : https://github.com/demarches-simplifiees/demarches-simplifiees.fr/pull/10625/commits/305b8c13c75a711a85521d0b19659293d8d92805
+ # the previous brokes a naming convention on ProcedurePresentation.filters|displayed_fields|sort
+ # this commit
+ # it adjusts live data to fit new convention and avoid validation error
+ class HotfixFormerProcedurePresentationNamingTask < MaintenanceTasks::Task
+ def collection
+ ProcedurePresentation.all
+ end
+
+ def process(element)
+ element.displayed_fields = element.displayed_fields.map do |displayed_field|
+ if displayed_field['table'] == 'type_de_champ_private'
+ displayed_field['table'] = 'type_de_champ'
+ end
+ displayed_field
+ end
+ element.filters.map do |status, filters_by_status|
+ element.filters[status] = filters_by_status.map do |filter_by_status|
+ if filter_by_status['table'] == 'type_de_champ_private'
+ filter_by_status['table'] = 'type_de_champ'
+ end
+ filter_by_status
+ end
+ end
+ if element.sort['table'] == 'type_de_champ_private'
+ element.sort['table'] = 'type_de_champ'
+ end
+ element.save!
+ rescue ActiveRecord::RecordInvalid
+ # do nothing, former invalid ProcedurePresentation still exist
+ # cf: La validation a échoué : Le champ « Displayed fields » etablissement.entreprise_siren n’est pas une colonne permise
+ end
+ end
+end
diff --git a/app/tasks/maintenance/move_dol_to_cold_storage_task.rb b/app/tasks/maintenance/move_dol_to_cold_storage_task.rb
index 8f039d94c..f4ab280b3 100644
--- a/app/tasks/maintenance/move_dol_to_cold_storage_task.rb
+++ b/app/tasks/maintenance/move_dol_to_cold_storage_task.rb
@@ -2,6 +2,10 @@
module Maintenance
class MoveDolToColdStorageTask < MaintenanceTasks::Task
+ # Opération de rattrapage suite à un cron qui ne fonctionnait plus.
+ # Permet de déplacer toutes les traces fonctionnelles (DossierOperationLog)
+ # vers le stockage object plutot que de les conserver en BDD
+ # 2024-04-15-01
attribute :start_text, :string
validates :start_text, presence: true
diff --git a/app/tasks/maintenance/normalize_rna_values_task.rb b/app/tasks/maintenance/normalize_rna_values_task.rb
new file mode 100644
index 000000000..3837d7bf7
--- /dev/null
+++ b/app/tasks/maintenance/normalize_rna_values_task.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class NormalizeRNAValuesTask < MaintenanceTasks::Task
+ def collection
+ Champs::RNAChamp.where.not(value: nil)
+ end
+
+ def process(element)
+ if /\s/.match?(element.value)
+ element.update_column(:value, element.value.gsub(/\s+/, ''))
+ end
+ end
+
+ def count
+ # to costly
+ end
+ end
+end
diff --git a/app/tasks/maintenance/phishing_alert_task.rb b/app/tasks/maintenance/phishing_alert_task.rb
new file mode 100644
index 000000000..0f0efa535
--- /dev/null
+++ b/app/tasks/maintenance/phishing_alert_task.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class PhishingAlertTask < MaintenanceTasks::Task
+ csv_collection
+
+ def process(row)
+ email = row["Identity"].delete('"')
+ user = User.find_by(email: email)
+
+ # if the user has been updated less than a minute ago
+ # we guess that the user has already been processed
+ # in another row of the csv
+ return if user.nil? || 1.minute.ago < user.updated_at
+
+ user.update(password: SecureRandom.hex)
+
+ PhishingAlertMailer.notify(user).deliver_later
+ end
+ end
+end
diff --git a/app/tasks/maintenance/populate_rna_json_value_task.rb b/app/tasks/maintenance/populate_rna_json_value_task.rb
new file mode 100644
index 000000000..e366d3aee
--- /dev/null
+++ b/app/tasks/maintenance/populate_rna_json_value_task.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# Dans le cadre de la story pour pouvoir rechercher un dossier en fonction des valeurs des champs branchées sur une API, voici une première pièce qui cible les champs RNA/RNF/SIRET (notamment les adresses pour de la recherche). Cette PR intègre :
+# la normalisation des adresses des champs RNA/RNF/SIRET
+# le fait de stocker ces données normalisées dans le champs.value_json (un jsonb)
+# le backfill les anciens champs RNA/RNF/SIRET
+module Maintenance
+ class PopulateRNAJSONValueTask < MaintenanceTasks::Task
+ def collection
+ Champs::RNAChamp.where.not(value: nil)
+ end
+
+ def process(champ)
+ return if champ&.dossier&.procedure&.id.blank?
+ data = APIEntreprise::RNAAdapter.new(champ.value, champ&.dossier&.procedure&.id).to_params
+ return if data.blank?
+ champ.update_with_external_data!(data:)
+ rescue URI::InvalidURIError
+ # some Champs::RNAChamp contain spaces which raise this error
+ rescue ActiveRecord::RecordNotFound
+ # some Champs::RNAChamp procedure had been soft deleted
+ end
+
+ def count
+ # not really interested in counting because it raises PG Statement timeout
+ end
+ end
+end
diff --git a/app/tasks/maintenance/populate_rnf_json_value_task.rb b/app/tasks/maintenance/populate_rnf_json_value_task.rb
new file mode 100644
index 000000000..baaca634d
--- /dev/null
+++ b/app/tasks/maintenance/populate_rnf_json_value_task.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+# Dans le cadre de la story pour pouvoir rechercher un dossier en fonction des valeurs des champs branchées sur une API, voici une première pièce qui cible les champs RNA/RNF/SIRET (notamment les adresses pour de la recherche). Cette PR intègre :
+# la normalisation des adresses des champs RNA/RNF/SIRET
+# le fait de stocker ces données normalisées dans le champs.value_json (un jsonb)
+# le backfill les anciens champs RNA/RNF/SIRET
+module Maintenance
+ class PopulateRNFJSONValueTask < MaintenanceTasks::Task
+ include Dry::Monads[:result]
+
+ def collection
+ Champs::RNFChamp.where("external_id != null and data != null") # had been found
+ # Collection to be iterated over
+ # Must be Active Record Relation or Array
+ end
+
+ def process(champ)
+ result = champ.fetch_external_data
+ case result
+ in Success(data)
+ begin
+ champ.update_with_external_data!(data:)
+ rescue ActiveRecord::RecordInvalid
+ # some champ might have dossier nil
+ end
+ else # fondation was removed, but we kept API data in data:, use it to restore stuff
+
+ champ.update_with_external_data!(data: champ.data.with_indifferent_access)
+ end
+ end
+
+ def count
+ # not really interested in counting because it raises PG Statement timeout
+ end
+ end
+end
diff --git a/app/tasks/maintenance/populate_siret_value_json_task.rb b/app/tasks/maintenance/populate_siret_value_json_task.rb
new file mode 100644
index 000000000..3b43ebb73
--- /dev/null
+++ b/app/tasks/maintenance/populate_siret_value_json_task.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# Dans le cadre de la story pour pouvoir rechercher un dossier en fonction des valeurs des champs branchées sur une API, voici une première pièce qui cible les champs RNA/RNF/SIRET (notamment les adresses pour de la recherche). Cette PR intègre :
+# la normalisation des adresses des champs RNA/RNF/SIRET
+# le fait de stocker ces données normalisées dans le champs.value_json (un jsonb)
+# le backfill les anciens champs RNA/RNF/SIRET
+module Maintenance
+ class PopulateSiretValueJSONTask < MaintenanceTasks::Task
+ def collection
+ Champs::SiretChamp.where.not(value: nil)
+ end
+
+ def process(champ)
+ return if champ.etablissement.blank?
+ champ.update!(value_json: APIGeoService.parse_etablissement_address(champ.etablissement))
+ rescue ActiveRecord::RecordInvalid
+ # noop, just a champ without dossier
+ end
+
+ def count
+ # not really interested in counting because it raises PG Statement timeout
+ end
+ end
+end
diff --git a/app/tasks/maintenance/prefill_individual_email_verified_at_task.rb b/app/tasks/maintenance/prefill_individual_email_verified_at_task.rb
new file mode 100644
index 000000000..fa6e045d2
--- /dev/null
+++ b/app/tasks/maintenance/prefill_individual_email_verified_at_task.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# We are going to confirm the various email addresses of the users in the system.
+# Individual model (mandant) needs their email_verified_at attribute to be set in order to receive emails.
+# This task sets the email_verified_at attribute to the current time for all the individual to be backward compatible
+# See https://github.com/demarches-simplifiees/demarches-simplifiees.fr/issues/10450
+module Maintenance
+ class PrefillIndividualEmailVerifiedAtTask < MaintenanceTasks::Task
+ def collection
+ Individual.in_batches
+ end
+
+ def process(batch_of_individuals)
+ batch_of_individuals.update_all(email_verified_at: Time.zone.now)
+ end
+ end
+end
diff --git a/app/tasks/maintenance/prefill_user_email_verified_at_task.rb b/app/tasks/maintenance/prefill_user_email_verified_at_task.rb
new file mode 100644
index 000000000..ad3ba6f5b
--- /dev/null
+++ b/app/tasks/maintenance/prefill_user_email_verified_at_task.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# We are going to confirm the various email addresses of the users in the system.
+# User model needs their email_verified_at attribute to be set in order to receive emails.
+# This task sets the email_verified_at attribute to the current time for all users to be backward compatible
+# See https://github.com/demarches-simplifiees/demarches-simplifiees.fr/issues/10450
+module Maintenance
+ class PrefillUserEmailVerifiedAtTask < MaintenanceTasks::Task
+ def collection
+ User.in_batches
+ end
+
+ def process(batch_of_users)
+ batch_of_users.update_all(email_verified_at: Time.zone.now)
+ end
+ end
+end
diff --git a/app/tasks/maintenance/recompute_blob_checksum_task.rb b/app/tasks/maintenance/recompute_blob_checksum_task.rb
new file mode 100644
index 000000000..474c535b9
--- /dev/null
+++ b/app/tasks/maintenance/recompute_blob_checksum_task.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class RecomputeBlobChecksumTask < MaintenanceTasks::Task
+ # Avant février 2024, les filigranes ont corrompu les hash des fichiers.
+ # Régulièrement, des dossiers en brouillon étaient déposés avec ce problème
+ # (on retrouve les fichiers corrompu dans l'onglet retry de sidekiq).
+ # Cette tache recalcule les hashes.
+ # 2024-05-27-01
+ attribute :blob_ids, :string
+ validates :blob_ids, presence: true
+
+ def collection
+ ids = blob_ids.split(',').map(&:strip).map(&:to_i)
+ ActiveStorage::Blob.where(id: ids)
+ end
+
+ def process(blob)
+ blob.upload(StringIO.new(blob.download), identify: false)
+ blob.save!
+ end
+
+ def count
+ # Optionally, define the number of rows that will be iterated over
+ # This is used to track the task's progress
+ end
+ end
+end
diff --git a/app/tasks/maintenance/remove_non_unique_champs_task.rb b/app/tasks/maintenance/remove_non_unique_champs_task.rb
new file mode 100644
index 000000000..0d9bf1d64
--- /dev/null
+++ b/app/tasks/maintenance/remove_non_unique_champs_task.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class RemoveNonUniqueChampsTask < MaintenanceTasks::Task
+ attribute :stable_ids, :string
+ validates :stable_ids, presence: true
+
+ def collection
+ champs = Champ.where(stable_id: stable_ids.split(',').map(&:strip).map(&:to_i))
+ champs
+ .group_by { [_1.dossier_id, _1.stream, _1.stable_id, _1.row_id] }
+ .values
+ .filter { _1.size > 1 }
+ end
+
+ def process(champs)
+ champs_to_remove = champs.sort_by(&:updated_at)[0...-1]
+ champs_to_remove.each do |champ|
+ champ.update_column(:stream, 'bad_data')
+ end
+ end
+ end
+end
diff --git a/app/tasks/maintenance/remove_piece_justificative_file_not_visible_task.rb b/app/tasks/maintenance/remove_piece_justificative_file_not_visible_task.rb
new file mode 100644
index 000000000..473b5698c
--- /dev/null
+++ b/app/tasks/maintenance/remove_piece_justificative_file_not_visible_task.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class RemovePieceJustificativeFileNotVisibleTask < MaintenanceTasks::Task
+ attribute :procedure_id, :string
+ validates :procedure_id, presence: true
+
+ def collection
+ procedure = Procedure.with_discarded.find(procedure_id.strip.to_i)
+ procedure.dossiers.state_not_brouillon
+ end
+
+ def process(dossier)
+ dossier.remove_piece_justificative_file_not_visible!
+ end
+ end
+end
diff --git a/app/tasks/maintenance/resolve_pending_correction_for_dossier_with_invalid_commune_external_id_task.rb b/app/tasks/maintenance/resolve_pending_correction_for_dossier_with_invalid_commune_external_id_task.rb
new file mode 100644
index 000000000..badfa4c43
--- /dev/null
+++ b/app/tasks/maintenance/resolve_pending_correction_for_dossier_with_invalid_commune_external_id_task.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Maintenance
+ # this maintenance task should be run due to app/tasks/maintenance/fix_champs_commune_having_value_but_not_external_id_task.rb
+ # if you have not ran app/tasks/maintenance/fix_champs_commune_having_value_but_not_external_id_task.rb, this task is not required
+ class ResolvePendingCorrectionForDossierWithInvalidCommuneExternalIdTask < MaintenanceTasks::Task
+ DEFAULT_INSTRUCTEUR_EMAIL = ENV.fetch('DEFAULT_INSTRUCTEUR_EMAIL') { CONTACT_EMAIL }
+
+ no_collection
+
+ def process
+ DossierCorrection.joins(:commentaire)
+ .where(commentaire: { instructeur_id: current_instructeur.id })
+ .where(resolved_at: nil)
+ .find_each do |dossier_correction|
+ penultimate_traitement, last_traitement = *dossier_correction.dossier.traitements.last(2)
+ dossier_correction.resolve!
+
+ next if penultimate_traitement.nil? || last_traitement.nil?
+
+ if last_traitement_by_us?(last_traitement) && last_transition_to_en_construction?(last_traitement, penultimate_traitement)
+ dossier_correction.dossier.passer_en_instruction(instructeur: current_instructeur) if dossier_correction.dossier.validate(:champs_public_value)
+ end
+ end
+ end
+
+ def current_instructeur
+ @current_instructeur = User.find_by(email: DEFAULT_INSTRUCTEUR_EMAIL).instructeur
+ end
+
+ def current_instructeur_id
+ current_instructeur.id
+ end
+
+ def current_instructeur_email
+ current_instructeur.email
+ end
+
+ def last_traitement_by_us?(traitement)
+ traitement&.instructeur_email == DEFAULT_INSTRUCTEUR_EMAIL
+ end
+
+ def last_transition_to_en_construction?(last_traitement, penultimate_traitement)
+ last_traitement.state == "en_construction" && penultimate_traitement.state == 'en_instruction'
+ end
+ end
+end
diff --git a/app/tasks/maintenance/rotate_api_particulier_token_encryption_task.rb b/app/tasks/maintenance/rotate_api_particulier_token_encryption_task.rb
new file mode 100644
index 000000000..a47c6e132
--- /dev/null
+++ b/app/tasks/maintenance/rotate_api_particulier_token_encryption_task.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class RotateAPIParticulierTokenEncryptionTask < MaintenanceTasks::Task
+ def collection
+ # rubocop:disable DS/Unscoped
+ Procedure.unscoped.where.not(encrypted_api_particulier_token: nil)
+ # rubocop:enable DS/Unscoped
+ end
+
+ def process(procedure)
+ decrypted_token = procedure.api_particulier_token
+
+ procedure.api_particulier_token = decrypted_token
+ procedure.save!(validate: false)
+ end
+
+ def count
+ collection.count
+ end
+ end
+end
diff --git a/app/tasks/maintenance/samsung_browser_is_supported_task.rb b/app/tasks/maintenance/samsung_browser_is_supported_task.rb
index 988e3c010..d9ebfbb3d 100644
--- a/app/tasks/maintenance/samsung_browser_is_supported_task.rb
+++ b/app/tasks/maintenance/samsung_browser_is_supported_task.rb
@@ -2,6 +2,9 @@
module Maintenance
class SamsungBrowserIsSupportedTask < MaintenanceTasks::Task
+ # Corrige une donnée si le navigateur utilisé
+ # dans l’historique des Traitements des dossiers
+ # 2024-02-21-01
def collection
Traitement.where(browser_name: 'Samsung Browser', browser_version: 12..)
end
diff --git a/app/tasks/maintenance/spread_dossier_deletion_task.rb b/app/tasks/maintenance/spread_dossier_deletion_task.rb
index 2e2f3c6a4..9c94bd6cf 100644
--- a/app/tasks/maintenance/spread_dossier_deletion_task.rb
+++ b/app/tasks/maintenance/spread_dossier_deletion_task.rb
@@ -2,6 +2,8 @@
module Maintenance
class SpreadDossierDeletionTask < MaintenanceTasks::Task
+ # Contourne un égorgement de suppression de millions de dossiers qui aurait eu lieu le même jour
+ # 2024-05-27-01 PR #10062
ERROR_OCCURED_AT = Date.new(2024, 2, 14)
ERROR_OCCURED_RANGE = ERROR_OCCURED_AT.at_midnight..(ERROR_OCCURED_AT + 1.day)
SPREAD_DURATION_IN_DAYS = 150
diff --git a/app/tasks/maintenance/t20241009_noop_attempt_run_on_deploy_task.rb b/app/tasks/maintenance/t20241009_noop_attempt_run_on_deploy_task.rb
new file mode 100644
index 000000000..36c5ae2e2
--- /dev/null
+++ b/app/tasks/maintenance/t20241009_noop_attempt_run_on_deploy_task.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class T20241009NoopAttemptRunOnDeployTask < MaintenanceTasks::Task
+ # Documentation: cette tâche ne fait rien mais sert à vérifier
+ # qu'elle sera bien exécutée sur le déploiement suivant
+ # pour remplacer after party.
+
+ include RunnableOnDeployConcern
+ include StatementsHelpersConcern
+
+ # Uncomment only if this task MUST run imperatively on its first deployment.
+ # If possible, leave commented for manual execution later.
+ run_on_first_deploy
+
+ def collection
+ 1.upto(10).to_a
+ end
+
+ def process(element)
+ # NOOP
+ end
+
+ def count
+ 10
+ end
+ end
+end
diff --git a/app/tasks/maintenance/t20241018fix_follows_with_nil_pieces_jointes_seen_at_task.rb b/app/tasks/maintenance/t20241018fix_follows_with_nil_pieces_jointes_seen_at_task.rb
new file mode 100644
index 000000000..96c62ad53
--- /dev/null
+++ b/app/tasks/maintenance/t20241018fix_follows_with_nil_pieces_jointes_seen_at_task.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class T20241018fixFollowsWithNilPiecesJointesSeenAtTask < MaintenanceTasks::Task
+ # Normalement tous les follows auraient du être mis à jour lors de la migration db/migrate/20240911064340_backfill_follows_with_pieces_jointes_seen_at.rb
+ # Mais, sur l'instance de DS, 57 follows créés lorsque la migration a tourné ont gardé une valeur nulle pour pieces_jointes_seen_at. On les met à jour ici.
+
+ include RunnableOnDeployConcern
+ include StatementsHelpersConcern
+
+ # Uncomment only if this task MUST run imperatively on its first deployment.
+ # If possible, leave commented for manual execution later.
+ # run_on_first_deploy
+
+ def collection
+ # Collection to be iterated over
+ # Must be Active Record Relation or Array
+ Follow.where(pieces_jointes_seen_at: nil)
+ end
+
+ def process(element)
+ # The work to be done in a single iteration of the task.
+ # This should be idempotent, as the same element may be processed more
+ # than once if the task is interrupted and resumed.
+ element.update_columns(pieces_jointes_seen_at: Time.zone.now)
+ end
+ end
+end
diff --git a/app/tasks/maintenance/update_api_entreprise_token_expires_at_task.rb b/app/tasks/maintenance/update_api_entreprise_token_expires_at_task.rb
new file mode 100644
index 000000000..206a270ec
--- /dev/null
+++ b/app/tasks/maintenance/update_api_entreprise_token_expires_at_task.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Maintenance
+ class UpdateAPIEntrepriseTokenExpiresAtTask < MaintenanceTasks::Task
+ def collection
+ Procedure.with_discarded.where.not(api_entreprise_token: nil)
+ end
+
+ def process(procedure)
+ procedure.set_api_entreprise_token_expires_at
+ procedure.save!
+ end
+ end
+end
diff --git a/app/tasks/maintenance/update_closing_reason_if_no_replaced_by_id_task.rb b/app/tasks/maintenance/update_closing_reason_if_no_replaced_by_id_task.rb
index daf013ca0..99e715dd3 100644
--- a/app/tasks/maintenance/update_closing_reason_if_no_replaced_by_id_task.rb
+++ b/app/tasks/maintenance/update_closing_reason_if_no_replaced_by_id_task.rb
@@ -2,6 +2,9 @@
module Maintenance
class UpdateClosingReasonIfNoReplacedByIdTask < MaintenanceTasks::Task
+ # Remet les messages de cloture d'une démarche proprement (sinon affichage KO).
+ # Avant BackfillClosingReasonInClosedProceduresTask
+ # 2024-03-21-01 PR #10158
def collection
Procedure
.with_discarded
diff --git a/app/tasks/maintenance/update_conditions_based_on_commune_or_epci_champ_task.rb b/app/tasks/maintenance/update_conditions_based_on_commune_or_epci_champ_task.rb
index c8f2afab5..bf67eb3b4 100644
--- a/app/tasks/maintenance/update_conditions_based_on_commune_or_epci_champ_task.rb
+++ b/app/tasks/maintenance/update_conditions_based_on_commune_or_epci_champ_task.rb
@@ -2,6 +2,10 @@
module Maintenance
class UpdateConditionsBasedOnCommuneOrEpciChampTask < MaintenanceTasks::Task
+ # Met à jour les conditions et règles de routage
+ # pour les champs communes et ECPI suite à l'ajout de nouveaux opérateurs
+ # Voir aussi UpdateRoutingRulesBasedOnCommuneOrEpciChampTask
+ # 2023-12-20-01 PR #9850
include Logic
def collection
diff --git a/app/tasks/maintenance/update_draft_revision_type_de_champs_task.rb b/app/tasks/maintenance/update_draft_revision_type_de_champs_task.rb
index af03e79ec..60b39e65b 100644
--- a/app/tasks/maintenance/update_draft_revision_type_de_champs_task.rb
+++ b/app/tasks/maintenance/update_draft_revision_type_de_champs_task.rb
@@ -2,6 +2,8 @@
module Maintenance
class UpdateDraftRevisionTypeDeChampsTask < MaintenanceTasks::Task
+ # Modifie le form d’une démarche à partir d’un CSV (dev pour les Fonds Verts)
+
csv_collection
# CSV structure:
diff --git a/app/tasks/maintenance/update_routing_rules_based_on_commune_or_epci_champ_task.rb b/app/tasks/maintenance/update_routing_rules_based_on_commune_or_epci_champ_task.rb
index afdda78e9..22f1ad2e4 100644
--- a/app/tasks/maintenance/update_routing_rules_based_on_commune_or_epci_champ_task.rb
+++ b/app/tasks/maintenance/update_routing_rules_based_on_commune_or_epci_champ_task.rb
@@ -2,6 +2,10 @@
module Maintenance
class UpdateRoutingRulesBasedOnCommuneOrEpciChampTask < MaintenanceTasks::Task
+ # Ces 2 tâches mettent à jour les conditions et règles de routage
+ # pour les champs communes et ECPI suite à l'ajout de nouveaux opérateurs
+ # Voir aussi UpdateConditionsBasedOnCommuneOrEpciChampTask
+ # 2023-12-20-01 PR #9850
include Logic
def collection
diff --git a/app/tasks/maintenance/update_service_etablissement_infos_task.rb b/app/tasks/maintenance/update_service_etablissement_infos_task.rb
index 8dcf511bd..a8fdfe15c 100644
--- a/app/tasks/maintenance/update_service_etablissement_infos_task.rb
+++ b/app/tasks/maintenance/update_service_etablissement_infos_task.rb
@@ -2,6 +2,9 @@
module Maintenance
class UpdateServiceEtablissementInfosTask < MaintenanceTasks::Task
+ # Géocode les services à partir des établissements
+ # 2024-05-27-01 PR #10106
+
# No more 20 geocoding by 10 seconds window
THROTTLE_LIMIT = 20
THROTTLE_PERIOD = 10.seconds
diff --git a/app/tasks/maintenance/update_zones_task.rb b/app/tasks/maintenance/update_zones_task.rb
index c985f946a..203b00adf 100644
--- a/app/tasks/maintenance/update_zones_task.rb
+++ b/app/tasks/maintenance/update_zones_task.rb
@@ -2,6 +2,8 @@
module Maintenance
class UpdateZonesTask < MaintenanceTasks::Task
+ # Synchronise les zones en base à partir du fichier de config zones.yml
+ # 2024-05-27-01 PR #10077
def collection
config = Psych.safe_load(Rails.root.join("config", "zones.yml").read)
config['ministeres']
diff --git a/app/types/column_type.rb b/app/types/column_type.rb
new file mode 100644
index 000000000..b54e49f9c
--- /dev/null
+++ b/app/types/column_type.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class ColumnType < ActiveRecord::Type::Value
+ # value can come from:
+ # setter: column (Column),
+ # form_input: column.id == { procedure_id:, column_id: }.to_json (String),
+ # from db: { procedure_id:, column_id: } (Hash)
+ def cast(value)
+ case value
+ in NilClass
+ nil
+ in Column
+ value
+ # from form
+ in String => id
+ h_id = JSON.parse(id, symbolize_names: true)
+ Column.find(h_id)
+ # from db
+ in Hash => h_id
+ Column.find(h_id)
+ end
+ end
+
+ # db -> ruby
+ def deserialize(value) = cast(value)
+
+ # ruby -> db
+ def serialize(value)
+ case value
+ in NilClass
+ nil
+ in Column
+ JSON.generate(value.h_id)
+ else
+ raise ArgumentError, "Invalid value for Column serialization: #{value}"
+ end
+ end
+end
diff --git a/app/types/export_item_type.rb b/app/types/export_item_type.rb
new file mode 100644
index 000000000..04c37c6ef
--- /dev/null
+++ b/app/types/export_item_type.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class ExportItemType < ActiveRecord::Type::Value
+ # form_input, or setter -> type
+ def cast(value)
+ value = value.deep_symbolize_keys if value.respond_to?(:deep_symbolize_keys)
+
+ case value
+ in ExportItem
+ value
+ in NilClass # default value
+ nil
+ # from db
+ in { template: Hash, enabled: TrueClass | FalseClass } => h
+
+ ExportItem.new(**h.slice(:template, :enabled, :stable_id))
+ # from form
+ in { template: String } => h
+
+ template = JSON.parse(h[:template]).deep_symbolize_keys
+ enabled = h[:enabled] == 'true'
+ stable_id = h[:stable_id]&.to_i
+ ExportItem.new(template:, enabled:, stable_id:)
+ end
+ end
+
+ # db -> ruby
+ def deserialize(value) = cast(value&.then { JSON.parse(_1) })
+
+ # ruby -> db
+ def serialize(value)
+ case value
+ in NilClass
+ nil
+ in ExportItem
+ JSON.generate({
+ template: value.template,
+ enabled: value.enabled,
+ stable_id: value.stable_id
+ }.compact)
+ else
+ raise ArgumentError, "Invalid value for ExportItem serialization: #{value}"
+ end
+ end
+end
diff --git a/app/types/exported_column_type.rb b/app/types/exported_column_type.rb
new file mode 100644
index 000000000..dddd35532
--- /dev/null
+++ b/app/types/exported_column_type.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class ExportedColumnType < ActiveRecord::Type::Value
+ # form_input or setter -> type
+ def cast(value)
+ value = value.deep_symbolize_keys if value.respond_to?(:deep_symbolize_keys)
+
+ case value
+ in ExportedColumn
+ value
+ in NilClass # default value
+ nil
+ # from db
+ in { id: String|Hash, libelle: String } => h
+ ExportedColumn.new(column: ColumnType.new.cast(h[:id]), libelle: h[:libelle])
+ # from form
+ in String
+ h = JSON.parse(value).deep_symbolize_keys
+ ExportedColumn.new(column: ColumnType.new.cast(h[:id]), libelle: h[:libelle])
+ end
+ end
+
+ # db -> ruby
+ def deserialize(value) = cast(value&.then { JSON.parse(_1) })
+
+ # ruby -> db
+ def serialize(value)
+ case value
+ in NilClass
+ nil
+ in ExportedColumn
+ JSON.generate({
+ id: value.column.h_id,
+ libelle: value.libelle
+ })
+ else
+ raise ArgumentError, "Invalid value for ExportedColumn serialization: #{value}"
+ end
+ end
+end
diff --git a/app/types/filtered_column_type.rb b/app/types/filtered_column_type.rb
new file mode 100644
index 000000000..ca097e4d5
--- /dev/null
+++ b/app/types/filtered_column_type.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class FilteredColumnType < ActiveRecord::Type::Value
+ # form_input or setter -> type
+ def cast(value)
+ value = value.deep_symbolize_keys if value.respond_to?(:deep_symbolize_keys)
+
+ case value
+ in FilteredColumn
+ value
+ in NilClass # default value
+ nil
+ # from form (id is a string) or from db (id is a hash)
+ in { id: String|Hash, filter: String } => h
+ FilteredColumn.new(column: ColumnType.new.cast(h[:id]), filter: h[:filter])
+ end
+ end
+
+ # db -> ruby
+ def deserialize(value) = cast(value&.then { JSON.parse(_1) })
+
+ # ruby -> db
+ def serialize(value)
+ case value
+ in NilClass
+ nil
+ in FilteredColumn
+ JSON.generate({
+ id: value.column.h_id,
+ filter: value.filter
+ })
+ else
+ raise ArgumentError, "Invalid value for FilteredColumn serialization: #{value}"
+ end
+ end
+end
diff --git a/app/types/sorted_column_type.rb b/app/types/sorted_column_type.rb
new file mode 100644
index 000000000..b9daae095
--- /dev/null
+++ b/app/types/sorted_column_type.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class SortedColumnType < ActiveRecord::Type::Value
+ # form_input or setter -> type
+ def cast(value)
+ value = value.deep_symbolize_keys if value.respond_to?(:deep_symbolize_keys)
+
+ case value
+ in SortedColumn
+ value
+ in NilClass # default value
+ nil
+ # from form (id is a string) or from db (id is a hash)
+ in { order: 'asc'|'desc', id: String|Hash } => h
+ SortedColumn.new(column: ColumnType.new.cast(h[:id]), order: h[:order])
+ end
+ end
+
+ # db -> ruby
+ def deserialize(value) = cast(value&.then { JSON.parse(_1) })
+
+ # ruby -> db
+ def serialize(value)
+ case value
+ in NilClass
+ nil
+ in SortedColumn
+ JSON.generate({
+ id: value.column.h_id,
+ order: value.order
+ })
+ else
+ raise ArgumentError, "Invalid value for SortedColumn serialization: #{value}"
+ end
+ end
+end
diff --git a/app/validators/export_template_validator.rb b/app/validators/export_template_validator.rb
new file mode 100644
index 000000000..51099b714
--- /dev/null
+++ b/app/validators/export_template_validator.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+class ExportTemplateValidator < ActiveModel::Validator
+ def validate(export_template)
+ return if !export_template.template_zip?
+
+ validate_all_templates(export_template)
+
+ return if export_template.errors.any? # no need to continue if the templates are invalid
+
+ validate_dossier_folder(export_template)
+ validate_export_pdf(export_template)
+ validate_pjs(export_template)
+
+ validate_different_templates(export_template)
+ end
+
+ private
+
+ def validate_all_templates(export_template)
+ [export_template.dossier_folder, export_template.export_pdf, *export_template.pjs].each(&:template_string)
+
+ rescue StandardError
+ export_template.errors.add(:base, :invalid_template)
+ end
+
+ def validate_dossier_folder(export_template)
+ if !mentions(export_template.dossier_folder.template).include?('dossier_number')
+ export_template.errors.add(:dossier_folder, :dossier_number_required)
+ end
+ end
+
+ def mentions(template)
+ TiptapService.used_tags_and_libelle_for(template).map(&:first)
+ end
+
+ def validate_export_pdf(export_template)
+ return if !export_template.export_pdf.enabled?
+
+ if export_template.export_pdf.template_string.empty?
+ export_template.errors.add(:export_pdf, :blank)
+ end
+ end
+
+ def validate_pjs(export_template)
+ libelle_by_stable_ids = pj_libelle_by_stable_id(export_template)
+
+ export_template.pjs.filter(&:enabled?).each do |pj|
+ if pj.template_string.empty?
+ libelle = libelle_by_stable_ids[pj.stable_id]
+ export_template.errors.add(libelle, I18n.t(:blank, scope: 'errors.messages'))
+ end
+ end
+ end
+
+ def validate_different_templates(export_template)
+ templates = [export_template.export_pdf, *export_template.pjs]
+ .filter(&:enabled?)
+ .map(&:template_string)
+
+ return if templates.uniq.size == templates.size
+
+ export_template.errors.add(:base, :different_templates)
+ end
+
+ def pj_libelle_by_stable_id(export_template)
+ export_template.procedure.exportables_pieces_jointes
+ .pluck(:stable_id, :libelle).to_h
+ end
+end
diff --git a/app/validators/expression_reguliere_validator.rb b/app/validators/expression_reguliere_validator.rb
index 08a44fdc4..52cb153f5 100644
--- a/app/validators/expression_reguliere_validator.rb
+++ b/app/validators/expression_reguliere_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExpressionReguliereValidator < ActiveModel::Validator
TIMEOUT = 1.second.freeze
diff --git a/app/validators/geo_json_validator.rb b/app/validators/geo_json_validator.rb
index ed55e8326..9fdd9be98 100644
--- a/app/validators/geo_json_validator.rb
+++ b/app/validators/geo_json_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GeoJSONValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if options[:allow_nil] == false && value.nil?
diff --git a/app/validators/iban_validator.rb b/app/validators/iban_validator.rb
index 5c343005e..377d74347 100644
--- a/app/validators/iban_validator.rb
+++ b/app/validators/iban_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'iban-tools'
class IbanValidator < ActiveModel::Validator
diff --git a/app/validators/jwt_token_validator.rb b/app/validators/jwt_token_validator.rb
index 031a9d1ab..9dcfc8b66 100644
--- a/app/validators/jwt_token_validator.rb
+++ b/app/validators/jwt_token_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class JwtTokenValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
begin
diff --git a/app/validators/mon_avis_embed_validator.rb b/app/validators/mon_avis_embed_validator.rb
index 276552163..b01ac2238 100644
--- a/app/validators/mon_avis_embed_validator.rb
+++ b/app/validators/mon_avis_embed_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# We need to ensure the embed code is not any random string in order to avoid injections
class MonAvisEmbedValidator < ActiveModel::Validator
class MonAvisEmbedError < StandardError; end
diff --git a/app/validators/password_complexity_validator.rb b/app/validators/password_complexity_validator.rb
index a915a8575..504578985 100644
--- a/app/validators/password_complexity_validator.rb
+++ b/app/validators/password_complexity_validator.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class PasswordComplexityValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- if value.present? && ZxcvbnService.new(value).score < PASSWORD_COMPLEXITY_FOR_ADMIN
+ if value.present? && ZxcvbnService.complexity(value) < PASSWORD_COMPLEXITY_FOR_ADMIN
record.errors.add(attribute, :not_strong)
end
end
diff --git a/app/validators/siret_format_validator.rb b/app/validators/siret_format_validator.rb
index 964dd9e4a..eaf415711 100644
--- a/app/validators/siret_format_validator.rb
+++ b/app/validators/siret_format_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SiretFormatValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if !format_is_valid(value)
diff --git a/app/validators/strict_email_validator.rb b/app/validators/strict_email_validator.rb
index 7549aa23d..01a126ffc 100644
--- a/app/validators/strict_email_validator.rb
+++ b/app/validators/strict_email_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StrictEmailValidator < ActiveModel::EachValidator
# default devise email is : /\A[^@\s]+@[^@\s]+\z/
# saying that it's quite permissive
diff --git a/app/validators/tags_validator.rb b/app/validators/tags_validator.rb
index 7d981936e..d3cdf2c0e 100644
--- a/app/validators/tags_validator.rb
+++ b/app/validators/tags_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TagsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
procedure = record.procedure
diff --git a/app/validators/types_de_champ/condition_validator.rb b/app/validators/types_de_champ/condition_validator.rb
new file mode 100644
index 000000000..2de95838c
--- /dev/null
+++ b/app/validators/types_de_champ/condition_validator.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class TypesDeChamp::ConditionValidator < ActiveModel::EachValidator
+ # condition are valid when
+ # tdc.condition.left is present in upper tdcs
+ # in case of types_de_champ_private, we should include types_de_champ_publics too
+ def validate_each(procedure, collection, tdcs)
+ return if tdcs.empty?
+
+ tdcs = tdcs_with_children(procedure, tdcs)
+ tdcs.each_with_index do |tdc, tdc_index|
+ next unless tdc.condition?
+
+ upper_tdcs = []
+ if collection == :draft_types_de_champ_private # in case of private tdc validation, we must include public tdcs
+ upper_tdcs += tdcs_with_children(procedure, procedure.draft_types_de_champ_public)
+ end
+ upper_tdcs += tdcs.take(tdc_index) # we take all upper_tdcs of current tdcs
+
+ errors = tdc.condition.errors(upper_tdcs)
+ next if errors.blank?
+
+ procedure.errors.add(
+ collection,
+ procedure.errors.generate_message(collection, :invalid_condition, { value: tdc.libelle }),
+ type_de_champ: tdc
+ )
+ end
+ end
+
+ # find children in repetitions
+ def tdcs_with_children(procedure, tdcs)
+ tdcs.to_a
+ .flat_map { _1.repetition? ? procedure.draft_revision.children_of(_1) : _1 }
+ end
+end
diff --git a/app/validators/types_de_champ/expression_reguliere_validator.rb b/app/validators/types_de_champ/expression_reguliere_validator.rb
new file mode 100644
index 000000000..775379bf6
--- /dev/null
+++ b/app/validators/types_de_champ/expression_reguliere_validator.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class TypesDeChamp::ExpressionReguliereValidator < ActiveModel::EachValidator
+ def validate_each(procedure, attribute, types_de_champ)
+ types_de_champ.to_a
+ .flat_map { _1.repetition? ? procedure.draft_revision.children_of(_1) : _1 }
+ .each do |tdc|
+ if tdc.expression_reguliere? && tdc.invalid_regexp?
+ procedure.errors.add(
+ attribute,
+ procedure.errors.generate_message(attribute, :expression_reguliere_invalid, { value: tdc.libelle }),
+ type_de_champ: tdc
+ )
+ end
+ end
+ end
+end
diff --git a/app/validators/types_de_champ/header_section_consistency_validator.rb b/app/validators/types_de_champ/header_section_consistency_validator.rb
new file mode 100644
index 000000000..58c69d0f6
--- /dev/null
+++ b/app/validators/types_de_champ/header_section_consistency_validator.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class TypesDeChamp::HeaderSectionConsistencyValidator < ActiveModel::EachValidator
+ def validate_each(procedure, attribute, types_de_champ)
+ public_tdcs = types_de_champ.to_a
+
+ root_tdcs_errors = errors_for_header_sections_order(procedure, attribute, public_tdcs)
+ repetition_tdcs_errors = public_tdcs
+ .filter_map { _1.repetition? ? procedure.draft_revision.children_of(_1) : nil }
+ .map { errors_for_header_sections_order(procedure, attribute, _1) }
+
+ repetition_tdcs_errors + root_tdcs_errors
+ end
+
+ private
+
+ def errors_for_header_sections_order(procedure, attribute, types_de_champ)
+ types_de_champ
+ .map.with_index
+ .filter_map { |tdc, i| tdc.header_section? ? [tdc, i] : nil }
+ .map { |tdc, i| [tdc, tdc.check_coherent_header_level(types_de_champ.take(i))] }
+ .filter { |_tdc, errors| errors.present? }
+ .each do |tdc, message|
+ procedure.errors.add(
+ attribute,
+ procedure.errors.generate_message(attribute, :inconsistent_header_section, { value: tdc.libelle, custom_message: message }),
+ type_de_champ: tdc
+ )
+ end
+ end
+end
diff --git a/app/validators/types_de_champ/no_empty_block_validator.rb b/app/validators/types_de_champ/no_empty_block_validator.rb
index e1ea17739..6cd39a2e4 100644
--- a/app/validators/types_de_champ/no_empty_block_validator.rb
+++ b/app/validators/types_de_champ/no_empty_block_validator.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class TypesDeChamp::NoEmptyBlockValidator < ActiveModel::EachValidator
def validate_each(procedure, attribute, types_de_champ)
- types_de_champ.filter(&:block?).each do |repetition|
+ types_de_champ.filter(&:repetition?).each do |repetition|
validate_block_not_empty(procedure, attribute, repetition)
end
end
@@ -11,7 +13,8 @@ class TypesDeChamp::NoEmptyBlockValidator < ActiveModel::EachValidator
if procedure.draft_revision.children_of(parent).empty?
procedure.errors.add(
attribute,
- procedure.errors.generate_message(attribute, :empty_repetition, { value: parent.libelle })
+ procedure.errors.generate_message(attribute, :empty_repetition, { value: parent.libelle }),
+ type_de_champ: parent
)
end
end
diff --git a/app/validators/types_de_champ/no_empty_drop_down_validator.rb b/app/validators/types_de_champ/no_empty_drop_down_validator.rb
index bd8fa21dd..d1028bce3 100644
--- a/app/validators/types_de_champ/no_empty_drop_down_validator.rb
+++ b/app/validators/types_de_champ/no_empty_drop_down_validator.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
class TypesDeChamp::NoEmptyDropDownValidator < ActiveModel::EachValidator
def validate_each(procedure, attribute, types_de_champ)
- types_de_champ.filter(&:drop_down_list?).each do |drop_down|
+ types_de_champ.filter(&:any_drop_down_list?).each do |drop_down|
validate_drop_down_not_empty(procedure, attribute, drop_down)
end
end
@@ -8,10 +10,11 @@ class TypesDeChamp::NoEmptyDropDownValidator < ActiveModel::EachValidator
private
def validate_drop_down_not_empty(procedure, attribute, drop_down)
- if drop_down.drop_down_list_enabled_non_empty_options.empty?
+ if drop_down.drop_down_options.empty?
procedure.errors.add(
attribute,
- procedure.errors.generate_message(attribute, :empty_drop_down, { value: drop_down.libelle })
+ procedure.errors.generate_message(attribute, :empty_drop_down, { value: drop_down.libelle }),
+ type_de_champ: drop_down
)
end
end
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index fb2f4df02..0eed00103 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'active_model'
require 'active_support/i18n'
require 'public_suffix'
diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb
index 49ba357dd..8e95058a4 100644
--- a/app/views/active_storage/blobs/_blob.html.erb
+++ b/app/views/active_storage/blobs/_blob.html.erb
@@ -1,14 +1,19 @@
- attachment--<%= blob.filename.extension %>">
- <% if blob.representable? %>
- <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
- <% end %>
-
-
- <% if caption = blob.try(:caption) %>
- <%= caption %>
+<% if blob.representable? %>
+ <% representation = blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
+ <% if representation.image&.attached? || representation.key.present? %>
+
+ <%= image_tag representation %>
<% else %>
- <%= blob.filename %>
- <%= number_to_human_size blob.byte_size %>
+
+ <%= image_tag blob %>
<% end %>
-
-
+
+
+ <% if caption = blob.try(:caption) %>
+ <%= caption %>
+ <% else %>
+ <%= blob.filename %>
+ <%= number_to_human_size blob.byte_size %>
+ <% end %>
+
+<% end %>
diff --git a/app/views/administrateurs/_autosave_notice.html.haml b/app/views/administrateurs/_autosave_notice.html.haml
deleted file mode 100644
index 989e3970d..000000000
--- a/app/views/administrateurs/_autosave_notice.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- success = local_assigns.fetch(:success, true)
-#autosave-notice.fr-badge.fr-badge--sm{ class: class_names("fr-badge--success" => success, "fr-badge--error" => !success) }= success ? t(".form_saved") : t(".form_error")
diff --git a/app/views/administrateurs/_breadcrumbs.html.haml b/app/views/administrateurs/_breadcrumbs.html.haml
index 95c4295ac..6c64f233e 100644
--- a/app/views/administrateurs/_breadcrumbs.html.haml
+++ b/app/views/administrateurs/_breadcrumbs.html.haml
@@ -28,6 +28,11 @@
- elsif @procedure.locked?
= link_to commencer_url(@procedure.path), commencer_url(@procedure.path), class: "fr-link"
.flex.fr-mt-1w
+
+ - if @procedure.api_entreprise_token_expired_or_expires_soon?
+ %span.fr-badge.fr-badge--error.fr-mr-1w
+ = t('to_modify', scope: [:layouts, :breadcrumb])
+
%span.fr-badge.fr-badge--success.fr-mr-1w
= t('published', scope: [:layouts, :breadcrumb])
= t('since', scope: [:layouts, :breadcrumb], number: @procedure.id, date: l(@procedure.published_at.to_date))
@@ -35,7 +40,6 @@
- else
%p.fr-mb-1w
= t('more_info_on_test', scope: [:layouts, :breadcrumb])
- = link_to t('go_to_FAQ', scope: [:layouts, :breadcrumb]), t("url_FAQ", scope: [:layouts, :breadcrumb]), title: new_tab_suffix(t('go_to_FAQ', scope: [:layouts, :breadcrumb])), **external_link_attributes
.flex
%span.fr-badge.fr-badge--new.fr-mr-1w
= t('draft', scope: [:layouts, :breadcrumb])
diff --git a/app/views/administrateurs/_main_navigation.html.haml b/app/views/administrateurs/_main_navigation.html.haml
index 4c6010d04..f775ff34a 100644
--- a/app/views/administrateurs/_main_navigation.html.haml
+++ b/app/views/administrateurs/_main_navigation.html.haml
@@ -1,4 +1,4 @@
-%nav#header-navigation.fr-nav{ role: 'navigation', 'aria-label': 'Menu principal administrateur' }
+#header-navigation.fr-nav
%ul.fr-nav__list
%li.fr-nav__item= link_to 'Mes démarches', admin_procedures_path, class:'fr-nav__link', 'aria-current': current_page?(controller: 'administrateurs/procedures', action: :index) ? 'true' : nil
- if Rails.application.config.ds_zonage_enabled
diff --git a/app/views/administrateurs/activate/new.html.haml b/app/views/administrateurs/activate/new.html.haml
index b826ddffe..0dca181ed 100644
--- a/app/views/administrateurs/activate/new.html.haml
+++ b/app/views/administrateurs/activate/new.html.haml
@@ -18,9 +18,9 @@
.fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field,
- opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }})
+ opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }, aria: {describedby: 'password_hint'}})
#password_complexity
= render PasswordComplexityComponent.new
- = f.submit t('.continue'), id: 'submit-password', class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }
+ = f.submit t('.continue'), id: 'submit-password', disabled: :disabled, class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }
diff --git a/app/views/administrateurs/api_tokens/edit.html.haml b/app/views/administrateurs/api_tokens/edit.html.haml
index 6efa775f5..1d4635e43 100644
--- a/app/views/administrateurs/api_tokens/edit.html.haml
+++ b/app/views/administrateurs/api_tokens/edit.html.haml
@@ -6,41 +6,76 @@
["Jeton d’API : #{@api_token.name}"]] }
.fr-container.fr-mt-2w
- %h1 Modification du jeton d'API « #{@api_token.name} »
- = form_with url: admin_api_token_path(@api_token), method: :patch, html: { class: 'fr-mt-2w' } do |f|
- .fr-input-group
- = f.label :name, class: 'fr-label' do
- = t('name', scope: [:administrateurs, :api_tokens, :nom])
- %span.fr-hint-text= t('name-hint', scope: [:administrateurs, :api_tokens, :nom])
- = f.text_field :name,
- class: 'fr-input width-33',
- autocomplete: 'off',
- autocapitalize: 'off',
- autocorrect: 'off',
- spellcheck: false,
- required: true,
- value: @api_token.name
+ %turbo-frame#tokenUpdate
+ %h1 Modification du jeton d'API « #{@api_token.name} »
- .fr-input-group.fr-mb-4w{
- class: class_names('fr-input-group--error': @invalid_network) }
- = f.label :name, class: 'fr-label' do
- = @api_token.eternal? ? "Entrez au moins 1 réseau autorisé" : "Entrez les adresses ip autorisées"
- %span.fr-hint-text adresses réseaux séparées par des espaces. ex: 176.31.79.200 192.168.33.0/24 2001:41d0:304:400::52f/128
- = f.text_field :networks,
- class: class_names('fr-input': true, 'fr-input--error': @invalid_network),
- autocomplete: 'off',
- autocapitalize: 'off',
- autocorrect: 'off',
- spellcheck: false,
- required: @api_token.eternal?,
- value: @api_token.authorized_networks_for_ui.gsub(/,/, ' ')
+ = form_with url: admin_api_token_path(@api_token), method: :patch, html: { class: 'fr-mt-2w' } do |f|
+ .fr-input-group
+ = f.label :name, class: 'fr-label' do
+ = t('name', scope: [:administrateurs, :api_tokens, :nom])
+ %span.fr-hint-text= t('name-hint', scope: [:administrateurs, :api_tokens, :nom])
+ .flex
+ = f.text_field :name,
+ class: 'fr-input width-33',
+ autocomplete: 'off',
+ autocapitalize: 'off',
+ autocorrect: 'off',
+ spellcheck: false,
+ required: true,
+ value: @api_token.name
- - if @invalid_network
- %p.fr-error-text vous devez entrer des adresses ipv4 ou ipv6 valides
+ %button.fr-btn.fr-btn--secondary.fr-ml-1w Renommer
- %ul.fr-btns-group.fr-btns-group--inline
- %li
- = f.button 'Modifier', type: :submit, class: "fr-btn fr-btn--primary"
- %li
- = link_to 'Revenir', profil_path, class: "fr-btn fr-btn--secondary"
+ = form_with url: admin_api_token_path(@api_token), method: :patch, html: { class: 'fr-mt-2w' } do |f|
+ .fr-input-group.fr-mb-4w{
+ class: class_names('fr-input-group--error': @invalid_network_message.present?) }
+ = f.label :name, class: 'fr-label' do
+ = @api_token.eternal? ? "Entrez au moins 1 réseau autorisé" : "Entrez les adresses ip autorisées"
+ %span.fr-hint-text adresses réseaux séparées par des espaces. ex: 176.31.79.200 192.168.33.0/24 2001:41d0:304:400::52f/128
+ .flex
+ = f.text_field :networks,
+ class: class_names('fr-input': true, 'fr-input--error': @invalid_network_message.present?),
+ autocomplete: 'off',
+ autocapitalize: 'off',
+ autocorrect: 'off',
+ spellcheck: false,
+ value: @api_token.authorized_networks_for_ui.gsub(/,/, ' ')
+
+ %button.fr-btn.fr-btn--secondary.fr-ml-1w Modifier
+
+ - if @invalid_network_message.present?
+ %p.fr-error-text= @invalid_network_message
+
+ = form_with url: admin_api_token_path(@api_token), method: :patch, html: { class: 'fr-mt-2w' } do |f|
+ .fr-mb-4w
+ - if @api_token.full_access?
+ %p Votre jeton d'API a accès à toutes vos démarches.
+ = hidden_field_tag :procedure_to_add, '[]'
+ %button.fr-btn.fr-btn--secondary.fr-btn--sm Restreindre l'accès à certaines les démarches
+ - else
+ .fr-select-group
+ %label.fr-label{ for: 'procedure_to_add' } Ajouter des démarches autorisées
+ .flex
+ = f.select :value,
+ options_for_select(@libelle_id_procedures),
+ { include_blank: true },
+ { class: 'fr-select width-33',
+ name: 'procedure_to_add'}
+
+ %button.fr-btn.fr-btn--secondary.fr-ml-1w Ajouter
+
+ %ul.fr-mb-4w
+ - @api_token.procedures.each do |procedure|
+ %li{ id: dom_id(procedure, :authorized) }
+ = procedure.libelle
+ = button_to 'Supprimer',
+ remove_procedure_admin_api_token_path(@api_token, procedure_id: procedure.id),
+ class: 'fr-btn fr-btn--tertiary-no-outline fr-btn--sm fr-btn--icon-left fr-icon-delete-line',
+ form_class: 'inline',
+ method: :delete,
+ form: { data: { turbo: 'true' } }
+
+ %ul.fr-btns-group.fr-btns-group--inline
+ %li
+ = link_to 'Revenir', profil_path, class: "fr-btn fr-btn--secondary"
diff --git a/app/views/administrateurs/archives/index.html.haml b/app/views/administrateurs/archives/index.html.haml
index b50059d6c..86cc6465e 100644
--- a/app/views/administrateurs/archives/index.html.haml
+++ b/app/views/administrateurs/archives/index.html.haml
@@ -4,11 +4,12 @@
['Export et Archives']] }
-.container
- %h1.mb-2
+.container.flex
+ %h1.mb-2.mr-2
Archives
-# index not renderable as administrateur flagged as manager, so render it anyway
- = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_admin_procedure_exports_path))
+ = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_admin_procedure_exports_path), show_export_template_tab: false)
+.container
= render Dossiers::ExportLinkComponent.new(procedure: @procedure, exports: @exports, export_url: method(:download_admin_procedure_exports_path))
= render partial: "shared/archives/notice"
diff --git a/app/views/administrateurs/attestation_template_v2s/_fixed_footer.html.haml b/app/views/administrateurs/attestation_template_v2s/_fixed_footer.html.haml
new file mode 100644
index 000000000..e66ef2949
--- /dev/null
+++ b/app/views/administrateurs/attestation_template_v2s/_fixed_footer.html.haml
@@ -0,0 +1,9 @@
+.fr-container
+ .fr-grid-row.fr-grid-row--middle.fr-pb-3v
+ .fr-col-12.fr-col-md-4
+ = link_to admin_procedure_path(id: procedure), class: 'fr-link' do
+ %span.fr-icon-arrow-left-line.fr-icon--sm
+ Revenir à l’écran de gestion
+
+ .fr-col-12.fr-col-md-8.text-right
+ %span#autosave-notice
diff --git a/app/views/administrateurs/attestation_template_v2s/_sticky_header.html.haml b/app/views/administrateurs/attestation_template_v2s/_sticky_header.html.haml
new file mode 100644
index 000000000..d209f5d4d
--- /dev/null
+++ b/app/views/administrateurs/attestation_template_v2s/_sticky_header.html.haml
@@ -0,0 +1,21 @@
+.sticky-header.sticky-header-warning
+ .fr-container
+ %p.flex.justify-between.align-center.fr-text-default--warning
+ %span
+ = dsfr_icon("fr-icon-warning-fill fr-mr-1v")
+ - if @procedure.attestation_templates.many?
+ Les modifications effectuées ne seront appliquées qu’à la prochaine publication.
+ - else
+ L’attestation ne sera délivrée qu’après sa publication.
+
+ %span.no-wrap
+ - if @procedure.attestation_templates.many?
+ = link_to reset_admin_procedure_attestation_template_v2_path(@procedure), class: "fr-btn fr-btn--secondary fr-ml-2w", method: :post do
+ Réinitialiser les modifications
+
+ %button.fr-btn.fr-ml-2w{ form: "attestation-template", name: field_name(:attestation_template, :state), value: "published",
+ data: { 'disable-with': "Publication en cours…", controller: 'autosave-submit' } }
+ - if @procedure.attestation_templates.many?
+ Publier les modifications
+ - else
+ Publier
diff --git a/app/views/administrateurs/attestation_template_v2s/edit.html.haml b/app/views/administrateurs/attestation_template_v2s/edit.html.haml
index 308dca5e4..cd97011c6 100644
--- a/app/views/administrateurs/attestation_template_v2s/edit.html.haml
+++ b/app/views/administrateurs/attestation_template_v2s/edit.html.haml
@@ -4,7 +4,8 @@
['Attestation']] }
= render NestedForms::FormOwnerComponent.new
-= form_for @attestation_template, url: admin_procedure_attestation_template_v2_path(@procedure), html: { multipart: true },
+= form_for @attestation_template, url: admin_procedure_attestation_template_v2_path(@procedure),
+ html: { multipart: true , id: "attestation-template" },
data: { turbo: 'true',
controller: 'autosubmit attestation',
autosubmit_debounce_delay_value: 1000,
@@ -12,18 +13,16 @@
attestation_logo_attachment_free_label_value: AttestationTemplate.human_attribute_name(:logo) } do |f|
#attestation-edit.fr-container.fr-my-4w{ data: { controller: 'tiptap', tiptap_insert_after_tag_value: ' ' } }
- .fr-mb-6w
- = render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur d’attestation", heading_level: 'h3') do |c|
- - c.with_body do
- Cette page permet la mise en forme de l’attestation avec un nouvel éditeur plus flexible
- tout en respectant la charte de l’état. Essayez-la et donnez-nous votre avis
- en nous envoyant un email à #{mail_to(Current.contact_email, subject: "Feedback attestation v2")}.
- %br
- %strong Les attestations délivrées suivent encore l’ancien format :
- l’activation des attestations basées sur ce format sera bientôt disponible.
- %br
+ - if @procedure.attestation_templates.v1.published.any?
+ .fr-mb-6w
+ = render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur d’attestation", heading_level: 'h3') do |c|
+ - c.with_body do
+ %p Cette page présente un nouvel éditeur d'attestations, plus flexible et conforme à la charte de l'État.
+ %p
+ %strong Pour modifier l’attestation existante (actuellement délivrée aux usagers),
+ = link_to("cliquez ici", edit_admin_procedure_attestation_template_path(@procedure)) + "."
+ %p Pour générer une attestation à la charte de l‘État, créez-la ci-dessous puis publiez-la: elle remplacera alors l’attestation actuelle.
- = link_to("Suivez ce lien pour revenir aux attestations actuellement délivrées", edit_admin_procedure_attestation_template_path(@procedure))
.fr-grid-row.fr-grid-row--gutters
.fr-col-12.fr-col-lg-7
@@ -34,13 +33,23 @@
L’attestation est émise au moment où un dossier est accepté, elle est jointe à l’email d’accusé d’acceptation.
Elle est également disponible au téléchargement depuis l’espace personnel de l’usager.
+ .fr-fieldset__element
+ = render Dsfr::CalloutComponent.new(title: "Activation de la délivrance de l’attestation", theme: :neutral) do |c|
+ - c.with_html_body do
+ .fr-toggle.fr-toggle--label-left
+ = f.check_box :activated, class: "fr-toggle__input", id: dom_id(@attestation_template, :activated)
+ %label.fr-toggle__label{ for: dom_id(@attestation_template, :activated),
+ data: { fr_checked_label: "Activée", fr_unchecked_label: "Désactivée" } }
+ Activer cette option permet la délivrance automatique de l’attestation dès l’acceptation du dossier.
+ Désactiver cette option arrête immédiatement l’émission de nouvelles attestations.
+
.fr-fieldset__element
%h2.fr-h4 En-tête
.fr-fieldset__element
- .fr-toggle
- = f.check_box :official_layout, class: "fr-toggle-input", id: dom_id(@attestation_template, :official_layout), data: { "attestation-target": "layoutToggle"}
- %label.fr-toggle__label{ for: dom_id(@attestation_template, :official_layout), data: { fr_checked_label: "Activé", fr_unchecked_label: "Désactivé" } }
+ .fr-toggle.fr-toggle--label-left
+ = f.check_box :official_layout, class: "fr-toggle__input", id: dom_id(@attestation_template, :official_layout), data: { "attestation-target": "layoutToggle"}
+ %label.fr-toggle__label{ for: dom_id(@attestation_template, :official_layout), data: { fr_checked_label: "Oui", fr_unchecked_label: "Non" } }
Je souhaite générer une attestation à la charte de l’état (logo avec Marianne)
.fr-fieldset__element{ class: class_names("hidden" => !@attestation_template.official_layout?), data: { "attestation-target": 'logoMarianneLabelFieldset'} }
@@ -48,7 +57,7 @@
- c.with_hint { "Exemple: Ministère de la Mer. 5 lignes maximum" }
.fr-fieldset__element{ data: { attestation_target: 'logoAttachmentFieldset' } }
- %label.fr-label{ for: field_id(@attestation_template, :logo) }
+ %label.fr-label{ for: dom_id(@attestation_template, :logo) }
- if @attestation_template.official_layout?
= AttestationTemplate.human_attribute_name(:logo_additional)
- else
@@ -77,10 +86,10 @@
%button.fr-btn.fr-btn--secondary.fr-btn--sm{ type: 'button', title: label, class: icon == :hidden ? "hidden" : "fr-icon-#{icon}", data: { action: 'click->tiptap#menuButton', tiptap_target: 'button', tiptap_action: action } }
= label
- #editor.editor{ data: { tiptap_target: 'editor' }, aria: { describedby: dom_id(f.object, "json-body-messages")} }
+ #editor.tiptap-editor{ data: { tiptap_target: 'editor' }, aria: { describedby: "attestation-template-json-body-messages"} }
= f.hidden_field :tiptap_body, data: { tiptap_target: 'input' }
- .fr-error-text{ id: dom_id(f.object, "json-body-messages"), class: class_names("hidden" => !f.object.errors.include?(:json_body)) }
+ .fr-error-text{ id: "attestation-template-json-body-messages", class: class_names("hidden" => !f.object.errors.include?(:json_body)) }
- if f.object.errors.include?(:json_body)
= render partial: "shared/errors_list", locals: { object: f.object, attribute: :json_body }
@@ -96,7 +105,7 @@
%h2.fr-h4 Pied de page
.fr-fieldset__element
- %label.fr-label{ for: field_id(@attestation_template, :signature) } Tampon ou signature
+ %label.fr-label{ for: dom_id(@attestation_template, :signature) } Tampon ou signature
%span.fr-hint-text
Dimensions conseillées : au minimum 500px de largeur ou de hauteur.
@@ -108,30 +117,21 @@
- c.with_hint { "Exemple: 20 avenue de Ségur, 75007 Paris" }
#preview-column.fr-col-12.fr-col-lg-5.fr-background-alt--blue-france
- .sticky--top.fr-px-1w
+ .sticky--top.fr-px-1w{ data: { controller: "sticky-top" } }
.flex.justify-between.align-center
%h2.fr-h4 Aperçu
%p= link_to 'Prévisualiser en taille réelle', admin_procedure_attestation_template_v2_path(@procedure, format: :pdf), class: 'fr-link', target: '_blank', rel: 'noopener'
%iframe.attestation-preview{ title: "Aperçu", src: admin_procedure_attestation_template_v2_path(@procedure, format: :pdf), data: { attestation_target: 'preview' } }
%p.fr-hint-text
L’aperçu est mis à jour automatiquement après chaque modification.
- Pour générer un aperçu fidèle avec tous les champs et les dates, créez-vous un dossier et acceptez-le : l’aperçu l’utilisera.
+ Pour générer un aperçu fidèle avec champs et dates,
+ = link_to("créez-vous un dossier", new_dossier_path(procedure_id: @procedure, brouillon: true), **external_link_attributes)
+ et acceptez-le : l’aperçu l’utilisera.
+
+ - if @procedure.feature_enabled?(:attestation_v2) && @attestation_template.draft?
+ - content_for(:sticky_header) do
+ = render partial: "sticky_header"
.padded-fixed-footer
- .fixed-footer
- .fr-container
- .fr-grid-row
- .fr-col-12.fr-col-md-7
- %ul.fr-btns-group.fr-btns-group--inline-md
- %li
- = link_to admin_procedure_path(id: @procedure), class: 'fr-btn fr-btn--secondary' do
- %span.fr-icon-arrow-go-back-line.fr-icon--sm.fr-mr-1v
- Revenir à la démarche
-
- .fr-col-12.fr-col-md-5
- -# .fr-toggle
- -# = f.check_box :activated, class: "fr-toggle-input", disabled: true, id: dom_id(@attestation_template, :activated)
- -# %label.fr-toggle__label{ for: dom_id(@attestation_template, :activated), data: { fr_checked_label: "Attestation activée", fr_unchecked_label: "Attestation désactivée" } }
- .text-right
- %span#autosave-notice
- %p.fr-hint-text L’activation de cette attestation sera bientôt disponible.
+ .fixed-footer#fixed_footer
+ = render partial: "fixed_footer", locals: { procedure: @procedure }
diff --git a/app/views/administrateurs/attestation_template_v2s/show.html.haml b/app/views/administrateurs/attestation_template_v2s/show.html.haml
index 06c87773f..e27889003 100644
--- a/app/views/administrateurs/attestation_template_v2s/show.html.haml
+++ b/app/views/administrateurs/attestation_template_v2s/show.html.haml
@@ -30,12 +30,13 @@
- if @attestation_template.label_direction.present?
= simple_format @attestation_template.label_direction, class: "direction"
+ - if @attestation_template.footer.present?
+ %footer
+ = simple_format @attestation_template.footer
+
.main
= sanitize(@body, attributes: %w[class style], tags: Rails.configuration.action_view.sanitized_allowed_tags + %w[header])
- if @attestation_template.signature.present?
.signature
= image_tag(@attestation_template.signature_url)
-
- - if @attestation_template.footer.present?
- = simple_format @attestation_template.footer, class: "footer"
diff --git a/app/views/administrateurs/attestation_template_v2s/update.turbo_stream.haml b/app/views/administrateurs/attestation_template_v2s/update.turbo_stream.haml
index 67ef140a1..ce033ac46 100644
--- a/app/views/administrateurs/attestation_template_v2s/update.turbo_stream.haml
+++ b/app/views/administrateurs/attestation_template_v2s/update.turbo_stream.haml
@@ -1,5 +1,8 @@
+- if @attestation_template.draft?
+ = turbo_stream.update "sticky-header", render(partial: "sticky_header")
+
= turbo_stream.show 'autosave-notice'
-= turbo_stream.replace 'autosave-notice', render(partial: 'administrateurs/autosave_notice', locals: { success: !@attestation_template.changed? })
+= turbo_stream.replace('autosave-notice', render(AutosaveNoticeComponent.new(success: !@attestation_template.changed?, label_scope: :attestation)))
= turbo_stream.hide 'autosave-notice', delay: 15000
- if @attestation_template.logo_blob&.previously_new_record?
@@ -10,7 +13,7 @@
= turbo_stream.update dom_id(@attestation_template, :signature_attachment) do
= render(Attachment::EditComponent.new(attached_file: @attestation_template.signature, direct_upload: false))
-- body_id = dom_id(@attestation_template, "json-body-messages")
+- body_id = "attestation-template-json-body-messages"
- if @attestation_template.errors.include?(:json_body)
= turbo_stream.update body_id do
= render partial: "shared/errors_list", locals: { object: @attestation_template, attribute: :json_body }
diff --git a/app/views/administrateurs/attestation_templates/show.pdf.prawn b/app/views/administrateurs/attestation_templates/show.pdf.prawn
index 3b2b89f2e..c71482690 100644
--- a/app/views/administrateurs/attestation_templates/show.pdf.prawn
+++ b/app/views/administrateurs/attestation_templates/show.pdf.prawn
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'prawn/measurement_extensions'
#----- A4 page size
diff --git a/app/views/administrateurs/conditions/_update.turbo_stream.haml b/app/views/administrateurs/conditions/_update.turbo_stream.haml
index 3fba14ec8..028fb8c0e 100644
--- a/app/views/administrateurs/conditions/_update.turbo_stream.haml
+++ b/app/views/administrateurs/conditions/_update.turbo_stream.haml
@@ -4,7 +4,7 @@
['Configuration des champs']],
preview: @procedure.draft_revision.valid? })
-= turbo_stream.replace 'errors-summary', render(TypesDeChampEditor::ErrorsSummary.new(revision: @procedure.draft_revision))
+= turbo_stream.replace 'errors-summary', render(Procedure::ErrorsSummary.new(procedure: @procedure, validation_context: @tdc.public? ? :types_de_champ_public_editor : :types_de_champ_private_editor))
- rendered = render @condition_component
diff --git a/app/views/administrateurs/dossier_submitted_messages/edit.html.haml b/app/views/administrateurs/dossier_submitted_messages/edit.html.haml
index 08ce1b7cd..dc289c291 100644
--- a/app/views/administrateurs/dossier_submitted_messages/edit.html.haml
+++ b/app/views/administrateurs/dossier_submitted_messages/edit.html.haml
@@ -3,31 +3,31 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
- ['Fin de dépot']] }
+ ['Fin de dépôt']] }
-.procedure-form
- .procedure-form__columns.container
- = form_for @dossier_submitted_message,
- url: url_for({ controller: 'administrateurs/dossier_submitted_messages', action: :update, id: @procedure.id }),
- html: { class: 'form procedure-form__column--form fr-background-alt--blue-france' } do |f|
+= form_for @dossier_submitted_message,
+ url: url_for({ controller: 'administrateurs/dossier_submitted_messages', action: :update, id: @procedure.id }),
+ html: { class: 'form' } do |f|
- %h1.page-title
- Fin du dépot
- %p.notice
- L'utilisateur se verra afficher ce message une fois le dossier envoyé
+ .procedure-form
+ .procedure-form__columns.container
+ .procedure-form__column--form.fr-background-alt--blue-france.fr-pt-5w
+ %h1.fr-h2
+ Fin de dépôt
+ %p.notice
+ L'utilisateur se verra afficher ce message une fois le dossier envoyé
- = render partial: 'administrateurs/dossier_submitted_messages/informations', locals: { f: f }
+ = render partial: 'administrateurs/dossier_submitted_messages/informations', locals: { f: f }
- .procedure-form__actions
- .actions-left
- = f.submit 'Enregistrer', class: 'fr-btn send'
- .procedure-form__column--preview
- %h3
- .procedure-form__preview-title
- Aperçu
- .notice
- Cet aperçu est mis à jour après chaque sauvegarde.
+ .procedure-form__column--preview
+ %h3
+ .procedure-form__preview-title
+ Aperçu
+ .notice
+ Cet aperçu est mis à jour après chaque sauvegarde.
- .procedure-preview
- = render partial: 'users/dossiers/merci', locals: { procedure: @procedure, dossier: nil}
+ .procedure-preview
+ = render partial: 'users/dossiers/merci', locals: { procedure: @procedure, dossier: nil}
+
+ = render Procedure::FixedFooterComponent.new(procedure: @procedure, form: f)
diff --git a/app/views/administrateurs/experts_procedures/index.html.haml b/app/views/administrateurs/experts_procedures/index.html.haml
index 886aa061f..865d1cb67 100644
--- a/app/views/administrateurs/experts_procedures/index.html.haml
+++ b/app/views/administrateurs/experts_procedures/index.html.haml
@@ -1,109 +1,120 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
- ['Liste des experts']] }
+ ['Avis externes']] }
-.container
- %h1.page-title.mt-2= t('.titles.main', libelle: @procedure.libelle)
+.fr-container
+ %h1.fr-h2
+ Avis externes
- .container.groupe-instructeur
+ = render Dsfr::CalloutComponent.new(title: nil) do |c|
+ - c.with_body do
+ Pendant l'instruction d'un dossier, les instructeurs peuvent demander leur avis à un ou plusieurs experts.
+ %p
+ = link_to('Comment gérer les avis externes', t('.experts_doc.url'),
+ title: t('.experts_doc.title'),
+ **external_link_attributes)
- .card
- .card-title= t('.titles.allow_invite_experts')
- %p= t('.descriptions.allow_invite_experts')
+ %ul.fr-toggle__list
+ %li
= form_for @procedure,
method: :put,
url: allow_expert_review_admin_procedure_path(@procedure),
- html: { class: 'form procedure-form__column--form no-background' } do |f|
- %label.toggle-switch{ data: { controller: 'autosubmit' } }
- = f.check_box :allow_expert_review, class: 'toggle-switch-checkbox'
- %span.toggle-switch-control.round
- %span.toggle-switch-label.on
- %span.toggle-switch-label.off
+ data: { controller: 'autosubmit', turbo: 'true' } do |f|
+
+ = render Dsfr::ToggleComponent.new(form: f,
+ target: :allow_expert_review,
+ title: t('.titles.allow_invite_experts'),
+ hint: t('.descriptions.allow_invite_experts'),
+ disabled: false,
+ extra_class_names: 'fr-toggle--border-bottom')
- if @procedure.allow_expert_review?
- .card
- .card-title= t('.titles.manage_procedure_experts')
- %p= t('.descriptions.manage_procedure_experts')
+ %li
+ = form_for @procedure,
+ method: :put,
+ url: allow_expert_messaging_admin_procedure_path(@procedure),
+ data: { controller: 'autosubmit', turbo: 'true' } do |f|
+
+ = render Dsfr::ToggleComponent.new(form: f,
+ target: :allow_expert_messaging,
+ title: t('.titles.allow_expert_messaging'),
+ hint: t('.descriptions.allow_expert_messaging'),
+ disabled: false,
+ extra_class_names: 'fr-toggle--border-bottom')
+
+ %li
= form_for @procedure,
method: :put,
url: experts_require_administrateur_invitation_admin_procedure_path(@procedure),
- html: { class: 'form procedure-form__column--form no-background' } do |f|
- %label.toggle-switch{ data: { controller: 'autosubmit' } }
- = f.check_box :experts_require_administrateur_invitation, class: 'toggle-switch-checkbox'
- %span.toggle-switch-control.round
- %span.toggle-switch-label.on
- %span.toggle-switch-label.off
+ data: { controller: 'autosubmit', turbo: 'true' } do |f|
- .card
- .card-title= t('.titles.allow_expert_messaging')
- %p= t('.descriptions.allow_expert_messaging')
- = form_for @procedure,
- method: :put,
- url: allow_expert_messaging_admin_procedure_path(@procedure),
- html: { class: 'form procedure-form__column--form no-background' } do |f|
- %label.toggle-switch{ data: { controller: 'autosubmit' } }
- = f.check_box :allow_expert_messaging, class: 'toggle-switch-checkbox'
- %span.toggle-switch-control.round
- %span.toggle-switch-label.on
- %span.toggle-switch-label.off
+ = render Dsfr::ToggleComponent.new(form: f,
+ target: :experts_require_administrateur_invitation,
+ title: t('.titles.manage_procedure_experts'),
+ hint: t('.descriptions.manage_procedure_experts'),
+ disabled: false)
- - if @procedure.experts_require_administrateur_invitation?
- .card
- .card-title Affecter des experts à la démarche
- = form_for :experts_procedure,
- url: admin_procedure_experts_path(@procedure),
- html: { class: 'form' } do |f|
- .instructeur-wrapper
- %p Pendant l'instruction d’un dossier, les instructeurs peuvent demander leur avis à un ou plusieurs experts.
- %p#experts-emails Entrez les adresses email des experts que vous souhaitez affecter à cette démarche
- = hidden_field_tag :emails, nil
- = react_component("ComboMultiple",
- options: [],
- selected: [], disabled: [],
- group: '.instructeur-wrapper',
- name: 'emails',
- label: 'Emails',
- describedby: 'experts-emails',
- acceptNewValues: true)
+ - if @procedure.experts_require_administrateur_invitation?
+ .card
+ = render Procedure::InvitationWithTypoComponent.new(maybe_typos: @maybe_typos, url: admin_procedure_experts_path(@procedure), title: "Avant d'ajouter l'email à la liste d'expert prédéfinie, veuillez confirmer" )
+ = form_for :experts_procedure,
+ url: admin_procedure_experts_path(@procedure),
+ html: { class: 'form' } do |f|
+
+ .instructeur-wrapper
+ %p#experts-emails Entrez les adresses emails des experts que vous souhaitez ajouter à la liste prédéfinie
+ %react-fragment
+ = render ReactComponent.new "ComboBox/MultiComboBox",
+ id: 'emails',
+ name: 'emails[]',
+ allows_custom_value: true,
+ 'aria-label': 'Emails',
+ 'aria-describedby': 'experts-emails'
+
+ = f.submit 'Ajouter à la liste', class: 'fr-btn'
- = f.submit 'Affecter à la démarche', class: 'button primary send'
- if @experts_procedure.present?
- %table.table.mt-2
- %thead
- %tr
- %th Liste des experts
- %th Nombre d’avis
- - if @procedure.experts_require_administrateur_invitation
- %th Notifier des décisions sur les dossiers
- %tbody
- - @experts_procedure.each do |expert_procedure|
+ .fr-table.fr-table--no-caption.fr-table--layout-fixed.fr-mt-3w
+ %table
+ %thead
%tr
- %td
- = dsfr_icon('fr-icon-user-fill')
- = expert_procedure.expert.email
- %td.text-center
- = expert_procedure.avis.count
+ %th Liste des experts
+ %th Nombre d’avis
- if @procedure.experts_require_administrateur_invitation
+ %th Notifier des décisions sur les dossiers
+ - if @procedure.experts_require_administrateur_invitation
+ %th Action
+ %tbody
+ - @experts_procedure.each do |expert_procedure|
+ %tr
+ %td
+ = dsfr_icon('fr-icon-user-fill')
+ = expert_procedure.expert.email
%td.text-center
- = form_for expert_procedure,
- url: admin_procedure_expert_path(id: expert_procedure),
- method: :put,
- data: { turbo: true },
- html: { class: 'form procedure-form__column--form no-background' } do |f|
- %label.toggle-switch{ data: { controller: 'autosubmit' } }
- = f.check_box :allow_decision_access, class: 'toggle-switch-checkbox'
- %span.toggle-switch-control.round
- %span.toggle-switch-label.on
- %span.toggle-switch-label.off
- - if @procedure.experts_require_administrateur_invitation
- %td.actions= button_to 'retirer',
- admin_procedure_expert_path(id: expert_procedure, procedure: @procedure),
- method: :delete,
- data: { confirm: "Êtes-vous sûr de vouloir révoquer l'expert « #{expert_procedure.expert.email} » de la démarche #{expert_procedure.procedure.libelle} ? Les instructeurs ne pourront plus lui demander d’avis" },
- class: 'button'
+ = expert_procedure.avis.count
+ - if @procedure.experts_require_administrateur_invitation
+ %td.text-center
+ = form_for expert_procedure,
+ url: admin_procedure_expert_path(id: expert_procedure),
+ method: :put,
+ data: { turbo: true },
+ html: { class: 'form procedure-form__column--form no-background' } do |f|
+ %label.toggle-switch{ data: { controller: 'autosubmit' } }
+ = f.check_box :allow_decision_access, class: 'toggle-switch-checkbox'
+ %span.toggle-switch-control.round
+ %span.toggle-switch-label.on
+ %span.toggle-switch-label.off
+ - if @procedure.experts_require_administrateur_invitation
+ %td.actions= button_to 'retirer',
+ admin_procedure_expert_path(id: expert_procedure, procedure: @procedure),
+ method: :delete,
+ data: { confirm: "Êtes-vous sûr de vouloir révoquer l'expert « #{expert_procedure.expert.email} » de la démarche #{expert_procedure.procedure.libelle} ? Les instructeurs ne pourront plus lui demander d’avis" },
+ class: 'fr-btn fr-btn--secondary'
- else
.blank-tab
%h2.empty-text Aucun expert invité pour le moment.
%p.empty-text-details Les instructeurs de cette démarche n’ont pas encore fait appel aux experts.
+
+= render Procedure::FixedFooterComponent.new(procedure: @procedure)
diff --git a/app/views/administrateurs/groupe_instructeurs/_custom_routing_modal.html.haml b/app/views/administrateurs/groupe_instructeurs/_custom_routing_modal.html.haml
new file mode 100644
index 000000000..4f14875e0
--- /dev/null
+++ b/app/views/administrateurs/groupe_instructeurs/_custom_routing_modal.html.haml
@@ -0,0 +1,20 @@
+%dialog{ aria: { labelledby: "fr-modal-title-modal-1" }, role: "dialog", id: "routing-mode-modal", class: "fr-modal fr-modal--opened" }
+ .fr-container.fr-container--fluid.fr-container-md
+ .fr-grid-row.fr-grid-row--center
+ .fr-col-12.fr-col-md-8.fr-col-lg-6
+ .fr-modal__body
+ .fr-modal__header
+ %button.fr-btn.fr-btn--close{ title: "Fermer la fenêtre modale", aria: { controls: "routing-mode-modal" } } Fermer
+ .fr-modal__content
+ %h1#fr-modal-title-modal-1.fr-modal__title
+ %span.fr-icon-arrow-right-line.fr-icon--lg
+ Configuration manuelle du routage
+ .fr-alert.fr-alert--success
+ %h2.fr-alert__title
+ Deux groupes par défaut ont été créés
+ %p
+ Vous devez maintenant les renommer et leur attribuer des règles de routage à partir du ou des champs « routables » de votre formulaire, soit des champs de type :
+ %ul
+ - TypeDeChamp.humanized_conditionable_types_by_category.each do |category|
+ %li
+ = category.join(', ')
diff --git a/app/views/administrateurs/groupe_instructeurs/_import_export.html.haml b/app/views/administrateurs/groupe_instructeurs/_import_export.html.haml
deleted file mode 100644
index 1286c969b..000000000
--- a/app/views/administrateurs/groupe_instructeurs/_import_export.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-- key = procedure.groupe_instructeurs.one? ? 'instructeurs' : 'groupes'
-%section.fr-accordion.fr-mb-3w
- %h3.fr-accordion__title
- %button.fr-accordion__btn{ "aria-controls" => "accordion-106", "aria-expanded" => "false" }
- = t(".csv_import.#{key}.title")
- .fr-collapse#accordion-106
- - csv_max_size = Administrateurs::GroupeInstructeursController::CSV_MAX_SIZE
- - if procedure.publiee_or_close?
- %p.notice
- = t(".csv_import.#{key}.notice_1_html", csv_max_size: number_to_human_size(csv_max_size))
- %p.notice
- = t(".csv_import.#{key}.notice_2")
- = form_tag import_admin_procedure_groupe_instructeurs_path(procedure), method: :post, multipart: true, class: "mt-4 form flex justify-between align-center" do
- = file_field_tag :csv_file, required: true, accept: 'text/csv', size: "1"
- = submit_tag t('.csv_import.import_file'), class: 'fr-btn fr-btn--secondary', data: { disable_with: "Envoi...", confirm: t('.csv_import.import_file_alert') }
- - else
- %p.mt-4.form.font-weight-bold.mb-2.text-lg
- = t(".csv_import.#{key}.title")
- %p.notice
- = t('.csv_import.import_file_procedure_not_published')
- - if procedure.groupe_instructeurs.many?
- .flex.justify-between.align-center.mt-4
- %div
- = t(".existing_groupe", count: procedure.groupe_instructeurs.count)
- = button_to "Exporter au format CSV",
- export_groupe_instructeurs_admin_procedure_groupe_instructeurs_path(procedure, format: :csv),
- method: :get,
- class: 'fr-btn fr-btn--secondary'
diff --git a/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml b/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml
index 3e8dd7643..968c6b938 100644
--- a/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml
+++ b/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml
@@ -1,42 +1,46 @@
.card
- .card-title Affectation des instructeurs
+ = render Procedure::InvitationWithTypoComponent.new(maybe_typos: @maybe_typos, url: add_instructeur_admin_procedure_groupe_instructeur_path(@procedure, groupe_instructeur.id), title: "Avant d'ajouter l'email, veuillez confirmer" )
+ .card-title= t('.instructeur_assignation')
= form_for :instructeur, url: { action: :add_instructeur, id: groupe_instructeur.id }, html: { class: 'form' } do |f|
.instructeur-wrapper
- if !procedure.routing_enabled?
- %p Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
+ %p= t('.instructeur_emails')
+ %p.fr-hint-text= t('.copy_paste_hint')
- if disabled_as_super_admin
= f.select :emails, available_instructeur_emails, {}, disabled: disabled_as_super_admin, id: 'instructeur_emails'
- else
- = hidden_field_tag :emails, nil
- = react_component("ComboMultiple",
- options: available_instructeur_emails, selected: [], disabled: [],
- group: '.instructeur-wrapper',
- id: 'instructeur_emails',
- name: 'emails',
- label: 'Emails',
- acceptNewValues: true)
+ %react-fragment
+ = render ReactComponent.new 'ComboBox/MultiComboBox', items: available_instructeur_emails, id: 'instructeur_emails', name: 'emails[]', allows_custom_value: true, 'aria-label': 'Emails'
- = f.submit 'Affecter', class: 'fr-btn', disabled: disabled_as_super_admin
+ = f.submit t('.assign'), class: 'fr-btn fr-btn--tertiary', disabled: disabled_as_super_admin
- %table.fr-table.fr-mt-2w.width-100
+ %hr.fr-mt-4w
+
+ .flex.justify-between.align-baseline
+ .card-title= t('.assigned_instructeur', count: instructeurs.count)
+ = button_to export_groupe_instructeurs_admin_procedure_groupe_instructeurs_path(procedure, format: :csv), method: :get, class: 'fr-btn fr-btn--tertiary fr-btn--icon-left fr-icon-download-line' do
+ Exporter la liste (.CSV)
+
+ %table.fr-table.fr-table--bordered.width-100
%thead
%tr
- %th{ colspan: 2 }= t('.assigned_instructeur', count: instructeurs.count)
+ %th= t('.title')
+ %th.text-right= t('.actions')
%tbody
- instructeurs.each do |instructeur|
%tr
%td
- = dsfr_icon('fr-icon-user-fill')
+ = dsfr_icon('fr-icon-user-line')
#{instructeur.email}
- confirmation_message = procedure.routing_enabled? ? "Êtes-vous sûr de vouloir retirer l’instructeur « #{instructeur.email} » du groupe « #{groupe_instructeur.label} » ?" : "Êtes-vous sûr de vouloir retirer l’instructeur « #{instructeur.email} » de la démarche ?"
- %td.actions= button_to 'Retirer',
+ %td.actions= button_to t('.remove'),
{ action: :remove_instructeur, id: groupe_instructeur.id },
{ method: :delete,
data: { confirm: confirmation_message },
params: { instructeur: { id: instructeur.id }},
- class: 'fr-btn fr-btn--secondary' }
+ class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-subtract-line' }
= paginate instructeurs, views_prefix: 'shared'
diff --git a/app/views/administrateurs/groupe_instructeurs/_simple_routing_modal.html.haml b/app/views/administrateurs/groupe_instructeurs/_simple_routing_modal.html.haml
new file mode 100644
index 000000000..ff817b750
--- /dev/null
+++ b/app/views/administrateurs/groupe_instructeurs/_simple_routing_modal.html.haml
@@ -0,0 +1,14 @@
+%dialog{ aria: { labelledby: "fr-modal-title-modal-1" }, role: "dialog", id: "routing-mode-modal", class: "fr-modal fr-modal--opened" }
+ .fr-container.fr-container--fluid.fr-container-md
+ .fr-grid-row.fr-grid-row--center
+ .fr-col-12.fr-col-md-8.fr-col-lg-6
+ .fr-modal__body
+ .fr-modal__header
+ %button.fr-btn.fr-btn--close{ title: "Fermer la fenêtre modale", aria: { controls: "routing-mode-modal" } } Fermer
+ .fr-modal__content
+ %h1#fr-modal-title-modal-1.fr-modal__title
+ %span.fr-icon-arrow-right-line.fr-icon--lg
+ Configuration automatique du routage
+ .fr-alert.fr-alert--success
+ %h2.fr-alert__title
+ Les groupes instructeurs ont été créés à partir du champ « #{procedure.routing_champs.first} »
diff --git a/app/views/administrateurs/groupe_instructeurs/simple_routing.html.haml b/app/views/administrateurs/groupe_instructeurs/simple_routing.html.haml
index 854ef9c18..de4deae6a 100644
--- a/app/views/administrateurs/groupe_instructeurs/simple_routing.html.haml
+++ b/app/views/administrateurs/groupe_instructeurs/simple_routing.html.haml
@@ -2,26 +2,53 @@
locals: { steps: [[t('.procedures'), admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['Groupes', admin_procedure_groupe_instructeurs_path(@procedure)],
- ['Routage à partir d’un champ']] }
+ ['Configuration automatique du routage']] }
-= render Procedure::InstructeursMenuComponent.new(procedure: @procedure) do
- - content_for(:title, 'Routage')
- %h1 Routage à partir d’un champ
- = form_for :create_simple_routing,
- method: :post,
- data: { controller: 'enable-submit-if-checked' },
- url: create_simple_routing_admin_procedure_groupe_instructeurs_path(@procedure) do |f|
+.container
+ .fr-grid-row
+ .fr-col.fr-col-12.fr-col-md-3
+ .fr-container
+ %ul.fr-btns-group.fr-btns-group--inline-md.fr-ml-0
+ %li
+ = link_to options_admin_procedure_groupe_instructeurs_path, class: 'fr-link fr-icon-arrow-left-line fr-link--icon-left fr-mb-2w fr-mr-2w' do
+ Revenir aux options
- %div{ data: { 'action': "click->enable-submit-if-checked#click" } }
- .notice
- Sélectionner le champ à partir duquel créer des groupes d’instructeurs
- - buttons_content = @procedure.active_revision.routable_types_de_champ.map { |tdc| { label: tdc.libelle, value: tdc.stable_id } }
- = render Dsfr::RadioButtonListComponent.new(form: f,
- target: :stable_id,
- buttons: buttons_content)
+ .fr-col
+ - content_for(:title, 'Routage')
+ %h1 Configuration du routage
+ %h2 Configuration automatique
+ .fr-alert.fr-alert--info.fr-mb-3w{ aria: { hidden: true } }
+ %p
+ Vous trouverez ci-dessous une liste de champs de votre formulaire à partir desquels configurer le routage de façon automatique. Les groupes d’instructeurs seront créés à partir des valeurs possibles du champ.
+ Seuls les champs suivants sont ouverts à ce mode de configuration :
+ %ul
+ - TypeDeChamp.humanized_simple_routable_types_by_category.each do |category|
+ %li
+ = category.join(', ')
- %ul.fr-btns-group.fr-btns-group--inline-sm
- %li
- = link_to 'Retour', options_admin_procedure_groupe_instructeurs_path(@procedure, state: :choix), class: 'fr-btn fr-btn--secondary'
- %li
- %button.fr-btn{ disabled: true, data: { disable_with: 'Création des groupes…', 'enable-submit-if-checked-target': 'submit' } } Créer les groupes
+ %p
+ Si besoin, vous pourrez ensuite affiner votre configuration de façon manuelle, également à partir des champs suivants :
+
+ %ul
+ - TypeDeChamp.humanized_custom_routable_types_by_category.each do |category|
+ %li
+ = category.join(', ')
+
+ = form_for :create_simple_routing,
+ method: :post,
+ data: { controller: 'enable-submit-if-checked' },
+ url: create_simple_routing_admin_procedure_groupe_instructeurs_path(@procedure) do |f|
+
+ .card.fr-pb-0{ data: { 'action': "click->enable-submit-if-checked#click" } }
+ .notice
+ Sélectionner le champ à partir duquel créer des groupes d’instructeurs
+ - buttons_content = @procedure.active_revision.simple_routable_types_de_champ.map { |tdc| { label: tdc.libelle, value: tdc.stable_id, hint: "[#{I18n.t(tdc.type_champ, scope: 'activerecord.attributes.type_de_champ.type_champs')}]", tooltip: tdc.drop_down_options.join(", ")} }
+ = render Dsfr::RadioButtonListComponent.new(form: f,
+ target: :stable_id,
+ buttons: buttons_content)
+
+ %ul.fr-btns-group.fr-btns-group--inline-sm
+ %li
+ = link_to 'Annuler', options_admin_procedure_groupe_instructeurs_path(@procedure, state: :choix), class: 'fr-btn fr-btn--secondary'
+ %li
+ %button.fr-btn{ disabled: true, data: { disable_with: 'Création des groupes…', 'enable-submit-if-checked-target': 'submit' } } Créer les groupes
diff --git a/app/views/administrateurs/ineligibilite_rules/_update.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/_update.turbo_stream.haml
new file mode 100644
index 000000000..a0ace0eca
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/_update.turbo_stream.haml
@@ -0,0 +1,7 @@
+- rendered = render @ineligibilite_rules_component
+
+- if rendered.present?
+ = turbo_stream.replace dom_id(@procedure.draft_revision, :ineligibilite_rules) do
+ - rendered
+- else
+ = turbo_stream.remove dom_id(@procedure.draft_revision, :ineligibilite_rules)
diff --git a/app/views/administrateurs/ineligibilite_rules/add_row.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/add_row.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/add_row.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/change_targeted_champ.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/change_targeted_champ.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/change_targeted_champ.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/delete_row.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/delete_row.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/delete_row.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/destroy.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/destroy.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/destroy.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/edit.html.haml b/app/views/administrateurs/ineligibilite_rules/edit.html.haml
new file mode 100644
index 000000000..6eb91d12f
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/edit.html.haml
@@ -0,0 +1,28 @@
+= render partial: 'administrateurs/breadcrumbs',
+ locals: { steps: [['Démarches', admin_procedures_path],
+ [@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
+ ['Inéligibilité des dossiers']] }
+
+
+.fr-container
+ .fr-grid-row
+ .fr-col-12.fr-col-offset-md-2.fr-col-md-8
+ %h1.fr-h1 Inéligibilité des dossiers
+
+ = render Dsfr::AlertComponent.new(title: nil, size: :sm, state: :info, heading_level: 'h2', extra_class_names: 'fr-my-2w') do |c|
+ - c.with_body do
+ %p
+ Les dossiers répondant à vos conditions d’inéligibilité ne pourront pas être déposés. Plus d’informations sur l’inéligibilité des dossiers dans la
+ = link_to('doc', ELIGIBILITE_URL, title: "Document sur l’inéligibilité des dossiers", **external_link_attributes)
+
+ - if !@procedure.draft_revision.conditionable_types_de_champ.present?
+ %p.fr-mt-2w.fr-mb-2w
+ Pour configurer l’inéligibilité des dossiers, votre formulaire doit comporter au moins un champ supportant les conditions d’inéligibilité. Il vous faut donc ajouter au moins un des champs suivant à votre formulaire :
+ %ul
+ - Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.each do
+ %li= "« #{t(_1, scope: [:activerecord, :attributes, :type_de_champ, :type_champs])} »"
+ %p.fr-mt-2w
+ = link_to 'Ajouter un champ supportant les conditions d’inéligibilité', champs_admin_procedure_path(@procedure), class: 'fr-link fr-icon-arrow-right-line fr-link--icon-right'
+ = render Procedure::FixedFooterComponent.new(procedure: @procedure)
+ - else
+ = render Conditions::IneligibiliteRulesComponent.new(draft_revision: @procedure.draft_revision)
diff --git a/app/views/administrateurs/ineligibilite_rules/update.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/update.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/update.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/jeton_particulier/show.html.haml b/app/views/administrateurs/jeton_particulier/show.html.haml
index 7527313eb..f0ded6bdb 100644
--- a/app/views/administrateurs/jeton_particulier/show.html.haml
+++ b/app/views/administrateurs/jeton_particulier/show.html.haml
@@ -2,14 +2,13 @@
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
[Procedure.human_attribute_name(:jeton_api_particulier), admin_procedure_api_particulier_path(@procedure)],
- ['Jeton']] }
+ ['Jeton Particulier']] }
-.container
- %h1.page-title
- = t('.configure_token')
+.fr-container
+ %h1.fr-h2
+ Jeton Particulier
-.container
- %h1
+.fr-container
= form_with model: @procedure, url: admin_procedure_api_particulier_jeton_path, local: true do |f|
= render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: 'fr-mb-2w') do |c|
- c.with_body do
diff --git a/app/views/administrateurs/labels/_form.html.haml b/app/views/administrateurs/labels/_form.html.haml
new file mode 100644
index 000000000..6742b1d35
--- /dev/null
+++ b/app/views/administrateurs/labels/_form.html.haml
@@ -0,0 +1,15 @@
+= form_with model: label, url: [:admin, @procedure, @label], local: true do |f|
+ = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field, opts: { maxlength: Label::NAME_MAX_LENGTH})
+
+ %fieldset.fr-fieldset
+ %legend.fr-fieldset__legend.fr-fieldset__legend--regular
+ = t('activerecord.attributes.label.color')
+ = asterisk
+
+ - @colors_collection.each do |color|
+ .fr-fieldset__element.fr-fieldset__element--inline
+ .fr-radio-group
+ = f.radio_button :color, color, checked: (label.color == color)
+ = f.label :color, t("activerecord.attributes.label/color.#{color}"), value: color, class: "fr-label fr-tag fr-tag--sm fr-tag--#{Label.class_name(color)}"
+
+ = render Procedure::FixedFooterComponent.new(procedure: @procedure, form: f)
diff --git a/app/views/administrateurs/labels/edit.html.haml b/app/views/administrateurs/labels/edit.html.haml
new file mode 100644
index 000000000..8f3784ca4
--- /dev/null
+++ b/app/views/administrateurs/labels/edit.html.haml
@@ -0,0 +1,20 @@
+- content_for :title, "Modifier le label"
+
+= render partial: 'administrateurs/breadcrumbs',
+ locals: { steps: [['Démarches', admin_procedures_path],
+ [@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
+ ['gestion des labels', [:admin, @procedure, :labels]],
+ ['Modifier le label']] }
+
+
+.fr-container
+ .fr-mb-3w
+ = link_to "Liste de tous les labels",
+ [:admin, @procedure, :labels],
+ class: "fr-link fr-icon-arrow-left-line fr-link--icon-left"
+
+ %h1.fr-h2
+ Modifier le label
+
+ = render partial: 'form',
+ locals: { label: @label, procedure_id: @procedure.id }
diff --git a/app/views/administrateurs/labels/index.html.haml b/app/views/administrateurs/labels/index.html.haml
new file mode 100644
index 000000000..f0dc512f5
--- /dev/null
+++ b/app/views/administrateurs/labels/index.html.haml
@@ -0,0 +1,43 @@
+- content_for :title, "Labels"
+
+= render partial: 'administrateurs/breadcrumbs',
+ locals: { steps: [['Démarches', admin_procedures_path],
+ [@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
+ ['Labels']] }
+
+.fr-container
+ %h1.fr-h2 Labels
+
+ = link_to "Nouveau label",
+ [:new, :admin, @procedure, :label],
+ class: "fr-btn fr-btn--primary fr-btn--icon-left fr-icon-add-circle-line mb-3"
+
+ - if @procedure.labels.present?
+ .fr-table.fr-table--layout-fixed.fr-table--bordered
+ %table
+ %caption Liste des labels
+ %thead
+ %tr
+ %th{ scope: "col" }
+ Nom
+ %th.change{ scope: "col" }
+ Actions
+
+ %tbody
+ - @labels.each do |label|
+ %tr
+ %td
+ = tag_label(label.name, label.color)
+ %td.change
+
+ = link_to 'Modifier',
+ [:edit, :admin, @procedure, label],
+ class: 'fr-btn fr-btn--sm fr-btn--secondary fr-btn--icon-left fr-icon-pencil-line'
+
+ = link_to 'Supprimer',
+ [:admin, @procedure, label],
+ method: :delete,
+ data: { confirm: "Confirmez vous la suppression de #{label.name}" },
+ class: 'fr-btn fr-btn--sm fr-btn--secondary fr-btn--icon-left fr-icon-delete-line fr-ml-1w'
+
+= render Procedure::FixedFooterComponent.new(procedure: @procedure)
diff --git a/app/views/administrateurs/labels/new.html.haml b/app/views/administrateurs/labels/new.html.haml
new file mode 100644
index 000000000..fc4713cd3
--- /dev/null
+++ b/app/views/administrateurs/labels/new.html.haml
@@ -0,0 +1,20 @@
+- content_for :title, "Nouveau label"
+
+= render partial: 'administrateurs/breadcrumbs',
+ locals: { steps: [['Démarches', admin_procedures_path],
+ [@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
+ ['gestion des labels', [:admin, @procedure, :labels]],
+ ['Nouveau label']] }
+
+
+.fr-container
+ .fr-mb-3w
+ = link_to "Liste de tous les labels",
+ [:admin, @procedure, :labels],
+ class: "fr-link fr-icon-arrow-left-line fr-link--icon-left"
+
+ %h1.fr-h2
+ Créer un nouveau label
+
+ = render partial: 'form',
+ locals: { label: @label, procedure_id: @procedure.id }
diff --git a/app/views/administrateurs/mail_templates/index.html.haml b/app/views/administrateurs/mail_templates/index.html.haml
index 07345a1fa..9fd744726 100644
--- a/app/views/administrateurs/mail_templates/index.html.haml
+++ b/app/views/administrateurs/mail_templates/index.html.haml
@@ -3,8 +3,19 @@
["#{@procedure.libelle.truncate_words(10)}", admin_procedure_path(@procedure)],
["Configuration des emails"]] }
-.container
- .fr-grid-row.fr-grid-row--gutters.fr-py-5w
+.fr-container
+ .fr-grid-row.fr-grid-row--gutters
+ .fr-col-12
+ %h1.fr-h2 Configuration des emails
+ - if @procedure.accuse_lecture?
+ = render Dsfr::AlertComponent.new(state: :info, size: :sm) do |c|
+ - c.with_body do
+ %p
+ L'accusé de lecture est activé sur cette démarche. Dans ce contexte, les emails « d’acceptation », « de rejet » et de « classement sans suite », ne sont pas modifiables afin de s'assurer que la décision finale reste masquée pour l'usager.
+
- @mail_templates.each do |mail_template|
.fr-col-md-6.fr-col-12
= render Procedure::EmailTemplateCardComponent.new(email_template: mail_template)
+
+
+= render Procedure::FixedFooterComponent.new(procedure: @procedure)
diff --git a/app/views/administrateurs/procedure_administrateurs/index.html.haml b/app/views/administrateurs/procedure_administrateurs/index.html.haml
index 8ad25f4e1..3e9bf37aa 100644
--- a/app/views/administrateurs/procedure_administrateurs/index.html.haml
+++ b/app/views/administrateurs/procedure_administrateurs/index.html.haml
@@ -4,10 +4,14 @@
['Administrateurs']], preview: false }
.fr-container
- %h1 Gérer les administrateurs de « #{@procedure.libelle} »
+ %h1.fr-h2 Administrateurs
- .fr-table.fr-table--bordered
+ .fr-mb-4w
+ = render 'add_admin_form', procedure: @procedure, disabled_as_super_admin: administrateur_as_manager?
+
+ .fr-table.fr-table--bordered.fr-table--layout-fixed
%table
+ %caption Liste des administrateurs
%thead
%th= 'Adresse email'
%th= 'Enregistré le'
@@ -16,5 +20,4 @@
%tbody#administrateurs
= render(Procedure::ProcedureAdministrateurs::AdministrateurComponent.with_collection(@procedure.administrateurs.order('users.email'), procedure: @procedure))
- .fr-mt-4w
- = render 'add_admin_form', procedure: @procedure, disabled_as_super_admin: administrateur_as_manager?
+= render Procedure::FixedFooterComponent.new(procedure: @procedure)
diff --git a/app/views/administrateurs/procedures/_api_entreprise_token_expiration_alert.html.haml b/app/views/administrateurs/procedures/_api_entreprise_token_expiration_alert.html.haml
new file mode 100644
index 000000000..d37e14142
--- /dev/null
+++ b/app/views/administrateurs/procedures/_api_entreprise_token_expiration_alert.html.haml
@@ -0,0 +1,14 @@
+- if procedure.api_entreprise_token_expires_at.present?
+ - if procedure.api_entreprise_token_expires_at < Time.zone.now
+ = render Dsfr::AlertComponent.new(state: :error, size: :sm, extra_class_names: 'fr-mb-2w') do |c|
+ - c.with_body do
+ %p
+ Votre jeton API Entreprise est expiré.
+ Merci de le renouveler.
+ - elsif procedure.api_entreprise_token_expired_or_expires_soon?
+ = render Dsfr::AlertComponent.new(state: :warning, size: :sm, extra_class_names: 'fr-mb-2w') do |c|
+ - c.with_body do
+ %p
+ Votre jeton API Entreprise expirera le
+ = procedure.api_entreprise_token_expires_at.strftime('%d/%m/%Y à %H:%M.')
+ Merci de le renouveler avant cette date.
diff --git a/app/views/administrateurs/procedures/_champs_summary.html.haml b/app/views/administrateurs/procedures/_champs_summary.html.haml
deleted file mode 100644
index 2db332926..000000000
--- a/app/views/administrateurs/procedures/_champs_summary.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-#summary{ class: @procedure.header_sections.present? ? 'fr-col-12 fr-col-md-3' : '' }
- - if @procedure.header_sections.present?
- %nav.fr-sidemenu.sticky.fr-hidden.fr-unhidden-md{ "aria-labelledby" => "fr-summary-title", role: "navigation" }
- %ul.fr-sidemenu__list
- - @procedure.header_sections.each do |header|
- %li.fr-sidemenu__item
- - level = header.type_de_champ.header_section_level_value.to_i
- - if level == 1
- %a.fr-sidemenu__link{ href: "##{dom_id(header, :type_de_champ_editor)}" }= header.libelle
- - else
- %a.fr-sidemenu__link{ class: level >= 3 ? 'custom-link-grey': '', href: "##{dom_id(header, :type_de_champ_editor)}" }= "-- #{header.libelle}"
diff --git a/app/views/administrateurs/procedures/_detail.html.haml b/app/views/administrateurs/procedures/_detail.html.haml
index b2e89c4b7..99b8b1410 100644
--- a/app/views/administrateurs/procedures/_detail.html.haml
+++ b/app/views/administrateurs/procedures/_detail.html.haml
@@ -5,15 +5,23 @@
- params = show_detail ? {} : { show_detail: true }
= button_to detail_admin_procedure_path(procedure["id"]), method: :post, params:, title:, class: [icon, "fr-icon--sm fr-mr-1w fr-mb-1w fr-text-action-high--blue-france fr-btn fr-btn--tertiary-no-outline" ] do
= title
-
%td
- if procedure.template
- %p.fr-badge.fr-badge--info.fr-badge--sm= "Modèle DS"
- %br
+ %p.fr-badge.fr-badge--info.fr-badge--sm= "Modèle"
+ %abbr{ title: APPLICATION_NAME }= acronymize(APPLICATION_NAME)
= procedure.libelle
%td= procedure.id
%td= procedure.estimated_dossiers_count
- %td= procedure.administrateurs.count
+ %td
+ - if procedure.respond_to?(:parsed_latest_zone_labels)
+ - procedure.parsed_latest_zone_labels.uniq.each do |zone_label|
+ %span.mb-2= zone_label
+ .mb-2
+ - else
+ - procedure.zones.uniq.each do |zone|
+ %span= zone.current_label
+ .mb-1
+
%td= t procedure.aasm_state, scope: 'activerecord.attributes.procedure.aasm_state'
%td= l(procedure.published_at, format: :message_date_without_time) if procedure.published_at
%td
@@ -21,16 +29,10 @@
= link_to('Cloner', admin_procedure_clone_path(procedure.id, from_new_from_existing: true), 'data-method' => :put, class: 'fr-btn fr-btn--tertiary fr-btn--sm')
-
-
- if show_detail
%tr.procedure{ id: "procedure_detail_#{procedure.id}" }
- %td.fr-highlight--beige-gris-galet{ colspan: '8' }
+ %td.fr-highlight--green-emeraude{ colspan: '8' }
.fr-container
- .fr-grid-row
- .fr-col-6
- - procedure.zones.uniq.each do |zone|
- = zone.label_at(procedure.published_or_created_at)
- .fr-col-6
- - procedure.administrateurs.uniq.each do |admin|
- = admin.email
+ .fr-col-6
+ - procedure.administrateurs.uniq.each do |admin|
+ = admin.email
diff --git a/app/views/administrateurs/procedures/_informations.html.haml b/app/views/administrateurs/procedures/_informations.html.haml
index 4fc3224b8..3fbb1b39e 100644
--- a/app/views/administrateurs/procedures/_informations.html.haml
+++ b/app/views/administrateurs/procedures/_informations.html.haml
@@ -16,7 +16,7 @@
.fr-fieldset__element
.fr-input-group
- = f.label :logo, 'Ajouter un logo de la démarche', class: 'fr-label'
+ = f.label :logo, 'Ajouter un logo de la démarche', class: 'fr-label', for: dom_id(@procedure, :logo)
= render Attachment::EditComponent.new(attached_file: @procedure.logo, view_as: :link)
.fr-fieldset__element
.fr-input-group
@@ -55,7 +55,7 @@
.fr-fieldset__element
.fr-input-group
- = f.label :deliberation, 'Cadre juridique - texte à importer', class: 'fr-label'
+ = f.label :deliberation, 'Cadre juridique - texte à importer', class: 'fr-label', for: dom_id(@procedure, :deliberation)
= render Attachment::EditComponent.new(attached_file: @procedure.deliberation, view_as: :download)
%fieldset.fr-fieldset
@@ -80,7 +80,7 @@
.fr-fieldset__element
.fr-input-group
- = f.label :notice, 'Notice explicative de la démarche', class: 'fr-label'
+ = f.label :notice, 'Notice explicative de la démarche', class: 'fr-label', for: dom_id(@procedure, :notice)
%p.fr-hint-text
Une notice explicative est un document que vous avez élaboré, destiné à guider l’usager dans sa démarche. Le bouton pour télécharger cette notice apparaît en haut du formulaire pour l’usager.
%br
@@ -101,7 +101,7 @@
Ma démarche s’adresse à un particulier
%span.fr-hint-text En choisissant cette option, l’usager devra renseigner son nom et prénom avant d’accéder au formulaire
.fr-radio-rich__img
- %svg.fr-artwork{ aria_hidden: "true", viewBox: "0 0 80 80", width: "80px", height: "80px" }
+ %svg.fr-artwork{ "aria-hidden": "true", viewBox: "0 0 80 80", width: "80px", height: "80px" }
%use.fr-artwork-decorative{ href: image_path("pictograms/digital/avatar.svg#artwork-decorative") }
%use.fr-artwork-minor{ href: image_path("pictograms/digital/avatar.svg#artwork-minor") }
%use.fr-artwork-major{ href: image_path("pictograms/digital/avatar.svg#artwork-major") }
@@ -114,25 +114,26 @@
%span.fr-hint-text
En choisissant cette option, l’usager devra renseigner son n° SIRET.
Grâce à l’API Entreprise, les informations sur la personne morale (raison sociale, adresse du siège, etc.) seront automatiquement renseignées.
.fr-radio-rich__img
- %svg.fr-artwork{ aria_hidden: "true", viewBox: "0 0 80 80", width: "80px", height: "80px" }
+ %svg.fr-artwork{ "aria-hidden": "true", viewBox: "0 0 80 80", width: "80px", height: "80px" }
%use.fr-artwork-decorative{ href: image_path("pictograms/buildings/school.svg#artwork-decorative") }
%use.fr-artwork-minor{ href: image_path("pictograms/buildings/school.svg#artwork-minor") }
%use.fr-artwork-major{ href: image_path("pictograms/buildings/school.svg#artwork-major") }
.fr-fieldset__element
- = f.label :tags, 'Associez les tags à la démarche', class: 'fr-label'
- %p.fr-hint-text Les tags sont des mots ou des expressions que vous attribuez aux démarches pour décrire leur contenu et pour les retrouver. Les tags sont partagés avec la communauté, ce qui vous permet de voir les tags attribués aux démarches créées par les autres administrateurs.
- = hidden_field_tag 'procedure[tags]', JSON.generate(@procedure.tags)
- = react_component("ComboMultiple",
- id: "procedure_tags_combo",
- options: Procedure.tags,
- selected: @procedure.tags,
- disabled: [],
- label: 'Tags',
- group: '.procedure_tags_combo',
- name: 'tags',
- describedby: 'procedure-tags',
- acceptNewValues: true)
+ = f.label :tags, 'Associez des thématiques à la démarche', class: 'fr-label'
+ %p.fr-hint-text
+ Par des mots ou des expressions que vous attribuez aux démarches pour décrire leur contenu et pour les retrouver.
+ Les thèmes sont partagées avec la communauté, ce qui vous permet de voir les thèmes attribués aux démarches créées par les autres administrateurs.
+ %react-fragment
+ = render ReactComponent.new "ComboBox/MultiComboBox",
+ id: "procedure_tags_combo",
+ items: ProcedureTag.order(:name).pluck(:name),
+ selected_keys: @procedure.procedure_tags.pluck(:name),
+ name: 'procedure[procedure_tag_names][]',
+ value_separator: ',|;',
+ allows_custom_value: false,
+ 'aria-label': 'Tags',
+ 'aria-describedby': 'procedure-tags'
%details.procedure-form__options-details
%summary.procedure-form__options-summary
diff --git a/app/views/administrateurs/procedures/_monavis.html.haml b/app/views/administrateurs/procedures/_monavis.html.haml
index 499f22d0e..62ac9d711 100644
--- a/app/views/administrateurs/procedures/_monavis.html.haml
+++ b/app/views/administrateurs/procedures/_monavis.html.haml
@@ -14,5 +14,5 @@
Une fois en possession du code généré sur le site MonAvis, vous pouvez le coller dans le champ ci-dessous :
.fr-input-group
- = f.label :monavis_embed, "Mon avis", class: "fr-label"
+ = f.label :monavis_embed, "Code généré sur le site MonAvis", class: "fr-label"
= f.text_area :monavis_embed, rows: '6', placeholder: '
', class: 'fr-input'
diff --git a/app/views/administrateurs/procedures/_procedures_list.html.haml b/app/views/administrateurs/procedures/_procedures_list.html.haml
index ccd0e4afc..99eecd912 100644
--- a/app/views/administrateurs/procedures/_procedures_list.html.haml
+++ b/app/views/administrateurs/procedures/_procedures_list.html.haml
@@ -1,4 +1,4 @@
-.fr-h6
+%h2.fr-h6
= page_entries_info procedures
- procedures.each do |procedure|
@@ -10,7 +10,7 @@
= image_tag procedure.logo, alt: procedure.libelle, class: 'logo'
%div
- .card-title
+ %h3.card-title
= link_to procedure.libelle, admin_procedure_path(procedure)
= link_to commencer_url(procedure.path), commencer_url(procedure.path), class: 'fr-link fr-mb-1w'
@@ -45,20 +45,24 @@
%div
= dsfr_icon('fr-icon-team-fill')
- if procedure.routing_enabled?
- %span.fr-badge= procedure.groupe_instructeurs.count
+ %span.fr-badge= procedure.groupe_instructeurs_count
- else
- %span.fr-badge= procedure.instructeurs.count
+ %span.fr-badge= procedure.instructeurs_count
= dsfr_icon('fr-icon-file-text-fill fr-ml-1w')
%span.fr-badge= procedure.dossiers.state_not_brouillon.visible_by_administration.count
.text-right
%p.fr-mb-0.width-max-content N° #{number_with_html_delimiter(procedure.id)}
+
- if procedure.close? || procedure.depubliee?
%span.fr-badge.fr-badge--sm.fr-badge--warning
= t('closed', scope: [:layouts, :breadcrumb])
- elsif procedure.publiee?
+ - if procedure.api_entreprise_token_expired_or_expires_soon?
+ %span.fr-badge.fr-badge--sm.fr-badge--error
+ = t('to_modify', scope: [:layouts, :breadcrumb])
%span.fr-badge.fr-badge--sm.fr-badge--success
= t('published', scope: [:layouts, :breadcrumb])
@@ -98,7 +102,7 @@
.dropdown-description
%h4= t('administrateurs.dropdown_actions.to_close')
- - if procedure.can_be_deleted_by_administrateur? && !procedure.discarded?
+ - if procedure.can_be_deleted_by_administrateur? && !procedure.discarded? && !procedure.publiee?
- menu.with_item do
= link_to admin_procedure_path(procedure), role: 'menuitem', method: :delete, data: { confirm: "Voulez-vous vraiment supprimer la démarche ? \nToute suppression est définitive et s'appliquera aux éventuels autres administrateurs de cette démarche !" } do
= dsfr_icon('fr-icon-delete-line')
diff --git a/app/views/administrateurs/procedures/_publication_form.html.haml b/app/views/administrateurs/procedures/_publication_form.html.haml
index 94ff52d42..284206699 100644
--- a/app/views/administrateurs/procedures/_publication_form.html.haml
+++ b/app/views/administrateurs/procedures/_publication_form.html.haml
@@ -2,13 +2,13 @@
url: admin_procedure_publish_path(procedure_id: procedure.id),
method: :put,
html: { class: 'form' } do |f|
- = render Procedure::PublicationWarningComponent.new(procedure: procedure)
+ = render Procedure::ErrorsSummary.new(procedure: @procedure, validation_context: :publication)
.mt-2
- if procedure.draft_changed?
%p.mb-2= t('.draft_changed_procedure_alert')
= render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: 'fr-mb-2w') do |c|
- c.with_body do
- = render Procedure::RevisionChangesComponent.new changes: procedure.revision_changes, previous_revision: procedure.published_revision
+ = render Procedure::RevisionChangesComponent.new new_revision: procedure.draft_revision, previous_revision: procedure.published_revision
- if procedure.close?
= render partial: 'publication_form_inputs', locals: { procedure: procedure, closed_procedures: @closed_procedures, form: f }
- elsif @procedure.brouillon? && @procedure.missing_steps.empty?
@@ -16,7 +16,6 @@
- c.with_body do
%p
= t('.faq_test_alert')
- = link_to t('.faq_test_alert_link'), t('.faq_test_alert_link_url'), **external_link_attributes
= render partial: 'publication_form_inputs', locals: { procedure: procedure, closed_procedures: @closed_procedures, form: f }
= render Dsfr::CalloutComponent.new(title: t('.dpd_title'), heading_level: 'h2') do |c|
- c.with_body do
diff --git a/app/views/administrateurs/procedures/_unpublished_changes_sticky_header.html.haml b/app/views/administrateurs/procedures/_unpublished_changes_sticky_header.html.haml
new file mode 100644
index 000000000..af2ec32b4
--- /dev/null
+++ b/app/views/administrateurs/procedures/_unpublished_changes_sticky_header.html.haml
@@ -0,0 +1,10 @@
+.sticky-header.sticky-header-warning
+ .fr-container
+ %p.flex.justify-between.align-center.fr-text-default--warning
+ %span
+ = dsfr_icon("fr-icon-warning-fill fr-mr-1v")
+ = t('.intro_html').html_safe
+ %span.no-wrap
+ = link_to t('.see_changes'), admin_procedure_path(procedure), class: 'fr-btn fr-btn--secondary fr-mr-2w'
+ = link_to_if procedure.draft_revision.valid? && procedure.valid?(:publication), t('.publish_changes'), admin_procedure_publish_revision_path(procedure), class: 'fr-btn', method: :put, data: { disable_with: "Publication...", confirm: 'Êtes-vous sûr de vouloir publier les modifications ?' } do
+ %button.fr-btn{ disabled: "disabled" }= t('.publish_changes')
diff --git a/app/views/administrateurs/procedures/accuse_lecture.html.haml b/app/views/administrateurs/procedures/accuse_lecture.html.haml
index 13e998dec..90be126cc 100644
--- a/app/views/administrateurs/procedures/accuse_lecture.html.haml
+++ b/app/views/administrateurs/procedures/accuse_lecture.html.haml
@@ -6,7 +6,7 @@
.fr-container
.fr-grid-row
.fr-col-12.fr-col-offset-md-2.fr-col-md-8
- %h1.page-title
+ %h1.fr-h2
Accusé de lecture
= render Dsfr::CalloutComponent.new(title: nil) do |c|
@@ -29,15 +29,8 @@
= render Dsfr::ToggleComponent.new(form: f,
target: :accuse_lecture,
- title: "Accusé de lecture de la démarche",
+ title: "Accusé de lecture de la décision par l’usager",
hint: "L’accusé de lecture est à activer uniquement pour les démarches avec voies de recours car il complexifie l’accès à la décision finale pour les usagers",
opt: {"checked" => @procedure.accuse_lecture})
-.padded-fixed-footer
- .fixed-footer
- .fr-container
- .fr-grid-row
- .fr-col-12.fr-col-offset-md-2.fr-col-md-8
- %ul.fr-btns-group.fr-btns-group--inline-md
- %li
- = link_to 'Enregistrer et revenir à la page de suivi', admin_procedure_path(id: @procedure), class: 'fr-btn'
+= render Procedure::FixedFooterComponent.new(procedure: @procedure, extra_class_names: 'fr-col-offset-md-2 fr-col-md-8' )
diff --git a/app/views/administrateurs/procedures/all.html.haml b/app/views/administrateurs/procedures/all.html.haml
index 43069b757..224e59fc6 100644
--- a/app/views/administrateurs/procedures/all.html.haml
+++ b/app/views/administrateurs/procedures/all.html.haml
@@ -57,8 +57,8 @@
%th{ scope: 'col' }
%th{ scope: 'col' } Démarche
%th{ scope: 'col' } №
- %th{ scope: 'col' } Dossiers
- %th{ scope: 'col' } Administrateurs
+ %th{ scope: 'col' } Nombre de dossiers
+ %th{ scope: 'col' } Zones
%th{ scope: 'col' } Statut
%th{ scope: 'col' } Date
%th{ scope: 'col' } Action
diff --git a/app/views/administrateurs/procedures/annotations.html.haml b/app/views/administrateurs/procedures/annotations.html.haml
index 05d198cf6..a8eb33b08 100644
--- a/app/views/administrateurs/procedures/annotations.html.haml
+++ b/app/views/administrateurs/procedures/annotations.html.haml
@@ -1,21 +1,26 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_back_path(@procedure)],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
- ['Configuration des annotations privées']], preview: true }
+ ['Annotations privées']], preview: true }
.fr-container
- %h1 Configuration des annotations privées
+ .flex.justify-between.align-center.fr-mb-3w
+ %h1.fr-h2 Annotations privées
+ - if @procedure.revised?
+ = link_to "Voir l'historique des modifications des annotations", modifications_admin_procedure_path(@procedure), class: 'fr-link'
+
= render NestedForms::FormOwnerComponent.new
.fr-grid-row
+ = render TypesDeChampEditor::HeaderSectionsSummaryComponent.new(procedure: @procedure, is_private: true)
.fr-col
= render TypesDeChampEditor::EditorComponent.new(revision: @procedure.draft_revision, is_annotation: true)
.padded-fixed-footer
.fixed-footer
.fr-container
- %ul.fr-btns-group.fr-btns-group--inline-md
+ %ul.fr-btns-group.fr-btns-group--inline-md.fr-ml-0
%li
- = link_to t('continue_annotations', scope: [:layouts, :breadcrumb]), admin_procedure_path(@procedure), title: t('continue_annotations', scope: [:layouts, :breadcrumb]), class: 'fr-btn'
+ = link_to "Revenir à l’écran de gestion", admin_procedure_path(@procedure), title: t('continue_annotations', scope: [:layouts, :breadcrumb]), class: 'fr-link fr-icon-arrow-left-line fr-link--icon-left fr-mb-2w fr-mr-2w'
- if @procedure.draft_revision.revision_types_de_champ_private.count > 0
%li
- = link_to t('preview_annotations', scope: [:layouts, :breadcrumb]), apercu_admin_procedure_path(@procedure, params: {tab: 'annotations-privees'}), target: "_blank", rel: "noopener", class: 'fr-btn fr-btn--secondary'
+ = link_to t('preview_annotations', scope: [:layouts, :breadcrumb]), apercu_admin_procedure_path(@procedure, params: {tab: 'annotations-privees'}), target: "_blank", rel: "noopener", class: 'fr-link fr-mb-2w'
diff --git a/app/views/administrateurs/procedures/champs.html.haml b/app/views/administrateurs/procedures/champs.html.haml
index d927556a1..7fa005e67 100644
--- a/app/views/administrateurs/procedures/champs.html.haml
+++ b/app/views/administrateurs/procedures/champs.html.haml
@@ -1,26 +1,35 @@
+
+- if @procedure.draft_changed?
+ - content_for(:sticky_header) do
+ = render partial: 'administrateurs/procedures/unpublished_changes_sticky_header', locals: { procedure: @procedure }
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_back_path(@procedure)],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
- ['Configuration des champs']], preview: @procedure.draft_revision.valid? }
+ ['Champs du formulaire']], preview: @procedure.draft_revision.valid? }
.fr-container
- %h1 Configuration des champs
+ .flex.justify-between.align-center.fr-mb-3w
+ %h1.fr-h2 Champs du formulaire
+ - if @procedure.revised?
+ = link_to "Voir l'historique des modifications du formulaire", modifications_admin_procedure_path(@procedure), class: 'fr-link'
+
= render NestedForms::FormOwnerComponent.new
.fr-grid-row
- = render partial: 'champs_summary'
+ = render TypesDeChampEditor::HeaderSectionsSummaryComponent.new(procedure: @procedure, is_private: false)
.fr-col
- = render TypesDeChampEditor::EditorComponent.new(revision: @procedure.draft_revision)
+ = render TypesDeChampEditor::EditorComponent.new(revision: @procedure.draft_revision, is_annotation: false)
.padded-fixed-footer
.fixed-footer
.fr-container
.flex
- %ul.fr-btns-group.fr-btns-group--inline-md
+ %ul.fr-btns-group.fr-btns-group--inline-md.fr-ml-0
%li
- = link_to t('continue', scope: [:layouts, :breadcrumb]), admin_procedure_path(@procedure), title: t('continue_title', scope: [:layouts, :breadcrumb]), class: 'fr-btn'
+ = link_to admin_procedure_path(id: @procedure), class: 'fr-link fr-icon-arrow-left-line fr-link--icon-left fr-mb-2w fr-mr-2w' do
+ Revenir à l’écran de gestion
- if @procedure.draft_revision.revision_types_de_champ_public.count > 0
%li
- = link_to t('preview', scope: [:layouts, :breadcrumb]), apercu_admin_procedure_path(@procedure), target: "_blank", rel: "noopener", class: 'fr-btn fr-btn--secondary'
+ = link_to t('preview', scope: [:layouts, :breadcrumb]), apercu_admin_procedure_path(@procedure), target: "_blank", rel: "noopener", class: 'fr-link fr-mb-2w'
.fr-ml-auto
#autosave-notice.hidden
= render TypesDeChampEditor::EstimatedFillDurationComponent.new(revision: @procedure.draft_revision, is_annotation: false)
diff --git a/app/views/administrateurs/procedures/edit.html.haml b/app/views/administrateurs/procedures/edit.html.haml
index 137932e3a..95164e8e6 100644
--- a/app/views/administrateurs/procedures/edit.html.haml
+++ b/app/views/administrateurs/procedures/edit.html.haml
@@ -3,7 +3,7 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_back_path(@procedure)],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
- ['Description']] }
+ ['Présentation']] }
= render NestedForms::FormOwnerComponent.new
= form_for @procedure,
@@ -12,17 +12,8 @@
.fr-container
.fr-grid-row
.fr-col-12.fr-col-offset-md-2.fr-col-md-8
- %h1.fr-h2 Description
+ %h1.fr-h2 Présentation
= render partial: 'administrateurs/procedures/informations', locals: { f: f }
- .padded-fixed-footer
- .fixed-footer
- .fr-container
- .fr-grid-row
- .fr-col-12.fr-col-offset-md-2.fr-col-md-8
- %ul.fr-btns-group.fr-btns-group--inline-md
- %li
- = f.button 'Enregistrer', class: 'fr-btn'
- %li
- = link_to 'Annuler', admin_procedure_path(id: @procedure), class: 'fr-btn fr-btn--secondary', data: { confirm: 'Êtes-vous sûr de vouloir annuler les modifications effectuées ?'}
+ = render Procedure::FixedFooterComponent.new(procedure: @procedure, form: f, extra_class_names: 'fr-col-offset-md-2 fr-col-md-8')
diff --git a/app/views/administrateurs/procedures/index.html.haml b/app/views/administrateurs/procedures/index.html.haml
index 60d2441da..ba2311111 100644
--- a/app/views/administrateurs/procedures/index.html.haml
+++ b/app/views/administrateurs/procedures/index.html.haml
@@ -1,12 +1,14 @@
.sub-header
- .procedure-admin-listing-container
- = link_to "Nouvelle Démarche", new_from_existing_admin_procedures_path, id: 'new-procedure', class: 'fr-btn'
+ .flex.fr-container
+ %h1.fr-h3 Mes démarches
+ .procedure-admin-listing-container.fr-mt-1v
+ = link_to "Nouvelle Démarche", new_from_existing_admin_procedures_path, id: 'new-procedure', class: 'fr-btn'
.fr-container
%nav.fr-tabs{ role: 'navigation', 'aria-label': t('views.users.dossiers.secondary_menu') }
%ul.fr-tabs__list{ role: 'tablist' }
= tab_item(t('pluralize.published', count: @procedures_publiees_count), admin_procedures_path(statut: 'publiees'), active: @statut == 'publiees', badge: number_with_html_delimiter(@procedures_publiees_count))
- = tab_item('En test', admin_procedures_path(statut: 'brouillons'), active: @statut == 'brouillons', badge: number_with_html_delimiter(@procedures_draft_count))
+ = tab_item('en test', admin_procedures_path(statut: 'brouillons'), active: @statut == 'brouillons', badge: number_with_html_delimiter(@procedures_draft_count))
= tab_item(t('pluralize.closed', count: @procedures_closed_count), admin_procedures_path(statut: 'archivees'), active: @statut == 'archivees', badge: number_with_html_delimiter(@procedures_closed_count))
= tab_item(t('pluralize.deleted', count: @procedures_deleted_count), admin_procedures_path(statut: 'supprimees'), active: @statut === 'supprimees', badge: number_with_html_delimiter(@procedures_deleted_count))
diff --git a/app/views/administrateurs/procedures/jeton.html.haml b/app/views/administrateurs/procedures/jeton.html.haml
index 0062ac47b..8172136d5 100644
--- a/app/views/administrateurs/procedures/jeton.html.haml
+++ b/app/views/administrateurs/procedures/jeton.html.haml
@@ -1,26 +1,29 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_back_path(@procedure)],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
- ['Jeton']] }
+ ['Jeton API Entreprise']] }
-.container
- %h1.page-title
- Configurer le jeton API Entreprise
+.fr-container
+ %h1.fr-h2 Jeton API Entreprise
-.container
- %h1
- = form_with model: @procedure, url: url_for({ controller: 'administrateurs/procedures', action: :update_jeton }) do |f|
+= form_with model: @procedure, url: url_for({ controller: 'administrateurs/procedures', action: :update_jeton }) do |f|
+ .fr-container
= render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: 'fr-mb-2w') do |c|
- c.with_body do
%p
Démarches Simplifiées utilise
= link_to 'API Entreprise', "https://entreprise.api.gouv.fr/"
qui permet de récupérer les informations administratives des entreprises et des associations.
- Si votre démarche nécessite des autorisations spécifiques que Démarches Simplifiées n’a pas par défaut, merci de renseigner ici le jeton
- = link_to 'API Entreprise', "https://api.gouv.fr/les-api/api-entreprise/demande-acces"
+ Si votre démarche nécessite des autorisations spécifiques que Démarches Simplifiées n’a pas par défaut, merci de renseigner ci-dessous
+ %strong le jeton API Entreprise
propre à votre démarche.
+ %p
+ Si besoin, vous pouvez demander une habilitation API Entreprise en cliquant sur le lien suivant :
+ = link_to "https://api.gouv.fr/les-api/api-entreprise/demande-acces.", "https://api.gouv.fr/les-api/api-entreprise/demande-acces"
- .fr-input-group
- = f.label :api_entreprise_token, "Jeton", class: 'fr-label'
- = f.password_field :api_entreprise_token, value: @procedure.read_attribute(:api_entreprise_token), class: 'fr-input'
- = f.button 'Enregistrer', class: 'fr-btn'
+
+ = render partial: 'administrateurs/procedures/api_entreprise_token_expiration_alert', locals: { procedure: @procedure }
+
+ = render Dsfr::InputComponent.new(form: f, attribute: :api_entreprise_token, input_type: :password_field, required: false, opts: { value: @procedure.read_attribute(:api_entreprise_token)})
+
+ = render Procedure::FixedFooterComponent.new(procedure: @procedure, form: f)
diff --git a/app/views/administrateurs/procedures/modifications.html.haml b/app/views/administrateurs/procedures/modifications.html.haml
index a3775ada7..73b8673bd 100644
--- a/app/views/administrateurs/procedures/modifications.html.haml
+++ b/app/views/administrateurs/procedures/modifications.html.haml
@@ -1,16 +1,18 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_back_path(@procedure)],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
- ['Modifications']] }
-.container
- %h1.page-title
+ ['Champs du formulaire', champs_admin_procedure_path(@procedure)],
+ ['Historique des modifications du formulaire']] }
+.fr-container
+ .fr-mb-3w
+ = link_to "Champs du formulaire", champs_admin_procedure_path(@procedure), class: "fr-link fr-icon-arrow-left-line fr-link--icon-left"
+ %h1.fr-h2
Historique des modifications du formulaire
-.container
+.fr-container
- previous_revision = nil
- @procedure.revisions.each do |revision|
- if previous_revision.present? && !revision.draft?
- - changes = previous_revision.compare(revision)
- dossiers = revision.dossiers.visible_by_administration
- dossiers_en_construction_count = dossiers.state_en_construction.count
- dossiers_en_instruction_count = dossiers.state_en_instruction.count
@@ -28,5 +30,7 @@
%p= t('.dossiers_en_construction', count: dossiers_en_construction_count)
- elsif !dossiers_en_instruction_count.zero?
%p= t('.dossiers_en_instruction', count: dossiers_en_instruction_count)
- = render Procedure::RevisionChangesComponent.new changes:, previous_revision:
+ = render Procedure::RevisionChangesComponent.new new_revision: revision, previous_revision:
- previous_revision = revision
+
+= render Procedure::FixedFooterComponent.new(procedure: @procedure)
diff --git a/app/views/administrateurs/procedures/monavis.html.haml b/app/views/administrateurs/procedures/monavis.html.haml
index 2cbc3cce3..ee5e98fe9 100644
--- a/app/views/administrateurs/procedures/monavis.html.haml
+++ b/app/views/administrateurs/procedures/monavis.html.haml
@@ -3,12 +3,12 @@
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['MonAvis']] }
-.container
- %h1.page-title
- Insérer un lien vers « MonAvis »
+.fr-container
+ %h1.fr-h2
+ Bouton « MonAvis »
-.container
- %h1
- = form_for @procedure, url: url_for({ controller: 'administrateurs/procedures', action: :update_monavis }), html: { class: 'form', multipart: true } do |f|
+= form_for @procedure, url: url_for({ controller: 'administrateurs/procedures', action: :update_monavis }), html: { class: 'form', multipart: true } do |f|
+ .fr-container
= render partial: 'monavis', locals: { f: f }
- = f.button 'Enregistrer', class: 'fr-btn'
+
+ = render Procedure::FixedFooterComponent.new(procedure: @procedure, form: f)
diff --git a/app/views/administrateurs/procedures/new_from_existing.html.haml b/app/views/administrateurs/procedures/new_from_existing.html.haml
index 76ccd4178..228004b1d 100644
--- a/app/views/administrateurs/procedures/new_from_existing.html.haml
+++ b/app/views/administrateurs/procedures/new_from_existing.html.haml
@@ -1,35 +1,34 @@
.container
- if current_administrateur.procedures.brouillons.count == 0
- .card.feedback
- .card-title
- Bienvenue,
- %br
- vous allez pouvoir créer une première démarche de test.
- Celle-ci sera visible uniquement par vous et ne sera publiée nulle part, alors pas de crainte à avoir.
- %br
- %br
- Besoin d’aide ?
- %br
- > Vous pouvez
- = link_to "visionner cette vidéo",
- "https://vimeo.com/261478872",
- target: "_blank"
- %br
- > Vous pouvez lire notre
- = link_to "documentation en ligne",
- ADMINISTRATEUR_TUTORIAL_URL,
- target: "_blank"
-
- %br
- > Vous pouvez enfin
- = link_to "prendre un rendez-vous téléphonique avec nous",
- CALENDLY_URL,
- target: "_blank"
-
- :javascript
- document.addEventListener("DOMContentLoaded", function() {
- $crisp.push(["do", "trigger:run", ["admin-signup"]]);
- });
+ = render Dsfr::CalloutComponent.new(title: nil, icon: "fr-icon-information-line", extra_class_names: 'fr-my-4w') do |c|
+ - c.with_html_body do
+ %p
+ Bienvenue,
+ %br
+ vous allez pouvoir créer une première démarche de test.
+ Celle-ci sera visible uniquement par vous et ne sera publiée nulle part, alors pas de crainte à avoir.
+ %br
+ %br
+ Besoin d’aide ?
+ %br
+ > Vous pouvez
+ = link_to "visionner cette vidéo",
+ "https://vimeo.com/261478872",
+ target: "_blank"
+ %br
+ > Vous pouvez lire notre
+ = link_to "documentation en ligne",
+ ADMINISTRATEUR_TUTORIAL_URL,
+ target: "_blank"
+ %br
+ > Vous pouvez enfin
+ = link_to "prendre un rendez-vous téléphonique avec nous",
+ CALENDLY_URL,
+ target: "_blank"
+ :javascript
+ document.addEventListener("DOMContentLoaded", function() {
+ $crisp.push(["do", "trigger:run", ["admin-signup"]]);
+ });
.form
diff --git a/app/views/administrateurs/procedures/publication.html.haml b/app/views/administrateurs/procedures/publication.html.haml
index 95513ddbf..9248dc2b6 100644
--- a/app/views/administrateurs/procedures/publication.html.haml
+++ b/app/views/administrateurs/procedures/publication.html.haml
@@ -46,6 +46,8 @@
%li= link_to("des instructeurs", admin_procedure_groupe_instructeur_path(@procedure, @procedure.defaut_groupe_instructeur))
- if @procedure.service.nil?
%li= link_to("un service", admin_services_path(procedure_id: @procedure))
+ - if @procedure.service_siret_test?
+ %li= link_to("un service avec un SIRET valide", admin_services_path(procedure_id: @procedure))
= link_to t('.back_to_procedure'), admin_procedure_path(id: @procedure), class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-arrow-go-back-line fr-mt-2w'
- else
diff --git a/app/views/administrateurs/procedures/show.html.haml b/app/views/administrateurs/procedures/show.html.haml
index 16ef212bf..877f4ef98 100644
--- a/app/views/administrateurs/procedures/show.html.haml
+++ b/app/views/administrateurs/procedures/show.html.haml
@@ -5,7 +5,7 @@
.fr-container.procedure-admin-container
%ul.fr-btns-group.fr-btns-group--inline-sm.fr-btns-group--icon-left
- - if @procedure.draft_revision.valid?
+ - if @procedure.validate(:publication)
- if !@procedure.brouillon?
= link_to 'Télécharger', admin_procedure_archives_path(@procedure), class: 'fr-btn fr-btn--tertiary fr-btn--icon-left fr-icon-download-line', id: "archive-procedure"
@@ -27,15 +27,20 @@
= link_to 'Clore', admin_procedure_close_path(procedure_id: @procedure.id), class: 'fr-btn fr-btn--tertiary fr-btn--icon-left fr-icon-calendar-close-fill', id: "close-procedure-link"
.fr-container
- = render TypesDeChampEditor::ErrorsSummary.new(revision: @procedure.draft_revision)
+ - if @procedure.api_entreprise_token_expired_or_expires_soon?
+ = render Dsfr::AlertComponent.new(state: :error, title: t(:technical_issues, scope: [:administrateurs, :procedures]), extra_class_names: 'fr-mb-2w') do |c|
+ - c.with_body do
+ %ul.fr-mb-0
+ %li
+ Le
+ = link_to "Jeton API Entreprise", jeton_admin_procedure_path(@procedure), class: 'error-anchor'
+ est expiré ou va expirer prochainement
-- if @procedure.draft_changed?
- .fr-container
+ - if @procedure.draft_changed?
= render Dsfr::CalloutComponent.new(title: t(:has_changes, scope: [:administrateurs, :revision_changes]), icon: "fr-fi-information-line") do |c|
- c.with_body do
- = render Procedure::RevisionChangesComponent.new changes: @procedure.revision_changes, previous_revision: @procedure.published_revision
-
- = render Procedure::PublicationWarningComponent.new(procedure: @procedure)
+ = render Procedure::ErrorsSummary.new(procedure: @procedure, validation_context: :publication)
+ = render Procedure::RevisionChangesComponent.new new_revision: @procedure.draft_revision, previous_revision: @procedure.published_revision
- c.with_bottom do
%ul.fr-mt-2w.fr-btns-group.fr-btns-group--inline
@@ -44,6 +49,9 @@
- else
%li= button_to 'Publier les modifications', admin_procedure_publication_path(@procedure), class: 'fr-btn', id: 'publish-procedure-link', data: { disable_with: "Publication..." }, disabled: !@procedure.draft_revision.valid? || @procedure.errors.present?, method: :get
%li= button_to "Réinitialiser les modifications", admin_procedure_reset_draft_path(@procedure), class: 'fr-btn fr-btn--secondary fr-mr-2w', data: { confirm: 'Êtes-vous sûr de vouloir réinitialiser les modifications ?' }, method: :put
+ - else
+ = render Procedure::ErrorsSummary.new(procedure: @procedure, validation_context: :publication)
+
- if !@procedure.procedure_expires_when_termine_enabled?
= render partial: 'administrateurs/procedures/suggest_expires_when_termine', locals: { procedure: @procedure }
@@ -66,18 +74,18 @@
= "Un email a été envoyé pour informer les usagers le #{ l(@procedure.closed_at.to_date) }"
.fr-container
- %h2= "Gestion de la démarche № #{@procedure.id}"
+ %h2= "Gestion de la démarche № #{number_with_html_delimiter(@procedure.id)}"
%h3.fr-h6 Indispensable avant publication
.fr-grid-row.fr-grid-row--gutters.fr-mb-5w
= render Procedure::Card::PresentationComponent.new(procedure: @procedure)
= render Procedure::Card::ZonesComponent.new(procedure: @procedure) if Rails.application.config.ds_zonage_enabled
= render Procedure::Card::ChampsComponent.new(procedure: @procedure)
+ = render Procedure::Card::IneligibiliteDossierComponent.new(procedure: @procedure)
= render Procedure::Card::ServiceComponent.new(procedure: @procedure, administrateur: current_administrateur)
= render Procedure::Card::AdministrateursComponent.new(procedure: @procedure)
= render Procedure::Card::InstructeursComponent.new(procedure: @procedure)
- = render Procedure::Card::ModificationsComponent.new(procedure: @procedure)
- %h3.fr-h6 Pour aller plus loin
+ %h3.fr-h6 Autres paramètres
.fr-grid-row.fr-grid-row--gutters.fr-mb-5w
= render Procedure::Card::AttestationComponent.new(procedure: @procedure)
= render Procedure::Card::ExpertsComponent.new(procedure: @procedure)
@@ -85,8 +93,9 @@
= render Procedure::Card::AnnotationsComponent.new(procedure: @procedure)
= render Procedure::Card::APIEntrepriseComponent.new(procedure: @procedure)
= render Procedure::Card::APIParticulierComponent.new(procedure: @procedure)
- = render Procedure::Card::SVASVRComponent.new(procedure: @procedure) if @procedure.sva_svr_enabled? || @procedure.feature_enabled?(:sva)
+ = render Procedure::Card::SVASVRComponent.new(procedure: @procedure)
= render Procedure::Card::MonAvisComponent.new(procedure: @procedure)
= render Procedure::Card::DossierSubmittedMessageComponent.new(procedure: @procedure)
= render Procedure::Card::ChorusComponent.new(procedure: @procedure)
= render Procedure::Card::AccuseLectureComponent.new(procedure: @procedure)
+ = render Procedure::Card::LabelsComponent.new(procedure: @procedure)
diff --git a/app/views/administrateurs/procedures/zones.html.haml b/app/views/administrateurs/procedures/zones.html.haml
index e8b6e91eb..f30e5c737 100644
--- a/app/views/administrateurs/procedures/zones.html.haml
+++ b/app/views/administrateurs/procedures/zones.html.haml
@@ -4,12 +4,12 @@
locals: { steps: [['Démarches', admin_procedures_back_path(@procedure)],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['Description']] }
-.container
- = form_for @procedure,
- url: url_for({ controller: 'administrateurs/procedures', action: :update, id: @procedure.id }),
- html: { multipart: true } do |f|
+= form_for @procedure,
+ url: url_for({ controller: 'administrateurs/procedures', action: :update, id: @procedure.id }),
+ html: { multipart: true } do |f|
- %h1.page-title Zones
+ .fr-container
+ %h1.fr-h2 Zones
- if Rails.application.config.ds_zonage_enabled
%fieldset.fr-fieldset{ aria: { labelledby: "zones-legend"} }
@@ -25,7 +25,4 @@
= b.check_box
= b.label class: "fr-label"
- .procedure-form__actions.sticky--bottom
- .actions-right
- = link_to 'Annuler', admin_procedure_path(id: @procedure), class: 'fr-btn fr-btn--tertiary fr-mr-2w', data: { confirm: 'Êtes-vous sûr de vouloir annuler les modifications effectuées ?'}
- = f.button 'Enregistrer', class: 'fr-btn fr-btn--primary'
+ = render Procedure::FixedFooterComponent.new(procedure: @procedure, form: f)
diff --git a/app/views/administrateurs/services/_form.html.haml b/app/views/administrateurs/services/_form.html.haml
index c3afc1522..8a463a3d7 100644
--- a/app/views/administrateurs/services/_form.html.haml
+++ b/app/views/administrateurs/services/_form.html.haml
@@ -1,4 +1,26 @@
-= form_with model: [ :admin, service], local: true do |f|
+= form_with model: [:admin, service], id: "service_form" do |f|
+
+ = render Dsfr::InputComponent.new(form: f, attribute: :siret, input_type: :text_field,
+ opts: { placeholder: "14 chiffres, sans espace",
+ onblur: token_list("Turbo.visit('#{prefill_admin_services_path(procedure_id: procedure.id)}?siret=' + this.value)" => service.new_record?) }) do |c|
+ - if service.etablissement_infos.blank? && local_assigns[:prefilled].nil?
+ - c.with_hint do
+ = "Indiquez le numéro de SIRET de l’organisme dont ce service dépend. Rechercher le SIRET sur "
+ = link_to("annuaire-entreprises.data.gouv.fr", annuaire_link, **external_link_attributes)
+ - if service.new_record?
+ %br
+ = "Nous préremplirons les informations de contact à partir de l’Annuaire Service Public correspondant."
+
+ .fr-mb-2w
+ - if local_assigns[:prefilled] == :success
+ %p.fr-info-text Génial ! La plupart des informations du service ont été préremplies ci-dessous. Vérifiez-les et complétez-les le cas échéant.
+ - elsif local_assigns[:prefilled] == :partial
+ %p.fr-info-text
+ Nous avons prérempli certaines informations correspondant à ce SIRET. Complétez les autres manuellement.
+ - elsif local_assigns[:prefilled] == :failure
+ %p.fr-error-text
+ Une erreur a empêché le préremplissage des informations.
+ Vérifiez que le numéro de SIRET est correct et complétez les informations manuellement le cas échéant.
= render Dsfr::InputComponent.new(form: f, attribute: :nom, input_type: :text_field)
@@ -7,13 +29,9 @@
.fr-input-group
= f.label :type_organisme, class: "fr-label" do
Type d’organisme
+ = render EditableChamp::AsteriskMandatoryComponent.new
- = f.select :type_organisme, Service.type_organismes.keys.map { |key| [ I18n.t("type_organisme.#{key}"), key] }, {}, class: 'fr-select'
-
- = render Dsfr::InputComponent.new(form: f, attribute: :siret, input_type: :text_field, opts: { placeholder: "14 chiffres, sans espace" }) do |c|
- - c.with_hint do
- = "Indiquez le numéro de SIRET de l’organisme dont ce service dépend. Rechercher le SIRET sur "
- = link_to("annuaire-entreprises.data.gouv.fr", annuaire_link, **external_link_attributes)
+ = f.select :type_organisme, Service.type_organismes.keys.map { |key| [ I18n.t("type_organisme.#{key}"), key] }, { include_blank: true }, { class: "fr-select" , required: true }
= render Dsfr::CalloutComponent.new(title: "Informations de contact") do |c|
- c.with_body do
@@ -31,14 +49,6 @@
= render Dsfr::InputComponent.new(form: f, attribute: :horaires, input_type: :text_area)
= render Dsfr::InputComponent.new(form: f, attribute: :adresse, input_type: :text_area)
- - if procedure_id.present?
- = hidden_field_tag :procedure_id, procedure_id
-
- .padded-fixed-footer
- .fixed-footer
- .fr-container
- %ul.fr-btns-group.fr-btns-group--inline-md
- %li
- = f.submit "Enregistrer", class: "fr-btn"
- %li
- = link_to "Annuler et revenir à la page de suivi", admin_procedure_path(id: @procedure.id), class: "fr-btn fr-btn--secondary"
+ - if local_assigns[:procedure].present?
+ = hidden_field_tag :procedure_id, procedure.id
+ = render Procedure::FixedFooterComponent.new(procedure: procedure, form: f)
diff --git a/app/views/administrateurs/services/edit.html.haml b/app/views/administrateurs/services/edit.html.haml
index 186294bfc..3cd11f6fc 100644
--- a/app/views/administrateurs/services/edit.html.haml
+++ b/app/views/administrateurs/services/edit.html.haml
@@ -5,21 +5,22 @@
['Modifier le service']] }
-.container
+.fr-container
+ .flex.justify-between.align-center.fr-mb-3w
+ = link_to "Liste de tous les services", admin_services_path(procedure_id: @procedure.id), class: "fr-link fr-icon-arrow-left-line fr-link--icon-left"
+ = link_to "+ Nouveau service", new_admin_service_path(procedure_id: @procedure.id), class: "fr-btn"
+
+ %h1.fr-h2
+ Modifier le service
+
- other_services = @service.procedures.reject {|procedure| procedure.id == @procedure.id }
- if other_services.count > 1
- = render Dsfr::AlertComponent.new(state: :warning, title: "Modifier ce service impactera la ou les démarches qui sont rattachée/s") do |c|
+ = render Dsfr::AlertComponent.new(state: :warning, title: "Modifier ce service impactera la ou les démarches qui sont rattachée/s", extra_class_names: 'fr-mb-3w') do |c|
- c.with_body do
%ul
- other_services.each do |proc|
%li= "#{proc.libelle} (N° #{proc.id})"
%p.mt-3 Si vous souhaitez modifier uniquement les informations pour ce service, créez un nouveau service puis associez-le à la démarche
- %p.mt-3
- = link_to "+ Nouveau service", new_admin_service_path(procedure_id: @procedure.id), class: "fr-btn"
-
-
- %h1.mt-2 Modifier le service
-
= render partial: 'form',
- locals: { service: @service, procedure_id: @procedure.id }
+ locals: { service: @service, procedure: @procedure }
diff --git a/app/views/administrateurs/services/index.html.haml b/app/views/administrateurs/services/index.html.haml
index b0fb75517..b0532b69b 100644
--- a/app/views/administrateurs/services/index.html.haml
+++ b/app/views/administrateurs/services/index.html.haml
@@ -1,34 +1,44 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
- ['Choix du service']] }
+ ['Service']] }
-#services-index.container
- %h1.fr-h1 Liste des Services
- %h2.fr-h4 La démarche “#{@procedure.libelle}” peut être affectée aux services dans la liste ci-dessous
+#services-index.fr-container
+ %h1.fr-h2 Service
- %table.fr-table.width-100.mt-3
- %thead
- %tr
- %th{ scope: "col" }
- Nom
- %th.change{ scope: "col" }
- = link_to "Nouveau service", new_admin_service_path(procedure_id: @procedure.id), class: "fr-btn fr-btn--secondary"
+ = link_to "Nouveau service", new_admin_service_path(procedure_id: @procedure.id), class: "fr-btn fr-btn--primary fr-btn--icon-left fr-icon-add-circle-line mb-3"
- %tbody
- - @services.each do |service|
+ .fr-table.fr-table--layout-fixed
+ %table
+ %caption Liste des services pouvant être affectés à la démarche
+ %thead
%tr
- %td
- = service.nom
- %td.change
- - if @procedure.service == service
- %strong.mr-2 (Assigné)
- - else
- = button_to "Assigner", add_to_procedure_admin_services_path(procedure: { id: @procedure.id, service_id: service.id, }), method: :patch, class: 'link mr-2', form_class: 'inline'
- = link_to('Modifier', edit_admin_service_path(service, procedure_id: @procedure.id), class: 'link my-2')
- - if @procedure.service != service
- = link_to 'Supprimer',
- admin_service_path(service, procedure_id: @procedure.id),
- method: :delete,
- data: { confirm: "Confirmez vous la suppression de #{service.nom}" },
- class: 'btn btn-link ml-2'
+ %th{ scope: "col" }
+ Nom
+ %th.fr-col-4{ scope: "col" }
+ Actions
+
+ %tbody
+ - @services.each do |service|
+ %tr
+ %td
+ = service.nom
+ %td.fr-col-4
+ .fr-container.flex.px-0
+ .fr-col-4.fr-col--middle
+ - if @procedure.service == service
+ %p.fr-badge.fr-badge--success.fr-badge--sm
+ ASSIGNÉ
+ - else
+ = button_to "Assigner", add_to_procedure_admin_services_path(procedure: { id: @procedure.id, service_id: service.id, }), method: :patch, class: 'fr-btn fr-btn--sm fr-btn--secondary fr-btn--icon-left fr-icon-checkbox-circle-line'
+ .fr-col-4
+ = link_to('Modifier', edit_admin_service_path(service, procedure_id: @procedure.id), class: 'fr-btn fr-btn--sm fr-btn--secondary fr-btn--icon-left fr-icon-pencil-line')
+ .fr-col-4
+ = button_to 'Supprimer',
+ admin_service_path(service, procedure_id: @procedure.id),
+ method: :delete,
+ data: { confirm: "Confirmez vous la suppression de #{service.nom}" },
+ class: 'fr-btn fr-btn--sm fr-btn--secondary fr-btn--icon-left fr-icon-delete-line',
+ disabled: (@procedure.service == service)
+
+= render Procedure::FixedFooterComponent.new(procedure: @procedure)
diff --git a/app/views/administrateurs/services/new.html.haml b/app/views/administrateurs/services/new.html.haml
index 691b864e7..7477ad9b2 100644
--- a/app/views/administrateurs/services/new.html.haml
+++ b/app/views/administrateurs/services/new.html.haml
@@ -8,4 +8,4 @@
%h1 Nouveau Service
= render partial: 'form',
- locals: { service: @service, procedure_id: @procedure.id }
+ locals: { service: @service, procedure: @procedure, prefilled: @prefilled }
diff --git a/app/views/administrateurs/sva_svr/edit.html.haml b/app/views/administrateurs/sva_svr/edit.html.haml
index 715938671..2c026b42b 100644
--- a/app/views/administrateurs/sva_svr/edit.html.haml
+++ b/app/views/administrateurs/sva_svr/edit.html.haml
@@ -1,10 +1,10 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
["#{@procedure.libelle.truncate_words(10)}", admin_procedure_path(@procedure)],
- ["Configuration SVA/SVR"]] }
+ ["Silence Vaut Accord ou Rejet"]] }
.fr-container.fr-my-5w
- %h1.fr-h1 Règle du Silence Vaut Accord ou Silence Vaut Rejet
+ %h1.fr-h2 Silence Vaut Accord ou Rejet
= render Dsfr::CalloutComponent.new(title: "Fonctionnement du SVA/SVR") do |c|
- c.with_body do
@@ -40,4 +40,4 @@
= link_to("Liste des démarches encadrées par ce principe", "https://www.service-public.fr/demarches-silence-vaut-accord", class: "fr-link", title: new_tab_suffix("Rechercher les démarches avec SVA sur service-public.fr"), **external_link_attributes)
- = render Procedure::SVASVRFormComponent.new(procedure: @procedure, configuration: @configuration)
+= render Procedure::SVASVRFormComponent.new(procedure: @procedure, configuration: @configuration)
diff --git a/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml b/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml
index 9421b594a..7a246c79a 100644
--- a/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml
+++ b/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml
@@ -10,15 +10,15 @@
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['Configuration des champs']],
- preview: @procedure.draft_revision.valid? })
+ preview: @procedure.validate(@coordinate&.private? ? :types_de_champ_private_editor : :types_de_champ_public_editor) })
-= turbo_stream.replace 'errors-summary', render(TypesDeChampEditor::ErrorsSummary.new(revision: @procedure.draft_revision))
+= turbo_stream.replace 'errors-summary', render(Procedure::ErrorsSummary.new(procedure: @procedure, validation_context: @coordinate&.private? ? :types_de_champ_private_editor : :types_de_champ_public_editor))
-= turbo_stream.replace 'summary', render(partial: 'administrateurs/procedures/champs_summary')
+= turbo_stream.replace 'summary', render(TypesDeChampEditor::HeaderSectionsSummaryComponent.new(procedure: @procedure, is_private: @coordinate&.private?))
- unless flash.alert
= turbo_stream.show 'autosave-notice'
- = turbo_stream.replace 'autosave-notice', render(partial: 'administrateurs/autosave_notice')
+ = turbo_stream.replace 'autosave-notice', render(AutosaveNoticeComponent.new(success: true, label_scope: :form))
= turbo_stream.hide 'autosave-notice', delay: 30000
- if @destroyed.present?
@@ -49,3 +49,10 @@
= turbo_stream.hide dom_id(@created.coordinate.parent, :type_de_champ_add_button)
- elsif @destroyed&.child? && @destroyed.parent.empty?
= turbo_stream.show dom_id(@destroyed.parent, :type_de_champ_add_button)
+
+- if @procedure.draft_changed?
+ = turbo_stream.update "sticky-header" do
+ = render partial: "administrateurs/procedures/unpublished_changes_sticky_header", locals: { procedure: @procedure }
+
+- else
+ = turbo_stream.update "sticky-header", ""
diff --git a/app/views/agent_connect/agent/explanation_2fa.html.haml b/app/views/agent_connect/agent/explanation_2fa.html.haml
new file mode 100644
index 000000000..ab52b99f1
--- /dev/null
+++ b/app/views/agent_connect/agent/explanation_2fa.html.haml
@@ -0,0 +1,14 @@
+.fr-container
+ %h1.fr-h2.fr-mt-4w Une validation en 2 étapes est désormais nécessaire.
+
+ %p.fr-mb-2w
+ La sécurité de votre compte augmente. Nous vous demandons à présent une validation en 2 étapes pour vous connecter.
+
+ %p.fr-mb-2w
+ Vous allez devoir configurer votre mode d'authentification sur le site MonComptePro :
+
+ %img{ src: image_url("instructions_moncomptepro.png"), alt: "MonComptePro", loading: 'lazy' }
+
+
+ %button.fr-btn.fr-btn--primary.fr-mb-2w
+ = link_to "Configurer mon appli d'authentification sur MonComptePro", ENV['MON_COMPTE_PRO_2FA_NOT_CONFIGURED_URL']
diff --git a/app/views/agent_connect/agent/index.html.haml b/app/views/agent_connect/agent/index.html.haml
index 375ca47ce..d83c40f8c 100644
--- a/app/views/agent_connect/agent/index.html.haml
+++ b/app/views/agent_connect/agent/index.html.haml
@@ -38,7 +38,7 @@
= t('views.users.sessions.new.for_tiers_alert')
.fr-fieldset__element
- %p.fr-text--sm= t('utils.mandatory_champs')
+ %p.fr-text--sm= t('utils.asterisk_html')
.fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email' }) do |c|
@@ -55,18 +55,16 @@
= f.check_box :remember_me
= f.label :remember_me, t('views.users.sessions.new.remember_me'), class: 'remember-me'
- %ul.fr-btns-group
- %li= f.submit t('views.users.sessions.new.connection'), class: "fr-btn"
+ .fr-btns-group= f.submit t('views.users.sessions.new.connection'), class: "fr-btn"
%hr
%h2.fr-h6= t('.you_are_a_citizen')
- %ul.fr-btns-group
- %li= link_to t('.citizen_page'), new_user_session_path, class: "fr-btn fr-btn--secondary width-100"
+ .fr-btns-group= link_to t('.citizen_page'), new_user_session_path, class: "fr-btn fr-btn--secondary"
.fr-col-lg.fr-p-6w
= render Dsfr::CalloutComponent.new(title: t('.full_deploy_title'), icon: 'fr-icon-information-line') do |c|
- c.with_body do
= t('.full_deploy_body')
%h2.fr-h6= t('.whats_ds', application_name: Current.application_name)
- = image_tag "landing/hero/dematerialiser.svg", class: "paperless-logo", alt: ""
+ = image_tag "landing/hero/dematerialiser.svg", class: "fr-responsive-img", alt: ""
diff --git a/app/views/agent_connect/agent/relogin_after_2fa_config.html.haml b/app/views/agent_connect/agent/relogin_after_2fa_config.html.haml
new file mode 100644
index 000000000..898f57497
--- /dev/null
+++ b/app/views/agent_connect/agent/relogin_after_2fa_config.html.haml
@@ -0,0 +1,12 @@
+.fr-container
+ %h1.fr-h2.fr-mt-4w Poursuivez votre connexion à #{APPLICATION_NAME}
+
+ = render Dsfr::AlertComponent.new(state: :success, extra_class_names: 'fr-mb-4w') do |c|
+ - c.with_body do
+ %p Votre application d'authentification a bien été configurée.
+
+ %p.fr-mb-4w
+ Vous allez maintenant pouvoir vous connecter à nouveau à #{APPLICATION_NAME} en effectuant la validation en 2 étapes avec votre application d'authentification.
+
+ %button.fr-btn.fr-btn--primary.fr-mb-2w
+ = link_to "Se connecter à #{APPLICATION_NAME} avec #{AgentConnect}", agent_connect_login_path
diff --git a/app/views/application/_general_footer_row.html.haml b/app/views/application/_general_footer_row.html.haml
index e1fe2d44f..f51f258c2 100644
--- a/app/views/application/_general_footer_row.html.haml
+++ b/app/views/application/_general_footer_row.html.haml
@@ -8,11 +8,11 @@
%li.fr-footer__bottom-item
= link_to t("links.footer.vote_feature.label"), FEATURE_UPVOTE_URL, title: t("links.footer.vote_feature.title"), class: "fr-footer__bottom-link", target: "_blank", rel: "noopener noreferrer"
%li.fr-footer__bottom-item
- = link_to t("links.footer.accessibilite.label"), ACCESSIBILITE_URL, title: t("links.footer.accessibilite.title"), class: "fr-footer__bottom-link", rel: "noopener noreferrer"
+ = link_to t("links.footer.accessibilite.label"), ACCESSIBILITE_URL, class: "fr-footer__bottom-link", rel: "noopener noreferrer"
%li.fr-footer__bottom-item
- = link_to t("links.footer.mentions_legales.label"), MENTIONS_LEGALES_URL, title: t("links.footer.mentions_legales.title"), class: "fr-footer__bottom-link", rel: "noopener noreferrer"
+ = link_to t("links.footer.mentions_legales.label"), MENTIONS_LEGALES_URL, class: "fr-footer__bottom-link", rel: "noopener noreferrer"
%li.fr-footer__bottom-item
- = link_to t("links.footer.cookies.label"), suivi_path, title: t("links.footer.cookies.title"), class: "fr-footer__bottom-link"
+ = link_to t("links.footer.cookies.label"), suivi_path, class: "fr-footer__bottom-link", hreflang: "fr"
%li.fr-footer__bottom-item
%button.fr-footer__bottom-link.fr-icon-theme-fill.fr-btn--icon-left{ aria: {controls: "fr-theme-modal" }, data: {'fr-opened': "false" } }
= t('links.footer.display_params')
diff --git a/app/views/attachments/destroy.turbo_stream.haml b/app/views/attachments/destroy.turbo_stream.haml
index 48cee0bc3..f17dcaabe 100644
--- a/app/views/attachments/destroy.turbo_stream.haml
+++ b/app/views/attachments/destroy.turbo_stream.haml
@@ -1,7 +1,9 @@
-= turbo_stream.remove dom_id(@attachment, :persisted_row)
-
-- if @champ_id
- = turbo_stream.show "attachment-multiple-empty-#{@champ_id}"
- = turbo_stream.focus_all "#attachment-multiple-empty-#{@champ_id} input"
-
-= turbo_stream.show_all ".attachment-input-#{@attachment.id}"
+- if @champ
+ = fields_for @champ.input_name, @champ do |form|
+ = turbo_stream.replace @champ.input_group_id do
+ = render EditableChamp::EditableChampComponent.new champ: @champ, form: form
+ = turbo_stream.focus_all "#attachment-multiple-empty-#{@champ.public_id} input"
+- else
+ = turbo_stream.replace dom_id(@attachment, :edit) do
+ = render Attachment::EditComponent.new(**@attachment_options)
+ = turbo_stream.focus_all "##{dom_id(@attachment.record, @attachment.name)}"
diff --git a/app/views/avis_mailer/avis_invitation.html.haml b/app/views/avis_mailer/avis_invitation.html.haml
index e4bb0db96..262029b0d 100644
--- a/app/views/avis_mailer/avis_invitation.html.haml
+++ b/app/views/avis_mailer/avis_invitation.html.haml
@@ -22,11 +22,7 @@
%p{ style: "padding: 8px; color: #333333; background-color: #EEEEEE; font-size: 14px;" }
= @avis.introduction
-- if @avis.expert.user.active?.present?
- %p
- = round_button("Donner votre avis", @url, :primary)
-- else
- %p
- = round_button("Inscrivez-vous pour donner votre avis", @url, :primary)
+%p
+ = round_button("Donner votre avis", @url, :primary)
= render partial: "layouts/mailers/signature"
diff --git a/app/views/avis_mailer/avis_invitation_and_confirm_email.html.haml b/app/views/avis_mailer/avis_invitation_and_confirm_email.html.haml
new file mode 100644
index 000000000..aab81b459
--- /dev/null
+++ b/app/views/avis_mailer/avis_invitation_and_confirm_email.html.haml
@@ -0,0 +1,33 @@
+- content_for(:title, 'Invitation à donner votre avis')
+
+- content_for(:footer) do
+ Merci de ne pas répondre à cet email. Donnez votre avis
+ = link_to("sur #{Current.application_name}", @url)
+ ou
+ = succeed '.' do
+ = mail_to(@avis.claimant.email, "contactez la personne qui vous a invité")
+
+%p
+ Bonjour,
+
+%p
+ Vous avez été invité par
+ %strong= @avis.claimant.email
+ = "à donner votre avis sur le dossier nº #{@avis.dossier.id} de la démarche :"
+ %strong= @avis.procedure.libelle
+
+%p
+ = "#{@avis.claimant.email} vous a écrit :"
+ %br
+%p{ style: "padding: 8px; color: #333333; background-color: #EEEEEE; font-size: 14px;" }
+ = @avis.introduction
+
+- if @avis.expert.user.active?
+ %p
+ = round_button('Confirmez votre adresse email pour donner votre avis', users_confirm_email_url(token: @token), :primary)
+- else
+ %p
+ = round_button("Inscrivez-vous pour donner votre avis", @url, :primary)
+
+
+= render partial: "layouts/mailers/signature"
diff --git a/app/views/carte/show.html.erb b/app/views/carte/show.html.erb
index 04d1aed9b..4847c96d2 100644
--- a/app/views/carte/show.html.erb
+++ b/app/views/carte/show.html.erb
@@ -249,7 +249,7 @@
<% end %>
<%= map_form.label :year, class: 'fr-label' %>
- <%= map_form.select(:year, (2018..Date.current.year).to_a.reverse, { include_blank: t(:from_beginning, scope: 'activemodel.attributes.map_filter') }, { class: "fr-select" }) %>
+ <%= map_form.select(:year, MapFilter::YEARS_INTERVAL.to_a.reverse, { include_blank: t(:from_beginning, scope: 'activemodel.attributes.map_filter') }, { class: "fr-select" }) %>
<%= map_form.submit(name: nil, class: 'hidden', data: { autosubmit_target: 'submitter' } ) %>
<% end %>
diff --git a/app/views/champs/repetition/remove.turbo_stream.haml b/app/views/champs/repetition/remove.turbo_stream.haml
index 5fb0fe0be..d7be234a4 100644
--- a/app/views/champs/repetition/remove.turbo_stream.haml
+++ b/app/views/champs/repetition/remove.turbo_stream.haml
@@ -1,6 +1,2 @@
-= turbo_stream.remove "safe-row-selector-#{@row_id}"
-
-- if @champ.rows.size > 0 && @champ.rows.last&.first&.present?
- = turbo_stream.focus @champ.rows.last&.first.focusable_input_id
-- else
- = turbo_stream.focus dom_id(@champ, :create_repetition)
+= turbo_stream.remove @to_remove
+= turbo_stream.focus @to_focus
diff --git a/app/views/commencer/show.html.haml b/app/views/commencer/show.html.haml
index 55b7a35c9..3c5619bfb 100644
--- a/app/views/commencer/show.html.haml
+++ b/app/views/commencer/show.html.haml
@@ -3,8 +3,8 @@
.commencer.form
- if !user_signed_in?
= render Dsfr::CalloutComponent.new(title: t(".start_procedure"), heading_level: 'h2') do |c|
- - c.with_body do
- = render partial: 'shared/france_connect_login', locals: { url: commencer_france_connect_path(path: @procedure.path, prefill_token: @prefilled_dossier&.prefill_token) }
+ - c.with_html_body do
+ = render partial: 'shared/france_connect_login', locals: { url: commencer_france_connect_path(path: @procedure.path, prefill_token: @prefilled_dossier&.prefill_token), heading_level: :h3 }
%ul.fr-btns-group.fr-btns-group--inline
%li
= link_to commencer_sign_up_path(path: @procedure.path, prefill_token: @prefilled_dossier&.prefill_token), class: 'fr-btn' do
@@ -13,7 +13,11 @@
#{Current.application_name}
%li= link_to t('views.shared.account.already_user'), commencer_sign_in_path(path: @procedure.path, prefill_token: @prefilled_dossier&.prefill_token), class: 'fr-btn fr-btn--secondary'
+ = render ProcedureDraftWarningComponent.new(revision: @revision, current_administrateur:, extra_class_names: "fr-mb-2w")
+
- else
+ = render ProcedureDraftWarningComponent.new(revision: @revision, current_administrateur:, extra_class_names: "fr-mb-2w")
+
- if @prefilled_dossier
= render Dsfr::CalloutComponent.new(title: t(".prefilled_draft"), heading_level: 'h2') do |c|
- c.with_body do
diff --git a/app/views/contact/_form.html.haml b/app/views/contact/_form.html.haml
new file mode 100644
index 000000000..c284df566
--- /dev/null
+++ b/app/views/contact/_form.html.haml
@@ -0,0 +1,60 @@
+= form_for form, url: contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :contact } do |f|
+ %p.fr-hint-text= t('asterisk_html', scope: [:utils])
+
+ - if form.require_email?
+ = render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email' }) do |c|
+ - c.with_label { ContactForm.human_attribute_name(form.for_admin? ? :email_pro : :email) }
+
+ %fieldset.fr-fieldset{ name: "question_type" }
+ %legend.fr-fieldset__legend.fr-fieldset__legend--regular
+ = t('.your_question')
+ = render EditableChamp::AsteriskMandatoryComponent.new
+ .fr-fieldset__content
+ - form.options.each do |(question, question_type, link)|
+ .fr-radio-group
+ = f.radio_button :question_type, question_type, required: true, data: {"contact-target": "inputRadio" }, checked: question_type == form.question_type
+ = f.label "question_type_#{question_type}", { 'aria-controls': link ? "card-#{question_type}" : nil, class: 'fr-label' } do
+ = question
+
+ - if link.present?
+ .fr-ml-3w{ id: "card-#{question_type}",
+ class: class_names('hidden' => question_type != form.question_type),
+ "aria-hidden": question_type != form.question_type,
+ data: { "contact-target": "content" } }
+ = render Dsfr::CalloutComponent.new(title: t('.our_answer')) do |c|
+ - c.with_html_body do
+ -# i18n-tasks-use t("contact.index.#{question_type}.answer_html")
+ = t('answer_html', scope: [:contact, :index, question_type], base_url: Current.application_base_url, "link_#{question_type}": link)
+
+
+ - if form.for_admin?
+ = render Dsfr::InputComponent.new(form: f, attribute: :phone, required: false)
+ - else
+ = render Dsfr::InputComponent.new(form: f, attribute: :dossier_id, required: false)
+
+ = render Dsfr::InputComponent.new(form: f, attribute: :subject)
+
+ = render Dsfr::InputComponent.new(form: f, attribute: :text, input_type: :text_area, opts: { rows: 6 })
+
+ - if !form.for_admin?
+ .fr-upload-group
+ = f.label :piece_jointe, class: 'fr-label' do
+ = t('pj', scope: [:utils])
+ %span.fr-hint-text
+ = t('.notice_upload_group')
+
+ %p.notice.hidden{ data: { 'contact-type-only': ContactForm::TYPE_AMELIORATION } }
+ = t('.notice_pj_product')
+ %p.notice.hidden{ data: { 'contact-type-only': ContactForm::TYPE_AUTRE } }
+ = t('.notice_pj_other')
+ = f.file_field :piece_jointe, class: 'fr-upload', accept: '.jpg, .jpeg, .png, .pdf'
+
+ - f.object.tags.each_with_index do |tag, index|
+ = f.hidden_field :tags, name: f.field_name(:tags, multiple: true), id: f.field_id(:tag, index), value: tag
+
+ = f.hidden_field :for_admin
+
+ = invisible_captcha
+
+ .fr-input-group.fr-my-3w
+ = f.submit t('send_mail', scope: [:utils]), type: :submit, class: 'fr-btn', data: { disable: true }
diff --git a/app/views/contact/admin.html.haml b/app/views/contact/admin.html.haml
new file mode 100644
index 000000000..1771256fc
--- /dev/null
+++ b/app/views/contact/admin.html.haml
@@ -0,0 +1,12 @@
+- content_for(:title, t('.contact_team'))
+- content_for :footer do
+ = render partial: "root/footer"
+
+#contact-form
+ .fr-container
+ %h1
+ = t('.contact_team')
+
+ .fr-highlight= t('.admin_intro_html', contact_path: contact_path)
+
+ = render partial: "form", object: @form
diff --git a/app/views/contact/index.html.haml b/app/views/contact/index.html.haml
new file mode 100644
index 000000000..0a33c87f1
--- /dev/null
+++ b/app/views/contact/index.html.haml
@@ -0,0 +1,12 @@
+- content_for(:title, t('.contact'))
+- content_for :footer do
+ = render partial: "root/footer"
+
+#contact-form
+ .fr-container
+ %h1
+ = t('.contact')
+
+ .fr-highlight= t('.intro_html')
+
+ = render partial: "form", object: @form
diff --git a/app/views/devise/_password_rules.html.haml b/app/views/devise/_password_rules.html.haml
deleted file mode 100644
index 2d4083d2f..000000000
--- a/app/views/devise/_password_rules.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-.fr-messages-group{ "aria-live" => "off", id: id }
- %p.fr-message= t('views.registrations.new.password_message')
- %p.fr-message.fr-message--info= t('views.registrations.new.password_placeholder', min_length: PASSWORD_MIN_LENGTH)
diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml
index 008f56ce7..4227eaea8 100644
--- a/app/views/devise/passwords/edit.html.haml
+++ b/app/views/devise/passwords/edit.html.haml
@@ -12,22 +12,21 @@
= f.hidden_field :reset_password_token
- %fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'edit-password-legend' } }
- %legend.fr-fieldset__legend#edit-password-legend
+ %fieldset.fr-mb-0.fr-fieldset
+ %legend.fr-fieldset__legend
%h1.fr-h2= I18n.t('views.users.passwords.edit.subtitle')
+ .fr-fieldset__element
+ %p.fr-text--sm= t('utils.asterisk_html')
+
.fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field,
- opts: { autofocus: 'true', autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH, data: { controller: populated_resource.validate_password_complexity? ? 'turbo-input' : false, turbo_input_url_value: show_password_complexity_path }}) do |c|
- - c.with_describedby do
- - if populated_resource.validate_password_complexity?
- %div{ id: c.describedby_id }
- #password_complexity
- = render PasswordComplexityComponent.new
- - else
- = render partial: "devise/password_rules", locals: { id: c.describedby_id }
+ opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }, aria: {describedby: 'password_hint'}})
+
+ #password_complexity
+ = render PasswordComplexityComponent.new
.fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :password_confirmation, input_type: :password_field, opts: { autocomplete: 'new-password' })
- = f.submit t('views.users.passwords.edit.submit'), id: 'submit-password', class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }
+ = f.submit t('views.users.passwords.edit.submit'), id: 'submit-password', disabled: :disabled, class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
index e2f68f417..795162cec 100644
--- a/app/views/devise/passwords/new.html.haml
+++ b/app/views/devise/passwords/new.html.haml
@@ -9,8 +9,8 @@
= devise_error_messages!
= form_for(resource, as: resource_name, url: password_path(resource_name)) do |f|
- %fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'new-password-legend' } }
- %legend.fr-fieldset__legend#new-password-legend
+ %fieldset.fr-mb-0.fr-fieldset
+ %legend.fr-fieldset__legend
%h1.fr-h2= t('devise.passwords.new.forgot_your_password')
.fr-fieldset__element
@@ -19,4 +19,4 @@
.fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email', autofocus: true })
- = f.submit t('devise.passwords.new.request_new_password'), class: 'fr-btn fr-btn--lg fr-mt-4w'
+ = f.submit t('devise.passwords.new.request_new_password'), class: 'fr-btn fr-btn--lg fr-mt-4w', 'data-email-input-target': 'next'
diff --git a/app/views/dossier_mailer/notify_automatic_deletion_to_administration.html.haml b/app/views/dossier_mailer/notify_automatic_deletion_to_administration.html.haml
index 7d2cb3e91..f0a18eb0c 100644
--- a/app/views/dossier_mailer/notify_automatic_deletion_to_administration.html.haml
+++ b/app/views/dossier_mailer/notify_automatic_deletion_to_administration.html.haml
@@ -3,9 +3,14 @@
%p= t(:hello, scope: [:views, :shared, :greetings])
%p
- = t('.header', count: @deleted_dossiers.size)
+ = t('.header', count: @hidden_dossiers.size)
%ul
- - @deleted_dossiers.each do |d|
- %li n° #{d.dossier_id} (#{d.procedure.libelle})
+ - @hidden_dossiers.each do |d|
+ %li n° #{d.id} (#{d.procedure.libelle})
+
+%p
+ = t('.footer', count: @hidden_dossiers.size)
+ = link_to("mes dossiers", dossiers_url)
+ \.
= render partial: "layouts/mailers/signature"
diff --git a/app/views/dossier_mailer/notify_automatic_deletion_to_user.html.haml b/app/views/dossier_mailer/notify_automatic_deletion_to_user.html.haml
index 8d2079e12..0da270439 100644
--- a/app/views/dossier_mailer/notify_automatic_deletion_to_user.html.haml
+++ b/app/views/dossier_mailer/notify_automatic_deletion_to_user.html.haml
@@ -3,15 +3,14 @@
%p= t(:hello, scope: [:views, :shared, :greetings])
%p
- = t('.header', count: @deleted_dossiers.size)
+ = t('.header', count: @hidden_dossiers.size)
%ul
- - @deleted_dossiers.each do |d|
- %li N° #{d.dossier_id} (#{d.procedure.libelle})
+ - @hidden_dossiers.each do |d|
+ %li N° #{d.id} (#{d.procedure.libelle})
%p
- %strong= t('.account_active', count: @deleted_dossiers.size)
-
-- if @state == Dossier.states.fetch(:en_construction)
- %p= t('.footer_en_construction', count: @deleted_dossiers.size, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks))
+ = t('.footer', count: @hidden_dossiers.size)
+ = link_to("mes dossiers", dossiers_url)
+ \.
= render partial: "layouts/mailers/signature"
diff --git a/app/views/dossier_mailer/notify_brouillon_near_deletion.html.haml b/app/views/dossier_mailer/notify_brouillon_near_deletion.html.haml
index 2b358eab6..1c70807a3 100644
--- a/app/views/dossier_mailer/notify_brouillon_near_deletion.html.haml
+++ b/app/views/dossier_mailer/notify_brouillon_near_deletion.html.haml
@@ -8,6 +8,8 @@
- @dossiers.each do |d|
%li= link_to("n° #{d.id} (#{d.procedure.libelle})", dossier_url(d))
-%p= sanitize(t('.footer', count: @dossiers.size))
+%p
+ = t('.account_active', count: @dossiers.size)
+
= render partial: "layouts/mailers/signature"
diff --git a/app/views/dossier_mailer/notify_deletion_to_administration.html.haml b/app/views/dossier_mailer/notify_deletion_to_administration.html.haml
index 55da5cfe9..af217d4a3 100644
--- a/app/views/dossier_mailer/notify_deletion_to_administration.html.haml
+++ b/app/views/dossier_mailer/notify_deletion_to_administration.html.haml
@@ -3,6 +3,6 @@
%p= t(:hello, scope: [:views, :shared, :greetings])
%p
- = t('.body', dossier_id: @deleted_dossier.dossier_id, procedure: @deleted_dossier.procedure.libelle)
+ = t('.body', dossier_id: @hidden_dossier.id, procedure: @hidden_dossier.procedure.libelle)
= render partial: "layouts/mailers/signature"
diff --git a/app/views/dossier_mailer/notify_near_deletion_to_administration.html.haml b/app/views/dossier_mailer/notify_near_deletion_to_administration.html.haml
index 1bd80d74b..0ad8ed135 100644
--- a/app/views/dossier_mailer/notify_near_deletion_to_administration.html.haml
+++ b/app/views/dossier_mailer/notify_near_deletion_to_administration.html.haml
@@ -4,18 +4,17 @@
%p
- if @state == Dossier.states.fetch(:en_construction)
- = t('.header_en_construction', count: @dossiers.size)
+ = t('.header_en_construction', count: @dossiers.size, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks))
- else
- = t('.header_termine', count: @dossiers.size)
+ = t('.header_termine', count: @dossiers.size, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks))
+
%ul
- @dossiers.each do |d|
%li
#{link_to("N° #{d.id} (#{d.procedure.libelle})", dossier_url(d))}. Retrouvez le dossier au format #{link_to("PDF", instructeur_dossier_url(d.procedure, d, format: :pdf))}
-%p
- - if @state == Dossier.states.fetch(:en_construction)
- = sanitize(t('.footer_en_construction', count: @dossiers.size, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks)))
- - else
- = sanitize(t('.footer_termine', count: @dossiers.size, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks)))
+- if @state == Dossier.states.fetch(:en_construction)
+ %p
+ = sanitize(t('.footer_en_construction'))
= render partial: "layouts/mailers/signature"
diff --git a/app/views/dossier_mailer/notify_near_deletion_to_user.html.haml b/app/views/dossier_mailer/notify_near_deletion_to_user.html.haml
index be5eeaa32..07fadd71d 100644
--- a/app/views/dossier_mailer/notify_near_deletion_to_user.html.haml
+++ b/app/views/dossier_mailer/notify_near_deletion_to_user.html.haml
@@ -12,13 +12,15 @@
%li
#{link_to("N° #{d.id} (#{d.procedure.libelle})", dossier_url(d))}
-%p
- %strong= t('.account_active', count: @dossiers.size)
-
%p
- if @state == Dossier.states.fetch(:en_construction)
= sanitize(t('.footer_en_construction', count: @dossiers.size, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks)))
- else
= sanitize(t('.footer_termine', count: @dossiers.size, dossiers_url: dossiers_url, remaining_weeks_before_expiration: distance_of_time_in_words(Expired::REMAINING_WEEKS_BEFORE_EXPIRATION.weeks)))
+ = link_to("mes dossiers", dossiers_url)
+ \.
+%p
+ = t('.account_active', count: @dossiers.size)
+
= render partial: "layouts/mailers/signature"
diff --git a/app/views/dossier_mailer/notify_transfer.html.haml b/app/views/dossier_mailer/notify_transfer.html.haml
index fac5aabd2..6ca8e2998 100644
--- a/app/views/dossier_mailer/notify_transfer.html.haml
+++ b/app/views/dossier_mailer/notify_transfer.html.haml
@@ -10,7 +10,13 @@
= dossier.procedure.libelle
%p
- = t('.transfer_text')
- = link_to t('.transfer_link'), dossiers_url(statut: 'dossiers-transferes')
+ - if @user.present?
+ = t('.transfer_text', app_name: Current.application_name)
+ %br
+ = link_to t('.transfer_link'), dossiers_url(statut: 'dossiers-transferes')
+ - else
+ = t('.no_user_transfer_text')
+ %br
+ = link_to t('.no_user_transfer_link', app_name: Current.application_name), new_user_registration_url
= render partial: "layouts/mailers/signature"
diff --git a/app/views/dossiers/dossier_vide.pdf.prawn b/app/views/dossiers/dossier_vide.pdf.prawn
index 266ae50c6..7ff408453 100644
--- a/app/views/dossiers/dossier_vide.pdf.prawn
+++ b/app/views/dossiers/dossier_vide.pdf.prawn
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'prawn/measurement_extensions'
# Render text in a box that expands vertically, then move the cursor down to the end of the rendered text
@@ -161,7 +163,7 @@ def render_single_champ(pdf, revision, type_de_champ)
add_libelle(pdf, type_de_champ)
add_optionnal_description(pdf, type_de_champ)
add_explanation(pdf, 'Cochez la mention applicable, une seule valeur possible')
- type_de_champ.drop_down_list_enabled_non_empty_options.each do |option|
+ type_de_champ.drop_down_options.each do |option|
format_with_checkbox(pdf, option)
end
pdf.text "\n"
@@ -169,7 +171,7 @@ def render_single_champ(pdf, revision, type_de_champ)
add_libelle(pdf, type_de_champ)
add_optionnal_description(pdf, type_de_champ)
add_explanation(pdf, 'Cochez la mention applicable, plusieurs valeurs possibles')
- type_de_champ.drop_down_list_enabled_non_empty_options.each do |option|
+ type_de_champ.drop_down_options.each do |option|
format_with_checkbox(pdf, option)
end
pdf.text "\n"
diff --git a/app/views/dossiers/show.pdf.prawn b/app/views/dossiers/show.pdf.prawn
index 9cbdcdbb2..3bcea71bf 100644
--- a/app/views/dossiers/show.pdf.prawn
+++ b/app/views/dossiers/show.pdf.prawn
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'prawn/measurement_extensions'
def default_margin
diff --git a/app/views/experts/avis/index.html.haml b/app/views/experts/avis/index.html.haml
index ca97bcc37..5dda97742 100644
--- a/app/views/experts/avis/index.html.haml
+++ b/app/views/experts/avis/index.html.haml
@@ -1,8 +1,10 @@
- content_for(:title, "Avis")
-.container
- %h1.page-title Avis
+.sub-header
+ .fr-container
+ %h1.fr-h3 Avis
+.fr-container
%ul.procedure-list.fr-pl-0
- @avis_by_procedure.each do |p, procedure_avis|
%li.flex.align-start.fr-my-3w.fr-p-2w{ id: dom_id(p) }
diff --git a/app/views/experts/avis/show.html.haml b/app/views/experts/avis/show.html.haml
index 3704bedac..7ae1c1e90 100644
--- a/app/views/experts/avis/show.html.haml
+++ b/app/views/experts/avis/show.html.haml
@@ -2,4 +2,9 @@
= render partial: 'header', locals: { avis: @avis, dossier: @dossier }
-= render partial: 'shared/dossiers/demande', locals: { dossier: @dossier, demande_seen_at: nil, profile: 'expert' }
+.fr-container
+ .fr-grid-row.fr-grid-row--center
+ - summary = ViewableChamp::HeaderSectionsSummaryComponent.new(dossier: @dossier, is_private: false)
+ = render summary
+ %div{ class: class_names("fr-col-12", "fr-col-xl-9" => summary.render?, "fr-col-xl-8" => !summary.render?) }
+ = render partial: "shared/dossiers/demande", locals: { dossier: @dossier, demande_seen_at: nil, profile: 'expert' }
diff --git a/app/views/experts/avis/sign_up.html.haml b/app/views/experts/avis/sign_up.html.haml
index c4a8f08fe..72d5da9ea 100644
--- a/app/views/experts/avis/sign_up.html.haml
+++ b/app/views/experts/avis/sign_up.html.haml
@@ -1,20 +1,22 @@
-.two-columns.avis-sign-up
- .columns-container
- .column.left
- %h2.fr-py-5w.text-center= @dossier.procedure.libelle
- %p.dossier Dossier nº #{@dossier.id}
- .column
+.fr-container.fr-my-5w
+ .fr-grid-row.fr-grid-row--center
+ .fr-col-lg-6
= form_for(User.new(email: @email), url: sign_up_expert_avis_path(email: @email), method: :post, html: { class: "fr-py-5w" }) do |f|
- %h1.fr-h2= t('views.registrations.new.title', name: Current.application_name)
+
+ %h1.fr-h2
+ = t('views.registrations.new.title', name: Current.application_name)
%fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'create-account-legend' } }
.fr-fieldset__element
%p.fr-text--sm= t('utils.mandatory_champs')
- .fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { disabled: true, autocomplete: 'email' })
.fr-fieldset__element
- = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH }) do |c|
- - c.with_describedby do
- = render partial: "devise/password_rules", locals: { id: c.describedby_id }
+ = render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { disabled: true, autocomplete: 'email' })
- %ul.fr-btns-group
- %li= f.submit t('views.shared.account.create'), class: "fr-btn"
+ .fr-fieldset__element
+ = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field,
+ opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }, aria: {describedby: 'password_hint'}})
+
+ #password_complexity
+ = render PasswordComplexityComponent.new
+
+ = f.submit t('views.shared.account.create'), id: 'submit-password', disabled: :disabled, class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }
diff --git a/app/views/faq/_breadcrumb.html.haml b/app/views/faq/_breadcrumb.html.haml
new file mode 100644
index 000000000..205b5effb
--- /dev/null
+++ b/app/views/faq/_breadcrumb.html.haml
@@ -0,0 +1,13 @@
+%nav.fr-breadcrumb{ role: "navigation", 'aria-label': t('you_are_here', scope: [:layouts, :breadcrumb]) }
+ %button.fr-breadcrumb__button{ 'aria-expanded' => "false", 'aria-controls' => "breadcrumb-1" }
+ = t('show', scope: [:layouts, :breadcrumb])
+ .fr-collapse#breadcrumb-1
+ %ol.fr-breadcrumb__list
+ %li= link_to t('root', scope: [:layouts, :breadcrumb]), root_path, class: 'fr-breadcrumb__link'
+
+ %li
+ %a.fr-breadcrumb__link{ **(defined?(faq_title) ? { href: faq_index_path } : { "aria-current": "page" }) }= t('faq', scope: [:layouts, :breadcrumb])
+
+ - if defined?(faq_title)
+ %li
+ %a.fr-breadcrumb__link{ 'aria-current' => "page" }= faq_title
diff --git a/app/views/faq/_sidebar.html.haml b/app/views/faq/_sidebar.html.haml
new file mode 100644
index 000000000..d6055490a
--- /dev/null
+++ b/app/views/faq/_sidebar.html.haml
@@ -0,0 +1,20 @@
+%nav.fr-sidemenu.fr-sidemenu--sticky{ role: "navigation", 'aria-labelledby': "fr-sidemenu-title" }
+ .fr-sidemenu__inner
+ %button.fr-sidemenu__btn{ 'aria-controls': "fr-sidemenu-wrapper", 'aria-expanded': "false" }
+ = t(:sidebar_button, scope: [:faq])
+ .fr-collapse#fr-sidemenu-wrapper
+ .fr-sidemenu__title#fr-sidemenu-title
+ = t(:name, scope: [:faq, :categories, current[:category]])
+ %ul.fr-sidemenu__list
+ - faqs.each_with_index do |(subcategory, faqs), index|
+ %li{ class: class_names("fr-sidemenu__item", "fr-sidemenu__item--active" => subcategory == current[:subcategory]) }
+ %button.fr-sidemenu__btn{ aria: { 'expanded': subcategory == current[:subcategory] ? "true" : "false",
+ 'controls': "fr-sidemenu-item-#{index}",
+ 'current' => subcategory == current[:subcategory] ? "true" : nil } }
+ = t(:name, scope: [:faq, :subcategories, subcategory])
+ .fr-collapse{ id: "fr-sidemenu-item-#{index}" }
+ %ul.fr-sidemenu__list
+ - faqs.each do |faq|
+ %li{ class: class_names("fr-sidemenu__item", "fr-sidemenu__item--active" => faq[:slug] == current[:slug]) }
+ = link_to faq[:title], faq_path(category: faq[:category], slug: faq[:slug]),
+ class: 'fr-sidemenu__link', target: "_self", "aria-current" => current[:slug] == faq[:slug] ? "page" : nil
diff --git a/app/views/faq/index.html.haml b/app/views/faq/index.html.haml
new file mode 100644
index 000000000..39ccb8821
--- /dev/null
+++ b/app/views/faq/index.html.haml
@@ -0,0 +1,27 @@
+- content_for(:title, t('.meta_title'))
+
+.fr-container.fr-my-4w
+ = render partial: "breadcrumb"
+ .fr-grid-row
+ .fr-col-12.fr-col-md-10
+ %h1= t('.title', app_name: Current.application_name)
+
+ - @faqs.each do |category, subcategories|
+ %h2= t(:name, scope: [:faq, :categories, category], raise: true) # i18n-tasks-use t("faq.categories.#{category}.name")
+ %p= t(:description, scope: [:faq, :categories, category], raise: true) # i18n-tasks-use t("faq.categories.#{category}.description")
+
+ .fr-accordions-group.fr-mb-6w
+ - subcategories.each_with_index do |(subcategory, faqs), index|
+ %section.fr-accordion
+ %h3.fr-accordion__title
+ %button.fr-accordion__btn{ 'aria-expanded': "false", 'aria-controls': "accordion-#{category}-#{index}" }
+ = t(:name, scope: [:faq, :subcategories, subcategory], raise: true) # i18n-tasks-use t("faq.subcategories.#{subcategory}.name")
+
+ .fr-collapse{ id: "accordion-#{category}-#{index}" }
+ - description = t(:description, scope: [:faq, :subcategories, subcategory], default: nil) # i18n-tasks-use t("faq.subcategories.#{subcategory}.description")
+ - if description
+ %p= description
+
+ %ul
+ - faqs.each do |faq|
+ %li= link_to faq[:title], faq_path(category: faq[:category], slug: faq[:slug]), class: "fr-link"
diff --git a/app/views/faq/show.html.haml b/app/views/faq/show.html.haml
new file mode 100644
index 000000000..45cfb50fb
--- /dev/null
+++ b/app/views/faq/show.html.haml
@@ -0,0 +1,13 @@
+- content_for(:title, @metadata[:title])
+
+.fr-container.fr-my-4w
+
+ .fr-grid-row
+ .fr-col-12.fr-col-md-4
+ = render partial: "sidebar", locals: { faqs: @siblings, current: @metadata }
+ .fr-col-12.fr-col-md-8
+ -# i18n-tasks-use t("faq.categories.#{@metadata[:category]}.short_name")
+ = render partial: "breadcrumb", locals: { faq_title: "#{t(:short_name, scope: [:faq, :categories, @metadata[:category]])} : #{@metadata[:title]}" }
+
+ .markdown-content
+ = @renderer.render(@content).html_safe
diff --git a/app/views/fields/jwt_field/_show.html.erb b/app/views/fields/jwt_field/_show.html.erb
new file mode 100644
index 000000000..6d9dbc907
--- /dev/null
+++ b/app/views/fields/jwt_field/_show.html.erb
@@ -0,0 +1 @@
+<%= field.to_s %>
diff --git a/app/views/france_connect/particulier/_password_confirmation.html.haml b/app/views/france_connect/particulier/_password_confirmation.html.haml
index fd48f3619..6a9ce4bbc 100644
--- a/app/views/france_connect/particulier/_password_confirmation.html.haml
+++ b/app/views/france_connect/particulier/_password_confirmation.html.haml
@@ -1,16 +1,7 @@
-%p
- = t('.already_exists', email: email, application_name: Current.application_name)
- %br
- = t('.fill_in_password')
+= form_tag france_connect_particulier_merge_using_password_path, data: { turbo: true }, class: 'mt-2 form fconnect-form', id: 'merge_using_password' do
+ = hidden_field_tag :merge_token, fci.merge_token, id: dom_id(fci, :fusion_merge_token)
+ .fr-input-group{ class: class_names('fr-input-group--error': wrong_password) }
+ = label_tag :password, t('views.registrations.new.password_label', min_length: 8), class: 'fr-label'
+ = password_field_tag :password, nil, autocomplete: 'current-password', class: 'mb-1 fr-input'
-= form_tag france_connect_particulier_merge_with_existing_account_path, data: { turbo: true, turbo_force: :server }, class: 'mt-2 form fconnect-form' do
- = hidden_field_tag :merge_token, merge_token
- = hidden_field_tag :email, email
- = label_tag :password, t('views.registrations.new.password_label', min_length: 8)
- = password_field_tag :password, nil, autocomplete: 'current-password', id: 'password-for-another-account'
- .mb-2
- = t('views.users.sessions.new.reset_password')
- = link_to france_connect_particulier_resend_and_renew_merge_confirmation_path(merge_token: merge_token), method: :post do
- = t('france_connect.particulier.merge.link_confirm_by_email')
- = button_tag t('.back'), type: 'button', class: 'button secondary', onclick: 'DS.showNewAccount(event);'
- = submit_tag t('france_connect.particulier.merge.button_merge'), class: 'button primary'
+ = submit_tag t('france_connect.particulier.merge.button_merge'), class: 'fr-btn'
diff --git a/app/views/france_connect/particulier/choose_email.html.haml b/app/views/france_connect/particulier/choose_email.html.haml
new file mode 100644
index 000000000..018d51b39
--- /dev/null
+++ b/app/views/france_connect/particulier/choose_email.html.haml
@@ -0,0 +1,43 @@
+.fr-container.fr-my-5w
+ .fr-grid-row.fr-col-offset-md-2.fr-col-md-8
+ .fr-col-12
+
+ %h1.text-center.mt-1= t('.choose_email_contact')
+
+ %p= t('.intro_html', email: @fci.email_france_connect)
+
+ %p= t('.use_email_for_notifications')
+
+ %fieldset.fr-fieldset
+ = form_with url: france_connect_particulier_merge_using_fc_email_path(merge_token: @fci.merge_token), method: :post, data: { controller: 'email-france-connect' } do |f|
+ = hidden_field_tag :merge_token, @fci.merge_token
+
+ %fieldset.fr-fieldset
+ %legend.fr-fieldset__legend
+ .fr-fieldset__element
+ .fr-radio-group
+ = f.radio_button :use_france_connect_email, true, id: 'use_france_connect_email_yes', class: 'fr-radio', required: true, data: { action: "email-france-connect#triggerEmailField", email_france_connect_target: "useFranceConnectEmail" }
+ %label.fr-label.fr-text--wrap{ for: 'use_france_connect_email_yes' }
+ = t('.keep_fc_email_html', email: h(@fci.email_france_connect)).html_safe
+ .fr-fieldset__element
+ .fr-radio-group
+ = f.radio_button :use_france_connect_email, false, id: 'use_france_connect_email_no', class: 'fr-radio', required: true, data: { action: "email-france-connect#triggerEmailField", email_france_connect_target: "useFranceConnectEmail" }
+ %label.fr-label.fr-text--wrap{ for: 'use_france_connect_email_no' }
+ = t('.use_another_email')
+
+ .fr-fieldset__element.fr-fieldset__element--inline.hidden{ aria: { hidden: true }, data: { email_france_connect_target: "emailField", controller: 'email-input', email_input_url_value: show_email_suggestions_path } }
+ = f.label :email, t('.alternative_email'), class: "fr-label"
+ %span.fr-hint-text.mb-1= t('activerecord.attributes.user.hints.email')
+ = f.email_field :email, class: "fr-input"
+
+ .suspect-email.hidden{ data: { "email-input-target": 'ariaRegion'}, aria: { live: 'off' } }
+ = render Dsfr::AlertComponent.new(title: t('utils.email_suggest.wanna_say'), state: :info, heading_level: :div) do |c|
+ - c.with_body do
+ %p{ data: { "email-input-target": 'suggestion'} } exemple@gmail.com ?
+ %p
+ = button_tag type: 'button', class: 'fr-btn fr-btn--sm fr-mr-3w', data: { action: 'click->email-input#accept'} do
+ = t('utils.yes')
+ = button_tag type: 'button', class: 'fr-btn fr-btn--sm', data: { action: 'click->email-input#discard'} do
+ = t('utils.no')
+ %div
+ = f.submit t('.confirm'), class: 'fr-btn'
diff --git a/app/views/france_connect/particulier/confirmation_sent.html.haml b/app/views/france_connect/particulier/confirmation_sent.html.haml
new file mode 100644
index 000000000..98ca08acc
--- /dev/null
+++ b/app/views/france_connect/particulier/confirmation_sent.html.haml
@@ -0,0 +1,12 @@
+.fr-container
+ .fr-col-12.fr-col-md-6.fr-col-offset-md-3
+ %h1.fr-mt-6w.fr-h2.center= t('.confirmation_sent_by_email')
+
+ %p.center= image_tag("user/confirmation-email.svg", alt: '')
+
+ = render Dsfr::AlertComponent.new(title: '', state: :info, heading_level: 'h2', extra_class_names: 'fr-mt-6w fr-mb-3w') do |c|
+ - c.with_body do
+ %p= t('.intro_html', email: h(email)).html_safe
+ %p= t('.click_the_link_in_the_email')
+
+ %p.center= link_to t('.continue'), destination_path, class: 'fr-btn'
diff --git a/app/views/france_connect/particulier/merge.html.haml b/app/views/france_connect/particulier/merge.html.haml
index 0d7b98f3a..c96f9b53e 100644
--- a/app/views/france_connect/particulier/merge.html.haml
+++ b/app/views/france_connect/particulier/merge.html.haml
@@ -1,46 +1,42 @@
= content_for :title, "Fusion des comptes FC et #{Current.application_name}"
-.container
+.fr-container
%h1.page-title= t('.title', application_name: Current.application_name)
%p= t('.subtitle_html', email: @fci.email_france_connect, application_name: Current.application_name)
- .form.mt-2
- %label= t('.label_select_merge_flow', email: @fci.email_france_connect)
- %fieldset.radios
- %label{ onclick: "DS.showFusion(event);" }
- = radio_button_tag :value, true, false, autocomplete: "off", id: 'it-is-mine'
- = t('utils.yes')
+ %fieldset.fr-fieldset{ aria: { labelledby: 'merge-account' } }
+ %legend.fr-fieldset__legend#merge-account= t('.label_select_merge_flow', email: @fci.email_france_connect)
+ .fr-fieldset__element.fr-fieldset__element--inline
+ .fr-radio-group
+ %input{ type: 'radio', id: 'it-is-mine', name: 'value', value: 'true', autocomplete: "off", onclick: "DS.showFusion(event);" }
+ %label{ for: 'it-is-mine' }= t('utils.yes')
+ .fr-fieldset__element.fr-fieldset__element--inline
+ .fr-radio-group
+ %input{ type: 'radio', id: 'it-is-not-mine', name: 'value', value: 'false', autocomplete: "off", onclick: "DS.showNewAccount(event);" }
+ %label{ for: 'it-is-not-mine' }= t('utils.no')
- %label{ onclick: "DS.showNewAccount(event);" }
- = radio_button_tag :value, false, false, autocomplete: "off", id: 'it-is-not-mine'
- = t('utils.no')
.fusion.hidden
%p= t('.title_fill_in_password')
- = form_tag france_connect_particulier_merge_with_existing_account_path, data: { turbo: true }, class: 'mt-2 form fconnect-form' do
- = hidden_field_tag :merge_token, @fci.merge_token, id: dom_id(@fci, :fusion_merge_token)
- = hidden_field_tag :email, @fci.email_france_connect, id: dom_id(@fci, :fusion_email)
- .fr-input-group
- = label_tag :password, t('views.registrations.new.password_label', min_length: 8), class: 'fr-label'
- = password_field_tag :password, nil, autocomplete: 'current-password', class: 'mb-1 fr-input'
- .mb-2
- = t('views.users.sessions.new.reset_password')
- = link_to france_connect_particulier_resend_and_renew_merge_confirmation_path(merge_token: @fci.merge_token), method: :post do
- = t('.link_confirm_by_email')
+ = render partial: 'password_confirmation', locals: { fci: @fci, wrong_password: @wrong_password }
- = submit_tag t('.button_merge'), class: 'fr-btn'
+ .mt-2
+ = button_to t('.link_confirm_by_email'),
+ france_connect_particulier_send_email_merge_request_path,
+ params: { email: @fci.email_france_connect, merge_token: @fci.merge_token },
+ class: 'fr-btn fr-btn--secondary'
.new-account.hidden
%p= t('.title_fill_in_email', application_name: Current.application_name)
- = form_tag france_connect_particulier_merge_with_new_account_path, data: { turbo: true }, class: 'mt-2 form' do
+ = form_tag france_connect_particulier_send_email_merge_request_path, class: 'mt-2 form' do
= hidden_field_tag :merge_token, @fci.merge_token, id: dom_id(@fci, :new_account_merge_token)
- = label_tag :email, t('views.registrations.new.email_label'), for: dom_id(@fci, :new_account_email)
- = email_field_tag :email, "", required: true, id: dom_id(@fci, :new_account_email)
- = submit_tag t('.button_use_this_email'), class: 'button primary'
+ = label_tag :email, t('views.registrations.new.email_label'), for: dom_id(@fci, :new_account_email), class: 'fr-label'
+ = email_field_tag :email, "", required: true, id: dom_id(@fci, :new_account_email), class: 'mb-1 fr-input'
+ = submit_tag t('.button_use_this_email'), class: 'fr-btn'
#new-account-password-confirmation.hidden
diff --git a/app/views/france_connect/particulier/merge_using_password.turbo_stream.haml b/app/views/france_connect/particulier/merge_using_password.turbo_stream.haml
new file mode 100644
index 000000000..d3985f5c2
--- /dev/null
+++ b/app/views/france_connect/particulier/merge_using_password.turbo_stream.haml
@@ -0,0 +1 @@
+= turbo_stream.replace('merge_using_password', partial: 'password_confirmation', locals: { fci: @fci, wrong_password: true })
diff --git a/app/views/france_connect/particulier/merge_with_existing_account.turbo_stream.haml b/app/views/france_connect/particulier/merge_with_existing_account.turbo_stream.haml
deleted file mode 100644
index e69de29bb..000000000
diff --git a/app/views/france_connect/particulier/merge_with_new_account.turbo_stream.haml b/app/views/france_connect/particulier/merge_with_new_account.turbo_stream.haml
deleted file mode 100644
index 7d14ef01a..000000000
--- a/app/views/france_connect/particulier/merge_with_new_account.turbo_stream.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-= turbo_stream.update 'new-account-password-confirmation', partial: 'password_confirmation', locals: { email: @email, merge_token: @merge_token }
-= turbo_stream.hide_all '.fusion'
-= turbo_stream.hide_all '.new-account'
-= turbo_stream.show 'new-account-password-confirmation'
diff --git a/app/views/gestionnaires/activate/new.html.haml b/app/views/gestionnaires/activate/new.html.haml
index c020d67bc..5f91c40ea 100644
--- a/app/views/gestionnaires/activate/new.html.haml
+++ b/app/views/gestionnaires/activate/new.html.haml
@@ -18,9 +18,9 @@
.fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field,
- opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }})
+ opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }, aria: {describedby: 'password_hint'}})
#password_complexity
= render PasswordComplexityComponent.new
- = f.submit t('.continue'), id: 'submit-password', class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }
+ = f.submit t('.continue'), id: 'submit-password', disabled: :disabled, class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }
diff --git a/app/views/gestionnaires/groupe_gestionnaires/_main_navigation.html.haml b/app/views/gestionnaires/groupe_gestionnaires/_main_navigation.html.haml
index 842b52017..f239b1ebc 100644
--- a/app/views/gestionnaires/groupe_gestionnaires/_main_navigation.html.haml
+++ b/app/views/gestionnaires/groupe_gestionnaires/_main_navigation.html.haml
@@ -1,4 +1,4 @@
- content_for(:main_navigation) do
- %nav#header-navigation.fr-nav{ role: 'navigation', 'aria-label': 'Menu principal gestionnaire' }
+ #header-navigation.fr-nav
%ul.fr-nav__list
%li.fr-nav__item= link_to 'Mes groupes gestionnaires', gestionnaire_groupe_gestionnaires_path, class:'fr-nav__link', 'aria-current': current_page?(controller: 'groupe_gestionnaires', action: :index) ? 'page' : nil
diff --git a/app/views/instructeur_mailer/confirm_and_notify_added_instructeur.html.haml b/app/views/instructeur_mailer/confirm_and_notify_added_instructeur.html.haml
new file mode 100644
index 000000000..9fd94075e
--- /dev/null
+++ b/app/views/instructeur_mailer/confirm_and_notify_added_instructeur.html.haml
@@ -0,0 +1,24 @@
+%p= t(:hello, scope: [:views, :shared, :greetings])
+
+%p
+ - number_of_groups = @group.procedure.groupe_instructeurs.many? ? 'many_groups' : 'one_group'
+ = t(".email_body.#{number_of_groups}", groupe: @group.label, email: @current_instructeur_email, procedure: @group.procedure.libelle)
+
+%p
+ Votre compte a été créé pour l'adresse email
+ %strong #{@instructeur.email}.
+
+%p
+ Pour l’activer, cliquez sur le lien suivant :
+ = link_to(users_activate_url(token: @reset_password_token), users_activate_url(token: @reset_password_token))
+
+%p
+ Lors de vos prochaines connexions sur #{Current.application_name} cliquez sur le bouton « Se connecter » positionné sur le haut de page ou bien sur ce lien :
+ = link_to new_user_session_url, new_user_session_url
+
+%p
+ Nous vous invitons aussi à consulter notre tutoriel à destination des nouveaux instructeurs :
+ = link_to(INSTRUCTEUR_TUTORIAL_URL, INSTRUCTEUR_TUTORIAL_URL)
+
+
+= render partial: "layouts/mailers/signature"
diff --git a/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml b/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml
index b60bf1c32..9c5a6bb78 100644
--- a/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml
+++ b/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml
@@ -7,12 +7,7 @@
%p.tab-paragrah.mb-1
Le destinataire suivra automatiquement le dossier
= form_for dossier, url: send_to_instructeurs_instructeur_dossier_path(dossier.procedure, dossier), method: :post, html: { class: 'form recipients-form fr-mb-4w' } do |f|
- = hidden_field_tag :recipients, nil
- = react_component("ComboMultiple",
- options: potential_recipients.map{|r| [r.email, r.id]},
- selected: [], disabled: [],
- group: '.recipients-form',
- name: 'recipients',
- label: 'Emails')
+ %react-fragment
+ = render ReactComponent.new "ComboBox/MultiComboBox", items: potential_recipients.map { [_1.email, _1.id] }, name: 'recipients[]', 'aria-label': 'Emails'
= f.submit "Envoyer", class: "fr-btn fr-mt-2w"
diff --git a/app/views/instructeurs/dossiers/_expiration_banner.html.haml b/app/views/instructeurs/dossiers/_expiration_banner.html.haml
index 1e5c918cf..5aa622148 100644
--- a/app/views/instructeurs/dossiers/_expiration_banner.html.haml
+++ b/app/views/instructeurs/dossiers/_expiration_banner.html.haml
@@ -6,8 +6,9 @@
- if dossier.conservation_extension.positive?
= t('instructeurs.dossiers.header.banner.expiration_date_extended')
- - if dossier.close_to_expiration?
- = render Dsfr::CalloutComponent.new(title: t('instructeurs.dossiers.header.banner.title'), theme: :warning) do |c|
+ - if dossier.close_to_expiration? || dossier.has_expired?
+ - title = dossier.has_expired? ? 'title_expired' : 'title'
+ = render Dsfr::CalloutComponent.new(title: t("instructeurs.dossiers.header.banner.#{title}"), theme: :warning) do |c|
- c.with_body do
- if dossier.brouillon?
= t('instructeurs.dossiers.header.banner.states.brouillon')
diff --git a/app/views/instructeurs/dossiers/_header.html.haml b/app/views/instructeurs/dossiers/_header.html.haml
index 90e146b58..6432f87aa 100644
--- a/app/views/instructeurs/dossiers/_header.html.haml
+++ b/app/views/instructeurs/dossiers/_header.html.haml
@@ -7,7 +7,7 @@
.sub-header
= render partial: 'instructeurs/dossiers/header_top', locals: { dossier: }
- = render partial: 'instructeurs/dossiers/header_bottom', locals: { dossier: }
+ = render partial: 'instructeurs/dossiers/header_bottom', locals: { dossier:, gallery_attachments: }
.fr-container
.print-header
diff --git a/app/views/instructeurs/dossiers/_header_actions.html.haml b/app/views/instructeurs/dossiers/_header_actions.html.haml
index f13702df7..69938b91d 100644
--- a/app/views/instructeurs/dossiers/_header_actions.html.haml
+++ b/app/views/instructeurs/dossiers/_header_actions.html.haml
@@ -8,6 +8,7 @@
dossier_is_followed: current_instructeur&.follow?(dossier),
close_to_expiration: dossier.close_to_expiration?,
hidden_by_administration: dossier.hidden_by_administration?,
+ hidden_by_expired: dossier.hidden_by_expired?,
has_pending_correction: dossier.pending_correction?,
has_blocking_pending_correction: dossier.procedure.feature_enabled?(:blocking_pending_correction) && dossier.pending_correction?,
turbo: true,
diff --git a/app/views/instructeurs/dossiers/_header_bottom.html.haml b/app/views/instructeurs/dossiers/_header_bottom.html.haml
index 57ea40455..b2cba4d8f 100644
--- a/app/views/instructeurs/dossiers/_header_bottom.html.haml
+++ b/app/views/instructeurs/dossiers/_header_bottom.html.haml
@@ -7,6 +7,11 @@
instructeur_dossier_path(dossier.procedure, dossier),
notification: notifications_summary[:demande])
+ - if gallery_attachments.present?
+ = dynamic_tab_item(t('views.instructeurs.dossiers.tab_steps.attachments'),
+ pieces_jointes_instructeur_dossier_path(dossier.procedure, dossier),
+ notification: notifications_summary[:pieces_jointes])
+
= dynamic_tab_item(t('views.instructeurs.dossiers.tab_steps.private_annotations'),
annotations_privees_instructeur_dossier_path(dossier.procedure, dossier),
notification: notifications_summary[:annotations_privees])
diff --git a/app/views/instructeurs/dossiers/_header_top.html.haml b/app/views/instructeurs/dossiers/_header_top.html.haml
index 765abe36b..7fe1fb2f8 100644
--- a/app/views/instructeurs/dossiers/_header_top.html.haml
+++ b/app/views/instructeurs/dossiers/_header_top.html.haml
@@ -1,11 +1,11 @@
#header-top.fr-container
- .flex.fr-mb-3w
+ .flex
%div
%h1.fr-h3.fr-mb-1w
= "Dossier nº #{dossier.id}"
= link_to dossier.procedure.libelle.truncate_words(10), instructeur_procedure_path(dossier.procedure), title: dossier.procedure.libelle, class: "fr-link"
- .fr-mt-2w.badge-group
+ .fr-mt-2w.fr-badge-group
= procedure_badge(dossier.procedure)
= status_badge(dossier.state)
@@ -16,7 +16,6 @@
= render Instructeurs::SVASVRDecisionBadgeComponent.new(projection_or_dossier: dossier, procedure: dossier.procedure, with_label: true)
-
.header-actions.fr-ml-auto
= render partial: 'instructeurs/dossiers/header_actions', locals: { dossier: }
= render partial: 'instructeurs/dossiers/print_and_export_actions', locals: { dossier: }
@@ -26,3 +25,30 @@
- if dossier.user_deleted?
%p.fr-mb-1w
%small L’usager a supprimé son compte. Vous pouvez archiver puis supprimer le dossier.
+
+ - if dossier.procedure.labels.present?
+ .fr-mb-3w
+ - if dossier.labels.present?
+ - dossier.labels.each do |label|
+ = tag_label(label.name, label.color)
+
+ = render Dropdown::MenuComponent.new(wrapper: :span, button_options: { class: ['fr-btn--sm fr-btn--tertiary-no-outline fr-pl-1v']}, menu_options: { class: ['dropdown-label left-aligned'] }) do |menu|
+ - if dossier.labels.empty?
+ - menu.with_button_inner_html do
+ Ajouter un label
+
+ - menu.with_form do
+ = form_with(url: dossier_labels_instructeur_dossier_path(dossier_id: dossier.id, procedure_id: dossier.procedure.id), method: :post, class: 'fr-p-3w', data: { controller: 'autosubmit', turbo: 'true' }) do |f|
+ %fieldset.fr-fieldset.fr-mt-2w.fr-mb-0
+ = f.collection_check_boxes :label_id, dossier.procedure.labels, :id, :name, include_hidden: false do |b|
+ .fr-fieldset__element
+ .fr-checkbox-group.fr-checkbox-group--sm.fr-mb-1w
+ = b.check_box(checked: DossierLabel.find_by(dossier_id: dossier.id, label_id: b.value).present? )
+ = b.label(class: "fr-label fr-tag fr-tag--sm fr-tag--#{Label.colors.fetch(b.object.color)}") { b.text }
+
+ %hr
+ %p.fr-text--sm.fr-text-mention--grey.fr-mb-0
+ %b Besoin d'autres labels ?
+ %br
+ Contactez les
+ = link_to 'administrateurs de la démarche', administrateurs_instructeur_procedure_path(dossier.procedure), class: 'fr-link fr-link--sm', **external_link_attributes
diff --git a/app/views/instructeurs/dossiers/_instruction_button_motivation.html.haml b/app/views/instructeurs/dossiers/_instruction_button_motivation.html.haml
index 76b4d576e..56c83c685 100644
--- a/app/views/instructeurs/dossiers/_instruction_button_motivation.html.haml
+++ b/app/views/instructeurs/dossiers/_instruction_button_motivation.html.haml
@@ -12,7 +12,7 @@
- if unspecified_attestation_champs.present?
.warning
Attention, les valeurs suivantes n’ont pas été renseignées mais sont nécessaires pour pouvoir envoyer une attestation valide :
- - unspecified_annotations_privees, unspecified_champs = unspecified_attestation_champs.partition(&:private)
+ - unspecified_annotations_privees, unspecified_champs = unspecified_attestation_champs.partition(&:private?)
- if unspecified_champs.present?
%h4 Champs de la demande
diff --git a/app/views/instructeurs/dossiers/annotations_privees.html.haml b/app/views/instructeurs/dossiers/annotations_privees.html.haml
index 3212cac3c..f603d753a 100644
--- a/app/views/instructeurs/dossiers/annotations_privees.html.haml
+++ b/app/views/instructeurs/dossiers/annotations_privees.html.haml
@@ -1,6 +1,11 @@
- content_for(:title, "Annotations privées · Dossier nº #{@dossier.id} (#{@dossier.owner_name})")
-= render partial: "header", locals: { dossier: @dossier }
+= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments }
#dossier-annotations-privees
- = render partial: "shared/dossiers/edit_annotations", locals: { dossier: @dossier, seen_at: @annotations_privees_seen_at }
+ .fr-container
+ .fr-grid-row.fr-grid-row--center
+ - summary = ViewableChamp::HeaderSectionsSummaryComponent.new(dossier: @dossier, is_private: true)
+ = render summary
+ %div{ class: class_names("fr-col-12", "fr-col-xl-9" => summary.render?, "fr-col-xl-8" => !summary.render?) }
+ = render partial: "shared/dossiers/edit_annotations", locals: { dossier: @dossier, seen_at: @annotations_privees_seen_at }
diff --git a/app/views/instructeurs/dossiers/avis.html.haml b/app/views/instructeurs/dossiers/avis.html.haml
index 46e608f9e..172816591 100644
--- a/app/views/instructeurs/dossiers/avis.html.haml
+++ b/app/views/instructeurs/dossiers/avis.html.haml
@@ -1,6 +1,6 @@
- content_for(:title, "Avis · Dossier nº #{@dossier.id} (#{@dossier.owner_name})")
-= render partial: "header", locals: { dossier: @dossier }
+= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments }
.container
.fr-grid-row
diff --git a/app/views/instructeurs/dossiers/avis_new.html.haml b/app/views/instructeurs/dossiers/avis_new.html.haml
index 143fe618d..6499ad2be 100644
--- a/app/views/instructeurs/dossiers/avis_new.html.haml
+++ b/app/views/instructeurs/dossiers/avis_new.html.haml
@@ -1,6 +1,6 @@
- content_for(:title, "Avis · Dossier nº #{@dossier.id} (#{@dossier.owner_name})")
-= render partial: "header", locals: { dossier: @dossier }
+= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments }
.container
.fr-grid-row
diff --git a/app/views/instructeurs/dossiers/messagerie.html.haml b/app/views/instructeurs/dossiers/messagerie.html.haml
index f98fbde63..212d521c8 100644
--- a/app/views/instructeurs/dossiers/messagerie.html.haml
+++ b/app/views/instructeurs/dossiers/messagerie.html.haml
@@ -1,5 +1,5 @@
- content_for(:title, "Messagerie · Dossier nº #{@dossier.id} (#{@dossier.owner_name})")
-= render partial: "header", locals: { dossier: @dossier }
+= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments }
= render partial: "shared/dossiers/messagerie", locals: { dossier: @dossier, connected_user: current_instructeur, messagerie_seen_at: @messagerie_seen_at , new_commentaire: @commentaire, form_url: commentaire_instructeur_dossier_path(@dossier.procedure, @dossier) }
diff --git a/app/views/instructeurs/dossiers/personnes_impliquees.html.haml b/app/views/instructeurs/dossiers/personnes_impliquees.html.haml
index b90952e91..30d3224ec 100644
--- a/app/views/instructeurs/dossiers/personnes_impliquees.html.haml
+++ b/app/views/instructeurs/dossiers/personnes_impliquees.html.haml
@@ -1,6 +1,6 @@
- content_for(:title, "Personnes impliquées · Dossier nº #{@dossier.id} (#{@dossier.owner_name})")
-= render partial: "header", locals: { dossier: @dossier }
+= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments }
.personnes-impliquees.container
= render partial: 'instructeurs/dossiers/envoyer_dossier_block', locals: { dossier: @dossier, potential_recipients: @potential_recipients }
diff --git a/app/views/instructeurs/dossiers/pieces_jointes.html.haml b/app/views/instructeurs/dossiers/pieces_jointes.html.haml
new file mode 100644
index 000000000..527b2c65c
--- /dev/null
+++ b/app/views/instructeurs/dossiers/pieces_jointes.html.haml
@@ -0,0 +1,8 @@
+- content_for(:title, "Pièces jointes")
+
+= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments }
+
+.fr-container
+ .gallery.gallery-pieces-jointes{ "data-controller": "lightbox" }
+ - @gallery_attachments.each do |attachment|
+ = render Attachment::GalleryItemComponent.new(attachment:, seen_at: @pieces_jointes_seen_at)
diff --git a/app/views/instructeurs/dossiers/reaffectation.html.haml b/app/views/instructeurs/dossiers/reaffectation.html.haml
index 5b5307592..364b5415a 100644
--- a/app/views/instructeurs/dossiers/reaffectation.html.haml
+++ b/app/views/instructeurs/dossiers/reaffectation.html.haml
@@ -1,6 +1,6 @@
- content_for(:title, "Réaffectation · Dossier nº #{@dossier.id} (#{@dossier.owner_name})")
-= render partial: "header", locals: { dossier: @dossier }
+= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments }
.container.groupe-instructeur
diff --git a/app/views/instructeurs/dossiers/show.html.haml b/app/views/instructeurs/dossiers/show.html.haml
index cbb8f7833..8c9ad55e4 100644
--- a/app/views/instructeurs/dossiers/show.html.haml
+++ b/app/views/instructeurs/dossiers/show.html.haml
@@ -1,6 +1,6 @@
- content_for(:title, "Demande · Dossier nº #{@dossier.id} (#{@dossier.owner_name})")
-= render partial: "header", locals: { dossier: @dossier }
+= render partial: "header", locals: { dossier: @dossier, gallery_attachments: @gallery_attachments }
- if @dossier.etablissement&.as_degraded_mode?
@@ -14,4 +14,9 @@
%p
Les informations sur l'entreprise arriveront d’ici quelques heures.
-= render partial: "shared/dossiers/demande", locals: { dossier: @dossier, demande_seen_at: @demande_seen_at, profile: 'instructeur' }
+.fr-container
+ .fr-grid-row.fr-grid-row--center
+ - summary = ViewableChamp::HeaderSectionsSummaryComponent.new(dossier: @dossier, is_private: false)
+ = render summary
+ %div{ class: class_names("fr-col-12", "fr-col-xl-9" => summary.render?, "fr-col-xl-8" => !summary.render?) }
+ = render partial: "shared/dossiers/demande", locals: { dossier: @dossier, demande_seen_at: @demande_seen_at, profile: 'instructeur' }
diff --git a/app/views/instructeurs/export_templates/_checkbox_group.html.haml b/app/views/instructeurs/export_templates/_checkbox_group.html.haml
new file mode 100644
index 000000000..80626d857
--- /dev/null
+++ b/app/views/instructeurs/export_templates/_checkbox_group.html.haml
@@ -0,0 +1,14 @@
+%fieldset.fr-fieldset{ id: "#{title.parameterize}-fieldset", data: { controller: 'checkbox-select-all' } }
+ %legend.fr-fieldset__legend--regular.fr-fieldset__legend.fr-h5.fr-pb-0
+ = title
+
+ .checkbox-group-bordered.fr-mx-1w.fr-mb-2w
+ .fr-fieldset__element.fr-background-contrast--grey.fr-py-2w.fr-px-4w
+ .fr-checkbox-group
+ = check_box_tag "#{title.parameterize}-select-all", "select-all", false, data: { "checkbox-select-all-target": 'checkboxAll' }
+ = label_tag "#{title.parameterize}-select-all", "Tout sélectionner"
+
+ - all_columns.each do |column|
+ .fr-fieldset__element.fr-px-4w
+ .fr-checkbox-group
+ = render ExportTemplate::CheckboxComponent.new(export_template:, exported_column: ExportedColumn.new(libelle: column.label, column:))
diff --git a/app/views/instructeurs/export_templates/_export_item.html.haml b/app/views/instructeurs/export_templates/_export_item.html.haml
new file mode 100644
index 000000000..6ab854adf
--- /dev/null
+++ b/app/views/instructeurs/export_templates/_export_item.html.haml
@@ -0,0 +1,26 @@
+.card.no-list
+ = hidden_field_tag("#{prefix}[stable_id]", item.stable_id)
+
+ .fr-checkbox-group{ data: { controller: 'hide-target' } }
+ - id = sanitize_to_id("#{prefix}_#{item.stable_id}_enabled")
+ = check_box_tag "#{prefix}[enabled]", true, item.enabled?, id:, data: { 'hide-target_target': 'source' }
+ = label_tag id, libelle, class: 'fr-label'
+
+ %div{ class: class_names('fr-hidden': !item.enabled?), data: { hide_target_target: 'toHide' } }
+ %div{ data: { controller: 'hide-target tiptap-to-template'} }
+ .fr-mt-2w{ data: { hide_target_target: 'toHide' } }
+ %span Nom du fichier :
+ %span{ data: { 'tiptap-to-template_target': 'output'} }= sanitize(item.template_string)
+ .fr-mt-2w
+ %button.fr-btn.fr-btn--tertiary.fr-btn--sm{ type: 'button', data: { 'hide-target_target': 'source' } } Renommer le fichier
+
+ .fr-mt-2w.fr-hidden{ data: { controller: 'tiptap', 'tiptap-attributes-value': { spellcheck: false }.to_json, hide_target_target: 'toHide' } }
+ %span Renommer le fichier :
+ .fr-mt-2w.tiptap-editor{ data: { tiptap_target: 'editor' } }
+ = hidden_field_tag "#{prefix}[template]", item.template_json, data: { tiptap_target: 'input' }, id: nil
+
+ .fr-mt-2w
+ %span.fr-text--sm Cliquez sur les étiquettes que vous souhaitez intégrer au nom du fichier
+ .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.pj_tags })
+
+ = button_tag "Valider", type: 'button', class: 'fr-btn fr-mt-2w', data: { 'tiptap-to-template_target': 'trigger', 'hide-target_target': 'source'}
diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml
new file mode 100644
index 000000000..629484f07
--- /dev/null
+++ b/app/views/instructeurs/export_templates/_form.html.haml
@@ -0,0 +1,90 @@
+- procedure = @export_template.procedure
+
+#export_template-edit.fr-my-4w
+ .fr-mb-6w
+ = render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur de modèle d'export", heading_level: 'h3') do |c|
+ - c.with_body do
+ Cette page permet d'éditer un modèle d'export et ainsi personnaliser le contenu des exports (pour l'instant,
+ uniquement au format zip). Ainsi, vous pouvez notamment normaliser le nom des pièces jointes.
+ Essayez-le et donnez-nous votre avis
+ en nous envoyant un email à #{mail_to(CONTACT_EMAIL, subject: "Editeur de modèle d'export")}.
+
+ .fr-grid-row.fr-grid-row--gutters
+ .fr-col-12.fr-col-md-8.fr-pr-4w
+ = form_with model: [:instructeur, procedure, export_template], data: { turbo: 'true', controller: 'autosubmit' } do |f|
+ %input.hidden{ type: 'submit', formaction: preview_instructeur_procedure_export_templates_path, data: { autosubmit_target: 'submitter' }, formnovalidate: 'true', formmethod: 'put' }
+
+ = f.hidden_field :kind, value: 'zip'
+
+ = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field)
+
+ .fr-input-group{ class: class_names('fr-hidden': groupe_instructeurs.one?) }
+ = f.label :groupe_instructeur_id, class: 'fr-label' do
+ = "#{ExportTemplate.human_attribute_name('groupe_instructeur_id')} #{asterisk}"
+ %span.fr-hint-text Avec quel groupe instructeur souhaitez-vous partager ce modèle d'export ?
+ = f.collection_select :groupe_instructeur_id, groupe_instructeurs, :id, :label, {}, class: 'fr-select'
+
+ .fr-input-group{ data: { controller: 'tiptap', 'tiptap-attributes-value': { spellcheck: false }.to_json } }
+ = f.label '[dossier_folder][template]', class: "fr-label" do
+ = "#{ExportTemplate.human_attribute_name('dossier_folder')} #{asterisk}"
+ %span.fr-hint-text Nom du répertoire contenant les différents fichiers à exporter
+ .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } }
+ = f.hidden_field "[dossier_folder][template]", data: { tiptap_target: 'input' }, value: export_template.dossier_folder.template_json
+ = f.hidden_field "[dossier_folder][enabled]", value: 'true'
+ .fr-mt-2w
+ %span.fr-text--sm Cliquez sur les étiquettes que vous souhaitez intégrer au nom du fichier
+ .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => export_template.tags })
+
+ = render Dsfr::NoticeComponent.new(data_attributes: { class: 'fr-my-4w' }) do |c|
+ - c.with_title do
+ Sélectionnez les fichiers que vous souhaitez exporter
+
+ %h3 Dossier au format PDF
+ = render partial: 'export_item',
+ locals: { item: export_template.export_pdf,
+ libelle: ExportTemplate.human_attribute_name(:export_pdf),
+ prefix: 'export_template[export_pdf]' }
+
+ - if procedure.exportables_pieces_jointes_for_all_versions.any?
+ %h3 Pièces justificatives
+
+ - procedure.exportables_pieces_jointes.each do |tdc|
+ - item = export_template.pj(tdc)
+ = render partial: 'export_item',
+ locals: { item:,
+ libelle: tdc.libelle,
+ prefix: 'export_template[pjs][]'}
+
+ - outdated_tdcs = procedure.outdated_exportables_pieces_jointes
+ - outdated_stable_ids = outdated_tdcs.map(&:stable_id)
+ - expanded = export_template.pjs.filter(&:enabled?).any? { _1.stable_id.in?(outdated_stable_ids) }
+
+ - if outdated_tdcs.any?
+ %section.fr-accordion.fr-mb-3w
+ %h3.fr-accordion__title
+ %button.fr-accordion__btn{ "aria-controls" => "accordion-106", "aria-expanded" => expanded.to_s, "type" => "button" }
+ pièces justificatives uniquement présentes dans les versions précédentes
+ .fr-collapse#accordion-106
+
+ - outdated_tdcs.each do |tdc|
+ - item = export_template.pj(tdc)
+ = render partial: 'export_item',
+ locals: { item:,
+ libelle: tdc.libelle,
+ prefix: 'export_template[pjs][]'}
+
+ .fixed-footer
+ .fr-container
+ %ul.fr-btns-group.fr-btns-group--inline-md
+ %li= f.button "Enregistrer", class: "fr-btn", data: { turbo: 'false' }
+ %li= link_to "Annuler", [:exports, :instructeur, procedure], class: "fr-btn fr-btn--secondary"
+ - if export_template.persisted?
+ %li
+ = link_to "Supprimer",
+ [:instructeur, procedure, export_template],
+ method: :delete,
+ data: { confirm: "Voulez-vous vraiment supprimer ce modèle ? Il sera supprimé pour tous les instructeurs du groupe"},
+ class: "fr-btn fr-btn--secondary"
+
+ .fr-col-12.fr-col-md-4.fr-background-alt--blue-france
+ = render partial: 'preview', locals: { export_template: }
diff --git a/app/views/instructeurs/export_templates/_form_tabular.html.haml b/app/views/instructeurs/export_templates/_form_tabular.html.haml
new file mode 100644
index 000000000..36712f6f0
--- /dev/null
+++ b/app/views/instructeurs/export_templates/_form_tabular.html.haml
@@ -0,0 +1,57 @@
+#export_template-edit.fr-my-4w
+ .fr-mb-6w
+ = render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur de modèle d'export", heading_level: 'h3') do |c|
+ - c.with_body do
+ = t('.info_html', mailto: mail_to(CONTACT_EMAIL, subject: 'Editeur de modèle d\'export'))
+
+.fr-grid-row.fr-grid-row--gutters
+ .fr-col-12.fr-col-md-8
+ = form_with model: [:instructeur, @procedure, export_template], local: true do |f|
+
+ %h2 Paramètres de l'export
+ = f.hidden_field "[dossier_folder][template]", value: export_template.dossier_folder.template_json
+ = f.hidden_field "[export_pdf][template]", value: export_template.export_pdf.template_json
+
+ = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field)
+
+ - if groupe_instructeurs.many?
+ .fr-input-group
+ = f.label :groupe_instructeur_id, class: 'fr-label' do
+ = f.object.class.human_attribute_name(:groupe_instructeur_id)
+ = render EditableChamp::AsteriskMandatoryComponent.new
+ %span.fr-hint-text
+ Avec quel groupe instructeur souhaitez-vous partager ce modèle d'export ?
+ = f.collection_select :groupe_instructeur_id, groupe_instructeurs, :id, :label, {}, class: 'fr-select'
+ - else
+ = f.hidden_field :groupe_instructeur_id
+
+ %fieldset.fr-fieldset.fr-fieldset--inline
+ %legend#radio-inline-legend.fr-fieldset__legend.fr-text--regular
+ Format export
+ = asterisk
+ .fr-fieldset__element.fr-fieldset__element--inline
+ .fr-radio-group
+ = f.radio_button :kind, "xlsx", id: "xlsx"
+ %label.fr-label{ for: "xlsx" } xlsx
+ .fr-radio-group
+ = f.radio_button :kind, "ods", id: "ods"
+ %label.fr-label{ for: "ods" } ods
+ .fr-radio-group
+ = f.radio_button :kind, "csv", id: "csv"
+ %label.fr-label{ for: "csv" } csv
+
+ %h2 Contenu de l'export
+ %p Sélectionnez les colonnes que vous souhaitez voir affichées dans le tableau de votre export.
+
+ = render partial: 'checkbox_group', locals: { title: 'Informations usager', all_columns: @export_template.procedure.usager_columns_for_export, export_template: @export_template }
+ = render partial: 'checkbox_group', locals: { title: 'Informations dossier', all_columns: @export_template.procedure.dossier_columns_for_export, export_template: @export_template }
+ = render ExportTemplate::ChampsComponent.new("Formulaire usager", @export_template, @types_de_champ_public)
+ = render ExportTemplate::ChampsComponent.new("Annotations privées", @export_template, @types_de_champ_private) if @types_de_champ_private.any?
+
+ .fixed-footer
+ .fr-container
+ %ul.fr-btns-group.fr-btns-group--inline-md
+ %li
+ = link_to "Annuler", instructeur_procedure_path(@procedure), class: "fr-btn fr-btn--secondary"
+ %li
+ = f.submit "Enregistrer", class: "fr-btn", data: @export_template.persisted? ? { confirm: t('.warning') } : {}
diff --git a/app/views/instructeurs/export_templates/_preview.html.haml b/app/views/instructeurs/export_templates/_preview.html.haml
new file mode 100644
index 000000000..0df0548e0
--- /dev/null
+++ b/app/views/instructeurs/export_templates/_preview.html.haml
@@ -0,0 +1,33 @@
+- procedure = export_template.procedure
+- dossier = procedure.dossier_for_preview(current_instructeur)
+
+#preview.export-template-preview.fr-p-2w.sticky--top
+ %h2.fr-h4 Aperçu
+ - if dossier.nil?
+ %p.fr-text--sm
+ Pour générer un aperçu fidèle avec tous les champs et les dates,
+ = link_to 'créez-vous un dossier', commencer_url(procedure.path), target: '_blank'
+ et acceptez-le : l’aperçu l’utilisera.
+
+ - else
+ %ul.tree.fr-text--sm
+ %li
+ %span.fr-icon-folder-zip-line
+ #{DownloadableFileService::EXPORT_DIRNAME}/
+ %li
+ %ul
+ %li
+ %span.fr-icon-folder-line
+ #{export_template.dossier_folder.path(dossier)}/
+ %ul
+ - if export_template.export_pdf.enabled?
+ %li
+ %span.fr-icon-pdf-2-line
+ #{export_template.export_pdf.path(dossier)}.pdf
+
+ - procedure.exportables_pieces_jointes.each do |tdc|
+ - export_pj = export_template.pj(tdc)
+ - if export_pj.enabled?
+ %li
+ %span.fr-icon-file-image-line
+ #{export_pj.path(dossier)}-1.jpg
diff --git a/app/views/instructeurs/export_templates/edit.html.haml b/app/views/instructeurs/export_templates/edit.html.haml
new file mode 100644
index 000000000..32a80d8d6
--- /dev/null
+++ b/app/views/instructeurs/export_templates/edit.html.haml
@@ -0,0 +1,10 @@
+= render partial: 'administrateurs/breadcrumbs',
+ locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)],
+ [t('.title')]] }
+.fr-container
+ %h1 Mise à jour modèle d'export
+
+ - if @export_template.tabular?
+ = render partial: 'form_tabular', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }
+ - else
+ = render partial: 'form', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }
diff --git a/app/views/instructeurs/export_templates/new.html.haml b/app/views/instructeurs/export_templates/new.html.haml
new file mode 100644
index 000000000..358bab190
--- /dev/null
+++ b/app/views/instructeurs/export_templates/new.html.haml
@@ -0,0 +1,9 @@
+= render partial: 'administrateurs/breadcrumbs',
+ locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)],
+ [t('.title')]] }
+.fr-container
+ %h1 Nouveau modèle d'export
+ - if @export_template.tabular?
+ = render partial: 'form_tabular', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }
+ - else
+ = render partial: 'form', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }
diff --git a/app/views/instructeurs/groupe_instructeurs/show.html.haml b/app/views/instructeurs/groupe_instructeurs/show.html.haml
index 1197d93bd..ea54dc215 100644
--- a/app/views/instructeurs/groupe_instructeurs/show.html.haml
+++ b/app/views/instructeurs/groupe_instructeurs/show.html.haml
@@ -21,11 +21,17 @@
Démarche « #{@procedure.libelle} »
.card.fr-mt-2w
- %h2.fr-h3 Gestion des instructeurs
- = form_for(Instructeur.new(user: User.new), url: { action: :add_instructeur }, html: { class: 'form' }) do |f|
- %h3.fr-h4 Affecter un nouvel instructeur
- = render Dsfr::InputComponent.new(form: f, attribute: :email)
- = f.submit 'Affecter', class: 'fr-btn fr-primary'
+ = render Procedure::InvitationWithTypoComponent.new(maybe_typos: @maybe_typos, url: add_instructeur_instructeur_groupe_path(@procedure, @groupe_instructeur.id), title: "Avant d'ajouter l'email, veuillez confirmer" )
+ %h2.fr-h3= t('.title')
+
+ = form_for :instructeur, url: { action: :add_instructeur, id: @groupe_instructeur.id }, html: { class: 'form' } do |f|
+ .instructeur-wrapper
+ %p= t('.instructeur_emails')
+ %p.fr-hint-text= t('.copy_paste_hint')
+ %react-fragment
+ = render ReactComponent.new 'ComboBox/MultiComboBox', id: 'instructeur_emails', name: 'emails[]', allows_custom_value: true, 'aria-label': 'Emails'
+
+ = f.submit t('.assign'), class: 'fr-btn fr-btn--tertiary'
%table.fr-table.fr-mt-2w.width-100
%thead
diff --git a/app/views/instructeurs/passwords/edit.html.haml b/app/views/instructeurs/passwords/edit.html.haml
deleted file mode 100644
index 802453c66..000000000
--- a/app/views/instructeurs/passwords/edit.html.haml
+++ /dev/null
@@ -1,29 +0,0 @@
-= devise_error_messages!
-
-#form-login
- %h2#instructeur_login Changement de mot de passe
-
- %br
- %br
- #new-user
- = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f|
- = f.hidden_field :reset_password_token
- %h4
- = f.label 'Nouveau mot de passe'
-
- .input-group
- .input-group-addon
- %span.fa.fa-asterisk
- = f.password_field :password, autofocus: true, autocomplete: "off", class: 'form-control'
- %br
- %h4
- = f.label 'Confirmez le nouveau mot de passe'
- .input-group
- .input-group-addon
- %span.fa.fa-asterisk
- = f.password_field :password_confirmation, autocomplete: "off", class: 'form-control'
- %br
- %br
- .actions
- = f.submit 'Changer le mot de passe', class: 'btn btn-primary'
- %br
diff --git a/app/views/instructeurs/passwords/new.html.haml b/app/views/instructeurs/passwords/new.html.haml
deleted file mode 100644
index aa4533459..000000000
--- a/app/views/instructeurs/passwords/new.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-= devise_error_messages!
-
-%br
-#form-login
- %h2#instructeur_login Mot de passe oublié
-
- %br
- %br
- #new-user
- = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f|
- %h4
- = f.label :email
- .input-group
- .input-group-addon
- %span.fa.fa-user
- = f.email_field :email, class: 'form-control', placeholder: 'Email'
- %br
- %br
- .actions
- = f.submit 'Demander un nouveau mot de passe', class: 'button large expand primary'
- %br
diff --git a/app/views/instructeurs/procedures/_dossier_actions.html.haml b/app/views/instructeurs/procedures/_dossier_actions.html.haml
index fb0b1435d..aeb6748ab 100644
--- a/app/views/instructeurs/procedures/_dossier_actions.html.haml
+++ b/app/views/instructeurs/procedures/_dossier_actions.html.haml
@@ -1,7 +1,13 @@
-- if hidden_by_administration
+- if hidden_by_administration && hidden_by_expired
+ %li
+ = button_to repousser_expiration_and_restore_instructeur_dossier_path(procedure_id, dossier_id), method: :post, class: "fr-btn fr-icon-refresh-line" do
+ = t('views.instructeurs.dossiers.restore_and_extend')
+
+- elsif hidden_by_administration
%li
= button_to restore_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, class: "fr-btn fr-icon-refresh-line" do
= t('views.instructeurs.dossiers.restore')
+
- elsif close_to_expiration || Dossier::TERMINE.include?(state)
%li
- if close_to_expiration
diff --git a/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml b/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml
index 4bcafb3ed..56c08315b 100644
--- a/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml
+++ b/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml
@@ -3,4 +3,4 @@
= t('views.instructeurs.dossiers.filters.title')
- menu.with_form do
- = render Dossiers::InstructeurFilterComponent.new(procedure: procedure, procedure_presentation: @procedure_presentation, statut: statut)
+ = render Instructeurs::ColumnFilterComponent.new(procedure_presentation:, statut:)
diff --git a/app/views/instructeurs/procedures/_dossiers_filter_tags.html.haml b/app/views/instructeurs/procedures/_dossiers_filter_tags.html.haml
deleted file mode 100644
index f3db643a3..000000000
--- a/app/views/instructeurs/procedures/_dossiers_filter_tags.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- if current_filters.count > 0
- .fr-mb-2w
- - current_filters.group_by { |filter| filter['table'] }.each_with_index do |(table, filters), i|
- - if i > 0
- = " et "
- - filters.each_with_index do |filter, i|
- - if i > 0
- = " ou "
- = link_to remove_filter_instructeur_procedure_path(procedure, { statut: statut, field: "#{filter['table']}/#{filter['column']}", value: filter['value'] }),
- class: "fr-tag fr-tag--dismiss fr-my-1w", aria: { label: "Retirer le filtre #{filter['column']}" } do
- = "#{filter['label'].truncate(50)} : #{procedure_presentation.human_value_for_filter(filter)}"
diff --git a/app/views/instructeurs/procedures/_header.html.haml b/app/views/instructeurs/procedures/_header.html.haml
index 6714ec668..99ee79e51 100644
--- a/app/views/instructeurs/procedures/_header.html.haml
+++ b/app/views/instructeurs/procedures/_header.html.haml
@@ -24,9 +24,13 @@
|
= link_to t('instructeurs.dossiers.header.banner.administrators_list'), administrateurs_instructeur_procedure_path(procedure), class: 'header-link'
|
+ = link_to t('views.instructeurs.dossiers.show_deleted_dossiers'), deleted_dossiers_instructeur_procedure_path(@procedure), class: "header-link"
+ |
= link_to t('instructeurs.dossiers.header.banner.exports_list'), exports_instructeur_procedure_path(procedure), class: 'header-link'
- if @has_export_notification
%span.notifications{ 'aria-label': t('instructeurs.dossiers.header.banner.exports_notification_label') }
+
+
#last-export-alert
= render partial: "last_export_alert", locals: { export: @last_export, statut: @statut }
diff --git a/app/views/instructeurs/procedures/_header_field.html.haml b/app/views/instructeurs/procedures/_header_field.html.haml
deleted file mode 100644
index e4a54d673..000000000
--- a/app/views/instructeurs/procedures/_header_field.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%th{ @procedure_presentation.aria_sort(@procedure_presentation.sort['order'], field), scope: "col", class: classname }
-
- = link_to update_sort_instructeur_procedure_path(@procedure, table: field['table'], column: field['column'], order: @procedure_presentation.opposite_order_for(field['table'], field['column'])) do
- - if @procedure_presentation.sortable?(field)
- - if @procedure_presentation.sort['order'] == 'asc'
- #{field['label']} ↑
- - else
- #{field['label']} ↓
- - else
- #{field['label']}
diff --git a/app/views/instructeurs/procedures/_list.html.haml b/app/views/instructeurs/procedures/_list.html.haml
index 868359274..20604c03a 100644
--- a/app/views/instructeurs/procedures/_list.html.haml
+++ b/app/views/instructeurs/procedures/_list.html.haml
@@ -1,13 +1,13 @@
%li.flex.align-start.fr-mb-5w
.flex
- = link_to instructeur_procedure_path(p), class: 'procedure-logo-link' do
- .procedure-logo{ style: "background-image: url(#{p.logo_url})" }
+ .procedure-logo{ style: "background-image: url(#{p.logo_url})" }
.procedure-details
.flex.clipboard-container
- %p.fr-mb-2w
+ .fr-mb-2w
= procedure_badge(p)
- = link_to("#{p.libelle} - n°#{p.id}", instructeur_procedure_path(p), class: "fr-link fr-ml-1w")
+ %h3.font-weight-normal.fr-link.fr-ml-1w
+ = link_to("#{p.libelle} - n°#{p.id}", instructeur_procedure_path(p))
= render Dsfr::CopyButtonComponent.new(title: t('instructeurs.procedures.index.copy_link_button'), text: commencer_url(p.path))
%ul.procedure-stats.flex
@@ -50,12 +50,12 @@
%li
%object
- = link_to(instructeur_procedure_path(p, statut: 'supprimes_recemment')) do
- - dossier_count = dossiers_supprimes_recemment_count_per_procedure[p.id] || 0
+ = link_to(instructeur_procedure_path(p, statut: 'supprimes')) do
+ - dossier_count = dossiers_supprimes_count_per_procedure[p.id] || 0
.stats-number
= number_with_html_delimiter(dossier_count)
.stats-legend
- = t('pluralize.dossiers_supprimes_recemment', count: dossier_count)
+ = t('pluralize.dossiers_supprimes', count: dossier_count)
- if p.procedure_expires_when_termine_enabled
%li
diff --git a/app/views/instructeurs/procedures/_synthese.html.haml b/app/views/instructeurs/procedures/_synthese.html.haml
index aa2760758..fcd0293d0 100644
--- a/app/views/instructeurs/procedures/_synthese.html.haml
+++ b/app/views/instructeurs/procedures/_synthese.html.haml
@@ -1,6 +1,6 @@
- if procedures.length > 1
.flex.align-center.fr-mb-2w
- %h2.fr-text--sm.fr-mb-1w= t('views.instructeurs.dossiers.dossier_synthesis')
+ %p.font-weight-bold.fr-text--sm.fr-mb-1w= t('views.instructeurs.dossiers.dossier_synthesis')
- all_dossiers_counts.each_with_index do |(label, dossier_count)|
- if dossier_count != 0
%span.fr-badge.fr-ml-1w.fr-mb-1w= number_with_html_delimiter(dossier_count) + ' ' + label
diff --git a/app/views/instructeurs/procedures/_tabs.html.haml b/app/views/instructeurs/procedures/_tabs.html.haml
index c9d6a2560..bfaa2760b 100644
--- a/app/views/instructeurs/procedures/_tabs.html.haml
+++ b/app/views/instructeurs/procedures/_tabs.html.haml
@@ -22,10 +22,10 @@
active: statut == 'tous',
badge: number_with_html_delimiter(tous_count))
- = tab_item(t(tab_i18n_key_from_status('supprimes_recemment'), count: supprimes_recemment_count),
- instructeur_procedure_path(procedure, statut: 'supprimes_recemment'),
- active: statut == 'supprimes_recemment',
- badge: number_with_html_delimiter(supprimes_recemment_count))
+ = tab_item(t(tab_i18n_key_from_status('supprimes'), count: supprimes_count),
+ instructeur_procedure_path(procedure, statut: 'supprimes'),
+ active: statut == 'supprimes',
+ badge: number_with_html_delimiter(supprimes_count))
- if procedure.procedure_expires_when_termine_enabled
= tab_item(t(tab_i18n_key_from_status('expirant'), count: expirant_count),
diff --git a/app/views/instructeurs/procedures/deleted_dossiers.html.haml b/app/views/instructeurs/procedures/deleted_dossiers.html.haml
index b3b31961f..3b7bb170a 100644
--- a/app/views/instructeurs/procedures/deleted_dossiers.html.haml
+++ b/app/views/instructeurs/procedures/deleted_dossiers.html.haml
@@ -1,55 +1,11 @@
- content_for(:title, "#{@procedure.libelle}")
-#procedure-show
- .sub-header
- .fr-container.flex
+= render partial: 'administrateurs/breadcrumbs',
+ locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)],
+ ['Historique des dossiers supprimés']] }
- .procedure-logo{ style: "background-image: url(#{@procedure.logo_url})",
- role: 'img', 'aria-label': "logo de la démarche #{@procedure.libelle}" }
+.fr-container
+ .fr-mb-3w
+ = link_to "Retour à la démarche", instructeur_procedure_path(@procedure), class: "fr-link fr-icon-arrow-left-line fr-link--icon-left"
- = render partial: 'header', locals: { procedure: @procedure, statut: @statut }
-
- .procedure-actions
- - if @can_download_dossiers
- = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_export_instructeur_procedure_path))
-
- .fr-container.flex= render partial: "tabs", locals: { procedure: @procedure,
- statut: @statut,
- a_suivre_count: @a_suivre_count,
- suivis_count: @suivis_count,
- traites_count: @traites_count,
- tous_count: @tous_count,
- supprimes_recemment_count: @supprimes_recemment_count,
- archives_count: @archives_count,
- expirant_count: @expirant_count,
- has_en_cours_notifications: @has_en_cours_notifications,
- has_termine_notifications: @has_termine_notifications }
-
- .fr-container
- %h1.titre-dossiers Dossiers supprimés
- %details
- %summary Les dossiers ont été supprimés. Vous ne pouvez plus les récupérer depuis Démarches Simplifiées.
- Ceci s'explique pour les raisons suivantes :
- %ul
- %li L’utilisateur a intentionnellement supprimé son dossier.
- %li Le délai de conservation maximal de #{@procedure.duree_conservation_dossiers_dans_ds} mois a expiré. Conformément au règlement RGPD, DS ne peut continuer à les héberger.
- - if @deleted_dossiers.any?
- = paginate @deleted_dossiers, views_prefix: 'shared'
- %table.table.dossiers-table.hoverable
- %thead
- %tr
- %th.number-col N° dossier
- %th Raison de suppression
- %th Date de suppression
- %tbody
- - @deleted_dossiers.each do |deleted_dossier|
- %tr
- %td.number-col
- = deleted_dossier.dossier_id
- %td
- = deletion_reason_badge(deleted_dossier.reason)
- %td.deleted-cell
- = l(deleted_dossier.deleted_at, format: '%d/%m/%y')
- = paginate @deleted_dossiers, views_prefix: 'shared'
- - else
- Aucun dossier supprimé
+= render Dossiers::DeletedDossiersComponent.new(deleted_dossiers: @deleted_dossiers)
diff --git a/app/views/instructeurs/procedures/download_export.turbo_stream.haml b/app/views/instructeurs/procedures/download_export.turbo_stream.haml
index b841c65a0..c6e47b799 100644
--- a/app/views/instructeurs/procedures/download_export.turbo_stream.haml
+++ b/app/views/instructeurs/procedures/download_export.turbo_stream.haml
@@ -2,10 +2,10 @@
- if @can_download_dossiers
- if @statut.nil?
= turbo_stream.update_all '.procedure-actions' do
- = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_export_instructeur_procedure_path))
+ = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), export_url: method(:download_export_instructeur_procedure_path))
- else
= turbo_stream.update_all '.dossiers-export' do
- = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, statut: @statut, count: @dossiers_count, export_url: method(:download_export_instructeur_procedure_path))
+ = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), statut: @statut, count: @dossiers_count, export_url: method(:download_export_instructeur_procedure_path))
= turbo_stream.update "last-export-alert" do
= render partial: "last_export_alert", locals: { export: @last_export, statut: @statut }
diff --git a/app/views/instructeurs/procedures/exports.html.haml b/app/views/instructeurs/procedures/exports.html.haml
index ed2f67fa8..fad0a8022 100644
--- a/app/views/instructeurs/procedures/exports.html.haml
+++ b/app/views/instructeurs/procedures/exports.html.haml
@@ -6,19 +6,59 @@
[t('.title')]] }
.fr-container
- %h1= t('.title')
- = render Dsfr::CalloutComponent.new(title: nil) do |c|
- - c.with_body do
- %p= t('.export_description', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i)
+ .fr-tabs.mb-3
+ %ul.fr-tabs__list{ role: 'tablist' }
+ %li{ role: 'presentation' }
+ %button.fr-tabs__tab.fr-tabs__tab--icon-left{ id: "tabpanel-exports", tabindex: "0", role: "tab", "aria-selected": "true", "aria-controls": "tabpanel-exports-panel" } Liste des exports
+ %li{ role: 'presentation' }
+ %button.fr-tabs__tab.fr-tabs__tab--icon-left{ id: "tabpanel-export-templates", tabindex: "-1", role: "tab", "aria-selected": "false", "aria-controls": "tabpanel-export-templates-panel" } Modèles d'export
- - if @exports.present?
- %div{ data: @exports.any?(&:pending?) ? { controller: "turbo-poll", turbo_poll_url_value: "", turbo_poll_interval_value: 10_000, turbo_poll_max_checks_value: 6 } : {} }
- = render Dossiers::ExportLinkComponent.new(procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
-
- - if @exports.any?{_1.format == Export.formats.fetch(:zip)}
- = render Dsfr::AlertComponent.new(title: t('.title_zip'), state: :info, extra_class_names: 'fr-mb-3w') do |c|
+ .fr-tabs__panel.fr-tabs__panel--selected{ id: "tabpanel-exports-panel", role: "tabpanel", "aria-labelledby": "tabpanel-exports", tabindex: "0" }
+ = render Dsfr::CalloutComponent.new(title: nil) do |c|
- c.with_body do
- %p= t('.export_description_zip_html')
+ %p= t('.export_description', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i)
- - else
- = t('.no_export_html', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i )
+ - if @exports.present?
+ %div{ data: @exports.any?(&:pending?) ? { controller: "turbo-poll", turbo_poll_url_value: "", turbo_poll_interval_value: 10_000, turbo_poll_max_checks_value: 6 } : {} }
+ = render Dossiers::ExportLinkComponent.new(procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
+
+ - if @exports.any?{_1.format == Export.formats.fetch(:zip)}
+ = render Dsfr::AlertComponent.new(title: t('.title_zip'), state: :info, extra_class_names: 'fr-mb-3w') do |c|
+ - c.with_body do
+ %p= t('.export_description_zip_html')
+
+ - else
+ = t('.no_export_html', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i )
+
+ .fr-tabs__panel.fr-tabs__panel{ id: "tabpanel-export-templates-panel", role: "tabpanel", "aria-labelledby": "tabpanel-export-templates", tabindex: "0" }
+ = render Dsfr::AlertComponent.new(state: :info) do |c|
+ - c.with_body do
+ %p= t('.export_template_list_description_html')
+
+
+ .fr-mt-5w
+ = link_to t('.new_zip_export_template'), new_instructeur_procedure_export_template_path(@procedure, kind: 'zip'), class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line fr-mr-1w"
+ = link_to t('.new_tabular_export_template'), new_instructeur_procedure_export_template_path(@procedure, kind: 'tabular'), class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line"
+
+ .fr-table.fr-table--bordered.fr-table--no-caption.fr-mt-5w
+ .fr-table__wrapper
+ .fr-table__container
+ .fr-table__content
+ %table
+ %thead
+ %tr
+ = tag.th "Nom du modèle", scope: 'col'
+ = tag.th "Format", scope: 'col'
+ = tag.th "Date de création", scope: 'col'
+ = tag.th "Partagé avec (groupe instructeurs)", scope: 'col' if @procedure.groupe_instructeurs.many?
+ = tag.th "Actions", scope: 'col'
+ %tbody
+ - @export_templates.each do |export_template|
+ %tr
+ %td= link_to export_template.name, [:edit, :instructeur, @procedure, export_template]
+ %td= pretty_kind(export_template.kind)
+ %td= l(export_template.created_at)
+ = tag.td export_template.groupe_instructeur.label if @procedure.groupe_instructeurs.many?
+ %td
+ = link_to "Modifier", [:edit, :instructeur, @procedure, export_template], class: "fr-btn fr-btn--icon-left fr-icon-edit-line fr-mr-1w"
+ = link_to "Supprimer", [:instructeur, @procedure, export_template], method: :delete, data: { confirm: "Voulez-vous vraiment supprimer ce modèle ? Il sera supprimé pour tous les instructeurs du groupe"}, class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-delete-line"
diff --git a/app/views/instructeurs/procedures/index.html.haml b/app/views/instructeurs/procedures/index.html.haml
index a3ff48798..f18d64e94 100644
--- a/app/views/instructeurs/procedures/index.html.haml
+++ b/app/views/instructeurs/procedures/index.html.haml
@@ -30,7 +30,7 @@
- if collection.present?
- .fr-h6
+ %h2.fr-h6
= page_entries_info collection
%ul.procedure-list.fr-pl-0
= render partial: 'instructeurs/procedures/list',
@@ -41,7 +41,7 @@
dossiers_archived_count_per_procedure: @dossiers_archived_count_per_procedure,
dossiers_termines_count_per_procedure: @dossiers_termines_count_per_procedure,
dossiers_expirant_count_per_procedure: @dossiers_expirant_count_per_procedure,
- dossiers_supprimes_recemment_count_per_procedure: @dossiers_supprimes_recemment_count_per_procedure,
+ dossiers_supprimes_count_per_procedure: @dossiers_supprimes_count_per_procedure,
followed_dossiers_count_per_procedure: @followed_dossiers_count_per_procedure,
procedure_ids_en_cours_with_notifications: @procedure_ids_en_cours_with_notifications,
procedure_ids_termines_with_notifications: @procedure_ids_termines_with_notifications }
diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml
index a067ef265..00753ecb1 100644
--- a/app/views/instructeurs/procedures/show.html.haml
+++ b/app/views/instructeurs/procedures/show.html.haml
@@ -11,7 +11,7 @@
.procedure-actions
- if @can_download_dossiers
- = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_export_instructeur_procedure_path))
+ = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), export_url: method(:download_export_instructeur_procedure_path))
.fr-container.flex= render partial: "tabs", locals: { procedure: @procedure,
statut: @statut,
@@ -19,7 +19,7 @@
suivis_count: @counts[:suivis],
traites_count: @counts[:traites],
tous_count: @counts[:tous],
- supprimes_recemment_count: @counts[:supprimes_recemment],
+ supprimes_count: @counts[:supprimes],
archives_count: @counts[:archives],
expirant_count: @counts[:expirant],
has_en_cours_notifications: @has_en_cours_notifications,
@@ -41,9 +41,9 @@
= t('views.instructeurs.dossiers.tab_explainations.tous_with_routing')
- else
= t('views.instructeurs.dossiers.tab_explainations.tous')
- - if @statut == 'supprimes_recemment'
+ - if @statut == 'supprimes'
%p
- = t('views.instructeurs.dossiers.tab_explainations.supprimes_recemment').html_safe
+ = t('views.instructeurs.dossiers.tab_explainations.supprimes').html_safe
- if @statut == 'archives'
%p
= t('views.instructeurs.dossiers.tab_explainations.archives')
@@ -61,21 +61,16 @@
%hr
.flex.align-center
- if @filtered_sorted_paginated_ids.present? || @current_filters.count > 0
- = render partial: "dossiers_filter_dropdown", locals: { procedure: @procedure, statut: @statut}
- = render Dossiers::NotifiedToggleComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation)
+ = render partial: "dossiers_filter_dropdown", locals: { procedure: @procedure, statut: @statut, procedure_presentation: @procedure_presentation }
+ = render Dossiers::NotifiedToggleComponent.new(procedure_presentation: @procedure_presentation) if @statut != 'a-suivre'
.fr-ml-auto
-
- - if @statut == 'archives'
- = link_to deleted_dossiers_instructeur_procedure_path(@procedure), class: "fr-link fr-icon-delete-line fr-link--icon-left fr-mr-2w" do
- = t('views.instructeurs.dossiers.show_deleted_dossiers')
-
- if @dossiers_count > 0
%span.dossiers-export
- = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
+ = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
- if @filtered_sorted_paginated_ids.present? || @current_filters.count > 0
- = render partial: "dossiers_filter_tags", locals: { procedure: @procedure, procedure_presentation: @procedure_presentation, current_filters: @current_filters, statut: @statut }
+ = render Instructeurs::FilterButtonsComponent.new(filters: @current_filters, procedure_presentation: @procedure_presentation, statut: @statut)
- batch_operation_component = Dossiers::BatchOperationComponent.new(statut: @statut, procedure: @procedure)
@@ -98,8 +93,7 @@
%th.text-center
%input{ type: "checkbox", disabled: @disable_checkbox_all, checked: @disable_checkbox_all, data: { action: "batch-operation#onCheckAll" }, id: dom_id(BatchOperation.new, :checkbox_all), aria: { label: t('views.instructeurs.dossiers.select_all') } }
- - @procedure_presentation.displayed_fields_for_headers.each do |field|
- = render partial: "header_field", locals: { field: field, classname: field['classname'] }
+ = render Instructeurs::ColumnTableHeaderComponent.new(procedure_presentation: @procedure_presentation)
%th.follow-col
Actions
@@ -109,18 +103,7 @@
- menu.with_button_inner_html do
= t('views.instructeurs.dossiers.personalize')
- menu.with_form do
- = form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do
- = hidden_field_tag :values, nil
- = react_component("ComboMultiple",
- options: @displayable_fields_for_select,
- selected: @displayable_fields_selected,
- disabled: [],
- label: 'Colonne à afficher',
- group: '.columns-form',
- name: 'values')
-
- = submit_tag t('views.instructeurs.dossiers.save'), class: 'fr-btn fr-btn--secondary'
-
+ = render Instructeurs::ColumnPickerComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation)
%tbody
= render Dossiers::BatchSelectMoreComponent.new(dossiers_count: @dossiers_count, filtered_sorted_ids: @filtered_sorted_ids)
@@ -149,12 +132,13 @@
%td
- if p.hidden_by_administration_at.present?
%span.cell-link
- = column
- = "- #{t('views.instructeurs.dossiers.deleted_by_user')}" if p.hidden_by_user_at.present?
+ = column.is_a?(Hash) ? tags_label(column[:value]) : column
+ - if p.hidden_by_user_at.present?
+ = "- #{t("views.instructeurs.dossiers.deleted_reason.#{p.hidden_by_reason}")}"
- else
%a.cell-link{ href: path }
- = column
- = "- #{t('views.instructeurs.dossiers.deleted_by_user')}" if p.hidden_by_user_at.present?
+ = column.is_a?(Hash) ? tags_label(column[:value]) : column
+ = "- #{t("views.instructeurs.dossiers.deleted_reason.#{p.hidden_by_reason}")}" if p.hidden_by_user_at.present?
%td.status-col
- status = [status_badge(p.state)]
@@ -177,7 +161,8 @@
archived: p.archived,
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
close_to_expiration: @statut == 'expirant',
- hidden_by_administration: @statut == 'supprimes_recemment',
+ hidden_by_administration: @statut == 'supprimes',
+ hidden_by_expired: p.hidden_by_reason == 'expired',
sva_svr: @procedure.sva_svr_enabled?,
has_blocking_pending_correction: @procedure.feature_enabled?(:blocking_pending_correction) && p.pending_correction?,
turbo: false,
diff --git a/app/views/instructeurs/procedures/update_filter.turbo_stream.haml b/app/views/instructeurs/procedures/update_filter.turbo_stream.haml
index a5cd42c91..efc3ed43b 100644
--- a/app/views/instructeurs/procedures/update_filter.turbo_stream.haml
+++ b/app/views/instructeurs/procedures/update_filter.turbo_stream.haml
@@ -1,2 +1,2 @@
= turbo_stream.replace 'filter-component' do
- = render Dossiers::InstructeurFilterComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation, statut: @statut, field_id: @field)
+ = render Instructeurs::ColumnFilterComponent.new(procedure_presentation: @procedure_presentation, statut: @statut, column: @column)
diff --git a/app/views/invites/_dropdown.html.haml b/app/views/invites/_dropdown.html.haml
index 45a00864c..e2da2ffab 100644
--- a/app/views/invites/_dropdown.html.haml
+++ b/app/views/invites/_dropdown.html.haml
@@ -1,10 +1,11 @@
- invites = dossier.invites.load
-= render Dropdown::MenuComponent.new(wrapper: :span, wrapper_options: {class: 'invite-user-action'}, button_options: { class: ['fr-btn--secondary'] }, menu_options: { id: 'invite-content' }) do |menu|
+= render Dropdown::MenuComponent.new(wrapper: :div, wrapper_options: {class: 'invite-user-action'}, button_options: { class: ['fr-btn--secondary'] }, menu_options: { id: 'invite-content' }) do |menu|
+ = 'lab'
- menu.with_button_inner_html do
= dsfr_icon('fr-icon-user-add-fill', :sm, :mr)
- if invites.present?
= t('views.invites.dropdown.view_invited_people')
- %span.badge= invites.size
+ %span.fr-badge.fr-ml-1v= invites.size
- else
- if dossier.read_only?
= t('views.invites.dropdown.invite_to_view')
diff --git a/app/views/invites/_form.html.haml b/app/views/invites/_form.html.haml
index 824d1b36b..8f3922f6f 100644
--- a/app/views/invites/_form.html.haml
+++ b/app/views/invites/_form.html.haml
@@ -4,13 +4,13 @@
%h5.fr-h6= t('views.invites.form.edit_dossier', count: invites.size)
- if invites.present?
- #invite-list{ morphing ? { tabindex: "-1" } : {} }
+ #invite-list
%ul
- - invites.each do |invite|
+ - invites.each_with_index do |invite, index|
%li
- = invite.email
+ %span{ :id => "invite_#{index}" }= invite.email
%small{ 'data-turbo': 'true' }
- = link_to t('views.invites.form.withdraw_permission'), invite_path(invite), data: { turbo_method: :delete, turbo_confirm: t('views.invites.form.want_to_withdraw_permission', email: invite.email) }, class: "fr-btn fr-btn--sm fr-btn--tertiary-no-outline"
+ = link_to t('views.invites.form.withdraw_permission'), invite_path(invite), data: { turbo_method: :delete, turbo_confirm: t('views.invites.form.want_to_withdraw_permission', email: invite.email) }, class: "fr-btn fr-btn--sm fr-btn--tertiary-no-outline", id: "link_#{index}", "aria-labelledby": "link_#{index} invite_#{index}"
- if dossier.brouillon?
%p= t('views.invites.form.submit_dossier_yourself')
diff --git a/app/views/layouts/_account_dropdown.haml b/app/views/layouts/_account_dropdown.haml
index d46aec7f1..a9279b58f 100644
--- a/app/views/layouts/_account_dropdown.haml
+++ b/app/views/layouts/_account_dropdown.haml
@@ -1,12 +1,13 @@
-%nav.fr-translate.fr-nav{ role: "navigation", "aria-label"=> t('menu_aria_label', scope: [:layouts]) }
+%nav.fr-translate.fr-nav{ role: "navigation", "aria-label"=> t('my_account', scope: [:layouts]) }
.fr-nav__item
%button.account-btn.fr-translate__btn.fr-btn{ "aria-controls" => "account", "aria-expanded" => "false", :title => t('my_account', scope: [:layouts]) }
- %span= current_email
+ %span.fr-mr-1w= current_email
- if dossier.present? && dossier&.france_connected_with_one_identity?
%span
via FranceConnect
- %span{ class: "fr-badge fr-badge--sm fr-ml-1w #{color_by_role(nav_bar_profile)}" }
- = t("layouts.#{nav_bar_profile}")
+ - if nav_bar_profile != :guest # don't confuse user with unknown profile
+ %span{ class: "fr-badge fr-badge--sm #{color_by_role(nav_bar_profile)}" }
+ = t("layouts.#{nav_bar_profile}")
#account.fr-collapse.fr-menu
%ul.fr-menu__list.max-content
- if multiple_devise_profile_connect?
diff --git a/app/views/layouts/_display_theme_modal.html.haml b/app/views/layouts/_display_theme_modal.html.haml
index 237a5f966..4d44b06b7 100644
--- a/app/views/layouts/_display_theme_modal.html.haml
+++ b/app/views/layouts/_display_theme_modal.html.haml
@@ -15,7 +15,7 @@
%input#fr-radios-theme-light{ name: "fr-radios-theme", type: "radio", value: "light" }/
%label.fr-label{ for: "fr-radios-theme-light" } Thème clair
.fr-radio-rich__img
- %svg.fr-artwork{ aria_hidden: "true", viewBox: "0 0 80 80", width: "80px", height: "80px" }
+ %svg.fr-artwork{ "aria-hidden": "true", viewBox: "0 0 80 80", width: "80px", height: "80px" }
%use.fr-artwork-decorative{ href: image_path("pictograms/environment/sun.svg#artwork-decorative") }
%use.fr-artwork-minor{ href: image_path("pictograms/environment/sun.svg#artwork-minor") }
%use.fr-artwork-major{ href: image_path("pictograms/environment/sun.svg#artwork-major") }
diff --git a/app/views/layouts/_flash_messages.html.haml b/app/views/layouts/_flash_messages.html.haml
index b0a3e5d72..d2e0eff4c 100644
--- a/app/views/layouts/_flash_messages.html.haml
+++ b/app/views/layouts/_flash_messages.html.haml
@@ -1,14 +1,13 @@
-#flash_messages{ aria: { live: 'assertive' } }
- - if flash.any?
- #flash_message.center
+#flash_messages{ tabindex: '-1', data: { turbo_force: :server } }
+ #flash_message.center{ class: defined?(unique_classname) ? unique_classname : '' }
+ - if flash.any?
- flash.each do |key, value|
- sticky = defined?(sticky) ? sticky : false
- fixed = defined?(fixed) ? fixed : false
- - if value.class == Array
- .alert{ class: flash_class(key, sticky: sticky, fixed: fixed), role: flash_role(key) }
+ .alert{ role: flash_role(key), class: flash_class(key, sticky: sticky, fixed: fixed) }
+ - if value.class == Array
- value.each do |message|
= sanitize_with_link(message)
%br
- - elsif value.present?
- .alert{ class: flash_class(key, sticky: sticky, fixed: fixed), role: flash_role(key) }
+ - elsif value.present?
= sanitize_with_link(value)
diff --git a/app/views/layouts/_header.haml b/app/views/layouts/_header.haml
index 4048831e0..5b6488051 100644
--- a/app/views/layouts/_header.haml
+++ b/app/views/layouts/_header.haml
@@ -1,90 +1,88 @@
--# We can't use &. because the controller may not implement #nav_bar_profile
-- nav_bar_profile = controller.try(:nav_bar_profile) || :guest
+-# We can't use &. or as helper methods because the controllers from view specs does not implement these methods
+- nav_bar_profile = controller.try(:nav_bar_profile) || controller.try(:fallback_nav_bar_profile) || :guest
- dossier = controller.try(:dossier_for_help)
- procedure = controller.try(:procedure_for_help)
- is_instructeur_context = nav_bar_profile == :instructeur && instructeur_signed_in?
- is_administrateur_context = nav_bar_profile == :administrateur && administrateur_signed_in?
- is_expert_context = nav_bar_profile == :expert && expert_signed_in?
- is_user_context = nav_bar_profile == :user
-- is_search_enabled = [params[:controller] == 'recherche', is_instructeur_context, is_expert_context, is_user_context && current_user.dossiers.count].any?
+- is_search_enabled = [params[:controller] == 'recherche', is_instructeur_context, is_expert_context].any?
%header{ class: ["fr-header", content_for?(:notice_info) && "fr-header__with-notice-info"], role: "banner", "data-controller": "dsfr-header" }
- .fr-header__body
- .fr-container
- .fr-header__body-row
- .fr-header__brand.fr-enlarge-link
- .fr-header__brand-top
- .fr-header__logo
- %p.fr-logo{ lang: "fr" }
- République
- = succeed "Française" do
- %br/
- .fr-header__navbar
- - if is_search_enabled
- %button.fr-btn--search.fr-btn{ "aria-controls" => "search-modal", "data-fr-opened" => "false", :title => t('views.users.dossiers.search.search_file') }= t('views.users.dossiers.search.search_file')
- %button#navbar-burger-button.fr-btn--menu.fr-btn{ "aria-controls" => "modal-header__menu", "data-fr-opened" => "false", title: "Menu" } Menu
- .fr-header__service
- - root_profile_link, root_profile_libelle = root_path_info_for_profile(nav_bar_profile)
+ %nav{ :role => "navigation", "aria-label" => t('layouts.header.main_menu') }
+ .fr-header__body
+ .fr-container
+ .fr-header__body-row
+ .fr-header__brand.fr-enlarge-link
+ .fr-header__brand-top
+ .fr-header__logo
+ %img{ :src => image_url("dgnum.svg"), alt: '', width: 105, height: 55.6, loading: 'lazy' }
+ .fr-header__navbar
+ - if is_search_enabled
+ %button.fr-btn--search.fr-btn{ "aria-controls" => "search-modal", "data-fr-opened" => "false", :title => t('views.users.dossiers.search.search_file') }= t('views.users.dossiers.search.search_file')
+ %button#navbar-burger-button.fr-btn--menu.fr-btn{ "aria-controls" => "modal-header__menu", "data-fr-opened" => "false", title: "Menu" } Menu
+ .fr-header__service
+ - root_profile_link, root_profile_libelle = root_path_info_for_profile(nav_bar_profile)
- = link_to root_profile_link, title: "#{root_profile_libelle} — #{Current.application_name}" do
- %span.fr-header__service-title{ lang: "fr" }= Current.application_name
+ = link_to root_profile_link, title: "#{root_profile_libelle} — #{Current.application_name}" do
+ %span.fr-header__service-title{ lang: "fr" }= Current.application_name
- .fr-header__tools
- .fr-header__tools-links.relative
+ .fr-header__tools
+ .fr-header__tools-links.relative
+
+ %ul.fr-btns-group.flex.align-center
+ - if instructeur_signed_in? || user_signed_in?
+ %li
+ = render partial: 'layouts/account_dropdown', locals: { nav_bar_profile: nav_bar_profile, dossier: dossier }
+ - elsif (request.path != new_user_session_path && request.path !=agent_connect_path)
+ - if request.path == new_user_registration_path
+ %li.fr-hidden-sm.fr-unhidden-lg.fr-link--sm.fr-mb-2w.fr-mr-1v= t('views.shared.account.already_user_question')
+ %li= link_to 'Agent', agent_connect_path, class: "fr-btn fr-btn--tertiary fr-icon-government-fill fr-btn--icon-left"
+ %li= link_to t('views.shared.account.signin'), new_user_session_path, class: "fr-btn fr-btn--tertiary fr-icon-account-circle-fill fr-btn--icon-left"
- %ul.fr-btns-group.flex.align-center
- - if instructeur_signed_in? || user_signed_in?
%li
- = render partial: 'layouts/account_dropdown', locals: { nav_bar_profile: nav_bar_profile, dossier: dossier }
- - elsif (request.path != new_user_session_path && request.path !=agent_connect_path)
- - if request.path == new_user_registration_path
- %li.fr-hidden-sm.fr-unhidden-lg.fr-link--sm.fr-mb-2w.fr-mr-1v= t('views.shared.account.already_user_question')
- %li= link_to 'Agent', agent_connect_path, class: "fr-btn fr-btn--tertiary fr-icon-government-fill fr-btn--icon-left"
- %li= link_to t('views.shared.account.signin'), new_user_session_path, class: "fr-btn fr-btn--tertiary fr-icon-account-circle-fill fr-btn--icon-left"
+ - if dossier.present? && nav_bar_profile == :user
+ = render partial: 'shared/help/help_dropdown_dossier', locals: { dossier: dossier }
- %li
- - if dossier.present? && nav_bar_profile == :user
- = render partial: 'shared/help/help_dropdown_dossier', locals: { dossier: dossier }
+ - elsif procedure.present? && (nav_bar_profile == :user || nav_bar_profile == :guest)
+ = render partial: 'shared/help/help_dropdown_procedure', locals: { procedure: procedure }
- - elsif procedure.present? && (nav_bar_profile == :user || nav_bar_profile == :guest)
- = render partial: 'shared/help/help_dropdown_procedure', locals: { procedure: procedure }
-
- - elsif nav_bar_profile == :instructeur
- = render partial: 'shared/help/help_dropdown_instructeur'
- - else
- // NB: on mobile in order to have links correctly aligned, we need a left icon
- = link_to t('help'), t("links.common.faq.url"), class: 'fr-btn dropdown-button', title: new_tab_suffix(t('help')), **external_link_attributes
+ - elsif nav_bar_profile == :instructeur
+ = render partial: 'shared/help/help_dropdown_instructeur'
+ - else
+ -# NB: on mobile in order to have links correctly aligned, we need a left icon #
+ = link_to t('help'), t("links.common.faq.url"), class: 'fr-btn'
- - if localization_enabled?
- %li= render partial: 'layouts/locale_dropdown'
+ - if localization_enabled?
+ %li= render partial: 'layouts/locale_dropdown'
- - if params[:controller] == 'recherche'
- = render partial: 'layouts/search_dossiers_form'
+ - if is_instructeur_context
+ = render partial: 'layouts/search_dossiers_form', locals: { context: :instructeur }
- - if is_instructeur_context
- = render partial: 'layouts/search_dossiers_form'
+ - elsif is_expert_context
+ = render partial: 'layouts/search_dossiers_form', locals: { context: :expert }
- - if is_expert_context
- = render partial: 'layouts/search_dossiers_form'
+ - elsif params[:controller] == 'recherche'
+ = render partial: 'layouts/search_dossiers_form'
- = render SwitchDomainBannerComponent.new(user: current_user)
+ = render SwitchDomainBannerComponent.new(user: current_user)
- #modal-header__menu.fr-header__menu.fr-modal{ "aria-labelledby": "navbar-burger-button" }
- .fr-container
- %button.fr-btn--close.fr-btn{ "aria-controls" => "modal-header__menu", title: t('close_modal', scope: [:layouts, :header]) }= t('close_modal', scope: [:layouts, :header])
- .fr-header__menu-links
- -# populated by dsfr js
+ #modal-header__menu.fr-header__menu.fr-modal{ "aria-labelledby": "navbar-burger-button" }
+ .fr-container
+ %button.fr-btn--close.fr-btn{ "aria-controls" => "modal-header__menu", title: t('close_modal', scope: [:layouts, :header]) }= t('close_modal', scope: [:layouts, :header])
+ .fr-header__menu-links
+ -# populated by dsfr js
- - if content_for?(:main_navigation)
- = yield(:main_navigation)
- - elsif is_administrateur_context
- = render 'administrateurs/main_navigation'
- - elsif is_instructeur_context || is_expert_context
- = render MainNavigation::InstructeurExpertNavigationComponent.new
- - elsif is_user_context
- = render 'users/main_navigation'
+ - if content_for?(:main_navigation)
+ = yield(:main_navigation)
+ - elsif is_administrateur_context
+ = render 'administrateurs/main_navigation'
+ - elsif is_instructeur_context || is_expert_context
+ = render MainNavigation::InstructeurExpertNavigationComponent.new
+ - elsif is_user_context
+ = render 'users/main_navigation'
- = yield(:notice_info)
+ = yield(:notice_info)
diff --git a/app/views/layouts/_locale_dropdown.html.haml b/app/views/layouts/_locale_dropdown.html.haml
index 287df6ec3..dd2387016 100644
--- a/app/views/layouts/_locale_dropdown.html.haml
+++ b/app/views/layouts/_locale_dropdown.html.haml
@@ -1,4 +1,4 @@
-%nav.fr-translate.fr-nav{ :role => "navigation", title: t('.select_locale') }
+.fr-translate.fr-nav
.fr-nav__item
%button.fr-translate__btn.fr-btn{ "aria-controls" => "translate", "aria-expanded" => "false", :title => t('.select_locale') }
= I18n.locale.upcase
diff --git a/app/views/layouts/_outdated_browser_banner.html.haml b/app/views/layouts/_outdated_browser_banner.html.haml
index cbf9e91cd..c3605fd12 100644
--- a/app/views/layouts/_outdated_browser_banner.html.haml
+++ b/app/views/layouts/_outdated_browser_banner.html.haml
@@ -1,9 +1,7 @@
- if show_outdated_browser_banner?
= render Dsfr::AlertComponent.new(state: :warning, title: "Navigateur trop ancien", heading_level: :h2) do |c|
- c.with_body do
- Votre navigateur internet, #{browser.name} #{browser.version}, est malheureusement trop ancien. Il ne sera plus compatible avec #{APPLICATION_NAME} à partir du
- %strong
- 1 juin 2024.
+ Votre navigateur internet, #{browser.name} #{browser.version}, est malheureusement trop ancien. Il n’est plus compatible avec #{APPLICATION_NAME}.
%br
Veuillez installer un navigateur plus récent en suivant le lien suivant :
%br
diff --git a/app/views/layouts/_search_dossiers_form.html.haml b/app/views/layouts/_search_dossiers_form.html.haml
index f0aab616e..3ae8dc46d 100644
--- a/app/views/layouts/_search_dossiers_form.html.haml
+++ b/app/views/layouts/_search_dossiers_form.html.haml
@@ -3,7 +3,8 @@
%button.fr-btn--close.fr-btn{ "aria-controls" => "search-modal", :title => t('close_modal', scope: [:layouts, :header]) }= t('close_modal', scope: [:layouts, :header])
#search-473.fr-search-bar.fr-search-bar--lg
= form_tag recherche_index_path, method: :get, :role => "search", class: "flex width-100" do
- = label_tag "q", t('views.users.dossiers.search.search_file'), class: 'fr-label'
+ = hidden_field_tag :context, local_assigns[:context]
+ = label_tag "q", t('views.users.dossiers.search.search_file'), class: 'sr-only'
= text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: t('views.users.dossiers.search.search_file'), class: "fr-input"
%button.fr-btn
= t('views.users.dossiers.search.simple')
diff --git a/app/views/layouts/_skiplinks.html.haml b/app/views/layouts/_skiplinks.html.haml
index ccd0f3f18..2e8333d47 100644
--- a/app/views/layouts/_skiplinks.html.haml
+++ b/app/views/layouts/_skiplinks.html.haml
@@ -1,5 +1,3 @@
.fr-skiplinks
%nav.fr-container{ role: "navigation", 'aria-label': t("skiplinks.quick") }
- %ul.fr-skiplinks__list
- %li
- %a.fr-link{ href: "#contenu" }= t('skiplinks.content')
+ %a.fr-link{ href: "#contenu" }= t('skiplinks.content')
diff --git a/app/views/layouts/all.html.haml b/app/views/layouts/all.html.haml
index 2b785e13f..c8c95491e 100644
--- a/app/views/layouts/all.html.haml
+++ b/app/views/layouts/all.html.haml
@@ -1,6 +1,5 @@
- content_for(:main_navigation) do
= render 'administrateurs/main_navigation'
-
- content_for :content do
.fr-container
%h1.fr-my-4w Toutes les démarches
@@ -25,16 +24,31 @@
= link_to all_admin_procedures_path(zone_ids: current_administrateur.zones), { data: { turbo: 'false' } } do
%span.fr-icon-arrow-go-back-line Réinitialiser
%ul
+
%li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" }
.fr-mb-1w
%button{ 'data-action': 'expand#toggle' }
%span.fr-icon-add-line.fr-icon--sm.fr-mr-1w.fr-text-action-high--blue-france{ 'aria-hidden': 'true', 'data-expand-target': 'icon' }
- Mes zones
- .fr-ml-1w{ 'data-expand-target': 'content' }
- = f.collection_check_boxes :zone_ids, @filter.admin_zones, :id, :current_label, include_hidden: false do |b|
- .fr-checkbox-group.fr-ml-2w.fr-py-1w
- = b.check_box(checked: @filter.zone_filtered?(b.value))
- = b.label(class: 'fr-label') { b.text }
+ Thématique
+ .fr-ml-1w.hidden{ 'data-expand-target': 'content' }
+ %div
+ = f.search_field :tags, placeholder: 'Choisissez un thème', list: 'tags_list', class: 'fr-input', data: { no_autosubmit: 'input', turbo_force: :server }, multiple: true
+ %datalist#tags_list
+ - ProcedureTag.order(:name).each do |tag|
+ %option{ value: tag.name, data: { id: tag.id } }
+ - if @filter.tags.present?
+ - @filter.tags.each do |tag|
+ = f.hidden_field :tags, value: tag, multiple: true, id: "tag-#{tag.tr(' ', '_')}"
+
+ %li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" }
+ .fr-mb-1w
+ %button{ 'data-action': 'expand#toggle' }
+ %span.fr-icon-add-line.fr-icon--sm.fr-mr-1w.fr-text-action-high--blue-france{ 'aria-hidden': 'true', 'data-expand-target': 'icon' }
+ Démarches modèles
+ .fr-ml-1w.hidden{ 'data-expand-target': 'content' }
+ .fr-checkbox-group.fr-ml-2w.fr-py-1w
+ = f.check_box :template, class: 'fr-input'
+ = f.label :template, 'Modèle DS', class: 'fr-label'
%li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" }
.fr-mb-1w
%button{ 'data-action': 'expand#toggle' }
@@ -45,6 +59,16 @@
.fr-checkbox-group.fr-ml-2w.fr-py-1w
= b.check_box(checked: @filter.zone_filtered?(b.value))
= b.label(class: 'fr-label') { b.text }
+ %li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" }
+ .fr-mb-1w
+ %button{ 'data-action': 'expand#toggle' }
+ %span.fr-icon-add-line.fr-icon--sm.fr-mr-1w.fr-text-action-high--blue-france{ 'aria-hidden': 'true', 'data-expand-target': 'icon' }
+ Mes zones
+ .fr-ml-1w.hidden{ 'data-expand-target': 'content' }
+ = f.collection_check_boxes :zone_ids, @filter.admin_zones, :id, :current_label, include_hidden: false do |b|
+ .fr-checkbox-group.fr-ml-2w.fr-py-1w
+ = b.check_box(checked: @filter.zone_filtered?(b.value))
+ = b.label(class: 'fr-label') { b.text }
%li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" }
.fr-mb-1w
%button{ 'data-action': 'expand#toggle' }
@@ -61,10 +85,20 @@
.fr-ml-1w.hidden{ 'data-expand-target': 'content' }
%div
= f.select :service_departement,
- APIGeoService.departements.map { ["#{_1[:code]} – #{_1[:name]}", _1[:code]] },
+ APIGeoService.departement_options,
{ selected: @filter.service_departement, include_blank: ''},
id: "service_dep_select",
class: 'fr-select'
+ %li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" }
+ .fr-mb-1w
+ %button{ 'data-action': 'expand#toggle' }
+ %span.fr-icon-add-line.fr-icon--sm.fr-mr-1w.fr-text-action-high--blue-france{ 'aria-hidden': 'true', 'data-expand-target': 'icon' }
+ Type d'usager
+ .fr-ml-1w.hidden{ 'data-expand-target': 'content' }
+ = f.collection_check_boxes :kind_usagers, ['individual', 'personne_morale'], :to_s, :to_s, include_hidden: false do |b|
+ .fr-checkbox-group.fr-ml-2w.fr-py-1w
+ = b.check_box(checked: @filter.kind_usager_filtered?(b.value))
+ = b.label(class: 'fr-label') { t b.text, scope: 'activerecord.attributes.procedure.kind_usager' }
%li.fr-py-2w{ 'data-controller': "expand" }
.fr-mb-1w.fr-pl-2w
%button{ 'data-action': 'click->expand#toggle' }
@@ -86,40 +120,6 @@
= b.check_box(checked: @filter.status_filtered?(b.value))
= b.label(class: 'fr-label') { t b.text, scope: 'activerecord.attributes.procedure.aasm_state' }
- %li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" }
- .fr-mb-1w
- %button{ 'data-action': 'expand#toggle' }
- %span.fr-icon-add-line.fr-icon--sm.fr-mr-1w.fr-text-action-high--blue-france{ 'aria-hidden': 'true', 'data-expand-target': 'icon' }
- Type d'usager
- .fr-ml-1w.hidden{ 'data-expand-target': 'content' }
- = f.collection_check_boxes :kind_usagers, ['individual', 'personne_morale'], :to_s, :to_s, include_hidden: false do |b|
- .fr-checkbox-group.fr-ml-2w.fr-py-1w
- = b.check_box(checked: @filter.kind_usager_filtered?(b.value))
- = b.label(class: 'fr-label') { t b.text, scope: 'activerecord.attributes.procedure.kind_usager' }
- %li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" }
- .fr-mb-1w
- %button{ 'data-action': 'expand#toggle' }
- %span.fr-icon-add-line.fr-icon--sm.fr-mr-1w.fr-text-action-high--blue-france{ 'aria-hidden': 'true', 'data-expand-target': 'icon' }
- Tags
- .fr-ml-1w.hidden{ 'data-expand-target': 'content' }
- %div
- = f.search_field :tags, placeholder: 'Choisissez un tag', list: 'tags_list', class: 'fr-input', data: { no_autosubmit: 'input', turbo_force: :server }, multiple: true
- %datalist#tags_list
- - Procedure.tags.each do |tag|
- %option{ value: tag }
- - if @filter.tags.present?
- - @filter.tags.each do |tag|
- = f.hidden_field :tags, value: tag, multiple: true, id: "tag-#{tag.tr(' ', '_')}"
- %li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" }
- .fr-mb-1w
- %button{ 'data-action': 'expand#toggle' }
- %span.fr-icon-add-line.fr-icon--sm.fr-mr-1w.fr-text-action-high--blue-france{ 'aria-hidden': 'true', 'data-expand-target': 'icon' }
- Démarches modèles
- .fr-ml-1w.hidden{ 'data-expand-target': 'content' }
- .fr-checkbox-group.fr-ml-2w.fr-py-1w
- = f.check_box :template, class: 'fr-input'
- = f.label :template, 'Modèle DS', class: 'fr-label'
-
.fr-col-9
= yield(:results)
= render template: 'layouts/application'
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index aba53cdd3..6c61cb438 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -9,6 +9,8 @@
%meta{ name: "format-detection", content: "telephone=no,date=no,address=no,email=no,url=no" }
= csrf_meta_tags
+ %script{ defer: true, data: { domain: "demarches.dgnum.eu" }, src: "https://analytics.dgnum.eu/js/script.js" }
+
%title
= content_for?(:title) ? "#{sanitize(yield(:title))} · #{Current.application_name}" : Current.application_name
@@ -22,13 +24,6 @@
- if administrateur_signed_in?
= vite_javascript_tag 'track-admin'
- - if vite_legacy?
- = vite_legacy_polyfill_tag
- = vite_legacy_javascript_tag 'application'
- - if administrateur_signed_in?
- = vite_legacy_javascript_tag 'track-admin'
- = vite_legacy_fallback_tag
-
= preload_link_tag(asset_url("Marianne-Regular.woff2"))
= preload_link_tag(asset_url("Spectral-Regular.ttf"))
@@ -52,6 +47,9 @@
#beta
Env Test
+ #sticky-header.sticky-header-container
+ = content_for(:sticky_header)
+
= render partial: "layouts/header"
%main#contenu{ role: :main }
= render partial: "layouts/flash_messages"
@@ -63,7 +61,5 @@
- else
= render 'footer'
- - if Rails.env.development?
- = vite_typescript_tag 'axe-core'
= yield :charts_js
= render Attachment::ProgressBarComponent.new
diff --git a/app/views/layouts/application.turbo_stream.haml b/app/views/layouts/application.turbo_stream.haml
index c359b707a..bb1d0ef95 100644
--- a/app/views/layouts/application.turbo_stream.haml
+++ b/app/views/layouts/application.turbo_stream.haml
@@ -1,7 +1,9 @@
- if flash.any?
- = turbo_stream.replace 'flash_messages', partial: 'layouts/flash_messages'
+ - unique_classname= "u#{SecureRandom.hex}"
+ = turbo_stream.replace 'flash_messages', partial: 'layouts/flash_messages', locals: { unique_classname: }
+ = turbo_stream.focus 'flash_messages'
= turbo_stream.show 'flash_messages'
- = turbo_stream.hide 'flash_messages', delay: 30000
+ = turbo_stream.hide_all ".#{unique_classname}", delay: 15000
- flash.clear
= yield
diff --git a/app/views/layouts/commencer/_no_procedure.html.haml b/app/views/layouts/commencer/_no_procedure.html.haml
index 36fb66cae..a3465ac22 100644
--- a/app/views/layouts/commencer/_no_procedure.html.haml
+++ b/app/views/layouts/commencer/_no_procedure.html.haml
@@ -1,10 +1,4 @@
-.no-procedure
- = image_tag "landing/hero/dematerialiser.svg", class: "paperless-logo", alt: ""
- .baseline.center
- .no-procedure-presentation
- %p.simple= t('.line1')
- %p= t('.line2')
- %p= t('.line3')
- %hr
- %p.small-simple= t('.are_you_new', app_name: Current.application_name)
- = link_to t('views.users.sessions.new.find_procedure'), t("links.common.faq.comment_trouver_ma_demarche_url"), title: new_tab_suffix(t('views.users.sessions.new.find_procedure')), class: "fr-btn fr-btn--secondary", **external_link_attributes
+.center
+ = image_tag "landing/hero/dematerialiser.svg", class: "fr-responsive-img fr-mb-1v", alt: "", "aria-hidden": "true"
+ %p.fr-m-4w= t('.text')
+ %hr
diff --git a/app/views/layouts/mailers/_jdma.html.haml b/app/views/layouts/mailers/_jdma.html.haml
new file mode 100644
index 000000000..4d6ce0af9
--- /dev/null
+++ b/app/views/layouts/mailers/_jdma.html.haml
@@ -0,0 +1,10 @@
+= vertical_margin(50)
+
+%div{ align: "center" }
+ %p
+ %strong Aidez-nous à améliorer ce service !
+ %br
+ Donnez-nous votre avis, cela ne prend que 2 minutes.
+ != @jdma_html
+
+= vertical_margin(20)
diff --git a/app/views/layouts/mailers/layout.html.erb b/app/views/layouts/mailers/layout.html.erb
index a7067a244..63b7e1784 100644
--- a/app/views/layouts/mailers/layout.html.erb
+++ b/app/views/layouts/mailers/layout.html.erb
@@ -98,7 +98,7 @@
" src="<%= image_url(MAILER_LOGO_SRC) %>" style="max-height:100px; padding:15px 30px 15px 30px; vertical-aligne:middle; display:inline !important; border:0; height:auto; outline:none; text-decoration:none; -ms-interpolation-mode:bicubic;" />
-
+
<%= Current.application_name %>
diff --git a/app/views/manager/administrateurs/data_exports.html.erb b/app/views/manager/administrateurs/data_exports.html.erb
new file mode 100644
index 000000000..1e3d897cd
--- /dev/null
+++ b/app/views/manager/administrateurs/data_exports.html.erb
@@ -0,0 +1,19 @@
+
+
+ Export des administrateurs et instructeurs
+
+
+
+
+ Pour les invitations aux webinaires
+
+
+ Pour les newsletters
+
+
diff --git a/app/views/manager/application/_javascript.html.erb b/app/views/manager/application/_javascript.html.erb
index 570441489..56f773546 100644
--- a/app/views/manager/application/_javascript.html.erb
+++ b/app/views/manager/application/_javascript.html.erb
@@ -11,6 +11,8 @@ by providing a `content_for(:javascript)` block.
<%= javascript_include_tag js_path %>
<% end %>
+<%= vite_client_tag %>
+<%= vite_react_refresh_tag %>
<%= vite_typescript_tag 'manager' %>
<%= yield :javascript %>
diff --git a/app/views/manager/application/_navigation.html.erb b/app/views/manager/application/_navigation.html.erb
index f2358dd28..83dc6da3e 100644
--- a/app/views/manager/application/_navigation.html.erb
+++ b/app/views/manager/application/_navigation.html.erb
@@ -29,6 +29,7 @@ as defined by the routes in the `admin/` namespace
<%= link_to "Features", manager_flipper_path, class: "navigation__link" %>
<%= link_to "Annonces", super_admins_release_notes_path, class: "navigation__link" %>
<%= link_to "Import data via CSV", manager_import_procedure_tags_path, class: "navigation__link" %>
+ <%= link_to "Export CSV data", manager_data_exports_path, class: "navigation__link" %>
<% if Rails.application.secrets.sendinblue[:enabled] && ENV["SAML_IDP_ENABLED"] == "enabled" %>
<%= link_to "Sendinblue", ENV.fetch("SENDINBLUE_LOGIN_URL"), class: "navigation__link", target: '_blank' %>
<% end %>
diff --git a/app/views/manager/dossiers/transfer_edit.html.erb b/app/views/manager/dossiers/transfer_edit.html.erb
index 7fcb26555..ab45a4722 100644
--- a/app/views/manager/dossiers/transfer_edit.html.erb
+++ b/app/views/manager/dossiers/transfer_edit.html.erb
@@ -12,7 +12,7 @@
User
- <%= link_to @dossier.user.email, manager_user_path(@dossier.user) %>
+ <%= link_to @dossier.user_email_for(:notification), manager_user_path(@dossier.user) %>
Text summary
diff --git a/app/views/manager/instructeurs/show.html.erb b/app/views/manager/instructeurs/show.html.erb
index 4ce7efb4b..b2ef2c6e1 100644
--- a/app/views/manager/instructeurs/show.html.erb
+++ b/app/views/manager/instructeurs/show.html.erb
@@ -29,7 +29,7 @@ as well as a link to its edit page.
'Modifier',
[:edit, namespace, page.resource],
class: "button",
- ) if valid_action?(:edit) && show_action?(:edit, page.resource) %>
+ ) if accessible_action?(page.resource, :edit) %>
<%= link_to 'Réinviter', reinvite_manager_instructeur_path(instructeur), method: :post, class: 'button' %>
diff --git a/app/views/manager/outdated_procedures/_collection.html.erb b/app/views/manager/outdated_procedures/_collection.html.erb
index 1a966d96b..6f48a14e1 100644
--- a/app/views/manager/outdated_procedures/_collection.html.erb
+++ b/app/views/manager/outdated_procedures/_collection.html.erb
@@ -78,7 +78,7 @@ to display a collection of resources in an HTML table.
<% collection_presenter.attributes_for(resource).each do |attribute| %>
|
- <% if show_action? :show, resource -%>
+ <% if accessible_action?(resource, :show) -%>
+ ) if accessible_action?(page.resource_name, :new) %>
diff --git a/app/views/manager/procedures/show.html.erb b/app/views/manager/procedures/show.html.erb
index 38e6919e9..703c492d1 100644
--- a/app/views/manager/procedures/show.html.erb
+++ b/app/views/manager/procedures/show.html.erb
@@ -31,7 +31,7 @@ as well as a link to its edit page.
t("administrate.actions.edit_resource", name: page.page_title),
[:edit, namespace, page.resource],
class: "button",
- ) if valid_action? :edit %>
+ ) if accessible_action?(page.resource, :edit) %>
<%= link_to 'Aperçu', apercu_admin_procedure_path(procedure), class: 'button' %>
@@ -77,7 +77,7 @@ as well as a link to its edit page.
J'utilise cette option ETQ support quand un usager a besoin de devenir administrateur sur une démarche
<% end %>
- <% if procedure.administrateurs.any? { |admin| admin.email == current_super_admin.email } %>
+ <% if procedure.administrateurs.any? { |admin| admin.email == current_super_admin.email } && procedure.instructeurs.any? { |instructeur| instructeur.email == current_super_admin.email } %>
Vous êtes administrateur de cette démarche. Aller à la démarche
<%= link_to("ETQ admin", admin_procedure_path(procedure), **external_link_attributes) %>
ou
@@ -93,16 +93,15 @@ as well as a link to its edit page.
<% elsif attribute.name == 'tags' %>
<%= form_for procedure, url: add_tags_manager_procedure_path(procedure), html: { class: 'form procedure-form__column--form fr-background-alt--blue-france mt-1' } do %>
- <%= hidden_field_tag 'procedure[tags]', nil %>
- <%= react_component("ComboMultiple",
- options: Procedure.tags,
- selected: procedure.tags,
- disabled: [],
- label: 'Tags',
- group: '.procedure-form__column--form',
- name: 'tags',
- describedby: 'procedure-tags',
- acceptNewValues: true) %>
+
+ <%= render ReactComponent.new "ComboBox/MultiComboBox",
+ items: Procedure.tags,
+ selected_keys: procedure.tags,
+ value_separator: ',|;',
+ allows_custom_value: true,
+ name: 'procedure[tags][]',
+ 'aria-label': 'Tags' %>
+
<% end %>
diff --git a/app/views/manager/published_procedures/index.html.erb b/app/views/manager/published_procedures/index.html.erb
new file mode 100644
index 000000000..05754ba9e
--- /dev/null
+++ b/app/views/manager/published_procedures/index.html.erb
@@ -0,0 +1,66 @@
+<%#
+# Index
+
+This view is the template for the index page.
+It is responsible for rendering the search bar, header and pagination.
+It renders the `_table` partial to display details about the resources.
+
+## Local variables:
+
+- `page`:
+ An instance of [Administrate::Page::Collection][1].
+ Contains helper methods to help display a table,
+ and knows which attributes should be displayed in the resource's table.
+- `resources`:
+ An instance of `ActiveRecord::Relation` containing the resources
+ that match the user's search criteria.
+ By default, these resources are passed to the table partial to be displayed.
+- `search_term`:
+ A string containing the term the user has searched for, if any.
+- `show_search_bar`:
+ A boolean that determines if the search bar should be shown.
+
+[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
+%>
+
+<% content_for(:title) do %>
+ <%= display_resource_name(page.resource_name) %>
+<% end %>
+
+
+
+ <%= content_for(:title) %>
+
+
+ <% if show_search_bar %>
+ <%= render(
+ "search",
+ search_term: search_term,
+ resource_name: display_resource_name(page.resource_name)
+ ) %>
+ <% end %>
+
+
+ <%= link_to(
+ t(
+ "administrate.actions.new_resource",
+ name: display_resource_name(page.resource_name, singular: true).downcase
+ ),
+ [:new, namespace, page.resource_path.to_sym],
+ class: "button",
+ ) if accessible_action?(page.resource_name, :new) %>
+
+
+
+
+ <%= render(
+ "collection",
+ collection_presenter: page,
+ collection_field_name: resource_name,
+ page: page,
+ resources: resources,
+ table_title: "page-title"
+ ) %>
+
+ <%= render "pagination", resources: resources %>
+
diff --git a/app/views/manager/users/show.html.erb b/app/views/manager/users/show.html.erb
index 41f353302..ac1881a35 100644
--- a/app/views/manager/users/show.html.erb
+++ b/app/views/manager/users/show.html.erb
@@ -26,6 +26,14 @@ as well as a link to its edit page.
+ <% if user.unverified_email? %>
+ <%= link_to(
+ "Débloquer mails",
+ [:unblock_mails, namespace, page.resource],
+ method: :post,
+ class: "button") %>
+ <% end %>
+
<%= link_to(
"Modifier",
edit_manager_user_path(page.resource),
diff --git a/app/views/notification_mailer/send_notification.html.haml b/app/views/notification_mailer/send_notification.html.haml
index b64553e7a..af6b90e4d 100644
--- a/app/views/notification_mailer/send_notification.html.haml
+++ b/app/views/notification_mailer/send_notification.html.haml
@@ -9,5 +9,8 @@
- if @services_publics_plus_url.present?
= render 'layouts/mailers/services_publics_plus'
+- if @jdma_html.present?
+ = render 'layouts/mailers/jdma'
+
- content_for :footer do
= render 'layouts/mailers/service_footer', service: @service, dossier: @dossier
diff --git a/app/views/notification_mailer/send_notification_for_tiers.html.haml b/app/views/notification_mailer/send_notification_for_tiers.html.haml
index 83c73e69d..565c80cc7 100644
--- a/app/views/notification_mailer/send_notification_for_tiers.html.haml
+++ b/app/views/notification_mailer/send_notification_for_tiers.html.haml
@@ -22,7 +22,7 @@
%p
= t("layouts.mailers.for_tiers.second_part")
- = "#{mail_to(@dossier.user.email)}."
+ = "#{mail_to(@dossier.user_email_for(:notification))}."
%p
= t(:best_regards, scope: [:views, :shared, :greetings])
diff --git a/app/views/phishing_alert_mailer/notify.html.haml b/app/views/phishing_alert_mailer/notify.html.haml
new file mode 100644
index 000000000..131a86784
--- /dev/null
+++ b/app/views/phishing_alert_mailer/notify.html.haml
@@ -0,0 +1,17 @@
+= content_for(:title, @subject)
+
+%p Bonjour
+
+%p Nous pensons que votre compte #{@user.email} a été la cible d'une tentative #{link_to("d'hameçonnage (phishing)", "https://www.service-public.fr/particuliers/vosdroits/F34800") }.
+
+%p Par mesure de précaution, nous avons réinitialisé votre mot de passe.
+
+%h3 Que devez-vous faire maintenant ?
+
+%ol
+ %li Pour accéder à votre compte, vous devez définir un nouveau mot de passe sur le site #{Current.application_name}. Sur la page de connexion, cliquez sur le lien "Mot de passe oublié" et suivez les instructions.
+ %li Nous vous recommandons de vérifier vos dossiers et de nous signaler tout problème en nous contactant à l'adresse suivante : #{mail_to(CONTACT_EMAIL)}.
+
+%p Nous restons à votre disposition pour toute question.
+
+= render partial: "layouts/mailers/signature"
diff --git a/app/views/recherche/index.html.haml b/app/views/recherche/index.html.haml
index 7fb4bce78..bb2964eb5 100644
--- a/app/views/recherche/index.html.haml
+++ b/app/views/recherche/index.html.haml
@@ -102,6 +102,7 @@
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
close_to_expiration: nil,
hidden_by_administration: nil,
+ hidden_by_expired: nil,
sva_svr: p.sva_svr_decision_on.present?,
has_blocking_pending_correction: p.pending_correction? && Flipper.enabled?(:blocking_pending_correction, ProcedureFlipperActor.new(procedure_id)),
turbo: false,
diff --git a/app/views/root/_footer.html.haml b/app/views/root/_footer.html.haml
index f1d296ce1..8c0ac4015 100644
--- a/app/views/root/_footer.html.haml
+++ b/app/views/root/_footer.html.haml
@@ -6,54 +6,48 @@
.fr-col-12.fr-col-sm-3.fr-col-md-3
%h3.fr-footer__top-cat= t("links.footer.top_labels.communication")
%ul.fr-footer__top-list
- %li.fr-footer__top-link
- = link_to t("links.footer.releases.label"), t("links.footer.releases.url"), title: t("links.footer.releases.title"), class: "fr-footer__top-link"
- %li.fr-footer__top-link
- = link_to t("links.footer.contact.label"), contact_path, title: t("links.footer.contact.title"), class: "fr-footer__top-link"
+ %li
+ = link_to t("links.footer.releases.label"), t("links.footer.releases.url"), title: t("links.footer.releases.title"), class: "fr-footer__top-link", hreflang: "fr"
+ %li
+ = link_to t("links.footer.contact.label"), contact_path, class: "fr-footer__top-link"
.fr-col-12.fr-col-sm-3.fr-col-md-3
%h3.fr-footer__top-cat= t("links.footer.top_labels.legals")
%ul.fr-footer__top-list
- %li.fr-footer__top-link
- = link_to t("links.footer.mentions_legales.label"), MENTIONS_LEGALES_URL, title: t("links.footer.mentions_legales.title"), class: "fr-footer__top-link", rel: "noopener noreferrer"
- %li.fr-footer__top-link
- = link_to t("links.footer.suivi.label"), suivi_path, title: t("links.footer.suivi.title"), class: "fr-footer__top-link"
- %li.fr-footer__top-link
- = link_to t("links.footer.stats.label"), stats_path, title: t("links.footer.stats.title"), class: "fr-footer__top-link"
- %li.fr-footer__top_link
+ %li
+ = link_to t("links.footer.mentions_legales.label"), MENTIONS_LEGALES_URL, class: "fr-footer__top-link", rel: "noopener noreferrer"
+ %li
+ = link_to t("links.footer.suivi.label"), suivi_path, class: "fr-footer__top-link", hreflang: "fr"
+ %li
+ = link_to t("links.footer.stats.label"), stats_path, class: "fr-footer__top-link", hreflang: "fr"
+ %li
= link_to t("links.footer.carte.label"), carte_path, title: t("links.footer.carte.title"), class: "fr-footer__top-link"
- %li.fr-footer__top-link
- = link_to t("links.footer.cgu.label"), t("links.footer.cgu.url"), title: t("links.footer.cgu.title"), class: "fr-footer__top-link", rel: "noopener noreferrer"
+ %li
+ %a.fr-footer__top-link{ :href => t("links.footer.cgu.url"), :rel => "noopener noreferrer", :hreflang => "fr" }
+ %abbr{ title: t("links.footer.cgu.title") }
+ = t("links.footer.cgu.label")
.fr-col-12.fr-col-sm-3.fr-col-md-3
%h3.fr-footer__top-cat= t("links.footer.top_labels.resources")
%ul.fr-footer__top-list
- %li.fr-footer__top-link
- = link_to t("links.footer.doc.label"), t("links.footer.doc.url"), title: t("links.footer.doc.title"), class: "fr-footer__top-link", rel: "noopener noreferrer"
- %li.fr-footer__top-link
- = link_to t("links.footer.api_doc.label"), t("links.footer.api_doc.url"), title: t("links.footer.api_doc.title"), class: "fr-footer__top-link", rel: "noopener noreferrer"
- %li.fr-footer__top-link
- = link_to t("links.common.faq.label"), t("links.common.faq.url"), title: t("links.common.faq.title"), class: "fr-footer__top-link", rel: "noopener noreferrer"
- %li.fr-footer__top-link
- = link_to t("links.footer.code.label"), t("links.footer.code.url"), title: t("links.footer.code.title"), class: "fr-footer__top-link", rel: "noopener noreferrer"
+ %li
+ = link_to t("links.footer.doc.label"), t("links.footer.doc.url"), title: t("links.footer.doc.title"), class: "fr-footer__top-link", rel: "noopener noreferrer", hreflang: "fr"
+ %li
+ = link_to t("links.footer.api_doc.label"), t("links.footer.api_doc.url"), title: t("links.footer.api_doc.title"), class: "fr-footer__top-link", rel: "noopener noreferrer", hreflang: "fr"
+ %li
+ = link_to t("links.footer.code.label"), t("links.footer.code.url"), class: "fr-footer__top-link", rel: "noopener noreferrer", hreflang: "fr"
.fr-col-12.fr-col-sm-3.fr-col-md-3
%h3.fr-footer__top-cat= t("links.footer.top_labels.diagnostic")
%ul.fr-footer__top-list
- %li.fr-footer__top-link
- = link_to t("links.footer.status_page.label"), t("links.footer.status_page.url"), title: t("links.footer.status_page.title"), class: "fr-footer__top-link", rel: "noopener noreferrer"
- %li.fr-footer__top-link
- = link_to t("links.footer.security.label"), t("links.footer.security.url"), title: t("links.footer.security.title"), class: "fr-footer__top-link", rel: "noopener noreferrer"
+ %li
+ = link_to t("links.footer.status_page.label"), t("links.footer.status_page.url"), title: t("links.footer.status_page.title"), class: "fr-footer__top-link", rel: "noopener noreferrer", hreflang: "fr"
+ %li
+ = link_to t("links.footer.security.label"), t("links.footer.security.url"), title: t("links.footer.security.title"), class: "fr-footer__top-link", rel: "noopener noreferrer", hreflang: "fr"
.fr-container
.fr-footer__body
- .fr-footer__brand.fr-enlarge-link{ lang: "fr" }
- %p.fr-logo
- gouvernement
- = link_to t("links.footer.dinum.url"), title: t("links.footer.dinum.title"), class: "fr-footer__brand-link" do
- = image_tag("footer/logo-dinum.svg", class: "fr-footer__logo logo-beta-gouv-fr", alt: t("links.footer.dinum.alt"))
- .fr-footer__content
%p.fr-footer__content-desc
= t('links.footer.description_1')
- = link_to(t('links.footer.link_1_label'), t('links.footer.link_1_url'), title: new_tab_suffix(t('links.footer.link_1_label')), **external_link_attributes) + "."
+ = link_to(t('links.footer.link_1_label'), t('links.footer.link_1_url'), title: new_tab_suffix(t('links.footer.link_1_label')), hreflang:'fr', **external_link_attributes) + "."
%p.fr-footer__content-desc
- = link_to t('links.footer.link_2_label'), t("links.footer.code.url"), title: new_tab_suffix(t('links.footer.link_2_label')), **external_link_attributes
+ = link_to t('links.footer.link_2_label'), t("links.footer.code.url"), title: new_tab_suffix(t('links.footer.link_2_label')), hreflang:'fr', **external_link_attributes
= t('links.footer.description_2')
= render partial: "shared/footer_content_list"
diff --git a/app/views/root/administration.html.haml b/app/views/root/administration.html.haml
index c6ff2667e..cf1687a5a 100644
--- a/app/views/root/administration.html.haml
+++ b/app/views/root/administration.html.haml
@@ -16,13 +16,6 @@
.fr-py-6w.fr-background-alt--blue-france
.container
.role-panel-wrapper.role-administrations-panel
- .role-panel-70
- %h2 Est-ce fait pour mon administration ?
- %p.fr-h5 Découvrez notre outil et posez nous vos questions lors de notre démonstration en ligne ou lisez notre documentation
-
- = link_to "Consulter notre vidéo de démonstration", DEMO_VIDEO_URL, class: "fr-btn fr-btn--lg fr-mr-1w fr-mb-2w", **external_link_attributes
- = link_to "Documentation", DOC_URL, class: "fr-btn fr-btn--secondary fr-btn--lg", **external_link_attributes
-
.role-panel-30.role-more-info-image.fr-mt-2w
%img.role-image{ :src => image_url("landing/roles/usagers.svg"), alt: "" }
@@ -74,28 +67,22 @@
.fr-py-6w.fr-background-alt--blue-france
.container
%h2.center.fr-mb-4w #{Current.application_name} en chiffres
- %ul.numbers
- %li.number
- .number-value
+ %dl.numbers
+ .number
+ %dt.number-label
+ administrations partenaires
+ %dd.number-value
= number_with_delimiter(Administrateur.with_publiees_ou_closes.uniq.count, :locale => :fr)
- .number-label<
- administrations
- %br<>
- partenaires
- %li.number
- .number-value
+ .number
+ %dt.number-label
+ dossiers déposés
+ %dd.number-value
= number_with_delimiter(Dossier.state_not_brouillon.count, :locale => :fr)
- .number-label<
- dossiers
- %br<>
- déposés
- %li.number
- .number-value
+ .number
+ %dt.number-label
+ de réduction des délais de traitement
+ %dd.number-value
= "#{number_with_delimiter(50, :locale => :fr)} %"
- .number-label<
- de réduction
- %br<>
- des délais de traitement
= render partial: "root/users" if LANDING_USERS_ENABLED
@@ -105,17 +92,3 @@
= render Dsfr::CardVerticalComponent.new(title: "Vous êtes prêt pour dématérialiser ?", desc: "Réduisez vos temps d’instruction de 50 %") do |c|
- c.with_footer_button do
= link_to("Créer votre compte administrateur", DEMANDE_INSCRIPTION_ADMIN_PAGE_URL, class: "fr-btn", **external_link_attributes)
-
- .fr-col-md-6.fr-col-12
- = render Dsfr::CardVerticalComponent.new(title: "Vous voulez en savoir plus ?", desc: "Participez à notre prochain Webinaire") do |c|
- - c.with_footer_button do
- = link_to("Inscription à notre prochain webinaire", INSCRIPTION_WEBINAIRE_URL, class: "fr-btn", **external_link_attributes)
-
- .fr-py-6w.fr-background-alt--blue-france
- .container
- .cta-panel-wrapper
- %div
- %h2 Une question, un problème ?
- %p.fr-h5 Consultez notre FAQ
- %div
- = link_to "Voir la FAQ", t("links.common.faq.url"), class: "fr-btn fr-btn--lg", **external_link_attributes
diff --git a/app/views/root/landing.html.haml b/app/views/root/landing.html.haml
index 851a8bb47..564735896 100644
--- a/app/views/root/landing.html.haml
+++ b/app/views/root/landing.html.haml
@@ -11,7 +11,7 @@
= t(".promise")
.hero-illustration
- %img{ :src => image_url("landing/hero/dematerialiser.svg"), alt: '', width: 499, height: 280, loading: 'lazy' }
+ %img{ :src => image_url("landing/hero/dematerialiser.svg"), alt: '', width: 499, height: 280, loading: 'lazy', 'aria-hidden': 'true' }
.fr-background-alt--blue-france.fr-py-6w
.container
@@ -23,35 +23,25 @@
%h2= t(".have_a_procedure")
%p.fr-h5= t(".fill_procedure")
- = link_to t(".how_to_find_procedure"), t("links.common.faq.comment_trouver_ma_demarche_url"), class: "fr-btn fr-btn--lg fr-mr-1w fr-mb-2w", title: new_tab_suffix(t(".how_to_find_procedure")), **external_link_attributes
= link_to t("views.users.sessions.new.connection"), new_user_session_path, class: "fr-btn fr-btn--secondary fr-btn--lg"
.fr-py-6w
.container
%h2.center.fr-mb-4w= t(".our_numbers", name: Current.application_name)
- cache [I18n.locale, "numbers-panel"], expires_in: 3.hours do
- %ul.numbers
- %li.number
- .number-value
+ %dl.numbers
+ .number
+ %dt.number-label= t(".numbers.administrations")
+ %dd.number-value
= number_with_delimiter(@stat&.administrations_partenaires)
- .number-label= t(".numbers.administrations")
- %li.number
- .number-value
+ .number
+ %dt.number-label= t(".numbers.files")
+ %dd.number-value
= number_with_delimiter(@stat&.dossiers_not_brouillon)
- .number-label= t(".numbers.files")
- %li.number
- .number-value
+ .number
+ %dt.number-label.number-label-third= t(".numbers.processing_time")
+ %dd.number-value
= "#{number_with_delimiter(50)} %"
- .number-label.number-label-third= t(".numbers.processing_time")
-
- .fr-background-alt--blue-france.fr-py-6w
- .container
- .cta-panel-wrapper
- %div
- %h2= t(".question")
- %p.fr-h5= t(".answer_in_faq")
- %div
- = link_to t(".online_help"), t("links.common.faq.url"), class: "fr-btn fr-btn--lg", title: new_tab_suffix(t(".online_help")), **external_link_attributes
.fr-py-6w
.container
diff --git a/app/views/root/patron.html.haml b/app/views/root/patron.html.haml
index 968b94d70..e6b2dd10e 100644
--- a/app/views/root/patron.html.haml
+++ b/app/views/root/patron.html.haml
@@ -153,11 +153,6 @@
%span.label.refused .label.refused
%span.label.without-continuation .label.without-continuation
- %h1 Badges
-
- %span.badge 1
- %span.badge.warning 1
-
%h1 Cards
.card
diff --git a/app/views/root/suivi.html.haml b/app/views/root/suivi.html.haml
index 7ddcb9a7f..af08c0d0c 100644
--- a/app/views/root/suivi.html.haml
+++ b/app/views/root/suivi.html.haml
@@ -8,8 +8,6 @@
%p
Ce site dépose un petit fichier texte (un « cookie ») sur votre ordinateur lorsque vous le consultez. Cela nous permet de mesurer le nombre de visites et de comprendre quelles sont les pages les plus consultées.
- %iframe{ :src => MATOMO_IFRAME_URL }
-
%h2.fr-my-4w Ce site n’affiche pas de bannière de consentement aux cookies, pourquoi ?
%p
C’est vrai, vous n’avez pas eu à cliquer sur un bloc qui recouvre la moitié de la page pour dire que vous êtes d’accord avec le dépôt de cookies.
@@ -18,7 +16,7 @@
Rien d’exceptionnel, pas de passe-droit. Nous respectons simplement la loi, qui dit que certains outils de suivi d’audience, correctement configurés pour respecter la vie privée, sont exemptés d’autorisation préalable.
%br
%br
- Nous utilisons pour cela Matomo, un outil libre, paramétré pour être en conformité avec la recommandation « Cookies » de la CNIL. Cela signifie que votre adresse IP, par exemple, est anonymisée avant d’être enregistrée. Il est donc impossible d’associer vos visites sur ce site à votre personne.
+ Nous utilisons pour cela Plausible, un outil libre, paramétré pour être en conformité avec la recommandation « Cookies » de la CNIL. Cela signifie que votre adresse IP, par exemple, est anonymisée avant d’être enregistrée. Il est donc impossible d’associer vos visites sur ce site à votre personne.
%h2.fr-my-4w Comment désactiver le suivi statistique sur mon navigateur ?
%p
diff --git a/app/views/shared/_footer_content_list.html.haml b/app/views/shared/_footer_content_list.html.haml
index ed162a378..03d4009df 100644
--- a/app/views/shared/_footer_content_list.html.haml
+++ b/app/views/shared/_footer_content_list.html.haml
@@ -1,9 +1,5 @@
%ul.fr-footer__content-list
%li.fr-footer__content-item
- = link_to t('users.procedure_footer.official_links.legifrance.title'), t('users.procedure_footer.official_links.legifrance.url'), title: new_tab_suffix(t('users.procedure_footer.official_links.legifrance.title')), class: 'fr-footer__content-link', **external_link_attributes
+ = link_to t('users.procedure_footer.official_links.dgnum.title'), t('users.procedure_footer.official_links.dgnum.url'), title: new_tab_suffix(t('users.procedure_footer.official_links.dgnum.title')), class: 'fr-footer__content-link', **external_link_attributes
%li.fr-footer__content-item
- = link_to t('users.procedure_footer.official_links.gouvernement.title'), t('users.procedure_footer.official_links.gouvernement.url'), title: new_tab_suffix(t('users.procedure_footer.official_links.gouvernement.title')), class: 'fr-footer__content-link', **external_link_attributes
- %li.fr-footer__content-item
- = link_to t('users.procedure_footer.official_links.service_public.title'), t('users.procedure_footer.official_links.service_public.url'), title: new_tab_suffix(t('users.procedure_footer.official_links.service_public.title')), class: 'fr-footer__content-link', **external_link_attributes
- %li.fr-footer__content-item
- = link_to t('users.procedure_footer.official_links.data_gouv.title'), t('users.procedure_footer.official_links.data_gouv.url'), title: new_tab_suffix(t('users.procedure_footer.official_links.data_gouv.title')), class: 'fr-footer__content-link', **external_link_attributes
+ = link_to t('users.procedure_footer.official_links.ens.title'), t('users.procedure_footer.official_links.ens.url'), title: new_tab_suffix(t('users.procedure_footer.official_links.ens.title')), class: 'fr-footer__content-link', **external_link_attributes
diff --git a/app/views/shared/_footer_copy.html.haml b/app/views/shared/_footer_copy.html.haml
index 86dc24fcb..d3818a219 100644
--- a/app/views/shared/_footer_copy.html.haml
+++ b/app/views/shared/_footer_copy.html.haml
@@ -1,2 +1,2 @@
.fr-footer__bottom-copy
- %p= t("links.footer.copy_html", link: link_to(t("links.footer.license"), "https://github.com/etalab/licence-ouverte/blob/master/LO.md", title: new_tab_suffix("licence etalab-2.0"), **external_link_attributes))
+ %p= t("links.footer.copy_html", link: link_to(t("links.footer.license"), "https://github.com/etalab/licence-ouverte/blob/master/LO.md", title: new_tab_suffix(t("links.footer.license")), hreflang: "fr", **external_link_attributes))
diff --git a/app/views/shared/_france_connect_login.html.haml b/app/views/shared/_france_connect_login.html.haml
index 688f2a2cc..2837e06e8 100644
--- a/app/views/shared/_france_connect_login.html.haml
+++ b/app/views/shared/_france_connect_login.html.haml
@@ -1,6 +1,6 @@
- if FranceConnectService.enabled?
.france-connect-login
- %h2.fr-h6
+ = tag.public_send(local_assigns.fetch(:heading_level, :h2), class: "fr-h6") do
= t('views.shared.france_connect_login.title')
%p
= t('views.shared.france_connect_login.description')
diff --git a/app/views/shared/_piece_justificative_template.html.haml b/app/views/shared/_piece_justificative_template.html.haml
index abb90edbf..6a6c0226f 100644
--- a/app/views/shared/_piece_justificative_template.html.haml
+++ b/app/views/shared/_piece_justificative_template.html.haml
@@ -1 +1 @@
-= render Dsfr::DownloadComponent.new(attachment: champ.type_de_champ.piece_justificative_template, url: champs_piece_justificative_template_path(champ), name: "Modèle à télécharger", ephemeral_link: administrateur_signed_in? )
+= render Dsfr::DownloadComponent.new(attachment: champ.type_de_champ.piece_justificative_template, url: champs_piece_justificative_template_path(champ.dossier, champ.stable_id, row_id: champ.row_id), name: t('views.shared.piece_justificative.name'), ephemeral_link: administrateur_signed_in?, title: t('views.shared.piece_justificative.title', filename: champ.type_de_champ.piece_justificative_template.blob.filename.to_s) )
diff --git a/app/views/shared/_procedure_description.html.haml b/app/views/shared/_procedure_description.html.haml
index 922d790c8..2b6bd2593 100644
--- a/app/views/shared/_procedure_description.html.haml
+++ b/app/views/shared/_procedure_description.html.haml
@@ -1,8 +1,5 @@
.procedure-logos
- - procedure_logo_alt = ''
- - if procedure.service.present?
- - procedure_logo_alt = "#{procedure.service.nom} − #{procedure.service.organisme}"
- = image_tag procedure.logo_url, alt: procedure_logo_alt
+ = image_tag procedure.logo_url, alt: ''
- if procedure.euro_flag
= image_tag("flag_of_europe.svg", id: 'euro_flag', class: (!procedure.euro_flag ? "hidden" : ""))
%h1.fr-h2
@@ -11,7 +8,7 @@
- if procedure.persisted? && procedure.estimated_duration_visible?
%p
%small
- %span.fr-icon-timer-line
+ %span.fr-icon-timer-line{ "aria-hidden" => "true" }
= t('shared.procedure_description.estimated_fill_duration', estimated_minutes: estimated_fill_duration_minutes(procedure))
- if procedure.auto_archive_on
@@ -48,21 +45,23 @@
#accordion-116.fr-collapse
= h render SimpleFormatComponent.new(procedure.description_pj, allow_a: true)
- - elsif procedure.pieces_jointes_list?
- %section.fr-accordion.pieces_jointes
- %h2.fr-accordion__title
- %button.fr-accordion__btn{ "aria-controls" => "accordion-116", "aria-expanded" => "false" }
- = t('shared.procedure_description.pieces_jointes')
- #accordion-116.fr-collapse
- - if procedure.pieces_jointes_list_without_conditionnal.present?
- %ul
- = render partial: "shared/procedure_pieces_jointes_list", collection: procedure.pieces_jointes_list_without_conditionnal, as: :pj
+ - else
+ - pj_without_condition, pj_with_condition = procedure.public_wrapped_partionned_pjs
+ - if pj_without_condition.present? || pj_with_condition.present?
+ %section.fr-accordion.pieces_jointes
+ %h2.fr-accordion__title
+ %button.fr-accordion__btn{ "aria-controls" => "accordion-116", "aria-expanded" => "false" }
+ = t('shared.procedure_description.pieces_jointes')
+ #accordion-116.fr-collapse
+ - if pj_without_condition.present?
+ %ul
+ = render partial: "shared/procedure_pieces_jointes_list", collection: pj_without_condition, as: :pj
- - if procedure.pieces_jointes_list_with_conditionnal.present?
- %h3.fr-text--sm.fr-mb-0.fr-mt-2w
- = t('shared.procedure_description.pieces_jointes_conditionnal_list_title')
- %ul
- = render partial: "shared/procedure_pieces_jointes_list", collection: procedure.pieces_jointes_list_with_conditionnal, as: :pj
+ - if pj_with_condition.present?
+ %h3.fr-text--sm.fr-mb-0.fr-mt-2w
+ = t('shared.procedure_description.pieces_jointes_conditionnal_list_title')
+ %ul
+ = render partial: "shared/procedure_pieces_jointes_list", collection: pj_with_condition, as: :pj
- estimated_delay_component = Procedure::EstimatedDelayComponent.new(procedure: procedure)
- if estimated_delay_component.render?
diff --git a/app/views/shared/_tab_item.html.haml b/app/views/shared/_tab_item.html.haml
index cd4103ef8..a9de4a2a7 100644
--- a/app/views/shared/_tab_item.html.haml
+++ b/app/views/shared/_tab_item.html.haml
@@ -3,5 +3,5 @@
%span.notifications{ 'aria-label': 'notifications' }
= link_to(url, 'aria-selected': active ? true : nil, class: 'fr-tabs__tab', role: 'tab' ) do
- if badge.present?
- %span.badge.fr-mr-1w= badge
+ %span.fr-badge.fr-badge--blue-ecume.fr-mr-1w= badge
= label
diff --git a/app/views/shared/avis/_form.html.haml b/app/views/shared/avis/_form.html.haml
index 43f3dad28..2b5e00b05 100644
--- a/app/views/shared/avis/_form.html.haml
+++ b/app/views/shared/avis/_form.html.haml
@@ -10,16 +10,9 @@
= render NestedForms::FormOwnerComponent.new
= form_for avis, url: url, html: { multipart: true, data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@dossier, :avis_by_instructeur) } } do |f|
- = hidden_field_tag 'avis[emails]', nil
.fr-input-group
- = react_component("ComboMultiple",
- options: current_expert_not_instructeur? ? [] : @experts_emails,
- selected: [], disabled: [],
- label: 'Emails',
- group: '.ask-avis',
- name: 'emails',
- describedby: 'avis-emails-description',
- acceptNewValues: !@dossier.procedure.experts_require_administrateur_invitation)
+ %react-fragment
+ = render ReactComponent.new "ComboBox/MultiComboBox", items: current_expert_not_instructeur? ? [] : @experts_emails, name: f.field_name(:emails, multiple: true), id: 'avis_emails', 'aria-label': 'Emails', 'aria-describedby': 'avis-emails-description', allows_custom_value: !@dossier.procedure.experts_require_administrateur_invitation
.fr-input-group
= f.label :introduction, t('helpers.label.introduction'), class: 'fr-label'
diff --git a/app/views/shared/champs/carte/_show.html.haml b/app/views/shared/champs/carte/_show.html.haml
index 4b957d132..98534e434 100644
--- a/app/views/shared/champs/carte/_show.html.haml
+++ b/app/views/shared/champs/carte/_show.html.haml
@@ -1,4 +1,5 @@
- if champ.geometry?
- = react_component("MapReader", { featureCollection: champ.to_feature_collection, options: champ.render_options } )
+ %react-fragment.width-100
+ = render ReactComponent.new "MapReader", feature_collection: champ.to_feature_collection, options: champ.render_options
.geo-areas
= render Dossiers::GeoAreasComponent.new(champ:, editing: false)
diff --git a/app/views/shared/champs/piece_justificative/_show.html.haml b/app/views/shared/champs/piece_justificative/_show.html.haml
index dd81dde84..46c3c0f3d 100644
--- a/app/views/shared/champs/piece_justificative/_show.html.haml
+++ b/app/views/shared/champs/piece_justificative/_show.html.haml
@@ -1,4 +1,9 @@
.fr-downloads-group
- %ul
- - champ.piece_justificative_file.attachments.each do |attachment|
- %li= render Attachment::ShowComponent.new(attachment:, new_tab: true)
+ - if profile == 'instructeur'
+ .gallery-items-list
+ - champ.piece_justificative_file.attachments.with_all_variant_records.each do |attachment|
+ = render Attachment::GalleryItemComponent.new(attachment:, gallery_demande: true)
+ - else
+ %ul
+ - champ.piece_justificative_file.attachments.each do |attachment|
+ %li= render Attachment::ShowComponent.new(attachment:, new_tab: true)
diff --git a/app/views/shared/champs/rna/_association.html.haml b/app/views/shared/champs/rna/_association.html.haml
index f0520db90..9fae5d37e 100644
--- a/app/views/shared/champs/rna/_association.html.haml
+++ b/app/views/shared/champs/rna/_association.html.haml
@@ -1,7 +1,6 @@
- case error
- when :invalid
- %p.fr-error-text
- Le numéro RNA doit commencer par un W majuscule suivi de 9 chiffres ou lettres
+ %p.fr-error-text= t('.invalid_number')
- when :not_found
%p.fr-error-text= t('.not_found')
- when :network_error
diff --git a/app/views/shared/champs/rna/_show.html.haml b/app/views/shared/champs/rna/_show.html.haml
index ce989b8e7..588ff8206 100644
--- a/app/views/shared/champs/rna/_show.html.haml
+++ b/app/views/shared/champs/rna/_show.html.haml
@@ -22,13 +22,4 @@
- c.with_value do
%p= l(champ.data[scope].to_date)
- - if champ.data['address'].present?
- = render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.rna_champ.data.address")) do |c|
- - c.with_value do
- %p= champ.full_address
-
- - ['code_insee', 'code_postal'].each do |scope|
- - if champ.data['address'][scope].present?
- = render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.rna_champ.data.#{scope}")) do |c|
- - c.with_value do
- %p= champ.data['address'][scope]
+ = render partial: "shared/dossiers/normalized_address", locals: { address: AddressProxy.new(champ) }
diff --git a/app/views/shared/champs/rnf/_show.html.haml b/app/views/shared/champs/rnf/_show.html.haml
index da251eff6..32f8e14e2 100644
--- a/app/views/shared/champs/rnf/_show.html.haml
+++ b/app/views/shared/champs/rnf/_show.html.haml
@@ -19,12 +19,4 @@
= render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.rnf_champ.data.#{scope}")) do |c|
- c.with_value do
%p= l(champ.data[scope].to_date)
-
- - if champ.data['address'].present?
- - ['label', 'cityCode', 'postalCode'].each do |scope|
- = render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.rnf_champ.data.#{scope}")) do |c|
- - c.with_value do
- %p= champ.data['address'][scope]
- = render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.rnf_champ.data.department")) do |c|
- - c.with_value do
- %p= "#{champ.data['address']['departmentCode']} – #{champ.data['address']['departmentName']}"
+ = render partial: "shared/dossiers/normalized_address", locals: { address: AddressProxy.new(champ) }
diff --git a/app/views/shared/champs/siret/_etablissement.html.haml b/app/views/shared/champs/siret/_etablissement.html.haml
index efaaa901c..fa4c07aeb 100644
--- a/app/views/shared/champs/siret/_etablissement.html.haml
+++ b/app/views/shared/champs/siret/_etablissement.html.haml
@@ -8,7 +8,6 @@
- when :not_found
%p.fr-error-text
Nous n’avons pas trouvé d’établissement correspondant à ce numéro de SIRET.
- = link_to('Plus d’informations', t("links.common.faq.erreur_siret_url"), **external_link_attributes)
- when :network_error
%p.fr-error-text= t('errors.messages.siret_network_error')
diff --git a/app/views/shared/champs/siret/_show.html.haml b/app/views/shared/champs/siret/_show.html.haml
index 7f819c6f8..44e31d87f 100644
--- a/app/views/shared/champs/siret/_show.html.haml
+++ b/app/views/shared/champs/siret/_show.html.haml
@@ -1,2 +1,2 @@
- if champ.etablissement.present?
- = render partial: "shared/dossiers/identite_entreprise", locals: { etablissement: champ.etablissement, profile: profile }
+ = render partial: "shared/dossiers/identite_entreprise", locals: { etablissement: champ.etablissement, profile: profile, champ: }
diff --git a/app/views/shared/dossiers/_demande.html.haml b/app/views/shared/dossiers/_demande.html.haml
index 4a7e9d1d9..62d8f989f 100644
--- a/app/views/shared/dossiers/_demande.html.haml
+++ b/app/views/shared/dossiers/_demande.html.haml
@@ -2,64 +2,63 @@
- content_for(:notice_info) do
= render partial: "shared/dossiers/france_connect_informations_notice", locals: { user_information: dossier.user.france_connect_informations.first }
-.fr-container.counter-start-header-section.dossier-show{ class: class_names("dossier-show-instructeur" => profile =="instructeur") }
- .fr-grid-row.fr-grid-row--center
- .fr-col-12.fr-col-xl-8
- - if profile == 'instructeur' && dossier.termine_and_accuse_lecture?
- = render Dsfr::CalloutComponent.new(title: nil) do |c|
- - c.with_html_body do
- = t('views.shared.dossiers.demande.accuse_lecture')
- - if dossier.accuse_lecture_agreement_at.present?
- = t('views.shared.dossiers.demande.accuse_lecture_with_agreement', agreement: l(dossier.accuse_lecture_agreement_at, format: :long))
- - else
- = t('views.shared.dossiers.demande.accuse_lecture_without_agreement')
+.counter-start-header-section.dossier-show.gallery.gallery-demande{ class: class_names("dossier-show-instructeur" => profile =="instructeur"), "data-controller": "lightbox" }
- %h2.fr-h6.fr-background-alt--grey.fr-mb-0
- .flex-grow.fr-py-3v.fr-px-2w= t('views.shared.dossiers.demande.en_construction')
+ - if profile == 'instructeur' && dossier.termine_and_accuse_lecture?
+ = render Dsfr::CalloutComponent.new(title: nil) do |c|
+ - c.with_html_body do
+ = t('views.shared.dossiers.demande.accuse_lecture')
+ - if dossier.accuse_lecture_agreement_at.present?
+ = t('views.shared.dossiers.demande.accuse_lecture_with_agreement', agreement: l(dossier.accuse_lecture_agreement_at, format: :long))
+ - else
+ = t('views.shared.dossiers.demande.accuse_lecture_without_agreement')
- - if dossier.depose_at.present?
- = render partial: "shared/dossiers/infos_generales", locals: { dossier: dossier, profile: profile }
+ %h2.fr-h6.fr-background-alt--grey.fr-mb-0
+ .flex-grow.fr-py-3v.fr-px-2w= t('views.shared.dossiers.demande.en_construction')
+
+ - if dossier.depose_at.present?
+ = render partial: "shared/dossiers/infos_generales", locals: { dossier: dossier, profile: profile }
- - if dossier.for_tiers?
- %h2.fr-h6.fr-background-alt--grey.fr-mb-0.flex
- .flex-grow.fr-py-3v.fr-px-2w= t('views.shared.dossiers.demande.mandataire_identity')
+ - if dossier.for_tiers?
+ %h2.fr-h6.fr-background-alt--grey.fr-mb-0.flex
+ .flex-grow.fr-py-3v.fr-px-2w= t('views.shared.dossiers.demande.mandataire_identity')
- - if dossier.individual.present? && profile == 'usager' && !dossier.read_only?
- = link_to t('views.shared.dossiers.demande.edit_identity'), identite_dossier_path(dossier), class: 'fr-py-3v fr-btn fr-btn--tertiary-no-outline'
+ - if dossier.individual.present? && profile == 'usager' && !dossier.read_only?
+ = link_to t('views.shared.dossiers.demande.edit_identity'), identite_dossier_path(dossier), class: 'fr-py-3v fr-btn fr-btn--tertiary-no-outline'
- = render partial: "shared/dossiers/mandataire_infos", locals: { user_deleted: dossier.user_deleted?, email: dossier.user_email_for(:display), dossier: dossier }
+ = render partial: "shared/dossiers/mandataire_infos", locals: { user_deleted: dossier.user_deleted?, email: dossier.user_email_for(:display), dossier: dossier }
- .tab-title
- %h2.fr-h6.fr-background-alt--grey.fr-mb-0.flex
- .flex-grow.fr-py-3v.fr-px-2w
- = t('views.shared.dossiers.demande.requester_identity')
+ .tab-title
+ %h2.fr-h6.fr-background-alt--grey.fr-mb-0.flex
+ .flex-grow.fr-py-3v.fr-px-2w
+ = t('views.shared.dossiers.demande.requester_identity')
- - if dossier.identity_updated_at.present? && demande_seen_at&.<(dossier.identity_updated_at)
- %span.fr-badge.fr-badge--new.fr-badge--sm
- = t('views.shared.dossiers.demande.requester_identity_updated_at', date: try_format_datetime(dossier.identity_updated_at))
+ - if dossier.identity_updated_at.present? && demande_seen_at&.<(dossier.identity_updated_at)
+ %span.fr-badge.fr-badge--new.fr-badge--sm
+ = t('views.shared.dossiers.demande.requester_identity_updated_at', date: try_format_datetime(dossier.identity_updated_at))
- - if dossier.etablissement.present? && profile == 'usager' && !dossier.read_only?
- = link_to t('views.shared.dossiers.demande.edit_siret'), siret_dossier_path(dossier), class: 'fr-py-3v fr-btn fr-btn--tertiary-no-outline'
+ - if dossier.etablissement.present? && profile == 'usager' && !dossier.read_only?
+ = link_to t('views.shared.dossiers.demande.edit_siret'), siret_dossier_path(dossier), class: 'fr-py-3v fr-btn fr-btn--tertiary-no-outline'
- - if dossier.individual.present? && profile == 'usager' && !dossier.read_only?
- = link_to t('views.shared.dossiers.demande.edit_identity'), identite_dossier_path(dossier), class: 'fr-py-3v fr-btn fr-btn--tertiary-no-outline'
+ - if dossier.individual.present? && profile == 'usager' && !dossier.read_only?
+ = link_to t('views.shared.dossiers.demande.edit_identity'), identite_dossier_path(dossier), class: 'fr-py-3v fr-btn fr-btn--tertiary-no-outline'
- = render partial: "shared/dossiers/user_infos", locals: { user_deleted: dossier.user_deleted?, email: dossier.user_email_for(:display), for_tiers: dossier.for_tiers?, beneficiaire_mail: dossier.for_tiers? ? dossier.individual.email : ""}
+ = render partial: "shared/dossiers/user_infos", locals: { user_deleted: dossier.user_deleted?, email: dossier.user_email_for(:display), for_tiers: dossier.for_tiers?, beneficiaire_mail: dossier.for_tiers? ? dossier.individual.email : ""}
- - if dossier.individual.present?
- = render partial: "shared/dossiers/identite_individual", locals: { dossier: dossier }
+ - if dossier.individual.present?
+ = render partial: "shared/dossiers/identite_individual", locals: { dossier: dossier }
- - if dossier.etablissement.present?
- .fr-mt-1w.fr-mb-4w.fr-px-2w
- = render partial: "shared/dossiers/identite_entreprise", locals: { etablissement: dossier.etablissement, profile: profile }
+ - if dossier.etablissement.present?
+ .fr-mt-1w.fr-mb-4w.fr-px-2w
+ = render partial: "shared/dossiers/identite_entreprise", locals: { etablissement: dossier.etablissement, profile: profile }
- %h2.fr-h6.fr-background-alt--grey.fr-mb-0.flex
- .flex-grow.fr-py-3v.fr-px-2w= t('views.shared.dossiers.demande.form')
+ %h2.fr-h6.fr-background-alt--grey.fr-mb-0.flex
+ .flex-grow.fr-py-3v.fr-px-2w= t('views.shared.dossiers.demande.form')
- - types_de_champ = dossier.revision.types_de_champ_public
- - if types_de_champ.any? || dossier.procedure.routing_enabled?
- = render ViewableChamp::SectionComponent.new(dossier:, types_de_champ:, demande_seen_at:, profile:)
+ - types_de_champ = dossier.revision.types_de_champ_public
+ - if types_de_champ.any? || dossier.procedure.routing_enabled?
+ = render ViewableChamp::SectionComponent.new(dossier:, types_de_champ:, demande_seen_at:, profile:)
diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml
index 8a24a008f..c797d35f2 100644
--- a/app/views/shared/dossiers/_edit.html.haml
+++ b/app/views/shared/dossiers/_edit.html.haml
@@ -10,7 +10,7 @@
= render NestedForms::FormOwnerComponent.new
= form_for dossier_for_editing, url: brouillon_dossier_url(dossier), method: :patch, html: { id: 'dossier-edit-form', class: 'form', multipart: true, novalidate: 'novalidate' } do |f|
- = render Dossiers::ErrorsFullMessagesComponent.new(dossier: @dossier, errors: @errors || [])
+ = render Dossiers::ErrorsFullMessagesComponent.new(dossier: dossier)
%header.mb-6
.fr-highlight
%p.fr-text--sm
@@ -21,8 +21,10 @@
= render Procedure::NoticeComponent.new(procedure: dossier.procedure)
- = render EditableChamp::SectionComponent.new(dossier: dossier_for_editing, types_de_champ: dossier_for_editing.revision.types_de_champ_public)
+ %fieldset.fr-fieldset= render EditableChamp::SectionComponent.new(dossier: dossier_for_editing, types_de_champ: dossier_for_editing.revision.types_de_champ_public)
= render Dossiers::PendingCorrectionCheckboxComponent.new(dossier: dossier)
+ = render Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: dossier)
+
= render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false)
diff --git a/app/views/shared/dossiers/_edit_annotations.html.haml b/app/views/shared/dossiers/_edit_annotations.html.haml
index 5457cd0c7..653b75cd2 100644
--- a/app/views/shared/dossiers/_edit_annotations.html.haml
+++ b/app/views/shared/dossiers/_edit_annotations.html.haml
@@ -3,7 +3,7 @@
%section.counter-start-header-section
= render NestedForms::FormOwnerComponent.new
= form_for dossier, url: annotations_instructeur_dossier_path(dossier.procedure, dossier), html: { class: 'form', multipart: true } do |f|
- = render EditableChamp::SectionComponent.new(dossier:, types_de_champ: dossier.revision.types_de_champ_private)
+ %fieldset.fr-fieldset= render EditableChamp::SectionComponent.new(dossier:, types_de_champ: dossier.revision.types_de_champ_private)
= render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: true)
- else
diff --git a/app/views/shared/dossiers/_header.html.haml b/app/views/shared/dossiers/_header.html.haml
index c162b080d..5a93fc5b4 100644
--- a/app/views/shared/dossiers/_header.html.haml
+++ b/app/views/shared/dossiers/_header.html.haml
@@ -2,7 +2,7 @@
= procedure_libelle(dossier.procedure)
= status_badge_user(dossier, 'super')
%h2
- = t('views.users.dossiers.show.header.dossier_number', dossier_id: dossier.id)
+ = t('views.users.dossiers.show.header.dossier_number_html', dossier_id: dossier.id)
= t('views.users.dossiers.show.header.created_date', date_du_dossier: I18n.l(dossier.created_at))
= render(partial: 'users/dossiers/expiration_banner', locals: {dossier: dossier})
diff --git a/app/views/shared/dossiers/_identite_entreprise.html.haml b/app/views/shared/dossiers/_identite_entreprise.html.haml
index a7b88fc7f..7d2bd5dbc 100644
--- a/app/views/shared/dossiers/_identite_entreprise.html.haml
+++ b/app/views/shared/dossiers/_identite_entreprise.html.haml
@@ -73,12 +73,7 @@
- c.with_value do
%p= etablissement.entreprise.numero_tva_intracommunautaire
- = render Dossiers::RowShowComponent.new(label: "Adresse") do |c|
- - c.with_value do
- %p
- - etablissement.adresse.split("\n").compact_blank.each do |line|
- = line
- %br
+ = render partial: "shared/dossiers/normalized_address", locals: { address: AddressProxy.new(defined?(champ) ? champ : etablissement)}
= render Dossiers::RowShowComponent.new(label: "Capital social") do |c|
- c.with_value do
diff --git a/app/views/shared/dossiers/_normalized_address.html.haml b/app/views/shared/dossiers/_normalized_address.html.haml
new file mode 100644
index 000000000..698a01cfa
--- /dev/null
+++ b/app/views/shared/dossiers/_normalized_address.html.haml
@@ -0,0 +1,23 @@
+= render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.normalized_address.full_address")) do |c|
+ - c.with_value do
+ %p
+ = address.street_address
+ %br
+ = [address.city_name, address.postal_code].join(" ")
+
+
+- ['city_code', 'postal_code'].each do |scope|
+ - if address.public_send(scope).present?
+ = render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.normalized_address.#{scope}")) do |c|
+ - c.with_value do
+ %p= address.public_send(scope)
+
+- if address.departement_name.present?
+ = render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.normalized_address.department")) do |c|
+ - c.with_value do
+ %p= "#{address.departement_name} – #{address.departement_code}"
+
+- if address.region_name.present?
+ = render Dossiers::RowShowComponent.new(label: t("activemodel.attributes.normalized_address.region")) do |c|
+ - c.with_value do
+ %p= "#{address.region_name} – #{address.region_code}"
diff --git a/app/views/shared/dossiers/_update_contact_information.html.haml b/app/views/shared/dossiers/_update_contact_information.html.haml
new file mode 100644
index 000000000..a4abc0620
--- /dev/null
+++ b/app/views/shared/dossiers/_update_contact_information.html.haml
@@ -0,0 +1,28 @@
+#contact_information
+ - service = dossier&.service || procedure.service
+ - if service.present?
+ %h3.fr-footer__top-cat= I18n.t('users.procedure_footer.managed_by.header')
+ .fr-footer__top-link.fr-pb-3w
+ %span{ lang: :fr }= service.pretty_nom
+ %div{ lang: :fr }
+ = render SimpleFormatComponent.new(service.adresse, class_names_map: {paragraph: 'fr-footer__content-desc'})
+ %h3.fr-footer__top-cat= I18n.t('users.procedure_footer.contact.header')
+ %ul.fr-footer__top-list
+ - if dossier.present? && dossier.messagerie_available?
+ %li
+ = link_to I18n.t('users.procedure_footer.contact.in_app_mail.link'), messagerie_dossier_path(dossier), class: 'fr-footer__link'
+ - elsif service.present?
+ %li
+ %span.fr-footer__top-link
+ = I18n.t('users.procedure_footer.contact.email.link')
+ = link_to service.email, "mailto:#{service.email}", class: "fr-footer__link"
+
+ - if service.telephone.present?
+ %li
+ %span.fr-footer__top-link
+ = I18n.t('users.procedure_footer.contact.phone.label')
+ = link_to I18n.t('users.procedure_footer.contact.phone.link', service_telephone: service.telephone), service.telephone_url, class: 'fr-footer__link'
+
+ - if service.horaires.present?
+ %li
+ = "#{I18n.t('users.procedure_footer.contact.schedule.prefix')}#{formatted_horaires(service.horaires)}"
diff --git a/app/views/shared/dossiers/messages/_form.html.haml b/app/views/shared/dossiers/messages/_form.html.haml
index f36c6c434..f8f9eb09c 100644
--- a/app/views/shared/dossiers/messages/_form.html.haml
+++ b/app/views/shared/dossiers/messages/_form.html.haml
@@ -10,10 +10,11 @@
= render Dsfr::InputComponent.new(form: f, attribute: :body, input_type: :text_area, opts: { rows: 5, placeholder: placeholder, title: placeholder, class: 'fr-input message-textarea'})
- if local_assigns.has_key?(:dossier)
- .fr-mt-3w{ data: { controller: "file-input-reset" } }
- = render Attachment::EditComponent.new(attached_file: commentaire.piece_jointe)
- %button.hidden.fr-btn.fr-btn--tertiary-no-outline.fr-btn--icon-left.fr-icon-delete-line{ data: { 'file-input-reset-target': 'reset', action: 'file-input-reset#reset' } }
- = t('views.shared.messages.remove_file')
+ .fr-mt-3w.fr-input-group
+ = f.label :piece_jointe, class: "fr-label", for: dom_id(commentaire, :piece_jointe)
+ %div{ data: { controller: "file-input-reset", delete_label: t('views.shared.messages.remove_file') } }
+ = render Attachment::MultipleComponent.new(attached_file: commentaire.piece_jointe)
+ %ul{ data: { 'file-input-reset-target': 'fileList' } }
.fr-mt-3w
= f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true }
diff --git a/app/views/shared/help/_help_dropdown_dossier.html.haml b/app/views/shared/help/_help_dropdown_dossier.html.haml
index f14542c8a..82fc2e87a 100644
--- a/app/views/shared/help/_help_dropdown_dossier.html.haml
+++ b/app/views/shared/help/_help_dropdown_dossier.html.haml
@@ -1,17 +1,17 @@
-= render Dropdown::MenuComponent.new(wrapper: :span, wrapper_options: { class: ['help-dropdown']}, menu_options: { id: "help-menu" }) do |menu|
- - menu.with_button_inner_html do
- = t('help')
+.fr-translate.fr-nav
+ .fr-nav__item
+ %button.help-btn.fr-translate__btn.fr-btn{ "aria-controls" => "help-menu", "aria-expanded" => "false" }
+ = t('help')
- - title = dossier.brouillon? ? t("help_dropdown.help_brouillon_title") : t("help_dropdown.help_filled_dossier")
+ #help-menu.help-content.fr-collapse.fr-menu
+ - title = dossier.brouillon? ? t("help_dropdown.help_brouillon_title") : t("help_dropdown.help_filled_dossier")
+ %ul.fr-menu__list
- - if dossier.messagerie_available?
- - menu.with_item do
- = render partial: 'shared/help/dropdown_items/messagerie_item', locals: { dossier: dossier, title: title }
+ - if dossier.messagerie_available?
+ %li.flex
+ = render partial: 'shared/help/dropdown_items/messagerie_item', locals: { dossier: dossier, title: title }
- - elsif dossier.procedure.service.present?
- - menu.with_item do
- = render partial: 'shared/help/dropdown_items/service_item',
- locals: { service: dossier.procedure.service, title: title }
-
- - menu.with_item do
- = render partial: 'shared/help/dropdown_items/faq_item'
+ - elsif dossier.procedure.service.present?
+ %li.flex
+ = render partial: 'shared/help/dropdown_items/service_item',
+ locals: { service: dossier.procedure.service, title: title }
diff --git a/app/views/shared/help/_help_dropdown_instructeur.html.haml b/app/views/shared/help/_help_dropdown_instructeur.html.haml
index 329b80e50..184977ba2 100644
--- a/app/views/shared/help/_help_dropdown_instructeur.html.haml
+++ b/app/views/shared/help/_help_dropdown_instructeur.html.haml
@@ -1,8 +1,9 @@
-= render Dropdown::MenuComponent.new(wrapper: :span, wrapper_options: { class: ['help-dropdown']}, menu_options: { id: "help-menu" }) do |menu|
- - menu.with_button_inner_html do
- = t('help')
+.fr-translate.fr-nav
+ .fr-nav__item
+ %button.help-btn.fr-translate__btn.fr-btn{ "aria-controls" => "help-menu", "aria-expanded" => "false" }
+ = t('help')
- - menu.with_item do
- = render partial: 'shared/help/dropdown_items/faq_item'
- - menu.with_item do
- = render partial: 'shared/help/dropdown_items/email_item'
+ #help-menu.help-content.fr-collapse.fr-menu
+ %ul.fr-menu__list
+ %li
+ = render partial: 'shared/help/dropdown_items/email_item'
diff --git a/app/views/shared/help/_help_dropdown_procedure.html.haml b/app/views/shared/help/_help_dropdown_procedure.html.haml
index dc4ddc14e..5e5daa1b5 100644
--- a/app/views/shared/help/_help_dropdown_procedure.html.haml
+++ b/app/views/shared/help/_help_dropdown_procedure.html.haml
@@ -1,9 +1,10 @@
-= render Dropdown::MenuComponent.new(wrapper: :span, wrapper_options: { class: ['help-dropdown']}, menu_options: { id: "help-menu" }) do |menu|
- - menu.with_button_inner_html do
- = t('help')
+.fr-translate.fr-nav
+ .fr-nav__item
+ %button.help-btn.fr-translate__btn.fr-btn{ "aria-controls" => "help-menu", "aria-expanded" => "false" }
+ = t('help')
- - if procedure.service.present?
- - menu.with_item do
- = render partial: 'shared/help/dropdown_items/service_item', locals: { service: procedure.service, title: t('help_dropdown.procedure_title') }
- - menu.with_item do
- = render partial: 'shared/help/dropdown_items/faq_item'
+ #help-menu.help-content.fr-collapse.fr-menu
+ %ul.fr-menu__list
+ - if procedure.service.present?
+ %li.flex
+ = render partial: 'shared/help/dropdown_items/service_item', locals: { service: procedure.service, title: t('help_dropdown.procedure_title') }
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 dae504d16..029ffa4ed 100644
--- a/app/views/shared/help/dropdown_items/_email_item.html.haml
+++ b/app/views/shared/help/dropdown_items/_email_item.html.haml
@@ -1,5 +1,5 @@
-= mail_to CONTACT_EMAIL, role: 'menuitem' do
+= mail_to CONTACT_EMAIL, class: 'flex' do
%span.fr-icon-mail-fill.fr-text-action-high--blue-france{ "aria-hidden": "true" }
- .dropdown-description.fr-text--sm
- %span.help-dropdown-title= t('help_dropdown.technical_contact_title')
- %p.fr-text--sm= t('help_dropdown.technical_contact_description', contact_email: CONTACT_EMAIL)
+ .fr-pl-1w
+ %h1= t('help_dropdown.technical_contact_title')
+ %p= t('help_dropdown.technical_contact_description', contact_email: 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 e4a4e60ef..264d303e4 100644
--- a/app/views/shared/help/dropdown_items/_faq_item.html.haml
+++ b/app/views/shared/help/dropdown_items/_faq_item.html.haml
@@ -1,6 +1,4 @@
-= link_to t("links.common.faq.url"), title: new_tab_suffix(t('help_dropdown.general_title')), **external_link_attributes, role: 'menuitem' do
- %span.fr-icon-question-fill.fr-text-action-high--blue-france{ "aria-hidden": "true" }
- .dropdown-description.fr-text--sm
- %span.help-dropdown-title
- = t('help_dropdown.problem_title')
- %p.fr-text--sm= t('help_dropdown.problem_description')
+%span.fr-icon-question-fill.fr-text-action-high--blue-france{ "aria-hidden": "true" }
+.fr-pl-1w
+ %h1= t('help_dropdown.problem_title')
+ = link_to t('help_dropdown.problem_description'), t("links.common.faq.url"), title: new_tab_suffix(t('help_dropdown.problem_description')), **external_link_attributes
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 0316831bc..d0684ffee 100644
--- a/app/views/shared/help/dropdown_items/_messagerie_item.html.haml
+++ b/app/views/shared/help/dropdown_items/_messagerie_item.html.haml
@@ -1,5 +1,4 @@
-= link_to messagerie_dossier_path(dossier), role: 'menuitem' do
- %span.fr-icon-mail-fill.fr-text-action-high--blue-france{ "aria-hidden": "true" }
- .dropdown-description.fr-text--sm
- %span.help-dropdown-title= title
- %p.fr-text--sm= t('help_dropdown.contact_instructeur')
+%span.fr-icon-mail-fill.fr-text-action-high--blue-france{ "aria-hidden": "true" }
+.fr-pl-1w
+ %h1= title
+ = link_to t('help_dropdown.contact_instructeur'), messagerie_dossier_path(dossier)
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 208601f51..4774799a2 100644
--- a/app/views/shared/help/dropdown_items/_service_item.html.haml
+++ b/app/views/shared/help/dropdown_items/_service_item.html.haml
@@ -1,14 +1,23 @@
%span.fr-icon-user-fill.fr-text-action-high--blue-france{ "aria-hidden": "true" }
-.dropdown-description.fr-text--sm
- %span.help-dropdown-title= title
- .help-dropdown-service-action
- %p.fr-text--sm= t('help_dropdown.contact_administration')
- %p.fr-text--sm.help-dropdown-service-item
- %span.fr-icon-mail-fill.fr-icon--sm{ "aria-hidden": "true" }
- = link_to service.email, "mailto:#{service.email}", role: 'menuitem'
- %p.fr-text--sm
- %span.fr-icon-phone-fill.fr-icon--sm{ "aria-hidden": "true" }
- = link_to service.telephone, service.telephone_url, role: 'menuitem'
- %p.fr-text--sm
- %span.fr-icon-time-fill.fr-icon--sm{ "aria-hidden": "true" }
- = service.horaires
+.fr-pl-1w
+ %h1= title
+ %p.fr-mb-1w= t('help_dropdown.contact_administration')
+ %dl
+ .flex.fr-mb-1v
+ %dt.fr-mr-1v
+ %span.fr-icon-mail-fill.fr-icon--sm{ "aria-hidden": "true" }
+ %span.visually-hidden= t('layouts.mailers.service_footer.by_email')
+ %dd
+ = link_to service.email, "mailto:#{service.email}"
+ .flex.fr-mb-1v
+ %dt.fr-mr-1v
+ %span.fr-icon-phone-fill.fr-icon--sm{ "aria-hidden": "true" }
+ %span.visually-hidden= t('layouts.mailers.service_footer.by_phone')
+ %dd
+ = link_to service.telephone, service.telephone_url
+ .flex
+ %dt.fr-mr-1v
+ %span.fr-icon-time-fill.fr-icon--sm{ "aria-hidden": "true" }
+ %span.visually-hidden= t('layouts.mailers.service_footer.schedule')
+ %dd
+ = service.horaires
diff --git a/app/views/shared/kaminari/_first_page.html.haml b/app/views/shared/kaminari/_first_page.html.haml
index 4a781e745..724f52f9f 100644
--- a/app/views/shared/kaminari/_first_page.html.haml
+++ b/app/views/shared/kaminari/_first_page.html.haml
@@ -1,2 +1,2 @@
%li
- = link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote, class: 'fr-pagination__link fr-pagination__link--first', 'aria-disabled': true, title: 'Première page', role: 'link'
+ = link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote, class: 'fr-pagination__link fr-pagination__link--first', title: 'Première page', role: 'link'
diff --git a/app/views/shared/kaminari/_last_page.html.haml b/app/views/shared/kaminari/_last_page.html.haml
index 7f4bfd218..af2811abf 100644
--- a/app/views/shared/kaminari/_last_page.html.haml
+++ b/app/views/shared/kaminari/_last_page.html.haml
@@ -1,2 +1,2 @@
%li
- = link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, rel: 'next', remote: remote, class: 'fr-pagination__link fr-pagination__link--last', 'aria-disabled': true, title: "Dernière page", role: 'link'
+ = link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, rel: 'next', remote: remote, class: 'fr-pagination__link fr-pagination__link--last', title: "Dernière page", role: 'link'
diff --git a/app/views/shared/kaminari/_next_page.html.haml b/app/views/shared/kaminari/_next_page.html.haml
index 405bbaca9..54531dd60 100644
--- a/app/views/shared/kaminari/_next_page.html.haml
+++ b/app/views/shared/kaminari/_next_page.html.haml
@@ -1,2 +1,2 @@
%li
- = link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: remote, class: 'fr-pagination__link fr-pagination__link--next fr-pagination__link--lg-label', 'aria-disabled': true, role: 'link'
+ = link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: remote, class: 'fr-pagination__link fr-pagination__link--next fr-pagination__link--lg-label', role: 'link'
diff --git a/app/views/shared/kaminari/_prev_page.html.haml b/app/views/shared/kaminari/_prev_page.html.haml
index a62f6dc73..265419213 100644
--- a/app/views/shared/kaminari/_prev_page.html.haml
+++ b/app/views/shared/kaminari/_prev_page.html.haml
@@ -1,2 +1,2 @@
%li
- = link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: remote, class: 'fr-pagination__link fr-pagination__link--prev fr-pagination__link--lg-label', 'aria-disabled': true, role: 'link'
+ = link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: remote, class: 'fr-pagination__link fr-pagination__link--prev fr-pagination__link--lg-label', role: 'link'
diff --git a/app/views/shared/procedures/_stats.html.haml b/app/views/shared/procedures/_stats.html.haml
index b39856c9e..fd6fb1c85 100644
--- a/app/views/shared/procedures/_stats.html.haml
+++ b/app/views/shared/procedures/_stats.html.haml
@@ -1,45 +1,56 @@
-.statistiques{ 'data-controller': 'chartkick' }
- %h1.new-h1= title
- .stat-cards
+.fr-container.fr-my-4w
+ %h1= title
+ .fr-grid-row.fr-grid-row--gutters
- if @usual_traitement_time.present?
- .stat-card.big-number-card
- %span.big-number-card-title= t('.usual_processing_time')
- = render Procedure::EstimatedDelayComponent.new(procedure: @procedure)
+ .fr-col-xs-12
+ .fr-callout
+ %h2.fr-callout__title= t('.usual_processing_time')
+ = render Procedure::EstimatedDelayComponent.new(procedure: @procedure)
- .stat-cards
- .stat-card.stat-card-half.pull-left
- %span.stat-card-title= t('.processing_time')
- .stat-card-details= t('.since_procedure_creation')
- .chart-container
- .chart
- - colors = %w(#C3D9FF #0069CC #1C7EC9) # from _colors.scss
- = column_chart @usual_traitement_time_by_month, ytitle: t('.nb_days'), legend: "bottom", label: t('.processing_time_graph_description')
+ .fr-col-xs-12.fr-col-sm-12.fr-col-lg-6
+ .fr-callout{ data: { controller: 'chartkick' } }
+ %h2.fr-callout__title= t('.processing_time')
+ %p.fr-callout__text.fr-text--md= t('.since_procedure_creation')
- .stat-card.stat-card-half.pull-left
- %span.stat-card-title= t('.status_evolution')
- .stat-card-details= t('.status_evolution_details')
- .chart-container
- .chart
- = area_chart @dossiers_funnel, ytitle: t('.dossiers_count'), label: t('.dossiers_count')
+ .fr-mt-4w
+ .chart-procedures-chart{ data: { 'chartkick-target': 'chart' } }
+ = column_chart @usual_traitement_time_by_month,
+ library: Chartkick.options[:default_library_config],
+ ytitle: t('.nb_days'), legend: "bottom", label: t('.processing_time_graph_description')
- .stat-cards
- .stat-card.stat-card-half.pull-left
- %span.stat-card-title= t('.acceptance_rate')
- .stat-card-details= t('.acceptance_rate_details')
- .chart-container
- .chart
- = pie_chart @termines_states,
- code: true,
- colors: %w(#387EC3 #AE2C2B #FAD859),
- label: t('.rate'),
- suffix: '%',
- library: { plotOptions: { pie: { dataLabels: { enabled: true, format: '{point.name} : {point.percentage: .1f}%' } } } }
+ .fr-col-xs-12.fr-col-sm-12.fr-col-lg-6
+ .fr-callout
+ %h2.fr-callout__title= t('.status_evolution')
+ %p.fr-callout__text.fr-text--md= t('.status_evolution_details')
+ .fr-mt-4w
+ .chart
+ = area_chart @dossiers_funnel,
+ library: Chartkick.options[:default_library_config],
+ ytitle: t('.dossiers_count'), label: t('.dossiers_count')
- .stat-card.stat-card-half.pull-left
- %span.stat-card-title= t('.weekly_distribution')
- .stat-card-details= t('.weekly_distribution_details')
- .chart-container
- .chart
- = line_chart @termines_by_week, colors: ["#387EC3", "#AE2C2B", "#FAD859"], ytitle: t('.dossiers_count')
-.clearfix
+ .fr-col-xs-12.fr-col-sm-12.fr-col-lg-6
+ .fr-callout
+ %h2.fr-callout__title= t('.acceptance_rate')
+ %p.fr-callout__text.fr-text--md= t('.acceptance_rate_details')
+
+ .fr-mt-4w
+ .chart
+ = pie_chart @termines_states,
+ library: Chartkick.options[:default_library_config],
+ code: true,
+ colors: ["var(--background-flat-success)", "var(--background-flat-error)", "#FAD859" ],
+ label: t('.rate'),
+ suffix: '%'
+
+ .fr-col-xs-12.fr-col-sm-12.fr-col-lg-6
+ .fr-callout
+ %h2.fr-callout__title= t('.weekly_distribution')
+ %p.fr-callout__text.fr-text--md= t('.weekly_distribution_details')
+
+ .fr-mt-4w
+ .chart
+ = line_chart @termines_by_week,
+ library: Chartkick.options[:default_library_config],
+ colors: ["var(--background-flat-success)", "var(--background-flat-error)", "#FAD859" ],
+ ytitle: t('.dossiers_count')
diff --git a/app/views/static_pages/accessibility_statement.html.haml b/app/views/static_pages/accessibility_statement.html.haml
index 7859f8554..3f2666061 100644
--- a/app/views/static_pages/accessibility_statement.html.haml
+++ b/app/views/static_pages/accessibility_statement.html.haml
@@ -9,20 +9,20 @@
.fr-col-xl-8
%h1.fr-mb-4w
= t('views.accessibility_statement.title')
- %p.fr-mb-2w= t('views.accessibility_statement.line_one')
- %p.fr-mb-2w= t('views.accessibility_statement.line_two', app_name: Current.application_name, host: URI(Current.application_base_url).host)
+ %p.fr-mb-2w= t('views.accessibility_statement.line_one_html')
+ %p.fr-mb-2w= t('views.accessibility_statement.line_two_html', app_name: Current.application_name, host: URI(Current.application_base_url).host)
%div
%h2
= t('views.accessibility_statement.compliance.title')
- %p.fr-mb-2w= t('views.accessibility_statement.compliance.line_one_html')
-
+ %p.fr-mb-2w= t('views.accessibility_statement.compliance.content_html')
%h3
= t('views.accessibility_statement.results.title')
- %p.fr-mb-2w= t('views.accessibility_statement.results.line_one')
- %p.fr-mb-2w= t('views.accessibility_statement.results.line_three')
+ %p.fr-mb-2w= t('views.accessibility_statement.results.content')
%p.fr-mb-2w
+ = t('views.accessibility_statement.results.programme.intro')
= link_to t("views.accessibility_statement.results.programme.label"), t("views.accessibility_statement.results.programme.url"), title: t("views.accessibility_statement.results.programme.title"), target: "_blank", rel: "noopener noreferrer"
+ = "."
%div
%h2
@@ -31,6 +31,9 @@
= t('views.accessibility_statement.no_accessible.subtitle_one')
.fr-mb-2w
= t('views.accessibility_statement.no_accessible.examples_html')
+ %h3
+ = t('views.accessibility_statement.no_accessible.subtitle_two')
+ = t('views.accessibility_statement.no_accessible.content_html')
%div
%h2
@@ -45,6 +48,10 @@
= "HTML 5"
%li
= "CSS 3"
+ %li
+ = "SVG"
+ %li
+ = "ARIA"
%li
= "Javascript"
%li
@@ -93,18 +100,6 @@
= link_to t("views.accessibility_statement.preparation.page_five.label"), new_user_session_path
%li
= t("views.accessibility_statement.preparation.page_six")
- %li
- = link_to t("views.accessibility_statement.preparation.page_seven.label"), t("views.accessibility_statement.preparation.page_seven.url"),
- title: t("views.accessibility_statement.preparation.page_seven.title"), **external_link_attributes
- %li
- = link_to t("views.accessibility_statement.preparation.page_eight.label"), t("views.accessibility_statement.preparation.page_eight.url"),
- title: t("views.accessibility_statement.preparation.page_eight.title"), **external_link_attributes
- %li
- = t("views.accessibility_statement.preparation.page_nine")
- %li
- = t("views.accessibility_statement.preparation.page_ten")
- %li
- = t("views.accessibility_statement.preparation.page_eleven")
%li
= t("views.accessibility_statement.preparation.page_twelve")
%li
@@ -131,11 +126,18 @@
%h2
= t('views.accessibility_statement.remedies.title')
%p.fr-mb-2w= t('views.accessibility_statement.remedies.line_one')
- %p.fr-mb-2w= t('views.accessibility_statement.remedies.line_two')
- %ul.fr-mb-2w
+ %h3
+ = t('views.accessibility_statement.remedies.arcom_title')
+ %p.fr-mb-2w= t('views.accessibility_statement.remedies.arcom_content_html')
+ %h3
+ = t('views.accessibility_statement.remedies.ddd_title')
+ %p.fr-mb-2w= t('views.accessibility_statement.remedies.ddd_intro')
+ %ul
%li
= link_to t("views.accessibility_statement.remedies.remedies_one.label"), t("views.accessibility_statement.remedies.remedies_one.url"), title: t("views.accessibility_statement.remedies.remedies_one.title"), target: "_blank", rel: "noopener noreferrer"
+ %br
+ = t('views.accessibility_statement.remedies.remedies_one.info')
%li
= link_to t("views.accessibility_statement.remedies.remedies_two.label"), t("views.accessibility_statement.remedies.remedies_two.url"), title: t("views.accessibility_statement.remedies.remedies_two.title"), target: "_blank", rel: "noopener noreferrer"
%li
- = t('views.accessibility_statement.remedies.remedies_three')
+ = t('views.accessibility_statement.remedies.remedies_three_html')
diff --git a/app/views/static_pages/legal_notice.html.haml b/app/views/static_pages/legal_notice.html.haml
index ffa99bd18..889d0f86f 100644
--- a/app/views/static_pages/legal_notice.html.haml
+++ b/app/views/static_pages/legal_notice.html.haml
@@ -13,9 +13,12 @@
.fr-mb-4w
%h2
= t('views.legal_notice.editing')
- %p.fr-mb-2w= t('views.legal_notice.editing_content.line_one')
- %p.fr-mb-2w= t('views.legal_notice.editing_content.line_two')
- %p.fr-mb-2w= t('views.legal_notice.editing_content.line_three')
+ %p
+ = t('views.legal_notice.editing_content.line_one')
+ %br
+ = t('views.legal_notice.editing_content.line_two')
+ %br
+ = t('views.legal_notice.editing_content.line_three')
.fr-mb-4w
%h2
@@ -25,8 +28,18 @@
.fr-mb-4w
%h2
= t('views.legal_notice.hosting')
- %p.fr-mb-2w= t('views.legal_notice.hosting_content.line_one')
- %p.fr-mb-2w= t('views.legal_notice.hosting_content.line_two')
- %p.fr-mb-2w= t('views.legal_notice.hosting_content.line_three')
- %p.fr-mb-2w= t('views.legal_notice.hosting_content.line_four')
- %p.fr-mb-2w= t('views.legal_notice.hosting_content.line_five')
+ %h3
+ = t('views.legal_notice.hosting_content.firm')
+ %dl.pl-0
+ .flex
+ %dt= t('views.legal_notice.hosting_content.RCS_term')
+ %dd= t('views.legal_notice.hosting_content.RCS_value')
+ .flex
+ %dt= t('views.legal_notice.hosting_content.APE_term')
+ %dd= t('views.legal_notice.hosting_content.APE_value')
+ .flex
+ %dt= t('views.legal_notice.hosting_content.TVA_term_html')
+ %dd= t('views.legal_notice.hosting_content.TVA_value')
+ .flex
+ %dt= t('views.legal_notice.hosting_content.headquarters_term')
+ %dd= t('views.legal_notice.hosting_content.headquarters_value')
diff --git a/app/views/stats/index.html.haml b/app/views/stats/index.html.haml
index d1712f63a..96c847e47 100644
--- a/app/views/stats/index.html.haml
+++ b/app/views/stats/index.html.haml
@@ -2,71 +2,75 @@
- content_for :footer do
= render partial: "root/footer"
-.statistiques{ 'data-controller': 'chartkick' }
- %h1.new-h1 Statistiques
+.fr-container.fr-my-4w
+ %h1 Statistiques d’utilisation de la plateforme
- .stat-cards
- .stat-card.stat-card-half.big-number-card.pull-left
- %span.big-number-card-title.long-title TOTAL DÉMARCHES DÉMAT. OU EN COURS DE DÉMAT.
- %span.big-number-card-number
- = number_with_delimiter(@procedures_numbers[:total])
- %span.big-number-card-detail
- #{number_with_delimiter(@procedures_numbers[:last_30_days_count])} (#{@procedures_numbers[:evolution]} %) sur les 30 derniers jours
- %span.big-number-card-detail
- = link_to "Voir carte de déploiement", carte_path
+ .fr-grid-row.fr-grid-row--gutters
+ .fr-col-xs-12.fr-col-sm-12.fr-col-lg-6
+ .fr-callout{ data: { controller: 'chartkick' } }
+ %h2.fr-callout__title Démarches dématérialisées (total)
+ %p.fr-callout__text.big-number-card-number.fr-mb-2w
+ %span.big-number-card-number= number_with_delimiter(@procedures_numbers[:total])
+ %p.fr-callout__text.fr-text--md.text-center
+ #{number_with_delimiter(@procedures_numbers[:last_30_days_count])} (#{@procedures_numbers[:evolution]} %) sur les 30 derniers jours
+ %br
+ = link_to "Voir carte de déploiement", carte_path
- .stat-card.stat-card-half.big-number-card.pull-left
- %span.big-number-card-title TOTAL DOSSIERS DÉPOSÉS
- %span.big-number-card-number
- = number_with_delimiter(@dossiers_numbers[:total])
- %span.big-number-card-detail
- #{number_with_delimiter(@dossiers_numbers[:last_30_days_count])} (#{@dossiers_numbers[:evolution]} %) sur les 30 derniers jours
- %span.big-number-card-detail
- = link_to "Voir carte de déploiement", carte_path(map_filter: { kind: :nb_dossiers })
+ %fieldset.fr-segmented.fr-segmented--sm.pull-right.fr-mt-2w.fr-my-1w
+ .fr-segmented__elements
+ .fr-segmented__element
+ %input{ value: "1", checked: true, type: "radio", id: "segmented-procedures-1", name: "segmented-procedures", data: { action: 'chartkick#toggleChart', 'toggle-chart': '.monthly-procedures-chart' } }
+ %label.fr-label{ for: "segmented-procedures-1" }
+ Par mois
+ .fr-segmented__element
+ %input{ value: "2", type: "radio", id: "segmented-procedures-2", name: "segmented-procedures", data: { action: 'chartkick#toggleChart', 'toggle-chart': '.cumulative-procedures-chart' } }
+ %label.fr-label{ for: "segmented-procedures-2" }
+ Cumul
+
+ .fr-mt-4w
+ .chart.monthly-procedures-chart{ data: { 'chartkick-target': 'chart' } }
+ = column_chart @procedures_in_the_last_4_months, library: Chartkick.options[:default_library_config]
+ .chart.cumulative-procedures-chart.hidden{ data: { 'chartkick-target': 'chart' } }
+ = area_chart @procedures_cumulative, library: Chartkick.options[:default_library_config]
+
+ .fr-col-xs-12.fr-col-sm-12.fr-col-lg-6
+ .fr-callout{ data: { controller: 'chartkick' } }
+ %h2.fr-callout__title Dossiers déposés (total)
+ %p.fr-callout__text.big-number-card-number.fr-mb-2w
+ = number_with_delimiter(@dossiers_numbers[:total])
+ %p.fr-callout__text.fr-text--md.text-center
+ #{number_with_delimiter(@dossiers_numbers[:last_30_days_count])} (#{@dossiers_numbers[:evolution]} %) sur les 30 derniers jours
+ %br
+ = link_to "Voir carte de déploiement", carte_path(map_filter: { kind: :nb_dossiers })
+
+ %fieldset.fr-segmented.fr-segmented--sm.pull-right.fr-mt-2w.fr-my-1w
+ .fr-segmented__elements
+ .fr-segmented__element
+ %input{ value: "1", checked: true, type: "radio", id: "segmented-dossiers-1", name: "segmented-dossiers", data: { action: 'chartkick#toggleChart', 'toggle-chart': '.monthly-dossiers-chart' } }
+ %label.fr-label{ for: "segmented-dossiers-1" }
+ Par mois
+ .fr-segmented__element
+ %input{ value: "2", type: "radio", id: "segmented-dossiers-2", name: "segmented-dossiers", data: { action: 'chartkick#toggleChart', 'toggle-chart': '.cumulative-dossiers-chart' } }
+ %label.fr-label{ for: "segmented-dossiers-2" }
+ Cumul
- .stat-card.stat-card-half.pull-left
- %ul.segmented-control.pull-right
- %li.segmented-control-item.segmented-control-item-active{ data: { 'toggle-chart': '.monthly-procedures-chart' } }
- Par mois
- %li.segmented-control-item{ data: { 'toggle-chart': '.cumulative-procedures-chart' } }
- Cumul
- %span.stat-card-title.pull-left Démarches dématérialisées
- .clearfix
+ .fr-mt-4w
+ .chart.monthly-dossiers-chart{ data: { 'chartkick-target': 'chart' } }
+ = column_chart @dossiers_in_the_last_4_months, library: Chartkick.options[:default_library_config]
+ .chart.cumulative-dossiers-chart.hidden{ data: { 'chartkick-target': 'chart' } }
+ = area_chart @dossiers_cumulative, library: Chartkick.options[:default_library_config]
- .chart-container
- .chart.monthly-procedures-chart
- = column_chart @procedures_in_the_last_4_months
- .chart.cumulative-procedures-chart.hidden
- = area_chart @procedures_cumulative
+ .fr-col-xs-12.fr-col-sm-12.fr-col-lg-6
+ .fr-callout
+ %h2.fr-callout__title Répartition des dossiers
- .stat-card.stat-card-half.pull-left
- %ul.segmented-control.pull-right
- %li.segmented-control-item.segmented-control-item-active{ data: { 'toggle-chart': '.monthly-dossiers-chart' } }
- Par mois
- %li.segmented-control-item{ data: { 'toggle-chart': '.cumulative-dossiers-chart' } }
- Cumul
- %span.stat-card-title.pull-left Dossiers déposés
- .clearfix
-
- .chart-container
- .chart.monthly-dossiers-chart
- = column_chart @dossiers_in_the_last_4_months
- .chart.cumulative-dossiers-chart.hidden
- = area_chart @dossiers_cumulative
-
- .stat-card.stat-card-half.pull-left
- %span.stat-card-title
- Répartition des dossiers
-
- .chart-container
- .chart
- = pie_chart @dossiers_states_for_pie,
- colors: ["#000091", "#7F7FC8", "#9A9AFF", "#00006D"]
-
- .clearfix
+ .fr-mt-4w
+ .chart
+ = pie_chart @dossiers_states_for_pie, library: Chartkick.options[:default_library_config],
+ colors: ["#000091", "#7F7FC8", "#9A9AFF", "#00006D"]
- if super_admin_signed_in?
- %h2.new-h2 Téléchargement
+ %h2.fr-h4 Téléchargement
- = link_to "Télécharger les statistiques (CSV)", stats_download_path(format: :csv), class: 'fr-btn fr-btn-primary mb-4'
+ = link_to "Télécharger les statistiques (CSV)", stats_download_path(format: :csv), class: 'fr-btn fr-btn-primary fr-mb-4w'
diff --git a/app/views/super_admins/release_notes/_main_navigation.html.haml b/app/views/super_admins/release_notes/_main_navigation.html.haml
index 1485a91fa..5dd71e776 100644
--- a/app/views/super_admins/release_notes/_main_navigation.html.haml
+++ b/app/views/super_admins/release_notes/_main_navigation.html.haml
@@ -1,5 +1,5 @@
- content_for(:main_navigation) do
- %nav.fr-nav#header-navigation{ role: "navigation", aria: { label: 'Menu principal annonces' } }
+ #header-navigation.fr-nav
%ul.fr-nav__list
%li.fr-nav__item
= link_to "Toutes les annonces", super_admins_release_notes_path, class: "fr-nav__link", target: "_self", aria: { current: action == :index ? "page" : nil }
diff --git a/app/views/support/admin.html.haml b/app/views/support/admin.html.haml
deleted file mode 100644
index dff48a0cc..000000000
--- a/app/views/support/admin.html.haml
+++ /dev/null
@@ -1,49 +0,0 @@
-- content_for(:title, 'Contact')
-
-#contact-form
- .container
- %h1.new-h1
- = t('.contact_team')
-
- .description
- = t('.admin_intro_html', contact_path: contact_path)
- %br
- %p.mandatory-explanation= t('asterisk_html', scope: [:utils])
-
- = form_tag contact_path, method: :post, class: 'form' do |f|
- - if !user_signed_in?
- .contact-champ
- = label_tag :email do
- = t('.pro_mail')
- %span.mandatory *
- = text_field_tag :email, params[:email], required: true
-
- .contact-champ
- = label_tag :type do
- = t('.your_question')
- %span.mandatory *
- = select_tag :type, options_for_select(@options, params[:type])
-
- .contact-champ
- = label_tag :phone do
- = t('.pro_phone_number')
- = text_field_tag :phone
-
- .contact-champ
- = label_tag :subject do
- = t('subject', scope: [:utils])
- = text_field_tag :subject, params[:subject], required: false
-
- .contact-champ
- = label_tag :text do
- = t('message', scope: [:utils])
- %span.mandatory *
- = text_area_tag :text, params[:text], rows: 6, required: true
-
- = invisible_captcha
-
- = hidden_field_tag :tags, @tags&.join(',')
- = hidden_field_tag :admin, true
-
- .send-wrapper
- = button_tag t('send_mail', scope: [:utils]), type: :submit, class: 'button send primary'
diff --git a/app/views/support/index.html.haml b/app/views/support/index.html.haml
deleted file mode 100644
index eadc29c96..000000000
--- a/app/views/support/index.html.haml
+++ /dev/null
@@ -1,76 +0,0 @@
-- content_for(:title, t('.contact'))
-- content_for :footer do
- = render partial: "root/footer"
-
-#contact-form
- .container
- %h1.new-h1
- = t('.contact')
-
- = form_tag contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :support } do
-
- .description
- .recommandations
- = t('.intro_html')
- %p.mandatory-explanation= t('asterisk_html', scope: [:utils])
-
- - if !user_signed_in?
- .fr-input-group
- = label_tag :email, class: 'fr-label' do
- Email
- = render EditableChamp::AsteriskMandatoryComponent.new
- = email_field_tag :email, params[:email], required: true, autocomplete: 'email', class: 'fr-input'
-
- %fieldset.fr-fieldset{ name: "type" }
- %legend.fr-fieldset__legend
- = t('.your_question')
- = render EditableChamp::AsteriskMandatoryComponent.new
- .fr-fieldset__content
- - @options.each do |(question, question_type, link)|
- .fr-radio-group
- = radio_button_tag :type, question_type, false, required: true, data: {"support-target": "inputRadio" }
- = label_tag "type_#{question_type}", { 'aria-controls': link ? "card-#{question_type}" : nil, class: 'fr-label' } do
- = question
-
- - if link.present?
- .fr-ml-3w.hidden{ id: "card-#{question_type}", "aria-hidden": true , data: { "support-target": "content" } }
- = render Dsfr::CalloutComponent.new(title: t('.our_answer')) do |c|
- - c.with_body do
- %p
- -# i18n-tasks-use t("support.index.#{question_type}.answer_html")
- = t('answer_html', scope: [:support, :index, question_type], base_url: Current.application_base_url, "link_#{question_type}": link)
-
- .fr-input-group
- = label_tag :dossier_id, t('file_number', scope: [:utils]), class: 'fr-label'
- = text_field_tag :dossier_id, @dossier_id, class: 'fr-input'
-
- .fr-input-group
- = label_tag :subject, class: 'fr-label' do
- = t('subject', scope: [:utils])
- = render EditableChamp::AsteriskMandatoryComponent.new
- = text_field_tag :subject, params[:subject], required: true, class: 'fr-input'
-
- .fr-input-group
- = label_tag :text, class: 'fr-label' do
- = t('message', scope: [:utils])
- = render EditableChamp::AsteriskMandatoryComponent.new
- = text_area_tag :text, params[:text], rows: 6, required: true, class: 'fr-input'
-
- .fr-upload-group
- = label_tag :piece_jointe, class: 'fr-label' do
- = t('pj', scope: [:utils])
- %span.fr-hint-text
- = t('.notice_upload_group')
-
- %p.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AMELIORATION } }
- = t('.notice_pj_product')
- %p.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AUTRE } }
- = t('.notice_pj_other')
- = file_field_tag :piece_jointe, class: 'fr-upload', max: 200.megabytes
-
- = hidden_field_tag :tags, @tags&.join(',')
-
- = invisible_captcha
-
- .send-wrapper.fr-my-3w
- = button_tag t('send_mail', scope: [:utils]), type: :submit, class: 'fr-btn send'
diff --git a/app/views/user_mailer/custom_confirmation_instructions.html.haml b/app/views/user_mailer/custom_confirmation_instructions.html.haml
new file mode 100644
index 000000000..c9ac6a792
--- /dev/null
+++ b/app/views/user_mailer/custom_confirmation_instructions.html.haml
@@ -0,0 +1,22 @@
+- content_for(:title, 'Confirmez votre email')
+%p
+ Bonjour
+ = @user.email
+ !
+
+%p
+ Veuillez confirmer votre email en cliquant sur le lien ci-dessous:
+ = round_button 'Je confirme', france_connect_confirm_email_url(@token), :primary
+
+
+%p Ce lien est valide #{distance_of_time_in_words(FranceConnectInformation::CONFIRMATION_EMAIL_VALIDITY)}.
+
+%p
+ Tant que vous n'aurez pas confirmé votre email, vous ne recevrez aucune notification sur l'avancement de vos dossiers.
+
+%p
+ Si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer ce message. Et si vous avez besoin d’assistance, n’hésitez pas à nous contacter à
+ = succeed '.' do
+ = mail_to CONTACT_EMAIL
+
+= render partial: "layouts/mailers/signature"
diff --git a/app/views/user_mailer/france_connect_merge_confirmation.haml b/app/views/user_mailer/france_connect_merge_confirmation.haml
index 2f6c99a33..211703aa5 100644
--- a/app/views/user_mailer/france_connect_merge_confirmation.haml
+++ b/app/views/user_mailer/france_connect_merge_confirmation.haml
@@ -1,5 +1,5 @@
- content_for(:title, @subject)
-- merge_link = france_connect_particulier_mail_merge_with_existing_account_url(email_merge_token: @email_merge_token)
+- merge_link = france_connect_particulier_merge_using_email_link_url(email_merge_token: @email_merge_token)
%p
Bonjour,
diff --git a/app/views/user_mailer/invite_instructeur.html.haml b/app/views/user_mailer/invite_instructeur.html.haml
index b7ba5b9e8..19e64170e 100644
--- a/app/views/user_mailer/invite_instructeur.html.haml
+++ b/app/views/user_mailer/invite_instructeur.html.haml
@@ -18,10 +18,6 @@
Lors de vos prochaines connexions sur #{Current.application_name} cliquez sur le bouton « Se connecter » positionné sur le haut de page ou bien sur ce lien :
= link_to new_user_session_url, new_user_session_url
-- if AgentConnectService.enabled?
- %p
- Vous êtes un agent de l'état et avez accès à AgentConnect ? Vous pouvez utiliser la connexion AgentConnect en suivant ce lien :
- = link_to agent_connect_url, agent_connect_url
%p
Nous vous invitons aussi à consulter notre tutoriel à destination des nouveaux instructeurs :
= link_to(INSTRUCTEUR_TUTORIAL_URL, INSTRUCTEUR_TUTORIAL_URL)
diff --git a/app/views/user_mailer/invite_tiers.html.haml b/app/views/user_mailer/invite_tiers.html.haml
new file mode 100644
index 000000000..3b3e1ce53
--- /dev/null
+++ b/app/views/user_mailer/invite_tiers.html.haml
@@ -0,0 +1,27 @@
+- content_for(:title, "Vérification de votre mail sur #{Current.application_name}")
+
+%p
+ Bonjour,
+
+ %p
+ - if @dossier.present?
+ Un dossier sur la démarche : #{@dossier.procedure.libelle} a été démarré en votre nom par #{@dossier.user.email}.
+ - else
+ Un dossier a été démarré en votre nom sur #{Current.application_name}"
+
+ %p
+ Pour continuer à recevoir les mails concernant votre dossier, vous devez confirmer votre adresse email en cliquant sur ce bouton :
+ = round_button 'Je confirme', users_confirm_email_url(token: @token), :primary
+
+ %p
+ Vous pouvez aussi utiliser ce lien :
+ = link_to(users_confirm_email_url(token: @token), users_confirm_email_url(token: @token))
+
+ %p
+ - if @dossier.present?
+ Pour en savoir plus, veuillez vous rapprocher de #{@dossier.user.email}.
+ - else
+ Nous restons à votre disposition si vous avez besoin d’accompagnement à l'adresse #{link_to CONTACT_EMAIL, "mailto:#{CONTACT_EMAIL}"}.
+
+
+= render partial: "layouts/mailers/signature"
diff --git a/app/views/user_mailer/resend_confirmation_email.html.haml b/app/views/user_mailer/resend_confirmation_email.html.haml
new file mode 100644
index 000000000..a189423c4
--- /dev/null
+++ b/app/views/user_mailer/resend_confirmation_email.html.haml
@@ -0,0 +1,18 @@
+- content_for(:title, "Vérification de votre mail sur #{Current.application_name}")
+
+%p
+ Bonjour,
+
+%p
+ Votre précédente confirmation de mail n'a pas fonctionné, vous pouvez essayer de nouveau en cliquant sur ce bouton :
+ = round_button 'Je confirme', users_confirm_email_url(token: @token), :primary
+
+%p
+ Vous pouvez aussi utiliser ce lien :
+ = link_to(users_confirm_email_url(token: @token), users_confirm_email_url(token: @token))
+
+%p
+ Nous restons à votre disposition si vous avez besoin d’accompagnement à l'adresse #{link_to CONTACT_EMAIL, "mailto:#{CONTACT_EMAIL}"}.
+
+
+= render partial: "layouts/mailers/signature"
diff --git a/app/views/users/_main_navigation.html.haml b/app/views/users/_main_navigation.html.haml
index 4d10e5a49..c78c44cae 100644
--- a/app/views/users/_main_navigation.html.haml
+++ b/app/views/users/_main_navigation.html.haml
@@ -1,8 +1,12 @@
-%nav#header-navigation.fr-nav{ role: :navigation, "aria-label" => t('main_menu', scope: [:layouts, :header]) }
+#header-navigation.fr-nav
%ul.fr-nav__list
- if params[:controller] == 'users/commencer'
%li.fr-nav__item
- = link_to t('back', scope: [:layouts, :header]), url_for(:back), title: t('back_title', scope: [:layouts, :header]), class: 'fr-nav__link', "aria-controls" => "modal-header__menu"
+ = link_to t('back', scope: [:layouts, :header]), url_for(:back), title: t('back_title', scope: [:layouts, :header]), class: 'fr-nav__link'
%li.fr-nav__item
- = link_to t('files', scope: [:layouts, :header]), dossiers_path, class: 'fr-nav__link', aria: { current: controller_name == 'dossiers' ? 'true' : nil, controls: "modal-header__menu" }
+ = link_to t('files', scope: [:layouts, :header]), dossiers_path, class: 'fr-nav__link', aria: { current: (controller_name == 'dossiers' && action_name != 'deleted_dossiers') ? 'true' : nil }
+
+ - if current_user.deleted_dossiers.present?
+ %li.fr-nav__item
+ = link_to 'Historique des dossiers supprimés', deleted_dossiers_path(), class: 'fr-nav__link', aria: { current: action_name == 'deleted_dossiers' ? 'true' : nil }
diff --git a/app/views/users/_procedure_footer.html.haml b/app/views/users/_procedure_footer.html.haml
index 409f0369b..74a993f88 100644
--- a/app/views/users/_procedure_footer.html.haml
+++ b/app/views/users/_procedure_footer.html.haml
@@ -1,37 +1,10 @@
%footer.fr-footer.footer-procedure#footer{ role: "contentinfo" }
- - service = dossier&.service || procedure.service
.fr-footer__top.fr-mb-0
.fr-container
+ %h2.sr-only= t("links.footer.top_labels.hidden_title_procedure")
.fr-grid-row.fr-grid-row--start.fr-grid-row--gutters
.fr-col-12.fr-col-sm-4.fr-col-md-4
- - if service.present?
- %h3.fr-footer__top-cat= I18n.t('users.procedure_footer.managed_by.header')
- .fr-footer__top-link.fr-pb-2w
- %span{ lang: :fr }= service.pretty_nom
- %div{ lang: :fr }
- = render SimpleFormatComponent.new(service.adresse, class_names_map: {paragraph: 'fr-footer__content-desc'})
- %h3.fr-footer__top-cat= I18n.t('users.procedure_footer.contact.header')
- %ul.fr-footer__top-list
- - if dossier.present? && dossier.messagerie_available?
- %li
- = link_to I18n.t('users.procedure_footer.contact.in_app_mail.link'), messagerie_dossier_path(dossier), class: 'fr-footer__top-link'
- - elsif service.present?
- %li
- %span.fr-footer__top-link
- = I18n.t('users.procedure_footer.contact.email.link')
- = link_to service.email, "mailto:#{service.email}", class: "fr-footer__top-link"
-
- - if service.present?
- - if service.telephone.present? || service.horaires.present?
- %li
- - horaires = "#{I18n.t('users.procedure_footer.contact.schedule.prefix')}#{formatted_horaires(service.horaires)}"
- - if service.telephone.present?
- = link_to service.telephone_url, class: 'fr-footer__top-link' do
- %p
- = I18n.t('users.procedure_footer.contact.phone.link', service_telephone: service.telephone)
- - if service.horaires.present?
- %p
- = horaires
+ = render partial: 'shared/dossiers/update_contact_information', locals: { dossier: dossier, procedure: procedure }
- politiques = politiques_conservation_de_donnees(procedure)
- if politiques.present?
@@ -40,34 +13,30 @@
%ul.fr-footer__top-list
- politiques.each do |politique|
%li
- = link_to t("users.procedure_footer.legals.data_retention_url"), class: "fr-footer__top-link", title: new_tab_suffix(t("users.procedure_footer.legals.data_retention_title")), **external_link_attributes do
+ = link_to t("users.procedure_footer.legals.data_retention_url"), class: "fr-footer__link", title: new_tab_suffix(t("users.procedure_footer.legals.data_retention_title", data_retention_title: politiques_conservation_de_donnees(procedure).join)), **external_link_attributes do
= politique
- if procedure.deliberation.attached?
%li
- = link_to url_for(procedure.deliberation), rel: 'noopener', class: 'fr-footer__top-link' do
+ = link_to url_for(procedure.deliberation), rel: 'noopener', class: 'fr-footer__link' do
= I18n.t("users.procedure_footer.legals.terms")
- else
%li
- = link_to I18n.t("users.procedure_footer.legals.terms"), procedure.cadre_juridique, rel: 'noopener', class: 'fr-footer__top-link'
+ = link_to I18n.t("users.procedure_footer.legals.terms"), procedure.cadre_juridique, rel: 'noopener', class: 'fr-footer__link'
- if procedure.lien_dpo.present?
%li
- = link_to url_or_email_to_lien_dpo(procedure), rel: 'noopener', class: 'fr-footer__top-link' do
+ = link_to url_or_email_to_lien_dpo(procedure), rel: 'noopener', class: 'fr-footer__link' do
= I18n.t("users.procedure_footer.legals.dpo")
%li
- = link_to I18n.t('users.procedure_footer.contact.stats.link'), statistiques_path(procedure.path), class: 'fr-footer__top-link', rel: 'noopener'
+ = link_to I18n.t('users.procedure_footer.contact.stats.link'), statistiques_path(procedure.path), class: 'fr-footer__link', rel: 'noopener'
.fr-col-12.fr-col-sm-4.fr-col-md-4
- unless procedure.close?
%h3.fr-footer__top-cat= I18n.t('users.procedure_footer.dematerialisation.header')
.fr-download
- %p
- = link_to I18n.t('users.procedure_footer.dematerialisation.title_1'), commencer_dossier_vide_for_revision_path(procedure.active_revision), class: 'fr-footer__top-link fr-download__link'
+ = link_to I18n.t('users.procedure_footer.dematerialisation.title_1'), commencer_dossier_vide_for_revision_path(procedure.active_revision), download: 'true', class: 'fr-download__link'
%h3.fr-footer__top-cat= I18n.t('users.procedure_footer.support.header')
- .fr-footer__brand.fr-enlarge-link
- = link_to t("users.procedure_footer.dematerialisation.link"), title: t("users.procedure_footer.dematerialisation.alt"), class: "fr-footer__brand-link" do
- = image_tag("footer/logo-france-services.svg", class: "fr-footer__logo logo-france-service-fr", alt: t("users.procedure_footer.dematerialisation.alt"))
.fr-footer__bottom.fr-mt-0
.fr-container
diff --git a/app/views/users/confirmations/new.html.haml b/app/views/users/confirmations/new.html.haml
index 587531801..970a0fac0 100644
--- a/app/views/users/confirmations/new.html.haml
+++ b/app/views/users/confirmations/new.html.haml
@@ -10,14 +10,16 @@
%h1.fr-mt-6w.fr-h2.center
= t('views.confirmation.new.title')
- %p.center{ aria: { hidden: true } }= image_tag("user/confirmation-email.svg", alt: t('views.confirmation.new.image_alt'))
+ %p.center= image_tag("user/confirmation-email.svg", alt: '')
= render Dsfr::AlertComponent.new(title: '', state: :info, heading_level: 'h2', extra_class_names: 'fr-mt-6w fr-mb-3w') do |c|
- c.with_body do
%p= t('views.confirmation.new.email_cta_html', email: resource.email)
%p= t('views.confirmation.new.email_guidelines_html')
+ %p.fr-text--sm.fr-text-mention--grey.fr-mb-1w
+ = t('views.confirmation.new.email_missing')
+
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { class: 'fr-mb-6w'}) do |f|
- %legend.fr-hint-text.fr-mb-3w= t('views.confirmation.new.email_missing')
= f.hidden_field :email
= f.submit t('views.confirmation.new.resent'), class: 'fr-btn fr-btn--secondary'
diff --git a/app/views/users/dossiers/_deleted_dossiers_list.html.haml b/app/views/users/dossiers/_deleted_dossiers_list.html.haml
deleted file mode 100644
index 86c8bf38c..000000000
--- a/app/views/users/dossiers/_deleted_dossiers_list.html.haml
+++ /dev/null
@@ -1,31 +0,0 @@
-- if deleted_dossiers.present?
- .fr-h6.fr-mb-2w
- = page_entries_info deleted_dossiers
-
- - deleted_dossiers.each do |dossier|
- .card
- .flex.justify-between
- %div
- %h2.card-title
- = dossier.procedure.libelle
-
- %p.fr-icon--sm.fr-icon-delete-line.fr-mb-0
- = t('views.users.dossiers.dossiers_list.deleted', date: l(dossier.updated_at.to_date))
- = "-"
- = t("activerecord.attributes.deleted_dossier.reason.#{dossier.reason}")
-
- .text-right
- %p.fr-mb-0
- = t('views.users.dossiers.dossiers_list.n_dossier')
- = dossier.dossier_id
-
- %span.fr-badge.fr-badge--sm.fr-badge--warning
- = t('views.users.dossiers.dossiers_list.deleted_badge')
-
- = paginate deleted_dossiers, views_prefix: 'shared'
-
-- else
- .blank-tab
- %h2.empty-text= t('views.users.dossiers.dossiers_list.no_result_title')
- %p.empty-text-details
- = t('views.users.dossiers.dossiers_list.no_result_text_html', app_base: Current.application_base_url)
diff --git a/app/views/users/dossiers/_dossier_actions.html.haml b/app/views/users/dossiers/_dossier_actions.html.haml
index 336a5746a..05eea1305 100644
--- a/app/views/users/dossiers/_dossier_actions.html.haml
+++ b/app/views/users/dossiers/_dossier_actions.html.haml
@@ -9,10 +9,14 @@
- if has_actions
- if has_edit_action
- if dossier.brouillon?
- = link_to t('views.users.dossiers.dossier_action.edit_draft'), (url_for_dossier(dossier)), class: 'fr-btn fr-btn--sm fr-mr-1w'
+ = link_to t('views.users.dossiers.dossier_action.edit_draft'), (url_for_dossier(dossier)), class: 'fr-btn fr-btn--sm fr-mr-1w fr-icon-draft-line fr-btn--icon-left'
- else
- = link_to t('views.users.dossiers.dossier_action.edit_dossier'), modifier_dossier_path(dossier), class: 'fr-btn fr-btn--sm fr-btn--tertiary fr-mr-1w'
+ = link_to t('views.users.dossiers.dossier_action.edit_dossier'), modifier_dossier_path(dossier), class: 'fr-btn fr-btn--sm fr-mr-1w fr-icon-draft-line fr-btn--icon-left'
+
+ - if has_new_dossier_action
+ = link_to (commencer_url(dossier.procedure.path)), class: 'fr-btn fr-btn--sm fr-btn--tertiary fr-mr-1w fr-icon-file-fill fr-btn--icon-left' do
+ = t('views.users.dossiers.dossier_action.start_other_dossier')
= render Dropdown::MenuComponent.new(wrapper: :div, wrapper_options: {class: 'invite-user-actions'}, menu_options: {id: dom_id(dossier, :actions_menu)}, button_options: {class: 'fr-btn--sm fr-btn--tertiary'}) do |menu|
- menu.with_button_inner_html do
@@ -29,12 +33,6 @@
= t('views.users.dossiers.dossier_action.transfer_dossier')
- if has_new_dossier_action
- - menu.with_item do
- = link_to(commencer_url(dossier.procedure.path), role: 'menuitem') do
- = dsfr_icon('fr-icon-file-fill', :sm)
- .dropdown-description
- = t('views.users.dossiers.dossier_action.start_other_dossier')
-
- menu.with_item do
= link_to(clone_dossier_path(dossier), method: :post, role: 'menuitem') do
= dsfr_icon('fr-icon-file-copy-line', :sm)
diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml
index 8ab0cfa5f..da58f4be2 100644
--- a/app/views/users/dossiers/_dossiers_list.html.haml
+++ b/app/views/users/dossiers/_dossiers_list.html.haml
@@ -1,13 +1,13 @@
- if dossiers.present?
- .fr-h6.fr-mb-2w
+ %h2.fr-h6.fr-mb-2w
= page_entries_info dossiers
- dossiers.each do |dossier|
.card
.flex.justify-between
%div
- %h2.card-title
- - if ["dossiers-transferes", "dossiers-supprimes-recemment"].exclude?(@statut)
+ %h3.card-title
+ - if ["dossiers-transferes", "dossiers-supprimes"].exclude?(@statut)
= link_to(url_for_dossier(dossier), class: 'cell-link') do
= dossier.procedure.libelle
- else
@@ -16,9 +16,12 @@
%p.fr-icon--sm.fr-icon-user-line
= demandeur_dossier(dossier)
- - if dossier.hidden_by_user?
+ - if dossier.hidden_by_expired?
%p.fr-icon--sm.fr-icon-delete-line
- = t('views.users.dossiers.dossiers_list.deleted', date: l(dossier.hidden_by_user_at.to_date))
+ = t('views.users.dossiers.dossiers_list.deleted_by_automatic', date: l(dossier.hidden_by_expired_at.to_date))
+ - elsif dossier.hidden_by_user?
+ %p.fr-icon--sm.fr-icon-delete-line
+ = t('views.users.dossiers.dossiers_list.deleted_by_user', date: l(dossier.hidden_by_user_at.to_date))
- else
%p.fr-icon--sm.fr-icon-edit-box-line
- if dossier.depose_at.present?
@@ -40,11 +43,7 @@
= t('views.users.dossiers.dossiers_list.n_dossier')
= number_with_html_delimiter(dossier.id)
- - if @statut == "dossiers-supprimes-recemment"
- %span.fr-badge.fr-badge--sm.fr-badge--warning
- = t('views.users.dossiers.dossiers_list.deleted_badge')
- - else
- = status_badge_user(dossier, 'fr-mb-1w')
+ = status_badge_user(dossier, 'fr-mb-1w')
- if dossier.pending_correction?
%br
@@ -55,17 +54,17 @@
- c.with_body do
%p
- if dossier.brouillon? && dossier.procedure.closing_reason_internal_procedure?
- = t('views.users.dossiers.dossiers_list.procedure_closed.brouillon.internal_procedure_html', link: link_to(t('views.users.dossiers.dossiers_list.procedure_closed.procedure'), commencer_path(dossier.procedure.replaced_by_procedure.path), **external_link_attributes, title: new_tab_suffix(t('views.users.dossiers.dossiers_list.procedure_closed.closing_details'))).html_safe)
- - elsif dossier.brouillon? && dossier.procedure.closing_reason_other?
- = t('views.users.dossiers.dossiers_list.procedure_closed.brouillon.other_html', link: link_to(t('views.users.dossiers.dossiers_list.procedure_closed.here'), closing_details_path(dossier.procedure.path), **external_link_attributes, title: new_tab_suffix(t('views.users.dossiers.dossiers_list.procedure_closed.closing_details'))).html_safe)
+ = t('views.users.dossiers.dossiers_list.procedure_closed.brouillon.internal_procedure_html', link: link_to(t('views.users.dossiers.dossiers_list.procedure_closed.procedure'), commencer_path(dossier.procedure.replaced_by_procedure.path), **external_link_attributes, title: new_tab_suffix(t('views.users.dossiers.dossiers_list.procedure_closed.title_new_procedure'))).html_safe)
+ - elsif dossier.brouillon?
+ = t('views.users.dossiers.dossiers_list.procedure_closed.brouillon.other_html', link: link_to(t('views.users.dossiers.dossiers_list.procedure_closed.more_details'), closing_details_path(dossier.procedure.path), **external_link_attributes, title: new_tab_suffix(t('views.users.dossiers.dossiers_list.procedure_closed.title_closing_details'))).html_safe)
- elsif dossier.en_construction_ou_instruction? && dossier.procedure.closing_reason_internal_procedure?
- = t('views.users.dossiers.dossiers_list.procedure_closed.en_cours.internal_procedure_html')
- - elsif dossier.en_construction_ou_instruction? && dossier.procedure.closing_reason_other?
- = t('views.users.dossiers.dossiers_list.procedure_closed.en_cours.other_html', link: link_to(t('views.users.dossiers.dossiers_list.procedure_closed.here'), closing_details_path(dossier.procedure.path), **external_link_attributes, title: new_tab_suffix(t('views.users.dossiers.dossiers_list.procedure_closed.closing_details'))).html_safe)
+ = t('views.users.dossiers.dossiers_list.procedure_closed.en_cours.internal_procedure_html', link: link_to(t('views.users.dossiers.dossiers_list.procedure_closed.procedure'), commencer_path(dossier.procedure.replaced_by_procedure.path), **external_link_attributes, title: new_tab_suffix(t('views.users.dossiers.dossiers_list.procedure_closed.title_new_procedure'))).html_safe)
+ - elsif dossier.en_construction_ou_instruction?
+ = t('views.users.dossiers.dossiers_list.procedure_closed.en_cours.other_html', link: link_to(t('views.users.dossiers.dossiers_list.procedure_closed.more_details'), closing_details_path(dossier.procedure.path), **external_link_attributes, title: new_tab_suffix(t('views.users.dossiers.dossiers_list.procedure_closed.title_closing_details'))).html_safe)
- elsif dossier.termine? && dossier.procedure.closing_reason_internal_procedure?
- = t('views.users.dossiers.dossiers_list.procedure_closed.termine.internal_procedure_html', link: link_to(t('views.users.dossiers.dossiers_list.procedure_closed.this_procedure'), commencer_path(dossier.procedure.replaced_by_procedure.path), **external_link_attributes, title: new_tab_suffix(t('views.users.dossiers.dossiers_list.procedure_closed.closing_details'))).html_safe)
- - elsif dossier.termine? && dossier.procedure.closing_reason_other?
- = t('views.users.dossiers.dossiers_list.procedure_closed.termine.other_html', link: link_to(t('views.users.dossiers.dossiers_list.procedure_closed.here'), closing_details_path(dossier.procedure.path), **external_link_attributes, title: new_tab_suffix(t('views.users.dossiers.dossiers_list.procedure_closed.closing_details'))).html_safe)
+ = t('views.users.dossiers.dossiers_list.procedure_closed.termine.internal_procedure_html', link: link_to(t('views.users.dossiers.dossiers_list.procedure_closed.this_procedure'), commencer_path(dossier.procedure.replaced_by_procedure.path), **external_link_attributes, title: new_tab_suffix(t('views.users.dossiers.dossiers_list.procedure_closed.title_new_procedure'))).html_safe)
+ - elsif dossier.termine?
+ = t('views.users.dossiers.dossiers_list.procedure_closed.termine.other_html', link: link_to(t('views.users.dossiers.dossiers_list.procedure_closed.more_details'), closing_details_path(dossier.procedure.path), **external_link_attributes, title: new_tab_suffix(t('views.users.dossiers.dossiers_list.procedure_closed.title_closing_details'))).html_safe)
- if dossier.pending_correction?
= render Dsfr::AlertComponent.new(state: :warning, size: :sm, extra_class_names: "fr-mb-2w") do |c|
@@ -79,9 +78,9 @@
- c.with_body do
%p
- if dossier.transfer.from_support?
- = t('views.users.dossiers.transfers.receiver_demande_en_cours_from_support', id: dossier.id, email: dossier.user.email)
+ = t('views.users.dossiers.transfers.receiver_demande_en_cours_from_support', id: dossier.id, email: dossier.user_email_for(:notification))
- else
- = t('views.users.dossiers.transfers.receiver_demande_en_cours', id: dossier.id, email: dossier.user.email)
+ = t('views.users.dossiers.transfers.receiver_demande_en_cours', id: dossier.id, email: dossier.user_email_for(:notification))
%p
= link_to t('views.users.dossiers.transfers.accept'), transfer_path(dossier.transfer), class: "fr-link fr-mr-1w", method: :put
= link_to t('views.users.dossiers.transfers.reject'), transfer_path(dossier.transfer), class: "fr-link", method: :delete
@@ -97,14 +96,24 @@
= link_to t('views.users.dossiers.transfers.revoke'), transfer_path(dossier.transfer), class: 'fr-link', method: :delete
- - if ["dossiers-transferes", "dossiers-supprimes-recemment"].exclude?(@statut)
+ - if ["dossiers-transferes", "dossiers-supprimes"].exclude?(@statut)
.flex.justify-end
= render partial: 'dossier_actions', locals: { dossier: dossier }
- - if @statut == "dossiers-supprimes-recemment"
+ - if @statut == "dossiers-supprimes"
.flex.justify-end
- = link_to restore_dossier_path(dossier.id), method: :patch, class: "fr-btn fr-btn--sm" do
- Restaurer
+ - if dossier.hidden_by_reason != 'expired'
+ = link_to restore_dossier_path(dossier.id), method: :patch, class: "fr-btn fr-btn--sm" do
+ Restaurer
+
+ - else
+ - if dossier.expiration_can_be_extended?
+ = button_to users_dossier_repousser_expiration_and_restore_path(dossier), class: 'fr-btn fr-btn--sm' do
+ Restaurer et étendre la conservation
+
+
+ - else
+ = render(partial: 'users/dossiers/show/download_dossier', locals: { dossier: dossier })
= paginate dossiers, views_prefix: 'shared'
@@ -131,4 +140,4 @@
%p.empty-text-details
= t('views.users.dossiers.dossiers_list.no_result_text_html', app_base: Current.application_base_url)
%p
- = link_to t("root.landing.how_to_find_procedure"), t("links.common.faq.comment_trouver_ma_demarche_url"), class: "fr-btn fr-btn--lg fr-mr-1w fr-mb-2w", **external_link_attributes
+ = link_to t("root.landing.how_to_find_procedure"), t("links.common.faq.comment_trouver_ma_demarche_url"), class: "fr-btn fr-btn--lg fr-mr-1w fr-mb-2w"
diff --git a/app/views/users/dossiers/_expiration_banner.html.haml b/app/views/users/dossiers/_expiration_banner.html.haml
index 509d1eb81..908dffa6e 100644
--- a/app/views/users/dossiers/_expiration_banner.html.haml
+++ b/app/views/users/dossiers/_expiration_banner.html.haml
@@ -3,8 +3,9 @@
%p.expires_at
%small= t("shared.dossiers.header.expires_at.#{dossier.state}", date: safe_expiration_date(dossier), duree_conservation_totale: dossier.duree_totale_conservation_in_months)
- - if dossier.close_to_expiration?
- = render Dsfr::CalloutComponent.new(title: t('users.dossiers.header.banner.title'), theme: :warning) do |c|
+ - if dossier.close_to_expiration? || dossier.has_expired?
+ - title = dossier.has_expired? ? 'title_expired' : 'title'
+ = render Dsfr::CalloutComponent.new(title: t("users.dossiers.header.banner.#{title}"), theme: :warning) do |c|
- c.with_body do
- if dossier.brouillon?
= t('users.dossiers.header.banner.states.brouillon')
diff --git a/app/views/users/dossiers/_merci.html.haml b/app/views/users/dossiers/_merci.html.haml
index 383f502d9..d131104f6 100644
--- a/app/views/users/dossiers/_merci.html.haml
+++ b/app/views/users/dossiers/_merci.html.haml
@@ -1,26 +1,34 @@
.merci.text-center.mb-7
- .container
- = image_tag('user/envoi-dossier.svg', alt: '', class: 'mt-8')
- %h1.mt-4.mb-3.mx-0= t('views.users.dossiers.merci.thanks')
- %h2.send.m-2.text-lg
- = t('views.users.dossiers.merci.dossier_send_l1')
- %strong= procedure.libelle
- = t('views.users.dossiers.merci.dossier_send_l2')
- %p.m-2
- = t('views.users.dossiers.merci.dossier_acces_l1')
- %strong= t('views.users.dossiers.merci.dossier_acces_l2')
- %p.m-2
- = t('views.users.dossiers.merci.dossier_edit_l1')
- - if !dossier&.read_only? && !procedure.declarative_accepte? && !procedure.sva_svr_enabled?
- %strong= t('views.users.dossiers.merci.dossier_edit_l2')
- = t('views.users.dossiers.merci.dossier_edit_l3')
- %strong= t('views.users.dossiers.merci.dossier_edit_l4')
- - if procedure.active_dossier_submitted_message
- %p.m-2= procedure.active_dossier_submitted_message.message_on_submit_by_usager
+ .fr-container
+ .fr-grid-row.fr-col-offset-md-2.fr-col-md-8
+ .fr-col-12
+ = image_tag('user/envoi-dossier.svg', alt: '', class: 'mt-8')
+ %h1.fr-mt-4w.fr-mb-3w.mx-0= t('views.users.dossiers.merci.thanks')
+ %h2.send.fr-m-2w.text-lg
+ = t('views.users.dossiers.merci.dossier_send_l1')
+ %strong= procedure.libelle
+ = t('views.users.dossiers.merci.dossier_send_l2')
+ %p.fr-m-2w
+ = t('views.users.dossiers.merci.dossier_acces_l1')
+ %strong= t('views.users.dossiers.merci.dossier_acces_l2')
+ %p.fr-m-2w
+ = t('views.users.dossiers.merci.dossier_edit_l1')
+ - if !dossier&.read_only? && !procedure.declarative_accepte? && !procedure.sva_svr_enabled?
+ %strong= t('views.users.dossiers.merci.dossier_edit_l2')
+ = t('views.users.dossiers.merci.dossier_edit_l3')
+ %strong= t('views.users.dossiers.merci.dossier_edit_l4')
+ - if procedure.active_dossier_submitted_message
+ %p.fr-m-2= procedure.active_dossier_submitted_message.message_on_submit_by_usager
+ %p.justify-center.flex.fr-mb-5w.fr-mt-2w
+ = render(partial: 'users/dossiers/show/download_dossier', locals: { dossier: dossier })
+ = link_to t('views.users.dossiers.merci.acces_dossier'), dossier ? dossier_path(dossier) : "#dossier" , class: 'fr-btn fr-mx-2w'
- .flex.column.align-center
- = link_to t('views.users.dossiers.merci.acces_dossier'), dossier ? dossier_path(dossier) : "#dossier" , class: 'fr-btn fr-btn--xl fr-mt-5w'
- = link_to t('views.users.dossiers.merci.submit_dossier'), commencer_url(procedure.path), class: 'fr-btn fr-btn--secondary fr-mt-3w'
+ %hr.fr-hr
+ = link_to t('views.users.dossiers.merci.submit_dossier'), commencer_url(procedure.path), class: 'fr-btn fr-btn--secondary fr-mt-2w'
- .monavis
- != procedure.monavis_embed
+ - if procedure.monavis_embed
+ .monavis
+ %p.fr-mt-5w.fr-mb-1w
+ %strong= t('views.users.dossiers.merci.jdma_l1')
+ %p= t('views.users.dossiers.merci.jdma_l2')
+ != procedure.monavis_embed_html_source("site")
diff --git a/app/views/users/dossiers/_procedure_removed_banner.html.haml b/app/views/users/dossiers/_procedure_removed_banner.html.haml
index cffffab84..a9156cdb5 100644
--- a/app/views/users/dossiers/_procedure_removed_banner.html.haml
+++ b/app/views/users/dossiers/_procedure_removed_banner.html.haml
@@ -17,4 +17,4 @@
= t('users.dossiers.header.banner.contact_service_html', service_name: dossier.procedure.service.nom, service_phone_number: Phonelib.parse(dossier.procedure.service.telephone_url).full_national, service_email: dossier.procedure.service.email)
- if !dossier.brouillon?
- = render(partial: 'users/dossiers/show/print_dossier', locals: { dossier: dossier })
+ = render(partial: 'users/dossiers/show/download_dossier', locals: { dossier: dossier })
diff --git a/app/views/users/dossiers/deleted_dossiers.html.haml b/app/views/users/dossiers/deleted_dossiers.html.haml
new file mode 100644
index 000000000..938f00cc3
--- /dev/null
+++ b/app/views/users/dossiers/deleted_dossiers.html.haml
@@ -0,0 +1,6 @@
+- content_for(:title, "Historique des dossiers supprimés")
+
+= render partial: 'administrateurs/breadcrumbs',
+ locals: { steps: [['Historique des dossiers supprimés']] }
+
+= render Dossiers::DeletedDossiersComponent.new(deleted_dossiers: @deleted_dossiers)
diff --git a/app/views/users/dossiers/demande.html.haml b/app/views/users/dossiers/demande.html.haml
index 346b2cfa1..ef21d650e 100644
--- a/app/views/users/dossiers/demande.html.haml
+++ b/app/views/users/dossiers/demande.html.haml
@@ -6,18 +6,15 @@
.dossier-container.fr-mb-4w
= render partial: 'users/dossiers/show/header', locals: { dossier: @dossier }
- - if @dossier.en_construction?
- .fr-container
- .fr-grid-row.fr-grid-row--center
- .fr-col-xl-10
+ .fr-container
+ .fr-grid-row.fr-grid-row--center
+ .fr-col-md-9
+ - if @dossier.en_construction?
= render Dossiers::EnConstructionNotSubmittedComponent.new(dossier: @dossier, user: current_user)
- = render partial: 'shared/dossiers/demande', locals: { dossier: @dossier, demande_seen_at: nil, profile: 'usager' }
+ = render partial: 'shared/dossiers/demande', locals: { dossier: @dossier, demande_seen_at: nil, profile: 'usager' }
-
- - if !@dossier.read_only?
- .fr-container.fr-mt-2w
- .fr-grid-row
- .fr-col-xl-8.fr-col-offset-xl-2
- %p= link_to t('views.users.dossiers.demande.edit_dossier'), modifier_dossier_path(@dossier), class: 'fr-btn fr-btn-sm',
- title: t('views.users.dossiers.demande.edit_dossier_title')
+ - if !@dossier.read_only?
+ .fr-px-2w.fr-mt-2w
+ %p= link_to t('views.users.dossiers.demande.edit_dossier'), modifier_dossier_path(@dossier), class: 'fr-btn fr-btn-sm',
+ title: t('views.users.dossiers.demande.edit_dossier_title')
diff --git a/app/views/users/dossiers/identite.html.haml b/app/views/users/dossiers/identite.html.haml
index ed0f5f432..82bb5bbab 100644
--- a/app/views/users/dossiers/identite.html.haml
+++ b/app/views/users/dossiers/identite.html.haml
@@ -1,100 +1,33 @@
-- content_for(:title, "Nouveau dossier (#{@dossier.procedure.libelle})")
+- content_for(:title, t(".title", scope: :metas, procedure_label: @dossier.procedure.libelle))
= render partial: "shared/dossiers/submit_is_over", locals: { dossier: @dossier }
- if !dossier_submission_is_closed?(@dossier)
- = form_for @dossier, url: update_identite_dossier_path(@dossier), html: { class: "form", "data-controller" => "for-tiers" } do |f|
+ - if @dossier.procedure.for_tiers_enabled?
+ = form_for @dossier, url: identite_dossier_path(@dossier), method: :patch, html: { class: "form" }, data: {turbo: true, controller: :autosubmit} do |f|
- %fieldset#radio-rich-hint.fr-fieldset{ "aria-labelledby" => "radio-rich-hint-legend radio-rich-hint-messages" }
- %legend#radio-rich-hint-legend.fr-fieldset__legend--regular.fr-fieldset__legend
- = t('views.users.dossiers.identite.legend')
+ %p.fr-text--sm= t('utils.asterisk_html')
- .fr-fieldset__element
- .fr-radio-group.fr-radio-rich
- = f.radio_button :for_tiers, false, required: true, id: "radio-self-manage", "data-action" => "click->for-tiers#toggleFieldRequirements", "data-for-tiers-target" => "forTiers"
- %label.fr-label{ for: "radio-self-manage" }
- = t('activerecord.attributes.dossier.for_tiers.false')
- .fr-radio-rich__img
- %span.fr-icon-user-fill
- .fr-fieldset__element
- .fr-radio-group.fr-radio-rich
- = f.radio_button :for_tiers, true, required: true, id: "radio-tiers-manage", "data-action" => "click->for-tiers#toggleFieldRequirements", "data-for-tiers-target" => "forTiers"
- %label.fr-label{ for: "radio-tiers-manage" }
- = t('activerecord.attributes.dossier.for_tiers.true')
- .fr-radio-rich__img
- %span.fr-icon-parent-fill
-
- .mandataire-infos{ "data-for-tiers-target" => "mandataireBlock" }
- .fr-alert.fr-alert--info.fr-mb-2w
- %p.fr-notice__text
- = t('views.users.dossiers.identite.callout_text')
- = link_to(t('views.users.dossiers.identite.callout_link'),
- 'https://www.legifrance.gouv.fr/codes/section_lc/LEGITEXT000006070721/LEGISCTA000006136404/#LEGISCTA000006136404',
- title: new_tab_suffix(t('views.users.dossiers.identite.callout_link_title')),
- **external_link_attributes)
-
-
- %fieldset.fr-fieldset
+ %fieldset#radio-rich-hint.fr-fieldset
%legend.fr-fieldset__legend--regular.fr-fieldset__legend
- %h2.fr-h4= t('views.users.dossiers.identite.self_title')
+ = t('views.users.dossiers.identite.legend')
+ = render EditableChamp::AsteriskMandatoryComponent.new
- .fr-fieldset__element.fr-fieldset__element--short-text
- = render Dsfr::InputComponent.new(form: f, attribute: :mandataire_first_name, opts: { "data-for-tiers-target" => "mandataireFirstName" })
+ .fr-fieldset__element
+ .fr-radio-group.fr-radio-rich
+ = f.radio_button :for_tiers, false, required: true, id: "radio-self-manage"
+ %label.fr-label{ for: "radio-self-manage" }
+ = t('activerecord.attributes.dossier.for_tiers.false')
+ .fr-radio-rich__img
+ %span.fr-icon-user-fill
+ .fr-fieldset__element
+ .fr-radio-group.fr-radio-rich
+ = f.radio_button :for_tiers, true, required: true, id: "radio-tiers-manage"
+ %label.fr-label{ for: "radio-tiers-manage" }
+ = t('activerecord.attributes.dossier.for_tiers.true')
+ .fr-radio-rich__img
+ %span.fr-icon-parent-fill
- .fr-fieldset__element.fr-fieldset__element--short-text
- = render Dsfr::InputComponent.new(form: f, attribute: :mandataire_last_name, opts: { "data-for-tiers-target" => "mandataireLastName" })
+ = f.submit t('views.users.dossiers.identite.continue'), class: 'hidden'
- = f.fields_for :individual, include_id: false do |individual|
- .individual-infos
- %fieldset.fr-fieldset
- %legend.fr-fieldset__legend--regular.fr-fieldset__legend{ "data-for-tiers-target" => "mandataireTitle" }
- %h2.fr-h4= t('views.users.dossiers.identite.self_title')
-
- %legend.fr-fieldset__legend--regular.fr-fieldset__legend.hidden{ "data-for-tiers-target" => "beneficiaireTitle" }
- %h2.fr-h4= t('views.users.dossiers.identite.beneficiaire_title')
-
-
- %legend.fr-fieldset__legend--regular.fr-fieldset__legend
- = t('activerecord.attributes.individual.gender')
- = render EditableChamp::AsteriskMandatoryComponent.new
- .fr-fieldset__element
- .fr-radio-group
- = individual.radio_button :gender, Individual::GENDER_FEMALE, required: true, id: "identite_champ_radio_#{Individual::GENDER_FEMALE}"
- %label.fr-label{ for: "identite_champ_radio_#{Individual::GENDER_FEMALE}" }
- = Individual.human_attribute_name('gender.female')
- .fr-fieldset__element
- .fr-radio-group
- = individual.radio_button :gender, Individual::GENDER_MALE, required: true, id: "identite_champ_radio_#{Individual::GENDER_MALE}"
- %label.fr-label{ for: "identite_champ_radio_#{Individual::GENDER_MALE}" }
- = Individual.human_attribute_name('gender.male')
-
- .fr-fieldset__element.fr-fieldset__element--short-text
- = render Dsfr::InputComponent.new(form: individual, attribute: :prenom, opts: { autocomplete: 'given-name' })
-
- .fr-fieldset__element.fr-fieldset__element--short-text
- = render Dsfr::InputComponent.new(form: individual, attribute: :nom, opts: { autocomplete: 'family-name' })
-
- %fieldset.fr-fieldset{ "data-for-tiers-target" => "beneficiaireNotificationBlock" }
- %legend.fr-fieldset__legend--regular.fr-fieldset__legend
- = t('activerecord.attributes.individual.notification_method')
- = render EditableChamp::AsteriskMandatoryComponent.new
-
- - Individual.notification_methods.each do |method, _|
- .fr-fieldset__element
- .fr-radio-group
- = individual.radio_button :notification_method, method, id: "notification_method_#{method}", "data-action" => "for-tiers#toggleFieldRequirements", "data-for-tiers-target" => "notificationMethod"
- %label.fr-label{ for: "notification_method_#{method}" }
- = t("activerecord.attributes.individual.notification_methods.#{method}")
-
-
- .fr-fieldset__element.fr-fieldset__element--short-text.hidden{ "data-for-tiers-target" => "email" }
- = render Dsfr::InputComponent.new(form: individual, attribute: :email)
-
-
- - if @dossier.procedure.ask_birthday?
- .fr-fieldset__element
- = render Dsfr::InputComponent.new(form: individual, attribute: :birthdate, input_type: :date_field,
- opts: { placeholder: 'Format : AAAA-MM-JJ', max: Date.today.iso8601, min: "1900-01-01", autocomplete: 'bday' })
-
-
- = f.submit t('views.users.dossiers.identite.continue'), class: "fr-btn"
+ = render Dossiers::IndividualFormComponent.new(dossier: @dossier)
diff --git a/app/views/users/dossiers/identite.turbo_stream.haml b/app/views/users/dossiers/identite.turbo_stream.haml
new file mode 100644
index 000000000..b7c032d59
--- /dev/null
+++ b/app/views/users/dossiers/identite.turbo_stream.haml
@@ -0,0 +1 @@
+= turbo_stream.replace 'identite-form', render(Dossiers::IndividualFormComponent.new(dossier: @dossier))
diff --git a/app/views/users/dossiers/index.html.haml b/app/views/users/dossiers/index.html.haml
index b481b94c3..b3e2be368 100644
--- a/app/views/users/dossiers/index.html.haml
+++ b/app/views/users/dossiers/index.html.haml
@@ -7,65 +7,63 @@
.fr-container
%h1.page-title.fr-h2= t('views.users.dossiers.index.dossiers')
- .fr-grid-row.fr-grid-row--gutters
- - if current_user.dossiers.count > 2 || current_user.dossiers_invites.count > 2
- .fr-col
- #search-2.fr-search-bar
- = form_tag dossiers_path, method: :get, :role => "search", class: "flex width-100 fr-mb-5w" do
- = hidden_field_tag :procedure_id, params[:procedure_id]
- = label_tag "q", t('views.users.dossiers.search.search_file'), class: 'fr-label'
- = text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: t('views.users.dossiers.search.search_file'), class: "fr-input"
- %button.fr-btn.fr-btn--sm
- = t('views.users.dossiers.search.simple')
- - if @procedures_for_select.size > 1
- .fr-col
- = render Dossiers::UserProcedureFilterComponent.new(procedures_for_select: @procedures_for_select)
+ - if current_user.dossiers.count > 2 || current_user.dossiers_invites.count > 2 || @procedures_for_select.size > 1
+ .fr-grid-row.fr-grid-row--gutters
+ - if current_user.dossiers.count > 2 || current_user.dossiers_invites.count > 2
+ .fr-col.fr-mb-5w
+ #search-2.fr-search-bar
+ = form_tag dossiers_path, method: :get, :role => "search", class: "width-100" do
+ = hidden_field_tag :procedure_id, params[:procedure_id]
+ = label_tag "q", t('views.users.dossiers.search.label'), class: 'fr-label fr-mb-1w'
+ .flex
+ = text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: t('views.users.dossiers.search.prompt'), class: "fr-input"
+ %button.fr-btn.fr-btn--sm
+ = t('views.users.dossiers.search.label')
+ - if @procedures_for_select.size > 1
+ .fr-col.fr-mb-5w
+ = render Dossiers::UserProcedureFilterComponent.new(procedures_for_select: @procedures_for_select)
- if @search_terms.blank?
- - cache([I18n.locale, current_user.id, @statut, current_user.dossiers, current_user.dossiers_invites], expires_in: 1.hour) do
- %nav.fr-tabs{ role: 'navigation', 'aria-label': t('views.users.dossiers.secondary_menu') }
- %ul.fr-tabs__list{ role: 'tablist' }
- - if @user_dossiers.present?
- = tab_item(t('pluralize.en_cours', count: @user_dossiers.count),
- dossiers_path(statut: 'en-cours', procedure_id: params[:procedure_id]),
- active: @statut == 'en-cours',
- badge: number_with_html_delimiter(@user_dossiers.count))
- - if @dossiers_traites.present?
- // TODO: when renaming this tab in "Terminé", update notify_near_deletion_to_user email wording accordingly.
- = tab_item(t('pluralize.traites', count: @dossiers_traites.count),
- dossiers_path(statut: 'traites', procedure_id: params[:procedure_id]),
- active: @statut == 'traites',
- badge: number_with_html_delimiter(@dossiers_traites.count))
+ - if [@user_dossiers, @dossiers_traites, @dossiers_invites, @dossiers_close_to_expiration, @dossiers_supprimes, @dossier_transferes].any?(&:present?)
+ - cache([I18n.locale, current_user.id, @statut, current_user.dossiers, current_user.dossiers_invites], expires_in: 1.hour) do
+ %nav.fr-tabs{ role: 'navigation', 'aria-label': t('views.users.dossiers.secondary_menu') }
+ %ul.fr-tabs__list{ role: 'tablist' }
+ - if @user_dossiers.present?
+ = tab_item(t('pluralize.en_cours', count: @user_dossiers.count),
+ dossiers_path(statut: 'en-cours', procedure_id: params[:procedure_id]),
+ active: @statut == 'en-cours',
+ badge: number_with_html_delimiter(@user_dossiers.count))
- - if @dossiers_invites.present?
- = tab_item(t('pluralize.dossiers_invites', count: @dossiers_invites.count),
- dossiers_path(statut: 'dossiers-invites', procedure_id: params[:procedure_id]),
- active: @statut == 'dossiers-invites',
- badge: number_with_html_delimiter(@dossiers_invites.count))
+ - if @dossiers_traites.present?
+ // TODO: when renaming this tab in "Terminé", update notify_near_deletion_to_user email wording accordingly.
+ = tab_item(t('pluralize.traites', count: @dossiers_traites.count),
+ dossiers_path(statut: 'traites', procedure_id: params[:procedure_id]),
+ active: @statut == 'traites',
+ badge: number_with_html_delimiter(@dossiers_traites.count))
- - if @dossiers_close_to_expiration.count > 0
- = tab_item(t('pluralize.dossiers_close_to_expiration', count: @dossiers_close_to_expiration.count),
- dossiers_path(statut: 'dossiers-expirant', procedure_id: params[:procedure_id]),
- active: @statut == 'dossiers-expirant',
- badge: number_with_html_delimiter(@dossiers_close_to_expiration.count))
+ - if @dossiers_invites.present?
+ = tab_item(t('pluralize.dossiers_invites', count: @dossiers_invites.count),
+ dossiers_path(statut: 'dossiers-invites', procedure_id: params[:procedure_id]),
+ active: @statut == 'dossiers-invites',
+ badge: number_with_html_delimiter(@dossiers_invites.count))
- - if @dossiers_supprimes_recemment.present?
- = tab_item(t('pluralize.dossiers_supprimes_recemment', count: @dossiers_supprimes_recemment.count),
- dossiers_path(statut: 'dossiers-supprimes-recemment', procedure_id: params[:procedure_id]),
- active: @statut == 'dossiers-supprimes-recemment',
- badge: number_with_html_delimiter(@dossiers_supprimes_recemment.count))
+ - if @dossiers_close_to_expiration.count > 0
+ = tab_item(t('pluralize.dossiers_close_to_expiration', count: @dossiers_close_to_expiration.count),
+ dossiers_path(statut: 'dossiers-expirant', procedure_id: params[:procedure_id]),
+ active: @statut == 'dossiers-expirant',
+ badge: number_with_html_delimiter(@dossiers_close_to_expiration.count))
- - if @dossiers_supprimes_definitivement.present?
- = tab_item(t('pluralize.dossiers_supprimes_definitivement', count: @dossiers_supprimes_definitivement.count),
- dossiers_path(statut: 'dossiers-supprimes-definitivement', procedure_id: params[:procedure_id]),
- active: @statut == 'dossiers-supprimes-definitivement',
- badge: number_with_html_delimiter(@dossiers_supprimes_definitivement.count))
+ - if @dossiers_supprimes.present?
+ = tab_item(t('pluralize.dossiers_supprimes', count: @dossiers_supprimes.count),
+ dossiers_path(statut: 'dossiers-supprimes', procedure_id: params[:procedure_id]),
+ active: @statut == 'dossiers-supprimes',
+ badge: number_with_html_delimiter(@dossiers_supprimes.count))
- - if @dossier_transferes.present?
- = tab_item(t('pluralize.dossiers_transferes', count: @dossier_transferes.count),
- dossiers_path(statut: 'dossiers-transferes', procedure_id: params[:procedure_id]),
- active: @statut == 'dossiers-transferes',
- badge: number_with_html_delimiter(@dossier_transferes.count))
+ - if @dossier_transferes.present?
+ = tab_item(t('pluralize.dossiers_transferes', count: @dossier_transferes.count),
+ dossiers_path(statut: 'dossiers-transferes', procedure_id: params[:procedure_id]),
+ active: @statut == 'dossiers-transferes',
+ badge: number_with_html_delimiter(@dossier_transferes.count))
.fr-container
.fr-grid-row.fr-grid-row--center
@@ -87,9 +85,4 @@
- else
= render Dossiers::UserFilterComponent.new(statut: @statut, filter: @filter, procedure_id: @procedure_id )
-
- - if @statut == "dossiers-supprimes-definitivement"
- -# /!\ in this context, @dossiers is a collection of DeletedDossier not Dossier
- = render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers }
- - else
- = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut, search: false }
+ = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut, search: false }
diff --git a/app/views/users/dossiers/papertrail.pdf.prawn b/app/views/users/dossiers/papertrail.pdf.prawn
index 898163eac..c1449555c 100644
--- a/app/views/users/dossiers/papertrail.pdf.prawn
+++ b/app/views/users/dossiers/papertrail.pdf.prawn
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'prawn/measurement_extensions'
#----- A4 page size
@@ -52,7 +54,7 @@ prawn_document(margin: [top_margin, right_margin, bottom_margin, left_margin], p
pdf.fill_color grey
pdf.text "#{Individual.human_attribute_name(:prenom)} : #{@dossier.individual.prenom}", size: 10, character_spacing: -0.2, align: :justify
pdf.text "#{Individual.human_attribute_name(:nom)} : #{@dossier.individual.nom.upcase}", size: 10, character_spacing: -0.2, align: :justify
- pdf.text "#{User.human_attribute_name(:email)} : #{@dossier.user.email}", size: 10, character_spacing: -0.2, align: :justify
+ pdf.text "#{User.human_attribute_name(:email)} : #{@dossier.user_email_for(:display)}", size: 10, character_spacing: -0.2, align: :justify
end
end
@@ -61,7 +63,7 @@ prawn_document(margin: [top_margin, right_margin, bottom_margin, left_margin], p
pdf.fill_color grey
pdf.text "Dénomination : " + raison_sociale_or_name(@dossier.etablissement), size: 10, character_spacing: -0.2, align: :justify
pdf.text "SIRET : " + @dossier.etablissement.siret, size: 10, character_spacing: -0.2, align: :justify
- pdf.text "#{User.human_attribute_name(:email)} : #{@dossier.user.email}", size: 10, character_spacing: -0.2, align: :justify
+ pdf.text "#{User.human_attribute_name(:email)} : #{@dossier.user_email_for(:display)}", size: 10, character_spacing: -0.2, align: :justify
end
end
diff --git a/app/views/users/dossiers/show/_download_dossier.html.haml b/app/views/users/dossiers/show/_download_dossier.html.haml
new file mode 100644
index 000000000..ed9245955
--- /dev/null
+++ b/app/views/users/dossiers/show/_download_dossier.html.haml
@@ -0,0 +1 @@
+= link_to "#{t('views.users.dossiers.merci.download_dossier')} (PDF)", dossier ? dossier_path(dossier, format: :pdf) : "#", download: "Mon dossier", target: "_blank", rel: "noopener", class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-download-line'
diff --git a/app/views/users/dossiers/show/_header.html.haml b/app/views/users/dossiers/show/_header.html.haml
index 2d0a73858..167029020 100644
--- a/app/views/users/dossiers/show/_header.html.haml
+++ b/app/views/users/dossiers/show/_header.html.haml
@@ -5,7 +5,7 @@
= status_badge_user(dossier, 'super')
= pending_correction_badge(:for_user) if dossier.pending_correction?
%h2
- = t('views.users.dossiers.show.header.dossier_number', dossier_id: dossier.id)
+ = t('views.users.dossiers.show.header.dossier_number_html', dossier_id: dossier.id)
- if dossier.depose_at.present?
= t('views.users.dossiers.show.header.submit_date', date_du_dossier: I18n.l(dossier.depose_at))
@@ -14,12 +14,13 @@
- if dossier.show_procedure_state_warning?
= render(partial: 'users/dossiers/procedure_removed_banner', locals: { dossier: dossier })
- elsif current_user.owns?(dossier)
- .header-actions
- = render partial: 'invites/dropdown', locals: { dossier: dossier, morphing: false }
- - if dossier.can_be_updated_by_user? && !current_page?(modifier_dossier_path(dossier))
- = link_to t('views.users.dossiers.demande.edit_dossier'), modifier_dossier_path(dossier), class: 'fr-btn fr-btn-sm',
- title: t('views.users.dossiers.demande.edit_dossier_title')
- = render(partial: 'users/dossiers/show/print_dossier', locals: { dossier: dossier })
+ .header-actions.fr-mb-3w
+ = render(partial: 'users/dossiers/show/download_dossier', locals: { dossier: dossier })
+ .ml-auto
+ = render partial: 'invites/dropdown', locals: { dossier: dossier, morphing: false }
+ - if dossier.can_be_updated_by_user? && !current_page?(modifier_dossier_path(dossier))
+ = link_to t('views.users.dossiers.demande.edit_dossier'), modifier_dossier_path(dossier), class: 'fr-btn fr-btn-sm fr-ml-1w',
+ title: t('views.users.dossiers.demande.edit_dossier_title')
%nav.fr-tabs
%ul.fr-tabs__list{ role: 'tablist' }
diff --git a/app/views/users/dossiers/show/_print_dossier.html.haml b/app/views/users/dossiers/show/_print_dossier.html.haml
deleted file mode 100644
index 1e2392431..000000000
--- a/app/views/users/dossiers/show/_print_dossier.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= link_to t('views.users.dossiers.show.header.print'), dossier_path(dossier, format: :pdf), target: "_blank", rel: "noopener", title: t('views.users.dossiers.show.header.print_dossier'), class: 'fr-btn fr-icon-printer-line fr-btn--tertiary'
diff --git a/app/views/users/dossiers/transferer_all.html.haml b/app/views/users/dossiers/transferer_all.html.haml
deleted file mode 100644
index c271974eb..000000000
--- a/app/views/users/dossiers/transferer_all.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-.container.mt-4
- Transferer les #{@transfer.dossiers.size} dossiers de votre compte vers le compte d’un autre usager :
-
- = form_for @transfer, url: transfers_path, html: { class: 'form mt-2' } do |f|
- = f.label :email, 'Email du compte destinataire'
- = f.email_field :email, required: true
- = f.submit "Envoyer la demande de transfert", class: 'button primary'
diff --git a/app/views/users/dossiers/update.turbo_stream.haml b/app/views/users/dossiers/update.turbo_stream.haml
index 91a898ab0..2cd14be47 100644
--- a/app/views/users/dossiers/update.turbo_stream.haml
+++ b/app/views/users/dossiers/update.turbo_stream.haml
@@ -1 +1,10 @@
= render partial: 'shared/dossiers/update_champs', locals: { to_show: @to_show, to_hide: @to_hide, to_update: @to_update, dossier: @dossier }
+
+- if !params.key?(:validate)
+ - if @can_passer_en_construction_was && !@can_passer_en_construction_is
+ = turbo_stream.append('contenu', render(Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: @dossier)))
+ - else @ineligibilite_rules_is_computable
+ = turbo_stream.remove(dom_id(@dossier, :ineligibilite_rules_broken))
+
+- if @update_contact_information
+ = turbo_stream.update "contact_information", partial: 'shared/dossiers/update_contact_information', locals: { dossier: @dossier, procedure: @dossier.procedure }
diff --git a/app/views/users/passwords/reset_link_sent.html.haml b/app/views/users/passwords/reset_link_sent.html.haml
index 200631a1f..fbbf6daab 100644
--- a/app/views/users/passwords/reset_link_sent.html.haml
+++ b/app/views/users/passwords/reset_link_sent.html.haml
@@ -3,33 +3,25 @@
- content_for :footer do
= render partial: 'root/footer'
-#link-sent.container
- = image_tag('user/confirmation-email.svg', "aria-hidden": true)
- %h1
- = t('views.users.passwords.reset_link_sent.got_it')
- %br
- = t('views.users.passwords.reset_link_sent.open_your_mailbox')
+.fr-container.fr-my-4w
+ .fr-grid-row.fr-grid-row--center
+ .fr-col-12.fr-col-md-9.fr-col-lg-7
+ %h1.fr-h2
+ = t('views.users.passwords.reset_link_sent.email_sent_html', email: @email, application_name: Current.application_name)
- %section.link-sent-info
- %p
- = t('views.users.passwords.reset_link_sent.email_sent_html', email: @email, application_name: Current.application_name)
- %p
- = t('views.users.passwords.reset_link_sent.click_link_to_reset_password')
- %p
- = t('views.users.shared.email_can_take_a_while_html')
-
- %section.link-sent-help
- %h2.link-sent-help-title= t('views.users.passwords.reset_link_sent.no_mail')
- %ol.link-sent-help-list
- %li
- = t('views.users.passwords.reset_link_sent.check_spams')
- %li
- = t('views.users.passwords.reset_link_sent.check_account', email: @email, application_name: Current.application_name)
- - if FranceConnectService.enabled?
- %li
- = t('views.users.passwords.reset_link_sent.check_france_connect_html', href: france_connect_particulier_path)
-
- %li
- = t('views.users.passwords.reset_link_sent.check_gpdr')
- %p
- = t('views.users.shared.contact_us_if_any_trouble_html', href: contact_url)
+ = render Dsfr::AlertComponent.new(title: t('views.users.passwords.reset_link_sent.no_mail'), state: '', extra_class_names: 'fr-alert--info' ) do |c|
+ - c.with_body do
+ %ol
+ %li
+ = t('views.users.shared.email_can_take_a_while_html')
+ %li
+ = t('views.users.passwords.reset_link_sent.check_spams')
+ %li
+ = t('views.users.passwords.reset_link_sent.check_account_html', email: @email, application_name: Current.application_name)
+ - if FranceConnectService.enabled?
+ %li
+ = t('views.users.passwords.reset_link_sent.check_france_connect_html', href: france_connect_particulier_path)
+ %li
+ = t('views.users.passwords.reset_link_sent.check_gpdr')
+ %p
+ = t('views.users.shared.contact_us_if_any_trouble_html', href: contact_url)
diff --git a/app/views/users/registrations/new.html.haml b/app/views/users/registrations/new.html.haml
index 62a30b357..41e5c23ea 100644
--- a/app/views/users/registrations/new.html.haml
+++ b/app/views/users/registrations/new.html.haml
@@ -1,4 +1,5 @@
= content_for(:page_id, 'auth')
+= content_for(:title, t('metas.signup.title'))
.auth-form
= devise_error_messages!
@@ -13,14 +14,16 @@
%h2.fr-h6= I18n.t('views.registrations.new.subtitle')
.fr-fieldset__element
- %p.fr-text--sm= t('utils.mandatory_champs')
+ %p.fr-text--sm= t('utils.asterisk_html')
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email', autofocus: true })
.fr-fieldset__element
- = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH }) do |c|
- - c.with_describedby do
- = render partial: "devise/password_rules", locals: { id: c.describedby_id }
+ = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field,
+ opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path, email_input_target: 'next'}, aria: {describedby: 'password_hint'}})
- %ul.fr-btns-group
- %li= f.submit t('views.shared.account.create'), class: "fr-btn"
+ #password_complexity
+ = render PasswordComplexityComponent.new
+
+ .fr-btns-group
+ = f.submit t('views.shared.account.create'), id: 'submit-password', disabled: :disabled, class: "fr-btn fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') }
diff --git a/app/views/users/sessions/link_sent.html.haml b/app/views/users/sessions/link_sent.html.haml
index b0cbd891f..1b64f8d87 100644
--- a/app/views/users/sessions/link_sent.html.haml
+++ b/app/views/users/sessions/link_sent.html.haml
@@ -3,27 +3,23 @@
- content_for :footer do
= render partial: 'root/footer'
-.fr-container.fr-my-5w
- .fr-grid-row
- .fr-col-12.fr-col-offset-md-1.fr-col-md-7
- %h1.fr-mt-6w Encore une petite étape !
+.fr-container
+ .fr-col-12.fr-col-md-6.fr-col-offset-md-3
+ %h1.fr-mt-6w.fr-h2.center
+ = t('views.confirmation.new.title')
- %section
- %p.fr-text--lead
- Nous venons de vous envoyer un courriel sur votre boite email #{@email}.
- Veuillez l’ouvrir et cliquer sur le lien de connexion sécurisée à #{Current.application_name}.
+ %p.center= image_tag("user/confirmation-email.svg", alt: '')
- %p.fr-text--lead
- Ce lien est valide une semaine et peut être réutilisé plusieurs fois.
+ = render Dsfr::AlertComponent.new(title: '', state: :info, heading_level: 'h2', extra_class_names: 'fr-mt-6w fr-mb-3w') do |c|
+ - c.with_body do
+ %p= t('views.users.sessions.link_sent.email_cta_html', email: @email)
+ %p= t('views.confirmation.new.email_guidelines_html')
- %p.fr-text--sm.fr-text-mention--grey
- Ce courriel peut mettre jusqu’à 15 minutes pour arriver. Si vous n’avez pas reçu de courriel (n’hésitez pas à vérifier dans les indésirables), cliquez sur le bouton ci-dessous.
+ %p.fr-text--sm.fr-text-mention--grey.fr-mb-1w
+ = t('views.confirmation.new.email_missing')
- = button_to instructeurs_reset_link_sent_path, class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-mail-line', method: 'POST' do
- Renvoyer le courriel
+ = button_to instructeurs_reset_link_sent_path, class: 'fr-btn fr-btn--secondary', method: 'POST' do
+ = t('views.confirmation.new.resent')
- %section
- %p.fr-mt-3w
- Si vous voyez cette page trop souvent, consultez notre aide : #{link_to t("links.common.faq.confirmer_compte_chaque_connexion_url"), t("links.common.faq.confirmer_compte_chaque_connexion_url"), **external_link_attributes}
- %p.fr-mt-3w
- = t('views.users.shared.contact_us_if_any_trouble_html', href: contact_admin_url)
+ %p.fr-text--sm.fr-text-mention--grey.fr-mt-3w.fr-mb-6w
+ = t('views.users.shared.contact_us_if_any_trouble_html', href: contact_admin_url)
diff --git a/app/views/users/sessions/new.html.haml b/app/views/users/sessions/new.html.haml
index a5ae68915..880d346d6 100644
--- a/app/views/users/sessions/new.html.haml
+++ b/app/views/users/sessions/new.html.haml
@@ -13,12 +13,12 @@
%h2.fr-h6= I18n.t('views.users.sessions.new.subtitle')
.fr-fieldset__element
- %p.fr-text--sm= t('utils.mandatory_champs')
+ %p.fr-text--sm= t('utils.asterisk_html')
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email', autofocus: true })
.fr-fieldset__element
- = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'current-password' })
+ = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'current-password', 'data-email-input-target': 'next' })
%p= link_to t('views.users.sessions.new.reset_password'), new_user_password_path, class: "fr-link"
@@ -30,11 +30,9 @@
= f.label :remember_me, t('views.users.sessions.new.remember_me'), class: 'remember-me'
.fr-fieldset__element
- %ul.fr-btns-group
- %li= f.submit t('views.users.sessions.new.connection'), class: "fr-btn fr-btn--lg"
+ .fr-btns-group= f.submit t('views.users.sessions.new.connection'), class: "fr-btn"
- if AgentConnectService.enabled?
%p.fr-hr-or= t('views.shared.france_connect_login.separator')
%h2.important-header.mb-1= t('views.users.sessions.new.state_civil_servant')
- %ul.fr-btns-group
- %li= link_to t('views.users.sessions.new.connect_with_agent_connect'), agent_connect_path, class: "fr-btn fr-btn--secondary"
+ .fr-btns-group= link_to t('views.users.sessions.new.connect_with_agent_connect'), agent_connect_path, class: "fr-btn fr-btn--secondary"
diff --git a/bin/setup b/bin/setup
index 9b8efb38d..22286e576 100755
--- a/bin/setup
+++ b/bin/setup
@@ -20,6 +20,7 @@ FileUtils.chdir APP_ROOT do
# Install JavaScript dependencies
system! 'bun --version'
system! 'bun install'
+ system! 'bunx playwright install chromium'
if ENV["UPDATE_WEBDRIVER"]
puts "\n== Updating webdrivers =="
diff --git a/bin/update b/bin/update
index a763ffe26..c0d9ecefe 100755
--- a/bin/update
+++ b/bin/update
@@ -17,6 +17,7 @@ FileUtils.chdir APP_ROOT do
system('bundle check') || system!('bundle install')
system! 'bun --version'
system! 'bun install'
+ system! 'bunx playwright install chromium'
if ENV["UPDATE_WEBDRIVER"]
puts "\n== Updating webdrivers =="
@@ -37,9 +38,12 @@ FileUtils.chdir APP_ROOT do
puts "\n== Running after_party tasks =="
system! 'bin/rails after_party:run'
+ puts "\n== Running on deploy maintenance tasks =="
+ system! 'bin/rails deploy:maintenance_tasks'
+
puts "\n== Removing old logs =="
system! 'bin/rails log:clear'
- puts "\n== Done =="
- puts "You can now start (or restart) the application server with `bin/rails server`."
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
end
diff --git a/bun.lockb b/bun.lockb
index 1270c43db..79b3381c9 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 000000000..a7dc5d4ec
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,10 @@
+coverage:
+ status:
+ project:
+ default:
+ informational: true
+ patch:
+ default:
+ informational: true
+comment:
+ require_changes: true
diff --git a/config.ru b/config.ru
index 4a3c09a68..2e0308469 100644
--- a/config.ru
+++ b/config.ru
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file is used by Rack-based servers to start the application.
require_relative "config/environment"
diff --git a/config/application.rb b/config/application.rb
index 638dfa89e..00dfa8033 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require_relative "boot"
require "rails/all"
@@ -11,7 +13,7 @@ Dotenv::Railtie.load
module TPS
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
- config.load_defaults 6.1
+ config.load_defaults 7.0
# Configuration for the application, engines, and railties goes here.
#
@@ -21,6 +23,7 @@ module TPS
Rails.autoloaders.main.ignore(Rails.root.join('lib/cops'))
Rails.autoloaders.main.ignore(Rails.root.join('lib/linters'))
Rails.autoloaders.main.ignore(Rails.root.join('lib/tasks/task_helper.rb'))
+ Rails.autoloaders.main.collapse('app/tasks/maintenance/concerns')
config.paths.add Rails.root.join('spec/mailers/previews').to_s, eager_load: true
config.autoload_paths << "#{Rails.root}/app/jobs/concerns"
@@ -51,14 +54,16 @@ module TPS
config.action_dispatch.ip_spoofing_check = false
# Set the queue name for the mail delivery jobs to 'mailers'
- config.action_mailer.deliver_later_queue_name = 'mailers'
+ config.action_mailer.deliver_later_queue_name = 'critical' # otherwise, :low
# Allow the error messages format to be customized
config.active_model.i18n_customize_full_message = true
# Set the queue name for the analysis jobs to 'active_storage_analysis'
- config.active_storage.queues.analysis = :active_storage_analysis
- config.active_storage.queues.purge = :purge
+ config.active_storage.queues.analysis = :default
+ config.active_storage.queues.purge = :low
+
+ config.active_support.cache_format_version = 7.0
config.to_prepare do
# Make main application helpers available in administrate
@@ -103,6 +108,7 @@ module TPS
config.active_record.encryption.primary_key = Rails.application.secrets.active_record_encryption.fetch(:primary_key)
config.active_record.encryption.key_derivation_salt = Rails.application.secrets.active_record_encryption.fetch(:key_derivation_salt)
+ config.active_record.partial_inserts = false
config.exceptions_app = self.routes
diff --git a/config/boot.rb b/config/boot.rb
index 3cda23b4d..38a47b2c5 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require "bundler/setup" # Set up gems listed in the Gemfile.
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
index 404123563..d064270c1 100644
--- a/config/brakeman.ignore
+++ b/config/brakeman.ignore
@@ -3,19 +3,19 @@
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
- "fingerprint": "1b805585567775589825c0eda58cb84c074fc760d0a7afb101c023a51427f2b5",
+ "fingerprint": "26f504696b074d18ef3f5568dc8f6a46d1283a67fe37822498fa25d0409664ab",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/users/dossiers/_merci.html.haml",
- "line": 26,
+ "line": 34,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
- "code": "current_user.dossiers.includes(:procedure).find(params[:id]).procedure.monavis_embed",
+ "code": "current_user.dossiers.includes(:procedure).find(params[:id]).procedure.monavis_embed_html_source(\"site\")",
"render_path": [
{
"type": "controller",
"class": "Users::DossiersController",
"method": "merci",
- "line": 302,
+ "line": 323,
"file": "app/controllers/users/dossiers_controller.rb",
"rendered": {
"name": "users/dossiers/merci",
@@ -51,7 +51,7 @@
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/graphql/connections/cursor_connection.rb",
- "line": 150,
+ "line": 152,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "items.order(order_column => ((:desc or :asc)), :id => ((:desc or :asc))).limit(limit).where(\"(#{order_table}.#{order_column}, #{order_table}.id) < (?, ?)\", timestamp, id)",
"render_path": null,
@@ -67,6 +67,86 @@
],
"note": ""
},
+ {
+ "warning_type": "SQL Injection",
+ "warning_code": 0,
+ "fingerprint": "7dc4935d5b68365bedb8f6b953f01b396cff4daa533c98ee56a84249ca5a1f90",
+ "check_name": "SQL",
+ "message": "Possible SQL injection",
+ "file": "app/tasks/maintenance/concerns/statements_helpers_concern.rb",
+ "line": 19,
+ "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
+ "code": "ApplicationRecord.connection.execute(\"SET LOCAL statement_timeout = '#{timeout}'\")",
+ "render_path": null,
+ "location": {
+ "type": "method",
+ "class": "Maintenance::StatementsHelpersConcern",
+ "method": "with_statement_timeout"
+ },
+ "user_input": "timeout",
+ "confidence": "Medium",
+ "cwe_id": [
+ 89
+ ],
+ "note": ""
+ },
+ {
+ "warning_type": "SQL Injection",
+ "warning_code": 0,
+ "fingerprint": "83b5a474065af330c47603d1f60fc31edaab55be162825385d53b77c1c98a6d7",
+ "check_name": "SQL",
+ "message": "Possible SQL injection",
+ "file": "app/models/columns/json_path_column.rb",
+ "line": 26,
+ "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
+ "code": "dossiers.with_type_de_champ(stable_id).where(\"champs.value_json @? '#{jsonpath} ? (@ like_regex \\\"#{quote_string(search_terms.join(\"|\"))}\\\" flag \\\"i\\\")'\")",
+ "render_path": null,
+ "location": {
+ "type": "method",
+ "class": "Columns::JSONPathColumn",
+ "method": "filtered_ids"
+ },
+ "user_input": "jsonpath",
+ "confidence": "Weak",
+ "cwe_id": [
+ 89
+ ],
+ "note": "escaped by hand"
+ },
+ {
+ "warning_type": "Cross-Site Scripting",
+ "warning_code": 2,
+ "fingerprint": "a7d18cc3434b4428a884f1217791f9a9db67839e73fb499f1ccd0f686f08eccc",
+ "check_name": "CrossSiteScripting",
+ "message": "Unescaped parameter value",
+ "file": "app/views/faq/show.html.haml",
+ "line": 13,
+ "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
+ "code": "Redcarpet::Markdown.new(Redcarpet::TrustedRenderer.new(view_context), :autolink => true).render(loader_service.find(\"#{params[:category]}/#{params[:slug]}\").content)",
+ "render_path": [
+ {
+ "type": "controller",
+ "class": "FAQController",
+ "method": "show",
+ "line": 14,
+ "file": "app/controllers/faq_controller.rb",
+ "rendered": {
+ "name": "faq/show",
+ "file": "app/views/faq/show.html.haml"
+ }
+ }
+ ],
+ "location": {
+ "type": "template",
+ "template": "faq/show"
+ },
+ "user_input": "params[:category]",
+ "confidence": "Weak",
+ "cwe_id": [
+ 79
+ ],
+ "note": "Theses params are not rendered"
+ },
{
"warning_type": "SQL Injection",
"warning_code": 0,
@@ -74,7 +154,7 @@
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/graphql/connections/cursor_connection.rb",
- "line": 153,
+ "line": 155,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "items.order(order_column => ((:desc or :asc)), :id => ((:desc or :asc))).limit(limit).where(\"(#{order_table}.#{order_column}, #{order_table}.id) > (?, ?)\", timestamp, id)",
"render_path": null,
@@ -93,20 +173,20 @@
{
"warning_type": "SQL Injection",
"warning_code": 0,
- "fingerprint": "bd1df30f95135357b646e21a03d95498874faffa32e3804fc643e9b6b957ee14",
+ "fingerprint": "afd2a1a41bd87fa62e065671670bd9bd8cc503ca4cbd3cfdb74a38a794146926",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/concerns/dossier_filtering_concern.rb",
- "line": 32,
+ "line": 35,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
- "code": "where(\"#{values.count} OR #{\"(#{ProcedurePresentation.sanitized_column(table, column)} ILIKE ?)\"}\", *values.map do\n \"%#{value}%\"\n end)",
+ "code": "where(\"#{DossierFilterService.sanitized_column(table, column)} LIKE ANY (ARRAY[?])\", search_terms.map do\n \"%#{sanitize_sql_like(_1)}%\"\n end)",
"render_path": null,
"location": {
"type": "method",
"class": "DossierFilteringConcern",
"method": null
},
- "user_input": "values.count",
+ "user_input": "DossierFilterService.sanitized_column(table, column)",
"confidence": "Medium",
"cwe_id": [
89
@@ -153,7 +233,7 @@
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/notification_mailer/send_notification_for_tiers.html.haml",
- "line": 29,
+ "line": 31,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "Current.application_name.gsub(\".\", \".\")",
"render_path": null,
@@ -169,6 +249,6 @@
"note": "Current is not a model"
}
],
- "updated": "2024-03-27 17:15:54 +0100",
+ "updated": "2024-11-12 17:33:07 +0100",
"brakeman_version": "6.1.2"
}
diff --git a/config/database.yml b/config/database.yml
index 9612bd9fd..d30715a17 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -14,6 +14,7 @@ development:
host: <%= ENV["DB_HOST"] %>
username: <%= ENV["DB_USERNAME"] %>
password: <%= ENV["DB_PASSWORD"] %>
+ port: <%= ENV["DB_PORT"] || 5432 %>
# Workaround for https://github.com/ged/ruby-pg/issues/311
gssencmode: disable
@@ -23,6 +24,7 @@ test:
host: localhost
username: tps_test
password: tps_test
+ port: <%= ENV["DB_PORT"] || 5432 %>
# Workaround for https://github.com/ged/ruby-pg/issues/311
gssencmode: disable
@@ -32,3 +34,4 @@ production: &production
host: <%= ENV["DB_HOST"] %>
username: <%= ENV["DB_USERNAME"] %>
password: <%= ENV["DB_PASSWORD"] %>
+ port: <%= ENV["DB_PORT"] || 5432 %>
diff --git a/config/deploy.rb b/config/deploy.rb
index e289888f8..bd6694041 100644
--- a/config/deploy.rb
+++ b/config/deploy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'mina/bundler'
require 'mina/git'
require 'mina/rails'
diff --git a/config/env.example b/config/env.example
index fe8967840..190b691ad 100644
--- a/config/env.example
+++ b/config/env.example
@@ -22,6 +22,7 @@ DB_HOST="localhost"
DB_POOL=""
DB_USERNAME="tps_development"
DB_PASSWORD="tps_development"
+DB_PORT=5432
# Protect access to the instance with a static login/password (useful for staging environments)
BASIC_AUTH_ENABLED="disabled"
diff --git a/config/env.example.optional b/config/env.example.optional
index df8621781..050e5d49b 100644
--- a/config/env.example.optional
+++ b/config/env.example.optional
@@ -32,6 +32,9 @@ DS_ENV="staging"
# AGENT_CONNECT_GOUV_SECRET=""
# AGENT_CONNECT_GOUV_REDIRECT=""
+# url to redirect user to when 2FA is not configured mon compte pro FI is used
+# MON_COMPTE_PRO_2FA_NOT_CONFIGURED_URL="https://app-sandbox.moncomptepro.beta.gouv.fr/connection-and-account?notification=2fa_not_configured"
+
# Certigna usage
# CERTIGNA_ENABLED="disabled" # "enabled" by default
@@ -61,6 +64,9 @@ DS_ENV="staging"
# Instance customization: URL of the Routage documentation
# ROUTAGE_URL=""
#
+# Instance customization: URL of the EligibiliteDossier documentation
+# ELIGIBILITE_URL=""
+#
# Instance customization: URL of the accessibility statement
# ACCESSIBILITE_URL=""
@@ -137,6 +143,7 @@ MATOMO_IFRAME_URL="https://matomo.example.org/index.php?module=CoreAdminHome&act
# DOLIST_ACCOUNT_ID=""
# DOLIST_NO_REPLY_EMAIL=""
# DOLIST_API_KEY=""
+# DOLIST_DEFAULT_SENDER_ID=""
# SMTP Provider: SIB (Brevo)
# SENDINBLUE_SMTP_ADDRESS=""
@@ -153,10 +160,6 @@ DOLIST_API_BALANCING_VALUE="50"
# Used only by a migration to choose your default regarding procedure archive dossiers after duree_conservation_dossiers_dans_ds
# DEFAULT_PROCEDURE_EXPIRES_WHEN_TERMINE_ENABLED=true
-# Enable vite legacy build (IE11). Legacy build is used in production (except if set to "disabled").
-# You might want to enable it in other environements for testing. Build time will be greatly impacted.
-VITE_LEGACY=""
-
# around july 2022, we changed the duree_conservation_dossiers_dans_ds, allow instances to choose their own duration
NEW_MAX_DUREE_CONSERVATION=12
@@ -243,9 +246,11 @@ REDIS_SIDEKIQ_MASTER='master_name'
REDIS_SIDEKIQ_PASSWORD='sentinel_and_redis_password'
REDIS_SIDEKIQ_USERNAME='sentinel_and_redis_username'
-# configuration for prometheus metrics web server
+# configuration for prometheus metrics web server on /metrics
# launched with sidekiq
-PROMETHEUS_EXPORTER_BIND="0.0.0.0"
+# adjust according to your prometheus probe, 127.0.0.1 or your local/admin net address
+# it's advised to avoid 0.0.0.0 or if you do, please configure ACL elsewhere (webserver, reverse proxy, ...)
+PROMETHEUS_EXPORTER_BIND="127.0.0.1"
PROMETHEUS_EXPORTER_PORT="9394"
PROMETHEUS_EXPORTER_ENABLED="disabled"
@@ -269,3 +274,14 @@ CRON_JOBS_DISABLED=""
# disable SIDEKIQ_RELIABLE_FETCH
# SKIP_RELIABLE_FETCH="true"
+
+# optional license key for lightgallery
+VITE_LIGHTGALLERY_LICENSE_KEY = ""
+
+# Email used to find the Instructeur who fixes data on production.
+# This email will be visible to users whom dossier had been fixed by our maintenance_tasks
+# By default we use CONTACT_EMAIL, but you can customize it
+MAINTENANCE_INSTRUCTEUR_EMAIL=""
+
+# want to stay on delayed job ? set as 'delayed_job'
+RAILS_QUEUE_ADAPTER="
\ No newline at end of file
diff --git a/config/environment.rb b/config/environment.rb
index cac531577..7df99e89c 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Load the Rails application.
require_relative "application"
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 4932f0bb4..e6fdffbb7 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/integer/time"
require Rails.root.join("app/lib/balancer_delivery_method")
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 64ed28732..cf942cd6c 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/integer/time"
require Rails.root.join("app/lib/balancer_delivery_method")
@@ -62,22 +64,26 @@ Rails.application.configure do
# Use a different cache store in production.
if ENV['REDIS_CACHE_URL'].present?
- redis_options = { url: ENV['REDIS_CACHE_URL'] }
- redis_options[:ssl] = (ENV['REDIS_CACHE_SSL'] == 'enabled')
+ redis_options = {
+ url: ENV['REDIS_CACHE_URL'],
+ connect_timeout: 0.2,
+ error_handler: -> (method:, returning:, exception:) {
+ Sentry.capture_exception exception, level: 'warning',
+ tags: { method: method, returning: returning }
+ }
+ }
+
+ 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
+ config.active_job.queue_adapter = ENV.fetch('RAILS_QUEUE_ADAPTER') { :sidekiq }
# config.active_job.queue_name_prefix = "tps_production"
config.action_mailer.perform_caching = false
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 7880fc7b5..f678a4d9f 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/integer/time"
# The test environment is used exclusively to run your application's
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 82d29a1c9..697dc1102 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -102,6 +102,8 @@ ignore_unused:
- 'activerecord.models.*'
- 'activerecord.attributes.*'
- 'activemodel.attributes.map_filter.*'
+- 'activemodel.attributes.helpscout/form.*'
+- 'activemodel.errors.models.*'
- 'activerecord.errors.*'
- 'errors.messages.blank'
- 'errors.messages.content_type_invalid'
@@ -133,7 +135,7 @@ ignore_unused:
## Ignore these keys completely:
ignore:
-- 'shared.champs.drop_down_list{,.other}' # pluralization "other" false positive
+- 'shared.champs.drop_down_list{,.other,.other_label}' # pluralization "other" false positive
## Sometimes, it isn't possible for i18n-tasks to match the key correctly,
## e.g. in case of a relative key defined in a helper method.
diff --git a/config/initializers/01_application_name.rb b/config/initializers/01_application_name.rb
index 0378fb48d..c80472307 100644
--- a/config/initializers/01_application_name.rb
+++ b/config/initializers/01_application_name.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file is named '01-application-name.rb' to load it before the other
# initializers, and thus make the APPLICATION_ constants available in
# the other initializers.
diff --git a/config/initializers/02_urls.rb b/config/initializers/02_urls.rb
index d6e031dae..8dc3211e7 100644
--- a/config/initializers/02_urls.rb
+++ b/config/initializers/02_urls.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# rubocop:disable DS/ApplicationName
# API URLs
API_ADRESSE_URL = ENV.fetch("API_ADRESSE_URL", "https://api-adresse.data.gouv.fr")
@@ -7,7 +9,7 @@ API_GEO_URL = ENV.fetch("API_GEO_URL", "https://geo.api.gouv.fr")
API_PARTICULIER_URL = ENV.fetch("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api")
API_TCHAP_URL = ENV.fetch("API_TCHAP_URL", "https://matrix.agent.tchap.gouv.fr/_matrix/identity/api/v1")
API_COJO_URL = ENV.fetch("API_COJO_URL", nil)
-API_RNF_URL = ENV.fetch("API_RNF_URL", "https://rnf.dso.numerique-interieur.com")
+API_RNF_URL = ENV.fetch("API_RNF_URL", "https://rnf.apps.app1.numerique-interieur.com")
API_RECHERCHE_ENTREPRISE_URL = ENV.fetch("API_RECHERCHE_ENTREPRISE_URL", "https://recherche-entreprises.api.gouv.fr")
HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2")
SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2")
@@ -37,6 +39,7 @@ CGU_URL = ENV.fetch("CGU_URL", [DOC_URL, "cgu"].join("/"))
MENTIONS_LEGALES_URL = ENV.fetch("MENTIONS_LEGALES_URL", "/mentions-legales")
ACCESSIBILITE_URL = ENV.fetch("ACCESSIBILITE_URL", "/declaration-accessibilite")
ROUTAGE_URL = ENV.fetch("ROUTAGE_URL", [DOC_URL, "/pour-aller-plus-loin/routage"].join("/"))
+ELIGIBILITE_URL = ENV.fetch("ELIGIBILITE_URL", [DOC_URL, "/pour-aller-plus-loin/eligibilite-des-dossiers"].join("/"))
API_DOC_URL = [DOC_URL, "api-graphql"].join("/")
WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].join("/")
WEBHOOK_ALTERNATIVE_DOC_URL = [DOC_URL, "api-graphql", "cas-dusages-exemple-dimplementation", "synchroniser-les-dossiers-modifies-sur-ma-demarche"].join("/")
diff --git a/config/initializers/acsv.rb b/config/initializers/acsv.rb
index d099dbe6a..f0094c9a1 100644
--- a/config/initializers/acsv.rb
+++ b/config/initializers/acsv.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'csv'
# PR : https://github.com/wvengen/ruby-acsv/pull/3
diff --git a/config/initializers/action_view_record_identifier.rb b/config/initializers/action_view_record_identifier.rb
index 43105f861..9315f9525 100644
--- a/config/initializers/action_view_record_identifier.rb
+++ b/config/initializers/action_view_record_identifier.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView::RecordIdentifier
alias original_dom_class dom_class
alias original_record_key_for_dom_id record_key_for_dom_id
diff --git a/config/initializers/active_model_serializer.rb b/config/initializers/active_model_serializer.rb
index d4702c3d5..c9335bec9 100644
--- a/config/initializers/active_model_serializer.rb
+++ b/config/initializers/active_model_serializer.rb
@@ -1 +1,3 @@
+# frozen_string_literal: true
+
ActiveModelSerializers.config.default_includes = '**'
diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb
index 042333ae3..620e3f022 100644
--- a/config/initializers/active_storage.rb
+++ b/config/initializers/active_storage.rb
@@ -1,10 +1,13 @@
+# frozen_string_literal: true
+
Rails.application.config.active_storage.service_urls_expire_in = 1.hour
+Rails.application.config.active_storage.variant_processor = :mini_magick
Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::ImageAnalyzer
Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
ActiveSupport.on_load(:active_storage_blob) do
- include BlobTitreIdentiteWatermarkConcern
+ include BlobImageProcessorConcern
include BlobVirusScannerConcern
include BlobSignedIdConcern
@@ -15,7 +18,7 @@ ActiveSupport.on_load(:active_storage_blob) do
end
ActiveSupport.on_load(:active_storage_attachment) do
- include AttachmentTitreIdentiteWatermarkConcern
+ include AttachmentImageProcessorConcern
include AttachmentVirusScannerConcern
end
diff --git a/config/initializers/administrate.rb b/config/initializers/administrate.rb
index 3cea41da7..51c5437b9 100644
--- a/config/initializers/administrate.rb
+++ b/config/initializers/administrate.rb
@@ -1 +1,3 @@
+# frozen_string_literal: true
+
Administrate::Engine.add_stylesheet('manager.css')
diff --git a/config/initializers/after_party.rb b/config/initializers/after_party.rb
index e9ef95728..337c1f4ee 100644
--- a/config/initializers/after_party.rb
+++ b/config/initializers/after_party.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
AfterParty.setup do |_config|
require "after_party/active_record.rb"
end
diff --git a/config/initializers/agent_connect.rb b/config/initializers/agent_connect.rb
index f1a7af19f..9d886751e 100644
--- a/config/initializers/agent_connect.rb
+++ b/config/initializers/agent_connect.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
if ENV['AGENT_CONNECT_BASE_URL'].present?
discover = OpenIDConnect::Discovery::Provider::Config.discover!("#{ENV.fetch('AGENT_CONNECT_BASE_URL')}/api/v2")
diff --git a/config/initializers/ancestry.rb b/config/initializers/ancestry.rb
index 961215897..a2441167b 100644
--- a/config/initializers/ancestry.rb
+++ b/config/initializers/ancestry.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
# use the newer format
Ancestry.default_ancestry_format = :materialized_path2
diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb
index 89d2efab2..6d56e4390 100644
--- a/config/initializers/application_controller_renderer.rb
+++ b/config/initializers/application_controller_renderer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# ActiveSupport::Reloader.to_prepare do
diff --git a/config/initializers/application_version.rb b/config/initializers/application_version.rb
new file mode 100644
index 000000000..c615edc05
--- /dev/null
+++ b/config/initializers/application_version.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class ApplicationVersion
+ @@current = nil
+
+ # Detect the current release version, which helps Sentry identifying the current release
+ # or can be used as cache key when for some contents susceptible to change between releases.
+ #
+ # The deploy process can write a "version" file at root
+ # containing a string identifying the release, like the sha256 commit used by its release.
+ # It defaults to a random string if the file is not found (so each restart will behave like a new version)
+ def self.current
+ @@current ||= begin
+ version = Rails.root.join('version')
+ version.readable? ? version.read.strip : SecureRandom.hex
+ end
+ @@current.presence
+ end
+end
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index 12057061a..a8dfbeca0 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
diff --git a/config/initializers/attribute_types.rb b/config/initializers/attribute_types.rb
index 153838f86..a43d3b536 100644
--- a/config/initializers/attribute_types.rb
+++ b/config/initializers/attribute_types.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SimpleJsonType < ActiveModel::Type::Value
def cast(value)
return nil if value.blank?
diff --git a/config/initializers/authorized_content_types.rb b/config/initializers/authorized_content_types.rb
index fe5410266..497757957 100644
--- a/config/initializers/authorized_content_types.rb
+++ b/config/initializers/authorized_content_types.rb
@@ -1,15 +1,33 @@
-AUTHORIZED_CONTENT_TYPES = [
- # multimedia
+# frozen_string_literal: true
+
+AUTHORIZED_PDF_TYPES = [
+ 'application/pdf', # text x 4628654
+ 'application/x-pdf', # text x 30
+ 'image/pdf', # text x 23
+ 'text/pdf' # text x 12
+]
+
+AUTHORIZED_IMAGE_TYPES = [
'image/jpeg', # multimedia x 1467465
'image/png', # multimedia x 126662
'image/tiff', # multimedia x 3985
'image/bmp', # multimedia x 3656
- 'video/mp4', # multimedia x 2075
'image/webp', # multimedia x 529
- 'video/quicktime', # multimedia x 486
'image/gif', # multimedia x 463
+ 'image/vnd.dwg' # multimedia x 137 auto desk
+]
+
+RARE_IMAGE_TYPES = [
+ 'image/tiff' # multimedia x 3985
+]
+
+PROCESSABLE_TYPES = AUTHORIZED_IMAGE_TYPES + AUTHORIZED_PDF_TYPES
+
+AUTHORIZED_CONTENT_TYPES = PROCESSABLE_TYPES + [
+ # multimedia
+ 'video/mp4', # multimedia x 2075
+ 'video/quicktime', # multimedia x 486
'video/3gpp', # multimedia x 216
- 'image/vnd.dwg', # multimedia x 137 auto desk
'audio/mpeg', # multimedia x 26
'video/x-ms-wm', # multimedia x 15 video microsoft ?
'audio/mp4', # audio .mp4, .m4a
@@ -45,7 +63,6 @@ AUTHORIZED_CONTENT_TYPES = [
'text/xml', # program x 10
# text / sheet / presentation
- 'application/pdf', # text x 4628654
'application/vnd.ms-excel', # text x 166674
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', # text x 103879
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', # text x 86336
@@ -69,18 +86,15 @@ AUTHORIZED_CONTENT_TYPES = [
'application/vnd.ms-word.document.macroenabled.12', # text x 61
'application/vnd.openxmlformats-officedocument.spreadsheetml.template', # text x 59
'application/vnd.openxmlformats-officedocument.presentationml.slideshow', # text x 32
- 'application/x-pdf', # text x 30
'application/kswps', # inconnu x 26 , text ?
'application/x-iwork-numbers-sffnumbers', # text x 25
'text/rtf', # text x 25
- 'image/pdf', # text x 23
'application/vnd.ms-xpsdocument', # text x 23
'application/vnd.ms-excel.sheet.binary.macroenabled.12', # text x 21
'application/vnd.ms-powerpoint.presentation.macroenabled.12', # text x 15
'application/x-msword', # text x 15
'application/vnd.oasis.opendocument.spreadsheet-template', # text x 14
'application/vnd.oasis.opendocument.text-master', # text x 12
- 'text/pdf', # text x 12
'application/x-abiword', # text x 11
'application/x-iwork-keynote-sffnumbers', # text x 11
'application/x-iwork-keynote-sffkey', # text x 10
diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb
index 33699c309..74f30e887 100644
--- a/config/initializers/backtrace_silencers.rb
+++ b/config/initializers/backtrace_silencers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
diff --git a/config/initializers/chartkick.rb b/config/initializers/chartkick.rb
index 4f44c1a38..75e18c8a2 100644
--- a/config/initializers/chartkick.rb
+++ b/config/initializers/chartkick.rb
@@ -1,6 +1,36 @@
+# frozen_string_literal: true
+
Chartkick.options = {
content_for: :charts_js,
- colors: ["#000091"],
+ colors: ["var(--background-action-high-blue-france)"],
thousands: ' ',
- decimal: ','
+ decimal: ',',
+ default_library_config: {
+ chart: { backgroundColor: 'var(--background-contrast-grey)' },
+ xAxis: {
+ lineColor: 'var(--border-action-high-grey)',
+ labels: { style: { color: "var(--text-default-grey)" } }
+ },
+ yAxis: {
+ gridLineColor: 'var(--border-plain-grey)',
+ lineColor: 'var(--border-action-high-grey)',
+ labels: { style: { color: "var(--text-default-grey)" } }
+ },
+ legend: {
+ itemStyle: {
+ color: "var(--text-default-grey)"
+ }
+ },
+ plotOptions: {
+ pie: {
+ dataLabels: {
+ color: "var(--text-default-grey)",
+ enabled: true, format: '{point.name} : {point.percentage: .1f}%',
+ style: {
+ textOutline: 'none'
+ }
+ }
+ }
+ }
+ }
}
diff --git a/config/initializers/contacts.rb b/config/initializers/contacts.rb
index af4271630..49931371c 100644
--- a/config/initializers/contacts.rb
+++ b/config/initializers/contacts.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# rubocop:disable DS/ApplicationName
# todo: will be externally configurable
if !defined?(CONTACT_EMAIL)
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index d213d1dfc..21a0a176c 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy
@@ -7,24 +9,21 @@
Rails.application.config.content_security_policy do |policy|
images_whitelist = ["*.openstreetmap.org", "*.cloud.ovh.net", "*"]
images_whitelist << URI(DS_PROXY_URL).host if DS_PROXY_URL.present?
- images_whitelist << URI(MATOMO_IFRAME_URL).host if MATOMO_IFRAME_URL.present?
policy.img_src(:self, :data, :blob, *images_whitelist)
# Javascript: allow us, SendInBlue and Matomo.
# We need unsafe_inline because miniprofiler and us have some inline buttons :(
- scripts_whitelist = ["*.crisp.chat", "crisp.chat", "cdn.jsdelivr.net", "maxcdn.bootstrapcdn.com", "code.jquery.com", "unpkg.com"]
- scripts_whitelist << URI(MATOMO_IFRAME_URL).host if MATOMO_IFRAME_URL.present?
+ scripts_whitelist = ["*.crisp.chat", "crisp.chat", "cdn.jsdelivr.net", "maxcdn.bootstrapcdn.com", "code.jquery.com", "unpkg.com", "*.dgnum.eu"]
policy.script_src(:self, :unsafe_eval, :unsafe_inline, :blob, *scripts_whitelist)
# CSS: We have a lot of inline style, and some |