d77530c461
Fixes an issue where, after clicking the "move champ up" button, the new form is rendered with an incoherent order.
445 lines
13 KiB
Ruby
445 lines
13 KiB
Ruby
require Rails.root.join('lib', 'percentile')
|
||
|
||
class Procedure < ApplicationRecord
|
||
MAX_DUREE_CONSERVATION = 36
|
||
|
||
has_many :types_de_piece_justificative, -> { ordered }, dependent: :destroy
|
||
has_many :types_de_champ, -> { public_only.ordered }, dependent: :destroy
|
||
has_many :types_de_champ_private, -> { private_only.ordered }, class_name: 'TypeDeChamp', dependent: :destroy
|
||
has_many :dossiers
|
||
has_many :deleted_dossiers, dependent: :destroy
|
||
|
||
has_one :module_api_carto, dependent: :destroy
|
||
has_one :attestation_template, dependent: :destroy
|
||
|
||
belongs_to :administrateur
|
||
belongs_to :parent_procedure, class_name: 'Procedure'
|
||
belongs_to :service
|
||
|
||
has_many :assign_to, dependent: :destroy
|
||
has_many :administrateurs_procedures
|
||
has_many :administrateurs, through: :administrateurs_procedures
|
||
has_many :gestionnaires, through: :assign_to
|
||
|
||
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_attached :notice
|
||
has_one_attached :deliberation
|
||
|
||
accepts_nested_attributes_for :types_de_champ, :reject_if => proc { |attributes| attributes['libelle'].blank? }, :allow_destroy => true
|
||
accepts_nested_attributes_for :types_de_piece_justificative, :reject_if => proc { |attributes| attributes['libelle'].blank? }, :allow_destroy => true
|
||
accepts_nested_attributes_for :types_de_champ_private
|
||
|
||
mount_uploader :logo, ProcedureLogoUploader
|
||
|
||
default_scope { where(hidden_at: nil) }
|
||
scope :brouillons, -> { where(aasm_state: :brouillon) }
|
||
scope :publiees, -> { where(aasm_state: :publiee) }
|
||
scope :archivees, -> { where(aasm_state: :archivee) }
|
||
scope :publiees_ou_archivees, -> { where(aasm_state: [:publiee, :archivee]) }
|
||
scope :by_libelle, -> { order(libelle: :asc) }
|
||
scope :created_during, -> (range) { where(created_at: range) }
|
||
scope :cloned_from_library, -> { where(cloned_from_library: true) }
|
||
scope :avec_lien, -> { where.not(path: nil) }
|
||
|
||
scope :for_api, -> {
|
||
includes(
|
||
:administrateur,
|
||
:types_de_champ_private,
|
||
:types_de_champ,
|
||
:types_de_piece_justificative,
|
||
:module_api_carto
|
||
)
|
||
}
|
||
|
||
validates :libelle, presence: true, allow_blank: false, allow_nil: false
|
||
validates :description, presence: true, allow_blank: false, allow_nil: false
|
||
validate :check_juridique
|
||
validates :path, format: { with: /\A[a-z0-9_\-]{3,50}\z/ }, uniqueness: { scope: :aasm_state, case_sensitive: false }, presence: true, allow_blank: false, allow_nil: true
|
||
# FIXME: remove duree_conservation_required flag once all procedures are converted to the new style
|
||
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 }, if: :durees_conservation_required
|
||
validates :duree_conservation_dossiers_hors_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :durees_conservation_required
|
||
validates :duree_conservation_dossiers_dans_ds, allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION }, unless: :durees_conservation_required
|
||
validates :duree_conservation_dossiers_hors_ds, allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, unless: :durees_conservation_required
|
||
|
||
before_save :update_juridique_required
|
||
before_save :update_durees_conservation_required
|
||
|
||
include AASM
|
||
|
||
aasm whiny_persistence: true do
|
||
state :brouillon, initial: true
|
||
state :publiee
|
||
state :archivee
|
||
state :hidden
|
||
|
||
event :publish, after: :after_publish, guard: :can_publish? do
|
||
transitions from: :brouillon, to: :publiee
|
||
end
|
||
|
||
event :reopen, after: :after_reopen, guard: :can_publish? do
|
||
transitions from: :archivee, to: :publiee
|
||
end
|
||
|
||
event :archive, after: :after_archive do
|
||
transitions from: :publiee, to: :archivee
|
||
end
|
||
|
||
event :hide, after: :after_hide do
|
||
transitions from: :brouillon, to: :hidden
|
||
transitions from: :publiee, to: :hidden
|
||
transitions from: :archivee, to: :hidden
|
||
end
|
||
|
||
event :draft, after: :after_draft do
|
||
transitions from: :publiee, to: :brouillon
|
||
end
|
||
end
|
||
|
||
def publish_or_reopen!(path)
|
||
if archivee? && may_reopen?(path)
|
||
reopen!(path)
|
||
elsif may_publish?(path)
|
||
reset!
|
||
publish!(path)
|
||
end
|
||
end
|
||
|
||
def reset!
|
||
if locked?
|
||
raise "Can not reset a locked procedure."
|
||
else
|
||
dossiers.destroy_all
|
||
end
|
||
end
|
||
|
||
def locked?
|
||
publiee_ou_archivee?
|
||
end
|
||
|
||
# This method is needed for transition. Eventually this will be the same as brouillon?.
|
||
def brouillon_avec_lien?
|
||
Flipflop.publish_draft? && brouillon? && path.present?
|
||
end
|
||
|
||
def publiee_ou_archivee?
|
||
publiee? || archivee?
|
||
end
|
||
|
||
def expose_legacy_carto_api?
|
||
module_api_carto&.use_api_carto? && module_api_carto&.migrated?
|
||
end
|
||
|
||
# Warning: dossier after_save build_default_champs must be removed
|
||
# to save a dossier created from this method
|
||
def new_dossier
|
||
champs = types_de_champ.map { |tdc| tdc.champ.build }
|
||
champs_private = types_de_champ_private.map { |tdc| tdc.champ.build }
|
||
|
||
Dossier.new(procedure: self, champs: champs, champs_private: champs_private)
|
||
end
|
||
|
||
def default_path
|
||
libelle&.parameterize&.first(50)
|
||
end
|
||
|
||
def organisation_name
|
||
service&.nom || organisation
|
||
end
|
||
|
||
def self.active(id)
|
||
publiees.find(id)
|
||
end
|
||
|
||
def switch_types_de_champ(index_of_first_element)
|
||
switch_list_order(types_de_champ, index_of_first_element)
|
||
end
|
||
|
||
def switch_types_de_champ_private(index_of_first_element)
|
||
switch_list_order(types_de_champ_private, index_of_first_element)
|
||
end
|
||
|
||
def switch_types_de_piece_justificative(index_of_first_element)
|
||
switch_list_order(types_de_piece_justificative, index_of_first_element)
|
||
end
|
||
|
||
def switch_list_order(list, index_of_first_element)
|
||
if index_of_first_element < 0 ||
|
||
index_of_first_element == list.count - 1 ||
|
||
list.count < 1
|
||
|
||
false
|
||
else
|
||
list[index_of_first_element].update(order_place: index_of_first_element + 1)
|
||
list[index_of_first_element + 1].update(order_place: index_of_first_element)
|
||
reload
|
||
|
||
true
|
||
end
|
||
end
|
||
|
||
def clone(admin, from_library)
|
||
populate_champ_stable_ids
|
||
procedure = self.deep_clone(include:
|
||
{
|
||
types_de_piece_justificative: nil,
|
||
attestation_template: nil,
|
||
types_de_champ: :drop_down_list,
|
||
types_de_champ_private: :drop_down_list
|
||
})
|
||
procedure.path = nil
|
||
procedure.aasm_state = :brouillon
|
||
procedure.test_started_at = nil
|
||
procedure.archived_at = nil
|
||
procedure.published_at = nil
|
||
procedure.logo_secure_token = nil
|
||
procedure.remote_logo_url = self.logo_url
|
||
procedure.lien_notice = nil
|
||
|
||
[:notice, :deliberation].each { |attachment| clone_attachment(procedure, attachment) }
|
||
|
||
procedure.administrateur = admin
|
||
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.cloned_from_library = from_library
|
||
procedure.parent_procedure = self
|
||
|
||
if from_library
|
||
procedure.service = nil
|
||
elsif self.service.present? && (self.administrateur_id != admin.id)
|
||
procedure.service = self.service.clone_and_assign_to_administrateur(admin)
|
||
end
|
||
|
||
procedure
|
||
end
|
||
|
||
def whitelisted?
|
||
whitelisted_at.present?
|
||
end
|
||
|
||
def has_old_pjs?
|
||
types_de_piece_justificative.any?
|
||
end
|
||
|
||
def total_dossier
|
||
self.dossiers.state_not_brouillon.size
|
||
end
|
||
|
||
def export_filename(format)
|
||
procedure_identifier = path || "procedure-#{id}"
|
||
"dossiers_#{procedure_identifier}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M')}.#{format}"
|
||
end
|
||
|
||
def export(options = {})
|
||
ProcedureExportService.new(self, **options.to_h.symbolize_keys)
|
||
end
|
||
|
||
def to_csv(options = {})
|
||
export(options).to_csv
|
||
end
|
||
|
||
def to_xlsx(options = {})
|
||
export(options).to_xlsx
|
||
end
|
||
|
||
def to_ods(options = {})
|
||
export(options).to_ods
|
||
end
|
||
|
||
def procedure_overview(start_date)
|
||
ProcedureOverview.new(self, start_date)
|
||
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
|
||
|
||
def self.default_sort
|
||
{
|
||
'table' => 'self',
|
||
'column' => 'id',
|
||
'order' => 'desc'
|
||
}
|
||
end
|
||
|
||
def whitelist!
|
||
update_attribute('whitelisted_at', Time.zone.now)
|
||
end
|
||
|
||
def closed_mail_template_attestation_inconsistency_state
|
||
# As an optimization, don’t check the predefined templates (they are presumed correct)
|
||
if closed_mail.present?
|
||
tag_present = closed_mail.body.include?("--lien attestation--")
|
||
if attestation_template&.activated? && !tag_present
|
||
:missing_tag
|
||
elsif !attestation_template&.activated? && tag_present
|
||
:extraneous_tag
|
||
end
|
||
end
|
||
end
|
||
|
||
def usual_traitement_time
|
||
percentile_time(:en_construction_at, :processed_at, 90)
|
||
end
|
||
|
||
def usual_verification_time
|
||
percentile_time(:en_construction_at, :en_instruction_at, 90)
|
||
end
|
||
|
||
def usual_instruction_time
|
||
percentile_time(:en_instruction_at, :processed_at, 90)
|
||
end
|
||
|
||
PATH_AVAILABLE = :available
|
||
PATH_AVAILABLE_PUBLIEE = :available_publiee
|
||
PATH_NOT_AVAILABLE = :not_available
|
||
PATH_NOT_AVAILABLE_BROUILLON = :not_available_brouillon
|
||
PATH_CAN_PUBLISH = [PATH_AVAILABLE, PATH_AVAILABLE_PUBLIEE]
|
||
|
||
def path_availability(path)
|
||
Procedure.path_availability(administrateur, path, id)
|
||
end
|
||
|
||
def self.path_availability(administrateur, path, exclude_id = nil)
|
||
if exclude_id.present?
|
||
procedure = where.not(id: exclude_id).find_by(path: path)
|
||
else
|
||
procedure = find_by(path: path)
|
||
end
|
||
|
||
if procedure.blank?
|
||
PATH_AVAILABLE
|
||
elsif administrateur.owns?(procedure)
|
||
if procedure.brouillon?
|
||
PATH_NOT_AVAILABLE_BROUILLON
|
||
else
|
||
PATH_AVAILABLE_PUBLIEE
|
||
end
|
||
else
|
||
PATH_NOT_AVAILABLE
|
||
end
|
||
end
|
||
|
||
def self.find_with_path(path)
|
||
where.not(aasm_state: :archivee).where("path LIKE ?", "%#{path}%")
|
||
end
|
||
|
||
def gestionnaire_for_cron_job
|
||
administrateur_email = administrateur.email
|
||
gestionnaire = Gestionnaire.find_by(email: administrateur_email)
|
||
gestionnaire || gestionnaires.first
|
||
end
|
||
|
||
def populate_champ_stable_ids
|
||
TypeDeChamp.where(procedure: self, stable_id: nil).find_each do |type_de_champ|
|
||
type_de_champ.update_column(:stable_id, type_de_champ.id)
|
||
end
|
||
end
|
||
|
||
private
|
||
|
||
def claim_path_ownership!(path)
|
||
procedure = Procedure.where(administrateur: administrateur).find_by(path: path)
|
||
|
||
if procedure&.publiee? && procedure != self
|
||
procedure.archive!
|
||
end
|
||
|
||
update!(path: path)
|
||
end
|
||
|
||
def can_publish?(path)
|
||
path_availability(path).in?(PATH_CAN_PUBLISH)
|
||
end
|
||
|
||
def after_publish(path)
|
||
update!(published_at: Time.zone.now)
|
||
|
||
claim_path_ownership!(path)
|
||
end
|
||
|
||
def after_reopen(path)
|
||
update!(published_at: Time.zone.now, archived_at: nil)
|
||
|
||
claim_path_ownership!(path)
|
||
end
|
||
|
||
def after_archive
|
||
update!(archived_at: Time.zone.now, path: nil)
|
||
end
|
||
|
||
def after_hide
|
||
now = Time.zone.now
|
||
update!(hidden_at: now, path: nil)
|
||
dossiers.update_all(hidden_at: now)
|
||
end
|
||
|
||
def after_draft
|
||
update!(published_at: nil)
|
||
end
|
||
|
||
def update_juridique_required
|
||
self.juridique_required ||= (cadre_juridique.present? || deliberation.attached?)
|
||
true
|
||
end
|
||
|
||
def clone_attachment(cloned_procedure, attachment_symbol)
|
||
attachment = send(attachment_symbol)
|
||
if attachment.attached?
|
||
response = Typhoeus.get(attachment.service_url, timeout: 5)
|
||
if response.success?
|
||
cloned_procedure.send(attachment_symbol).attach(
|
||
io: StringIO.new(response.body),
|
||
filename: attachment.filename
|
||
)
|
||
end
|
||
end
|
||
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 update_durees_conservation_required
|
||
self.durees_conservation_required ||= duree_conservation_dossiers_hors_ds.present? && duree_conservation_dossiers_dans_ds.present?
|
||
true
|
||
end
|
||
|
||
def percentile_time(start_attribute, end_attribute, p)
|
||
times = dossiers
|
||
.state_termine
|
||
.where(end_attribute => 1.month.ago..DateTime.current)
|
||
.pluck(start_attribute, end_attribute)
|
||
.map { |(start_date, end_date)| end_date - start_date }
|
||
|
||
if times.present?
|
||
times.percentile(p).ceil
|
||
end
|
||
end
|
||
end
|