diff --git a/app/models/concerns/dossier_rebase_concern.rb b/app/models/concerns/dossier_rebase_concern.rb index 73a1e2a53..dc0041b39 100644 --- a/app/models/concerns/dossier_rebase_concern.rb +++ b/app/models/concerns/dossier_rebase_concern.rb @@ -52,112 +52,134 @@ module DossierRebaseConcern def rebase attachments_to_purge = [] geo_areas_to_delete = [] - changes_by_type_de_champ = pending_changes + champs_to_delete = [] + + # revision we are rebasing to + target_revision = procedure.published_revision + + # group changes by stable_id + changes_by_stable_id = pending_changes .filter { |change| change[:model] == :type_de_champ } .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) + # group current revision types de champ by stable_id + current_types_de_champ_by_stable_id = revision.types_de_champ.index_by(&:stable_id) + + # group published types de champ coordinates by stable_id + published_coordinates_by_stable_id = target_revision + .revision_types_de_champ + .includes(:type_de_champ, :parent) + .index_by(&:stable_id) + + # add and remove champs + changes_by_stable_id.each do |stable_id, changes| + type_de_champ = current_types_de_champ_by_stable_id[stable_id] + published_coordinate = published_coordinates_by_stable_id[stable_id] changes.each do |change| case change[:op] when :add - add_new_champs_for_revision(published_type_de_champ) + add_new_champs_for_revision(published_coordinate) 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 } + # find all champs with respective update changes and the published type de champ + champs_with_changes = Champ.where(dossier: self).includes(:type_de_champ).filter_map do |champ| + # type de champ from published revision + type_de_champ = published_coordinates_by_stable_id[champ.stable_id]&.type_de_champ + # only update op changes + changes = (changes_by_stable_id[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 - if change[:from].include?(:cadastres) && !change[:to].include?(:cadastres) - geo_areas_to_delete += champ.cadastres - end - end - update[:rebased_at] = Time.zone.now - end + if type_de_champ + [champ, type_de_champ, changes] end end - self.update_column(:revision_id, procedure.published_revision_id) + # apply changes to existing champs and reset values when needed + update_champs_for_revision(champs_with_changes) do |champ, change, update| + 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 + champs_to_delete += champ.champs + if champ.piece_justificative_file.attached? + attachments_to_purge << champ.piece_justificative_file + end + when :drop_down_options + update[:value] = nil + when :carte_layers + # if we are removing cadastres layer, we need to remove cadastre geo areas + if change[:from].include?(:cadastres) && !change[:to].include?(:cadastres) + geo_areas_to_delete += champ.cadastres + end + end + update[:rebased_at] = Time.zone.now + end + + # update dossier revision + self.update_column(:revision_id, target_revision.id) + + # clear orphaned data attachments_to_purge.each(&:purge_later) geo_areas_to_delete.each(&:destroy) + champs_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| + def add_new_champs_for_revision(published_coordinate) + if published_coordinate.child? + # If this type de champ is a child, we create a new champ for each row of the parent + parent_stable_id = published_coordinate.parent.stable_id + champs_repetition = Champ + .includes(:champs, :type_de_champ) + .where(dossier: self, type_de_champ: { stable_id: parent_stable_id }) + + champs_repetition.each do |champ_repetition| champ_repetition.champs.map(&:row).uniq.each do |row| - champ = published_type_de_champ.champ.build(row: row) - champ_repetition.champs << champ + create_champ(published_coordinate, champ_repetition, row: row) end end else - champ = published_type_de_champ.build_champ(revision: procedure.published_revision) - self.champs << champ + create_champ(published_coordinate, self) 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 + def create_champ(published_coordinate, parent, row: nil) + params = { revision: published_coordinate.revision } + params[:row] = row if row + champ = published_coordinate + .type_de_champ + .build_champ(params) + parent.champs << champ + end - update = {} + def update_champs_for_revision(champs_with_changes) + champs_with_changes.each do |champ, type_de_champ, changes| + update = {} - yield update + changes.each do |change| + yield champ, change, update + end - if champ.type_de_champ != published_type_de_champ - update[:type_de_champ_id] = published_type_de_champ.id - end + # update type de champ to reflect new revision + if champ.type_de_champ != type_de_champ + update[:type_de_champ_id] = type_de_champ.id + end - if update.present? - champ.update_columns(update) + if update.present? + champ.update_columns(update) + end 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)) + def delete_champs_for_revision(type_de_champ) + Champ + .where(dossier: self, type_de_champ: type_de_champ) .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_public + 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/procedure_revision.rb b/app/models/procedure_revision.rb index a95add156..1892d5080 100644 --- a/app/models/procedure_revision.rb +++ b/app/models/procedure_revision.rb @@ -18,7 +18,7 @@ class ProcedureRevision < ApplicationRecord has_many :dossiers, inverse_of: :revision, foreign_key: :revision_id - has_many :revision_types_de_champ, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision + has_many :revision_types_de_champ, -> { order(:position, :id) }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision has_many :revision_types_de_champ_public, -> { root.public_only.ordered }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision has_many :revision_types_de_champ_private, -> { root.private_only.ordered }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision has_many :types_de_champ, through: :revision_types_de_champ, source: :type_de_champ @@ -125,14 +125,13 @@ class ProcedureRevision < ApplicationRecord end def different_from?(revision) - types_de_champ != revision.types_de_champ || + revision_types_de_champ != revision.revision_types_de_champ || attestation_template != revision.attestation_template end def compare(revision) changes = [] - changes += compare_types_de_champ(types_de_champ_public, revision.types_de_champ_public) - changes += compare_types_de_champ(types_de_champ_private, revision.types_de_champ_private) + changes += compare_revision_types_de_champ(revision_types_de_champ, revision.revision_types_de_champ) changes += compare_attestation_template(attestation_template, revision.attestation_template) changes end @@ -266,43 +265,43 @@ class ProcedureRevision < ApplicationRecord changes end - def compare_types_de_champ(from_tdc, to_tdc) - if from_tdc == to_tdc + def compare_revision_types_de_champ(from_coordinates, to_coordinates) + if from_coordinates == to_coordinates [] else - from_h = from_tdc.index_by(&:stable_id) - to_h = to_tdc.index_by(&:stable_id) + from_h = from_coordinates.index_by(&:stable_id) + to_h = to_coordinates.index_by(&:stable_id) from_sids = from_h.keys to_sids = to_h.keys removed = (from_sids - to_sids).map do |sid| - { model: :type_de_champ, op: :remove, label: from_h[sid].libelle, private: from_h[sid].private?, position: from_sids.index(sid), stable_id: sid } + { model: :type_de_champ, 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| - { model: :type_de_champ, op: :add, label: to_h[sid].libelle, private: to_h[sid].private?, position: to_sids.index(sid), stable_id: sid } + { model: :type_de_champ, 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) 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| - { model: :type_de_champ, op: :move, label: from_h[sid].libelle, private: from_h[sid].private?, from: from_index, to: to_index, position: to_index, stable_id: sid } + .map { |sid| [sid, from_h[sid], to_h[sid]] } + .filter { |_, from, to| from.position != to.position } + .map do |sid, from, to| + { model: :type_de_champ, op: :move, label: from.libelle, private: from.private?, from: from.position, to: to.position, _position: to_sids.index(sid), stable_id: sid } 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 + compare_type_de_champ(from.type_de_champ, to.type_de_champ) + .each { |h| h[:_position] = to_sids.index(sid) } + end (removed + added + moved + changed) - .sort_by { |h| h[:position] } - .each { |h| h.delete(:position) } + .sort_by { |h| h[:_position] } + .each { |h| h.delete(:_position) } end end @@ -431,10 +430,6 @@ class ProcedureRevision < ApplicationRecord stable_id: from_type_de_champ.stable_id } 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 diff --git a/spec/models/procedure_revision_spec.rb b/spec/models/procedure_revision_spec.rb index 78fdf005a..eeee6f7ce 100644 --- a/spec/models/procedure_revision_spec.rb +++ b/spec/models/procedure_revision_spec.rb @@ -300,7 +300,7 @@ describe ProcedureRevision do let(:first_tdc) { draft.types_de_champ_public.first } let(:new_draft) { procedure.create_new_revision } - subject { procedure.active_revision.compare(new_draft) } + subject { procedure.active_revision.compare(new_draft.reload) } context 'when a type de champ is added' do let(:procedure) { create(:procedure) }