demarches-normaliennes/app/models/procedure.rb

1035 lines
33 KiB
Ruby
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
class Procedure < ApplicationRecord
include ProcedureStatsConcern
include EncryptableConcern
include InitiationProcedureConcern
include ProcedureGroupeInstructeurAPIHackConcern
include ProcedureSVASVRConcern
include ProcedureChorusConcern
include PiecesJointesListConcern
include ColumnsConcern
include Discard::Model
self.discard_column = :hidden_at
default_scope -> { kept }
OLD_MAX_DUREE_CONSERVATION = 36
NEW_MAX_DUREE_CONSERVATION = Expired::DEFAULT_DOSSIER_RENTENTION_IN_MONTH
MIN_WEIGHT = 350000
DOSSIERS_COUNT_EXPIRING = 1.hour
attr_encrypted :api_particulier_token
has_many :revisions, -> { order(:id) }, class_name: 'ProcedureRevision', inverse_of: :procedure
belongs_to :draft_revision, class_name: 'ProcedureRevision', optional: false
belongs_to :published_revision, class_name: 'ProcedureRevision', optional: true
has_many :deleted_dossiers, dependent: :destroy
has_many :draft_types_de_champ_public, through: :draft_revision, source: :types_de_champ_public
has_many :draft_types_de_champ_private, through: :draft_revision, source: :types_de_champ_private
has_many :published_types_de_champ_public, through: :published_revision, source: :types_de_champ_public
has_many :published_types_de_champ_private, through: :published_revision, source: :types_de_champ_private
has_one :published_dossier_submitted_message, dependent: :destroy, through: :published_revision, source: :dossier_submitted_message
has_one :draft_dossier_submitted_message, dependent: :destroy, through: :draft_revision, source: :dossier_submitted_message
has_many :dossier_submitted_messages, through: :revisions, source: :dossier_submitted_message
has_many :experts_procedures, dependent: :destroy
has_many :experts, through: :experts_procedures
has_many :replaced_procedures, -> { with_discarded }, inverse_of: :replaced_by_procedure, class_name: "Procedure",
foreign_key: "replaced_by_procedure_id", dependent: :nullify
has_one :module_api_carto, dependent: :destroy
has_many :attestation_templates, dependent: :destroy
has_one :attestation_template_v1, -> { AttestationTemplate.v1 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure
has_many :attestation_templates_v2, -> { AttestationTemplate.v2 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure
has_one :attestation_template, -> { published }, dependent: :destroy, inverse_of: :procedure
belongs_to :parent_procedure, class_name: 'Procedure', optional: true
belongs_to :canonical_procedure, class_name: 'Procedure', optional: true
belongs_to :replaced_by_procedure, -> { with_discarded }, inverse_of: :replaced_procedures, class_name: "Procedure", optional: true
belongs_to :service, optional: true
belongs_to :zone, optional: true
has_and_belongs_to_many :zones
has_and_belongs_to_many :procedure_tags
has_many :bulk_messages, dependent: :destroy
def active_dossier_submitted_message
published_dossier_submitted_message || draft_dossier_submitted_message
end
def active_revision
brouillon? ? draft_revision : published_revision
end
def all_revisions_types_de_champ(parent: nil)
if brouillon?
if parent.nil?
TypeDeChamp.fillable
.joins(:revision_types_de_champ)
.where(revision_types_de_champ: { revision_id: draft_revision_id, parent_id: nil })
.order(:private, :position)
else
draft_revision.children_of(parent)
end
else
cache_key = ['all_revisions_types_de_champ', published_revision, parent].compact
Rails.cache.fetch(cache_key, expires_in: 1.month) { published_revisions_types_de_champ(parent) }
end
end
def types_de_champ_for_procedure_export
all_revisions_types_de_champ.not_repetition
end
def types_de_champ_for_tags
TypeDeChamp
.fillable
.joins(:revisions)
.where(procedure_revisions: brouillon? ? { id: draft_revision_id } : { procedure_id: id })
.where(revision_types_de_champ: { parent_id: nil })
.order(:created_at)
.distinct(:id)
end
def types_de_champ_public_for_tags
types_de_champ_for_tags.public_only
end
def types_de_champ_private_for_tags
types_de_champ_for_tags.private_only
end
def revisions_with_pending_dossiers
@revisions_with_pending_dossiers ||= begin
ids = dossiers
.where.not(revision_id: [draft_revision_id, published_revision_id].compact)
.state_en_construction_ou_instruction
.distinct(:revision_id)
.pluck(:revision_id)
ProcedureRevision.includes(revision_types_de_champ: [:type_de_champ]).where(id: ids)
end
end
has_many :administrateurs_procedures, dependent: :delete_all
has_many :administrateurs, through: :administrateurs_procedures, after_remove: -> (procedure, _admin) { procedure.validate! }
has_many :groupe_instructeurs, -> { order(:label) }, inverse_of: :procedure, dependent: :destroy
has_many :instructeurs, through: :groupe_instructeurs
has_many :export_templates, through: :groupe_instructeurs
has_many :active_groupe_instructeurs, -> { active }, class_name: 'GroupeInstructeur', inverse_of: false
has_many :closed_groupe_instructeurs, -> { closed }, class_name: 'GroupeInstructeur', inverse_of: false
# This relationship is used in following dossiers through. We can not use revisions relationship
# as order scope introduces invalid sql in some combinations.
has_many :unordered_revisions, class_name: 'ProcedureRevision', inverse_of: :procedure, dependent: :destroy
has_many :dossiers, through: :unordered_revisions, dependent: :restrict_with_exception
has_one :initiated_mail, class_name: "Mails::InitiatedMail", dependent: :destroy
has_one :received_mail, class_name: "Mails::ReceivedMail", dependent: :destroy
has_one :closed_mail, class_name: "Mails::ClosedMail", dependent: :destroy
has_one :refused_mail, class_name: "Mails::RefusedMail", dependent: :destroy
has_one :without_continuation_mail, class_name: "Mails::WithoutContinuationMail", dependent: :destroy
has_one :re_instructed_mail, class_name: "Mails::ReInstructedMail", dependent: :destroy
belongs_to :defaut_groupe_instructeur, class_name: 'GroupeInstructeur', inverse_of: false, optional: true
has_one_attached :logo
has_one_attached :notice
has_one_attached :deliberation
scope :brouillons, -> { where(aasm_state: :brouillon) }
scope :publiees, -> { where(aasm_state: :publiee) }
scope :publiees_ou_brouillons, -> { where(aasm_state: [:publiee, :brouillon]) }
scope :closes, -> { where(aasm_state: [:close, :depubliee]) }
scope :opendata, -> { where(opendata: true) }
scope :publiees_ou_closes, -> { where(aasm_state: [:publiee, :close, :depubliee]) }
scope :with_external_urls, -> { where.not(lien_notice: [nil, '']).or(where.not(lien_dpo: [nil, ''])) }
scope :publiques, -> do
publiees_ou_closes
.opendata
.where('estimated_dossiers_count >= ?', 4)
.where.not('lien_site_web LIKE ?', '%mail%')
.where.not('lien_site_web LIKE ?', '%intra%')
end
scope :by_libelle, -> { order(libelle: :asc) }
scope :created_during, -> (range) { where(created_at: range) }
scope :cloned_from_library, -> { where(cloned_from_library: true) }
scope :declarative, -> { where.not(declarative_with_state: nil) }
scope :discarded_expired, -> do
with_discarded
.discarded
.where('hidden_at < ?', 1.month.ago)
end
scope :for_api, -> {
includes(
:administrateurs,
:module_api_carto,
published_revision: [
:types_de_champ_private,
:types_de_champ_public
],
draft_revision: [
:types_de_champ_private,
:types_de_champ_public
]
)
}
enum declarative_with_state: {
en_instruction: 'en_instruction',
accepte: 'accepte'
}
enum closing_reason: {
internal_procedure: 'internal_procedure',
other: 'other'
}, _prefix: true
scope :for_api_v2, -> {
includes(:draft_revision, :published_revision, administrateurs: :user)
}
validates :libelle, presence: true, allow_blank: false, allow_nil: false
validates :description, presence: true, allow_blank: false, allow_nil: false
validates :administrateurs, presence: true
validates :lien_site_web, presence: true, if: :publiee?
validates :lien_notice, url: { no_local: true, allow_blank: true }
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: [: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: [:types_de_champ_private_editor, :publication]
validate :check_juridique, on: [:create, :publication]
validates :replaced_by_procedure_id, presence: true, if: :closing_reason_internal_procedure?
validates :path, presence: true, format: { with: /\A[a-z0-9_\-]{3,200}\z/ }, uniqueness: { scope: [:path, :closed_at, :hidden_at, :unpublished_at], case_sensitive: false }
validates :duree_conservation_dossiers_dans_ds, allow_nil: false,
numericality: {
only_integer: true,
greater_than_or_equal_to: 1,
less_than_or_equal_to: :max_duree_conservation_dossiers_dans_ds
}
validates :max_duree_conservation_dossiers_dans_ds, allow_nil: false,
numericality: {
only_integer: true,
greater_than_or_equal_to: 1,
less_than_or_equal_to: Expired::MAX_DOSSIER_RENTENTION_IN_MONTH
}
validates_with MonAvisEmbedValidator
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
validates_associated :refused_mail, on: :publication
validates_associated :without_continuation_mail, on: :publication
validates_associated :re_instructed_mail, on: :publication
validates_associated :attestation_template, on: :publication, if: -> { attestation_template&.activated? }
FILE_MAX_SIZE = 20.megabytes
validates :notice, content_type: [
"application/msword",
"application/pdf",
"application/vnd.ms-powerpoint",
"application/vnd.oasis.opendocument.presentation",
"application/vnd.oasis.opendocument.text",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"image/jpeg",
"image/jpg",
"image/png",
"text/plain"
], size: { less_than: FILE_MAX_SIZE }, if: -> { new_record? || created_at > Date.new(2020, 2, 28) }
validates :deliberation, content_type: [
"application/msword",
"application/pdf",
"application/vnd.oasis.opendocument.text",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"image/jpeg",
"image/jpg",
"image/png",
"text/plain"
], size: { less_than: FILE_MAX_SIZE }, if: -> { new_record? || created_at > Date.new(2020, 4, 29) }
LOGO_MAX_SIZE = 5.megabytes
validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'],
size: { less_than: LOGO_MAX_SIZE },
if: -> { new_record? || created_at > Date.new(2020, 11, 13) }
validates :api_entreprise_token, jwt_token: true, allow_blank: true
validates :api_particulier_token, format: { with: /\A[A-Za-z0-9\-_=.]{15,}\z/ }, allow_blank: true
validate :validate_auto_archive_on_in_the_future, if: :will_save_change_to_auto_archive_on?
before_save :set_api_entreprise_token_expires_at, if: :will_save_change_to_api_entreprise_token?
before_save :update_juridique_required
after_save :extend_conservation_for_dossiers
after_initialize :ensure_path_exists
before_save :ensure_path_exists
after_create :ensure_defaut_groupe_instructeur
include AASM
aasm whiny_persistence: true do
state :brouillon, initial: true
state :publiee
state :close
state :depubliee
event :publish, before: :before_publish do
transitions from: :brouillon, to: :publiee, after: :after_publish
transitions from: :close, to: :publiee, after: :after_republish
transitions from: :depubliee, to: :publiee, after: :after_republish
end
event :close, after: :after_close do
transitions from: :publiee, to: :close
end
event :unpublish, after: :after_unpublish do
transitions from: :publiee, to: :depubliee
end
end
def dossiers_close_to_expiration
dossiers.close_to_expiration.count
end
def publish_or_reopen!(administrateur)
Procedure.transaction do
if brouillon?
reset!
end
other_procedure = other_procedure_with_path(path)
if other_procedure.present? && administrateur.owns?(other_procedure)
other_procedure.unpublish!
publish!(other_procedure.canonical_procedure || other_procedure)
else
publish!
end
end
end
def reset!
if !locked? || draft_changed?
dossier_ids_to_destroy = draft_revision.dossiers.ids
if dossier_ids_to_destroy.present?
Rails.logger.info("Resetting #{dossier_ids_to_destroy.size} dossiers on procedure #{id}: #{dossier_ids_to_destroy}")
draft_revision.dossiers.destroy_all
end
end
end
def suggested_path(administrateur)
if path_customized?
return path
end
slug = libelle&.parameterize&.first(50)
suggestion = slug
counter = 1
while !path_available?(suggestion)
counter = counter + 1
suggestion = "#{slug}-#{counter}"
end
suggestion
end
def other_procedure_with_path(path)
Procedure.publiees
.where.not(id: self.id)
.find_by(path: path)
end
def path_available?(path)
other_procedure_with_path(path).blank?
end
def canonical_procedure_child?(procedure)
!canonical_procedure || canonical_procedure == procedure || canonical_procedure == procedure.canonical_procedure
end
def locked?
publiee? || close? || depubliee?
end
def draft_changed?
preload_draft_and_published_revisions
!brouillon? && (types_de_champ_revision_changes.present? || ineligibilite_rules_revision_changes.present?)
end
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
revisions = []
if !association(:published_revision).loaded? && published_revision_id.present?
revisions.push(published_revision)
end
if !association(:draft_revision).loaded? && draft_revision_id.present?
revisions.push(draft_revision)
end
ProcedureRevisionPreloader.new(revisions).all if !revisions.empty?
end
def accepts_new_dossiers?
publiee? || brouillon?
end
def replaced_by_procedure?
replaced_by_procedure_id.present?
end
def dossier_can_transition_to_en_construction?
accepts_new_dossiers? || depubliee?
end
def expose_legacy_carto_api?
module_api_carto&.use_api_carto? && module_api_carto&.migrated?
end
def declarative?
declarative_with_state.present?
end
def declarative_accepte?
declarative_with_state == Procedure.declarative_with_states.fetch(:accepte)
end
def declarative_en_instruction?
declarative_with_state == Procedure.declarative_with_states.fetch(:en_instruction)
end
def self.declarative_attributes_for_select
declarative_with_states.map do |state, _|
[I18n.t("activerecord.attributes.#{model_name.i18n_key}.declarative_with_state/#{state}"), state]
end
end
def feature_enabled?(feature)
Flipper.enabled?(feature, self)
end
def path_customized?
!path.match?(/[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}/)
end
def organisation_name
service&.nom || organisation
end
def self.active(id)
publiees.find(id)
end
def clone(admin, from_library)
is_different_admin = !admin.owns?(self)
populate_champ_stable_ids
include_list = {
attestation_template: [],
draft_revision: {
revision_types_de_champ: [:type_de_champ],
dossier_submitted_message: []
}
}
include_list[:groupe_instructeurs] = [:instructeurs, :contact_information] if !is_different_admin
procedure = self.deep_clone(include: include_list) do |original, kopy|
ClonePiecesJustificativesService.clone_attachments(original, kopy)
end
procedure.path = SecureRandom.uuid
procedure.aasm_state = :brouillon
procedure.closed_at = nil
procedure.unpublished_at = nil
procedure.published_at = nil
procedure.auto_archive_on = nil
procedure.lien_notice = nil
procedure.duree_conservation_etendue_par_ds = false
if procedure.duree_conservation_dossiers_dans_ds > NEW_MAX_DUREE_CONSERVATION
procedure.duree_conservation_dossiers_dans_ds = NEW_MAX_DUREE_CONSERVATION
procedure.max_duree_conservation_dossiers_dans_ds = NEW_MAX_DUREE_CONSERVATION
end
procedure.estimated_dossiers_count = 0
procedure.published_revision = nil
procedure.draft_revision.procedure = procedure
if is_different_admin
procedure.administrateurs = [admin]
procedure.api_entreprise_token = nil
procedure.encrypted_api_particulier_token = nil
procedure.opendata = true
procedure.api_particulier_scopes = []
procedure.routing_enabled = false
else
procedure.administrateurs = administrateurs
end
procedure.initiated_mail = initiated_mail&.dup
procedure.received_mail = received_mail&.dup
procedure.closed_mail = closed_mail&.dup
procedure.refused_mail = refused_mail&.dup
procedure.without_continuation_mail = without_continuation_mail&.dup
procedure.re_instructed_mail = re_instructed_mail&.dup
procedure.ask_birthday = false # see issue #4242
procedure.cloned_from_library = from_library
procedure.parent_procedure = self
procedure.canonical_procedure = nil
procedure.replaced_by_procedure = nil
procedure.service = nil
procedure.closing_reason = nil
procedure.closing_details = nil
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|
next if [:notice, :deliberation, :logo].exclude?(attribute)
procedure.public_send("#{attribute}=", nil)
end
end
transaction do
procedure.save!
move_new_children_to_new_parent_coordinate(procedure.draft_revision)
end
if is_different_admin || from_library
procedure.draft_revision.types_de_champ_public.each { |tdc| tdc.options&.delete(:old_pj) }
end
new_defaut_groupe = procedure.groupe_instructeurs
.find_by(label: defaut_groupe_instructeur.label) || procedure.groupe_instructeurs.first
procedure.update!(defaut_groupe_instructeur: new_defaut_groupe)
Flipper.features.each do |feature|
next if feature.enabled? # don't clone features globally enabled
next unless feature_enabled?(feature.key)
Flipper.enable(feature.key, procedure)
end
procedure
end
def whitelisted?
whitelisted_at.present?
end
def hidden_as_template?
hidden_at_as_template.present?
end
def hide_as_template!
touch(:hidden_at_as_template)
end
def unhide_as_template!
self.hidden_at_as_template = nil
save
end
def total_dossier
self.dossiers.state_not_brouillon.size
end
def procedure_overview(start_date, groups)
ProcedureOverview.new(self, start_date, groups)
end
def passer_en_construction_email_template
initiated_mail || Mails::InitiatedMail.default_for_procedure(self)
end
def passer_en_instruction_email_template
received_mail || Mails::ReceivedMail.default_for_procedure(self)
end
def accepter_email_template
closed_mail || Mails::ClosedMail.default_for_procedure(self)
end
def refuser_email_template
refused_mail || Mails::RefusedMail.default_for_procedure(self)
end
def classer_sans_suite_email_template
without_continuation_mail || Mails::WithoutContinuationMail.default_for_procedure(self)
end
def repasser_en_instruction_email_template
re_instructed_mail || Mails::ReInstructedMail.default_for_procedure(self)
end
def email_template_for(state)
case state
when Dossier.states.fetch(:en_construction)
passer_en_construction_email_template
when Dossier.states.fetch(:en_instruction)
passer_en_instruction_email_template
when DossierOperationLog.operations.fetch(:repasser_en_instruction)
repasser_en_instruction_email_template
when Dossier.states.fetch(:accepte)
accepter_email_template
when Dossier.states.fetch(:refuse)
refuser_email_template
when Dossier.states.fetch(:sans_suite)
classer_sans_suite_email_template
else
raise "Unknown dossier state: #{state}"
end
end
def whitelist!
touch(:whitelisted_at)
end
def closed_mail_template_attestation_inconsistency_state
# As an optimization, dont check the predefined templates (they are presumed correct)
if closed_mail.present?
tag_present = closed_mail.body.to_s.include?("--lien attestation--")
if attestation_template&.activated? && !tag_present
:missing_tag
elsif !attestation_template&.activated? && tag_present
:extraneous_tag
end
end
end
def populate_champ_stable_ids
TypeDeChamp
.joins(:revisions)
.where(procedure_revisions: { procedure_id: id }, stable_id: nil)
.find_each do |type_de_champ|
type_de_champ.update_column(:stable_id, type_de_champ.id)
end
end
def missing_steps
result = []
if service.nil?
result << :service
end
if service_siret_test?
result << :service
end
if missing_instructeurs?
result << :instructeurs
end
if missing_zones?
result << :zones
end
result
end
def logo_url
if logo.attached?
logo_variant = logo.variant(resize_to_limit: [400, 400])
logo_variant.key.present? ? logo_variant.processed.url : Rails.application.routes.url_helpers.url_for(logo)
else
ActionController::Base.helpers.image_url(PROCEDURE_DEFAULT_LOGO_SRC)
end
end
def missing_instructeurs?
!AssignTo.exists?(groupe_instructeur: groupe_instructeurs)
end
def missing_zones?
if Rails.application.config.ds_zonage_enabled
zones.empty?
else
false
end
end
def service_siret_test?
service&.siret == Service::SIRET_TEST
end
def revised?
revisions.size > 2
end
def revisions_count
# We start counting from the first revision after publication and we are not counting the draft (there is always one)
revisions.size - 2
end
def instructeurs_self_management?
instructeurs_self_management_enabled?
end
def groupe_instructeurs_but_defaut
groupe_instructeurs - [defaut_groupe_instructeur]
end
def routing_champs
active_revision.types_de_champ_public.filter(&:used_by_routing_rules?).map(&:libelle)
end
def can_be_deleted_by_administrateur?
brouillon? || dossiers.state_en_instruction.empty?
end
def can_be_deleted_by_manager?
kept? && can_be_deleted_by_administrateur?
end
def discard_and_keep_track!(author)
if brouillon?
reset!
elsif publiee?
close!
end
dossiers.visible_by_administration.find_each do |dossier|
dossier.hide_and_keep_track!(author, :procedure_removed)
end
discard!
end
def purge_discarded
if dossiers.empty?
destroy
end
end
def self.purge_discarded
discarded_expired.find_each do |p|
p.purge_discarded
rescue StandardError => e
Sentry.capture_exception(e, extra: { procedure_id: p.id })
end
end
def restore(author)
if discarded? && undiscard
dossiers.hidden_by_administration.find_each do |dossier|
dossier.restore(author)
end
end
end
def flipper_id
"Procedure;#{id}"
end
def api_entreprise_role?(role)
APIEntrepriseToken.new(api_entreprise_token).role?(role)
end
def api_entreprise_token
self[:api_entreprise_token].presence || Rails.application.secrets.api_entreprise[:key]
end
def api_entreprise_token_expired?
APIEntrepriseToken.new(api_entreprise_token).expired?
end
def create_new_revision(revision = nil)
transaction do
new_revision = (revision || draft_revision)
.deep_clone(include: [:revision_types_de_champ])
.tap { |revision| revision.published_at = nil }
.tap(&:save!)
move_new_children_to_new_parent_coordinate(new_revision)
# they are not aware of the new tdcs
new_revision.types_de_champ_public.reset
new_revision.types_de_champ_private.reset
new_revision
end
end
def average_dossier_weight
if dossiers.termine.any?
dossiers_sample = dossiers.termine.limit(100)
total_size = Champ
.includes(piece_justificative_file_attachments: :blob)
.where(type: Champs::PieceJustificativeChamp.to_s, dossier: dossiers_sample)
.sum('active_storage_blobs.byte_size')
MIN_WEIGHT + total_size / dossiers_sample.length
else
nil
end
end
def publish_revision!
reset!
transaction do
self.published_revision = draft_revision
self.draft_revision = create_new_revision
save!(context: :publication)
published_revision.touch(:published_at)
end
dossiers
.state_not_termine
.find_each(&:rebase_later)
end
def reset_draft_revision!
if published_revision.present? && draft_changed?
reset!
transaction do
draft_revision.types_de_champ.filter(&:only_present_on_draft?).each(&:destroy)
draft_revision.update(dossier_submitted_message: nil)
draft_revision.destroy
update!(draft_revision: create_new_revision(published_revision))
end
end
end
def cnaf_enabled?
api_particulier_sources['cnaf'].present?
end
def dgfip_enabled?
api_particulier_sources['dgfip'].present?
end
def pole_emploi_enabled?
api_particulier_sources['pole_emploi'].present?
end
def mesri_enabled?
api_particulier_sources['mesri'].present?
end
def published_or_created_at
published_at || created_at
end
def publiee_or_close?
publiee? || close?
end
def self.tags
unnest = Arel::Nodes::NamedFunction.new('UNNEST', [self.arel_table[:tags]])
query = self.select(unnest.as('tags')).publiees.distinct.order('tags')
self.connection.query(query.to_sql).flatten
end
def compute_dossiers_count
now = Time.zone.now
if now > (self.dossiers_count_computed_at || self.created_at) + DOSSIERS_COUNT_EXPIRING
self.update(estimated_dossiers_count: self.dossiers.visible_by_administration.count,
dossiers_count_computed_at: now)
end
end
def move_new_children_to_new_parent_coordinate(new_draft)
children = new_draft.revision_types_de_champ
.includes(parent: :type_de_champ)
.where.not(parent_id: nil)
coordinates_by_stable_id = new_draft.revision_types_de_champ
.includes(:type_de_champ)
.index_by(&:stable_id)
children.each do |child|
child.update!(parent: coordinates_by_stable_id.fetch(child.parent.stable_id))
end
new_draft.reload
end
def before_publish
assign_attributes(closed_at: nil, unpublished_at: nil)
end
def after_publish(canonical_procedure = nil)
self.canonical_procedure = canonical_procedure
self.published_revision = draft_revision
self.draft_revision = create_new_revision
save!(context: :publication)
touch(:published_at)
published_revision.touch(:published_at)
end
def after_republish(canonical_procedure = nil)
touch(:published_at)
end
def after_close
touch(:closed_at)
end
def after_unpublish
touch(:unpublished_at)
end
def update_juridique_required
self.juridique_required ||= (cadre_juridique.present? || deliberation.attached?)
true
end
def check_juridique
if juridique_required? && (cadre_juridique.blank? && !deliberation.attached?)
errors.add(:cadre_juridique, " : veuillez remplir le texte de loi ou la délibération")
end
end
def ensure_path_exists
if self.path.blank?
self.path = SecureRandom.uuid
end
end
def extend_conservation_for_dossiers
return if !previous_changes.include?(:duree_conservation_dossiers_dans_ds)
before, after = duree_conservation_dossiers_dans_ds_previous_change
return if [before, after].any?(&:nil?)
return if (after - before).negative?
ResetExpiringDossiersJob.perform_later(self)
end
def ensure_defaut_groupe_instructeur
if self.groupe_instructeurs.empty?
gi = groupe_instructeurs.create(label: GroupeInstructeur::DEFAUT_LABEL)
self.update(defaut_groupe_instructeur_id: gi.id)
end
end
def stable_ids_used_by_routing_rules
@stable_ids_used_by_routing_rules ||= groupe_instructeurs.flat_map { _1.routing_rule&.sources }.compact
end
# We need this to unfuck administrate + aasm
def self.human_attribute_name(attribute, options = {})
if attribute == :aasm_state
'Statut'
else
super
end
end
def toggle_routing
update!(routing_enabled: self.groupe_instructeurs.active.many?)
end
def lien_dpo_email?
lien_dpo.present? && lien_dpo.match?(/@/)
end
def dossier_for_preview(user)
# Try to use a preview or a dossier filled by current user
dossiers.where(for_procedure_preview: true).or(dossiers.visible_by_administration)
.order(Arel.sql("CASE WHEN user_id = #{user.id} THEN 1 ELSE 0 END DESC,
CASE WHEN state = 'accepte' THEN 1 ELSE 0 END DESC,
CASE WHEN state = 'brouillon' THEN 0 ELSE 1 END DESC,
CASE WHEN for_procedure_preview = True THEN 1 ELSE 0 END DESC,
id DESC")) \
.first
end
def reset_closing_params
update!(closing_reason: nil, closing_details: nil, replaced_by_procedure_id: nil, closing_notification_brouillon: false, closing_notification_en_cours: false)
end
def monavis_embed_html_source(source)
monavis_embed.gsub('nd_source=button', "nd_source=#{source}").gsub('<a ', '<a target="_blank" rel="noopener noreferrer" ')
end
def set_api_entreprise_token_expires_at
self.api_entreprise_token_expires_at = APIEntrepriseToken.new(api_entreprise_token).expiration
end
private
def published_revisions_types_de_champ(parent = nil)
# all published revisions
revision_ids = revisions.ids - [draft_revision_id]
# fetch all parent types de champ
parent_ids = if parent.present?
ProcedureRevisionTypeDeChamp
.where(revision_id: revision_ids)
.joins(:type_de_champ)
.where(type_de_champ: { stable_id: parent.stable_id })
.ids
end
# fetch all type_de_champ.stable_id for all the revisions expect draft
# and for each stable_id take the bigger (more recent) type_de_champ.id
recent_ids = TypeDeChamp
.fillable
.joins(:revision_types_de_champ)
.where(revision_types_de_champ: { revision_id: revision_ids, parent_id: parent_ids })
.group(:stable_id).select('MAX(types_de_champ.id)')
# fetch the more recent procedure_revision_types_de_champ
# which includes recents_ids
recents_prtdc = ProcedureRevisionTypeDeChamp
.where(type_de_champ_id: recent_ids)
.where.not(revision_id: draft_revision_id)
.group(:type_de_champ_id)
.select('MAX(id)')
TypeDeChamp
.joins(:revision_types_de_champ)
.where(revision_types_de_champ: { id: recents_prtdc }).then do |relation|
if feature_enabled?(:export_order_by_revision) # Fonds Verts, en attente d'exports personnalisables
relation.order(:private, 'revision_types_de_champ.revision_id': :desc, position: :asc)
else
relation.order(:private, :position, 'revision_types_de_champ.revision_id': :desc)
end
end
end
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?
errors.add(:auto_archive_on, 'doit être dans le futur')
end
end