diff --git a/app/models/champ.rb b/app/models/champ.rb index a0732e657..dd49db425 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -215,6 +215,27 @@ class Champ < ApplicationRecord end end + def clone(dossier:, parent: nil) + kopy = deep_clone(only: [:data, :private, :row, :type, :value, :value_json, :external_id, :type_de_champ_id]) + + kopy.dossier = dossier + kopy.parent = parent if parent + kopy + end + + def clone_piece_justificative(kopy) + if piece_justificative_file.attached? + piece_justificative_file.open do |tempfile| + kopy.piece_justificative_file.attach({ + io: File.open(tempfile.path), + filename: piece_justificative_file.filename, + content_type: piece_justificative_file.content_type, + metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } + }) + end + end + end + private def champs_for_condition diff --git a/app/models/champs/carte_champ.rb b/app/models/champs/carte_champ.rb index 4f351ba0e..d0c83a519 100644 --- a/app/models/champs/carte_champ.rb +++ b/app/models/champs/carte_champ.rb @@ -115,6 +115,13 @@ class Champs::CarteChamp < Champ geo_areas.blank? end + def clone(dossier:, parent: nil) + kopy = super(dossier: dossier, parent: parent) + + kopy.geo_areas = geo_areas.map(&:dup) + kopy + end + private def selection_utilisateur_legacy_geometry diff --git a/app/models/champs/piece_justificative_champ.rb b/app/models/champs/piece_justificative_champ.rb index dfde878e5..6af1decd9 100644 --- a/app/models/champs/piece_justificative_champ.rb +++ b/app/models/champs/piece_justificative_champ.rb @@ -51,4 +51,11 @@ class Champs::PieceJustificativeChamp < Champ piece_justificative_file.service_url end end + + def clone(dossier:, parent: nil) + kopy = super(dossier: dossier, parent: parent) + + clone_piece_justificative(kopy) + kopy + end end diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb index 83d691ffc..62772608f 100644 --- a/app/models/champs/repetition_champ.rb +++ b/app/models/champs/repetition_champ.rb @@ -77,4 +77,13 @@ class Champs::RepetitionChamp < Champ ] + Dossier.champs_for_export(champs, types_de_champ) end end + + def clone(dossier:, parent: nil) + kopy = super(dossier: dossier, parent: parent) + + kopy.champs = champs.map do |champ_de_repetition| + champ_de_repetition.clone(dossier: dossier, parent: kopy) + end + kopy + end end diff --git a/app/models/champs/siret_champ.rb b/app/models/champs/siret_champ.rb index 2b527caf9..0c87e603c 100644 --- a/app/models/champs/siret_champ.rb +++ b/app/models/champs/siret_champ.rb @@ -27,4 +27,11 @@ class Champs::SiretChamp < Champ def mandatory_blank? mandatory? && Siret.new(siret: value).invalid? end + + def clone(dossier:, parent: nil) + kopy = super(dossier: dossier, parent: parent) + + kopy.etablissement = etablissement.dup + kopy + end end diff --git a/app/models/champs/titre_identite_champ.rb b/app/models/champs/titre_identite_champ.rb index e93da6768..1b4b2d56f 100644 --- a/app/models/champs/titre_identite_champ.rb +++ b/app/models/champs/titre_identite_champ.rb @@ -43,4 +43,11 @@ class Champs::TitreIdentiteChamp < Champ def for_api nil end + + def clone(dossier:, parent: nil) + kopy = super(dossier: dossier, parent: parent) + + clone_piece_justificative(kopy) + kopy + end end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index a3267af8c..63728f4f8 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -35,6 +35,7 @@ # updated_at :datetime # dossier_transfer_id :bigint # groupe_instructeur_id :bigint +# parent_dossier_id :bigint # revision_id :bigint # user_id :integer # @@ -413,7 +414,7 @@ class Dossier < ApplicationRecord delegate :siret, :siren, to: :etablissement, allow_nil: true delegate :france_connect_information, to: :user, allow_nil: true - before_save :build_default_champs, if: Proc.new { revision_id_was.nil? } + before_save :build_default_champs_for_new_dossier, if: Proc.new { revision_id_was.nil? && parent_dossier_id.nil? } before_save :update_search_terms after_save :send_web_hook @@ -473,8 +474,8 @@ class Dossier < ApplicationRecord self.private_search_terms = champs_private.flat_map(&:search_terms).compact.join(' ') end - def build_default_champs - revision.build_champs_public.each do |champ| + def build_default_champs_for_new_dossier + revision.build_champs.each do |champ| champs_public << champ end revision.build_champs_private.each do |champ| @@ -1178,6 +1179,23 @@ class Dossier < ApplicationRecord @sections[champ.parent || (champ.public? ? :public : :private)] end + def clone + cloned_dossier = deep_clone(only: [:autorisation_donnees, :user_id, :revision_id, :groupe_instructeur_id], + include: [:individual, :etablissement]) do |original, kopy| + if original.is_a?(Dossier) + kopy.parent_dossier_id = original.id + kopy.state = Dossier.states.fetch(:brouillon) + kopy.champs = original.champs.map { |champ| champ.clone(dossier: kopy) } + kopy.champs_private = kopy.revision.types_de_champ_private.map { |tdc| tdc.build_champ(revision: kopy.revision, dossier: kopy) } + end + end + + transaction do + cloned_dossier.save! + end + cloned_dossier + end + private def create_missing_traitemets diff --git a/db/migrate/20221107163131_add_parent_dossier_id_to_dossier.rb b/db/migrate/20221107163131_add_parent_dossier_id_to_dossier.rb new file mode 100644 index 000000000..1e830b695 --- /dev/null +++ b/db/migrate/20221107163131_add_parent_dossier_id_to_dossier.rb @@ -0,0 +1,5 @@ +class AddParentDossierIdToDossier < ActiveRecord::Migration[6.1] + def change + add_column :dossiers, :parent_dossier_id, :bigint + end +end diff --git a/db/schema.rb b/db/schema.rb index d92779c4d..6b3020205 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,8 +10,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_11_04_071959) do +ActiveRecord::Schema.define(version: 2022_11_07_163131) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -327,7 +327,8 @@ ActiveRecord::Schema.define(version: 2022_11_04_071959) do t.datetime "last_champ_updated_at" t.datetime "last_commentaire_updated_at" t.text "motivation" - t.text "private_search_terms" + t.bigint "parent_dossier_id" + t.string "private_search_terms" t.datetime "processed_at" t.bigint "revision_id" t.text "search_terms" diff --git a/spec/factories/dossier.rb b/spec/factories/dossier.rb index c37bb62f1..a5fa004df 100644 --- a/spec/factories/dossier.rb +++ b/spec/factories/dossier.rb @@ -103,6 +103,18 @@ FactoryBot.define do commentaires { [build(:commentaire), build(:commentaire)] } end + trait :with_invites do + invites { [build(:invite)] } + end + + trait :with_avis do + avis { [build(:avis)] } + end + + trait :with_dossier_operation_logs do + dossier_operation_logs { [build(:dossier_operation_log)] } + end + trait :followed do after(:create) do |dossier, _evaluator| g = create(:instructeur) diff --git a/spec/factories/procedure.rb b/spec/factories/procedure.rb index a7f6e61f2..0715b419f 100644 --- a/spec/factories/procedure.rb +++ b/spec/factories/procedure.rb @@ -279,6 +279,11 @@ FactoryBot.define do build(:type_de_champ_explication, procedure: procedure) end end + trait :with_carte do + after(:build) do |procedure, _evaluator| + build(:type_de_champ_carte, procedure: procedure) + end + end trait :published do aasm_state { :publiee } diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index 328fc621a..7f11a6cbd 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -1685,6 +1685,175 @@ describe Dossier do it { expect(dossier.spreadsheet_columns(types_de_champ: [])).to include(["État du dossier", "Brouillon"]) } end + describe '#clone' do + let(:procedure) { create(:procedure, :with_type_de_champ, :with_type_de_champ_private) } + let(:dossier) { create(:dossier, procedure: procedure) } + let(:new_dossier) { dossier.clone } + + context 'reset most attributes' do + it { expect(new_dossier.id).not_to eq(dossier.id) } + it { expect(new_dossier.api_entreprise_job_exceptions).to be_nil } + it { expect(new_dossier.archived).to be_falsey } + it { expect(new_dossier.brouillon_close_to_expiration_notice_sent_at).to be_nil } + it { expect(new_dossier.conservation_extension).to eq(0.seconds) } + it { expect(new_dossier.declarative_triggered_at).to be_nil } + it { expect(new_dossier.deleted_user_email_never_send).to be_nil } + it { expect(new_dossier.depose_at).to be_nil } + it { expect(new_dossier.en_construction_at).to be_nil } + it { expect(new_dossier.en_construction_close_to_expiration_notice_sent_at).to be_nil } + it { expect(new_dossier.en_instruction_at).to be_nil } + it { expect(new_dossier.for_procedure_preview).to be_falsey } + it { expect(new_dossier.groupe_instructeur_updated_at).to be_nil } + it { expect(new_dossier.hidden_at).to be_nil } + it { expect(new_dossier.hidden_by_administration_at).to be_nil } + it { expect(new_dossier.hidden_by_reason).to be_nil } + it { expect(new_dossier.hidden_by_user_at).to be_nil } + it { expect(new_dossier.identity_updated_at).to be_nil } + it { expect(new_dossier.last_avis_updated_at).to be_nil } + it { expect(new_dossier.last_champ_private_updated_at).to be_nil } + it { expect(new_dossier.last_champ_updated_at).to be_nil } + it { expect(new_dossier.last_commentaire_updated_at).to be_nil } + it { expect(new_dossier.motivation).to be_nil } + it { expect(new_dossier.private_search_terms).to eq("") } + it { expect(new_dossier.processed_at).to be_nil } + it { expect(new_dossier.search_terms).to match(dossier.user.email) } + it { expect(new_dossier.termine_close_to_expiration_notice_sent_at).to be_nil } + it { expect(new_dossier.dossier_transfer_id).to be_nil } + end + + context 'copies some attributes' do + it { expect(new_dossier.groupe_instructeur).to eq(dossier.groupe_instructeur) } + it { expect(new_dossier.autorisation_donnees).to eq(dossier.autorisation_donnees) } + it { expect(new_dossier.revision_id).to eq(dossier.revision_id) } + it { expect(new_dossier.user_id).to eq(dossier.user_id) } + end + + context 'forces some attributes' do + let(:dossier) { create(:dossier, :accepte) } + + it { expect(new_dossier.brouillon?).to eq(true) } + it { expect(new_dossier.parent_dossier_id).to eq(dossier.id) } + end + + context 'links type_de_champ' do + it { expect(new_dossier.types_de_champ.count).to eq(1) } + it { expect(new_dossier.types_de_champ).to eq(dossier.types_de_champ) } + it { expect(new_dossier.types_de_champ_private.count).to eq(1) } + it { expect(new_dossier.types_de_champ_private).to eq(dossier.types_de_champ_private) } + end + + context 'procedure with_individual' do + let(:procedure) { create(:procedure, :for_individual) } + it { expect(new_dossier.individual.slice(:nom, :prenom, :gender)).to eq(dossier.individual.slice(:nom, :prenom, :gender)) } + it { expect(new_dossier.individual.id).not_to eq(dossier.individual.id) } + end + + context 'procedure with etablissement' do + let(:dossier) { create(:dossier, :with_entreprise) } + it { expect(new_dossier.etablissement.slice(:siret)).to eq(dossier.etablissement.slice(:siret)) } + it { expect(new_dossier.etablissement.id).not_to eq(dossier.etablissement.id) } + end + + describe 'skips commentaires' do + let(:dossier) { create(:dossier, :with_commentaires) } + + it { expect(new_dossier.commentaires.count).not_to eq(dossier.commentaires.count) } + it { expect(new_dossier.commentaires.size).to eq(0) } + end + + describe 'skips invites' do + let(:dossier) { create(:dossier, :with_invites) } + + it { expect(new_dossier.invites.count).not_to eq(dossier.invites.count) } + it { expect(new_dossier.invites.count).to eq(0) } + end + + describe 'skips avis' do + let(:dossier) { create(:dossier, :with_avis) } + + it { expect(new_dossier.avis.count).not_to eq(dossier.avis.count) } + it { expect(new_dossier.avis.count).to eq(0) } + it { expect(new_dossier.experts.count).not_to eq(dossier.experts.count) } + it { expect(new_dossier.experts.count).to eq(0) } + end + + describe 'skips dossier_operation_logs' do + let(:dossier) { create(:dossier, :accepte, :with_dossier_operation_logs) } + + it { expect(new_dossier.dossier_operation_logs.count).not_to eq(dossier.dossier_operation_logs.count) } + it { expect(new_dossier.dossier_operation_logs.count).to eq(0) } + end + + describe 'champs' do + it { expect(new_dossier.id).not_to eq(dossier.id) } + + context 'public are duplicated' do + it { expect(new_dossier.champs.count).to eq(dossier.champs.count) } + it { expect(new_dossier.champs.ids).not_to eq(dossier.champs.ids) } + + it 'keeps champs.values' do + original_first_champ = dossier.champs.first + original_first_champ.update!(value: 'kthxbye') + + expect(new_dossier.champs.first.value).to eq(original_first_champ.value) + end + + context 'for Champs::Repetition with rows, original_champ.repetition and rows are duped' do + let(:dossier) { create(:dossier) } + let(:type_de_champ_repetition) { create(:type_de_champ_repetition, procedure: dossier.procedure) } + let(:champ_repetition) { create(:champ_repetition, type_de_champ: type_de_champ_repetition, dossier: dossier) } + before { dossier.champs << champ_repetition } + + it { expect(Champs::RepetitionChamp.where(dossier: new_dossier).first.champs.count).to eq(4) } + it { expect(Champs::RepetitionChamp.where(dossier: new_dossier).first.champs.ids).not_to eq(champ_repetition.champs.ids) } + end + + context 'for Champs::CarteChamp with geo areas, original_champ.geo_areas are duped' do + let(:dossier) { create(:dossier) } + let(:type_de_champ_carte) { create(:type_de_champ_carte, procedure: dossier.procedure) } + let(:geo_area) { create(:geo_area, :selection_utilisateur, :polygon) } + let(:champ_carte) { create(:champ_carte, type_de_champ: type_de_champ_carte, geo_areas: [geo_area]) } + before { dossier.champs << champ_carte } + + it { expect(Champs::CarteChamp.where(dossier: new_dossier).first.geo_areas.count).to eq(1) } + it { expect(Champs::CarteChamp.where(dossier: new_dossier).first.geo_areas.ids).not_to eq(champ_carte.geo_areas.ids) } + end + + context 'for Champs::SiretChamp, original_champ.etablissement is duped' do + let(:dossier) { create(:dossier) } + let(:type_de_champs_siret) { create(:type_de_champ_siret, procedure: dossier.procedure) } + let(:etablissement) { create(:etablissement) } + let(:champ_siret) { create(:champ_siret, type_de_champ: type_de_champs_siret, etablissement: create(:etablissement)) } + before { dossier.champs << champ_siret } + + it { expect(Champs::SiretChamp.where(dossier: dossier).first.etablissement).not_to be_nil } + it { expect(Champs::SiretChamp.where(dossier: new_dossier).first.etablissement.id).not_to eq(champ_siret.etablissement.id) } + end + + context 'for Champs::PieceJustificative, original_champ.piece_justificative_file is duped' do + let(:dossier) { create(:dossier) } + let(:champ_piece_justificative) { create(:champ_piece_justificative, dossier_id: dossier.id) } + before { dossier.champs << champ_piece_justificative } + it { expect(Champs::PieceJustificativeChamp.where(dossier: dossier).first.piece_justificative_file).not_to be_nil } + it { expect(Champs::PieceJustificativeChamp.where(dossier: new_dossier).first.piece_justificative_file.blob.id).not_to eq(champ_piece_justificative.piece_justificative_file.blob.id) } + end + end + + context 'private are renewd' do + it { expect(new_dossier.champs_private.count).to eq(dossier.champs_private.count) } + it { expect(new_dossier.champs_private.ids).not_to eq(dossier.champs_private.ids) } + + it 'reset champs private values' do + original_first_champs_private = dossier.champs_private.first + original_first_champs_private.update!(value: 'kthxbye') + + expect(new_dossier.champs_private.first.value).not_to eq(original_first_champs_private.value) + expect(new_dossier.champs_private.first.value).to eq(nil) + end + end + end + end + describe '#processed_in_month' do include ActiveSupport::Testing::TimeHelpers