demarches-normaliennes/app/models/procedure.rb

1000 lines
32 KiB
Ruby
Raw Normal View History

2020-08-06 16:35:45 +02:00
# == Schema Information
#
# Table name: procedures
#
2021-04-27 13:55:12 +02:00
# id :integer not null, primary key
# aasm_state :string default("brouillon")
# allow_expert_messaging :boolean default(TRUE), not null
2021-04-27 13:55:12 +02:00
# allow_expert_review :boolean default(TRUE), not null
# api_entreprise_token :string
# api_particulier_scopes :text default([]), is an Array
# api_particulier_sources :jsonb
2021-04-27 13:55:12 +02:00
# ask_birthday :boolean default(FALSE), not null
# auto_archive_on :date
# cadre_juridique :string
# cloned_from_library :boolean default(FALSE)
# closed_at :datetime
# declarative_with_state :string
# description :string
# description_pj :string
# description_target_audience :string
2023-01-06 17:47:28 +01:00
# dossiers_count_computed_at :datetime
2021-04-27 13:55:12 +02:00
# duree_conservation_dossiers_dans_ds :integer
# duree_conservation_etendue_par_ds :boolean default(FALSE), not null
# encrypted_api_particulier_token :string
2023-01-06 17:47:28 +01:00
# estimated_dossiers_count :integer
# estimated_duration_visible :boolean default(TRUE), not null
2021-04-27 13:55:12 +02:00
# euro_flag :boolean default(FALSE)
2021-04-16 11:11:00 +02:00
# experts_require_administrateur_invitation :boolean default(FALSE)
2021-04-27 13:55:12 +02:00
# for_individual :boolean default(FALSE)
# hidden_at :datetime
# instructeurs_self_management_enabled :boolean
2021-04-27 13:55:12 +02:00
# juridique_required :boolean default(TRUE)
# libelle :string
# lien_dpo :string
# lien_dpo_error :text
2021-04-27 13:55:12 +02:00
# lien_notice :string
# lien_notice_error :text
2021-04-27 13:55:12 +02:00
# lien_site_web :string
# max_duree_conservation_dossiers_dans_ds :integer default(12), not null
2023-01-11 11:17:34 +01:00
# migrated_champ_routage :boolean
2021-04-27 13:55:12 +02:00
# monavis_embed :text
# opendata :boolean default(TRUE)
2021-04-27 13:55:12 +02:00
# organisation :string
# path :string not null
# piece_justificative_multiple :boolean default(TRUE), not null
# procedure_expires_when_termine_enabled :boolean default(TRUE)
2021-04-27 13:55:12 +02:00
# published_at :datetime
# routing_criteria_name :text default("Votre ville")
# routing_enabled :boolean
2023-05-25 19:06:43 +02:00
# sva_svr :jsonb not null
2022-10-06 11:21:14 +02:00
# tags :text default([]), is an Array
2021-04-27 13:55:12 +02:00
# unpublished_at :datetime
# web_hook_url :string
# whitelisted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# canonical_procedure_id :bigint
# defaut_groupe_instructeur_id :bigint
2021-04-27 13:55:12 +02:00
# draft_revision_id :bigint
# parent_procedure_id :bigint
# published_revision_id :bigint
2022-05-23 18:58:13 +02:00
# replaced_by_procedure_id :bigint
2021-04-27 13:55:12 +02:00
# service_id :bigint
2021-12-01 21:05:15 +01:00
# zone_id :bigint
2020-08-06 16:35:45 +02:00
#
2018-03-06 13:44:29 +01:00
class Procedure < ApplicationRecord
include ProcedureStatsConcern
include EncryptableConcern
include InitiationProcedureConcern
include ProcedureGroupeInstructeurAPIHackConcern
2023-05-25 19:09:59 +02:00
include ProcedureSVASVRConcern
2020-02-05 16:09:03 +01:00
include Discard::Model
self.discard_column = :hidden_at
self.ignored_columns += [
:direction,
:durees_conservation_required,
:cerfa_flag,
:test_started_at,
:lien_demarche
]
2020-02-05 16:09:03 +01:00
default_scope -> { kept }
OLD_MAX_DUREE_CONSERVATION = 36
NEW_MAX_DUREE_CONSERVATION = ENV.fetch('NEW_MAX_DUREE_CONSERVATION') { 12 }.to_i
MIN_WEIGHT = 350000
2023-01-06 17:47:28 +01:00
DOSSIERS_COUNT_EXPIRING = 1.hour
attr_encrypted :api_particulier_token
2020-09-17 11:15:21 +02:00
has_many :revisions, -> { order(:id) }, class_name: 'ProcedureRevision', inverse_of: :procedure
2020-08-27 19:55:10 +02:00
belongs_to :draft_revision, class_name: 'ProcedureRevision', optional: false
2020-06-26 11:37:28 +02:00
belongs_to :published_revision, class_name: 'ProcedureRevision', optional: true
has_many :deleted_dossiers, dependent: :destroy
2017-03-07 10:25:28 +01:00
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
2021-01-07 15:45:02 +01:00
has_many :experts_procedures, dependent: :destroy
has_many :experts, through: :experts_procedures
2022-05-23 18:58:13 +02:00
has_many :replaced_procedures, -> { with_discarded }, inverse_of: :replaced_by_procedure, class_name: "Procedure",
foreign_key: "replaced_by_procedure_id", dependent: :nullify
2021-01-07 15:45:02 +01:00
has_one :module_api_carto, dependent: :destroy
has_one :attestation_template, dependent: :destroy
belongs_to :parent_procedure, class_name: 'Procedure', optional: true
belongs_to :canonical_procedure, class_name: 'Procedure', optional: true
2022-05-23 18:58:13 +02:00
belongs_to :replaced_by_procedure, -> { with_discarded }, inverse_of: :replaced_procedures, class_name: "Procedure", optional: true
belongs_to :service, optional: true
2021-12-01 21:05:15 +01:00
belongs_to :zone, optional: true
2022-08-24 12:39:07 +02:00
has_and_belongs_to_many :zones
def active_dossier_submitted_message
published_dossier_submitted_message || draft_dossier_submitted_message
end
2020-06-26 11:37:28 +02:00
def active_revision
brouillon? ? draft_revision : published_revision
end
def types_de_champ_for_procedure_presentation(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
# 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 })
.order(:private, :position, 'revision_types_de_champ.revision_id': :desc)
end
end
2021-01-21 14:11:17 +01:00
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
2021-01-21 14:11:17 +01:00
end
def types_de_champ_private_for_tags
types_de_champ_for_tags.private_only
2021-01-21 14:11:17 +01:00
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 :active_groupe_instructeurs, -> { active }, class_name: 'GroupeInstructeur', inverse_of: false
has_many :closed_groupe_instructeurs, -> { closed }, class_name: 'GroupeInstructeur', inverse_of: false
2020-09-17 11:15:21 +02:00
# 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
2019-08-22 17:58:31 +02:00
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
belongs_to :defaut_groupe_instructeur, class_name: 'GroupeInstructeur', inverse_of: false, optional: true
2019-08-28 13:11:58 +02:00
has_one_attached :logo
2018-04-11 14:35:52 +02:00
has_one_attached :notice
2018-05-31 10:59:38 +02:00
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) }
2017-05-26 21:30:11 +02:00
2020-03-26 17:37:55 +01:00
scope :discarded_expired, -> do
with_discarded
.discarded
.where('hidden_at < ?', 1.month.ago)
end
2018-11-01 14:04:32 +01:00
scope :for_api, -> {
includes(
:administrateurs,
2020-08-27 19:55:10 +02:00
:module_api_carto,
published_revision: [
:types_de_champ_private,
:types_de_champ_public
2020-08-27 19:55:10 +02:00
],
draft_revision: [
:types_de_champ_private,
:types_de_champ_public
2020-08-27 19:55:10 +02:00
]
2018-11-01 14:04:32 +01:00
)
}
enum declarative_with_state: {
en_instruction: 'en_instruction',
accepte: 'accepte'
}
scope :for_api_v2, -> {
2020-07-16 12:48:35 +02:00
includes(:draft_revision, :published_revision, administrateurs: :user)
}
scope :for_download, -> {
includes(
:groupe_instructeurs,
dossiers: {
champs_public: [
piece_justificative_file_attachments: :blob,
champs: [
piece_justificative_file_attachments: :blob
]
]
}
)
}
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/no_empty_block': true,
'types_de_champ/no_empty_drop_down': true,
on: :publication
validates :draft_types_de_champ_private,
'types_de_champ/no_empty_block': true,
'types_de_champ/no_empty_drop_down': true,
on: :publication
validate :check_juridique, on: [:create, :publication]
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: 60
}
2019-07-17 17:13:08 +02:00
validates_with MonAvisEmbedValidator
validates_associated :draft_revision, on: :publication
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 :attestation_template, on: :publication, if: -> { attestation_template&.activated? }
FILE_MAX_SIZE = 20.megabytes
2020-03-16 17:55:16 +01:00
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",
2020-03-16 17:55:16 +01:00
"text/plain"
], size: { less_than: FILE_MAX_SIZE }, if: -> { new_record? || created_at > Date.new(2020, 2, 28) }
2020-03-16 17:55:16 +01:00
validates :deliberation, content_type: [
"application/msword",
"application/pdf",
"application/vnd.oasis.opendocument.text",
2020-03-16 17:55:16 +01:00
"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) }
2021-03-04 16:13:19 +01:00
LOGO_MAX_SIZE = 5.megabytes
2021-03-04 16:13:19 +01:00
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) }
2020-03-16 17:55:16 +01:00
2020-07-08 17:00:21 +02:00
validates :api_entreprise_token, jwt_token: true, allow_blank: true
2021-09-09 19:48:34 +02:00
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?
2020-07-08 17:00:21 +02:00
before_save :update_juridique_required
after_initialize :ensure_path_exists
before_save :ensure_path_exists
after_create :ensure_defaut_groupe_instructeur
2018-05-17 15:38:49 +02:00
include AASM
aasm whiny_persistence: true do
state :brouillon, initial: true
state :publiee
state :close
2019-12-04 15:45:06 +01:00
state :depubliee
2018-05-17 15:38:49 +02:00
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
2018-05-17 15:38:49 +02:00
end
event :close, after: :after_close do
transitions from: :publiee, to: :close
2018-05-17 15:38:49 +02:00
end
2019-12-04 15:45:06 +01:00
event :unpublish, after: :after_unpublish do
transitions from: :publiee, to: :depubliee
end
2018-05-17 15:38:49 +02:00
end
2018-05-17 15:39:37 +02:00
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)
2019-12-04 15:45:06 +01:00
other_procedure.unpublish!
publish!(other_procedure.canonical_procedure || other_procedure)
else
publish!
end
2018-09-07 18:42:12 +02:00
end
end
def reset!
2021-06-24 11:57:05 +02:00
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?(administrateur, 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?(administrateur, path)
procedure = other_procedure_with_path(path)
procedure.blank? || (administrateur.owns?(procedure) && canonical_procedure_child?(procedure))
end
def canonical_procedure_child?(procedure)
!canonical_procedure || canonical_procedure == procedure || canonical_procedure == procedure.canonical_procedure
end
2018-05-17 15:41:44 +02:00
def locked?
2019-12-04 15:45:06 +01:00
publiee? || close? || depubliee?
2018-05-17 15:41:44 +02:00
end
def draft_changed?
2022-06-02 15:26:04 +02:00
!brouillon? && published_revision.different_from?(draft_revision) && revision_changes.present?
end
def revision_changes
published_revision.compare(draft_revision)
end
def accepts_new_dossiers?
2019-12-04 15:45:06 +01:00
publiee? || brouillon?
end
def replaced_by_procedure?
replaced_by_procedure_id.present?
end
2019-12-04 15:45:06 +01:00
def dossier_can_transition_to_en_construction?
accepts_new_dossiers? || depubliee?
2018-05-17 15:41:44 +02:00
end
2018-10-31 13:28:39 +01:00
def expose_legacy_carto_api?
2018-11-27 15:53:13 +01:00
module_api_carto&.use_api_carto? && module_api_carto&.migrated?
2018-10-31 13:28:39 +01:00
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
2021-03-03 18:20:34 +01:00
def feature_enabled?(feature)
Flipper.enabled?(feature, self)
end
2019-07-05 14:38:20 +02:00
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)
2017-07-11 15:52:06 +02:00
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 if !is_different_admin
2021-07-01 13:33:22 +02:00
procedure = self.deep_clone(include: include_list) do |original, kopy|
PiecesJustificativesService.clone_attachments(original, kopy)
2021-07-01 13:33:22 +02:00
end
procedure.path = SecureRandom.uuid
2018-05-28 14:58:40 +02:00
procedure.aasm_state = :brouillon
procedure.closed_at = nil
2019-12-04 15:45:06 +01:00
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 = []
else
procedure.administrateurs = administrateurs
end
2018-05-30 18:45:46 +02:00
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.ask_birthday = false # see issue #4242
2017-03-07 18:19:48 +01:00
procedure.cloned_from_library = from_library
2018-04-24 15:23:07 +02:00
procedure.parent_procedure = self
procedure.canonical_procedure = nil
2022-05-23 18:58:13 +02:00
procedure.replaced_by_procedure = nil
procedure.service = nil
2023-05-16 17:54:37 +02:00
if !procedure.valid?
procedure.errors.attribute_names.each do |attribute|
next if [:notice, :deliberation, :logo].exclude?(attribute)
procedure.public_send("#{attribute}=", nil)
end
2023-05-16 17:54:37 +02:00
end
2022-05-24 15:57:31 +02:00
2023-05-16 17:54:37 +02:00
transaction do
procedure.save!
move_new_children_to_new_parent_coordinate(procedure.draft_revision)
end
2020-06-26 12:00:21 +02:00
if is_different_admin || from_library
procedure.draft_revision.types_de_champ_public.each { |tdc| tdc.options&.delete(:old_pj) }
2020-06-26 12:00:21 +02:00
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)
procedure
2016-06-15 11:34:05 +02:00
end
2018-01-10 17:52:43 +01:00
def whitelisted?
whitelisted_at.present?
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 initiated_mail_template
initiated_mail || Mails::InitiatedMail.default_for_procedure(self)
end
def received_mail_template
received_mail || Mails::ReceivedMail.default_for_procedure(self)
end
def closed_mail_template
closed_mail || Mails::ClosedMail.default_for_procedure(self)
end
def refused_mail_template
refused_mail || Mails::RefusedMail.default_for_procedure(self)
end
def without_continuation_mail_template
without_continuation_mail || Mails::WithoutContinuationMail.default_for_procedure(self)
end
2021-04-29 19:10:22 +02:00
def mail_template_for(state)
case state
when Dossier.states.fetch(:en_construction)
initiated_mail_template
when Dossier.states.fetch(:en_instruction)
received_mail_template
when Dossier.states.fetch(:accepte)
closed_mail_template
when Dossier.states.fetch(:refuse)
refused_mail_template
when Dossier.states.fetch(:sans_suite)
without_continuation_mail_template
else
raise "Unknown dossier state: #{state}"
end
end
2017-09-27 15:16:07 +02:00
def self.default_sort
{
'table' => 'self',
'column' => 'id',
'order' => 'desc'
2018-09-20 17:02:28 +02:00
}
2017-09-27 15:16:07 +02:00
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
2019-08-21 13:53:53 +02:00
if missing_instructeurs?
result << :instructeurs
end
2022-09-27 19:46:50 +02:00
if missing_zones?
result << :zones
end
result
end
def logo_url
2019-08-28 13:11:58 +02:00
if logo.attached?
Rails.application.routes.url_helpers.url_for(logo)
else
ActionController::Base.helpers.image_url(PROCEDURE_DEFAULT_LOGO_SRC)
end
end
2019-08-21 13:53:53 +02:00
def missing_instructeurs?
!AssignTo.exists?(groupe_instructeur: groupe_instructeurs)
end
2022-09-27 19:46:50 +02:00
def missing_zones?
2022-12-20 09:22:28 +01:00
if Rails.application.config.ds_zonage_enabled
2022-09-27 19:46:50 +02:00
zones.empty?
else
false
end
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?
routing_enabled? || instructeurs_self_management_enabled?
end
def defaut_groupe_instructeur_for_new_dossier
2022-11-04 10:10:57 +01:00
if !routing_enabled? || feature_enabled?(:procedure_routage_api)
defaut_groupe_instructeur
end
end
2023-04-27 10:11:27 +02:00
def groupe_instructeurs_but_defaut
groupe_instructeurs - [defaut_groupe_instructeur]
end
def can_be_deleted_by_administrateur?
brouillon? || dossiers.state_en_instruction.empty?
end
2020-03-26 09:08:52 +01:00
def can_be_deleted_by_manager?
kept? && can_be_deleted_by_administrateur?
2020-03-26 09:08:52 +01:00
end
def discard_and_keep_track!(author)
if brouillon?
reset!
elsif publiee?
close!
end
dossiers.visible_by_administration.each do |dossier|
dossier.hide_and_keep_track!(author, :procedure_removed)
2020-03-26 09:08:52 +01:00
end
2020-02-05 16:09:03 +01:00
discard!
end
def purge_discarded
if dossiers.empty?
destroy
end
end
def self.purge_discarded
discarded_expired.find_each(&:purge_discarded)
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
2020-04-28 10:15:08 +02:00
def api_entreprise_role?(role)
2020-08-05 18:40:47 +02:00
APIEntrepriseToken.new(api_entreprise_token).role?(role)
2020-04-29 10:23:35 +02:00
end
def api_entreprise_token
self[:api_entreprise_token].presence || Rails.application.secrets.api_entreprise[:key]
2020-04-28 10:15:08 +02:00
end
def api_entreprise_token_expired?
2020-08-05 18:40:47 +02:00
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!)
2022-05-06 09:51:44 +02:00
move_new_children_to_new_parent_coordinate(new_revision)
2022-05-12 09:26:43 +02:00
# 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
2020-06-26 12:00:21 +02:00
end
def average_dossier_weight
if dossiers.termine.any?
2021-06-01 14:25:30 +02:00
dossiers_sample = dossiers.termine.limit(100)
total_size = Champ
.includes(piece_justificative_file_attachments: :blob)
2021-06-04 10:38:53 +02:00
.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
2022-10-07 14:13:42 +02:00
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
2021-11-24 17:30:35 +01:00
def dgfip_enabled?
api_particulier_sources['dgfip'].present?
end
def pole_emploi_enabled?
api_particulier_sources['pole_emploi'].present?
end
2021-12-15 15:25:02 +01:00
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
2023-01-06 17:47:28 +01:00
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
2022-05-24 15:57:31 +02:00
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)
2022-05-24 15:57:31 +02:00
children.each do |child|
child.update!(parent: coordinates_by_stable_id.fetch(child.parent.stable_id))
2022-05-24 15:57:31 +02:00
end
new_draft.reload
2022-05-24 15:57:31 +02:00
end
def before_publish
assign_attributes(closed_at: nil, unpublished_at: nil)
2018-09-07 18:36:31 +02:00
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)
2018-09-07 18:36:31 +02:00
end
def after_close
touch(:closed_at)
2018-09-07 18:36:31 +02:00
end
2019-12-04 15:45:06 +01:00
def after_unpublish
touch(:unpublished_at)
2019-12-04 15:45:06 +01:00
end
def update_juridique_required
self.juridique_required ||= (cadre_juridique.present? || deliberation.attached?)
true
end
2018-05-31 10:59:38 +02:00
def check_juridique
if juridique_required? && (cadre_juridique.blank? && !deliberation.attached?)
2018-05-31 10:59:38 +02:00
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?
2019-03-06 14:42:27 +01:00
self.path = SecureRandom.uuid
end
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 pieces_jointes_list?
pieces_jointes_list_without_conditionnal.present? || pieces_jointes_list_with_conditionnal.present?
end
def pieces_jointes_list_without_conditionnal
active_revision.types_de_champ_public.not_condition.filter(&:piece_justificative?)
end
def pieces_jointes_list_with_conditionnal
active_revision.types_de_champ_public.where.not(condition: nil).filter(&:piece_justificative?)
end
def toggle_routing
update!(routing_enabled: self.groupe_instructeurs.active.many?)
end
def lien_dpo_email?
lien_dpo.present? && lien_dpo.match?(/@/)
end
private
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