Merge pull request #10465 from colinux/attestations-v2-prod

ETQ admin je peux activer la délivrance des attestations v2 (sous feature flag)
This commit is contained in:
Colin Darie 2024-06-24 08:56:31 +00:00 committed by GitHub
commit ccf5b255ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 724 additions and 258 deletions

View file

@ -623,40 +623,10 @@ textarea::placeholder {
color: $dark-grey; color: $dark-grey;
} }
@media (max-width: 62em) {
.padded-fixed-footer {
padding-top: 120px;
}
}
@media (min-width: 62em) {
.padded-fixed-footer {
padding-top: 60px;
}
}
[data-fr-theme="dark"] .fixed-footer {
border-top: 2px solid var(--background-action-low-blue-france-hover);
background-color: var(--background-action-low-blue-france);
}
.mandatory { .mandatory {
fill: currentColor; fill: currentColor;
} }
.fixed-footer {
border-top: 2px solid $blue-france-500;
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-top: $default-padding;
background-color: $white;
z-index: 2;
}
.fr-menu__list { .fr-menu__list {
padding: $default-spacer; padding: $default-spacer;
overflow-y: auto; overflow-y: auto;

View file

@ -0,0 +1,49 @@
@import "constants";
.fixed-footer {
border-top: 2px solid var(--border-plain-blue-france);
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-top: $default-padding;
background-color: var(--background-default-grey);
z-index: 2;
}
@media (max-width: 62em) {
.padded-fixed-footer {
padding-top: 120px;
}
}
@media (min-width: 62em) {
.padded-fixed-footer {
padding-top: 60px;
}
}
[data-fr-theme="dark"] .fixed-footer {
background-color: var(--background-action-low-blue-france);
}
.sticky-header {
padding-top: $default-padding;
padding-bottom: $default-padding;
&-container {
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 800;
}
&-warning {
background-color: var(--background-contrast-warning);
}
p {
margin: 0;
}
}

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AutosaveNoticeComponent < ApplicationComponent
attr_reader :label_scope
def initialize(success:, label_scope:)
@success = success
@label_scope = label_scope
end
def success? = @success
def label
success? ? t(".#{label_scope}.saved") : t(".#{label_scope}.error")
end
end

View file

@ -0,0 +1,8 @@
---
en:
form:
saved: 'Form saved'
error: 'Form in error'
attestation:
saved: 'Attestation saved'
error: 'Attestation in error'

View file

@ -0,0 +1,8 @@
---
fr:
form:
saved: 'Formulaire enregistré'
error: 'Formulaire en erreur'
attestation:
saved: 'Attestation enregistrée'
error: 'Attestation en erreur'

View file

@ -0,0 +1,2 @@
#autosave-notice.fr-badge.fr-badge--sm{ class: class_names("fr-badge--success" => success?, "fr-badge--error" => !success?) }
= label

View file

@ -26,6 +26,8 @@ class Dsfr::CalloutComponent < ApplicationComponent
"fr-callout--brown-caramel" "fr-callout--brown-caramel"
when :success when :success
"fr-callout--green-emeraude" "fr-callout--green-emeraude"
when :neutral
# default
else else
"fr-background-alt--blue-france" "fr-background-alt--blue-france"
end end

View file

@ -98,9 +98,7 @@ module Dsfr
}) })
end end
if autoresize? @opts.deep_merge!(data: { controller: token_list(@opts.dig(:data, :controller), 'autoresize' => autoresize?) })
@opts.deep_merge!(data: { controller: 'autoresize' })
end
@opts @opts
end end

View file

@ -5,6 +5,14 @@ class Procedure::Card::AttestationComponent < ApplicationComponent
private private
def edit_attestation_path
if @procedure.attestation_templates_v2.any? || @procedure.feature_enabled?(:attestation_v2)
helpers.edit_admin_procedure_attestation_template_v2_path(@procedure)
else
helpers.edit_admin_procedure_attestation_template_path(@procedure)
end
end
def error_messages def error_messages
@procedure.errors.messages_for(:attestation_template).to_sentence @procedure.errors.messages_for(:attestation_template).to_sentence
end end

View file

@ -1,5 +1,5 @@
.fr-col-6.fr-col-md-4.fr-col-lg-3 .fr-col-6.fr-col-md-4.fr-col-lg-3
= link_to edit_admin_procedure_attestation_template_path(@procedure), class: 'fr-tile fr-enlarge-link' do = link_to edit_attestation_path, class: 'fr-tile fr-enlarge-link' do
.fr-tile__body.flex.column.align-center.justify-between .fr-tile__body.flex.column.align-center.justify-between
- if @procedure.attestation_template&.activated? - if @procedure.attestation_template&.activated?
%div %div

View file

@ -20,27 +20,9 @@ module Administrateurs
format.pdf do format.pdf do
html = render_to_string('/administrateurs/attestation_template_v2s/show', layout: 'attestation', formats: [:html]) html = render_to_string('/administrateurs/attestation_template_v2s/show', layout: 'attestation', formats: [:html])
headers = { pdf = WeasyprintService.generate_pdf(html, procedure_id: @procedure.id, path: request.path, user_id: current_user.id)
'Content-Type' => 'application/json',
'X-Request-Id' => Current.request_id
}
body = { send_data(pdf, filename: 'attestation.pdf', type: 'application/pdf', disposition: 'inline')
html: html,
upstream_context: {
procedure_id: @procedure.id,
path: request.path,
user_id: current_user.id
}
}.to_json
response = Typhoeus.post(WEASYPRINT_URL, headers:, body:)
if response.success?
send_data(response.body, filename: 'attestation.pdf', type: 'application/pdf', disposition: 'inline')
else
raise StandardError.new("PDF Generation failed: #{response.return_code} #{response.status_message}")
end
end end
end end
end end
@ -77,6 +59,19 @@ module Administrateurs
def update def update
attestation_params = editor_params attestation_params = editor_params
# toggle activation
if @attestation_template.persisted? && @attestation_template.activated? != cast_bool(attestation_params[:activated])
@procedure.attestation_templates.v2.update_all(activated: attestation_params[:activated])
render :update && return
end
if @attestation_template.published? && should_edit_draft?
@attestation_template = @attestation_template.dup
@attestation_template.state = :draft
@attestation_template.procedure = @procedure
end
logo_file = attestation_params.delete(:logo) logo_file = attestation_params.delete(:logo)
signature_file = attestation_params.delete(:signature) signature_file = attestation_params.delete(:signature)
@ -88,15 +83,40 @@ module Administrateurs
attestation_params[:signature] = uninterlace_png(signature_file) attestation_params[:signature] = uninterlace_png(signature_file)
end end
if !@attestation_template.update(attestation_params) @attestation_template.assign_attributes(attestation_params)
flash.alert = "Le modèle de lattestation contient des erreurs et n'a pas pu être enregistré. Corriger les erreurs."
end
render :update if @attestation_template.invalid?
flash.alert = "Lattestation contient des erreurs et n'a pas pu être enregistrée. Corriger les erreurs."
else
# - draft just published
if @attestation_template.published? && should_edit_draft?
published = @procedure.attestation_templates.published
@attestation_template.transaction do
were_published = published.destroy_all
@attestation_template.save!
flash.notice = were_published.any? ? "La nouvelle version de lattestation a été publiée." : "Lattestation a été publiée."
end
redirect_to edit_admin_procedure_attestation_template_v2_path(@procedure)
else
# - draft updated
# - or, attestation already published, without need for publication (draft procedure)
@attestation_template.save!
render :update
end
end
end end
def create = update def create = update
def reset
@procedure.attestation_templates_v2.draft&.destroy_all
flash.notice = "Les modifications ont été réinitialisées."
redirect_to edit_admin_procedure_attestation_template_v2_path(@procedure)
end
private private
def ensure_feature_active def ensure_feature_active
@ -104,11 +124,19 @@ module Administrateurs
end end
def retrieve_attestation_template def retrieve_attestation_template
@attestation_template = @procedure.attestation_template_v2 || @procedure.build_attestation_template_v2(json_body: AttestationTemplate::TIPTAP_BODY_DEFAULT) v2s = @procedure.attestation_templates_v2
@attestation_template = v2s.find(&:draft?) || v2s.find(&:published?) || build_default_attestation
end end
def build_default_attestation
state = should_edit_draft? ? :draft : :published
@procedure.build_attestation_template(version: 2, json_body: AttestationTemplate::TIPTAP_BODY_DEFAULT, activated: true, state:)
end
def should_edit_draft? = !@procedure.brouillon?
def editor_params def editor_params
params.required(:attestation_template).permit(:official_layout, :label_logo, :label_direction, :tiptap_body, :footer, :logo, :signature, :activated) params.required(:attestation_template).permit(:activated, :official_layout, :label_logo, :label_direction, :tiptap_body, :footer, :logo, :signature, :activated, :state)
end end
end end
end end

View file

@ -112,7 +112,7 @@ module Administrateurs
revision_types_de_champ: { type_de_champ: { piece_justificative_template_attachment: :blob } } revision_types_de_champ: { type_de_champ: { piece_justificative_template_attachment: :blob } }
}, },
attestation_template_v1: [], attestation_template_v1: [],
attestation_template_v2: [], attestation_templates_v2: [],
initiated_mail: [], initiated_mail: [],
received_mail: [], received_mail: [],
closed_mail: [], closed_mail: [],

View file

@ -30,9 +30,10 @@ module Instructeurs
end end
def apercu_attestation def apercu_attestation
@attestation = dossier.attestation_template.render_attributes_for(dossier: dossier) send_data dossier.attestation_template.send(:build_pdf, dossier),
filename: 'attestation.pdf',
render 'administrateurs/attestation_templates/show', formats: [:pdf] type: 'application/pdf',
disposition: 'inline'
end end
def bilans_bdf def bilans_bdf

View file

@ -0,0 +1,43 @@
import { ApplicationController } from './application_controller';
export class StickyTopController extends ApplicationController {
// Ajusts top of sticky top components when there is a sticky header.
connect(): void {
const header = document.getElementById('sticky-header');
if (!header) {
return;
}
this.adjustTop(header);
window.addEventListener('resize', () => this.adjustTop(header));
this.listenHeaderMutations(header);
}
private listenHeaderMutations(header: HTMLElement) {
const config = { childList: true, subtree: true };
const callback: MutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
this.adjustTop(header);
break;
}
}
};
const observer = new MutationObserver(callback);
observer.observe(header, config);
}
private adjustTop(header: HTMLElement) {
const headerHeight = header.clientHeight;
if (headerHeight > 0) {
(this.element as HTMLElement).style.top = `${headerHeight + 8}px`;
}
}
}

View file

@ -2,11 +2,16 @@ class AttestationTemplate < ApplicationRecord
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
include TagsSubstitutionConcern include TagsSubstitutionConcern
belongs_to :procedure, inverse_of: :attestation_template_v2 belongs_to :procedure, inverse_of: :attestation_template
has_one_attached :logo has_one_attached :logo
has_one_attached :signature has_one_attached :signature
enum state: {
draft: 'draft',
published: 'published'
}
validates :title, tags: true, if: -> { procedure.present? && version == 1 } validates :title, tags: true, if: -> { procedure.present? && version == 1 }
validates :body, tags: true, if: -> { procedure.present? && version == 1 } validates :body, tags: true, if: -> { procedure.present? && version == 1 }
validates :json_body, tags: true, if: -> { procedure.present? && version == 2 } validates :json_body, tags: true, if: -> { procedure.present? && version == 2 }
@ -67,9 +72,10 @@ class AttestationTemplate < ApplicationRecord
}.freeze }.freeze
def attestation_for(dossier) def attestation_for(dossier)
attestation = Attestation.new(title: replace_tags(title, dossier, escape: false)) attestation = Attestation.new
attestation.title = replace_tags(title, dossier, escape: false) if version == 1
attestation.pdf.attach( attestation.pdf.attach(
io: build_pdf(dossier), io: StringIO.new(build_pdf(dossier)),
filename: "attestation-dossier-#{dossier.id}.pdf", filename: "attestation-dossier-#{dossier.id}.pdf",
content_type: 'application/pdf', content_type: 'application/pdf',
# we don't want to run virus scanner on this file # we don't want to run virus scanner on this file
@ -91,7 +97,7 @@ class AttestationTemplate < ApplicationRecord
end end
def dup def dup
attestation_template = AttestationTemplate.new(title: title, body: body, footer: footer, activated: activated) attestation_template = super
ClonePiecesJustificativesService.clone_attachments(self, attestation_template) ClonePiecesJustificativesService.clone_attachments(self, attestation_template)
attestation_template attestation_template
end end
@ -179,7 +185,7 @@ class AttestationTemplate < ApplicationRecord
if dossier.present? if dossier.present?
# 2x faster this way than with `replace_tags` which would reparse text # 2x faster this way than with `replace_tags` which would reparse text
used_tags = tiptap.used_tags_and_libelle_for(json.deep_symbolize_keys) used_tags = TiptapService.used_tags_and_libelle_for(json.deep_symbolize_keys)
substitutions = tags_substitutions(used_tags, dossier, escape: false) substitutions = tags_substitutions(used_tags, dossier, escape: false)
body = tiptap.to_html(json, substitutions) body = tiptap.to_html(json, substitutions)
@ -202,17 +208,41 @@ class AttestationTemplate < ApplicationRecord
end end
def used_tags def used_tags
used_tags_for(title) + used_tags_for(body) if version == 2
json = json_body&.deep_symbolize_keys
TiptapService.used_tags_and_libelle_for(json.deep_symbolize_keys).map(&:first)
else
used_tags_for(title) + used_tags_for(body)
end
end end
def build_pdf(dossier) def build_pdf(dossier)
if version == 2
build_v2_pdf(dossier)
else
build_v1_pdf(dossier)
end
end
def build_v1_pdf(dossier)
attestation = render_attributes_for(dossier: dossier) attestation = render_attributes_for(dossier: dossier)
attestation_view = ApplicationController.render( ApplicationController.render(
template: 'administrateurs/attestation_templates/show', template: 'administrateurs/attestation_templates/show',
formats: :pdf, formats: :pdf,
assigns: { attestation: attestation } assigns: { attestation: attestation }
) )
end
StringIO.new(attestation_view) def build_v2_pdf(dossier)
body = render_attributes_for(dossier:).fetch(:body)
html = ApplicationController.render(
template: '/administrateurs/attestation_template_v2s/show',
formats: [:html],
layout: 'attestation',
assigns: { attestation_template: self, body: body }
)
WeasyprintService.generate_pdf(html, { procedure_id: procedure.id, dossier_id: dossier.id })
end end
end end

View file

@ -257,7 +257,7 @@ module TagsSubstitutionConcern
def used_type_de_champ_tags(text_or_tiptap) def used_type_de_champ_tags(text_or_tiptap)
used_tags = used_tags =
if text_or_tiptap.respond_to?(:deconstruct_keys) # hash pattern matching if text_or_tiptap.respond_to?(:deconstruct_keys) # hash pattern matching
TiptapService.new.used_tags_and_libelle_for(text_or_tiptap.deep_symbolize_keys) TiptapService.used_tags_and_libelle_for(text_or_tiptap.deep_symbolize_keys)
else else
used_tags_and_libelle_for(text_or_tiptap.to_s) used_tags_and_libelle_for(text_or_tiptap.to_s)
end end

View file

@ -66,11 +66,10 @@ class ExportTemplate < ApplicationRecord
end end
def render_attributes_for(content_for, dossier, attachment = nil) def render_attributes_for(content_for, dossier, attachment = nil)
tiptap = TiptapService.new used_tags = TiptapService.used_tags_and_libelle_for(content_for.deep_symbolize_keys)
used_tags = tiptap.used_tags_and_libelle_for(content_for.deep_symbolize_keys)
substitutions = tags_substitutions(used_tags, dossier, escape: false, memoize: true) substitutions = tags_substitutions(used_tags, dossier, escape: false, memoize: true)
substitutions['original-filename'] = attachment.filename.base if attachment substitutions['original-filename'] = attachment.filename.base if attachment
tiptap.to_path(content_for.deep_symbolize_keys, substitutions) TiptapService.new.to_path(content_for.deep_symbolize_keys, substitutions)
end end
def specific_tags def specific_tags

View file

@ -50,9 +50,9 @@ class Procedure < ApplicationRecord
has_one :module_api_carto, dependent: :destroy has_one :module_api_carto, dependent: :destroy
has_many :attestation_templates, dependent: :destroy has_many :attestation_templates, dependent: :destroy
has_one :attestation_template_v1, -> { AttestationTemplate.v1 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure has_one :attestation_template_v1, -> { AttestationTemplate.v1 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure
has_one :attestation_template_v2, -> { AttestationTemplate.v2 }, 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, -> { order(Arel.sql("CASE WHEN version = '1' THEN 0 ELSE 1 END")) }, dependent: :destroy, inverse_of: :procedure has_one :attestation_template, -> { published }, dependent: :destroy, inverse_of: :procedure
belongs_to :parent_procedure, class_name: 'Procedure', optional: true belongs_to :parent_procedure, class_name: 'Procedure', optional: true
belongs_to :canonical_procedure, class_name: 'Procedure', optional: true belongs_to :canonical_procedure, class_name: 'Procedure', optional: true

View file

@ -1,18 +1,6 @@
class TiptapService class TiptapService
def to_html(node, substitutions = {})
return '' if node.nil?
children(node[:content], substitutions, 0)
end
def to_path(node, substitutions = {})
return '' if node.nil?
children_path(node[:content], substitutions)
end
# NOTE: node must be deep symbolized keys # NOTE: node must be deep symbolized keys
def used_tags_and_libelle_for(node, tags = Set.new) def self.used_tags_and_libelle_for(node, tags = Set.new)
case node case node
in type: 'mention', attrs: { id:, label: }, **rest in type: 'mention', attrs: { id:, label: }, **rest
tags << [id, label] tags << [id, label]
@ -25,6 +13,18 @@ class TiptapService
tags tags
end end
def to_html(node, substitutions = {})
return '' if node.nil?
children(node[:content], substitutions, 0)
end
def to_path(node, substitutions = {})
return '' if node.nil?
children_path(node[:content], substitutions)
end
private private
def initialize def initialize

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class WeasyprintService
def self.generate_pdf(html, options = {})
headers = {
'Content-Type' => 'application/json',
'X-Request-Id' => Current.request_id
}
body = {
html:,
upstream_context: options
}.to_json
response = Typhoeus.post(WEASYPRINT_URL, headers:, body:)
if response.success?
response.body
else
raise StandardError, "PDF Generation failed: #{response.code} #{response.status_message}"
end
end
end

View file

@ -1,2 +0,0 @@
- success = local_assigns.fetch(:success, true)
#autosave-notice.fr-badge.fr-badge--sm{ class: class_names("fr-badge--success" => success, "fr-badge--error" => !success) }= success ? t(".form_saved") : t(".form_error")

View file

@ -0,0 +1,9 @@
.fr-container
.fr-grid-row.fr-grid-row--middle.fr-pb-3v
.fr-col-12.fr-col-md-4
= link_to admin_procedure_path(id: procedure), class: 'fr-link' do
%span.fr-icon-arrow-left-line.fr-icon--sm
Revenir à lécran de gestion
.fr-col-12.fr-col-md-8.text-right
%span#autosave-notice

View file

@ -0,0 +1,21 @@
.sticky-header.sticky-header-warning
.fr-container
%p.flex.justify-between.align-center.fr-text-default--warning
%span
= dsfr_icon("fr-icon-warning-fill fr-mr-1v")
- if @procedure.attestation_templates.many?
Les modifications effectuées ne seront appliquées quà la prochaine publication.
- else
Lattestation ne sera délivrée quaprès sa publication.
%span.no-wrap
- if @procedure.attestation_templates.many?
= link_to reset_admin_procedure_attestation_template_v2_path(@procedure), class: "fr-btn fr-btn--secondary fr-ml-2w", method: :post do
Réinitialiser les modifications
%button.fr-btn.fr-ml-2w{ form: "attestation-template", name: field_name(:attestation_template, :state), value: "published",
data: { 'disable-with': "Publication en cours…", controller: 'autosave-submit' } }
- if @procedure.attestation_templates.many?
Publier les modifications
- else
Publier

View file

@ -4,7 +4,8 @@
['Attestation']] } ['Attestation']] }
= render NestedForms::FormOwnerComponent.new = render NestedForms::FormOwnerComponent.new
= form_for @attestation_template, url: admin_procedure_attestation_template_v2_path(@procedure), html: { multipart: true }, = form_for @attestation_template, url: admin_procedure_attestation_template_v2_path(@procedure),
html: { multipart: true , id: "attestation-template" },
data: { turbo: 'true', data: { turbo: 'true',
controller: 'autosubmit attestation', controller: 'autosubmit attestation',
autosubmit_debounce_delay_value: 1000, autosubmit_debounce_delay_value: 1000,
@ -19,11 +20,12 @@
tout en respectant la charte de létat. Essayez-la et donnez-nous votre avis tout en respectant la charte de létat. Essayez-la et donnez-nous votre avis
en nous envoyant un email à #{mail_to(Current.contact_email, subject: "Feedback attestation v2")}. en nous envoyant un email à #{mail_to(Current.contact_email, subject: "Feedback attestation v2")}.
%br %br
%strong Les attestations délivrées suivent encore lancien format : - if !@procedure.feature_enabled?(:attestation_v2)
lactivation des attestations basées sur ce format sera bientôt disponible. %strong Les attestations délivrées suivent encore lancien format :
%br lactivation des attestations basées sur ce format sera bientôt disponible.
%br
= link_to("Suivez ce lien pour revenir aux attestations actuellement délivrées", edit_admin_procedure_attestation_template_path(@procedure)) = link_to("Suivez ce lien pour revenir aux attestations actuellement délivrées", edit_admin_procedure_attestation_template_path(@procedure))
.fr-grid-row.fr-grid-row--gutters .fr-grid-row.fr-grid-row--gutters
.fr-col-12.fr-col-lg-7 .fr-col-12.fr-col-lg-7
@ -34,13 +36,23 @@
Lattestation est émise au moment où un dossier est accepté, elle est jointe à lemail daccusé dacceptation. Lattestation est émise au moment où un dossier est accepté, elle est jointe à lemail daccusé dacceptation.
Elle est également disponible au téléchargement depuis lespace personnel de lusager. Elle est également disponible au téléchargement depuis lespace personnel de lusager.
.fr-fieldset__element
= render Dsfr::CalloutComponent.new(title: "Activation de la délivrance de lattestation", theme: :neutral) do |c|
- c.with_html_body do
.fr-toggle.fr-toggle--label-left
= f.check_box :activated, class: "fr-toggle__input", id: dom_id(@attestation_template, :activated)
%label.fr-toggle__label{ for: dom_id(@attestation_template, :activated),
data: { fr_checked_label: "Activée", fr_unchecked_label: "Désactivée" } }
Activer cette option permet la délivrance automatique de lattestation dès lacceptation du dossier.
Désactiver cette option arrête immédiatement lémission de nouvelles attestations.
.fr-fieldset__element .fr-fieldset__element
%h2.fr-h4 En-tête %h2.fr-h4 En-tête
.fr-fieldset__element .fr-fieldset__element
.fr-toggle.fr-toggle--label-left .fr-toggle.fr-toggle--label-left
= f.check_box :official_layout, class: "fr-toggle__input", id: dom_id(@attestation_template, :official_layout), data: { "attestation-target": "layoutToggle"} = f.check_box :official_layout, class: "fr-toggle__input", id: dom_id(@attestation_template, :official_layout), data: { "attestation-target": "layoutToggle"}
%label.fr-toggle__label{ for: dom_id(@attestation_template, :official_layout), data: { fr_checked_label: "Activé", fr_unchecked_label: "Désactivé" } } %label.fr-toggle__label{ for: dom_id(@attestation_template, :official_layout), data: { fr_checked_label: "Oui", fr_unchecked_label: "Non" } }
Je souhaite générer une attestation à la charte de létat (logo avec Marianne) Je souhaite générer une attestation à la charte de létat (logo avec Marianne)
.fr-fieldset__element{ class: class_names("hidden" => !@attestation_template.official_layout?), data: { "attestation-target": 'logoMarianneLabelFieldset'} } .fr-fieldset__element{ class: class_names("hidden" => !@attestation_template.official_layout?), data: { "attestation-target": 'logoMarianneLabelFieldset'} }
@ -77,10 +89,10 @@
%button.fr-btn.fr-btn--secondary.fr-btn--sm{ type: 'button', title: label, class: icon == :hidden ? "hidden" : "fr-icon-#{icon}", data: { action: 'click->tiptap#menuButton', tiptap_target: 'button', tiptap_action: action } } %button.fr-btn.fr-btn--secondary.fr-btn--sm{ type: 'button', title: label, class: icon == :hidden ? "hidden" : "fr-icon-#{icon}", data: { action: 'click->tiptap#menuButton', tiptap_target: 'button', tiptap_action: action } }
= label = label
#editor.tiptap-editor{ data: { tiptap_target: 'editor' }, aria: { describedby: dom_id(f.object, "json-body-messages")} } #editor.tiptap-editor{ data: { tiptap_target: 'editor' }, aria: { describedby: "attestation-template-json-body-messages"} }
= f.hidden_field :tiptap_body, data: { tiptap_target: 'input' } = f.hidden_field :tiptap_body, data: { tiptap_target: 'input' }
.fr-error-text{ id: dom_id(f.object, "json-body-messages"), class: class_names("hidden" => !f.object.errors.include?(:json_body)) } .fr-error-text{ id: "attestation-template-json-body-messages", class: class_names("hidden" => !f.object.errors.include?(:json_body)) }
- if f.object.errors.include?(:json_body) - if f.object.errors.include?(:json_body)
= render partial: "shared/errors_list", locals: { object: f.object, attribute: :json_body } = render partial: "shared/errors_list", locals: { object: f.object, attribute: :json_body }
@ -108,7 +120,7 @@
- c.with_hint { "Exemple: 20 avenue de Ségur, 75007 Paris" } - c.with_hint { "Exemple: 20 avenue de Ségur, 75007 Paris" }
#preview-column.fr-col-12.fr-col-lg-5.fr-background-alt--blue-france #preview-column.fr-col-12.fr-col-lg-5.fr-background-alt--blue-france
.sticky--top.fr-px-1w .sticky--top.fr-px-1w{ data: { controller: "sticky-top" } }
.flex.justify-between.align-center .flex.justify-between.align-center
%h2.fr-h4 Aperçu %h2.fr-h4 Aperçu
%p= link_to 'Prévisualiser en taille réelle', admin_procedure_attestation_template_v2_path(@procedure, format: :pdf), class: 'fr-link', target: '_blank', rel: 'noopener' %p= link_to 'Prévisualiser en taille réelle', admin_procedure_attestation_template_v2_path(@procedure, format: :pdf), class: 'fr-link', target: '_blank', rel: 'noopener'
@ -117,21 +129,10 @@
Laperçu est mis à jour automatiquement après chaque modification. Laperçu est mis à jour automatiquement après chaque modification.
Pour générer un aperçu fidèle avec tous les champs et les dates, créez-vous un dossier et acceptez-le : laperçu lutilisera. Pour générer un aperçu fidèle avec tous les champs et les dates, créez-vous un dossier et acceptez-le : laperçu lutilisera.
.padded-fixed-footer - if @procedure.feature_enabled?(:attestation_v2) && @attestation_template.draft?
.fixed-footer - content_for(:sticky_header) do
.fr-container = render partial: "sticky_header"
.fr-grid-row
.fr-col-12.fr-col-md-7
%ul.fr-btns-group.fr-btns-group--inline-md
%li
= link_to admin_procedure_path(id: @procedure), class: 'fr-btn fr-btn--secondary' do
%span.fr-icon-arrow-go-back-line.fr-icon--sm.fr-mr-1v
Revenir à la démarche
.fr-col-12.fr-col-md-5 .padded-fixed-footer
-# .fr-toggle .fixed-footer#fixed_footer
-# = f.check_box :activated, class: "fr-toggle-input", disabled: true, id: dom_id(@attestation_template, :activated) = render partial: "fixed_footer", locals: { procedure: @procedure }
-# %label.fr-toggle__label{ for: dom_id(@attestation_template, :activated), data: { fr_checked_label: "Attestation activée", fr_unchecked_label: "Attestation désactivée" } }
.text-right
%span#autosave-notice
%p.fr-hint-text Lactivation de cette attestation sera bientôt disponible.

View file

@ -1,5 +1,8 @@
- if @attestation_template.draft?
= turbo_stream.update "sticky-header", render(partial: "sticky_header")
= turbo_stream.show 'autosave-notice' = turbo_stream.show 'autosave-notice'
= turbo_stream.replace 'autosave-notice', render(partial: 'administrateurs/autosave_notice', locals: { success: !@attestation_template.changed? }) = turbo_stream.replace('autosave-notice', render(AutosaveNoticeComponent.new(success: !@attestation_template.changed?, label_scope: :attestation)))
= turbo_stream.hide 'autosave-notice', delay: 15000 = turbo_stream.hide 'autosave-notice', delay: 15000
- if @attestation_template.logo_blob&.previously_new_record? - if @attestation_template.logo_blob&.previously_new_record?
@ -10,7 +13,7 @@
= turbo_stream.update dom_id(@attestation_template, :signature_attachment) do = turbo_stream.update dom_id(@attestation_template, :signature_attachment) do
= render(Attachment::EditComponent.new(attached_file: @attestation_template.signature, direct_upload: false)) = render(Attachment::EditComponent.new(attached_file: @attestation_template.signature, direct_upload: false))
- body_id = dom_id(@attestation_template, "json-body-messages") - body_id = "attestation-template-json-body-messages"
- if @attestation_template.errors.include?(:json_body) - if @attestation_template.errors.include?(:json_body)
= turbo_stream.update body_id do = turbo_stream.update body_id do
= render partial: "shared/errors_list", locals: { object: @attestation_template, attribute: :json_body } = render partial: "shared/errors_list", locals: { object: @attestation_template, attribute: :json_body }

View file

@ -18,7 +18,7 @@
- unless flash.alert - unless flash.alert
= turbo_stream.show 'autosave-notice' = turbo_stream.show 'autosave-notice'
= turbo_stream.replace 'autosave-notice', render(partial: 'administrateurs/autosave_notice') = turbo_stream.replace 'autosave-notice', render(AutosaveNoticeComponent.new(success: true, label_scope: :form))
= turbo_stream.hide 'autosave-notice', delay: 30000 = turbo_stream.hide 'autosave-notice', delay: 30000
- if @destroyed.present? - if @destroyed.present?

View file

@ -45,6 +45,9 @@
#beta #beta
Env Test Env Test
#sticky-header.sticky-header-container
= content_for(:sticky_header)
= render partial: "layouts/header" = render partial: "layouts/header"
%main#contenu{ role: :main } %main#contenu{ role: :main }
= render partial: "layouts/flash_messages" = render partial: "layouts/flash_messages"

View file

@ -71,6 +71,3 @@ en:
path_not_available: path_not_available:
owner: This URL is identical to another of your published procedures. If you publish this procedure, the old one will be unpublished and will no longer be accessible to the public. owner: This URL is identical to another of your published procedures. If you publish this procedure, the old one will be unpublished and will no longer be accessible to the public.
not_owner: This URL is identical to another procedure, you must modify it. not_owner: This URL is identical to another procedure, you must modify it.
autosave_notice:
form_saved: "Form saved"
form_error: "Form in error"

View file

@ -71,6 +71,3 @@ fr:
path_not_available: path_not_available:
owner: Cette url est identique à celle dune autre de vos démarches publiées. Si vous publiez cette démarche, lancienne sera dépubliée et ne sera plus accessible au public. owner: Cette url est identique à celle dune autre de vos démarches publiées. Si vous publiez cette démarche, lancienne sera dépubliée et ne sera plus accessible au public.
not_owner: Cette url est identique à celle dune autre démarche, vous devez la modifier afin de pouvoir publier votre démarche. not_owner: Cette url est identique à celle dune autre démarche, vous devez la modifier afin de pouvoir publier votre démarche.
autosave_notice:
form_saved: "Formulaire enregistré"
form_error: "Formulaire en erreur"

View file

@ -684,7 +684,9 @@ Rails.application.routes.draw do
get 'add_champ_engagement_juridique' get 'add_champ_engagement_juridique'
end end
resource :attestation_template_v2, only: [:show, :edit, :update, :create] resource :attestation_template_v2, only: [:show, :edit, :update, :create] do
post :reset
end
resource :dossier_submitted_message, only: [:edit, :update, :create] resource :dossier_submitted_message, only: [:edit, :update, :create]
# ADDED TO ACCESS IT FROM THE IFRAME # ADDED TO ACCESS IT FROM THE IFRAME

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddStateToAttestationTemplates < ActiveRecord::Migration[7.0]
def change
add_column :attestation_templates, :state, :string, default: 'published'
end
end

View file

@ -0,0 +1,12 @@
class AddAttestationTemplateUnicityIndex < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
# this index was not created on production
if index_exists?(:attestation_templates, [:procedure_id, :version])
remove_index :attestation_templates, [:procedure_id, :version], unique: true, algorithm: :concurrently
end
add_index :attestation_templates, [:procedure_id, :version, :state], name: "index_attestation_templates_on_procedure_version_state", unique: true, algorithm: :concurrently
end
end

View file

@ -175,10 +175,11 @@ ActiveRecord::Schema[7.0].define(version: 2024_05_27_090508) do
t.string "label_logo" t.string "label_logo"
t.boolean "official_layout", default: true, null: false t.boolean "official_layout", default: true, null: false
t.integer "procedure_id" t.integer "procedure_id"
t.string "state", default: "published"
t.text "title" t.text "title"
t.datetime "updated_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false
t.integer "version", default: 1, null: false t.integer "version", default: 1, null: false
t.index ["procedure_id", "version"], name: "index_attestation_templates_on_procedure_id_and_version", unique: true t.index ["procedure_id", "version", "state"], name: "index_attestation_templates_on_procedure_version_state", unique: true
end end
create_table "attestations", id: :serial, force: :cascade do |t| create_table "attestations", id: :serial, force: :cascade do |t|

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
namespace :after_party do
desc 'Deployment task: attestation_template_v2_as_draft'
task backfill_attestation_template_v2_as_draft: :environment do
puts "Running deploy task 'backfill_attestation_template_v2_as_draft'"
AttestationTemplate.v2.update_all(state: :draft)
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -1,7 +1,7 @@
describe Administrateurs::AttestationTemplateV2sController, type: :controller do describe Administrateurs::AttestationTemplateV2sController, type: :controller do
let(:admin) { create(:administrateur) } let(:admin) { create(:administrateur) }
let(:attestation_template) { build(:attestation_template, :v2) } let(:attestation_template) { build(:attestation_template, :v2) }
let!(:procedure) { create(:procedure, administrateur: admin, attestation_template: attestation_template, libelle: "Ma démarche") } let(:procedure) { create(:procedure, :published, administrateur: admin, attestation_template:, libelle: "Ma démarche") }
let(:logo) { fixture_file_upload('spec/fixtures/files/white.png', 'image/png') } let(:logo) { fixture_file_upload('spec/fixtures/files/white.png', 'image/png') }
let(:signature) { fixture_file_upload('spec/fixtures/files/black.png', 'image/png') } let(:signature) { fixture_file_upload('spec/fixtures/files/black.png', 'image/png') }
@ -11,7 +11,7 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
label_logo: "Ministère des specs", label_logo: "Ministère des specs",
label_direction: "RSPEC", label_direction: "RSPEC",
footer: "en bas", footer: "en bas",
activated: false, activated: true,
tiptap_body: { tiptap_body: {
type: :doc, type: :doc,
content: [ content: [
@ -31,11 +31,12 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
describe 'GET #show' do describe 'GET #show' do
subject do subject do
get :show, params: { procedure_id: procedure.id } get :show, params: { procedure_id: procedure.id, format: }
response.body response.body
end end
context 'if an attestation template exists on the procedure' do context 'html' do
let(:format) { :html }
render_views render_views
context 'with preview dossier' do context 'with preview dossier' do
@ -93,20 +94,42 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
end end
end end
end end
context 'pdf' do
render_views
let(:format) { :pdf }
let(:attestation_template) { build(:attestation_template, :v2, signature:) }
let(:dossier) { create(:dossier, :en_construction, procedure:, for_procedure_preview: true) }
before do
html_content = /Ministère des devs.+Mon titre pour Ma démarche.+n° #{dossier.id}/m
context = { procedure_id: procedure.id }
allow(WeasyprintService).to receive(:generate_pdf).with(a_string_matching(html_content), hash_including(context)).and_return('PDF_DATA')
end
it do
is_expected.to eq('PDF_DATA')
end
end
end end
describe 'GET edit' do describe 'GET edit' do
render_views
let(:attestation_template) { nil }
subject do subject do
get :edit, params: { procedure_id: procedure.id } get :edit, params: { procedure_id: procedure.id }
response.body response.body
end end
context 'if an attestation template does not exists yet on the procedure' do context 'if an attestation template does not exists yet on the procedure' do
let(:attestation_template) { nil }
it 'creates new v2 attestation template' do it 'creates new v2 attestation template' do
subject subject
expect(assigns(:attestation_template).version).to eq(2) expect(assigns(:attestation_template).version).to eq(2)
expect(assigns(:attestation_template)).to be_draft
expect(response.body).to have_button("Publier")
expect(response.body).not_to have_link("Réinitialiser les modifications")
end end
end end
@ -116,13 +139,51 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
it 'build new v2 attestation template' do it 'build new v2 attestation template' do
subject subject
expect(assigns(:attestation_template).version).to eq(2) expect(assigns(:attestation_template).version).to eq(2)
expect(assigns(:attestation_template)).to be_draft
end end
end end
context 'if attestation template already exist on v2' do context 'attestation template published exist without draft' do
it 'assigns v2 attestation template' do let(:attestation_template) { build(:attestation_template, :v2, :published) }
it 'mention publication' do
subject subject
expect(assigns(:attestation_template)).to eq(attestation_template) expect(assigns(:attestation_template)).to eq(attestation_template)
expect(response.body).not_to have_link("Réinitialiser les modifications")
expect(response.body).not_to have_button("Publier les modifications")
end
end
context 'attestation template draft already exist on v2' do
let(:attestation_template) { build(:attestation_template, :v2, :draft) }
it 'assigns this draft' do
subject
expect(assigns(:attestation_template)).to eq(attestation_template)
expect(response.body).not_to have_link("Réinitialiser les modifications")
expect(response.body).to have_button("Publier")
end
context 'and a published template also exists' do
before { create(:attestation_template, :v2, :published, procedure:) }
it 'mention publication' do
subject
expect(assigns(:attestation_template)).to eq(attestation_template)
expect(response.body).to have_link("Réinitialiser les modifications")
expect(response.body).to have_button("Publier les modifications")
end
end
end
context 'when procedure is draft' do
let(:procedure) { create(:procedure, :draft, administrateur: admin, attestation_template:, libelle: "Ma démarche") }
it 'built template is already live (published)' do
subject
expect(assigns(:attestation_template).version).to eq(2)
expect(assigns(:attestation_template)).to be_published
expect(response.body).not_to have_button(/Publier/)
end end
end end
end end
@ -140,16 +201,17 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
it "create template" do it "create template" do
subject subject
attestation_template = procedure.reload.attestation_template attestation_template = procedure.reload.attestation_templates.first
expect(attestation_template).to be_draft
expect(attestation_template.official_layout).to eq(true) expect(attestation_template.official_layout).to eq(true)
expect(attestation_template.label_logo).to eq("Ministère des specs") expect(attestation_template.label_logo).to eq("Ministère des specs")
expect(attestation_template.label_direction).to eq("RSPEC") expect(attestation_template.label_direction).to eq("RSPEC")
expect(attestation_template.footer).to eq("en bas") expect(attestation_template.footer).to eq("en bas")
expect(attestation_template.activated).to eq(false) expect(attestation_template.activated).to eq(true)
expect(attestation_template.tiptap_body).to eq(update_params[:tiptap_body]) expect(attestation_template.tiptap_body).to eq(update_params[:tiptap_body])
expect(response.body).to include("Formulaire enregistré") expect(response.body).to include("Attestation enregistrée")
end end
context "with files" do context "with files" do
@ -157,7 +219,7 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
it "upload files" do it "upload files" do
subject subject
attestation_template = procedure.reload.attestation_template attestation_template = procedure.reload.attestation_templates.first
expect(attestation_template.logo.download).to eq(logo.read) expect(attestation_template.logo.download).to eq(logo.read)
expect(attestation_template.signature.download).to eq(signature.read) expect(attestation_template.signature.download).to eq(signature.read)
@ -174,18 +236,25 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
end end
context 'when attestation template is valid' do context 'when attestation template is valid' do
it "update template" do it "create a draft template" do
subject expect { subject }.to change { procedure.attestation_templates.count }.by(1)
attestation_template.reload
# published remains inchanged
expect(attestation_template.reload).to be_published
expect(attestation_template.label_logo).to eq("Ministère des devs")
attestation_template = procedure.attestation_templates.draft.first
expect(attestation_template).to be_draft
expect(attestation_template.official_layout).to eq(true) expect(attestation_template.official_layout).to eq(true)
expect(attestation_template.label_logo).to eq("Ministère des specs") expect(attestation_template.label_logo).to eq("Ministère des specs")
expect(attestation_template.label_direction).to eq("RSPEC") expect(attestation_template.label_direction).to eq("RSPEC")
expect(attestation_template.footer).to eq("en bas") expect(attestation_template.footer).to eq("en bas")
expect(attestation_template.activated).to eq(false) expect(attestation_template.activated).to eq(true)
expect(attestation_template.tiptap_body).to eq(update_params[:tiptap_body]) expect(attestation_template.tiptap_body).to eq(update_params[:tiptap_body])
expect(response.body).to include("Formulaire enregistré") expect(response.body).to include("Attestation enregistrée")
expect(response.body).to include("Publier")
end end
context "with files" do context "with files" do
@ -193,7 +262,8 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
it "upload files" do it "upload files" do
subject subject
attestation_template.reload
attestation_template = procedure.attestation_templates.draft.first
expect(attestation_template.logo.download).to eq(logo.read) expect(attestation_template.logo.download).to eq(logo.read)
expect(attestation_template.signature.download).to eq(signature.read) expect(attestation_template.signature.download).to eq(signature.read)
@ -205,12 +275,67 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
super().merge(tiptap_body: { type: :doc, content: [{ type: :mention, attrs: { id: "tdc12", label: "oops" } }] }.to_json) super().merge(tiptap_body: { type: :doc, content: [{ type: :mention, attrs: { id: "tdc12", label: "oops" } }] }.to_json)
end end
it "render error" do it "renders error" do
subject subject
expect(response.body).to include("Formulaire en erreur") expect(response.body).to include("Attestation en erreur")
expect(response.body).to include('Supprimer cette balise') expect(response.body).to include('Supprimer la balise')
end
end
context "publishing a draft" do
let(:attestation_template) { build(:attestation_template, :draft, :v2) }
let(:update_params) { super().merge(state: :published) }
it "publish and redirect with notice" do
subject
expect(attestation_template.reload).to be_published
expect(flash.notice).to eq("Lattestation a été publiée.")
end
end
end
context 'toggle activation' do
let(:update_params) { super().merge(activated: false) }
it 'toggle attribute of current published attestation' do
subject
expect(procedure.attestation_templates.v2.count).to eq(1)
expect(procedure.attestation_templates.v2.first.activated?).to eq(false)
expect(flash.notice).to be_nil
end
context 'when there is a draft' do
before {
create(:attestation_template, :v2, :draft, procedure:)
}
it 'toggle attribute of both draft & published v2 attestations' do
subject
expect(procedure.attestation_templates.v2.count).to eq(2)
expect(procedure.attestation_templates.v2.all?(&:activated?)).to eq(false)
end end
end end
end end
end end
describe 'POST reset' do
render_views
before {
create(:attestation_template, :v2, :draft, procedure:)
}
subject do
patch :reset, params: { procedure_id: procedure.id }
response.body
end
it "delete draft, keep published" do
expect(procedure.attestation_templates.count).to eq(2)
expect(subject).to redirect_to(edit_admin_procedure_attestation_template_v2_path(procedure))
expect(flash.notice).to include("réinitialisées")
expect(procedure.attestation_templates.count).to eq(1)
expect(procedure.attestation_templates.first).to eq(attestation_template)
end
end
end end

View file

@ -32,10 +32,10 @@ FactoryBot.define do
{ "type" => "paragraph", "attrs" => { "textAlign" => "left" }, "content" => [{ "text" => "Dossier: n° ", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }, { "type" => "paragraph", "attrs" => { "textAlign" => "left" }, "content" => [{ "text" => "Dossier: n° ", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] },
{ {
"type" => "paragraph", "type" => "paragraph",
"content" => [ "content" => [
{ "text" => "Nom: ", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "individual_last_name", "label" => "prénom" } }, { "text" => " ", "type" => "text" }, { "text" => "Nom: ", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "individual_last_name", "label" => "prénom" } }, { "text" => " ", "type" => "text" },
{ "type" => "mention", "attrs" => { "id" => "individual_first_name", "label" => "nom" } }, { "text" => " ", "type" => "text" } { "type" => "mention", "attrs" => { "id" => "individual_first_name", "label" => "nom" } }, { "text" => " ", "type" => "text" }
] ]
} }
] ]
} }

View file

@ -21,9 +21,11 @@ describe AttestationTemplate, type: :model do
context 'with an attestation without images' do context 'with an attestation without images' do
let(:attributes) { attributes_for(:attestation_template) } let(:attributes) { attributes_for(:attestation_template) }
it { is_expected.to have_attributes(attributes) } it "works" do
it { is_expected.to have_attributes(id: nil) } is_expected.to have_attributes(attributes)
it { expect(subject.logo.attached?).to be_falsey } is_expected.to have_attributes(id: nil)
expect(subject.logo.attached?).to be_falsey
end
end end
context 'with an attestation with images' do context 'with an attestation with images' do
@ -56,81 +58,78 @@ describe AttestationTemplate, type: :model do
create(:procedure, create(:procedure,
types_de_champ_public: types_de_champ, types_de_champ_public: types_de_champ,
types_de_champ_private: types_de_champ_private, types_de_champ_private: types_de_champ_private,
for_individual: for_individual,
attestation_template: attestation_template) attestation_template: attestation_template)
end end
let(:for_individual) { false }
let(:individual) { nil }
let(:etablissement) { create(:etablissement) } let(:etablissement) { create(:etablissement) }
let(:types_de_champ) { [] } let(:types_de_champ) { [] }
let(:types_de_champ_private) { [] } let(:types_de_champ_private) { [] }
let!(:dossier) { create(:dossier, procedure: procedure, individual: individual, etablissement: etablissement) } let(:dossier) { create(:dossier, :accepte, procedure:) }
let(:template_title) { 'title' }
let(:template_body) { 'body' } let(:types_de_champ) do
let(:attestation_template) do [
build(:attestation_template, { libelle: 'libelleA' },
title: template_title, { libelle: 'libelleB' }
body: template_body, ]
logo: @logo,
signature: @signature)
end end
before do before do
Timecop.freeze(Time.zone.now) dossier.champs_public
end .find { |champ| champ.libelle == 'libelleA' }
.update(value: 'libelle1')
after do dossier.champs_public
Timecop.return .find { |champ| champ.libelle == 'libelleB' }
end .update(value: 'libelle2')
let(:view_args) do
arguments = nil
allow(ApplicationController).to receive(:render).and_wrap_original do |m, *args|
arguments = args.first[:assigns]
m.call(*args)
end
attestation_template.attestation_for(dossier)
arguments
end end
let(:attestation) { attestation_template.attestation_for(dossier) } let(:attestation) { attestation_template.attestation_for(dossier) }
context 'when the procedure has a type de champ named libelleA et libelleB' do context 'attestation v1' do
let(:types_de_champ) do let(:template_title) { 'title --libelleA--' }
[ let(:template_body) { 'body --libelleB--' }
{ libelle: 'libelleA' }, let(:attestation_template) do
{ libelle: 'libelleB' } build(:attestation_template,
] title: template_title,
body: template_body)
end end
context 'and the are used in the template title and body' do let(:view_args) do
let(:template_title) { 'title --libelleA--' } arguments = nil
let(:template_body) { 'body --libelleB--' }
context 'and their value in the dossier are not nil' do allow(ApplicationController).to receive(:render).and_wrap_original do |m, *args|
before do arguments = args.first[:assigns]
dossier.champs_public m.call(*args)
.find { |champ| champ.libelle == 'libelleA' }
.update(value: 'libelle1')
dossier.champs_public
.find { |champ| champ.libelle == 'libelleB' }
.update(value: 'libelle2')
end
it 'passes the correct parameters to the view' do
expect(view_args[:attestation][:title]).to eq('title libelle1')
expect(view_args[:attestation][:body]).to eq('body libelle2')
end
it 'generates an attestation' do
expect(attestation.title).to eq('title libelle1')
expect(attestation.pdf).to be_attached
end
end end
attestation_template.attestation_for(dossier)
arguments
end
it 'passes the correct parameters and generates an attestation' do
expect(view_args[:attestation][:title]).to eq('title libelle1')
expect(view_args[:attestation][:body]).to eq('body libelle2')
expect(attestation.title).to eq('title libelle1')
expect(attestation.pdf).to be_attached
end
end
context 'attestation v2' do
let(:attestation_template) do
build(:attestation_template, :v2, :with_files, label_logo: "Ministère des specs")
end
before do
stub_request(:post, WEASYPRINT_URL)
.with(body: {
html: /Ministère des specs.+Mon titre pour #{procedure.libelle}.+Dossier: n° #{dossier.id}/m,
upstream_context: { procedure_id: procedure.id, dossier_id: dossier.id }
})
.to_return(body: 'PDF_DATA')
end
it 'generates an attestation' do
expect(attestation.pdf).to be_attached
end end
end end
end end

View file

@ -685,8 +685,24 @@ describe Dossier, type: :model do
describe "#unspecified_attestation_champs" do describe "#unspecified_attestation_champs" do
let(:procedure) { create(:procedure, attestation_template: attestation_template, types_de_champ_public: types_de_champ, types_de_champ_private: types_de_champ_private) } let(:procedure) { create(:procedure, attestation_template: attestation_template, types_de_champ_public: types_de_champ, types_de_champ_private: types_de_champ_private) }
let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) }
let(:types_de_champ) { [] }
let(:types_de_champ_private) { [] } let(:types_de_champ) { [tdc_1, tdc_2, tdc_3, tdc_4] }
let(:types_de_champ_private) { [tdc_5, tdc_6, tdc_7, tdc_8] }
let(:tdc_1) { { libelle: "specified champ-in-title" } }
let(:tdc_2) { { libelle: "unspecified champ-in-title" } }
let(:tdc_3) { { libelle: "specified champ-in-body" } }
let(:tdc_4) { { libelle: "unspecified champ-in-body" } }
let(:tdc_5) { { libelle: "specified annotation privée-in-title" } }
let(:tdc_6) { { libelle: "unspecified annotation privée-in-title" } }
let(:tdc_7) { { libelle: "specified annotation privée-in-body" } }
let(:tdc_8) { { libelle: "unspecified annotation privée-in-body" } }
before do
(dossier.champs_public + dossier.champs_private)
.filter { |c| c.libelle.match?(/^specified/) }
.each { |c| c.update_attribute(:value, "specified") }
end
subject { dossier.unspecified_attestation_champs.map(&:libelle) } subject { dossier.unspecified_attestation_champs.map(&:libelle) }
@ -696,11 +712,11 @@ describe Dossier, type: :model do
it { is_expected.to eq([]) } it { is_expected.to eq([]) }
end end
context "with attestation template" do context "with attestation template v1" do
# Test all combinations: # Test all combinations:
# - with tag specified and unspecified # - with tag specified and unspecified
# - with tag in body and tag in title # - with tag in body and tag in title
# - with tag correponsing to a champ and an annotation privée # - with tag correponding to a champ and an annotation privée
# - with a dash in the champ libelle / tag # - with a dash in the champ libelle / tag
let(:title) { "voici --specified champ-in-title-- un --unspecified champ-in-title-- beau --specified annotation privée-in-title-- titre --unspecified annotation privée-in-title-- non --numéro du dossier--" } let(:title) { "voici --specified champ-in-title-- un --unspecified champ-in-title-- beau --specified annotation privée-in-title-- titre --unspecified annotation privée-in-title-- non --numéro du dossier--" }
let(:body) { "voici --specified champ-in-body-- un --unspecified champ-in-body-- beau --specified annotation privée-in-body-- body --unspecified annotation privée-in-body-- non ?" } let(:body) { "voici --specified champ-in-body-- un --unspecified champ-in-body-- beau --specified annotation privée-in-body-- body --unspecified annotation privée-in-body-- non ?" }
@ -712,27 +728,9 @@ describe Dossier, type: :model do
it { is_expected.to eq([]) } it { is_expected.to eq([]) }
end end
context "wich is enabled" do context "which is enabled" do
let(:activated) { true } let(:activated) { true }
let(:types_de_champ) { [tdc_1, tdc_2, tdc_3, tdc_4] }
let(:types_de_champ_private) { [tdc_5, tdc_6, tdc_7, tdc_8] }
let(:tdc_1) { { libelle: "specified champ-in-title" } }
let(:tdc_2) { { libelle: "unspecified champ-in-title" } }
let(:tdc_3) { { libelle: "specified champ-in-body" } }
let(:tdc_4) { { libelle: "unspecified champ-in-body" } }
let(:tdc_5) { { libelle: "specified annotation privée-in-title" } }
let(:tdc_6) { { libelle: "unspecified annotation privée-in-title" } }
let(:tdc_7) { { libelle: "specified annotation privée-in-body" } }
let(:tdc_8) { { libelle: "unspecified annotation privée-in-body" } }
before do
(dossier.champs_public + dossier.champs_private)
.filter { |c| c.libelle.match?(/^specified/) }
.each { |c| c.update_attribute(:value, "specified") }
end
it do it do
is_expected.to eq([ is_expected.to eq([
"unspecified champ-in-title", "unspecified champ-in-title",
@ -743,6 +741,40 @@ describe Dossier, type: :model do
end end
end end
end end
context "with attestation template v2" do
# Test all combinations:
# - with tag specified and unspecified
# - with tag correponding to a champ and an annotation privée
let(:body) {
[
{ "type" => "mention", "attrs" => { "id" => "tdc#{procedure.types_de_champ_for_tags.find {  _1.libelle == "unspecified champ-in-body" }.stable_id}", "label" => "unspecified champ-in-body" } }
]
}
let(:attestation_template) { build(:attestation_template, :v2) }
before do
tdc_content = (types_de_champ + types_de_champ_private).filter_map do |tdc_config|
next if tdc_config[:libelle].include?("in-title")
{
"type" => "mention",
"attrs" => { "id" => "tdc#{procedure.types_de_champ_for_tags.find { _1.libelle == tdc_config[:libelle] }.stable_id}", "label" => tdc_config[:libelle] }
}
end
json_body = attestation_template.json_body["content"]
attestation_template.json_body["content"][-1]["content"].concat(tdc_content)
attestation_template.save!
end
it do
is_expected.to eq([
"unspecified champ-in-body",
"unspecified annotation privée-in-body"
])
end
end
end end
describe '#build_attestation' do describe '#build_attestation' do

View file

@ -1813,23 +1813,23 @@ describe Procedure do
describe "#attestation_template" do describe "#attestation_template" do
let(:procedure) { create(:procedure) } let(:procedure) { create(:procedure) }
subject { procedure.reload }
context "when there is a v2 created after v1" do context "when there is a v2 draft and a v1" do
before do before do
create(:attestation_template, procedure: procedure) create(:attestation_template, procedure: procedure)
create(:attestation_template, :v2, procedure: procedure) create(:attestation_template, :v2, :draft, procedure: procedure)
end end
it { expect(procedure.attestation_template.version).to eq(1) } it { expect(subject.attestation_template.version).to eq(1) }
end end
context "when there is a v2 created before v1" do context "when there is only a v1" do
before do before do
create(:attestation_template, :v2, procedure: procedure) create(:attestation_template, procedure: procedure)
create(:attestation_template, procedure: procedure, activated: true)
end end
it { expect(procedure.attestation_template.version).to eq(1) } it { expect(subject.attestation_template.version).to eq(1) }
end end
context "when there is only a v2" do context "when there is only a v2" do
@ -1837,7 +1837,23 @@ describe Procedure do
create(:attestation_template, :v2, procedure: procedure) create(:attestation_template, :v2, procedure: procedure)
end end
it { expect(procedure.attestation_template.version).to eq(2) } it { expect(subject.attestation_template.version).to eq(2) }
end
context "when there is a v2 draft" do
before do
create(:attestation_template, :v2, :draft, procedure: procedure)
end
it { expect(subject.attestation_template).to be_nil }
context "and a published" do
before do
create(:attestation_template, :v2, :published, procedure: procedure)
end
it { expect(subject.attestation_template).to be_published }
end
end end
end end

View file

@ -189,7 +189,7 @@ RSpec.describe TiptapService do
describe '#used_tags' do describe '#used_tags' do
it 'returns used tags' do it 'returns used tags' do
expect(described_class.new.used_tags_and_libelle_for(json)).to eq(Set.new([['name', 'Nom']])) expect(described_class.used_tags_and_libelle_for(json)).to eq(Set.new([['name', 'Nom']]))
end end
end end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
describe WeasyprintService do
let(:html) { '<html><body>Hello, World!</body></html>' }
let(:options) { { procedure_id: 1, dossier_id: 2 } }
describe '#generate_pdf' do
context 'when the Weasyprint API responds successfully' do
before do
stub_request(:post, WEASYPRINT_URL)
.with(body: { html: html, upstream_context: options })
.to_return(body: 'PDF_DATA')
end
it 'returns a StringIO object with the PDF data' do
pdf = described_class.generate_pdf(html, options)
expect(pdf).to eq('PDF_DATA')
end
end
end
end

View file

@ -14,8 +14,14 @@ describe 'As an administrateur, I want to manage the procedures attestation',
before { login_as(administrateur.user, scope: :user) } before { login_as(administrateur.user, scope: :user) }
def find_attestation_card(with_nested_selector: nil) def find_attestation_card(with_nested_selector: nil)
attestation_path = if procedure.attestation_template&.version == 2 || procedure.feature_enabled?(:attestation_v2)
edit_admin_procedure_attestation_template_v2_path(procedure)
else
edit_admin_procedure_attestation_template_path(procedure)
end
full_selector = [ full_selector = [
"a[href=\"#{edit_admin_procedure_attestation_template_path(procedure)}\"]", "a[href=\"#{attestation_path}\"]",
with_nested_selector with_nested_selector
].compact.join(" ") ].compact.join(" ")
page.find(full_selector) page.find(full_selector)
@ -65,6 +71,13 @@ describe 'As an administrateur, I want to manage the procedures attestation',
end end
context 'Update attestation v2' do context 'Update attestation v2' do
let(:procedure) do
create(:procedure, :published,
administrateurs: [administrateur],
libelle: 'libellé de la procédure',
path: 'libelle-de-la-procedure')
end
before do before do
Flipper.enable(:attestation_v2) Flipper.enable(:attestation_v2)
@ -81,7 +94,7 @@ describe 'As an administrateur, I want to manage the procedures attestation',
find("a").click find("a").click
end end
expect(procedure.reload.attestation_template_v2).to be_nil expect(procedure.reload.attestation_templates.v2).to be_empty
expect(page).to have_css("label", text: "Logo additionnel") expect(page).to have_css("label", text: "Logo additionnel")
@ -90,12 +103,12 @@ describe 'As an administrateur, I want to manage the procedures attestation',
attestation = nil attestation = nil
wait_until { wait_until {
attestation = procedure.reload.attestation_template_v2 attestation = procedure.reload.attestation_templates.v2.draft.first
attestation.present? attestation.present?
} }
expect(page).to have_content("Attestation enregistrée")
expect(attestation.label_logo).to eq("System Test") expect(attestation.label_logo).to eq("System Test")
expect(attestation.activated?).to be_falsey expect(attestation.activated?).to be_truthy
expect(page).to have_content("Formulaire enregistré")
click_on "date de décision" click_on "date de décision"
@ -129,7 +142,16 @@ describe 'As an administrateur, I want to manage the procedures attestation',
} }
fill_in "Contenu du pied de page", with: ["line1", "line2", "line3", "line4"].join("\n") fill_in "Contenu du pied de page", with: ["line1", "line2", "line3", "line4"].join("\n")
expect(page).to have_field("Contenu du pied de page", with: "line1\nline2\nline3\nline4") expect(page).to have_field("Contenu du pied de page", with: "line1\nline2\nline3line4")
click_on "Publier"
expect(attestation.reload).to be_published
expect(page).to have_text("Lattestation a été publiée")
fill_in "Intitulé de la direction", with: "plop"
click_on "Publier les modifications"
expect(procedure.reload.attestation_template.label_direction).to eq("plop")
expect(page).to have_text(/La nouvelle version de lattestation/)
end end
context "tag in error" do context "tag in error" do
@ -137,7 +159,7 @@ describe 'As an administrateur, I want to manage the procedures attestation',
tdc = procedure.active_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'age') tdc = procedure.active_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'age')
procedure.publish_revision! procedure.publish_revision!
attestation = procedure.build_attestation_template_v2(json_body: AttestationTemplate::TIPTAP_BODY_DEFAULT, label_logo: "test") attestation = procedure.build_attestation_template(version: 2, json_body: AttestationTemplate::TIPTAP_BODY_DEFAULT, label_logo: "test")
attestation.json_body["content"] << { type: :mention, attrs: { id: "tdc#{tdc.stable_id}", label: tdc.libelle } } attestation.json_body["content"] << { type: :mention, attrs: { id: "tdc#{tdc.stable_id}", label: tdc.libelle } }
attestation.save! attestation.save!
@ -150,14 +172,14 @@ describe 'As an administrateur, I want to manage the procedures attestation',
click_on "date de décision" click_on "date de décision"
expect(page).to have_content("Formulaire en erreur") expect(page).to have_content("Attestation en erreur")
expect(page).to have_content("Le champ « Contenu de lattestation » contient la balise \"age\"") expect(page).to have_content("Le champ « Contenu de lattestation » contient la balise \"age\"")
page.execute_script("document.getElementById('attestation_template_tiptap_body').type = 'text'") page.execute_script("document.getElementById('attestation_template_tiptap_body').type = 'text'")
fill_in "attestation_template[tiptap_body]", with: AttestationTemplate::TIPTAP_BODY_DEFAULT.to_json fill_in "attestation_template[tiptap_body]", with: AttestationTemplate::TIPTAP_BODY_DEFAULT.to_json
expect(page).to have_content("Formulaire enregistré") expect(page).to have_content("Attestation enregistrée")
expect(page).not_to have_content("Formulaire en erreur") expect(page).not_to have_content("Attestation en erreur")
expect(page).not_to have_content("Le champ « Contenu de lattestation » contient la balise \"age\"") expect(page).not_to have_content("Le champ « Contenu de lattestation » contient la balise \"age\"")
end end
end end