From dd4c1f2facc3b91a289dd93fc1a9f25c69b8f0c6 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 2 Nov 2022 09:24:20 +0100 Subject: [PATCH] feat(emails): validate tags in notification emails --- app/models/mails/closed_mail.rb | 3 + app/models/mails/initiated_mail.rb | 3 + app/models/mails/received_mail.rb | 3 + app/models/mails/refused_mail.rb | 3 + app/models/mails/without_continuation_mail.rb | 3 + app/models/procedure.rb | 8 ++ app/validators/tags_validator.rb | 64 ++++++++++++++ .../models/attestation_template/fr.yml | 20 +++++ config/locales/models/closed_mail/fr.yml | 20 +++++ config/locales/models/initiated_mail/fr.yml | 20 +++++ config/locales/models/received_mail/fr.yml | 20 +++++ config/locales/models/refused_mail/fr.yml | 20 +++++ .../models/without_continuation_mail/fr.yml | 20 +++++ spec/models/mail_template_spec.rb | 87 +++++++++++++++++++ 14 files changed, 294 insertions(+) create mode 100644 app/validators/tags_validator.rb create mode 100644 config/locales/models/attestation_template/fr.yml create mode 100644 config/locales/models/closed_mail/fr.yml create mode 100644 config/locales/models/initiated_mail/fr.yml create mode 100644 config/locales/models/received_mail/fr.yml create mode 100644 config/locales/models/refused_mail/fr.yml create mode 100644 config/locales/models/without_continuation_mail/fr.yml create mode 100644 spec/models/mail_template_spec.rb diff --git a/app/models/mails/closed_mail.rb b/app/models/mails/closed_mail.rb index 7d32ebba8..60660e23a 100644 --- a/app/models/mails/closed_mail.rb +++ b/app/models/mails/closed_mail.rb @@ -15,6 +15,9 @@ module Mails belongs_to :procedure, optional: false + validates :subject, tags: true + validates :body, tags: true + SLUG = "closed_mail" DISPLAYED_NAME = "Accusé d’acceptation" DEFAULT_SUBJECT = 'Votre dossier nº --numéro du dossier-- a été accepté (--libellé démarche--)' diff --git a/app/models/mails/initiated_mail.rb b/app/models/mails/initiated_mail.rb index 627473b43..0178971ff 100644 --- a/app/models/mails/initiated_mail.rb +++ b/app/models/mails/initiated_mail.rb @@ -15,6 +15,9 @@ module Mails belongs_to :procedure, optional: false + validates :subject, tags: true + validates :body, tags: true + SLUG = "initiated_mail" DEFAULT_TEMPLATE_NAME = "notification_mailer/default_templates/initiated_mail" DISPLAYED_NAME = I18n.t('activerecord.models.mail.initiated_mail.proof_of_receipt') diff --git a/app/models/mails/received_mail.rb b/app/models/mails/received_mail.rb index 3eb4ecbfe..ccfc6ecc1 100644 --- a/app/models/mails/received_mail.rb +++ b/app/models/mails/received_mail.rb @@ -15,6 +15,9 @@ module Mails belongs_to :procedure, optional: false + validates :subject, tags: true + validates :body, tags: true + SLUG = "received_mail" DEFAULT_TEMPLATE_NAME = "notification_mailer/default_templates/received_mail" DISPLAYED_NAME = I18n.t('activerecord.models.mail.received_mail.under_instruction') diff --git a/app/models/mails/refused_mail.rb b/app/models/mails/refused_mail.rb index 2f67c33b7..50dfea0ea 100644 --- a/app/models/mails/refused_mail.rb +++ b/app/models/mails/refused_mail.rb @@ -15,6 +15,9 @@ module Mails belongs_to :procedure, optional: false + validates :subject, tags: true + validates :body, tags: true + SLUG = "refused_mail" DEFAULT_TEMPLATE_NAME = "notification_mailer/default_templates/refused_mail" DISPLAYED_NAME = 'Accusé de rejet du dossier' diff --git a/app/models/mails/without_continuation_mail.rb b/app/models/mails/without_continuation_mail.rb index 0c185b363..28558f330 100644 --- a/app/models/mails/without_continuation_mail.rb +++ b/app/models/mails/without_continuation_mail.rb @@ -15,6 +15,9 @@ module Mails belongs_to :procedure, optional: false + validates :subject, tags: true + validates :body, tags: true + SLUG = "without_continuation" DEFAULT_TEMPLATE_NAME = "notification_mailer/default_templates/without_continuation_mail" DISPLAYED_NAME = 'Accusé de classement sans suite' diff --git a/app/models/procedure.rb b/app/models/procedure.rb index a9e016e5f..f414b091f 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -181,6 +181,14 @@ class Procedure < ApplicationRecord types_de_champ_for_tags.private_only end + def revision_ids_with_pending_dossiers + dossiers + .where.not(revision_id: [draft_revision_id, published_revision_id].compact) + .state_en_construction_ou_instruction + .distinct(:revision_id) + .pluck(:revision_id) + end + has_many :administrateurs_procedures, dependent: :delete_all has_many :administrateurs, through: :administrateurs_procedures, after_remove: -> (procedure, _admin) { procedure.validate! } has_many :groupe_instructeurs, dependent: :destroy diff --git a/app/validators/tags_validator.rb b/app/validators/tags_validator.rb new file mode 100644 index 000000000..d45a858db --- /dev/null +++ b/app/validators/tags_validator.rb @@ -0,0 +1,64 @@ +class TagsValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + procedure = record.procedure + tags = record.used_type_de_champ_tags(value || '') + + invalid_tags = tags.filter_map do |(tag, stable_id)| + tag if stable_id.nil? + end + + invalid_for_draft_revision = invalid_tags_for_revision(record, attribute, tags, procedure.draft_revision_id) + + invalid_for_published_revision = if procedure.published_revision_id.present? + invalid_tags_for_revision(record, attribute, tags, procedure.published_revision_id) + else + [] + end + + invalid_for_previous_revision = procedure + .revision_ids_with_pending_dossiers + .flat_map do |revision_id| + invalid_tags_for_revision(record, attribute, tags, revision_id) + end.uniq + + # champ is added in draft revision but not yet published + champ_missing_in_published_revision = (invalid_for_published_revision - invalid_for_draft_revision) + add_errors(record, attribute, :champ_missing_in_published_revision, champ_missing_in_published_revision) + + # champ is removed but the removal is not yet published + champ_missing_in_draft_revision = (invalid_for_draft_revision - invalid_for_published_revision) + add_errors(record, attribute, :champ_missing_in_draft_revision, champ_missing_in_draft_revision) + + # champ is removed and the removal is published + champ_missing_in_published_and_draft_revision = invalid_for_published_revision.intersection(invalid_for_draft_revision) + add_errors(record, attribute, :champ_missing_in_published_and_draft_revision, champ_missing_in_published_and_draft_revision) + + # champ is missing from one of the revisions in pending dossiers + add_errors(record, attribute, :champ_missing_in_previous_revision, invalid_for_previous_revision) + + # unknown champ + add_errors(record, attribute, :champ_missing, invalid_tags) + end + + private + + def add_errors(record, attribute, message, tags) + tags.each do |tag| + record.errors.add(attribute, message, tag: tag) + end + end + + def invalid_tags_for_revision(record, attribute, tags, revision_id) + revision_stable_ids = TypeDeChamp + .joins(:revision_types_de_champ) + .where(procedure_revision_types_de_champ: { revision_id: revision_id, parent_id: nil }) + .distinct(:stable_id) + .pluck(:stable_id) + + tags.filter_map do |(tag, stable_id)| + if stable_id.present? && !stable_id.in?(revision_stable_ids) + tag + end + end + end +end diff --git a/config/locales/models/attestation_template/fr.yml b/config/locales/models/attestation_template/fr.yml new file mode 100644 index 000000000..3949499dd --- /dev/null +++ b/config/locales/models/attestation_template/fr.yml @@ -0,0 +1,20 @@ +fr: + activerecord: + errors: + models: + attestation_template: + attributes: + title: + format: Le titre du modèl de l’attestation %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement + body: + format: Le contenu du modèl de l’attestation %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement diff --git a/config/locales/models/closed_mail/fr.yml b/config/locales/models/closed_mail/fr.yml new file mode 100644 index 000000000..e7d916333 --- /dev/null +++ b/config/locales/models/closed_mail/fr.yml @@ -0,0 +1,20 @@ +fr: + activerecord: + errors: + models: + mails/closed_mail: + attributes: + subject: + format: Le titre de l’email de notification d’acceptation de dossier %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement + body: + format: Le contenu de l’email de notification d’acceptation de dossier %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement diff --git a/config/locales/models/initiated_mail/fr.yml b/config/locales/models/initiated_mail/fr.yml new file mode 100644 index 000000000..636bf9a13 --- /dev/null +++ b/config/locales/models/initiated_mail/fr.yml @@ -0,0 +1,20 @@ +fr: + activerecord: + errors: + models: + mails/initiated_mail: + attributes: + subject: + format: Le titre de l’email de notification de passage du dossier en instruction %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement + body: + format: Le contenu de l’email de notification de passage du dossier en instruction %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement diff --git a/config/locales/models/received_mail/fr.yml b/config/locales/models/received_mail/fr.yml new file mode 100644 index 000000000..8a71af9c9 --- /dev/null +++ b/config/locales/models/received_mail/fr.yml @@ -0,0 +1,20 @@ +fr: + activerecord: + errors: + models: + mails/received_mail: + attributes: + subject: + format: Le titre de l’email de notification de dépot de dossier %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement + body: + format: Le contenu de l’email de notification de dépot de dossier %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement diff --git a/config/locales/models/refused_mail/fr.yml b/config/locales/models/refused_mail/fr.yml new file mode 100644 index 000000000..cc7909cb1 --- /dev/null +++ b/config/locales/models/refused_mail/fr.yml @@ -0,0 +1,20 @@ +fr: + activerecord: + errors: + models: + mails/refused_mail: + attributes: + subject: + format: Le titre de l’email de notification de refus de dossier %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement + body: + format: Le contenu de l’email de notification de refus de dossier %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement diff --git a/config/locales/models/without_continuation_mail/fr.yml b/config/locales/models/without_continuation_mail/fr.yml new file mode 100644 index 000000000..e376af725 --- /dev/null +++ b/config/locales/models/without_continuation_mail/fr.yml @@ -0,0 +1,20 @@ +fr: + activerecord: + errors: + models: + mails/without_continuation_mail: + attributes: + subject: + format: Le titre de l’email de notification de classement sans suite de dossier %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement + body: + format: Le contenu de l’email de notification de classement sans suite de dossier %{message} + champ_missing: réfère au champ "%{tag}" qui n’existe pas + champ_missing_in_draft_revision: réfère au champ "%{tag}" qui à été supprimé mais la supression n’est pas encore publiée + champ_missing_in_published_revision: réfère au champ "%{tag}" qui n’est pas encore publiée + champ_missing_in_published_and_draft_revision: réfère au champ "%{tag}" qui à été supprimé + champ_missing_in_previous_revision: réfère au champ "%{tag}" qui n’existe pas sur un des dossiers en cours de traitement diff --git a/spec/models/mail_template_spec.rb b/spec/models/mail_template_spec.rb new file mode 100644 index 000000000..80a66d417 --- /dev/null +++ b/spec/models/mail_template_spec.rb @@ -0,0 +1,87 @@ +describe Mails::InitiatedMail, type: :model do + let(:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :text, libelle: 'nom' }]) } + let(:type_de_champ) { procedure.draft_revision.types_de_champ_public.first } + let(:mail) { described_class.default_for_procedure(procedure) } + + let(:email_subject) { '' } + let(:email_body) { '' } + + subject do + mail.subject = email_subject + mail.body = email_body + mail.validate + mail + end + + describe 'body' do + context 'empty template' do + it { expect(subject.errors).to be_empty } + end + + context 'template with valid tag' do + let(:email_body) { 'foo --numéro du dossier-- bar' } + + it { expect(subject.errors).to be_empty } + end + + context 'template with new valid tag' do + let(:email_body) { 'foo --age-- bar' } + + before do + procedure.draft_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'age') + procedure.publish_revision! + end + + it { expect(subject.errors).to be_empty } + end + + context 'template with invalid tag' do + let(:email_body) { 'foo --numéro du -- bar' } + + it { expect(subject.errors.full_messages).to eq(["Le contenu de l’email de notification de passage du dossier en instruction réfère au champ \"numéro du \" qui n’existe pas"]) } + end + + context 'template with unpublished tag' do + let(:email_body) { 'foo --age-- bar' } + + before do + procedure.draft_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'age') + end + + it { expect(subject.errors.full_messages).to eq(["Le contenu de l’email de notification de passage du dossier en instruction réfère au champ \"age\" qui n’est pas encore publiée"]) } + end + + context 'template with removed but unpublished tag' do + let(:email_body) { 'foo --nom-- bar' } + + before do + procedure.draft_revision.remove_type_de_champ(type_de_champ.stable_id) + end + + it { expect(subject.errors.full_messages).to eq(["Le contenu de l’email de notification de passage du dossier en instruction réfère au champ \"nom\" qui à été supprimé mais la supression n’est pas encore publiée"]) } + end + + context 'template with removed tag' do + let(:email_body) { 'foo --nom-- bar' } + + before do + procedure.draft_revision.remove_type_de_champ(type_de_champ.stable_id) + procedure.publish_revision! + end + + it { expect(subject.errors.full_messages).to eq(["Le contenu de l’email de notification de passage du dossier en instruction réfère au champ \"nom\" qui à été supprimé"]) } + end + + context 'template with new tag and old dossier' do + let(:email_body) { 'foo --age-- bar' } + + before do + create(:dossier, :en_construction, procedure: procedure) + procedure.draft_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'age') + procedure.publish_revision! + end + + it { expect(subject.errors.full_messages).to eq(["Le contenu de l’email de notification de passage du dossier en instruction réfère au champ \"age\" qui n’existe pas sur un des dossiers en cours de traitement"]) } + end + end +end