diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7584990cb..9b0a6ffef 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -430,22 +430,6 @@ class ApplicationController < ActionController::Base controller_instance.try(:nav_bar_profile) end - # Extract a value from params based on the "path" - # - # params: { dossiers: { champs_public_attributes: { 1234 => { value: "hello" } } } } - # - # Usage: read_param_value("dossiers[champs_public_attributes][1234]", "value") - def read_param_value(path, name) - parts = path.split(/\[|\]\[|\]/) + [name] - parts.reduce(params) do |value, part| - if part.to_i != 0 - value[part.to_i] || value[part] - else - value[part] - end - end - end - def cast_bool(value) ActiveRecord::Type::Boolean.new.deserialize(value) end diff --git a/app/controllers/champs/carte_controller.rb b/app/controllers/champs/carte_controller.rb index dee8e5853..d2fe38e77 100644 --- a/app/controllers/champs/carte_controller.rb +++ b/app/controllers/champs/carte_controller.rb @@ -1,19 +1,15 @@ -class Champs::CarteController < ApplicationController - before_action :authenticate_logged_user! - +class Champs::CarteController < Champs::ChampController def index - @champ = policy_scope(Champ).find(params[:champ_id]) @focus = params[:focus].present? end def create - champ = policy_scope(Champ).find(params[:champ_id]) geo_area = if params_source == GeoArea.sources.fetch(:cadastre) - champ.geo_areas.find_by("properties->>'id' = :id", id: create_params_feature[:properties][:id]) + @champ.geo_areas.find_by("properties->>'id' = :id", id: create_params_feature[:properties][:id]) end if geo_area.nil? - geo_area = champ.geo_areas.build(source: params_source, properties: {}) + geo_area = @champ.geo_areas.build(source: params_source, properties: {}) if save_feature(geo_area, create_params_feature) render json: { feature: geo_area.to_feature }, status: :created @@ -26,8 +22,7 @@ class Champs::CarteController < ApplicationController end def update - champ = policy_scope(Champ).find(params[:champ_id]) - geo_area = champ.geo_areas.find(params[:id]) + geo_area = @champ.geo_areas.find(params[:id]) if save_feature(geo_area, update_params_feature) head :no_content @@ -37,9 +32,8 @@ class Champs::CarteController < ApplicationController end def destroy - champ = policy_scope(Champ).find(params[:champ_id]) - champ.geo_areas.find(params[:id]).destroy! - champ.touch + @champ.geo_areas.find(params[:id]).destroy! + @champ.touch head :no_content end @@ -82,7 +76,7 @@ class Champs::CarteController < ApplicationController geo_area.properties.merge!(feature[:properties]) end if geo_area.save - geo_area.champ.touch + @champ.touch true end end diff --git a/app/controllers/champs/champ_controller.rb b/app/controllers/champs/champ_controller.rb new file mode 100644 index 000000000..5c03ee863 --- /dev/null +++ b/app/controllers/champs/champ_controller.rb @@ -0,0 +1,26 @@ +class Champs::ChampController < ApplicationController + before_action :authenticate_logged_user! + before_action :set_champ + + private + + def find_champ + if params[:champ_id].present? + policy_scope(Champ) + .includes(:type_de_champ, dossier: :champs) + .find(params[:champ_id]) + else + dossier = policy_scope(Dossier).includes(:champs, revision: [:types_de_champ]).find(params[:dossier_id]) + type_de_champ = dossier.find_type_de_champ_by_stable_id(params[:stable_id]) + dossier.champ_for_update(type_de_champ, params_row_id) + end + end + + def params_row_id + params[:row_id] + end + + def set_champ + @champ = find_champ + end +end diff --git a/app/controllers/champs/options_controller.rb b/app/controllers/champs/options_controller.rb index 0338700aa..909a4f4be 100644 --- a/app/controllers/champs/options_controller.rb +++ b/app/controllers/champs/options_controller.rb @@ -1,13 +1,10 @@ -class Champs::OptionsController < ApplicationController +class Champs::OptionsController < Champs::ChampController include TurboChampsConcern - before_action :authenticate_logged_user! - def remove - champ = policy_scope(Champ).includes(:champs).find(params[:champ_id]) - champ.remove_option([params[:option]].compact, true) - champs = champ.private? ? champ.dossier.champs_private_all : champ.dossier.champs_public_all - @dossier = champ.private? ? nil : champ.dossier - @to_show, @to_hide, @to_update = champs_to_turbo_update({ params[:champ_id] => true }, champs) + @champ.remove_option([params[:option]].compact, true) + @dossier = @champ.private? ? nil : @champ.dossier + champs_attributes = params[:champ_id].present? ? { @champ.id => true } : { @champ.public_id => { with_public_id: true } } + @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_attributes, @champ.dossier.champs) end end diff --git a/app/controllers/champs/piece_justificative_controller.rb b/app/controllers/champs/piece_justificative_controller.rb index 20d865492..43d1b8134 100644 --- a/app/controllers/champs/piece_justificative_controller.rb +++ b/app/controllers/champs/piece_justificative_controller.rb @@ -1,7 +1,4 @@ -class Champs::PieceJustificativeController < ApplicationController - before_action :authenticate_logged_user! - before_action :set_champ - +class Champs::PieceJustificativeController < Champs::ChampController def show respond_to do |format| format.turbo_stream @@ -23,10 +20,6 @@ class Champs::PieceJustificativeController < ApplicationController private - def set_champ - @champ = policy_scope(Champ).find(params[:champ_id]) - end - def attach_piece_justificative save_succeed = nil diff --git a/app/controllers/champs/repetition_controller.rb b/app/controllers/champs/repetition_controller.rb index 998dc00cb..6a09dd275 100644 --- a/app/controllers/champs/repetition_controller.rb +++ b/app/controllers/champs/repetition_controller.rb @@ -1,8 +1,5 @@ -class Champs::RepetitionController < ApplicationController - before_action :authenticate_logged_user! - +class Champs::RepetitionController < Champs::ChampController def add - @champ = find_champ row = @champ.add_row(@champ.dossier.revision) @first_champ_id = row.map(&:focusable_input_id).compact.first @row_id = row.first&.row_id @@ -10,21 +7,14 @@ class Champs::RepetitionController < ApplicationController end def remove - @champ = find_champ - @champ.champs.where(row_id: params[:row_id]).destroy_all - @champ.reload - @row_id = params[:row_id] + @champ.remove_row(params[:row_id]) + @to_remove = "safe-row-selector-#{params[:row_id]}" + @to_focus = @champ.focusable_input_id || helpers.dom_id(@champ, :create_repetition) end private - def find_champ - if params[:champ_id].present? - policy_scope(Champ).includes(:champs).find(params[:champ_id]) - else - policy_scope(Champ) - .includes(:champs, :type_de_champ) - .find_by!(dossier_id: params[:dossier_id], type_de_champ: { stable_id: params[:stable_id] }) - end + def params_row_id + nil end end diff --git a/app/controllers/champs/rna_controller.rb b/app/controllers/champs/rna_controller.rb index a8f49f7bd..e96ab6671 100644 --- a/app/controllers/champs/rna_controller.rb +++ b/app/controllers/champs/rna_controller.rb @@ -1,9 +1,7 @@ -class Champs::RNAController < ApplicationController - before_action :authenticate_logged_user! - +class Champs::RNAController < Champs::ChampController def show - @champ = policy_scope(Champ).find(params[:champ_id]) - rna = read_param_value(@champ.input_name, 'value') + champs_attributes = params.dig(:dossier, :champs_public_attributes) || params.dig(:dossier, :champs_private_attributes) + rna = champs_attributes.values.first[:value] unless @champ.fetch_association!(rna) @error = @champ.association_fetch_error_key diff --git a/app/controllers/champs/siret_controller.rb b/app/controllers/champs/siret_controller.rb index 23e4ab2ee..83d98f60c 100644 --- a/app/controllers/champs/siret_controller.rb +++ b/app/controllers/champs/siret_controller.rb @@ -1,10 +1,9 @@ -class Champs::SiretController < ApplicationController - before_action :authenticate_logged_user! - +class Champs::SiretController < Champs::ChampController def show - @champ = policy_scope(Champ).find(params[:champ_id]) + champs_attributes = params.dig(:dossier, :champs_public_attributes) || params.dig(:dossier, :champs_private_attributes) + siret = champs_attributes.values.first[:value] - if @champ.fetch_etablissement!(read_param_value(@champ.input_name, 'value'), current_user) + if @champ.fetch_etablissement!(siret, current_user) @siret = @champ.etablissement.siret else @siret = @champ.etablissement_fetch_error_key diff --git a/app/controllers/concerns/turbo_champs_concern.rb b/app/controllers/concerns/turbo_champs_concern.rb index af5be0438..1a8fc819f 100644 --- a/app/controllers/concerns/turbo_champs_concern.rb +++ b/app/controllers/concerns/turbo_champs_concern.rb @@ -4,9 +4,14 @@ module TurboChampsConcern private def champs_to_turbo_update(params, champs) - champ_ids = params.keys.map(&:to_i) + to_update = if params.values.filter { _1.key?(:with_public_id) }.empty? + champ_ids = params.keys.map(&:to_i) + champs.filter { _1.id.in?(champ_ids) } + else + champ_public_ids = params.keys + champs.filter { _1.public_id.in?(champ_public_ids) } + end.filter { _1.refresh_after_update? || _1.forked_with_changes? } - to_update = champs.filter { _1.id.in?(champ_ids) && (_1.refresh_after_update? || _1.forked_with_changes?) } to_show, to_hide = champs.filter(&:conditional?) .partition(&:visible?) .map { champs_to_one_selector(_1 - to_update) } diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 51c45ebe6..212a6c8b0 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -273,8 +273,8 @@ module Instructeurs end def update_annotations - dossier_with_champs.assign_attributes(champs_private_params) - if dossier.champs_private_all.any?(&:changed?) + dossier_with_champs.update_champs_attributes(champs_private_attributes_params, :private) + if dossier.champs.any?(&:changed_for_autosave?) || dossier.champs_private_all.any?(&:changed_for_autosave?) # TODO remove second condition after one deploy dossier.last_champ_private_updated_at = Time.zone.now end @@ -282,7 +282,7 @@ module Instructeurs respond_to do |format| format.turbo_stream do - @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_private_params.fetch(:champs_private_all_attributes), dossier.champs_private_all) + @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_private_attributes_params, dossier.champs.filter(&:private?)) end end end @@ -294,7 +294,12 @@ module Instructeurs def annotation @dossier = dossier_with_champs(pj_template: false) - annotation = @dossier.champs_private_all.find(params[:annotation_id]) + annotation = if params[:with_public_id].present? + type_de_champ = @dossier.find_type_de_champ_by_stable_id(params[:annotation_id], :private) + @dossier.project_champ(type_de_champ, params[:row_id]) + else + @dossier.champs_private_all.find(params[:annotation_id]) + end respond_to do |format| format.turbo_stream do @@ -392,12 +397,36 @@ module Instructeurs end def champs_private_params - champs_params = params.require(:dossier).permit(champs_private_attributes: [ - :id, :value, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :code_departement, value: [], - champs_attributes: [:id, :_destroy, :value, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :code_departement, :feature, value: []] - ]) - champs_params[:champs_private_all_attributes] = champs_params.delete(:champs_private_attributes) || {} - champs_params + champ_attributes = [ + :id, + :value, + :value_other, + :external_id, + :primary_value, + :secondary_value, + :numero_allocataire, + :code_postal, + :identifiant, + :numero_fiscal, + :reference_avis, + :ine, + :piece_justificative_file, + :code_departement, + :accreditation_number, + :accreditation_birthdate, + :feature, + :with_public_id, + value: [] + ] + # Strong attributes do not support records (indexed hash); they only support hashes with + # static keys. We create a static hash based on the available keys. + public_ids = params.dig(:dossier, :champs_private_attributes)&.keys || [] + champs_private_attributes = public_ids.map { [_1, champ_attributes] }.to_h + params.require(:dossier).permit(champs_private_attributes:) + end + + def champs_private_attributes_params + champs_private_params.fetch(:champs_private_attributes) end def mark_demande_as_read diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 00234d794..06dd6d742 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -284,7 +284,7 @@ module Users end format.turbo_stream do - @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_params.fetch(:champs_public_all_attributes), dossier.champs_public_all) + @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_attributes_params, dossier.champs.filter(&:public?)) render :update, layout: false end end @@ -298,7 +298,7 @@ module Users respond_to do |format| format.turbo_stream do - @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_params.fetch(:champs_public_all_attributes), dossier.champs_public_all) + @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_attributes_params, dossier.champs.filter(&:public?)) render :update, layout: false end end @@ -310,7 +310,12 @@ module Users def champ @dossier = dossier_with_champs(pj_template: false) - champ = @dossier.champs_public_all.find(params[:champ_id]) + champ = if params[:with_public_id].present? + type_de_champ = @dossier.find_type_de_champ_by_stable_id(params[:champ_id], :public) + @dossier.project_champ(type_de_champ, params[:row_id]) + else + @dossier.champs_public_all.find(params[:champ_id]) + end respond_to do |format| format.turbo_stream do @@ -478,7 +483,7 @@ module Users end def champs_public_params - champs_params = params.require(:dossier).permit(champs_public_attributes: [ + champ_attributes = [ :id, :value, :value_other, @@ -496,10 +501,18 @@ module Users :accreditation_number, :accreditation_birthdate, :feature, + :with_public_id, value: [] - ]) - champs_params[:champs_public_all_attributes] = champs_params.delete(:champs_public_attributes) || {} - champs_params + ] + # Strong attributes do not support records (indexed hash); they only support hashes with + # static keys. We create a static hash based on the available keys. + public_ids = params.dig(:dossier, :champs_public_attributes)&.keys || [] + champs_public_attributes = public_ids.map { [_1, champ_attributes] }.to_h + params.require(:dossier).permit(champs_public_attributes:) + end + + def champs_public_attributes_params + champs_public_params.fetch(:champs_public_attributes) end def dossier_scope @@ -532,8 +545,8 @@ module Users end def update_dossier_and_compute_errors - @dossier.assign_attributes(champs_public_params) - if @dossier.champs_public_all.any?(&:changed_for_autosave?) + @dossier.update_champs_attributes(champs_public_attributes_params, :public) + if @dossier.champs.any?(&:changed_for_autosave?) || @dossier.champs_public_all.any?(&:changed_for_autosave?) # TODO remove second condition after one deploy @dossier.last_champ_updated_at = Time.zone.now end diff --git a/app/graphql/mutations/dossier_modifier_annotation.rb b/app/graphql/mutations/dossier_modifier_annotation.rb index 46f8bea3e..30dbd6f7e 100644 --- a/app/graphql/mutations/dossier_modifier_annotation.rb +++ b/app/graphql/mutations/dossier_modifier_annotation.rb @@ -39,10 +39,12 @@ module Mutations def find_annotation(dossier, annotation_id) stable_id, row_id = Champ.decode_typed_id(annotation_id) + type_de_champ = dossier.revision.types_de_champ + .private_only + .find_by(type_champ: annotation_type_champ, stable_id:) - Champ.joins(:type_de_champ).find_by(type_de_champ: { - type_champ: annotation_type_champ, stable_id:, private: true - }, private: true, row_id:, dossier:) + return nil if type_de_champ.nil? + dossier.champ_for_update(type_de_champ, row_id) end def annotation_type_champ diff --git a/app/graphql/mutations/dossier_modifier_annotation_ajouter_ligne.rb b/app/graphql/mutations/dossier_modifier_annotation_ajouter_ligne.rb index ef146581a..260cb2734 100644 --- a/app/graphql/mutations/dossier_modifier_annotation_ajouter_ligne.rb +++ b/app/graphql/mutations/dossier_modifier_annotation_ajouter_ligne.rb @@ -26,11 +26,13 @@ module Mutations private def find_annotation(dossier, annotation_id) - stable_id, row_id = Champ.decode_typed_id(annotation_id) + stable_id, _row_id = Champ.decode_typed_id(annotation_id) + type_de_champ = dossier.revision.types_de_champ + .private_only + .find_by(type_champ: TypeDeChamp.type_champs.fetch(:repetition), stable_id:) - Champ.joins(:type_de_champ).find_by(type_de_champ: { - type_champ: TypeDeChamp.type_champs.fetch(:repetition), stable_id:, private: true - }, private: true, row_id:, dossier:) + return nil if type_de_champ.nil? + dossier.project_champ(type_de_champ, nil) end end end diff --git a/app/models/champ.rb b/app/models/champ.rb index b3672d9f0..fc7ee4aa1 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -2,6 +2,9 @@ class Champ < ApplicationRecord include ChampConditionalConcern include ChampsValidateConcern + # TODO: remove after one deploy + attr_writer :with_public_id + belongs_to :dossier, inverse_of: false, touch: true, optional: false belongs_to :type_de_champ, inverse_of: :champ, optional: false belongs_to :parent, class_name: 'Champ', optional: true diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb index 6bb189c49..7035488db 100644 --- a/app/models/champs/repetition_champ.rb +++ b/app/models/champs/repetition_champ.rb @@ -25,6 +25,15 @@ class Champs::RepetitionChamp < Champ added_champs end + def remove_row(row_id) + dossier.champs.where(row_id:).destroy_all + dossier.champs.reload + end + + def focusable_input_id + rows.last&.first&.focusable_input_id + end + def blank? champs.empty? end diff --git a/app/models/concerns/dossier_champs_concern.rb b/app/models/concerns/dossier_champs_concern.rb new file mode 100644 index 000000000..ab5a7545e --- /dev/null +++ b/app/models/concerns/dossier_champs_concern.rb @@ -0,0 +1,131 @@ +module DossierChampsConcern + extend ActiveSupport::Concern + + def champs_for_revision(scope: nil, root: false) + champs_index = champs.group_by(&:stable_id) + # Due to some bad data we can have multiple copies of the same champ. Ignore extra copy. + .transform_values { _1.sort_by(&:id).uniq(&:row_id) } + + if scope.is_a?(TypeDeChamp) + revision + .children_of(scope) + .flat_map { champs_index[_1.stable_id] || [] } + .filter(&:child?) # TODO: remove once bad data (child champ without a row id) is cleaned + else + revision + .types_de_champ_for(scope:, root:) + .flat_map { champs_index[_1.stable_id] || [] } + end + end + + # Get all the champs values for the types de champ in the final list. + # Dossier might not have corresponding champ – display nil. + # To do so, we build a virtual champ when there is no value so we can call for_export with all indexes + def champs_for_export(types_de_champ, row_id = nil) + types_de_champ.flat_map do |type_de_champ| + champ = champ_for_export(type_de_champ, row_id) + type_de_champ.libelles_for_export.map do |(libelle, path)| + [libelle, champ&.for_export(path)] + end + end + end + + def project_champ(type_de_champ, row_id) + champ = champs_by_public_id[type_de_champ.public_id(row_id)] + if champ.nil? + type_de_champ.build_champ(dossier: self, row_id:) + else + champ + 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 + @champs_by_public_id ||= champs.sort_by(&:id).index_by(&:public_id) + end + + def champ_for_export(type_de_champ, row_id) + champ = champs_by_public_id[type_de_champ.public_id(row_id)] + if champ.blank? || !champ.visible? + nil + else + 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/app/models/dossier.rb b/app/models/dossier.rb index d2aafd943..334d883a7 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -9,6 +9,7 @@ class Dossier < ApplicationRecord include DossierSearchableConcern include DossierSectionsConcern include DossierStateConcern + include DossierChampsConcern enum state: { brouillon: 'brouillon', @@ -1036,18 +1037,6 @@ class Dossier < ApplicationRecord columns + champs_for_export(types_de_champ) end - # Get all the champs values for the types de champ in the final list. - # Dossier might not have corresponding champ – display nil. - # To do so, we build a virtual champ when there is no value so we can call for_export with all indexes - def champs_for_export(types_de_champ, row_id = nil) - types_de_champ.flat_map do |type_de_champ| - champ = champ_for_export(type_de_champ, row_id) - type_de_champ.libelles_for_export.map do |(libelle, path)| - [libelle, champ&.for_export(path)] - end - end - end - def linked_dossiers_for(instructeur_or_expert) dossier_ids = champs_for_revision.filter(&:dossier_link?).filter_map(&:value) instructeur_or_expert.dossiers.where(id: dossier_ids) @@ -1141,45 +1130,10 @@ class Dossier < ApplicationRecord user.france_connected_with_one_identity? end - def champs_for_revision(scope: nil, root: false) - champs_index = champs.group_by(&:stable_id) - # Due to some bad data we can have multiple copies of the same champ. Ignore extra copy. - .transform_values { _1.sort_by(&:id).uniq(&:row_id) } - - if scope.is_a?(TypeDeChamp) - revision - .children_of(scope) - .flat_map { champs_index[_1.stable_id] || [] } - .filter(&:child?) # TODO: remove once bad data (child champ without a row id) is cleaned - else - revision - .types_de_champ_for(scope:, root:) - .flat_map { champs_index[_1.stable_id] || [] } - end - end - def has_annotations? revision.revision_types_de_champ_private.present? end - def project_champ(type_de_champ, row_id) - champ = champs_by_public_id[type_de_champ.public_id(row_id)] - if champ.nil? - type_de_champ.build_champ(dossier: self, row_id:) - else - champ - end - end - - def champ_for_export(type_de_champ, row_id) - champ = champs_by_public_id[type_de_champ.public_id(row_id)] - if champ.blank? || !champ.visible? - nil - else - champ - end - end - def hide_info_with_accuse_lecture? procedure.accuse_lecture? && termine? && accuse_lecture_agreement_at.blank? end @@ -1190,10 +1144,6 @@ class Dossier < ApplicationRecord private - def champs_by_public_id - @champs_by_public_id ||= champs.sort_by(&:id).index_by(&:public_id) - end - def create_missing_traitemets if en_construction_at.present? && traitements.en_construction.empty? self.traitements.passer_en_construction(processed_at: en_construction_at) diff --git a/app/models/prefill_champs.rb b/app/models/prefill_champs.rb index f013c96d4..12d9c8ca0 100644 --- a/app/models/prefill_champs.rb +++ b/app/models/prefill_champs.rb @@ -23,7 +23,7 @@ class PrefillChamps .to_h dossier - .find_champs_by_stable_ids(value_by_stable_id.keys) + .champs_for_prefill(value_by_stable_id.keys) .map { |champ| [champ, value_by_stable_id[champ.stable_id]] } .map { |champ, value| PrefillValue.new(champ:, value:, dossier:) } end diff --git a/app/models/types_de_champ/prefill_repetition_type_de_champ.rb b/app/models/types_de_champ/prefill_repetition_type_de_champ.rb index 7ff8076ed..e90457e4b 100644 --- a/app/models/types_de_champ/prefill_repetition_type_de_champ.rb +++ b/app/models/types_de_champ/prefill_repetition_type_de_champ.rb @@ -54,14 +54,17 @@ class TypesDeChamp::PrefillRepetitionTypeDeChamp < TypesDeChamp::PrefillTypeDeCh def to_assignable_attributes return unless repetition.is_a?(Hash) - row = champ.rows[index] || champ.add_row(champ.dossier_revision) + row = champ.rows[index] || champ.add_row(revision) + row_id = row.first.row_id repetition.map do |key, value| next unless key.is_a?(String) && key.starts_with?("champ_") - subchamp = row.find { |champ| champ.stable_id == Champ.stable_id_from_typed_id(key) } - next unless subchamp + stable_id = Champ.stable_id_from_typed_id(key) + type_de_champ = revision.types_de_champ.find { _1.stable_id == stable_id } + next unless type_de_champ + subchamp = champ.dossier.champ_for_update(type_de_champ, row_id) TypesDeChamp::PrefillTypeDeChamp.build(subchamp.type_de_champ, revision).to_assignable_attributes(subchamp, value) end.compact end diff --git a/app/policies/dossier_policy.rb b/app/policies/dossier_policy.rb new file mode 100644 index 000000000..c50b43f0f --- /dev/null +++ b/app/policies/dossier_policy.rb @@ -0,0 +1,39 @@ +class DossierPolicy < ApplicationPolicy + # Scope for WRITING to a dossier. + # + # (If the need for a scope to READ a dossier emerges, we can implement another scope + # in this file, following this example: https://github.com/varvet/pundit/issues/368#issuecomment-196111115) + class Scope < ApplicationScope + def resolve + if user.blank? + return scope.none + end + + # The join must be the same for all elements of the WHERE clause. + # + # NB: here we want to do `.left_outer_joins(:invites, { :groupe_instructeur: :instructeurs })`, + # but for some reasons ActiveRecord <= 5.2 generates bogus SQL. Hence the manual version of it below. + joined_scope = scope + .joins('LEFT OUTER JOIN invites ON invites.dossier_id = dossiers.id OR invites.dossier_id = dossiers.editing_fork_origin_id') + .joins('LEFT OUTER JOIN groupe_instructeurs ON groupe_instructeurs.id = dossiers.groupe_instructeur_id') + .joins('LEFT OUTER JOIN assign_tos ON assign_tos.groupe_instructeur_id = groupe_instructeurs.id') + .joins('LEFT OUTER JOIN instructeurs ON instructeurs.id = assign_tos.instructeur_id') + + # Users can access public champs on their own dossiers. + resolved_scope = joined_scope.where(user_id: user.id) + + # Invited users can access public champs on dossiers they are invited to + invite_clause = joined_scope.where('invites.user_id': user.id) + resolved_scope = resolved_scope.or(invite_clause) + + if instructeur.present? + # Additionnaly, instructeurs can access private champs + # on dossiers they are allowed to instruct. + instructeur_clause = joined_scope.where('instructeurs.id': instructeur.id) + resolved_scope = resolved_scope.or(instructeur_clause) + end + + resolved_scope.or(joined_scope.where(for_procedure_preview: true)) + end + end +end diff --git a/app/views/champs/repetition/remove.turbo_stream.haml b/app/views/champs/repetition/remove.turbo_stream.haml index 5fb0fe0be..d7be234a4 100644 --- a/app/views/champs/repetition/remove.turbo_stream.haml +++ b/app/views/champs/repetition/remove.turbo_stream.haml @@ -1,6 +1,2 @@ -= turbo_stream.remove "safe-row-selector-#{@row_id}" - -- if @champ.rows.size > 0 && @champ.rows.last&.first&.present? - = turbo_stream.focus @champ.rows.last&.first.focusable_input_id -- else - = turbo_stream.focus dom_id(@champ, :create_repetition) += turbo_stream.remove @to_remove += turbo_stream.focus @to_focus diff --git a/config/routes.rb b/config/routes.rb index 0ab8203f2..caf5a3358 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -195,9 +195,21 @@ Rails.application.routes.draw do namespace :champs do post ':dossier_id/:stable_id/repetition', to: 'repetition#add', as: :repetition delete ':dossier_id/:stable_id/repetition', to: 'repetition#remove' - post ':champ_id/repetition', to: 'repetition#add' - delete ':champ_id/repetition', to: 'repetition#remove' + get ':dossier_id/:stable_id/siret', to: 'siret#show' + get ':dossier_id/:stable_id/rna', to: 'rna#show' + delete ':dossier_id/:stable_id/options', to: 'options#remove' + + get ':dossier_id/:stable_id/carte/features', to: 'carte#index' + post ':dossier_id/:stable_id/carte/features', to: 'carte#create' + patch ':dossier_id/:stable_id/carte/features/:id', to: 'carte#update' + delete ':dossier_id/:stable_id/carte/features/:id', to: 'carte#destroy' + + get ':dossier_id/:stable_id/piece_justificative', to: 'piece_justificative#show' + put ':dossier_id/:stable_id/piece_justificative', to: 'piece_justificative#update' + get ':dossier_id/:stable_id/piece_justificative/template', to: 'piece_justificative#template' + + # TODO: remove after migration is ower get ':champ_id/siret', to: 'siret#show', as: :siret get ':champ_id/rna', to: 'rna#show', as: :rna delete ':champ_id/options', to: 'options#remove', as: :options diff --git a/spec/controllers/champs/repetition_controller_spec.rb b/spec/controllers/champs/repetition_controller_spec.rb index 2f6fed04e..3b80b5c32 100644 --- a/spec/controllers/champs/repetition_controller_spec.rb +++ b/spec/controllers/champs/repetition_controller_spec.rb @@ -5,8 +5,9 @@ describe Champs::RepetitionController, type: :controller do before { sign_in dossier.user } it 'removes repetition' do - rows, repetitions = dossier.champs.partition { _1.parent_id.present? } - expect { delete :remove, params: { champ_id: repetitions.first.id, row_id: rows.first.row_id }, format: :turbo_stream } + rows, repetitions = dossier.champs.partition(&:child?) + repetition = repetitions.first + expect { delete :remove, params: { dossier_id: repetition.dossier, stable_id: repetition.stable_id, row_id: rows.first.row_id }, format: :turbo_stream } .to change { dossier.reload.champs.size }.from(3).to(1) end end diff --git a/spec/controllers/champs/rna_controller_spec.rb b/spec/controllers/champs/rna_controller_spec.rb index c85d49685..1c738f794 100644 --- a/spec/controllers/champs/rna_controller_spec.rb +++ b/spec/controllers/champs/rna_controller_spec.rb @@ -7,8 +7,8 @@ describe Champs::RNAController, type: :controller do let(:champ) { dossier.champs_public.first } let(:champs_public_attributes) do - champ_attributes = [] - champ_attributes[champ.id] = { value: rna } + champ_attributes = {} + champ_attributes[champ.public_id] = { value: rna } champ_attributes end let(:params) do diff --git a/spec/controllers/champs/siret_controller_spec.rb b/spec/controllers/champs/siret_controller_spec.rb index 67a1ac8de..4e5212e36 100644 --- a/spec/controllers/champs/siret_controller_spec.rb +++ b/spec/controllers/champs/siret_controller_spec.rb @@ -7,13 +7,14 @@ describe Champs::SiretController, type: :controller do let(:champ) { dossier.champs_public.first } let(:champs_public_attributes) do - champ_attributes = [] - champ_attributes[champ.id] = { value: siret } + champ_attributes = {} + champ_attributes[champ.public_id] = { value: siret } champ_attributes end let(:params) do { - champ_id: champ.id, + dossier_id: champ.dossier_id, + stable_id: champ.stable_id, dossier: { champs_public_attributes: champs_public_attributes } diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index 108c675ba..f4f065948 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -1005,25 +1005,25 @@ describe Instructeurs::DossiersController, type: :controller do dossier_id: dossier.id, dossier: { champs_private_attributes: { - '0': { - id: champ_multiple_drop_down_list.id, + champ_multiple_drop_down_list.public_id => { + with_public_id: true, value: ['', 'val1', 'val2'] }, - '1': { - id: champ_datetime.id, + champ_datetime.public_id => { + with_public_id: true, value: '2019-12-21T13:17' }, - '2': { - id: champ_linked_drop_down_list.id, + champ_linked_drop_down_list.public_id => { + with_public_id: true, primary_value: 'primary', secondary_value: 'secondary' }, - '3': { - id: champ_repetition.champs.first.id, + champ_repetition.champs.first.public_id => { + with_public_id: true, value: 'text' }, - '4': { - id: champ_drop_down_list.id, + champ_drop_down_list.public_id => { + with_public_id: true, value: '__other__', value_other: 'other value' } @@ -1074,12 +1074,11 @@ describe Instructeurs::DossiersController, type: :controller do end end - context 'with invalid champs_public (DecimalNumberChamp)' do - let(:types_de_champ_public) do - [ - { type: :decimal_number } - ] - end + after do + Timecop.return + end + + context "with new values for champs_private (legacy)" do let(:params) do { procedure_id: procedure.id, @@ -1095,9 +1094,63 @@ describe Instructeurs::DossiersController, type: :controller do } end + it 'update champs_private' do + patch :update_annotations, params: params, format: :turbo_stream + champ_datetime.reload + expect(champ_datetime.value).to eq(Time.zone.parse('2024-03-30T07:03:00').iso8601) + end + end + + context "without new values for champs_private" do + let(:params) do + { + procedure_id: procedure.id, + dossier_id: dossier.id, + dossier: { + champs_private_attributes: {}, + champs_public_attributes: { + champ_multiple_drop_down_list.public_id => { + with_public_id: true, + value: ['', 'val1', 'val2'] + } + } + } + } + end + + it { + expect(dossier.reload.last_champ_private_updated_at).to eq(nil) + expect(response).to have_http_status(200) + } + end + + context "with invalid champs_public (DecimalNumberChamp)" do + let(:types_de_champ_public) do + [ + { type: :decimal_number } + ] + end + + let(:champ_decimal_number) { dossier.champs_public.first } + + let(:params) do + { + procedure_id: procedure.id, + dossier_id: dossier.id, + dossier: { + champs_private_attributes: { + champ_datetime.public_id => { + with_public_id: true, + value: '2024-03-30T07:03' + } + } + } + } + end + it 'update champs_private' do too_long_float = '3.1415' - dossier.champs_public.first.update_column(:value, too_long_float) + champ_decimal_number.update_column(:value, too_long_float) patch :update_annotations, params: params, format: :turbo_stream champ_datetime.reload expect(champ_datetime.value).to eq(Time.zone.parse('2024-03-30T07:03:00').iso8601) diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb index 5253ba54c..ad99422a4 100644 --- a/spec/controllers/users/dossiers_controller_spec.rb +++ b/spec/controllers/users/dossiers_controller_spec.rb @@ -674,12 +674,12 @@ describe Users::DossiersController, type: :controller do dossier: { groupe_instructeur_id: dossier.groupe_instructeur_id, champs_public_attributes: { - first_champ.id => { - id: first_champ.id, + first_champ.public_id => { + with_public_id: true, value: value }, - piece_justificative_champ.id => { - id: piece_justificative_champ.id, + piece_justificative_champ.public_id => { + with_public_id: true, piece_justificative_file: file } } @@ -719,7 +719,7 @@ describe Users::DossiersController, type: :controller do { id: dossier.id, dossier: { - champs_public_attributes: { first_champ.id => { id: first_champ.id } } + champs_public_attributes: { first_champ.public_id => { with_public_id: true } } } } end @@ -747,7 +747,7 @@ describe Users::DossiersController, type: :controller do { id: dossier.id, dossier: { - champs_public_attributes: { first_champ.id => { id: first_champ.id, value: value } } + champs_public_attributes: { first_champ.public_id => { with_public_id: true, value: value } } } } end @@ -790,12 +790,12 @@ describe Users::DossiersController, type: :controller do dossier: { groupe_instructeur_id: dossier.groupe_instructeur_id, champs_public_attributes: { - first_champ.id => { - id: first_champ.id, + first_champ.public_id => { + with_public_id: true, value: value }, - piece_justificative_champ.id => { - id: piece_justificative_champ.id, + piece_justificative_champ.public_id => { + with_public_id: true, piece_justificative_file: file } } @@ -855,8 +855,8 @@ describe Users::DossiersController, type: :controller do id: dossier.id, dossier: { champs_public_attributes: { - piece_justificative_champ.id => { - id: piece_justificative_champ.id, + piece_justificative_champ.public_id => { + with_public_id: true, piece_justificative_file: file } } @@ -951,8 +951,8 @@ describe Users::DossiersController, type: :controller do id: dossier.id, dossier: { champs_public_attributes: { - first_champ.id => { - id: first_champ.id, + first_champ.public_id => { + with_public_id: true, value: value } } 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 } diff --git a/spec/policies/dossier_policy_spec.rb b/spec/policies/dossier_policy_spec.rb new file mode 100644 index 000000000..dc0a25aca --- /dev/null +++ b/spec/policies/dossier_policy_spec.rb @@ -0,0 +1,79 @@ +describe DossierPolicy do + let(:procedure) { create(:procedure, :with_type_de_champ, :with_type_de_champ_private) } + let(:dossier) { create(:dossier, procedure: procedure, user: dossier_owner) } + let(:dossier_owner) { create(:user) } + + let(:signed_in_user) { create(:user) } + let(:account) { { user: signed_in_user } } + + subject { Pundit.policy_scope(account, Dossier) } + + shared_examples_for 'they can access dossier' do + it { expect(subject.find_by(id: dossier.id)).to eq(dossier) } + end + + shared_examples_for 'they can’t access dossier' do + it { expect(subject.find_by(id: dossier.id)).to eq(nil) } + end + + context 'when an user only has user rights' do + context 'as the dossier owner' do + let(:signed_in_user) { dossier_owner } + + it_behaves_like 'they can access dossier' + end + + context 'as a person invited on the dossier' do + let(:invite) { create(:invite, :with_user, dossier: dossier) } + let(:signed_in_user) { invite.user } + + it_behaves_like 'they can access dossier' + end + + context 'as another user' do + let(:signed_in_user) { create(:user) } + + it_behaves_like 'they can’t access dossier' + end + end + + context 'when the user also has instruction rights' do + let(:instructeur) { create(:instructeur, user: signed_in_user) } + let(:account) { { user: signed_in_user, instructeur: instructeur } } + + context 'as the dossier instructeur and owner' do + let(:signed_in_user) { dossier_owner } + before { instructeur.assign_to_procedure(dossier.procedure) } + + it_behaves_like 'they can access dossier' + end + + context 'as the dossier instructeur (but not owner)' do + let(:signed_in_user) { create(:user) } + before { instructeur.assign_to_procedure(dossier.procedure) } + + it_behaves_like 'they can access dossier' + end + + context 'as an instructeur not assigned to the procedure' do + let(:signed_in_user) { create(:user) } + + it_behaves_like 'they can’t access dossier' + end + end + + context 'when the champ is on a forked dossier' do + let(:signed_in_user) { dossier_owner } + let(:origin) { create(:dossier, procedure: procedure, user: dossier_owner) } + let(:dossier) { origin.find_or_create_editing_fork(dossier_owner) } + + it_behaves_like 'they can access dossier' + + context 'when the user is invited on the origin dossier' do + let(:invite) { create(:invite, :with_user, dossier: origin) } + let(:signed_in_user) { invite.user } + + it_behaves_like 'they can access dossier' + end + end +end