From 0c7bc6b5556de6a3e02610d32cb405d1caece17c Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 27 Mar 2024 11:31:41 +0100 Subject: [PATCH] feat(dossier): add methods to upsert champ values --- app/models/concerns/dossier_champs_concern.rb | 75 +++++ .../concerns/dossier_prefillable_concern.rb | 4 - .../concern/dossier_champs_concern_spec.rb | 294 ++++++++++++++++++ spec/models/dossier_spec.rb | 35 --- 4 files changed, 369 insertions(+), 39 deletions(-) create mode 100644 spec/models/concern/dossier_champs_concern_spec.rb diff --git a/app/models/concerns/dossier_champs_concern.rb b/app/models/concerns/dossier_champs_concern.rb index 9bd0cbd5c..ab5a7545e 100644 --- a/app/models/concerns/dossier_champs_concern.rb +++ b/app/models/concerns/dossier_champs_concern.rb @@ -39,6 +39,46 @@ module DossierChampsConcern end end + def find_type_de_champ_by_stable_id(stable_id, scope = nil) + case scope + when :public + revision.types_de_champ.public_only + when :private + revision.types_de_champ.private_only + else + revision.types_de_champ + end.find_by!(stable_id:) + end + + def champs_for_prefill(stable_ids) + revision + .types_de_champ + .filter { _1.stable_id.in?(stable_ids) } + .filter { !revision.child?(_1) } + .map { champ_for_update(_1, nil) } + end + + def champ_for_update(type_de_champ, row_id) + champ, attributes = champ_with_attributes_for_update(type_de_champ, row_id) + champ.assign_attributes(attributes) + champ + end + + def update_champs_attributes(attributes, scope) + # TODO: remove after one deploy + if attributes.present? && attributes.values.filter { _1.key?(:with_public_id) }.empty? + assign_attributes("champs_#{scope}_all_attributes".to_sym => attributes) + @champs_by_public_id = nil + return + end + + champs_attributes = attributes.to_h.map do |public_id, attributes| + champ_attributes_by_public_id(public_id, attributes, scope) + end + + assign_attributes(champs_attributes:) + end + private def champs_by_public_id @@ -53,4 +93,39 @@ module DossierChampsConcern champ end end + + def champ_attributes_by_public_id(public_id, attributes, scope) + stable_id, row_id = public_id.split('-') + type_de_champ = find_type_de_champ_by_stable_id(stable_id, scope) + champ_with_attributes_for_update(type_de_champ, row_id).last.merge(attributes) + end + + def champ_with_attributes_for_update(type_de_champ, row_id) + attributes = type_de_champ.params_for_champ + # TODO: Once we have the right index in place, we should change this to use `create_or_find_by` instead of `find_or_create_by` + champ = champs + .create_with(type_de_champ:, **attributes) + .find_or_create_by!(stable_id: type_de_champ.stable_id, row_id:) + + attributes[:id] = champ.id + + # Needed when a revision change the champ type in this case, we reset the champ data + if champ.type != attributes[:type] + attributes[:value] = nil + attributes[:value_json] = nil + attributes[:external_id] = nil + attributes[:data] = nil + end + + parent = revision.parent_of(type_de_champ) + if parent.present? + attributes[:parent] = champs.find { _1.type_de_champ_id == parent.id } + else + attributes[:parent] = nil + end + + @champs_by_public_id = nil + + [champ, attributes] + end end diff --git a/app/models/concerns/dossier_prefillable_concern.rb b/app/models/concerns/dossier_prefillable_concern.rb index bbcfdc8e1..d0c21a2bf 100644 --- a/app/models/concerns/dossier_prefillable_concern.rb +++ b/app/models/concerns/dossier_prefillable_concern.rb @@ -13,8 +13,4 @@ module DossierPrefillableConcern assign_attributes(attributes) save(validate: false) end - - def find_champs_by_stable_ids(stable_ids) - champs.joins(:type_de_champ).where(types_de_champ: { stable_id: stable_ids.compact.uniq }) - end end diff --git a/spec/models/concern/dossier_champs_concern_spec.rb b/spec/models/concern/dossier_champs_concern_spec.rb new file mode 100644 index 000000000..f0d2a0357 --- /dev/null +++ b/spec/models/concern/dossier_champs_concern_spec.rb @@ -0,0 +1,294 @@ +RSpec.describe DossierChampsConcern do + let(:procedure) do + create(:procedure, types_de_champ_public:, types_de_champ_private:) + end + let(:types_de_champ_public) do + [ + { type: :text, libelle: "Un champ text", stable_id: 99 }, + { type: :text, libelle: "Un autre champ text", stable_id: 991 }, + { type: :yes_no, libelle: "Un champ yes no", stable_id: 992 }, + { type: :repetition, libelle: "Un champ répétable", stable_id: 993, mandatory: true, children: [{ type: :text, libelle: 'Nom', stable_id: 994 }] } + ] + end + let(:types_de_champ_private) do + [ + { type: :text, libelle: "Une annotation", stable_id: 995 } + ] + end + let(:dossier) { create(:dossier, procedure:) } + + describe "#find_type_de_champ_by_stable_id(public)" do + subject { dossier.find_type_de_champ_by_stable_id(992, :public) } + + it { is_expected.to be_truthy } + end + + describe "#find_type_de_champ_by_stable_id(private)" do + subject { dossier.find_type_de_champ_by_stable_id(995, :private) } + + it { is_expected.to be_truthy } + end + + describe "#project_champ" do + let(:type_de_champ_repetition) { dossier.find_type_de_champ_by_stable_id(993) } + let(:type_de_champ_public) { dossier.find_type_de_champ_by_stable_id(99) } + let(:type_de_champ_private) { dossier.find_type_de_champ_by_stable_id(995) } + let(:row_ids) { dossier.project_champ(type_de_champ_repetition, nil).row_ids } + + context "public champ" do + let(:row_id) { nil } + subject { dossier.project_champ(type_de_champ_public, row_id) } + + it { expect(subject.persisted?).to be_truthy } + + context "in repetition" do + let(:type_de_champ_public) { dossier.find_type_de_champ_by_stable_id(994) } + let(:row_id) { row_ids.first } + + it { + expect(subject.persisted?).to be_truthy + expect(subject.row_id).to eq(row_id) + expect(subject.parent_id).not_to be_nil + } + end + + context "missing champ" do + before { dossier; Champs::TextChamp.destroy_all } + + it { + expect(subject.new_record?).to be_truthy + expect(subject.is_a?(Champs::TextChamp)).to be_truthy + } + + context "in repetition" do + let(:type_de_champ_public) { dossier.find_type_de_champ_by_stable_id(994) } + let(:row_id) { row_ids.first } + + it { + expect(subject.new_record?).to be_truthy + expect(subject.is_a?(Champs::TextChamp)).to be_truthy + expect(subject.row_id).to eq(row_id) + } + end + end + end + + context "private champ" do + subject { dossier.project_champ(type_de_champ_private, nil) } + + it { expect(subject.persisted?).to be_truthy } + + context "missing champ" do + before { dossier; Champs::TextChamp.destroy_all } + + it { + expect(subject.new_record?).to be_truthy + expect(subject.is_a?(Champs::TextChamp)).to be_truthy + } + end + end + end + + describe "#champs_for_export" do + subject { dossier.champs_for_export(dossier.revision.types_de_champ_public) } + + it { expect(subject.size).to eq(4) } + it { expect(subject.first).to eq(["Un champ text", nil]) } + end + + describe "#champs_for_prefill" do + subject { dossier.champs_for_prefill([991, 995]) } + + it { + expect(subject.size).to eq(2) + expect(subject.map(&:libelle)).to eq(["Une annotation", "Un autre champ text"]) + expect(subject.all?(&:persisted?)).to be_truthy + } + + context "missing champ" do + before { dossier; Champs::TextChamp.destroy_all } + + it { + expect(subject.size).to eq(2) + expect(subject.map(&:libelle)).to eq(["Une annotation", "Un autre champ text"]) + expect(subject.all?(&:persisted?)).to be_truthy + } + end + end + + describe "#champ_for_update" do + let(:type_de_champ_repetition) { dossier.find_type_de_champ_by_stable_id(993) } + let(:type_de_champ_public) { dossier.find_type_de_champ_by_stable_id(99) } + let(:type_de_champ_private) { dossier.find_type_de_champ_by_stable_id(995) } + let(:row_ids) { dossier.project_champ(type_de_champ_repetition, nil).row_ids } + let(:row_id) { nil } + + context "public champ" do + subject { dossier.champ_for_update(type_de_champ_public, row_id) } + + it { + expect(subject.persisted?).to be_truthy + expect(subject.row_id).to eq(row_id) + } + + context "in repetition" do + let(:type_de_champ_public) { dossier.find_type_de_champ_by_stable_id(994) } + let(:row_id) { row_ids.first } + + it { + expect(subject.persisted?).to be_truthy + expect(subject.row_id).to eq(row_id) + expect(subject.parent_id).not_to be_nil + } + end + + context "missing champ" do + before { dossier; Champs::TextChamp.destroy_all } + + it { + expect(subject.persisted?).to be_truthy + expect(subject.is_a?(Champs::TextChamp)).to be_truthy + } + + context "in repetition" do + let(:type_de_champ_public) { dossier.find_type_de_champ_by_stable_id(994) } + let(:row_id) { row_ids.first } + + it { + expect(subject.persisted?).to be_truthy + expect(subject.is_a?(Champs::TextChamp)).to be_truthy + expect(subject.row_id).to eq(row_id) + expect(subject.parent_id).not_to be_nil + } + end + end + end + + context "private champ" do + subject { dossier.champ_for_update(type_de_champ_private, row_id) } + + it { + expect(subject.persisted?).to be_truthy + expect(subject.row_id).to eq(row_id) + } + end + end + + describe "#update_champs_attributes(public)" do + let(:type_de_champ_repetition) { dossier.find_type_de_champ_by_stable_id(993) } + let(:row_ids) { dossier.project_champ(type_de_champ_repetition, nil).row_ids } + let(:row_id) { row_ids.first } + + let(:attributes) do + { + "99" => { value: "Hello", with_public_id: true }, + "991" => { value: "World", with_public_id: true }, + "994-#{row_id}" => { value: "Greer", with_public_id: true } + } + end + + let(:champ_99) { dossier.project_champ(dossier.find_type_de_champ_by_stable_id(99), nil) } + let(:champ_991) { dossier.project_champ(dossier.find_type_de_champ_by_stable_id(991), nil) } + let(:champ_994) { dossier.project_champ(dossier.find_type_de_champ_by_stable_id(994), row_id) } + + subject { dossier.update_champs_attributes(attributes, :public) } + + it { + subject + expect(dossier.champs.any?(&:changed_for_autosave?)).to be_truthy + expect(champ_99.changed?).to be_truthy + expect(champ_991.changed?).to be_truthy + expect(champ_994.changed?).to be_truthy + expect(champ_99.value).to eq("Hello") + expect(champ_991.value).to eq("World") + expect(champ_994.value).to eq("Greer") + } + + context "missing champs" do + before { dossier; Champs::TextChamp.destroy_all; } + + it { + subject + expect(dossier.champs.any?(&:changed_for_autosave?)).to be_truthy + expect(champ_99.changed?).to be_truthy + expect(champ_991.changed?).to be_truthy + expect(champ_994.changed?).to be_truthy + expect(champ_99.value).to eq("Hello") + expect(champ_991.value).to eq("World") + expect(champ_994.value).to eq("Greer") + } + end + + context 'legacy attributes' do + let(:attributes) do + { + champ_99.id => { value: "Hello", id: champ_99.id }, + champ_991.id => { value: "World", id: champ_991.id }, + champ_994.id => { value: "Greer", id: champ_994.id } + } + end + + let(:champ_99_updated) { dossier.project_champ(dossier.find_type_de_champ_by_stable_id(99), nil) } + let(:champ_991_updated) { dossier.project_champ(dossier.find_type_de_champ_by_stable_id(991), nil) } + let(:champ_994_updated) { dossier.project_champ(dossier.find_type_de_champ_by_stable_id(994), row_id) } + + it { + subject + expect(dossier.champs_public_all.any?(&:changed_for_autosave?)).to be_truthy + dossier.save + dossier.reload + expect(champ_99_updated.value).to eq("Hello") + expect(champ_991_updated.value).to eq("World") + expect(champ_994_updated.value).to eq("Greer") + } + end + end + + describe "#update_champs_attributes(private)" do + let(:attributes) do + { + "995" => { value: "Hello", with_public_id: true } + } + end + + let(:annotation_995) { dossier.project_champ(dossier.find_type_de_champ_by_stable_id(995), nil) } + + subject { dossier.update_champs_attributes(attributes, :private) } + + it { + subject + expect(dossier.champs.any?(&:changed_for_autosave?)).to be_truthy + expect(annotation_995.changed?).to be_truthy + expect(annotation_995.value).to eq("Hello") + } + + context "missing champs" do + before { dossier; Champs::TextChamp.destroy_all; } + + it { + subject + expect(dossier.champs.any?(&:changed_for_autosave?)).to be_truthy + expect(annotation_995.changed?).to be_truthy + expect(annotation_995.value).to eq("Hello") + } + end + + context 'legacy attributes' do + let(:attributes) do + { + annotation_995.id => { value: "Hello", id: annotation_995.id } + } + end + + let(:annotation_995_updated) { dossier.project_champ(dossier.find_type_de_champ_by_stable_id(995), nil) } + + it { + subject + expect(dossier.champs_private_all.any?(&:changed_for_autosave?)).to be_truthy + dossier.save + dossier.reload + expect(annotation_995_updated.value).to eq("Hello") + } + end + end +end diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index 8a05debe4..f1fbe3ab5 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -2183,41 +2183,6 @@ describe Dossier, type: :model do end end - describe '#find_champs_by_stable_ids' do - let(:procedure) { create(:procedure, :published) } - let(:dossier) { create(:dossier, :brouillon, procedure: procedure) } - - subject { dossier.find_champs_by_stable_ids(stable_ids) } - - context 'when stable_ids is empty' do - let(:stable_ids) { [] } - - it { expect(subject).to match([]) } - end - - context 'when stable_ids contains nil or blank values' do - let(:stable_ids) { [nil, ""] } - - it { expect(subject).to match([]) } - end - - context 'when stable_ids contains present values' do - context 'when the dossier has no champ with the given stable ids' do - let(:stable_ids) { ['My Neighbor Totoro', 'Miyazaki'] } - - it { expect(subject).to match([]) } - end - - context 'when the dossier has champs with the given stable ids' do - let!(:type_de_champ_1) { create(:type_de_champ_text, procedure: procedure) } - let!(:type_de_champ_2) { create(:type_de_champ_textarea, procedure: procedure) } - let(:stable_ids) { [type_de_champ_1.stable_id, type_de_champ_2.stable_id] } - - it { expect(subject).to match_array(dossier.champs_public.joins(:type_de_champ).where(types_de_champ: { stable_id: stable_ids })) } - end - end - end - describe 'BatchOperation' do subject { build(:dossier) } it { is_expected.to belong_to(:batch_operation).optional }