diff --git a/app/assets/stylesheets/utils.scss b/app/assets/stylesheets/utils.scss
index 19d084813..8dbdc9b45 100644
--- a/app/assets/stylesheets/utils.scss
+++ b/app/assets/stylesheets/utils.scss
@@ -61,3 +61,7 @@
.mb-2 {
margin-bottom: 2 * $default-spacer;
}
+
+.mb-1 {
+ margin-bottom: $default-spacer;
+}
diff --git a/app/controllers/new_administrateur/administrateur_controller.rb b/app/controllers/new_administrateur/administrateur_controller.rb
index 6a8da5753..7c5f4cdda 100644
--- a/app/controllers/new_administrateur/administrateur_controller.rb
+++ b/app/controllers/new_administrateur/administrateur_controller.rb
@@ -19,6 +19,13 @@ module NewAdministrateur
end
end
+ def procedure_revisable?
+ if @procedure.locked? && !@procedure.feature_enabled?(:procedure_revisions)
+ flash.alert = 'Démarche verrouillée'
+ redirect_to admin_procedure_path(@procedure)
+ end
+ end
+
def reset_procedure
if @procedure.brouillon?
@procedure.reset!
diff --git a/app/controllers/new_administrateur/procedures_controller.rb b/app/controllers/new_administrateur/procedures_controller.rb
index 7ef2b56f3..c0c697143 100644
--- a/app/controllers/new_administrateur/procedures_controller.rb
+++ b/app/controllers/new_administrateur/procedures_controller.rb
@@ -1,7 +1,7 @@
module NewAdministrateur
class ProceduresController < AdministrateurController
before_action :retrieve_procedure, only: [:champs, :annotations, :edit, :monavis, :update_monavis, :jeton, :update_jeton, :publication, :publish, :transfert, :allow_expert_review, :experts_require_administrateur_invitation]
- before_action :procedure_locked?, only: [:champs, :annotations]
+ before_action :procedure_revisable?, only: [:champs, :annotations]
ITEMS_PER_PAGE = 25
@@ -154,12 +154,16 @@ module NewAdministrateur
def publish
@procedure.assign_attributes(publish_params)
- if @procedure.publish_or_reopen!(current_administrateur)
+ if @procedure.draft_changed?
+ @procedure.publish_revision!
+ flash.notice = "Nouvelle version de la démarche publiée"
redirect_to admin_procedure_path(@procedure)
+ elsif @procedure.publish_or_reopen!(current_administrateur)
flash.notice = "Démarche publiée"
- else
redirect_to admin_procedure_path(@procedure)
+ else
flash.alert = @procedure.errors.full_messages
+ redirect_to admin_procedure_path(@procedure)
end
end
diff --git a/app/controllers/new_administrateur/types_de_champ_controller.rb b/app/controllers/new_administrateur/types_de_champ_controller.rb
index 160110985..4d4e1685c 100644
--- a/app/controllers/new_administrateur/types_de_champ_controller.rb
+++ b/app/controllers/new_administrateur/types_de_champ_controller.rb
@@ -1,7 +1,7 @@
module NewAdministrateur
class TypesDeChampController < AdministrateurController
before_action :retrieve_procedure, only: [:create, :update, :move, :destroy]
- before_action :procedure_locked?, only: [:create, :update, :move, :destroy]
+ before_action :procedure_revisable?, only: [:create, :update, :move, :destroy]
def create
type_de_champ = @procedure.draft_revision.add_type_de_champ(type_de_champ_create_params)
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index a2ba7403a..11fdb15f7 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -764,19 +764,19 @@ class Dossier < ApplicationRecord
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end
- def spreadsheet_columns_csv(types_de_champ:, types_de_champ_private:)
- spreadsheet_columns(with_etablissement: true, types_de_champ: types_de_champ, types_de_champ_private: types_de_champ_private)
+ def spreadsheet_columns_csv(types_de_champ:)
+ spreadsheet_columns(with_etablissement: true, types_de_champ: types_de_champ)
end
- def spreadsheet_columns_xlsx(types_de_champ:, types_de_champ_private:)
- spreadsheet_columns(types_de_champ: types_de_champ, types_de_champ_private: types_de_champ_private)
+ def spreadsheet_columns_xlsx(types_de_champ:)
+ spreadsheet_columns(types_de_champ: types_de_champ)
end
- def spreadsheet_columns_ods(types_de_champ:, types_de_champ_private:)
- spreadsheet_columns(types_de_champ: types_de_champ, types_de_champ_private: types_de_champ_private)
+ def spreadsheet_columns_ods(types_de_champ:)
+ spreadsheet_columns(types_de_champ: types_de_champ)
end
- def spreadsheet_columns(with_etablissement: false, types_de_champ:, types_de_champ_private:)
+ def spreadsheet_columns(with_etablissement: false, types_de_champ:)
columns = [
['ID', id.to_s],
['Email', user_email_for(:display)]
@@ -843,26 +843,12 @@ class Dossier < ApplicationRecord
columns << ['Groupe instructeur', groupe_instructeur.label]
end
- columns + champs_for_export(types_de_champ) + champs_private_for_export(types_de_champ_private)
+ columns + champs_for_export(types_de_champ)
end
def champs_for_export(types_de_champ)
# Index values by stable_id
- values = champs.reject(&:exclude_from_export?).reduce({}) do |champs, champ|
- champs[champ.stable_id] = champ.for_export
- champs
- end
-
- # Get all the champs values for the types de champ in the final list.
- # Dossier might not have corresponding champ – display nil.
- types_de_champ.map do |type_de_champ|
- [type_de_champ.libelle, values[type_de_champ.stable_id]]
- end
- end
-
- def champs_private_for_export(types_de_champ)
- # Index values by stable_id
- values = champs_private.reject(&:exclude_from_export?).reduce({}) do |champs, champ|
+ values = (champs + champs_private).reject(&:exclude_from_export?).reduce({}) do |champs, champ|
champs[champ.stable_id] = champ.for_export
champs
end
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index 07ca5e6ff..25674e078 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -87,6 +87,41 @@ class Procedure < ApplicationRecord
brouillon? ? draft_types_de_champ_private : published_types_de_champ_private
end
+ def types_de_champ_for_procedure_presentation
+ explanatory_types_de_champ = [:header_section, :explication, :repetition].map { |k| TypeDeChamp.type_champs.fetch(k) }
+
+ if brouillon?
+ TypeDeChamp
+ .joins(:revisions)
+ .where.not(type_champ: explanatory_types_de_champ)
+ .where(procedure_revisions: { id: draft_revision_id })
+ .order(:private, :position)
+ else
+ # fetch all type_de_champ.stable_id for all the revisions expect draft
+ # and for each stable_id take the bigger (more recent) type_de_champ.id
+ recent_ids = TypeDeChamp
+ .joins(:revisions)
+ .where.not(type_champ: explanatory_types_de_champ)
+ .where(procedure_revisions: { procedure_id: id })
+ .where.not(procedure_revisions: { id: draft_revision_id })
+ .group(:stable_id)
+ .select('MAX(types_de_champ.id)')
+
+ # fetch the more recent procedure_revision_types_de_champ
+ # which includes recents_ids
+ recents_prtdc = ProcedureRevisionTypeDeChamp
+ .where(type_de_champ_id: recent_ids)
+ .where.not(revision_id: draft_revision_id)
+ .group(:type_de_champ_id)
+ .select('MAX(id)')
+
+ TypeDeChamp
+ .joins(:revision_types_de_champ)
+ .where(revision_types_de_champ: { id: recents_prtdc })
+ .order(:private, :position, 'revision_types_de_champ.revision_id': :desc)
+ end
+ end
+
def types_de_champ_for_tags
if brouillon?
draft_types_de_champ
@@ -111,14 +146,6 @@ class Procedure < ApplicationRecord
end
end
- def types_de_champ_for_export
- types_de_champ.reject(&:exclude_from_export?)
- end
-
- def types_de_champ_private_for_export
- types_de_champ_private.reject(&:exclude_from_export?)
- end
-
has_many :administrateurs_procedures
has_many :administrateurs, through: :administrateurs_procedures, after_remove: -> (procedure, _admin) { procedure.validate! }
has_many :groupe_instructeurs, dependent: :destroy
@@ -333,6 +360,22 @@ class Procedure < ApplicationRecord
publiee? || close? || depubliee?
end
+ def draft_changed?
+ publiee? && published_revision.changed?(draft_revision)
+ end
+
+ def revision_changes
+ published_revision.compare(draft_revision)
+ end
+
+ def revision_types_de_champ_private_changes
+ revision_changes.filter { |change| change[:private] }
+ end
+
+ def revision_types_de_champ_changes
+ revision_changes.filter { |change| !change[:private] }
+ end
+
def accepts_new_dossiers?
publiee? || brouillon?
end
@@ -689,6 +732,11 @@ class Procedure < ApplicationRecord
end
end
+ def publish_revision!
+ update!(draft_revision: create_new_revision, published_revision: draft_revision)
+ published_revision.touch(:published_at)
+ end
+
private
def before_publish
diff --git a/app/models/procedure_presentation.rb b/app/models/procedure_presentation.rb
index 4ccaccd75..309b63d31 100644
--- a/app/models/procedure_presentation.rb
+++ b/app/models/procedure_presentation.rb
@@ -66,19 +66,9 @@ class ProcedurePresentation < ApplicationRecord
)
end
- explanatory_types_de_champ = [:header_section, :explication].map { |k| TypeDeChamp.type_champs.fetch(k) }
-
- fields.concat procedure.types_de_champ
- .where.not(type_champ: explanatory_types_de_champ)
- .order(:id)
- .pluck(:libelle, :stable_id)
- .map { |(libelle, stable_id)| field_hash(libelle, TYPE_DE_CHAMP, stable_id.to_s) }
-
- fields.concat procedure.types_de_champ_private
- .where.not(type_champ: explanatory_types_de_champ)
- .order(:id)
- .pluck(:libelle, :stable_id)
- .map { |(libelle, stable_id)| field_hash(libelle, TYPE_DE_CHAMP_PRIVATE, stable_id.to_s) }
+ fields.concat procedure.types_de_champ_for_procedure_presentation
+ .pluck(:libelle, :private, :stable_id)
+ .map { |(libelle, is_private, stable_id)| field_hash(libelle, is_private ? TYPE_DE_CHAMP_PRIVATE : TYPE_DE_CHAMP, stable_id.to_s) }
fields
end
diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb
index 42c646ee2..939450b5e 100644
--- a/app/models/procedure_revision.rb
+++ b/app/models/procedure_revision.rb
@@ -101,8 +101,131 @@ class ProcedureRevision < ApplicationRecord
!draft?
end
+ def changed?(revision)
+ types_de_champ != revision.types_de_champ || types_de_champ_private != revision.types_de_champ_private
+ end
+
+ def compare(revision)
+ changes = []
+ changes += compare_types_de_champ(types_de_champ, revision.types_de_champ)
+ changes += compare_types_de_champ(types_de_champ_private, revision.types_de_champ_private)
+ changes
+ end
+
private
+ def compare_types_de_champ(from_tdc, to_tdc)
+ if from_tdc == to_tdc
+ []
+ else
+ from_h = from_tdc.index_by(&:stable_id)
+ to_h = to_tdc.index_by(&:stable_id)
+
+ from_sids = from_h.keys
+ 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) }
+ 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) }
+ end
+
+ kept = from_sids.intersection(to_sids)
+
+ moved = kept
+ .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 }
+ end
+
+ changed = kept
+ .map { |sid| [sid, from_h[sid], to_h[sid]] }
+ .flat_map do |sid, from, to|
+ compare_type_de_champ(from, to)
+ .each { |h| h[:position] = to_sids.index(sid) }
+ end
+
+ (removed + added + moved + changed)
+ .sort_by { |h| h[:position] }
+ .each { |h| h.delete(:position) }
+ end
+ end
+
+ def compare_type_de_champ(from_type_de_champ, to_type_de_champ)
+ changes = []
+ if from_type_de_champ.type_champ != to_type_de_champ.type_champ
+ changes << {
+ op: :update,
+ attribute: :type_champ,
+ 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
+ }
+ end
+ if from_type_de_champ.libelle != to_type_de_champ.libelle
+ changes << {
+ op: :update,
+ attribute: :libelle,
+ label: from_type_de_champ.libelle,
+ private: from_type_de_champ.private?,
+ from: from_type_de_champ.libelle,
+ to: to_type_de_champ.libelle
+ }
+ end
+ if from_type_de_champ.description != to_type_de_champ.description
+ changes << {
+ op: :update,
+ attribute: :description,
+ label: from_type_de_champ.libelle,
+ private: from_type_de_champ.private?,
+ from: from_type_de_champ.description,
+ to: to_type_de_champ.description
+ }
+ end
+ if from_type_de_champ.mandatory? != to_type_de_champ.mandatory?
+ changes << {
+ op: :update,
+ attribute: :mandatory,
+ label: from_type_de_champ.libelle,
+ private: from_type_de_champ.private?,
+ from: from_type_de_champ.mandatory?,
+ to: to_type_de_champ.mandatory?
+ }
+ end
+ if to_type_de_champ.drop_down_list?
+ if from_type_de_champ.drop_down_list_options != to_type_de_champ.drop_down_list_options
+ changes << {
+ op: :update,
+ attribute: :drop_down_options,
+ 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
+ }
+ end
+ elsif to_type_de_champ.piece_justificative?
+ if from_type_de_champ.piece_justificative_template_checksum != to_type_de_champ.piece_justificative_template_checksum
+ changes << {
+ op: :update,
+ attribute: :piece_justificative_template,
+ 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
+ }
+ end
+ elsif to_type_de_champ.repetition?
+ if from_type_de_champ.types_de_champ != to_type_de_champ.types_de_champ
+ changes += compare_types_de_champ(from_type_de_champ.types_de_champ, to_type_de_champ.types_de_champ)
+ end
+ end
+ changes
+ end
+
def revise_type_de_champ(type_de_champ)
types_de_champ_association = type_de_champ.private? ? :revision_types_de_champ_private : :revision_types_de_champ
association = send(types_de_champ_association).find_by!(type_de_champ: type_de_champ)
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index 88fa0585c..35f168586 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -226,6 +226,12 @@ class TypeDeChamp < ApplicationRecord
end
end
+ def piece_justificative_template_checksum
+ if piece_justificative_template.attached?
+ piece_justificative_template.checksum
+ end
+ end
+
def drop_down_list_value
if drop_down_list_options.present?
drop_down_list_options.reject(&:empty?).join("\r\n")
diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb
index 3cafccdff..d6c8ed69d 100644
--- a/app/services/procedure_export_service.rb
+++ b/app/services/procedure_export_service.rb
@@ -84,11 +84,10 @@ class ProcedureExportService
end
def spreadsheet_columns(format)
- types_de_champ = @procedure.types_de_champ_for_export
- types_de_champ_private = @procedure.types_de_champ_private_for_export
+ types_de_champ = @procedure.types_de_champ_for_procedure_presentation.to_a
Proc.new do |instance|
- instance.send(:"spreadsheet_columns_#{format}", types_de_champ: types_de_champ, types_de_champ_private: types_de_champ_private)
+ instance.send(:"spreadsheet_columns_#{format}", types_de_champ: types_de_champ)
end
end
end
diff --git a/app/views/new_administrateur/procedures/_publication_form.html.haml b/app/views/new_administrateur/procedures/_publication_form.html.haml
index 8699b1c9e..881996d81 100644
--- a/app/views/new_administrateur/procedures/_publication_form.html.haml
+++ b/app/views/new_administrateur/procedures/_publication_form.html.haml
@@ -1,40 +1,44 @@
.card.mb-4
%h2.card-title Publiez votre démarche
= form_tag admin_procedure_publish_path(procedure_id: procedure.id), method: :put, class: 'form' do
- %p.mb-4 Publiez votre démarche, et partagez la à vos usagers. Aucune modification ne sera possible.
- %p Personnalisez le lien public de la démarche pour en faciliter l’accès (obligatoire pour publier votre démarche) :
- %p.empty-text
- = commencer_url(path: '')
- = text_field_tag(:path, procedure.path,
- id: 'procedure_path',
- label: 'Adresse de diffusion',
- placeholder: 'chemin-de-la-démarche',
- required: true,
- class: 'form',
- pattern: '^[a-z0-9_-]{3,200}$',
- title: "De 3 à 200 caractères; minuscules, chiffres et tiret seulement",
- data: { debounce: true, url: admin_procedure_publish_validate_path(procedure)},
- autocomplete: 'off',
- style: 'width: 300px; display: inline;')
- .text-info.mb-4
- Attention, diffusez toujours le lien complet affiché ci-dessus, et non pas un lien générique vers #{APPLICATION_NAME}. Ne dites pas non plus aux usagers de se rendre sur le site générique #{APPLICATION_NAME}, donnez-leur toujours le lien complet.
- %h2.card-title Diffusion de la démarche
- %p Où les utilisateurs trouveront-ils le lien de la démarche ? Typiquement, il s’agit d’une page de votre site web.
- %p.center
- = text_field_tag(:lien_site_web, procedure.lien_site_web,
+ - if procedure.draft_changed?
+ %p.mb-4 Publiez une nouvelle version de votre démarche. Les changements suivants seront appliqués :
+ = render partial: 'revision_changes', locals: { changes: procedure.revision_changes }
+ - else
+ %p.mb-4 Publiez votre démarche, et partagez la à vos usagers. Aucune modification ne sera possible.
+ %p Personnalisez le lien public de la démarche pour en faciliter l’accès (obligatoire pour publier votre démarche) :
+ %p.empty-text
+ = commencer_url(path: '')
+ = text_field_tag(:path, procedure.path,
+ id: 'procedure_path',
+ label: 'Adresse de diffusion',
+ placeholder: 'chemin-de-la-démarche',
required: true,
- class: 'form-control',
+ class: 'form',
+ pattern: '^[a-z0-9_-]{3,200}$',
+ title: "De 3 à 200 caractères; minuscules, chiffres et tiret seulement",
+ data: { debounce: true, url: admin_procedure_publish_validate_path(procedure)},
autocomplete: 'off',
- placeholder: 'https://exemple.gouv.fr/ma_demarche')
+ style: 'width: 300px; display: inline;')
+ .text-info.mb-4
+ Attention, diffusez toujours le lien complet affiché ci-dessus, et non pas un lien générique vers #{APPLICATION_NAME}. Ne dites pas non plus aux usagers de se rendre sur le site générique #{APPLICATION_NAME}, donnez-leur toujours le lien complet.
+ %h2.card-title Diffusion de la démarche
+ %p Où les utilisateurs trouveront-ils le lien de la démarche ? Typiquement, il s’agit d’une page de votre site web.
+ %p.center
+ = text_field_tag(:lien_site_web, procedure.lien_site_web,
+ required: true,
+ class: 'form-control',
+ autocomplete: 'off',
+ placeholder: 'https://exemple.gouv.fr/ma_demarche')
- procedure.validate(:publication)
- errors = procedure.errors
-# Ignore the :taken error if the path can be claimed
- - if errors.details[:path]&.pluck(:error)&.include?(:taken) && @procedure.path_available?(administrateur, procedure.path)
+ - if errors.details[:path]&.pluck(:error)&.include?(:taken) && procedure.path_available?(administrateur, procedure.path)
- errors.delete(:path)
- options = { class: "button primary", id: 'publish' }
- if errors.details[:path].present?
- options[:disabled] = :disabled
.flex.justify-end
- = submit_tag procedure_publish_text(@procedure, :submit), options
+ = submit_tag procedure_publish_text(procedure, :submit), options
diff --git a/app/views/new_administrateur/procedures/_revision_changes.html.haml b/app/views/new_administrateur/procedures/_revision_changes.html.haml
new file mode 100644
index 000000000..aa430ede6
--- /dev/null
+++ b/app/views/new_administrateur/procedures/_revision_changes.html.haml
@@ -0,0 +1,35 @@
+%ul
+ - changes.each do |change|
+ - case change[:op]
+ - when :add
+ %li.mb-1= "Le champ « #{change[:label]} » a été ajouté."
+ - when :remove
+ %li.mb-1= "Le champ « #{change[:label]} » a été supprimé."
+ - when :update
+ - case change[:attribute]
+ - when :libelle
+ %li.mb-1= "Le libellé du champ « #{change[:label]} » a changé en « #{change[:to]} »."
+ - when :type_champ
+ %li.mb-1= "Le type du champ « #{change[:label]} » a changé. Il est maintenant de type « #{t("activerecord.attributes.type_de_champ.type_champs.#{change[:to]}")} »."
+ - when :description
+ %li.mb-1= "La description du champ « #{change[:label]} » a changé. La nouvelle description est « #{change[:to]} »."
+ - when :mandatory
+ - if change[:from] == false
+ %li.mb-1= "Le champ « #{change[:label]} » est maintenant obligatoire."
+ - else
+ %li.mb-1= "Le champ « #{change[:label]} » n'est plus obligatoire."
+ - when :piece_justificative_template
+ %li.mb-1= "Le champ « #{change[:label]} » a changé de modèle de pièce justificative."
+ - when :drop_down_options
+ - added = change[:to].sort - change[:from].sort
+ - removed = change[:from].sort - change[:to].sort
+ %li.mb-1
+ = "Les options de sélection du champ « #{change[:label]} » ont changé."
+ %ul
+ - if added.present?
+ %li= "Valeurs ajoutés : #{added.map{ |term| "« #{term.strip} »" }.join(", ")}."
+ - if removed.present?
+ %li= "Valeurs supprimés : #{removed.map{ |term| "« #{term.strip} »" }.join(", ")}."
+ - move_changes = changes.filter { |change| change[:op] == :move }.size
+ - if move_changes != 0
+ %li.mb-1= t(:has_move_changes, count: move_changes, scope: [:new_administrateur, :revision_changes])
diff --git a/app/views/new_administrateur/procedures/publication.html.haml b/app/views/new_administrateur/procedures/publication.html.haml
index efd17f78e..4b07fd278 100644
--- a/app/views/new_administrateur/procedures/publication.html.haml
+++ b/app/views/new_administrateur/procedures/publication.html.haml
@@ -14,6 +14,9 @@
- if @procedure.close? || @procedure.depubliee?
%p.mb-4 Cette démarche est close et n’est donc plus accessible par le public. Vous pouvez la réactiver :
= render partial: 'publication_form', locals: { procedure: @procedure, administrateur: @current_administrateur }
+ - elsif @procedure.draft_changed?
+ %p.mb-4 Cette démarche est déjà publiée. Elle a été modifiée depuis sa publication. Vous pouvez publier les changements effectués dans une nouvelle version de cette démarche :
+ = render partial: 'publication_form', locals: { procedure: @procedure, administrateur: @current_administrateur }
- elsif @procedure.publiee?
%p Cette démarche est publiée, certains éléments ne peuvent plus être modifiés.
Pour y accéder vous pouvez utiliser le lien :
diff --git a/app/views/new_administrateur/procedures/show.html.haml b/app/views/new_administrateur/procedures/show.html.haml
index 82d2983d0..4a86a7953 100644
--- a/app/views/new_administrateur/procedures/show.html.haml
+++ b/app/views/new_administrateur/procedures/show.html.haml
@@ -29,6 +29,27 @@
%span.icon.archive
Clore
+ - if @procedure.draft_changed?
+ = link_to 'Publier les modifications', admin_procedure_publication_path(@procedure), class: 'button primary', id: 'publish-procedure-link', data: { disable_with: "Publication..." }
+
+- if @procedure.draft_changed?
+ - types_de_champ_changes = @procedure.revision_types_de_champ_changes
+ - types_de_champ_private_changes = @procedure.revision_types_de_champ_private_changes
+
+ - if types_de_champ_changes.present?
+ .container
+ .card.featured
+ .card-title
+ = t(:has_changes, count: types_de_champ_changes.size, scope: [:new_administrateur, :revision_changes])
+ = render partial: 'revision_changes', locals: { changes: types_de_champ_changes }
+
+ - if types_de_champ_private_changes.present?
+ .container
+ .card.featured
+ .card-title
+ = t(:has_private_changes, count: types_de_champ_private_changes.size, scope: [:new_administrateur, :revision_changes])
+ = render partial: 'revision_changes', locals: { changes: types_de_champ_private_changes }
+
.container
%h2.procedure-admin-explanation Indispensable avant publication
.procedure-grid
@@ -41,7 +62,7 @@
%p.card-admin-subtitle Logo, nom, description
%p.button Modifier
- - if !@procedure.locked?
+ - if !@procedure.locked? || @procedure.feature_enabled?(:procedure_revisions)
= link_to champs_admin_procedure_path(@procedure), class: 'card-admin' do
- if @procedure.draft_types_de_champ.count > 0
%div
@@ -159,7 +180,7 @@
%p.card-admin-subtitle Notifications automatiques
%p.button Modifier
- - if !@procedure.locked?
+ - if !@procedure.locked? || @procedure.feature_enabled?(:procedure_revisions)
= link_to annotations_admin_procedure_path(@procedure), class: 'card-admin' do
- if @procedure.draft_types_de_champ_private.present?
%div
diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb
index cec532155..17c876839 100644
--- a/config/initializers/flipper.rb
+++ b/config/initializers/flipper.rb
@@ -32,6 +32,7 @@ features = [
:hide_instructeur_email,
:instructeur_bypass_email_login_token,
:make_experts_notifiable,
+ :procedure_revisions,
:procedure_routage_api
]
diff --git a/config/locales/views/new_administrateur/groupe_instructeurs/fr.yml b/config/locales/views/new_administrateur/groupe_instructeurs/fr.yml
index 2243a9caf..6f1b05090 100644
--- a/config/locales/views/new_administrateur/groupe_instructeurs/fr.yml
+++ b/config/locales/views/new_administrateur/groupe_instructeurs/fr.yml
@@ -27,3 +27,13 @@ fr:
existing_groupe:
one: "%{count} groupe existe"
other: "%{count} groupes existent"
+ revision_changes:
+ has_changes:
+ one: Un champ a été changé
+ other: "%{count} champs ont été changés"
+ has_private_changes:
+ one: Une annotation privée a été changée
+ other: "%{count} deux annotations privées ont été changées"
+ has_move_changes:
+ one: Un champ a changé de position
+ other: "%{count} champs ont changé de position"
diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb
index 90f76235f..e4ac26716 100644
--- a/spec/models/dossier_spec.rb
+++ b/spec/models/dossier_spec.rb
@@ -1364,8 +1364,8 @@ describe Dossier do
it "should have champs from all revisions" do
expect(dossier.types_de_champ.map(&:libelle)).to eq([text_type_de_champ.libelle, datetime_type_de_champ.libelle, "Yes/no", explication_type_de_champ.libelle])
expect(dossier_second_revision.types_de_champ.map(&:libelle)).to eq([datetime_type_de_champ.libelle, "Updated yes/no", explication_type_de_champ.libelle, "New text field"])
- expect(dossier.champs_for_export(dossier.procedure.types_de_champ_for_export).map { |(libelle)| libelle }).to eq([datetime_type_de_champ.libelle, "Updated yes/no", "New text field"])
- expect(dossier.champs_for_export(dossier.procedure.types_de_champ_for_export)).to eq(dossier_second_revision.champs_for_export(dossier_second_revision.procedure.types_de_champ_for_export))
+ expect(dossier.champs_for_export(dossier.procedure.types_de_champ_for_procedure_presentation).map { |(libelle)| libelle }).to eq([text_type_de_champ.libelle, datetime_type_de_champ.libelle, "Updated yes/no", "New text field"])
+ expect(dossier.champs_for_export(dossier.procedure.types_de_champ_for_procedure_presentation)).to eq(dossier_second_revision.champs_for_export(dossier_second_revision.procedure.types_de_champ_for_procedure_presentation))
end
end
@@ -1373,7 +1373,7 @@ describe Dossier do
let(:procedure) { create(:procedure, :with_type_de_champ, :with_explication) }
it "should not contain non-exportable types de champ" do
- expect(dossier.champs_for_export(dossier.procedure.types_de_champ_for_export).map { |(libelle)| libelle }).to eq([text_type_de_champ.libelle])
+ expect(dossier.champs_for_export(dossier.procedure.types_de_champ_for_procedure_presentation).map { |(libelle)| libelle }).to eq([text_type_de_champ.libelle])
end
end
end
@@ -1451,6 +1451,6 @@ describe Dossier do
describe "#spreadsheet_columns" do
let(:dossier) { create(:dossier) }
- it { expect(dossier.spreadsheet_columns(types_de_champ: [], types_de_champ_private: [])).to include(["État du dossier", "Brouillon"]) }
+ it { expect(dossier.spreadsheet_columns(types_de_champ: [])).to include(["État du dossier", "Brouillon"]) }
end
end
diff --git a/spec/models/procedure_presentation_and_revisions_spec.rb b/spec/models/procedure_presentation_and_revisions_spec.rb
new file mode 100644
index 000000000..087b2e17e
--- /dev/null
+++ b/spec/models/procedure_presentation_and_revisions_spec.rb
@@ -0,0 +1,90 @@
+describe ProcedurePresentation do
+ describe "#types_de_champ_for_procedure_presentation" do
+ subject { procedure.types_de_champ_for_procedure_presentation.pluck(:libelle) }
+
+ context 'for a draft procedure' do
+ let(:procedure) { create(:procedure) }
+
+ context 'when there are one tdc on a published revision' do
+ let!(:tdc) { { type_champ: :number, libelle: 'libelle 1' } }
+
+ before { procedure.draft_revision.add_type_de_champ(tdc) }
+
+ it { is_expected.to match(['libelle 1']) }
+ end
+ end
+
+ context 'for a published procedure' do
+ let(:procedure) { create(:procedure, :published) }
+ let!(:tdc) { { type_champ: :number, libelle: 'libelle 1' } }
+
+ before do
+ procedure.draft_revision.add_type_de_champ(tdc)
+ procedure.publish_revision!
+ end
+
+ it { is_expected.to match(['libelle 1']) }
+
+ context 'when there is another published revision with an added tdc' do
+ let!(:added_tdc) { { type_champ: :number, libelle: 'libelle 2' } }
+
+ before do
+ procedure.draft_revision.add_type_de_champ(added_tdc)
+ procedure.publish_revision!
+ end
+
+ it { is_expected.to match(['libelle 1', 'libelle 2']) }
+ end
+
+ context 'add one tdc above the first one' do
+ let!(:tdc2) { { type_champ: :number, libelle: 'libelle 2' } }
+
+ before do
+ created_tdc2 = procedure.draft_revision.add_type_de_champ(tdc2)
+ procedure.draft_revision.move_type_de_champ(created_tdc2.stable_id, 0)
+ procedure.publish_revision!
+ end
+
+ it { is_expected.to match(['libelle 2', 'libelle 1']) }
+
+ context 'and finaly, when this tdc is removed' do
+ let!(:previous_tdc2) { procedure.published_revision.types_de_champ.find_by(libelle: 'libelle 2') }
+
+ before do
+ procedure.draft_revision.remove_type_de_champ(previous_tdc2.stable_id)
+
+ procedure.publish_revision!
+ end
+
+ it { is_expected.to match(['libelle 1', 'libelle 2']) }
+ end
+ end
+
+ context 'when there is another published revision with a renamed tdc' do
+ let!(:previous_tdc) { procedure.published_revision.types_de_champ.first }
+ let!(:changed_tdc) { { type_champ: :number, libelle: 'changed libelle 1' } }
+
+ before do
+ type_de_champ = procedure.draft_revision.find_or_clone_type_de_champ(previous_tdc.id)
+ type_de_champ.update(changed_tdc)
+
+ procedure.publish_revision!
+ end
+
+ it { is_expected.to match(['changed libelle 1']) }
+ end
+
+ context 'when there is another published which removes a previous tdc' do
+ let!(:previous_tdc) { procedure.published_revision.types_de_champ.first }
+
+ before do
+ type_de_champ = procedure.draft_revision.remove_type_de_champ(previous_tdc.id)
+
+ procedure.publish_revision!
+ end
+
+ it { is_expected.to match(['libelle 1']) }
+ end
+ end
+ end
+end
diff --git a/spec/models/procedure_revision_spec.rb b/spec/models/procedure_revision_spec.rb
index be67e94c6..0d14d7837 100644
--- a/spec/models/procedure_revision_spec.rb
+++ b/spec/models/procedure_revision_spec.rb
@@ -56,7 +56,7 @@ describe ProcedureRevision do
revision.reload
expect(revision.types_de_champ.index(type_de_champ)).to eq(2)
expect(revision.procedure.types_de_champ.index(type_de_champ)).to eq(2)
- expect(revision.procedure.types_de_champ_for_export.index(type_de_champ)).to eq(2)
+ expect(revision.procedure.types_de_champ_for_procedure_presentation.index(type_de_champ)).to eq(2)
end
it 'move up' do
@@ -66,7 +66,7 @@ describe ProcedureRevision do
revision.reload
expect(revision.types_de_champ.index(last_type_de_champ)).to eq(0)
expect(revision.procedure.types_de_champ.index(last_type_de_champ)).to eq(0)
- expect(revision.procedure.types_de_champ_for_export.index(last_type_de_champ)).to eq(0)
+ expect(revision.procedure.types_de_champ_for_procedure_presentation.index(last_type_de_champ)).to eq(0)
end
context 'repetition' do
@@ -152,5 +152,167 @@ describe ProcedureRevision do
expect(new_revision.revision_types_de_champ).not_to eq(revision.revision_types_de_champ)
expect(new_revision.revision_types_de_champ_private).not_to eq(revision.revision_types_de_champ_private)
end
+
+ describe '#compare' do
+ let(:type_de_champ_first) { revision.types_de_champ.first }
+ let(:type_de_champ_second) { revision.types_de_champ.second }
+
+ it 'type_de_champ' do
+ expect(new_revision.types_de_champ.size).to eq(2)
+ new_type_de_champ = new_revision.add_type_de_champ({
+ type_champ: TypeDeChamp.type_champs.fetch(:text),
+ libelle: "Un champ text"
+ })
+ revision.reload
+ expect(new_revision.types_de_champ.size).to eq(3)
+ expect(new_revision.types_de_champ.last).to eq(new_type_de_champ)
+ expect(new_revision.revision_types_de_champ.last.position).to eq(2)
+ expect(new_revision.revision_types_de_champ.last.type_de_champ).to eq(new_type_de_champ)
+ expect(new_revision.revision_types_de_champ.last.type_de_champ.revision).to eq(new_revision)
+ expect(procedure.active_revision.changed?(new_revision)).to be_truthy
+ expect(procedure.active_revision.compare(new_revision)).to eq([
+ {
+ op: :add,
+ label: "Un champ text",
+ private: false
+ }
+ ])
+
+ new_revision.find_or_clone_type_de_champ(new_revision.types_de_champ.first.stable_id).update(libelle: 'modifier le libelle')
+ expect(procedure.active_revision.compare(new_revision.reload)).to eq([
+ {
+ op: :update,
+ attribute: :libelle,
+ label: type_de_champ_first.libelle,
+ private: false,
+ from: type_de_champ_first.libelle,
+ to: "modifier le libelle"
+ },
+ {
+ op: :add,
+ label: "Un champ text",
+ private: false
+ }
+ ])
+ expect(new_revision.types_de_champ.first.revision).to eq(new_revision)
+
+ new_revision.move_type_de_champ(new_revision.types_de_champ.second.stable_id, 2)
+ expect(procedure.active_revision.compare(new_revision.reload)).to eq([
+ {
+ op: :update,
+ attribute: :libelle,
+ label: type_de_champ_first.libelle,
+ private: false,
+ from: type_de_champ_first.libelle,
+ to: "modifier le libelle"
+ },
+ {
+ op: :add,
+ label: "Un champ text",
+ private: false
+ },
+ {
+ op: :move,
+ label: type_de_champ_second.libelle,
+ private: false,
+ from: 1,
+ to: 2
+ }
+ ])
+ expect(new_revision.types_de_champ.last.revision).to eq(revision)
+
+ new_revision.remove_type_de_champ(new_revision.types_de_champ.first.stable_id)
+ expect(procedure.active_revision.compare(new_revision.reload)).to eq([
+ {
+ op: :remove,
+ label: type_de_champ_first.libelle,
+ private: false
+ },
+ {
+ op: :add,
+ label: "Un champ text",
+ private: false
+ }
+ ])
+
+ new_revision.find_or_clone_type_de_champ(new_revision.types_de_champ.last.stable_id).update(description: 'une description')
+ new_revision.find_or_clone_type_de_champ(new_revision.types_de_champ.last.stable_id).update(mandatory: true)
+ expect(procedure.active_revision.compare(new_revision.reload)).to eq([
+ {
+ op: :remove,
+ label: type_de_champ_first.libelle,
+ private: false
+ },
+ {
+ op: :add,
+ label: "Un champ text",
+ private: false
+ },
+ {
+ op: :update,
+ attribute: :description,
+ label: type_de_champ_second.libelle,
+ private: false,
+ from: type_de_champ_second.description,
+ to: "une description"
+ },
+ {
+ op: :update,
+ attribute: :mandatory,
+ label: type_de_champ_second.libelle,
+ private: false,
+ from: false,
+ to: true
+ }
+ ])
+
+ new_revision.find_or_clone_type_de_champ(new_revision.types_de_champ.last.types_de_champ.first.stable_id).update(type_champ: :drop_down_list)
+ new_revision.find_or_clone_type_de_champ(new_revision.types_de_champ.last.types_de_champ.first.stable_id).update(drop_down_options: ['one', 'two'])
+ expect(procedure.active_revision.compare(new_revision.reload)).to eq([
+ {
+ op: :remove,
+ label: type_de_champ_first.libelle,
+ private: false
+ },
+ {
+ op: :add,
+ label: "Un champ text",
+ private: false
+ },
+ {
+ op: :update,
+ attribute: :description,
+ label: type_de_champ_second.libelle,
+ private: false,
+ from: type_de_champ_second.description,
+ to: "une description"
+ },
+ {
+ op: :update,
+ attribute: :mandatory,
+ label: type_de_champ_second.libelle,
+ private: false,
+ from: false,
+ to: true
+ },
+ {
+ op: :update,
+ attribute: :type_champ,
+ label: "sub type de champ",
+ private: false,
+ from: "text",
+ to: "drop_down_list"
+ },
+ {
+ op: :update,
+ attribute: :drop_down_options,
+ label: "sub type de champ",
+ private: false,
+ from: [],
+ to: ["one", "two"]
+ }
+ ])
+ end
+ end
end
end