demarches-normaliennes/app/models/champ.rb
Pierre de La Morinerie 3499f5af9a models: remove invalid Dossier ↔︎ Champ inverse relationship
`Dossier.champs` is not really an inverse of `Champs.dossier`: when a
Champ record is created, it should not always be added to dossier.champs
(for instance if the champ is private).

NB: this breaks the workaround we added in #3907 to fix the parent
dossier not being touched in some cases (the workaround was to add an
inverse relationship, but we now have to remove it).

The new workaround is to watch for `changed_for_autosave?` on champs.
Unlike `changed?`, `changed_for_autosave?` also detects changes to
attachments. This allows us to touch both `last_champ_updated_at` and
`updated_at` in a single pass.
2021-04-06 10:26:17 +02:00

185 lines
5 KiB
Ruby

# == Schema Information
#
# Table name: champs
#
# id :integer not null, primary key
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# row :integer
# type :string
# value :string
# created_at :datetime
# updated_at :datetime
# dossier_id :integer
# etablissement_id :integer
# external_id :string
# parent_id :bigint
# type_de_champ_id :integer
#
class Champ < ApplicationRecord
belongs_to :dossier, -> { with_discarded }, inverse_of: false, touch: true, optional: false
belongs_to :type_de_champ, inverse_of: :champ, optional: false
belongs_to :parent, class_name: 'Champ', optional: true
has_many :commentaires
has_one_attached :piece_justificative_file
# We declare champ specific relationships (Champs::CarteChamp, Champs::SiretChamp and Champs::RepetitionChamp)
# here because otherwise we can't easily use includes in our queries.
has_many :geo_areas, dependent: :destroy
belongs_to :etablissement, optional: true, dependent: :destroy
has_many :champs, -> { ordered }, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
delegate :libelle,
:type_champ,
:procedure,
:order_place,
:mandatory?,
:description,
:drop_down_list_options,
:drop_down_list_options?,
:drop_down_list_disabled_options,
:drop_down_list_enabled_non_empty_options,
:exclude_from_export?,
:exclude_from_view?,
:repetition?,
:dossier_link?,
:titre_identite?,
to: :type_de_champ
scope :updated_since?, -> (date) { where('champs.updated_at > ?', date) }
scope :public_only, -> { where(private: false) }
scope :private_only, -> { where(private: true) }
scope :ordered, -> { includes(:type_de_champ).order(:row, 'types_de_champ.order_place') }
scope :public_ordered, -> { public_only.joins(dossier: { revision: :revision_types_de_champ }).where('procedure_revision_types_de_champ.type_de_champ_id = champs.type_de_champ_id').order(:position) }
# we need to do private champs order as manual join to avoid conflicting join names
scope :private_ordered, -> do
private_only.joins('
INNER JOIN dossiers dossiers_private on dossiers_private.id = champs.dossier_id
INNER JOIN types_de_champ types_de_champ_private on types_de_champ_private.id = champs.type_de_champ_id
INNER JOIN procedure_revision_types_de_champ procedure_revision_types_de_champ_private
ON procedure_revision_types_de_champ_private.revision_id = dossiers_private.revision_id')
.where('procedure_revision_types_de_champ_private.type_de_champ_id = champs.type_de_champ_id')
.order(:position)
end
scope :root, -> { where(parent_id: nil) }
before_create :set_dossier_id, if: :needs_dossier_id?
before_validation :set_dossier_id, if: :needs_dossier_id?
before_save :cleanup_if_empty
after_update_commit :fetch_external_data_later
validates :type_de_champ_id, uniqueness: { scope: [:dossier_id, :row] }
def public?
!private?
end
def siblings
if parent
parent&.champs
elsif public?
dossier&.champs
else
dossier&.champs_private
end
end
def mandatory_and_blank?
mandatory? && blank?
end
def blank?
case type_de_champ.type_champ
when TypeDeChamp.type_champs.fetch(:carte)
geo_areas.blank? || value == '[]'
when TypeDeChamp.type_champs.fetch(:multiple_drop_down_list)
value.blank? || value == '[]'
else
value.blank?
end
end
def search_terms
[to_s]
end
def to_s
value.present? ? value.to_s : ''
end
def for_export
value.presence
end
def for_api
value
end
def for_api_v2
to_s
end
def for_tag
value.present? ? value.to_s : ''
end
def main_value_name
:value
end
def to_typed_id
type_de_champ.to_typed_id
end
def html_label?
true
end
def stable_id
type_de_champ.stable_id
end
def log_fetch_external_data_exception(exception)
exceptions = self.fetch_external_data_exceptions ||= []
exceptions << exception.inspect
update_column(:fetch_external_data_exceptions, exceptions)
end
def fetch_external_data?
false
end
def fetch_external_data
raise NotImplemented.new(:fetch_external_data)
end
private
def needs_dossier_id?
!dossier_id && parent_id
end
def set_dossier_id
self.dossier_id = parent.dossier_id
end
def cleanup_if_empty
if external_id_changed?
self.data = nil
end
end
def fetch_external_data_later
if fetch_external_data? && external_id.present? && data.nil?
ChampFetchExternalDataJob.perform_later(self, external_id)
end
end
class NotImplemented < ::StandardError
def initialize(method)
super(":#{method} not implemented")
end
end
end