+
+
+ );
+ }
+ return null;
+}
+
+TypeDeChampDropDownOther.propTypes = {
+ isVisible: PropTypes.bool,
+ handler: PropTypes.object
+};
+
+export default TypeDeChampDropDownOther;
diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChampDropDownSecondary.jsx b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampDropDownSecondary.jsx
new file mode 100644
index 000000000..6f1ee62f8
--- /dev/null
+++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChampDropDownSecondary.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export default function TypeDeChampDropDownSecondary({
+ isVisible,
+ libelleHandler,
+ descriptionHandler
+}) {
+ if (isVisible) {
+ return (
+
+
+
+
+
+
+ );
+ }
+ return null;
+}
+
+TypeDeChampDropDownSecondary.propTypes = {
+ isVisible: PropTypes.bool,
+ libelleHandler: PropTypes.object,
+ descriptionHandler: PropTypes.object
+};
diff --git a/app/javascript/new_design/champs/drop-down-list.js b/app/javascript/new_design/champs/drop-down-list.js
new file mode 100644
index 000000000..874946ae2
--- /dev/null
+++ b/app/javascript/new_design/champs/drop-down-list.js
@@ -0,0 +1,20 @@
+import { delegate, show, hide } from '@utils';
+
+delegate(
+ 'change',
+ '.editable-champ-drop_down_list select, .editable-champ-drop_down_list input[type="radio"]',
+ (event) => {
+ const parent = event.target.closest('.editable-champ-drop_down_list');
+ const inputGroup = parent?.querySelector('.drop_down_other');
+ if (inputGroup) {
+ const input = inputGroup.querySelector('input');
+ if (event.target.value === '__other__') {
+ show(inputGroup);
+ input.disabled = false;
+ } else {
+ hide(inputGroup);
+ input.disabled = true;
+ }
+ }
+ }
+);
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index bfaa7cda4..f6a7fb267 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -27,6 +27,7 @@ import '../new_design/dossiers/auto-upload';
import '../new_design/champs/carte';
import '../new_design/champs/linked-drop-down-list';
import '../new_design/champs/repetition';
+import '../new_design/champs/drop-down-list';
import {
toggleCondidentielExplanation,
diff --git a/app/jobs/cron/discarded_dossiers_deletion_job.rb b/app/jobs/cron/discarded_dossiers_deletion_job.rb
index 45dd82fe6..9db9623bf 100644
--- a/app/jobs/cron/discarded_dossiers_deletion_job.rb
+++ b/app/jobs/cron/discarded_dossiers_deletion_job.rb
@@ -1,16 +1,7 @@
class Cron::DiscardedDossiersDeletionJob < Cron::CronJob
self.schedule_expression = "every day at 2 am"
- def perform(*args)
- DossierOperationLog.where(dossier: Dossier.discarded_en_construction_expired)
- .where.not(operation: DossierOperationLog.operations.fetch(:supprimer))
- .destroy_all
- DossierOperationLog.where(dossier: Dossier.discarded_termine_expired)
- .where.not(operation: DossierOperationLog.operations.fetch(:supprimer))
- .destroy_all
-
- Dossier.discarded_brouillon_expired.destroy_all
- Dossier.discarded_en_construction_expired.destroy_all
- Dossier.discarded_termine_expired.destroy_all
+ def perform
+ Dossier.purge_discarded
end
end
diff --git a/app/jobs/cron/purge_stale_transfers_job.rb b/app/jobs/cron/purge_stale_transfers_job.rb
index c59609506..bf54c1c81 100644
--- a/app/jobs/cron/purge_stale_transfers_job.rb
+++ b/app/jobs/cron/purge_stale_transfers_job.rb
@@ -2,6 +2,6 @@ class Cron::PurgeStaleTransfersJob < Cron::CronJob
self.schedule_expression = "every day at midnight"
def perform
- DossierTransfer.stale.destroy_all
+ DossierTransfer.destroy_stale
end
end
diff --git a/app/jobs/dossier_rebase_job.rb b/app/jobs/dossier_rebase_job.rb
new file mode 100644
index 000000000..916d9d1e1
--- /dev/null
+++ b/app/jobs/dossier_rebase_job.rb
@@ -0,0 +1,8 @@
+class DossierRebaseJob < ApplicationJob
+ # If by the time the job runs the Dossier has been deleted, ignore the rebase
+ discard_on ActiveRecord::RecordNotFound
+
+ def perform(dossier)
+ dossier.rebase!
+ end
+end
diff --git a/app/models/avis.rb b/app/models/avis.rb
index ff45b0fe2..bd23547cd 100644
--- a/app/models/avis.rb
+++ b/app/models/avis.rb
@@ -49,6 +49,7 @@ class Avis < ApplicationRecord
scope :for_dossier, -> (dossier_id) { where(dossier_id: dossier_id) }
scope :by_latest, -> { order(updated_at: :desc) }
scope :updated_since?, -> (date) { where('avis.updated_at > ?', date) }
+ scope :discarded_termine_expired, -> { unscope(:joins).where(dossier: Dossier.discarded_termine_expired) }
# The form allows subtmitting avis requests to several emails at once,
# hence this virtual attribute.
diff --git a/app/models/champ.rb b/app/models/champ.rb
index 61930b3ab..552d0676d 100644
--- a/app/models/champ.rb
+++ b/app/models/champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
@@ -37,9 +38,12 @@ class Champ < ApplicationRecord
:mandatory?,
:description,
:drop_down_list_options,
+ :drop_down_other,
:drop_down_list_options?,
:drop_down_list_disabled_options,
:drop_down_list_enabled_non_empty_options,
+ :drop_down_secondary_libelle,
+ :drop_down_secondary_description,
:exclude_from_export?,
:exclude_from_view?,
:repetition?,
diff --git a/app/models/champs/address_champ.rb b/app/models/champs/address_champ.rb
index 3e7870b50..c6298b99a 100644
--- a/app/models/champs/address_champ.rb
+++ b/app/models/champs/address_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/annuaire_education_champ.rb b/app/models/champs/annuaire_education_champ.rb
index 22141ca17..727a5a4bf 100644
--- a/app/models/champs/annuaire_education_champ.rb
+++ b/app/models/champs/annuaire_education_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/carte_champ.rb b/app/models/champs/carte_champ.rb
index 1e91e529e..7e3795925 100644
--- a/app/models/champs/carte_champ.rb
+++ b/app/models/champs/carte_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/checkbox_champ.rb b/app/models/champs/checkbox_champ.rb
index 82f009577..a500abb59 100644
--- a/app/models/champs/checkbox_champ.rb
+++ b/app/models/champs/checkbox_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/civilite_champ.rb b/app/models/champs/civilite_champ.rb
index ace32a968..cf5316ea0 100644
--- a/app/models/champs/civilite_champ.rb
+++ b/app/models/champs/civilite_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/commune_champ.rb b/app/models/champs/commune_champ.rb
index 349221cf1..4477da8e3 100644
--- a/app/models/champs/commune_champ.rb
+++ b/app/models/champs/commune_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/date_champ.rb b/app/models/champs/date_champ.rb
index 2e09bd29d..db0644ed7 100644
--- a/app/models/champs/date_champ.rb
+++ b/app/models/champs/date_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/datetime_champ.rb b/app/models/champs/datetime_champ.rb
index f7f5bd496..8899811b1 100644
--- a/app/models/champs/datetime_champ.rb
+++ b/app/models/champs/datetime_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/decimal_number_champ.rb b/app/models/champs/decimal_number_champ.rb
index f574c07ca..1bc48db23 100644
--- a/app/models/champs/decimal_number_champ.rb
+++ b/app/models/champs/decimal_number_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/departement_champ.rb b/app/models/champs/departement_champ.rb
index ac5c89633..b9b68db43 100644
--- a/app/models/champs/departement_champ.rb
+++ b/app/models/champs/departement_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/dossier_link_champ.rb b/app/models/champs/dossier_link_champ.rb
index 866f70197..55c8763e9 100644
--- a/app/models/champs/dossier_link_champ.rb
+++ b/app/models/champs/dossier_link_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/drop_down_list_champ.rb b/app/models/champs/drop_down_list_champ.rb
index 92f52e3f1..248eb443f 100644
--- a/app/models/champs/drop_down_list_champ.rb
+++ b/app/models/champs/drop_down_list_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
@@ -20,6 +21,7 @@
#
class Champs::DropDownListChamp < Champ
THRESHOLD_NB_OPTIONS_AS_RADIO = 5
+ OTHER = '__other__'
def render_as_radios?
enabled_non_empty_options.size <= THRESHOLD_NB_OPTIONS_AS_RADIO
@@ -30,7 +32,15 @@ class Champs::DropDownListChamp < Champ
end
def options
- drop_down_list_options
+ if drop_down_other?
+ drop_down_list_options + [["Autre", OTHER]]
+ else
+ drop_down_list_options
+ end
+ end
+
+ def selected
+ other_value_present? ? OTHER : value
end
def disabled_options
@@ -40,4 +50,26 @@ class Champs::DropDownListChamp < Champ
def enabled_non_empty_options
drop_down_list_enabled_non_empty_options
end
+
+ def other_value_present?
+ drop_down_other? && value.present? && drop_down_list_options.exclude?(value)
+ end
+
+ def drop_down_other?
+ drop_down_other
+ end
+
+ def value=(value)
+ if value != OTHER
+ write_attribute(:value, value)
+ end
+ end
+
+ def value_other=(value)
+ write_attribute(:value, value)
+ end
+
+ def value_other
+ other_value_present? ? value : ""
+ end
end
diff --git a/app/models/champs/email_champ.rb b/app/models/champs/email_champ.rb
index c8a1949b0..acdd6568c 100644
--- a/app/models/champs/email_champ.rb
+++ b/app/models/champs/email_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/engagement_champ.rb b/app/models/champs/engagement_champ.rb
index 986623136..bdd6ff0a6 100644
--- a/app/models/champs/engagement_champ.rb
+++ b/app/models/champs/engagement_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/explication_champ.rb b/app/models/champs/explication_champ.rb
index d20108393..bac3d0aff 100644
--- a/app/models/champs/explication_champ.rb
+++ b/app/models/champs/explication_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/header_section_champ.rb b/app/models/champs/header_section_champ.rb
index 0ff9c7659..2cc24c888 100644
--- a/app/models/champs/header_section_champ.rb
+++ b/app/models/champs/header_section_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/iban_champ.rb b/app/models/champs/iban_champ.rb
index 71ba23281..df94eac66 100644
--- a/app/models/champs/iban_champ.rb
+++ b/app/models/champs/iban_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/integer_number_champ.rb b/app/models/champs/integer_number_champ.rb
index 8272800e1..5187db3d9 100644
--- a/app/models/champs/integer_number_champ.rb
+++ b/app/models/champs/integer_number_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/linked_drop_down_list_champ.rb b/app/models/champs/linked_drop_down_list_champ.rb
index f7022d727..e013d1c24 100644
--- a/app/models/champs/linked_drop_down_list_champ.rb
+++ b/app/models/champs/linked_drop_down_list_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/multiple_drop_down_list_champ.rb b/app/models/champs/multiple_drop_down_list_champ.rb
index 87429b0ee..899dd0159 100644
--- a/app/models/champs/multiple_drop_down_list_champ.rb
+++ b/app/models/champs/multiple_drop_down_list_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/number_champ.rb b/app/models/champs/number_champ.rb
index fa2ec9b47..d43a6d3df 100644
--- a/app/models/champs/number_champ.rb
+++ b/app/models/champs/number_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/pays_champ.rb b/app/models/champs/pays_champ.rb
index 634391a31..2eb3eee76 100644
--- a/app/models/champs/pays_champ.rb
+++ b/app/models/champs/pays_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/phone_champ.rb b/app/models/champs/phone_champ.rb
index 15002fe60..baa032d5b 100644
--- a/app/models/champs/phone_champ.rb
+++ b/app/models/champs/phone_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/piece_justificative_champ.rb b/app/models/champs/piece_justificative_champ.rb
index 245750c01..5076f963a 100644
--- a/app/models/champs/piece_justificative_champ.rb
+++ b/app/models/champs/piece_justificative_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/region_champ.rb b/app/models/champs/region_champ.rb
index 255e4abb2..5fcbe8934 100644
--- a/app/models/champs/region_champ.rb
+++ b/app/models/champs/region_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb
index b07fd0958..75451686b 100644
--- a/app/models/champs/repetition_champ.rb
+++ b/app/models/champs/repetition_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/siret_champ.rb b/app/models/champs/siret_champ.rb
index b9b5b27b5..8b7347235 100644
--- a/app/models/champs/siret_champ.rb
+++ b/app/models/champs/siret_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/text_champ.rb b/app/models/champs/text_champ.rb
index 1376cce48..9c48ba036 100644
--- a/app/models/champs/text_champ.rb
+++ b/app/models/champs/text_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/textarea_champ.rb b/app/models/champs/textarea_champ.rb
index 028950a5f..39570d81a 100644
--- a/app/models/champs/textarea_champ.rb
+++ b/app/models/champs/textarea_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/titre_identite_champ.rb b/app/models/champs/titre_identite_champ.rb
index 982ae7ae7..1d74c3245 100644
--- a/app/models/champs/titre_identite_champ.rb
+++ b/app/models/champs/titre_identite_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/champs/yes_no_champ.rb b/app/models/champs/yes_no_champ.rb
index fc8346cfb..293beaba2 100644
--- a/app/models/champs/yes_no_champ.rb
+++ b/app/models/champs/yes_no_champ.rb
@@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
+# rebased_at :datetime
# row :integer
# type :string
# value :string
diff --git a/app/models/concerns/dossier_rebase_concern.rb b/app/models/concerns/dossier_rebase_concern.rb
new file mode 100644
index 000000000..00e9aacc2
--- /dev/null
+++ b/app/models/concerns/dossier_rebase_concern.rb
@@ -0,0 +1,121 @@
+module DossierRebaseConcern
+ extend ActiveSupport::Concern
+
+ def rebase!
+ if brouillon? && revision != procedure.published_revision
+ transaction do
+ rebase
+ end
+ end
+ end
+
+ private
+
+ def rebase
+ attachments_to_purge = []
+ geo_areas_to_delete = []
+ changes_by_type_de_champ = revision.compare(procedure.published_revision).group_by { |change| change[:stable_id] }
+
+ changes_by_type_de_champ.each do |stable_id, changes|
+ type_de_champ = find_type_de_champ_by_stable_id(stable_id)
+ published_type_de_champ = find_type_de_champ_by_stable_id(stable_id, published: true)
+
+ changes.each do |change|
+ case change[:op]
+ when :add
+ add_new_champs_for_revision(published_type_de_champ)
+ when :remove
+ delete_champs_for_revision(type_de_champ)
+ end
+ end
+ end
+
+ flattened_all_champs.each do |champ|
+ changes_by_stable_id = (changes_by_type_de_champ[champ.stable_id] || [])
+ .filter { |change| change[:op] == :update }
+
+ update_champ_for_revision(champ) do |update|
+ changes_by_stable_id.each do |change|
+ case change[:attribute]
+ when :type_champ
+ update[:type] = "Champs::#{change[:to].classify}Champ"
+ update[:value] = nil
+ update[:external_id] = nil
+ update[:data] = nil
+ geo_areas_to_delete += champ.geo_areas
+ if champ.piece_justificative_file.attached?
+ attachments_to_purge << champ.piece_justificative_file
+ end
+ when :drop_down_options
+ update[:value] = nil
+ when :carte_layers
+ geo_areas_to_delete += champ.geo_areas
+ end
+ update[:rebased_at] = Time.zone.now
+ end
+ end
+ end
+
+ self.update_column(:revision_id, procedure.published_revision_id)
+ attachments_to_purge.each(&:purge_later)
+ geo_areas_to_delete.each(&:destroy)
+ end
+
+ def add_new_champs_for_revision(published_type_de_champ)
+ if published_type_de_champ.parent
+ find_champs_by_stable_id(published_type_de_champ.parent.stable_id).each do |champ_repetition|
+ champ_repetition.rows.size.times do |row|
+ champ = published_type_de_champ.champ.build(row: row)
+ champ_repetition.champs << champ
+ end
+ end
+ else
+ champ = published_type_de_champ.build_champ
+ self.champs << champ
+ end
+ end
+
+ def update_champ_for_revision(champ)
+ published_type_de_champ = find_type_de_champ_by_stable_id(champ.stable_id, published: true)
+ return if !published_type_de_champ
+
+ update = {}
+
+ yield update
+
+ if champ.type_de_champ != published_type_de_champ
+ update[:type_de_champ_id] = published_type_de_champ.id
+ end
+
+ if update.present?
+ champ.update_columns(update)
+ end
+ end
+
+ def delete_champs_for_revision(published_type_de_champ)
+ Champ.where(id: find_champs_by_stable_id(published_type_de_champ.stable_id).map(&:id))
+ .destroy_all
+ end
+
+ def flattened_all_types_de_champ(published: false)
+ revision = published ? procedure.published_revision : self.revision
+ types_de_champ = revision.types_de_champ + revision.types_de_champ_private
+ (types_de_champ + types_de_champ.filter(&:repetition?).flat_map(&:types_de_champ))
+ .index_by(&:stable_id)
+ end
+
+ def flattened_all_champs
+ all_champs = (champs + champs_private)
+ all_champs + all_champs.filter(&:repetition?).flat_map(&:champs)
+ end
+
+ def find_type_de_champ_by_stable_id(stable_id, published: false)
+ flattened_all_types_de_champ(published: published)[stable_id]
+ end
+
+ def find_champs_by_stable_id(stable_id)
+ flattened_all_champs.filter do |champ|
+ champ.stable_id == stable_id
+ end
+ end
+end
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index 6a35c42d9..8f3f2bb13 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -36,6 +36,7 @@
class Dossier < ApplicationRecord
self.ignored_columns = [:en_construction_conservation_extension]
include DossierFilteringConcern
+ include DossierRebaseConcern
include Discard::Model
self.discard_column = :hidden_at
@@ -677,6 +678,7 @@ class Dossier < ApplicationRecord
end
end
+ update!(dossier_transfer_id: nil)
discard!
end
@@ -958,6 +960,21 @@ class Dossier < ApplicationRecord
user&.locale || I18n.default_locale
end
+ def self.purge_discarded
+ discarded_brouillon_expired.destroy_all
+
+ transaction do
+ DossierOperationLog.discarded_en_construction_expired.destroy_all
+ discarded_en_construction_expired.destroy_all
+ end
+
+ transaction do
+ DossierOperationLog.discarded_termine_expired.destroy_all
+ Avis.discarded_termine_expired.destroy_all
+ discarded_termine_expired.destroy_all
+ end
+ end
+
private
def defaut_groupe_instructeur?
diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb
index 2144ebc57..9ecd29d3b 100644
--- a/app/models/dossier_operation_log.rb
+++ b/app/models/dossier_operation_log.rb
@@ -36,6 +36,10 @@ class DossierOperationLog < ApplicationRecord
belongs_to :dossier, optional: true
belongs_to :bill_signature, optional: true
+ scope :not_deletion, -> { where.not(operation: operations.fetch(:supprimer)) }
+ scope :discarded_en_construction_expired, -> { where(dossier: Dossier.discarded_en_construction_expired).not_deletion }
+ scope :discarded_termine_expired, -> { where(dossier: Dossier.discarded_termine_expired).not_deletion }
+
def self.create_and_serialize(params)
dossier = params.fetch(:dossier)
diff --git a/app/models/dossier_transfer.rb b/app/models/dossier_transfer.rb
index 731a0ab18..d2a6a3ac8 100644
--- a/app/models/dossier_transfer.rb
+++ b/app/models/dossier_transfer.rb
@@ -37,7 +37,7 @@ class DossierTransfer < ApplicationRecord
}
end)
transfer.dossiers.update_all(user_id: current_user.id)
- transfer.destroy
+ transfer.destroy_and_nullify
end
end
@@ -45,6 +45,22 @@ class DossierTransfer < ApplicationRecord
User.find_by(email: email)&.locale || I18n.default_locale
end
+ def destroy_and_nullify
+ transaction do
+ # Rails cascading is not working with default scopes. Doing nullify cascade manually.
+ dossiers.with_discarded.update_all(dossier_transfer_id: nil)
+ destroy
+ end
+ end
+
+ def self.destroy_stale
+ transaction do
+ # Rails cascading is not working with default scopes. Doing nullify cascade manually.
+ Dossier.with_discarded.where(transfer: stale).update_all(dossier_transfer_id: nil)
+ stale.destroy_all
+ end
+ end
+
private
def send_notification
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index df9aba673..ebd09ad1b 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -714,6 +714,9 @@ class Procedure < ApplicationRecord
def publish_revision!
update!(draft_revision: create_new_revision, published_revision: draft_revision)
published_revision.touch(:published_at)
+ dossiers.state_brouillon.find_each do |dossier|
+ DossierRebaseJob.perform_later(dossier)
+ end
end
def cnaf_enabled?
diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb
index 75513d724..25865fc96 100644
--- a/app/models/procedure_revision.rb
+++ b/app/models/procedure_revision.rb
@@ -134,11 +134,11 @@ class ProcedureRevision < ApplicationRecord
to_sids = to_h.keys
removed = (from_sids - to_sids).map do |sid|
- { op: :remove, label: from_h[sid].libelle, private: from_h[sid].private?, position: from_sids.index(sid) }
+ { op: :remove, label: from_h[sid].libelle, private: from_h[sid].private?, position: from_sids.index(sid), stable_id: sid }
end
added = (to_sids - from_sids).map do |sid|
- { op: :add, label: to_h[sid].libelle, private: to_h[sid].private?, position: to_sids.index(sid) }
+ { op: :add, label: to_h[sid].libelle, private: to_h[sid].private?, position: to_sids.index(sid), stable_id: sid }
end
kept = from_sids.intersection(to_sids)
@@ -147,7 +147,7 @@ class ProcedureRevision < ApplicationRecord
.map { |sid| [sid, from_sids.index(sid), to_sids.index(sid)] }
.filter { |_, from_index, to_index| from_index != to_index }
.map do |sid, from_index, to_index|
- { op: :move, label: from_h[sid].libelle, private: from_h[sid].private?, from: from_index, to: to_index, position: to_index }
+ { op: :move, label: from_h[sid].libelle, private: from_h[sid].private?, from: from_index, to: to_index, position: to_index, stable_id: sid }
end
changed = kept
@@ -172,7 +172,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.type_champ,
- to: to_type_de_champ.type_champ
+ to: to_type_de_champ.type_champ,
+ stable_id: from_type_de_champ.stable_id
}
end
if from_type_de_champ.libelle != to_type_de_champ.libelle
@@ -182,7 +183,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.libelle,
- to: to_type_de_champ.libelle
+ to: to_type_de_champ.libelle,
+ stable_id: from_type_de_champ.stable_id
}
end
if from_type_de_champ.description != to_type_de_champ.description
@@ -192,7 +194,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.description,
- to: to_type_de_champ.description
+ to: to_type_de_champ.description,
+ stable_id: from_type_de_champ.stable_id
}
end
if from_type_de_champ.mandatory? != to_type_de_champ.mandatory?
@@ -202,7 +205,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.mandatory?,
- to: to_type_de_champ.mandatory?
+ to: to_type_de_champ.mandatory?,
+ stable_id: from_type_de_champ.stable_id
}
end
if to_type_de_champ.drop_down_list?
@@ -213,7 +217,41 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.drop_down_list_options,
- to: to_type_de_champ.drop_down_list_options
+ to: to_type_de_champ.drop_down_list_options,
+ stable_id: from_type_de_champ.stable_id
+ }
+ 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
+ changes << {
+ op: :update,
+ attribute: :drop_down_secondary_libelle,
+ label: from_type_de_champ.libelle,
+ private: from_type_de_champ.private?,
+ from: from_type_de_champ.drop_down_secondary_libelle,
+ to: to_type_de_champ.drop_down_secondary_libelle
+ }
+ end
+ if from_type_de_champ.drop_down_secondary_description != to_type_de_champ.drop_down_secondary_description
+ changes << {
+ op: :update,
+ attribute: :drop_down_secondary_description,
+ label: from_type_de_champ.libelle,
+ private: from_type_de_champ.private?,
+ from: from_type_de_champ.drop_down_secondary_description,
+ to: to_type_de_champ.drop_down_secondary_description
+ }
+ end
+ end
+ if from_type_de_champ.drop_down_other != to_type_de_champ.drop_down_other
+ changes << {
+ op: :update,
+ attribute: :drop_down_other,
+ label: from_type_de_champ.libelle,
+ private: from_type_de_champ.private?,
+ from: from_type_de_champ.drop_down_other,
+ to: to_type_de_champ.drop_down_other,
+ stable_id: from_type_de_champ.stable_id
}
end
elsif to_type_de_champ.carte?
@@ -224,7 +262,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.carte_optional_layers,
- to: to_type_de_champ.carte_optional_layers
+ to: to_type_de_champ.carte_optional_layers,
+ stable_id: from_type_de_champ.stable_id
}
end
elsif to_type_de_champ.piece_justificative?
@@ -235,7 +274,8 @@ class ProcedureRevision < ApplicationRecord
label: from_type_de_champ.libelle,
private: from_type_de_champ.private?,
from: from_type_de_champ.piece_justificative_template_filename,
- to: to_type_de_champ.piece_justificative_template_filename
+ to: to_type_de_champ.piece_justificative_template_filename,
+ stable_id: from_type_de_champ.stable_id
}
end
elsif to_type_de_champ.repetition?
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index dcf5b096d..a415595ec 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -58,7 +58,7 @@ class TypeDeChamp < ApplicationRecord
belongs_to :parent, class_name: 'TypeDeChamp', optional: true
has_many :types_de_champ, -> { ordered }, foreign_key: :parent_id, class_name: 'TypeDeChamp', inverse_of: :parent, dependent: :destroy
- store_accessor :options, :cadastres, :old_pj, :drop_down_options, :skip_pj_validation, :skip_content_type_pj_validation
+ store_accessor :options, :cadastres, :old_pj, :drop_down_options, :skip_pj_validation, :skip_content_type_pj_validation, :drop_down_secondary_libelle, :drop_down_secondary_description, :drop_down_other
has_many :revision_types_de_champ, class_name: 'ProcedureRevisionTypeDeChamp', dependent: :destroy, inverse_of: :type_de_champ
has_many :revisions, through: :revision_types_de_champ
@@ -331,9 +331,12 @@ class TypeDeChamp < ApplicationRecord
],
methods: [
:drop_down_list_value,
+ :drop_down_other,
:piece_justificative_template_filename,
:piece_justificative_template_url,
- :editable_options
+ :editable_options,
+ :drop_down_secondary_libelle,
+ :drop_down_secondary_description
]
}
TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE
diff --git a/app/models/user.rb b/app/models/user.rb
index 32219d6b7..f2143203a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -182,17 +182,16 @@ class User < ApplicationRecord
raise "Cannot delete this user because they are also instructeur, expert or administrateur"
end
- Invite.where(dossier: dossiers.with_discarded).destroy_all
- dossiers.state_en_construction.each do |dossier|
- dossier.discard_and_keep_track!(administration, :user_removed)
+ transaction do
+ Invite.where(dossier: dossiers.with_discarded).destroy_all
+ dossiers.state_en_construction.each do |dossier|
+ dossier.discard_and_keep_track!(administration, :user_removed)
+ end
+ DossierOperationLog.where(dossier: dossiers.with_discarded.discarded).not_deletion.destroy_all
+ dossiers.with_discarded.discarded.destroy_all
+ dossiers.update_all(deleted_user_email_never_send: email, user_id: nil, dossier_transfer_id: nil)
+ destroy!
end
- DossierOperationLog
- .where(dossier: dossiers.with_discarded.discarded)
- .where.not(operation: DossierOperationLog.operations.fetch(:supprimer))
- .destroy_all
- dossiers.with_discarded.discarded.destroy_all
- dossiers.update_all(deleted_user_email_never_send: email, user_id: nil)
- destroy!
end
private
diff --git a/app/services/zxcvbn_service.rb b/app/services/zxcvbn_service.rb
index 0b11d4135..a6097ccc0 100644
--- a/app/services/zxcvbn_service.rb
+++ b/app/services/zxcvbn_service.rb
@@ -1,4 +1,32 @@
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(File.read(Rails.root.join("config", "initializers", "zxcvbn_dictionnaries.yaml")))
+
+ tester = Zxcvbn::Tester.new
+ tester.add_word_lists(dictionaries)
+ tester
+ end
+ end
+
def initialize(password)
@password = password
end
@@ -18,6 +46,6 @@ class ZxcvbnService
private
def compute_zxcvbn
- Zxcvbn.test(@password, [], ZXCVBN_DICTIONNARIES)
+ self.class.tester.test(@password)
end
end
diff --git a/app/views/new_administrateur/procedures/_revision_changes.html.haml b/app/views/new_administrateur/procedures/_revision_changes.html.haml
index 2769bbd7c..1c7e936c5 100644
--- a/app/views/new_administrateur/procedures/_revision_changes.html.haml
+++ b/app/views/new_administrateur/procedures/_revision_changes.html.haml
@@ -14,6 +14,10 @@
%li.mb-1= t("update_type_champ#{postfix}", label: change[:label], to: t("activerecord.attributes.type_de_champ.type_champs.#{change[:to]}"), scope: [:new_administrateur, :revision_changes])
- when :description
%li.mb-1= t("update_description#{postfix}", label: change[:label], to: change[:to], scope: [:new_administrateur, :revision_changes])
+ - when :drop_down_secondary_libelle
+ %li.mb-1= t("update_drop_down_secondary_libelle#{postfix}", label: change[:label], to: change[:to], scope: [:new_administrateur, :revision_changes])
+ - when :drop_down_secondary_description
+ %li.mb-1= t("update_drop_down_secondary_description#{postfix}", label: change[:label], to: change[:to], scope: [:new_administrateur, :revision_changes])
- when :mandatory
- if change[:from] == false
-# i18n-tasks-use t('new_administrateur.revision_changes.update_mandatory.enabled')
@@ -37,6 +41,11 @@
%li= t(:add_option, scope: [:new_administrateur, :revision_changes], items: added.map{ |term| "« #{term.strip} »" }.join(", "))
- if removed.present?
%li= t(:remove_option, scope: [:new_administrateur, :revision_changes], items: removed.map{ |term| "« #{term.strip} »" }.join(", "))
+ - when :drop_down_other
+ - if change[:from] == false
+ %li.mb-1= t("new_administrateur.revision_changes.update_drop_down_other#{postfix}.enabled", label: change[:label])
+ - else
+ %li.mb-1= t("new_administrateur.revision_changes.update_drop_down_other#{postfix}.disabled", label: change[:label])
- when :carte_layers
- added = change[:to].sort - change[:from].sort
- removed = change[:from].sort - change[:to].sort
diff --git a/app/views/shared/dossiers/_drop_down_other_input.html.haml b/app/views/shared/dossiers/_drop_down_other_input.html.haml
new file mode 100644
index 000000000..349e8c458
--- /dev/null
+++ b/app/views/shared/dossiers/_drop_down_other_input.html.haml
@@ -0,0 +1,4 @@
+.drop_down_other{ class: champ.other_value_present? ? '' : 'hidden' }
+ .notice
+ %p Veuillez saisir votre autre choix
+ = form.text_field :value_other, maxlength: "200", placeholder: "Saisissez ici", disabled: !champ.other_value_present?
diff --git a/app/views/shared/dossiers/editable_champs/_champ_label_content.html.haml b/app/views/shared/dossiers/editable_champs/_champ_label_content.html.haml
index 7174f7ee5..39902cb87 100644
--- a/app/views/shared/dossiers/editable_champs/_champ_label_content.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_champ_label_content.html.haml
@@ -5,3 +5,7 @@
- if champ.updated_at.present? && seen_at.present?
%span.updated-at{ class: highlight_if_unseen_class(seen_at, champ.updated_at) }
= "modifié le #{try_format_datetime(champ.updated_at)}"
+
+- if champ.rebased_at.present? && champ.rebased_at > (seen_at || champ.updated_at) && current_user.owns_or_invite?(champ.dossier)
+ %span.updated-at.highlighted
+ Le type de ce champ où sa description a été modifiée par l'administration. Vérifier son contenu.
diff --git a/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml b/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml
index 153217cff..ec7f81ee7 100644
--- a/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml
@@ -5,13 +5,18 @@
%label
= form.radio_button :value, option
= option
+
- if !champ.mandatory?
%label.blank-radio
= form.radio_button :value, ''
Non renseigné
+ - if champ.drop_down_other?
+ %label
+ = form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: champ.other_value_present?
+ Autre
- else
- = form.select :value,
- champ.options,
- disabled: champ.disabled_options,
- required: champ.mandatory?
+ = form.select :value, champ.options, selected: champ.selected, required: champ.mandatory?
+
+ - if champ.drop_down_other?
+ = render partial: "shared/dossiers/drop_down_other_input", locals: { form: form, champ: champ }
diff --git a/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml b/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml
index 8f1a154e4..0f88b773e 100644
--- a/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml
@@ -4,10 +4,12 @@
{ required: champ.mandatory? },
{ data: { secondary_options: champ.secondary_options } }
%span
- = form.label :secondary_value, class: 'hidden' do
- Valeur secondaire dépendant de la première
+ = form.label :secondary_value do
+ = champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première"
- if champ.mandatory?
%span.mandatory *
+ - if champ.drop_down_secondary_description.present?
+ .notice= string_to_html(champ.drop_down_secondary_description)
= form.select :secondary_value,
champ.secondary_options[champ.primary_value],
{ required: champ.mandatory? },
diff --git a/config/initializers/zxcvbn.rb b/config/initializers/zxcvbn.rb
deleted file mode 100644
index 416e80192..000000000
--- a/config/initializers/zxcvbn.rb
+++ /dev/null
@@ -1 +0,0 @@
-ZXCVBN_DICTIONNARIES = YAML.safe_load(File.read(Rails.root.join("config", "initializers", "zxcvbn_dictionnaries.yaml")))
diff --git a/config/locales/views/new_administrateur/revision_changes/fr.yml b/config/locales/views/new_administrateur/revision_changes/fr.yml
index 6c15a1ecd..495f1728f 100644
--- a/config/locales/views/new_administrateur/revision_changes/fr.yml
+++ b/config/locales/views/new_administrateur/revision_changes/fr.yml
@@ -9,12 +9,17 @@ fr:
other: Les positions de %{count} champs ont été modifiées
update_libelle: Le libellé du champ « %{label} » a été modifié. Le nouveau libellé est « %{to} »
update_description: La description du champ « %{label} » a été modifiée. La nouvelle description est « %{to} »
+ update_drop_down_secondary_libelle: Le libellé secondaire du champ « %{label} » a été modifié. Le nouveau libellé est « %{to} »
+ update_drop_down_secondary_description: La description secondaire du champ « %{label} » a été modifiée. La nouvelle description est « %{to} »
update_type_champ: Le type du champ « %{label} » a été modifié. Il est maintenant de type « %{to} »
update_mandatory:
enabled: Le champ « %{label} » est maintenant obligatoire
disabled: Le champ « %{label} » n’est plus obligatoire
update_piece_justificative_template: Le modèle de pièce justificative du champ « %{label} » a été modifié
update_drop_down_options: Les options de sélection du champ « %{label} » ont été modifiées
+ update_drop_down_other:
+ enabled: Le champ « %{label} » comporte maintenant un choix « Autre »
+ disabled: Le champ « %{label} » ne comporte plus de choix « Autre »
update_carte_layers: Les référentiels cartographiques du champ « %{label} » ont été modifiés
add_private: L’annotation privée « %{label} » a été ajoutée
remove_private: L’annotation privée « %{label} » a été supprimée
@@ -23,6 +28,8 @@ fr:
other: Les positions de %{count} annotations privées ont été modifiées
update_libelle_private: Le libellé de l’annotation privée « %{label} » a été modifié. Le nouveau libellé est « %{to} »
update_description_private: La description de l’annotation privée « %{label} » a été modifiée. La nouvelle description est « %{to} »
+ update_drop_down_secondary_libelle_private: Le libellé secondaire de l’annotation « %{label} » a été modifié. Le nouveau libellé est « %{to} »
+ update_drop_down_secondary_description_private: La description secondaire de l’annotation « %{label} » a été modifiée. La nouvelle description est « %{to} »
update_type_champ_private: Le type de l’annotation privée « %{label} » a été modifié. Elle est maintenant de type « %{to} »
update_mandatory_private:
enabled: L’annotation privée « %{label} » est maintenant obligatoire
diff --git a/config/puma.rb b/config/puma.rb
index ace31ab20..8b9bb8681 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -36,30 +36,9 @@ if ENV.fetch("RAILS_ENV") == "production"
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
- # process behavior so workers use less memory. If you use this option
- # you need to make sure to reconnect any threads in the `on_worker_boot`
- # block.
+ # process behavior so workers use less memory.
#
preload_app!
-
- # If you are preloading your application and using Active Record, it's
- # recommended that you close any connections to the database before workers
- # are forked to prevent connection leakage.
- #
- before_fork do
- ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
- end
-
- # The code in the `on_worker_boot` will be called if you are using
- # clustered mode by specifying a number of `workers`. After each worker
- # process is booted, this block will be run. If you are using the `preload_app!`
- # option, you will want to use this block to reconnect to any threads
- # or connections that may have been created at application boot, as Ruby
- # cannot share connections between processes.
- #
- on_worker_boot do
- ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
- end
end
# Allow puma to be restarted by `rails restart` command.
diff --git a/db/migrate/20211013131241_add_rebased_at_to_champs.rb b/db/migrate/20211013131241_add_rebased_at_to_champs.rb
new file mode 100644
index 000000000..209b37992
--- /dev/null
+++ b/db/migrate/20211013131241_add_rebased_at_to_champs.rb
@@ -0,0 +1,5 @@
+class AddRebasedAtToChamps < ActiveRecord::Migration[6.1]
+ def change
+ add_column :champs, :rebased_at, :datetime
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b8c245f1c..f5a4cc1ba 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2021_10_20_104237) do
+ActiveRecord::Schema.define(version: 2021_10_20_114237) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -190,6 +190,7 @@ ActiveRecord::Schema.define(version: 2021_10_20_104237) do
t.string "external_id"
t.string "fetch_external_data_exceptions", array: true
t.jsonb "value_json"
+ t.datetime "rebased_at"
t.index ["dossier_id"], name: "index_champs_on_dossier_id"
t.index ["etablissement_id"], name: "index_champs_on_etablissement_id"
t.index ["parent_id"], name: "index_champs_on_parent_id"
diff --git a/spec/controllers/manager/users_controller_spec.rb b/spec/controllers/manager/users_controller_spec.rb
index 22d77bd2c..241e59fa0 100644
--- a/spec/controllers/manager/users_controller_spec.rb
+++ b/spec/controllers/manager/users_controller_spec.rb
@@ -61,7 +61,7 @@ describe Manager::UsersController, type: :controller do
context 'and the old account belongs to an instructeur, expert and administrateur' do
let!(:instructeur) { create(:instructeur, user: user) }
let!(:expert) { create(:expert, user: user) }
- let!(:administrateur) { create(:administrateur, user: user) }
+ let!(:administrateur) { create(:administrateur, user: user, instructeur: instructeur) }
it 'transfers instructeur account' do
subject
diff --git a/spec/controllers/webhook_controller_spec.rb b/spec/controllers/webhook_controller_spec.rb
index b5548e224..c2e2ed89e 100644
--- a/spec/controllers/webhook_controller_spec.rb
+++ b/spec/controllers/webhook_controller_spec.rb
@@ -40,7 +40,7 @@ describe WebhookController, type: :controller do
context 'when there are an associated Instructeur and Administrateur' do
let!(:instructeur) { create(:instructeur, user: user) }
- let!(:admin) { create(:administrateur, user: user) }
+ let!(:admin) { create(:administrateur, user: user, instructeur: instructeur) }
it 'returns a link to the Instructeur profile in the Manager' do
expect(payload).to have_key('html')
diff --git a/spec/factories/administrateur.rb b/spec/factories/administrateur.rb
index b665f9249..479ee824c 100644
--- a/spec/factories/administrateur.rb
+++ b/spec/factories/administrateur.rb
@@ -1,13 +1,18 @@
FactoryBot.define do
sequence(:administrateur_email) { |n| "admin#{n}@admin.com" }
factory :administrateur do
+ user { association :user, email: email, password: password }
+
transient do
email { generate(:administrateur_email) }
password { 'Mon [hien 4im3 {es banane$' }
+ instructeur { build(:instructeur, user: user) }
end
- initialize_with do
- User.create_or_promote_to_administrateur(email, password).administrateur
+ after(:build) do |administrateur, evaluator|
+ if administrateur.user
+ administrateur.user.instructeur = evaluator.instructeur
+ end
end
end
diff --git a/spec/factories/expert.rb b/spec/factories/expert.rb
index 7e1b5863e..13fc41410 100644
--- a/spec/factories/expert.rb
+++ b/spec/factories/expert.rb
@@ -2,13 +2,11 @@ FactoryBot.define do
sequence(:create_expert_email) { |n| "expert#{n}@expert.com" }
factory :expert do
+ user { association :user, email: email, password: password }
+
transient do
email { generate(:expert_email) }
password { 'somethingverycomplated!' }
end
-
- initialize_with do
- User.create_or_promote_to_expert(email, password).expert
- end
end
end
diff --git a/spec/factories/instructeur.rb b/spec/factories/instructeur.rb
index 7b73a5f9c..11f28a5d5 100644
--- a/spec/factories/instructeur.rb
+++ b/spec/factories/instructeur.rb
@@ -2,13 +2,11 @@ FactoryBot.define do
sequence(:instructeur_email) { |n| "inst#{n}@inst.com" }
factory :instructeur do
+ user { association :user, email: email, password: password }
+
transient do
email { generate(:instructeur_email) }
password { 'somethingverycomplated!' }
end
-
- initialize_with do
- User.create_or_promote_to_instructeur(email, password).instructeur
- end
end
end
diff --git a/spec/factories/procedure.rb b/spec/factories/procedure.rb
index 47a85de6a..eb874dfa6 100644
--- a/spec/factories/procedure.rb
+++ b/spec/factories/procedure.rb
@@ -24,7 +24,7 @@ FactoryBot.define do
if evaluator.administrateur
procedure.administrateurs = [evaluator.administrateur]
elsif procedure.administrateurs.empty?
- procedure.administrateurs = [create(:administrateur)]
+ procedure.administrateurs = [build(:administrateur)]
end
procedure.draft_revision = build(:procedure_revision, procedure: procedure)
diff --git a/spec/models/administrateur_spec.rb b/spec/models/administrateur_spec.rb
index a00173c4d..55e6c4d32 100644
--- a/spec/models/administrateur_spec.rb
+++ b/spec/models/administrateur_spec.rb
@@ -1,7 +1,7 @@
describe Administrateur, type: :model do
let(:administration) { create(:administration) }
- describe 'assocations' do
+ describe 'associations' do
it { is_expected.to have_and_belong_to_many(:instructeurs) }
it { is_expected.to have_many(:procedures) }
end
diff --git a/spec/models/champs/phone_champ_spec.rb b/spec/models/champs/phone_champ_spec.rb
index e69356e41..4d1f12f6e 100644
--- a/spec/models/champs/phone_champ_spec.rb
+++ b/spec/models/champs/phone_champ_spec.rb
@@ -1,38 +1,44 @@
describe Champs::PhoneChamp do
+ let(:phone_champ) { build(:champ_phone) }
+
describe '#valid?' do
it do
- expect(build(:champ_phone, value: nil)).to be_valid
- expect(build(:champ_phone, value: "0123456789 0123456789")).to_not be_valid
- expect(build(:champ_phone, value: "01.23.45.67.89 01.23.45.67.89")).to_not be_valid
- expect(build(:champ_phone, value: "3646")).to be_valid
- expect(build(:champ_phone, value: "0123456789")).to be_valid
- expect(build(:champ_phone, value: "01.23.45.67.89")).to be_valid
- expect(build(:champ_phone, value: "0123 45.67.89")).to be_valid
- expect(build(:champ_phone, value: "0033 123-456-789")).to be_valid
- expect(build(:champ_phone, value: "0033 123-456-789")).to be_valid
- expect(build(:champ_phone, value: "0033(0)123456789")).to be_valid
- expect(build(:champ_phone, value: "+33-1.23.45.67.89")).to be_valid
- expect(build(:champ_phone, value: "+33 - 123 456 789")).to be_valid
- expect(build(:champ_phone, value: "+33(0) 123 456 789")).to be_valid
- expect(build(:champ_phone, value: "+33 (0)123 45 67 89")).to be_valid
- expect(build(:champ_phone, value: "+33 (0)1 2345-6789")).to be_valid
- expect(build(:champ_phone, value: "+33(0) - 123456789")).to be_valid
- expect(build(:champ_phone, value: "+1(0) - 123456789")).to be_valid
- expect(build(:champ_phone, value: "+49 2109 87654321")).to be_valid
- expect(build(:champ_phone, value: "012345678")).to be_valid
+ expect(champ_with_value(nil)).to be_valid
+ expect(champ_with_value("0123456789 0123456789")).to_not be_valid
+ expect(champ_with_value("01.23.45.67.89 01.23.45.67.89")).to_not be_valid
+ expect(champ_with_value("3646")).to be_valid
+ expect(champ_with_value("0123456789")).to be_valid
+ expect(champ_with_value("01.23.45.67.89")).to be_valid
+ expect(champ_with_value("0123 45.67.89")).to be_valid
+ expect(champ_with_value("0033 123-456-789")).to be_valid
+ expect(champ_with_value("0033 123-456-789")).to be_valid
+ expect(champ_with_value("0033(0)123456789")).to be_valid
+ expect(champ_with_value("+33-1.23.45.67.89")).to be_valid
+ expect(champ_with_value("+33 - 123 456 789")).to be_valid
+ expect(champ_with_value("+33(0) 123 456 789")).to be_valid
+ expect(champ_with_value("+33 (0)123 45 67 89")).to be_valid
+ expect(champ_with_value("+33 (0)1 2345-6789")).to be_valid
+ expect(champ_with_value("+33(0) - 123456789")).to be_valid
+ expect(champ_with_value("+1(0) - 123456789")).to be_valid
+ expect(champ_with_value("+49 2109 87654321")).to be_valid
+ expect(champ_with_value("012345678")).to be_valid
# polynesian numbers should not return errors in any way
## landline numbers start with 40 or 45
- expect(build(:champ_phone, value: "45187272")).to be_valid
- expect(build(:champ_phone, value: "40 473 500")).to be_valid
- expect(build(:champ_phone, value: "40473500")).to be_valid
- expect(build(:champ_phone, value: "45473500")).to be_valid
+ expect(champ_with_value("45187272")).to be_valid
+ expect(champ_with_value("40 473 500")).to be_valid
+ expect(champ_with_value("40473500")).to be_valid
+ expect(champ_with_value("45473500")).to be_valid
## +689 is the international indicator
- expect(build(:champ_phone, value: "+689 45473500")).to be_valid
- expect(build(:champ_phone, value: "0145473500")).to be_valid
+ expect(champ_with_value("+689 45473500")).to be_valid
+ expect(champ_with_value("0145473500")).to be_valid
## polynesian mobile numbers start with 87, 88, 89
- expect(build(:champ_phone, value: "87473500")).to be_valid
- expect(build(:champ_phone, value: "88473500")).to be_valid
- expect(build(:champ_phone, value: "89473500")).to be_valid
+ expect(champ_with_value("87473500")).to be_valid
+ expect(champ_with_value("88473500")).to be_valid
+ expect(champ_with_value("89473500")).to be_valid
+ end
+
+ def champ_with_value(number)
+ phone_champ.tap { |c| c.value = number }
end
end
end
diff --git a/spec/models/commentaire_spec.rb b/spec/models/commentaire_spec.rb
index ac7a2cab8..33fe318fe 100644
--- a/spec/models/commentaire_spec.rb
+++ b/spec/models/commentaire_spec.rb
@@ -47,8 +47,8 @@ describe Commentaire do
let(:dossier) { create(:dossier, procedure: procedure) }
context 'with a commentaire created by a instructeur' do
+ let(:instructeur) { create :instructeur, email: 'some_user@exemple.fr' }
let(:commentaire) { build :commentaire, instructeur: instructeur, dossier: dossier }
- let(:instructeur) { build :instructeur, email: 'some_user@exemple.fr' }
context 'when the procedure shows instructeurs email' do
before { Flipper.disable(:hide_instructeur_email, procedure) }
diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb
index df75ca854..c9cd324b1 100644
--- a/spec/models/dossier_spec.rb
+++ b/spec/models/dossier_spec.rb
@@ -1459,4 +1459,71 @@ describe Dossier do
it { expect(dossier.spreadsheet_columns(types_de_champ: [])).to include(["État du dossier", "Brouillon"]) }
end
+
+ describe "#rebase" do
+ let(:procedure) { create(:procedure, :with_type_de_champ_mandatory, :with_yes_no, :with_repetition, :with_datetime) }
+ let(:dossier) { create(:dossier, procedure: procedure) }
+
+ let(:yes_no_type_de_champ) { procedure.types_de_champ.find { |tdc| tdc.type_champ == TypeDeChamp.type_champs.fetch(:yes_no) } }
+
+ let(:text_type_de_champ) { procedure.types_de_champ.find(&:mandatory?) }
+ let(:text_champ) { dossier.champs.find(&:mandatory?) }
+ let(:rebased_text_champ) { dossier.champs.find { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:text) } }
+
+ let(:datetime_type_de_champ) { procedure.types_de_champ.find { |tdc| tdc.type_champ == TypeDeChamp.type_champs.fetch(:datetime) } }
+ let(:datetime_champ) { dossier.champs.find { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:datetime) } }
+ let(:rebased_datetime_champ) { dossier.champs.find { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:date) } }
+
+ let(:repetition_type_de_champ) { procedure.types_de_champ.find { |tdc| tdc.type_champ == TypeDeChamp.type_champs.fetch(:repetition) } }
+ let(:repetition_text_type_de_champ) { repetition_type_de_champ.types_de_champ.find { |tdc| tdc.type_champ == TypeDeChamp.type_champs.fetch(:text) } }
+ let(:repetition_champ) { dossier.champs.find { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:repetition) } }
+ let(:rebased_repetition_champ) { dossier.champs.find { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:repetition) } }
+
+ before do
+ procedure.publish!
+ procedure.draft_revision.add_type_de_champ({
+ type_champ: TypeDeChamp.type_champs.fetch(:text),
+ libelle: "Un champ text"
+ })
+ procedure.draft_revision.find_or_clone_type_de_champ(text_type_de_champ).update(mandatory: false, libelle: "nouveau libelle")
+ procedure.draft_revision.find_or_clone_type_de_champ(datetime_type_de_champ).update(type_champ: TypeDeChamp.type_champs.fetch(:date))
+ procedure.draft_revision.find_or_clone_type_de_champ(repetition_text_type_de_champ).update(libelle: "nouveau libelle dans une repetition")
+ procedure.draft_revision.add_type_de_champ({
+ type_champ: TypeDeChamp.type_champs.fetch(:checkbox),
+ libelle: "oui ou non",
+ parent_id: repetition_type_de_champ.stable_id
+ })
+ procedure.draft_revision.remove_type_de_champ(yes_no_type_de_champ.stable_id)
+
+ datetime_champ.update(value: Date.today.to_s)
+ text_champ.update(value: 'bonjour')
+ end
+
+ it "updates the brouillon champs with the latest revision changes" do
+ revision_id = dossier.revision_id
+ libelle = text_type_de_champ.libelle
+
+ expect(dossier.revision).to eq(procedure.published_revision)
+ expect(dossier.champs.size).to eq(4)
+ expect(repetition_champ.rows.size).to eq(1)
+ expect(repetition_champ.rows[0].size).to eq(1)
+
+ procedure.publish_revision!
+ perform_enqueued_jobs
+ procedure.reload
+ dossier.reload
+
+ expect(procedure.revisions.size).to eq(3)
+ expect(dossier.revision).to eq(procedure.published_revision)
+ expect(dossier.champs.size).to eq(4)
+ expect(rebased_text_champ.value).to eq(text_champ.value)
+ expect(rebased_text_champ.type_de_champ_id).not_to eq(text_champ.type_de_champ_id)
+ expect(rebased_datetime_champ.type_champ).to eq(TypeDeChamp.type_champs.fetch(:date))
+ expect(rebased_datetime_champ.value).to be_nil
+ expect(rebased_repetition_champ.rows.size).to eq(1)
+ expect(rebased_repetition_champ.rows[0].size).to eq(2)
+ expect(rebased_text_champ.rebased_at).not_to be_nil
+ expect(rebased_datetime_champ.rebased_at).not_to be_nil
+ end
+ end
end
diff --git a/spec/models/dossier_transfer_spec.rb b/spec/models/dossier_transfer_spec.rb
index a834b6a26..f25aa1dfa 100644
--- a/spec/models/dossier_transfer_spec.rb
+++ b/spec/models/dossier_transfer_spec.rb
@@ -46,16 +46,36 @@ RSpec.describe DossierTransfer, type: :model do
it { expect(DossierTransfer.with_dossiers.count).to eq(0) }
end
end
+ end
- describe 'dossier relationship' do
- let(:transfer) { create(:dossier_transfer) }
- let(:dossier) { create(:dossier, user: user, transfer: transfer) }
+ describe '#destroy_and_nullify' do
+ let(:transfer) { create(:dossier_transfer) }
+ let(:dossier) { create(:dossier, user: user, transfer: transfer) }
+ let(:discarded_dossier) { create(:dossier, user: user, transfer: dossier.transfer) }
- it 'nullify transfer relationship on dossier' do
- expect(dossier.transfer).to eq(transfer)
- transfer.destroy
- expect(dossier.reload.transfer).to be_nil
- end
+ before do
+ discarded_dossier.discard!
+ end
+
+ it 'nullify transfer relationship on dossier' do
+ expect(dossier.transfer).to eq(transfer)
+ transfer.destroy_and_nullify
+ expect(dossier.reload.transfer).to be_nil
+ end
+ end
+
+ describe '#destroy_stale' do
+ let(:transfer) { create(:dossier_transfer, created_at: 1.month.ago) }
+ let(:dossier) { create(:dossier, user: user, transfer: transfer) }
+ let(:discarded_dossier) { create(:dossier, user: user, transfer: dossier.transfer) }
+
+ before do
+ discarded_dossier.discard!
+ end
+
+ it 'nullify the transfer on discarded dossier' do
+ DossierTransfer.destroy_stale
+ expect(DossierTransfer.count).to eq(0)
end
end
end
diff --git a/spec/models/geo_area_spec.rb b/spec/models/geo_area_spec.rb
index dfb5aa10e..e0c0dfc52 100644
--- a/spec/models/geo_area_spec.rb
+++ b/spec/models/geo_area_spec.rb
@@ -1,30 +1,30 @@
RSpec.describe GeoArea, type: :model do
describe '#area' do
- let(:geo_area) { build(:geo_area, :polygon) }
+ let(:geo_area) { build(:geo_area, :polygon, champ: nil) }
it { expect(geo_area.area).to eq(103.6) }
end
describe '#area (hourglass polygon)' do
- let(:geo_area) { build(:geo_area, :hourglass_polygon) }
+ let(:geo_area) { build(:geo_area, :hourglass_polygon, champ: nil) }
it { expect(geo_area.area).to eq(32.4) }
end
describe '#length' do
- let(:geo_area) { build(:geo_area, :line_string) }
+ let(:geo_area) { build(:geo_area, :line_string, champ: nil) }
it { expect(geo_area.length).to eq(21.2) }
end
describe '#location' do
- let(:geo_area) { build(:geo_area, :point) }
+ let(:geo_area) { build(:geo_area, :point, champ: nil) }
it { expect(geo_area.location).to eq("46°32'19\"N 2°25'42\"E") }
end
describe '#rgeo_geometry' do
- let(:geo_area) { build(:geo_area, :polygon) }
+ let(:geo_area) { build(:geo_area, :polygon, champ: nil) }
let(:polygon) do
{
"type" => "Polygon",
@@ -46,44 +46,47 @@ RSpec.describe GeoArea, type: :model do
it { expect(geo_area.geometry).to eq(polygon) }
context 'polygon_with_extra_coordinate' do
- let(:geo_area) { build(:geo_area, :polygon_with_extra_coordinate) }
+ let(:geo_area) { build(:geo_area, :polygon_with_extra_coordinate, champ: nil) }
it { expect(geo_area.geometry).not_to eq(polygon) }
it { expect(geo_area.safe_geometry).to eq(polygon) }
end
end
- describe '#valid?' do
- let(:geo_area) { build(:geo_area, :polygon) }
+ describe 'validations' do
+ context 'geometry' do
+ subject! { geo_area.validate }
- context 'polygon' do
- it { expect(geo_area.valid?).to be_truthy }
- end
+ context 'polygon' do
+ let(:geo_area) { build(:geo_area, :polygon, champ: nil) }
+ it { expect(geo_area.errors).not_to have_key(:geometry) }
+ end
- context 'hourglass_polygon' do
- let(:geo_area) { build(:geo_area, :hourglass_polygon) }
- it { expect(geo_area.valid?).to be_falsey }
- end
+ context 'hourglass_polygon' do
+ let(:geo_area) { build(:geo_area, :hourglass_polygon, champ: nil) }
+ it { expect(geo_area.errors).to have_key(:geometry) }
+ end
- context 'line_string' do
- let(:geo_area) { build(:geo_area, :line_string) }
- it { expect(geo_area.valid?).to be_truthy }
- end
+ context 'line_string' do
+ let(:geo_area) { build(:geo_area, :line_string, champ: nil) }
+ it { expect(geo_area.errors).not_to have_key(:geometry) }
+ end
- context 'point' do
- let(:geo_area) { build(:geo_area, :point) }
- it { expect(geo_area.valid?).to be_truthy }
- end
+ context 'point' do
+ let(:geo_area) { build(:geo_area, :point, champ: nil) }
+ it { expect(geo_area.errors).not_to have_key(:geometry) }
+ end
- context 'invalid_right_hand_rule_polygon' do
- let(:geo_area) { build(:geo_area, :invalid_right_hand_rule_polygon) }
- it { expect(geo_area.valid?).to be_falsey }
+ context 'invalid_right_hand_rule_polygon' do
+ let(:geo_area) { build(:geo_area, :invalid_right_hand_rule_polygon, champ: nil) }
+ it { expect(geo_area.errors).to have_key(:geometry) }
+ end
end
end
describe "cadastre properties" do
- let(:geo_area) { build(:geo_area, :cadastre) }
- let(:legacy_geo_area) { build(:geo_area, :legacy_cadastre) }
+ let(:geo_area) { build(:geo_area, :cadastre, champ: nil) }
+ let(:legacy_geo_area) { build(:geo_area, :legacy_cadastre, champ: nil) }
it "should be backward compatible" do
expect("#{geo_area.code_dep}#{geo_area.code_com}").to eq(geo_area.commune)
@@ -103,7 +106,7 @@ RSpec.describe GeoArea, type: :model do
describe 'description' do
context 'when properties is nil' do
- let(:geo_area) { build(:geo_area, properties: nil) }
+ let(:geo_area) { build(:geo_area, properties: nil, champ: nil) }
it { expect(geo_area.description).to be_nil }
end
diff --git a/spec/models/procedure_revision_spec.rb b/spec/models/procedure_revision_spec.rb
index c7280fb95..efdc05dc1 100644
--- a/spec/models/procedure_revision_spec.rb
+++ b/spec/models/procedure_revision_spec.rb
@@ -174,7 +174,8 @@ describe ProcedureRevision do
{
op: :add,
label: "Un champ text",
- private: false
+ private: false,
+ stable_id: new_type_de_champ.stable_id
}
])
@@ -186,12 +187,14 @@ describe ProcedureRevision do
label: type_de_champ_first.libelle,
private: false,
from: type_de_champ_first.libelle,
- to: "modifier le libelle"
+ to: "modifier le libelle",
+ stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
- private: false
+ private: false,
+ stable_id: new_type_de_champ.stable_id
}
])
expect(new_revision.types_de_champ.first.revision).to eq(new_revision)
@@ -204,19 +207,22 @@ describe ProcedureRevision do
label: type_de_champ_first.libelle,
private: false,
from: type_de_champ_first.libelle,
- to: "modifier le libelle"
+ to: "modifier le libelle",
+ stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
- private: false
+ private: false,
+ stable_id: new_type_de_champ.stable_id
},
{
op: :move,
label: type_de_champ_second.libelle,
private: false,
from: 1,
- to: 2
+ to: 2,
+ stable_id: type_de_champ_second.stable_id
}
])
expect(new_revision.types_de_champ.last.revision).to eq(revision)
@@ -226,12 +232,14 @@ describe ProcedureRevision do
{
op: :remove,
label: type_de_champ_first.libelle,
- private: false
+ private: false,
+ stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
- private: false
+ private: false,
+ stable_id: new_type_de_champ.stable_id
}
])
@@ -241,12 +249,14 @@ describe ProcedureRevision do
{
op: :remove,
label: type_de_champ_first.libelle,
- private: false
+ private: false,
+ stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
- private: false
+ private: false,
+ stable_id: new_type_de_champ.stable_id
},
{
op: :update,
@@ -254,7 +264,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: type_de_champ_second.description,
- to: "une description"
+ to: "une description",
+ stable_id: type_de_champ_second.stable_id
},
{
op: :update,
@@ -262,7 +273,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: false,
- to: true
+ to: true,
+ stable_id: type_de_champ_second.stable_id
}
])
@@ -272,12 +284,14 @@ describe ProcedureRevision do
{
op: :remove,
label: type_de_champ_first.libelle,
- private: false
+ private: false,
+ stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
- private: false
+ private: false,
+ stable_id: new_type_de_champ.stable_id
},
{
op: :update,
@@ -285,7 +299,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: type_de_champ_second.description,
- to: "une description"
+ to: "une description",
+ stable_id: type_de_champ_second.stable_id
},
{
op: :update,
@@ -293,7 +308,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: false,
- to: true
+ to: true,
+ stable_id: type_de_champ_second.stable_id
},
{
op: :update,
@@ -301,7 +317,8 @@ describe ProcedureRevision do
label: "sub type de champ",
private: false,
from: "text",
- to: "drop_down_list"
+ to: "drop_down_list",
+ stable_id: new_revision.types_de_champ.last.types_de_champ.first.stable_id
},
{
op: :update,
@@ -309,7 +326,8 @@ describe ProcedureRevision do
label: "sub type de champ",
private: false,
from: [],
- to: ["one", "two"]
+ to: ["one", "two"],
+ stable_id: new_revision.types_de_champ.last.types_de_champ.first.stable_id
}
])
@@ -319,12 +337,14 @@ describe ProcedureRevision do
{
op: :remove,
label: type_de_champ_first.libelle,
- private: false
+ private: false,
+ stable_id: type_de_champ_first.stable_id
},
{
op: :add,
label: "Un champ text",
- private: false
+ private: false,
+ stable_id: new_type_de_champ.stable_id
},
{
op: :update,
@@ -332,7 +352,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: type_de_champ_second.description,
- to: "une description"
+ to: "une description",
+ stable_id: type_de_champ_second.stable_id
},
{
op: :update,
@@ -340,7 +361,8 @@ describe ProcedureRevision do
label: type_de_champ_second.libelle,
private: false,
from: false,
- to: true
+ to: true,
+ stable_id: type_de_champ_second.stable_id
},
{
op: :update,
@@ -348,7 +370,8 @@ describe ProcedureRevision do
label: "sub type de champ",
private: false,
from: "text",
- to: "carte"
+ to: "carte",
+ stable_id: new_revision.types_de_champ.last.types_de_champ.first.stable_id
},
{
op: :update,
@@ -356,7 +379,8 @@ describe ProcedureRevision do
label: "sub type de champ",
private: false,
from: [],
- to: [:cadastres, :znieff]
+ to: [:cadastres, :znieff],
+ stable_id: new_revision.types_de_champ.last.types_de_champ.first.stable_id
}
])
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 8d4780268..6989089d7 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -372,7 +372,7 @@ describe User, type: :model do
end
context 'for administrateurs' do
- let(:user) { build(:user, email: 'admin@exemple.fr', password: password, administrateur: build(:administrateur)) }
+ let(:user) { build(:user, email: 'admin@exemple.fr', password: password, administrateur: create(:administrateur, user: nil)) }
context 'when the password is too short' do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
diff --git a/spec/policies/champ_policy_spec.rb b/spec/policies/champ_policy_spec.rb
index 3a49c5b08..a7e02747f 100644
--- a/spec/policies/champ_policy_spec.rb
+++ b/spec/policies/champ_policy_spec.rb
@@ -52,7 +52,7 @@ describe ChampPolicy do
end
context 'when the user also has instruction rights' do
- let(:instructeur) { create(:instructeur, email: signed_in_user.email, password: signed_in_user.password) }
+ let(:instructeur) { create(:instructeur, user: signed_in_user) }
let(:account) { { user: signed_in_user, instructeur: instructeur } }
context 'as the dossier instructeur and owner' do
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 5141112b9..705f4d462 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -58,7 +58,7 @@ RSpec.configure do |config|
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
- config.use_transactional_fixtures = false
+ config.use_transactional_fixtures = true
# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
@@ -127,4 +127,5 @@ RSpec.configure do |config|
config.include Shoulda::Matchers::ActiveModel, type: :model
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::ControllerHelpers, type: :view
+ config.include Devise::Test::IntegrationHelpers, type: :system
end
diff --git a/spec/services/zxcvbn_service_spec.rb b/spec/services/zxcvbn_service_spec.rb
new file mode 100644
index 000000000..337da8c35
--- /dev/null
+++ b/spec/services/zxcvbn_service_spec.rb
@@ -0,0 +1,47 @@
+describe ZxcvbnService do
+ let(:password) { 'medium-strength-password' }
+ subject(:service) { ZxcvbnService.new(password) }
+
+ describe '#score' do
+ it 'returns the password complexity score' do
+ expect(service.score).to eq 3
+ end
+ end
+
+ describe '#complexity' do
+ it 'returns the password score, vulnerability and length' do
+ expect(service.complexity).to eq [3, 'medium, strength, password', 24]
+ end
+ end
+
+ describe 'caching' do
+ it 'lazily caches the tester between calls and instances' do
+ allow(Zxcvbn::Tester).to receive(:new).and_call_original
+ allow(YAML).to receive(:safe_load).and_call_original
+
+ first_service = ZxcvbnService.new('some-password')
+ first_service.score
+ first_service.complexity
+ other_service = ZxcvbnService.new('other-password')
+ other_service.score
+ other_service.complexity
+
+ expect(Zxcvbn::Tester).to have_received(:new).at_most(:once)
+ expect(YAML).to have_received(:safe_load).at_most(:once)
+ end
+
+ it 'lazily caches the tester between threads' do
+ allow(Zxcvbn::Tester).to receive(:new).and_call_original
+
+ threads = 1.upto(4).map do
+ Thread.new do
+ ZxcvbnService.new(password).score
+ end
+ end.map(&:join)
+
+ scores = threads.map(&:value)
+ expect(scores).to eq([3, 3, 3, 3])
+ expect(Zxcvbn::Tester).to have_received(:new).at_most(:once)
+ end
+ end
+end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 40eea57e5..10ed09c19 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -3,9 +3,6 @@ require 'capybara-screenshot/rspec'
require 'capybara/email/rspec'
require 'selenium/webdriver'
-Capybara.javascript_driver = :headless_chrome
-Capybara.ignore_hidden_elements = false
-
Capybara.register_driver :chrome do |app|
Capybara::Selenium::Driver.new(app, browser: :chrome)
end
@@ -34,11 +31,10 @@ Capybara.register_driver :headless_chrome do |app|
end
end
-# FIXME: remove this line when https://github.com/rspec/rspec-rails/issues/1897 has been fixed
-Capybara.server = :puma, { Silent: true }
-
Capybara.default_max_wait_time = 2
+Capybara.ignore_hidden_elements = false
+
# Save a snapshot of the HTML page when an integration test fails
Capybara::Screenshot.autosave_on_failure = true
# Keep only the screenshots generated from the last failing test suite
@@ -49,13 +45,21 @@ Capybara::Screenshot.register_driver :headless_chrome do |driver, path|
end
RSpec.configure do |config|
- # Set the user preferred language before Javascript feature specs.
+ config.before(:each, type: :system) do
+ driven_by :rack_test
+ end
+
+ config.before(:each, type: :system, js: true) do
+ driven_by :headless_chrome
+ end
+
+ # Set the user preferred language before Javascript system specs.
#
- # Features specs without Javascript run in a Rack stack, and respect the Accept-Language value.
+ # System specs without Javascript run in a Rack stack, and respect the Accept-Language value.
# However specs using Javascript are run into a Headless Chrome, which doesn't support setting
# the default Accept-Language value reliably.
# So instead we set the locale cookie explicitly before each Javascript test.
- config.before(:each, js: true) do
+ config.before(:each, type: :system, js: true) do
visit '/' # Webdriver needs visiting a page before setting the cookie
Capybara.current_session.driver.browser.manage.add_cookie(
name: :locale,
diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb
deleted file mode 100644
index 553e7ce34..000000000
--- a/spec/support/database_cleaner.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-RSpec.configure do |config|
- expect_list = []
-
- config.before(:suite) do
- DatabaseCleaner.clean_with(:truncation, except: expect_list)
- end
-
- config.before(:each) do
- DatabaseCleaner.strategy = :transaction
- end
-
- config.before(:each, js: true) do
- DatabaseCleaner.strategy = :deletion, { except: expect_list }
- end
-
- config.before(:each) do
- DatabaseCleaner.start
- end
-
- config.after(:each) do
- DatabaseCleaner.clean
- end
-end
diff --git a/spec/support/feature_helpers.rb b/spec/support/system_helpers.rb
similarity index 98%
rename from spec/support/feature_helpers.rb
rename to spec/support/system_helpers.rb
index 371a906e8..3cda3ddb2 100644
--- a/spec/support/feature_helpers.rb
+++ b/spec/support/system_helpers.rb
@@ -1,4 +1,4 @@
-module FeatureHelpers
+module SystemHelpers
include ActiveJob::TestHelper
def login_admin
@@ -177,5 +177,5 @@ module FeatureHelpers
end
RSpec.configure do |config|
- config.include FeatureHelpers, type: :feature
+ config.include SystemHelpers, type: :system
end
diff --git a/spec/features/accessibilite/wcag_usager_spec.rb b/spec/system/accessibilite/wcag_usager_spec.rb
similarity index 98%
rename from spec/features/accessibilite/wcag_usager_spec.rb
rename to spec/system/accessibilite/wcag_usager_spec.rb
index a6a716129..5ac9658aa 100644
--- a/spec/features/accessibilite/wcag_usager_spec.rb
+++ b/spec/system/accessibilite/wcag_usager_spec.rb
@@ -1,4 +1,4 @@
-feature 'wcag rules for usager', js: true do
+describe 'wcag rules for usager', js: true do
let(:procedure) { create(:procedure, :with_type_de_champ, :with_all_champs, :with_service, :for_individual, :published) }
let(:password) { 'a very complicated password' }
let(:litteraire_user) { create(:user, password: password) }
diff --git a/spec/features/admin/admin_creation_spec.rb b/spec/system/admin/admin_creation_spec.rb
similarity index 96%
rename from spec/features/admin/admin_creation_spec.rb
rename to spec/system/admin/admin_creation_spec.rb
index 938bd1c56..f3786f7e8 100644
--- a/spec/features/admin/admin_creation_spec.rb
+++ b/spec/system/admin/admin_creation_spec.rb
@@ -1,4 +1,4 @@
-feature 'As an administrateur', js: true do
+describe 'As an administrateur', js: true do
let(:super_admin) { create(:super_admin) }
let(:admin_email) { 'new_admin@gouv.fr' }
let(:new_admin) { Administrateur.by_email(admin_email) }
diff --git a/spec/features/admin/procedure_cloning_spec.rb b/spec/system/admin/procedure_cloning_spec.rb
similarity index 92%
rename from spec/features/admin/procedure_cloning_spec.rb
rename to spec/system/admin/procedure_cloning_spec.rb
index 9a7019d55..bd4535ecc 100644
--- a/spec/features/admin/procedure_cloning_spec.rb
+++ b/spec/system/admin/procedure_cloning_spec.rb
@@ -1,6 +1,6 @@
-require 'features/admin/procedure_spec_helper'
+require 'system/admin/procedure_spec_helper'
-feature 'As an administrateur I wanna clone a procedure', js: true do
+describe 'As an administrateur I wanna clone a procedure', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur) }
diff --git a/spec/features/admin/procedure_creation_spec.rb b/spec/system/admin/procedure_creation_spec.rb
similarity index 96%
rename from spec/features/admin/procedure_creation_spec.rb
rename to spec/system/admin/procedure_creation_spec.rb
index 8208246f2..09025a0ba 100644
--- a/spec/features/admin/procedure_creation_spec.rb
+++ b/spec/system/admin/procedure_creation_spec.rb
@@ -1,6 +1,6 @@
-require 'features/admin/procedure_spec_helper'
+require 'system/admin/procedure_spec_helper'
-feature 'As an administrateur I wanna create a new procedure', js: true do
+describe 'As an administrateur I wanna create a new procedure', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur, :with_procedure) }
diff --git a/spec/features/admin/procedure_locked_spec.rb b/spec/system/admin/procedure_locked_spec.rb
similarity index 96%
rename from spec/features/admin/procedure_locked_spec.rb
rename to spec/system/admin/procedure_locked_spec.rb
index 1e18c114d..58da6c7a8 100644
--- a/spec/features/admin/procedure_locked_spec.rb
+++ b/spec/system/admin/procedure_locked_spec.rb
@@ -1,4 +1,4 @@
-feature 'procedure locked' do
+describe 'procedure locked' do
let(:administrateur) { create(:administrateur) }
before do
diff --git a/spec/features/admin/procedure_publish_spec.rb b/spec/system/admin/procedure_publish_spec.rb
similarity index 96%
rename from spec/features/admin/procedure_publish_spec.rb
rename to spec/system/admin/procedure_publish_spec.rb
index f7df44200..f60d5aed4 100644
--- a/spec/features/admin/procedure_publish_spec.rb
+++ b/spec/system/admin/procedure_publish_spec.rb
@@ -1,6 +1,6 @@
-require 'features/admin/procedure_spec_helper'
+require 'system/admin/procedure_spec_helper'
-feature 'Publication de démarches', js: true do
+describe 'Publication de démarches', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur) }
diff --git a/spec/features/admin/procedure_spec_helper.rb b/spec/system/admin/procedure_spec_helper.rb
similarity index 100%
rename from spec/features/admin/procedure_spec_helper.rb
rename to spec/system/admin/procedure_spec_helper.rb
diff --git a/spec/features/admin/procedure_update_spec.rb b/spec/system/admin/procedure_update_spec.rb
similarity index 95%
rename from spec/features/admin/procedure_update_spec.rb
rename to spec/system/admin/procedure_update_spec.rb
index 0e13b5646..adcf50507 100644
--- a/spec/features/admin/procedure_update_spec.rb
+++ b/spec/system/admin/procedure_update_spec.rb
@@ -1,6 +1,6 @@
-require 'features/admin/procedure_spec_helper'
+require 'system/admin/procedure_spec_helper'
-feature 'Administrateurs can edit procedures', js: true do
+describe 'Administrateurs can edit procedures', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur) }
diff --git a/spec/features/api_particulier/api_particulier_spec.rb b/spec/system/api_particulier/api_particulier_spec.rb
similarity index 99%
rename from spec/features/api_particulier/api_particulier_spec.rb
rename to spec/system/api_particulier/api_particulier_spec.rb
index de793f453..ce398eca2 100644
--- a/spec/features/api_particulier/api_particulier_spec.rb
+++ b/spec/system/api_particulier/api_particulier_spec.rb
@@ -1,4 +1,4 @@
-feature 'fetch API Particulier Data', js: true do
+describe 'fetch API Particulier Data', js: true do
let(:administrateur) { create(:administrateur) }
let(:expected_token) { 'd7e9c9f4c3ca00caadde31f50fd4521a' }
diff --git a/spec/features/experts/expert_spec.rb b/spec/system/experts/expert_spec.rb
similarity index 99%
rename from spec/features/experts/expert_spec.rb
rename to spec/system/experts/expert_spec.rb
index 06eaa6126..92837c9d8 100644
--- a/spec/features/experts/expert_spec.rb
+++ b/spec/system/experts/expert_spec.rb
@@ -1,4 +1,4 @@
-feature 'Inviting an expert:' do
+describe 'Inviting an expert:' do
include ActiveJob::TestHelper
include ActionView::Helpers
diff --git a/spec/features/forgery_spec.rb b/spec/system/forgery_spec.rb
similarity index 93%
rename from spec/features/forgery_spec.rb
rename to spec/system/forgery_spec.rb
index e094fa9a4..93cffbf5f 100644
--- a/spec/features/forgery_spec.rb
+++ b/spec/system/forgery_spec.rb
@@ -1,4 +1,4 @@
-feature 'Protecting against request forgeries:', :allow_forgery_protection, :show_exception_pages do
+describe 'Protecting against request forgeries:', :allow_forgery_protection, :show_exception_pages do
let(:user) { create(:user, password: password) }
let(:password) { 'ThisIsTheUserPassword' }
diff --git a/spec/features/france_connect/france_connect_particulier_spec.rb b/spec/system/france_connect/france_connect_particulier_spec.rb
similarity index 98%
rename from spec/features/france_connect/france_connect_particulier_spec.rb
rename to spec/system/france_connect/france_connect_particulier_spec.rb
index 4498f8031..a0a8b8e01 100644
--- a/spec/features/france_connect/france_connect_particulier_spec.rb
+++ b/spec/system/france_connect/france_connect_particulier_spec.rb
@@ -1,4 +1,4 @@
-feature 'France Connect Particulier Connexion' do
+describe 'France Connect Particulier Connexion' do
let(:code) { 'plop' }
let(:given_name) { 'titi' }
let(:family_name) { 'toto' }
diff --git a/spec/features/help_spec.rb b/spec/system/help_spec.rb
similarity index 98%
rename from spec/features/help_spec.rb
rename to spec/system/help_spec.rb
index 4aa7de431..c338837de 100644
--- a/spec/features/help_spec.rb
+++ b/spec/system/help_spec.rb
@@ -1,4 +1,4 @@
-feature 'Getting help:' do
+describe 'Getting help:' do
scenario 'a Help button is visible on public pages' do
visit '/'
within('.new-header') do
diff --git a/spec/features/i18n_spec.rb b/spec/system/i18n_spec.rb
similarity index 90%
rename from spec/features/i18n_spec.rb
rename to spec/system/i18n_spec.rb
index 0c372a0cd..681e6a32f 100644
--- a/spec/features/i18n_spec.rb
+++ b/spec/system/i18n_spec.rb
@@ -1,4 +1,4 @@
-feature 'Accessing the website in different languages:' do
+describe 'Accessing the website in different languages:' do
context 'when the i18n feature-flag is enabled' do
before { ENV['LOCALIZATION_ENABLED'] = 'true' }
after { ENV['LOCALIZATION_ENABLED'] = 'false' }
diff --git a/spec/features/instructeurs/expert_spec.rb b/spec/system/instructeurs/expert_spec.rb
similarity index 98%
rename from spec/features/instructeurs/expert_spec.rb
rename to spec/system/instructeurs/expert_spec.rb
index 84b3d56a6..4923b5142 100644
--- a/spec/features/instructeurs/expert_spec.rb
+++ b/spec/system/instructeurs/expert_spec.rb
@@ -1,4 +1,4 @@
-feature 'Inviting an expert:', js: true do
+describe 'Inviting an expert:', js: true do
include ActiveJob::TestHelper
include ActionView::Helpers
diff --git a/spec/features/instructeurs/instructeur_creation_spec.rb b/spec/system/instructeurs/instructeur_creation_spec.rb
similarity index 95%
rename from spec/features/instructeurs/instructeur_creation_spec.rb
rename to spec/system/instructeurs/instructeur_creation_spec.rb
index 978b068c3..400417e28 100644
--- a/spec/features/instructeurs/instructeur_creation_spec.rb
+++ b/spec/system/instructeurs/instructeur_creation_spec.rb
@@ -1,4 +1,4 @@
-feature 'As an instructeur', js: true do
+describe 'As an instructeur', js: true do
let(:administrateur) { create(:administrateur, :with_procedure) }
let(:procedure) { administrateur.procedures.first }
let(:instructeur_email) { 'new_instructeur@gouv.fr' }
diff --git a/spec/features/instructeurs/instruction_spec.rb b/spec/system/instructeurs/instruction_spec.rb
similarity index 99%
rename from spec/features/instructeurs/instruction_spec.rb
rename to spec/system/instructeurs/instruction_spec.rb
index eca6e1773..818a2cf8b 100644
--- a/spec/features/instructeurs/instruction_spec.rb
+++ b/spec/system/instructeurs/instruction_spec.rb
@@ -1,4 +1,4 @@
-feature 'Instructing a dossier:', js: true do
+describe 'Instructing a dossier:', js: true do
include ActiveJob::TestHelper
let(:password) { 'my-s3cure-p4ssword' }
diff --git a/spec/features/instructeurs/procedure_filters_spec.rb b/spec/system/instructeurs/procedure_filters_spec.rb
similarity index 99%
rename from spec/features/instructeurs/procedure_filters_spec.rb
rename to spec/system/instructeurs/procedure_filters_spec.rb
index 342151059..fee58f35a 100644
--- a/spec/features/instructeurs/procedure_filters_spec.rb
+++ b/spec/system/instructeurs/procedure_filters_spec.rb
@@ -1,4 +1,4 @@
-feature "procedure filters" do
+describe "procedure filters" do
let(:instructeur) { create(:instructeur) }
let(:procedure) { create(:procedure, :published, :with_type_de_champ, instructeurs: [instructeur]) }
let!(:type_de_champ) { procedure.types_de_champ.first }
diff --git a/spec/features/new_administrateur/types_de_champ_spec.rb b/spec/system/new_administrateur/types_de_champ_spec.rb
similarity index 98%
rename from spec/features/new_administrateur/types_de_champ_spec.rb
rename to spec/system/new_administrateur/types_de_champ_spec.rb
index ed9878c9a..18fa17229 100644
--- a/spec/features/new_administrateur/types_de_champ_spec.rb
+++ b/spec/system/new_administrateur/types_de_champ_spec.rb
@@ -1,4 +1,4 @@
-feature 'As an administrateur I can edit types de champ', js: true do
+describe 'As an administrateur I can edit types de champ', js: true do
let(:administrateur) { procedure.administrateurs.first }
let(:procedure) { create(:procedure) }
diff --git a/spec/features/outdated_browser_spec.rb b/spec/system/outdated_browser_spec.rb
similarity index 97%
rename from spec/features/outdated_browser_spec.rb
rename to spec/system/outdated_browser_spec.rb
index 0d8b5d104..a632b951f 100644
--- a/spec/features/outdated_browser_spec.rb
+++ b/spec/system/outdated_browser_spec.rb
@@ -1,4 +1,4 @@
-feature 'Outdated browsers support:' do
+describe 'Outdated browsers support:' do
context 'when the user browser is outdated' do
before(:each) do
ie_10_user_agent = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0; .NET4.0E; .NET4.0C; InfoPath.3)'
diff --git a/spec/features/routing/full_scenario_spec.rb b/spec/system/routing/full_scenario_spec.rb
similarity index 99%
rename from spec/features/routing/full_scenario_spec.rb
rename to spec/system/routing/full_scenario_spec.rb
index 47cdee2d5..cc5bb2837 100644
--- a/spec/features/routing/full_scenario_spec.rb
+++ b/spec/system/routing/full_scenario_spec.rb
@@ -1,4 +1,4 @@
-feature 'The routing', js: true do
+describe 'The routing', js: true do
let(:password) { 'a very complicated password' }
let(:procedure) { create(:procedure, :with_type_de_champ, :with_service, :for_individual) }
let(:administrateur) { create(:administrateur, procedures: [procedure]) }
diff --git a/spec/features/sessions/sign_in_spec.rb b/spec/system/sessions/sign_in_spec.rb
similarity index 98%
rename from spec/features/sessions/sign_in_spec.rb
rename to spec/system/sessions/sign_in_spec.rb
index b734463c0..bc68e01f6 100644
--- a/spec/features/sessions/sign_in_spec.rb
+++ b/spec/system/sessions/sign_in_spec.rb
@@ -1,4 +1,4 @@
-feature 'Signin in:' do
+describe 'Signin in:' do
let!(:user) { create(:user, password: password) }
let(:password) { 'my-s3cure-p4ssword' }
diff --git a/spec/features/users/brouillon_spec.rb b/spec/system/users/brouillon_spec.rb
similarity index 99%
rename from spec/features/users/brouillon_spec.rb
rename to spec/system/users/brouillon_spec.rb
index 8abfe3ccd..16cf8156d 100644
--- a/spec/features/users/brouillon_spec.rb
+++ b/spec/system/users/brouillon_spec.rb
@@ -1,4 +1,4 @@
-feature 'The user' do
+describe 'The user' do
let(:password) { 'my-s3cure-p4ssword' }
let!(:user) { create(:user, password: password) }
diff --git a/spec/features/users/change_email_spec.rb b/spec/system/users/change_email_spec.rb
similarity index 96%
rename from spec/features/users/change_email_spec.rb
rename to spec/system/users/change_email_spec.rb
index 016f3ee90..c2a8bfcf0 100644
--- a/spec/features/users/change_email_spec.rb
+++ b/spec/system/users/change_email_spec.rb
@@ -1,4 +1,4 @@
-feature 'Changing an email' do
+describe 'Changing an email' do
let(:old_email) { 'old@email.com' }
let(:user) { create(:user, email: old_email) }
diff --git a/spec/features/users/dossier_creation_spec.rb b/spec/system/users/dossier_creation_spec.rb
similarity index 99%
rename from spec/features/users/dossier_creation_spec.rb
rename to spec/system/users/dossier_creation_spec.rb
index 99e075a0a..130cabaca 100644
--- a/spec/features/users/dossier_creation_spec.rb
+++ b/spec/system/users/dossier_creation_spec.rb
@@ -1,4 +1,4 @@
-feature 'Creating a new dossier:' do
+describe 'Creating a new dossier:' do
let(:user) { create(:user) }
let(:siret) { '41816609600051' }
let(:siren) { siret[0...9] }
diff --git a/spec/features/users/dossier_details_spec.rb b/spec/system/users/dossier_details_spec.rb
similarity index 97%
rename from spec/features/users/dossier_details_spec.rb
rename to spec/system/users/dossier_details_spec.rb
index 2f37ce02c..741626461 100644
--- a/spec/features/users/dossier_details_spec.rb
+++ b/spec/system/users/dossier_details_spec.rb
@@ -1,4 +1,4 @@
-require 'features/users/dossier_shared_examples.rb'
+require 'system/users/dossier_shared_examples.rb'
describe 'Dossier details:' do
let(:user) { create(:user) }
diff --git a/spec/features/users/dossier_shared_examples.rb b/spec/system/users/dossier_shared_examples.rb
similarity index 100%
rename from spec/features/users/dossier_shared_examples.rb
rename to spec/system/users/dossier_shared_examples.rb
diff --git a/spec/system/users/dropdown_spec.rb b/spec/system/users/dropdown_spec.rb
new file mode 100644
index 000000000..67e6d3494
--- /dev/null
+++ b/spec/system/users/dropdown_spec.rb
@@ -0,0 +1,43 @@
+describe 'dropdown list with other option activated' do
+ let(:password) { 'my-s3cure-p4ssword' }
+ let!(:user) { create(:user, password: password) }
+
+ let(:list_items) do
+ <<~END_OF_LIST
+ --Primary 1--
+ Secondary 1.1
+ Secondary 1.2
+ END_OF_LIST
+ end
+
+ let(:type_de_champ) { build(:type_de_champ_drop_down_list, libelle: 'simple dropdown other', drop_down_list_value: list_items, drop_down_other: true) }
+
+ let(:procedure) do
+ create(:procedure, :published, :for_individual, types_de_champ: [type_de_champ])
+ end
+
+ let(:user_dossier) { user.dossiers.first }
+
+ before do
+ login_as(user, scope: :user)
+ visit "/commencer/#{procedure.path}"
+ click_on 'Commencer la démarche'
+ end
+
+ scenario 'Select other option and the other input hidden must appear', js: true do
+ fill_individual
+
+ find('.radios').find('label:last-child').find('input').select_option
+ expect(page).to have_selector('.drop_down_other', visible: true)
+ end
+
+ private
+
+ def fill_individual
+ choose 'Monsieur'
+ fill_in('individual_prenom', with: 'prenom')
+ fill_in('individual_nom', with: 'nom')
+ click_on 'Continuer'
+ expect(page).to have_current_path(brouillon_dossier_path(user_dossier))
+ end
+end
diff --git a/spec/features/users/invite_spec.rb b/spec/system/users/invite_spec.rb
similarity index 98%
rename from spec/features/users/invite_spec.rb
rename to spec/system/users/invite_spec.rb
index 117b9f68b..9ece00f57 100644
--- a/spec/features/users/invite_spec.rb
+++ b/spec/system/users/invite_spec.rb
@@ -1,6 +1,6 @@
-require 'features/users/dossier_shared_examples.rb'
+require 'system/users/dossier_shared_examples.rb'
-feature 'Invitations' do
+describe 'Invitations' do
let(:owner) { create(:user) }
let(:invited_user) { create(:user, email: 'user_invite@exemple.fr') }
let(:procedure) { create(:simple_procedure) }
diff --git a/spec/features/users/linked_dropdown_spec.rb b/spec/system/users/linked_dropdown_spec.rb
similarity index 98%
rename from spec/features/users/linked_dropdown_spec.rb
rename to spec/system/users/linked_dropdown_spec.rb
index bc809d901..c4bf21a92 100644
--- a/spec/features/users/linked_dropdown_spec.rb
+++ b/spec/system/users/linked_dropdown_spec.rb
@@ -1,4 +1,4 @@
-feature 'linked dropdown lists' do
+describe 'linked dropdown lists' do
let(:password) { 'my-s3cure-p4ssword' }
let!(:user) { create(:user, password: password) }
diff --git a/spec/features/users/list_dossiers_spec.rb b/spec/system/users/list_dossiers_spec.rb
similarity index 100%
rename from spec/features/users/list_dossiers_spec.rb
rename to spec/system/users/list_dossiers_spec.rb
diff --git a/spec/features/users/managing_password_spec.rb b/spec/system/users/managing_password_spec.rb
similarity index 99%
rename from spec/features/users/managing_password_spec.rb
rename to spec/system/users/managing_password_spec.rb
index 7ef69b716..cee681c7f 100644
--- a/spec/features/users/managing_password_spec.rb
+++ b/spec/system/users/managing_password_spec.rb
@@ -1,4 +1,4 @@
-feature 'Managing password:' do
+describe 'Managing password:' do
context 'for simple users' do
let(:user) { create(:user) }
let(:new_password) { 'a simple password' }
diff --git a/spec/features/users/sign_out_spec.rb b/spec/system/users/sign_out_spec.rb
similarity index 92%
rename from spec/features/users/sign_out_spec.rb
rename to spec/system/users/sign_out_spec.rb
index 85516afcb..9bed3514d 100644
--- a/spec/features/users/sign_out_spec.rb
+++ b/spec/system/users/sign_out_spec.rb
@@ -1,4 +1,4 @@
-feature 'Sign out' do
+describe 'Sign out' do
context 'when a user is logged in' do
let(:user) { create(:administrateur).user }
diff --git a/spec/features/users/sign_up_spec.rb b/spec/system/users/sign_up_spec.rb
similarity index 99%
rename from spec/features/users/sign_up_spec.rb
rename to spec/system/users/sign_up_spec.rb
index 3129120ae..429a9dc2f 100644
--- a/spec/features/users/sign_up_spec.rb
+++ b/spec/system/users/sign_up_spec.rb
@@ -1,4 +1,4 @@
-feature 'Signing up:' do
+describe 'Signing up:' do
let(:user_email) { generate :user_email }
let(:user_password) { 'my-s3cure-p4ssword' }
let(:procedure) { create :simple_procedure, :with_service }
diff --git a/spec/features/users/transfer_dossier_spec.rb b/spec/system/users/transfer_dossier_spec.rb
similarity index 100%
rename from spec/features/users/transfer_dossier_spec.rb
rename to spec/system/users/transfer_dossier_spec.rb