Merge branch 'main' into etq-usager-bouton-jdma

This commit is contained in:
Benoit Queyron 2024-06-11 18:09:01 +02:00 committed by GitHub
commit 97ef01b075
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
170 changed files with 4076 additions and 1251 deletions

View file

@ -88,10 +88,6 @@ class Champ < ApplicationRecord
parent_id.present?
end
def stable_id_with_row
[row_id, stable_id].compact
end
# used for the `required` html attribute
# check visibility to avoid hidden required input
# which prevent the form from being sent.

View file

@ -21,6 +21,10 @@ module ChampConditionalConcern
end
end
def reset_visible # recompute after a dossier update
remove_instance_variable :@visible if instance_variable_defined? :@visible
end
private
def champs_for_condition

View file

@ -22,7 +22,7 @@ module DossierRebaseConcern
end
def pending_changes
procedure.published_revision.present? ? revision.compare(procedure.published_revision) : []
procedure.published_revision.present? ? revision.compare_types_de_champ(procedure.published_revision) : []
end
def can_rebase_mandatory_change?(stable_id)

View file

@ -307,7 +307,8 @@ module TagsSubstitutionConcern
def format_date(date)
if date.present?
date.strftime('%d/%m/%Y')
format = defined?(self.class::FORMAT_DATE) ? self.class::FORMAT_DATE : '%d/%m/%Y'
date.strftime(format)
else
''
end

View file

@ -59,7 +59,7 @@ class Dossier < ApplicationRecord
has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier
has_many :followers_instructeurs, through: :follows, source: :instructeur
has_many :previous_followers_instructeurs, -> { distinct }, through: :previous_follows, source: :instructeur
has_many :avis, inverse_of: :dossier, dependent: :destroy
has_many :avis, -> { order(:created_at) }, inverse_of: :dossier, dependent: :destroy
has_many :experts, through: :avis
has_many :traitements, -> { order(:processed_at) }, inverse_of: :dossier, dependent: :destroy do
def passer_en_construction(instructeur: nil, processed_at: Time.zone.now)
@ -156,7 +156,7 @@ class Dossier < ApplicationRecord
state :sans_suite
event :passer_en_construction, after: :after_passer_en_construction, after_commit: :after_commit_passer_en_construction do
transitions from: :brouillon, to: :en_construction
transitions from: :brouillon, to: :en_construction, guard: :can_passer_en_construction?
end
event :passer_en_instruction, after: :after_passer_en_instruction, after_commit: :after_commit_passer_en_instruction do
@ -558,8 +558,18 @@ class Dossier < ApplicationRecord
false
end
def blocked_with_pending_correction?
procedure.feature_enabled?(:blocking_pending_correction) && pending_correction?
end
def can_passer_en_construction?
return true if !revision.ineligibilite_enabled
!revision.ineligibilite_rules.compute(champs_for_revision(scope: :public))
end
def can_passer_en_instruction?
return false if procedure.feature_enabled?(:blocking_pending_correction) && pending_correction?
return false if blocked_with_pending_correction?
true
end
@ -932,6 +942,7 @@ class Dossier < ApplicationRecord
.map do |champ|
champ.errors.add(:value, :missing)
end
.each { errors.import(_1) }
end
def demander_un_avis!(avis)
@ -1006,7 +1017,6 @@ class Dossier < ApplicationRecord
else
columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
end
if procedure.chorusable? && procedure.chorus_configuration.complete?
columns += [
['Domaine Fonctionnel', procedure.chorus_configuration.domaine_fonctionnel&.fetch("code") { '' }],

View file

@ -7,6 +7,7 @@ class ExportTemplate < ApplicationRecord
validates_with ExportTemplateValidator
DOSSIER_STATE = Dossier.states.fetch(:en_construction)
FORMAT_DATE = "%Y-%m-%d"
def set_default_values
content["default_dossier_directory"] = tiptap_json("dossier-")
@ -48,7 +49,7 @@ class ExportTemplate < ApplicationRecord
def attachment_and_path(dossier, attachment, index: 0, row_index: nil, champ: nil)
[
attachment,
path(dossier, attachment, index, row_index, champ)
path(dossier, attachment, index:, row_index:, champ:)
]
end
@ -116,7 +117,7 @@ class ExportTemplate < ApplicationRecord
"#{render_attributes_for(content["pdf_name"], dossier)}.pdf"
end
def path(dossier, attachment, index, row_index, champ)
def path(dossier, attachment, index: 0, row_index: nil, champ: nil)
if attachment.name == 'pdf_export_for_instructeur'
return export_path(dossier)
end
@ -128,6 +129,8 @@ class ExportTemplate < ApplicationRecord
'messagerie'
when 'Avis'
'avis'
when 'Attestation', 'Etablissement'
'pieces_justificatives'
else
# for attachment
return attachment_path(dossier, attachment, index, row_index, champ)

View file

@ -259,13 +259,19 @@ class Procedure < ApplicationRecord
validates :lien_dpo, url: { no_local: true, allow_blank: true, accept_email: true }
validates :draft_types_de_champ_public,
'types_de_champ/condition': true,
'types_de_champ/expression_reguliere': true,
'types_de_champ/header_section_consistency': true,
'types_de_champ/no_empty_block': true,
'types_de_champ/no_empty_drop_down': true,
on: :publication
on: [:types_de_champ_public_editor, :publication]
validates :draft_types_de_champ_private,
'types_de_champ/condition': true,
'types_de_champ/header_section_consistency': true,
'types_de_champ/no_empty_block': true,
'types_de_champ/no_empty_drop_down': true,
on: :publication
on: [:types_de_champ_private_editor, :publication]
validate :check_juridique, on: [:create, :publication]
@ -287,7 +293,7 @@ class Procedure < ApplicationRecord
validates_with MonAvisEmbedValidator
validates_associated :draft_revision, on: :publication
validate :validates_associated_draft_revision_with_context
validates_associated :initiated_mail, on: :publication
validates_associated :received_mail, on: :publication
validates_associated :closed_mail, on: :publication
@ -425,11 +431,15 @@ class Procedure < ApplicationRecord
def draft_changed?
preload_draft_and_published_revisions
!brouillon? && published_revision.different_from?(draft_revision) && revision_changes.present?
!brouillon? && (types_de_champ_revision_changes.present? || ineligibilite_rules_revision_changes.present?)
end
def revision_changes
published_revision.compare(draft_revision)
def types_de_champ_revision_changes
published_revision.compare_types_de_champ(draft_revision)
end
def ineligibilite_rules_revision_changes
published_revision.compare_ineligibilite_rules(draft_revision)
end
def preload_draft_and_published_revisions
@ -553,6 +563,7 @@ class Procedure < ApplicationRecord
procedure.closing_notification_brouillon = false
procedure.closing_notification_en_cours = false
procedure.template = false
procedure.monavis_embed = nil
if !procedure.valid?
procedure.errors.attribute_names.each do |attribute|
@ -1014,6 +1025,13 @@ class Procedure < ApplicationRecord
private
def validates_associated_draft_revision_with_context
return if draft_revision.blank?
return if draft_revision.validate(validation_context)
draft_revision.errors.map { errors.import(_1) }
end
def validate_auto_archive_on_in_the_future
return if auto_archive_on.nil?
return if auto_archive_on.future?

View file

@ -14,7 +14,7 @@ ProcedureDetail = Struct.new(:id, :libelle, :published_at, :aasm_state, :estimat
end
def parsed_latest_zone_labels
# Replace curly braces with square brackets to make it a valid JSON array
return [] if latest_zone_labels.nil? || latest_zone_labels.strip.empty?
JSON.parse(latest_zone_labels.tr('{', '[').tr('}', ']'))
rescue JSON::ParserError
[]

View file

@ -1,4 +1,5 @@
class ProcedureRevision < ApplicationRecord
include Logic
self.implicit_order_column = :created_at
belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false
belongs_to :dossier_submitted_message, inverse_of: :revisions, optional: true, dependent: :destroy
@ -17,12 +18,19 @@ class ProcedureRevision < ApplicationRecord
scope :ordered, -> { order(:created_at) }
validate :conditions_are_valid?
validate :header_sections_are_valid?
validate :expressions_regulieres_are_valid?
validates :ineligibilite_message, presence: true, if: -> { ineligibilite_enabled? }
delegate :path, to: :procedure, prefix: true
validate :ineligibilite_rules_are_valid?,
on: [:ineligibilite_rules_editor, :publication]
validates :ineligibilite_message,
presence: true,
if: -> { ineligibilite_enabled? },
on: [:ineligibilite_rules_editor, :publication]
serialize :ineligibilite_rules, LogicSerializer
def build_champs_public
# reload: it can be out of sync in test if some tdcs are added wihtout using add_tdc
types_de_champ_public.reload.map(&:build_champ)
@ -140,16 +148,18 @@ class ProcedureRevision < ApplicationRecord
!draft?
end
def different_from?(revision)
revision_types_de_champ != revision.revision_types_de_champ
end
def compare(revision)
def compare_types_de_champ(revision)
changes = []
changes += compare_revision_types_de_champ(revision_types_de_champ, revision.revision_types_de_champ)
changes
end
def compare_ineligibilite_rules(revision)
changes = []
changes += compare_revision_ineligibilite_rules(revision)
changes
end
def dossier_for_preview(user)
dossier = Dossier
.create_with(autorisation_donnees: true)
@ -255,6 +265,10 @@ class ProcedureRevision < ApplicationRecord
types_de_champ_public.filter(&:routable?)
end
def conditionable_types_de_champ
types_de_champ_for(scope: :public).filter(&:conditionable?)
end
private
def compute_estimated_fill_duration
@ -322,6 +336,29 @@ class ProcedureRevision < ApplicationRecord
end
end
def compare_revision_ineligibilite_rules(new_revision)
from_ineligibilite_rules = ineligibilite_rules
to_ineligibilite_rules = new_revision.ineligibilite_rules
changes = []
if from_ineligibilite_rules.present? && to_ineligibilite_rules.blank?
changes << ProcedureRevisionChange::RemoveEligibiliteRuleChange
end
if from_ineligibilite_rules.blank? && to_ineligibilite_rules.present?
changes << ProcedureRevisionChange::AddEligibiliteRuleChange
end
if from_ineligibilite_rules != to_ineligibilite_rules
changes << ProcedureRevisionChange::UpdateEligibiliteRuleChange
end
if ineligibilite_message != new_revision.ineligibilite_message
changes << ProcedureRevisionChange::UpdateEligibiliteMessageChange
end
if ineligibilite_enabled != new_revision.ineligibilite_enabled
changes << (new_revision.ineligibilite_enabled ? ProcedureRevisionChange::EligibiliteEnabledChange : ProcedureRevisionChange::EligibiliteDisabledChange)
end
changes.map { _1.new(self, new_revision) }
end
def compare_type_de_champ(from_type_de_champ, to_type_de_champ, from_coordinates, to_coordinates)
changes = []
if from_type_de_champ.type_champ != to_type_de_champ.type_champ
@ -446,6 +483,13 @@ class ProcedureRevision < ApplicationRecord
changes
end
def ineligibilite_rules_are_valid?
if ineligibilite_rules
ineligibilite_rules.errors(types_de_champ_for(scope: :public).to_a)
.each { errors.add(:ineligibilite_rules, :invalid) }
end
end
def replace_type_de_champ_by_clone(coordinate)
cloned_type_de_champ = coordinate.type_de_champ.deep_clone do |original, kopy|
ClonePiecesJustificativesService.clone_attachments(original, kopy)
@ -453,48 +497,4 @@ class ProcedureRevision < ApplicationRecord
coordinate.update!(type_de_champ: cloned_type_de_champ)
cloned_type_de_champ
end
def conditions_are_valid?
public_tdcs = types_de_champ_public.to_a
.flat_map { _1.repetition? ? children_of(_1) : _1 }
public_tdcs
.map.with_index
.filter_map { |tdc, i| tdc.condition? ? [tdc, i] : nil }
.map do |tdc, i|
[tdc, tdc.condition.errors(public_tdcs.take(i))]
end
.filter { |_tdc, errors| errors.present? }
.each { |tdc, message| errors.add(:condition, message, type_de_champ: tdc) }
end
def header_sections_are_valid?
public_tdcs = types_de_champ_public.to_a
root_tdcs_errors = errors_for_header_sections_order(public_tdcs)
repetition_tdcs_errors = public_tdcs
.filter_map { _1.repetition? ? children_of(_1) : nil }
.map { errors_for_header_sections_order(_1) }
repetition_tdcs_errors + root_tdcs_errors
end
def expressions_regulieres_are_valid?
types_de_champ_public.to_a
.flat_map { _1.repetition? ? children_of(_1) : _1 }
.each do |tdc|
if tdc.expression_reguliere? && tdc.invalid_regexp?
errors.add(:expression_reguliere, type_de_champ: tdc)
end
end
end
def errors_for_header_sections_order(tdcs)
tdcs
.map.with_index
.filter_map { |tdc, i| tdc.header_section? ? [tdc, i] : nil }
.map { |tdc, i| [tdc, tdc.check_coherent_header_level(tdcs.take(i))] }
.filter { |_tdc, errors| errors.present? }
.each { |tdc, message| errors.add(:header_section, message, type_de_champ: tdc) }
end
end

View file

@ -1,17 +1,19 @@
class ProcedureRevisionChange
attr_reader :type_de_champ
def initialize(type_de_champ)
@type_de_champ = type_de_champ
class TypeDeChange
attr_reader :type_de_champ
def initialize(type_de_champ)
@type_de_champ = type_de_champ
end
def label = @type_de_champ.libelle
def stable_id = @type_de_champ.stable_id
def private? = @type_de_champ.private?
def child? = @type_de_champ.child?
def to_h = { op:, stable_id:, label:, private: private? }
end
def label = @type_de_champ.libelle
def stable_id = @type_de_champ.stable_id
def private? = @type_de_champ.private?
def child? = @type_de_champ.child?
def to_h = { op:, stable_id:, label:, private: private? }
class AddChamp < ProcedureRevisionChange
class AddChamp < TypeDeChange
def initialize(type_de_champ)
super(type_de_champ)
end
@ -23,7 +25,7 @@ class ProcedureRevisionChange
def to_h = super.merge(mandatory: mandatory?)
end
class RemoveChamp < ProcedureRevisionChange
class RemoveChamp < TypeDeChange
def initialize(type_de_champ)
super(type_de_champ)
end
@ -32,7 +34,7 @@ class ProcedureRevisionChange
def can_rebase?(dossier = nil) = true
end
class MoveChamp < ProcedureRevisionChange
class MoveChamp < TypeDeChange
attr_reader :from, :to
def initialize(type_de_champ, from, to)
@ -46,7 +48,7 @@ class ProcedureRevisionChange
def to_h = super.merge(from:, to:)
end
class UpdateChamp < ProcedureRevisionChange
class UpdateChamp < TypeDeChange
attr_reader :attribute, :from, :to
def initialize(type_de_champ, attribute, from, to)
@ -75,4 +77,48 @@ class ProcedureRevisionChange
end
end
end
class EligibiliteRulesChange
attr_reader :previous_revision, :new_revision
def initialize(previous_revision, new_revision)
@previous_revision = previous_revision
@new_revision = new_revision
@previous_ineligibilite_rules = @previous_revision.ineligibilite_rules
@new_ineligibilite_rules = @new_revision.ineligibilite_rules
end
def i18n_params
{
previous_condition: @previous_ineligibilite_rules&.to_s(previous_revision.types_de_champ.filter { @previous_ineligibilite_rules.sources.include? _1.stable_id }),
new_condition: @new_ineligibilite_rules&.to_s(new_revision.types_de_champ.filter { @new_ineligibilite_rules.sources.include? _1.stable_id })
}
end
end
class AddEligibiliteRuleChange < EligibiliteRulesChange
def op = :add
end
class RemoveEligibiliteRuleChange < EligibiliteRulesChange
def op = :remove
end
class UpdateEligibiliteRuleChange < EligibiliteRulesChange
def op = :update
end
class EligibiliteEnabledChange < EligibiliteRulesChange
def op = :enabled
def i18n_params = {}
end
class EligibiliteDisabledChange < EligibiliteRulesChange
def op = :disabled
def i18n_params = {}
end
class UpdateEligibiliteMessageChange < EligibiliteRulesChange
def op = :message_updated
def i18n_params = { ineligibilite_message: @new_revision.ineligibilite_message }
end
end

View file

@ -75,4 +75,8 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
def used_by_routing_rules?
stable_id.in?(procedure.stable_ids_used_by_routing_rules)
end
def used_by_ineligibilite_rules?
revision.ineligibilite_enabled? && stable_id.in?(revision.ineligibilite_rules&.sources || [])
end
end

View file

@ -505,15 +505,15 @@ class TypeDeChamp < ApplicationRecord
end
def check_coherent_header_level(upper_tdcs)
errs = []
previous_level = previous_section_level(upper_tdcs)
current_level = header_section_level_value.to_i
difference = current_level - previous_level
if current_level > previous_level && difference != 1
errs << I18n.t('activerecord.errors.type_de_champ.attributes.header_section_level.gap_error', level: current_level - previous_level - 1)
I18n.t('activerecord.errors.type_de_champ.attributes.header_section_level.gap_error', level: current_level - previous_level - 1)
else
nil
end
errs
end
def current_section_level(revision)
@ -657,6 +657,10 @@ class TypeDeChamp < ApplicationRecord
type_champ.in?(ROUTABLE_TYPES)
end
def conditionable?
Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(type_champ)
end
def invalid_regexp?
self.errors.delete(:expression_reguliere)
self.errors.delete(:expression_reguliere_exemple_text)