diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index ebc1ad87c..fa82639c5 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -623,40 +623,10 @@ textarea::placeholder { 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 { 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 { padding: $default-spacer; overflow-y: auto; diff --git a/app/assets/stylesheets/sticky.scss b/app/assets/stylesheets/sticky.scss new file mode 100644 index 000000000..c9920a4ff --- /dev/null +++ b/app/assets/stylesheets/sticky.scss @@ -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; + } +} diff --git a/app/components/autosave_notice_component.rb b/app/components/autosave_notice_component.rb new file mode 100644 index 000000000..7d78c119d --- /dev/null +++ b/app/components/autosave_notice_component.rb @@ -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 diff --git a/app/components/autosave_notice_component/autosave_notice_component.en.yml b/app/components/autosave_notice_component/autosave_notice_component.en.yml new file mode 100644 index 000000000..ec721bd1a --- /dev/null +++ b/app/components/autosave_notice_component/autosave_notice_component.en.yml @@ -0,0 +1,8 @@ +--- +en: + form: + saved: 'Form saved' + error: 'Form in error' + attestation: + saved: 'Attestation saved' + error: 'Attestation in error' diff --git a/app/components/autosave_notice_component/autosave_notice_component.fr.yml b/app/components/autosave_notice_component/autosave_notice_component.fr.yml new file mode 100644 index 000000000..9f56b2f82 --- /dev/null +++ b/app/components/autosave_notice_component/autosave_notice_component.fr.yml @@ -0,0 +1,8 @@ +--- +fr: + form: + saved: 'Formulaire enregistré' + error: 'Formulaire en erreur' + attestation: + saved: 'Attestation enregistrée' + error: 'Attestation en erreur' diff --git a/app/components/autosave_notice_component/autosave_notice_component.html.haml b/app/components/autosave_notice_component/autosave_notice_component.html.haml new file mode 100644 index 000000000..2c59662fa --- /dev/null +++ b/app/components/autosave_notice_component/autosave_notice_component.html.haml @@ -0,0 +1,2 @@ +#autosave-notice.fr-badge.fr-badge--sm{ class: class_names("fr-badge--success" => success?, "fr-badge--error" => !success?) } + = label diff --git a/app/components/dsfr/callout_component.rb b/app/components/dsfr/callout_component.rb index 2d394d939..3e343011b 100644 --- a/app/components/dsfr/callout_component.rb +++ b/app/components/dsfr/callout_component.rb @@ -26,6 +26,8 @@ class Dsfr::CalloutComponent < ApplicationComponent "fr-callout--brown-caramel" when :success "fr-callout--green-emeraude" + when :neutral + # default else "fr-background-alt--blue-france" end diff --git a/app/components/dsfr/input_errorable.rb b/app/components/dsfr/input_errorable.rb index 836ab9c1b..efe7de775 100644 --- a/app/components/dsfr/input_errorable.rb +++ b/app/components/dsfr/input_errorable.rb @@ -98,9 +98,7 @@ module Dsfr }) end - if autoresize? - @opts.deep_merge!(data: { controller: 'autoresize' }) - end + @opts.deep_merge!(data: { controller: token_list(@opts.dig(:data, :controller), 'autoresize' => autoresize?) }) @opts end diff --git a/app/components/procedure/card/attestation_component.rb b/app/components/procedure/card/attestation_component.rb index 386864a6f..5cbac3dff 100644 --- a/app/components/procedure/card/attestation_component.rb +++ b/app/components/procedure/card/attestation_component.rb @@ -5,6 +5,14 @@ class Procedure::Card::AttestationComponent < ApplicationComponent 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 @procedure.errors.messages_for(:attestation_template).to_sentence end diff --git a/app/components/procedure/card/attestation_component/attestation_component.html.haml b/app/components/procedure/card/attestation_component/attestation_component.html.haml index 0b86b5997..40a68535e 100644 --- a/app/components/procedure/card/attestation_component/attestation_component.html.haml +++ b/app/components/procedure/card/attestation_component/attestation_component.html.haml @@ -1,5 +1,5 @@ .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 - if @procedure.attestation_template&.activated? %div diff --git a/app/controllers/administrateurs/attestation_template_v2s_controller.rb b/app/controllers/administrateurs/attestation_template_v2s_controller.rb index f5ace97c7..269373773 100644 --- a/app/controllers/administrateurs/attestation_template_v2s_controller.rb +++ b/app/controllers/administrateurs/attestation_template_v2s_controller.rb @@ -20,27 +20,9 @@ module Administrateurs format.pdf do html = render_to_string('/administrateurs/attestation_template_v2s/show', layout: 'attestation', formats: [:html]) - headers = { - 'Content-Type' => 'application/json', - 'X-Request-Id' => Current.request_id - } + pdf = WeasyprintService.generate_pdf(html, procedure_id: @procedure.id, path: request.path, user_id: current_user.id) - body = { - 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 + send_data(pdf, filename: 'attestation.pdf', type: 'application/pdf', disposition: 'inline') end end end @@ -77,6 +59,19 @@ module Administrateurs def update 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) signature_file = attestation_params.delete(:signature) @@ -88,15 +83,40 @@ module Administrateurs attestation_params[:signature] = uninterlace_png(signature_file) end - if !@attestation_template.update(attestation_params) - flash.alert = "Le modèle de l’attestation contient des erreurs et n'a pas pu être enregistré. Corriger les erreurs." - end + @attestation_template.assign_attributes(attestation_params) - render :update + if @attestation_template.invalid? + flash.alert = "L’attestation 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 l’attestation a été publiée." : "L’attestation 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 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 def ensure_feature_active @@ -104,11 +124,19 @@ module Administrateurs end 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 + 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 - 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 diff --git a/app/controllers/administrateurs/procedures_controller.rb b/app/controllers/administrateurs/procedures_controller.rb index 1c68058ea..874290617 100644 --- a/app/controllers/administrateurs/procedures_controller.rb +++ b/app/controllers/administrateurs/procedures_controller.rb @@ -112,7 +112,7 @@ module Administrateurs revision_types_de_champ: { type_de_champ: { piece_justificative_template_attachment: :blob } } }, attestation_template_v1: [], - attestation_template_v2: [], + attestation_templates_v2: [], initiated_mail: [], received_mail: [], closed_mail: [], diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 3fa84c917..189477bff 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -30,9 +30,10 @@ module Instructeurs end def apercu_attestation - @attestation = dossier.attestation_template.render_attributes_for(dossier: dossier) - - render 'administrateurs/attestation_templates/show', formats: [:pdf] + send_data dossier.attestation_template.send(:build_pdf, dossier), + filename: 'attestation.pdf', + type: 'application/pdf', + disposition: 'inline' end def bilans_bdf diff --git a/app/javascript/controllers/sticky_top_controller.ts b/app/javascript/controllers/sticky_top_controller.ts new file mode 100644 index 000000000..c8490cea0 --- /dev/null +++ b/app/javascript/controllers/sticky_top_controller.ts @@ -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`; + } + } +} diff --git a/app/models/attestation_template.rb b/app/models/attestation_template.rb index eea88355c..98a5dbe00 100644 --- a/app/models/attestation_template.rb +++ b/app/models/attestation_template.rb @@ -2,11 +2,16 @@ class AttestationTemplate < ApplicationRecord include ActionView::Helpers::NumberHelper include TagsSubstitutionConcern - belongs_to :procedure, inverse_of: :attestation_template_v2 + belongs_to :procedure, inverse_of: :attestation_template has_one_attached :logo has_one_attached :signature + enum state: { + draft: 'draft', + published: 'published' + } + validates :title, 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 } @@ -67,9 +72,10 @@ class AttestationTemplate < ApplicationRecord }.freeze 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( - io: build_pdf(dossier), + io: StringIO.new(build_pdf(dossier)), filename: "attestation-dossier-#{dossier.id}.pdf", content_type: 'application/pdf', # we don't want to run virus scanner on this file @@ -91,7 +97,7 @@ class AttestationTemplate < ApplicationRecord end def dup - attestation_template = AttestationTemplate.new(title: title, body: body, footer: footer, activated: activated) + attestation_template = super ClonePiecesJustificativesService.clone_attachments(self, attestation_template) attestation_template end @@ -179,7 +185,7 @@ class AttestationTemplate < ApplicationRecord if dossier.present? # 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) body = tiptap.to_html(json, substitutions) @@ -202,17 +208,41 @@ class AttestationTemplate < ApplicationRecord end 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 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_view = ApplicationController.render( + ApplicationController.render( template: 'administrateurs/attestation_templates/show', formats: :pdf, 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 diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index ae899dc04..6ccef4219 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -257,7 +257,7 @@ module TagsSubstitutionConcern def used_type_de_champ_tags(text_or_tiptap) used_tags = 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 used_tags_and_libelle_for(text_or_tiptap.to_s) end diff --git a/app/models/export_template.rb b/app/models/export_template.rb index 550bb57cd..f3ee41235 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -66,11 +66,10 @@ class ExportTemplate < ApplicationRecord end def render_attributes_for(content_for, dossier, attachment = nil) - tiptap = TiptapService.new - used_tags = tiptap.used_tags_and_libelle_for(content_for.deep_symbolize_keys) + used_tags = TiptapService.used_tags_and_libelle_for(content_for.deep_symbolize_keys) substitutions = tags_substitutions(used_tags, dossier, escape: false, memoize: true) 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 def specific_tags diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 950f7a70d..c336dcce5 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -50,9 +50,9 @@ class Procedure < ApplicationRecord has_one :module_api_carto, 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_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 :canonical_procedure, class_name: 'Procedure', optional: true diff --git a/app/services/tiptap_service.rb b/app/services/tiptap_service.rb index 5d0cb4325..3bbee3494 100644 --- a/app/services/tiptap_service.rb +++ b/app/services/tiptap_service.rb @@ -1,18 +1,6 @@ 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 - def used_tags_and_libelle_for(node, tags = Set.new) + def self.used_tags_and_libelle_for(node, tags = Set.new) case node in type: 'mention', attrs: { id:, label: }, **rest tags << [id, label] @@ -25,6 +13,18 @@ class TiptapService tags 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 def initialize diff --git a/app/services/weasyprint_service.rb b/app/services/weasyprint_service.rb new file mode 100644 index 000000000..d0ab2944b --- /dev/null +++ b/app/services/weasyprint_service.rb @@ -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 diff --git a/app/views/administrateurs/_autosave_notice.html.haml b/app/views/administrateurs/_autosave_notice.html.haml deleted file mode 100644 index 989e3970d..000000000 --- a/app/views/administrateurs/_autosave_notice.html.haml +++ /dev/null @@ -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") diff --git a/app/views/administrateurs/attestation_template_v2s/_fixed_footer.html.haml b/app/views/administrateurs/attestation_template_v2s/_fixed_footer.html.haml new file mode 100644 index 000000000..e66ef2949 --- /dev/null +++ b/app/views/administrateurs/attestation_template_v2s/_fixed_footer.html.haml @@ -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 diff --git a/app/views/administrateurs/attestation_template_v2s/_sticky_header.html.haml b/app/views/administrateurs/attestation_template_v2s/_sticky_header.html.haml new file mode 100644 index 000000000..d209f5d4d --- /dev/null +++ b/app/views/administrateurs/attestation_template_v2s/_sticky_header.html.haml @@ -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 + L’attestation ne sera délivrée qu’aprè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 diff --git a/app/views/administrateurs/attestation_template_v2s/edit.html.haml b/app/views/administrateurs/attestation_template_v2s/edit.html.haml index 8ef9c6cfc..7c995e6b2 100644 --- a/app/views/administrateurs/attestation_template_v2s/edit.html.haml +++ b/app/views/administrateurs/attestation_template_v2s/edit.html.haml @@ -4,7 +4,8 @@ ['Attestation']] } = 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', controller: 'autosubmit attestation', autosubmit_debounce_delay_value: 1000, @@ -19,11 +20,12 @@ 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")}. %br - %strong Les attestations délivrées suivent encore l’ancien format : - l’activation des attestations basées sur ce format sera bientôt disponible. - %br + - if !@procedure.feature_enabled?(:attestation_v2) + %strong Les attestations délivrées suivent encore l’ancien format : + l’activation 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-col-12.fr-col-lg-7 @@ -34,13 +36,23 @@ L’attestation est émise au moment où un dossier est accepté, elle est jointe à l’email d’accusé d’acceptation. Elle est également disponible au téléchargement depuis l’espace personnel de l’usager. + .fr-fieldset__element + = render Dsfr::CalloutComponent.new(title: "Activation de la délivrance de l’attestation", 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 l’attestation dès l’acceptation du dossier. + Désactiver cette option arrête immédiatement l’émission de nouvelles attestations. + .fr-fieldset__element %h2.fr-h4 En-tête .fr-fieldset__element .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"} - %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) .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 } } = 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' } - .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) = 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" } #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 %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' @@ -117,21 +129,10 @@ L’aperç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 : l’aperçu l’utilisera. - .padded-fixed-footer - .fixed-footer - .fr-container - .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 + - if @procedure.feature_enabled?(:attestation_v2) && @attestation_template.draft? + - content_for(:sticky_header) do + = render partial: "sticky_header" - .fr-col-12.fr-col-md-5 - -# .fr-toggle - -# = f.check_box :activated, class: "fr-toggle-input", disabled: true, id: dom_id(@attestation_template, :activated) - -# %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 L’activation de cette attestation sera bientôt disponible. + .padded-fixed-footer + .fixed-footer#fixed_footer + = render partial: "fixed_footer", locals: { procedure: @procedure } diff --git a/app/views/administrateurs/attestation_template_v2s/update.turbo_stream.haml b/app/views/administrateurs/attestation_template_v2s/update.turbo_stream.haml index 67ef140a1..ce033ac46 100644 --- a/app/views/administrateurs/attestation_template_v2s/update.turbo_stream.haml +++ b/app/views/administrateurs/attestation_template_v2s/update.turbo_stream.haml @@ -1,5 +1,8 @@ +- if @attestation_template.draft? + = turbo_stream.update "sticky-header", render(partial: "sticky_header") + = 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 - if @attestation_template.logo_blob&.previously_new_record? @@ -10,7 +13,7 @@ = turbo_stream.update dom_id(@attestation_template, :signature_attachment) do = 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) = turbo_stream.update body_id do = render partial: "shared/errors_list", locals: { object: @attestation_template, attribute: :json_body } diff --git a/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml b/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml index 2cde00e10..d781b8cfc 100644 --- a/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml +++ b/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml @@ -18,7 +18,7 @@ - unless flash.alert = 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 - if @destroyed.present? diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 36755078f..ceb84ff47 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -45,6 +45,9 @@ #beta Env Test + #sticky-header.sticky-header-container + = content_for(:sticky_header) + = render partial: "layouts/header" %main#contenu{ role: :main } = render partial: "layouts/flash_messages" diff --git a/config/locales/views/administrateurs/procedures/en.yml b/config/locales/views/administrateurs/procedures/en.yml index db0bf6b5a..bcbf8daac 100644 --- a/config/locales/views/administrateurs/procedures/en.yml +++ b/config/locales/views/administrateurs/procedures/en.yml @@ -71,6 +71,3 @@ en: 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. not_owner: This URL is identical to another procedure, you must modify it. - autosave_notice: - form_saved: "Form saved" - form_error: "Form in error" diff --git a/config/locales/views/administrateurs/procedures/fr.yml b/config/locales/views/administrateurs/procedures/fr.yml index 0fcc5ad96..90fb3f4f7 100644 --- a/config/locales/views/administrateurs/procedures/fr.yml +++ b/config/locales/views/administrateurs/procedures/fr.yml @@ -71,6 +71,3 @@ fr: path_not_available: owner: Cette url est identique à celle d’une autre de vos démarches publiées. Si vous publiez cette démarche, l’ancienne sera dépubliée et ne sera plus accessible au public. not_owner: Cette url est identique à celle d’une 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" diff --git a/config/routes.rb b/config/routes.rb index d16b5f778..75a44fd05 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -684,7 +684,9 @@ Rails.application.routes.draw do get 'add_champ_engagement_juridique' 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] # ADDED TO ACCESS IT FROM THE IFRAME diff --git a/db/migrate/20240514164228_add_state_to_attestation_templates.rb b/db/migrate/20240514164228_add_state_to_attestation_templates.rb new file mode 100644 index 000000000..8a7842502 --- /dev/null +++ b/db/migrate/20240514164228_add_state_to_attestation_templates.rb @@ -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 diff --git a/db/migrate/20240522084927_add_attestation_template_unicity_index.rb b/db/migrate/20240522084927_add_attestation_template_unicity_index.rb new file mode 100644 index 000000000..5dd74d831 --- /dev/null +++ b/db/migrate/20240522084927_add_attestation_template_unicity_index.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index b33a10582..28c317cac 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -175,10 +175,11 @@ ActiveRecord::Schema[7.0].define(version: 2024_05_27_090508) do t.string "label_logo" t.boolean "official_layout", default: true, null: false t.integer "procedure_id" + t.string "state", default: "published" t.text "title" t.datetime "updated_at", precision: nil, 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 create_table "attestations", id: :serial, force: :cascade do |t| diff --git a/lib/tasks/deployment/20240528155104_backfill_attestation_template_v2_as_draft.rake b/lib/tasks/deployment/20240528155104_backfill_attestation_template_v2_as_draft.rake new file mode 100644 index 000000000..d56ad5d5f --- /dev/null +++ b/lib/tasks/deployment/20240528155104_backfill_attestation_template_v2_as_draft.rake @@ -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 diff --git a/spec/controllers/administrateurs/attestation_template_v2s_controller_spec.rb b/spec/controllers/administrateurs/attestation_template_v2s_controller_spec.rb index d04d7aa89..d736aa9c5 100644 --- a/spec/controllers/administrateurs/attestation_template_v2s_controller_spec.rb +++ b/spec/controllers/administrateurs/attestation_template_v2s_controller_spec.rb @@ -1,7 +1,7 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do let(:admin) { create(:administrateur) } 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(: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_direction: "RSPEC", footer: "en bas", - activated: false, + activated: true, tiptap_body: { type: :doc, content: [ @@ -31,11 +31,12 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do describe 'GET #show' do subject do - get :show, params: { procedure_id: procedure.id } + get :show, params: { procedure_id: procedure.id, format: } response.body end - context 'if an attestation template exists on the procedure' do + context 'html' do + let(:format) { :html } render_views context 'with preview dossier' do @@ -93,20 +94,42 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do 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 describe 'GET edit' do + render_views + let(:attestation_template) { nil } + subject do get :edit, params: { procedure_id: procedure.id } response.body end context 'if an attestation template does not exists yet on the procedure' do - let(:attestation_template) { nil } - it 'creates new v2 attestation template' do subject 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 @@ -116,13 +139,51 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do it 'build new v2 attestation template' do subject expect(assigns(:attestation_template).version).to eq(2) + expect(assigns(:attestation_template)).to be_draft end end - context 'if attestation template already exist on v2' do - it 'assigns v2 attestation template' do + context 'attestation template published exist without draft' do + let(:attestation_template) { build(:attestation_template, :v2, :published) } + + it 'mention publication' do subject 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 @@ -140,16 +201,17 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do it "create template" do 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.label_logo).to eq("Ministère des specs") expect(attestation_template.label_direction).to eq("RSPEC") 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(response.body).to include("Formulaire enregistré") + expect(response.body).to include("Attestation enregistrée") end context "with files" do @@ -157,7 +219,7 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do it "upload files" do 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.signature.download).to eq(signature.read) @@ -174,18 +236,25 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do end context 'when attestation template is valid' do - it "update template" do - subject - attestation_template.reload + it "create a draft template" do + expect { subject }.to change { procedure.attestation_templates.count }.by(1) + # 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.label_logo).to eq("Ministère des specs") expect(attestation_template.label_direction).to eq("RSPEC") 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(response.body).to include("Formulaire enregistré") + expect(response.body).to include("Attestation enregistrée") + expect(response.body).to include("Publier") end context "with files" do @@ -193,7 +262,8 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do it "upload files" do subject - attestation_template.reload + + attestation_template = procedure.attestation_templates.draft.first expect(attestation_template.logo.download).to eq(logo.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) end - it "render error" do + it "renders error" do subject - expect(response.body).to include("Formulaire en erreur") - expect(response.body).to include('Supprimer cette balise') + expect(response.body).to include("Attestation en erreur") + 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("L’attestation 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 + + 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 diff --git a/spec/factories/attestation_template.rb b/spec/factories/attestation_template.rb index be4c39626..d18770402 100644 --- a/spec/factories/attestation_template.rb +++ b/spec/factories/attestation_template.rb @@ -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", - "content" => [ - { "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" } - ] + "content" => [ + { "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" } + ] } ] } diff --git a/spec/models/attestation_template_spec.rb b/spec/models/attestation_template_spec.rb index 638da8b4f..58835bf2e 100644 --- a/spec/models/attestation_template_spec.rb +++ b/spec/models/attestation_template_spec.rb @@ -21,9 +21,11 @@ describe AttestationTemplate, type: :model do context 'with an attestation without images' do let(:attributes) { attributes_for(:attestation_template) } - it { is_expected.to have_attributes(attributes) } - it { is_expected.to have_attributes(id: nil) } - it { expect(subject.logo.attached?).to be_falsey } + it "works" do + is_expected.to have_attributes(attributes) + is_expected.to have_attributes(id: nil) + expect(subject.logo.attached?).to be_falsey + end end context 'with an attestation with images' do @@ -56,81 +58,78 @@ describe AttestationTemplate, type: :model do create(:procedure, types_de_champ_public: types_de_champ, types_de_champ_private: types_de_champ_private, - for_individual: for_individual, attestation_template: attestation_template) end - let(:for_individual) { false } - let(:individual) { nil } let(:etablissement) { create(:etablissement) } let(:types_de_champ) { [] } let(:types_de_champ_private) { [] } - let!(:dossier) { create(:dossier, procedure: procedure, individual: individual, etablissement: etablissement) } - let(:template_title) { 'title' } - let(:template_body) { 'body' } - let(:attestation_template) do - build(:attestation_template, - title: template_title, - body: template_body, - logo: @logo, - signature: @signature) + let(:dossier) { create(:dossier, :accepte, procedure:) } + + let(:types_de_champ) do + [ + { libelle: 'libelleA' }, + { libelle: 'libelleB' } + ] end before do - Timecop.freeze(Time.zone.now) - end + dossier.champs_public + .find { |champ| champ.libelle == 'libelleA' } + .update(value: 'libelle1') - after do - Timecop.return - end - - 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 + dossier.champs_public + .find { |champ| champ.libelle == 'libelleB' } + .update(value: 'libelle2') end let(:attestation) { attestation_template.attestation_for(dossier) } - context 'when the procedure has a type de champ named libelleA et libelleB' do - let(:types_de_champ) do - [ - { libelle: 'libelleA' }, - { libelle: 'libelleB' } - ] + context 'attestation v1' do + let(:template_title) { 'title --libelleA--' } + let(:template_body) { 'body --libelleB--' } + let(:attestation_template) do + build(:attestation_template, + title: template_title, + body: template_body) end - context 'and the are used in the template title and body' do - let(:template_title) { 'title --libelleA--' } - let(:template_body) { 'body --libelleB--' } + let(:view_args) do + arguments = nil - context 'and their value in the dossier are not nil' do - before do - dossier.champs_public - .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 + 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 + + 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 diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index 92f0b0944..76fb5414a 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -685,8 +685,24 @@ describe Dossier, type: :model 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(: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) } @@ -696,11 +712,11 @@ describe Dossier, type: :model do it { is_expected.to eq([]) } end - context "with attestation template" do + context "with attestation template v1" do # Test all combinations: # - with tag specified and unspecified # - 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 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 ?" } @@ -712,27 +728,9 @@ describe Dossier, type: :model do it { is_expected.to eq([]) } end - context "wich is enabled" do + context "which is enabled" do 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 is_expected.to eq([ "unspecified champ-in-title", @@ -743,6 +741,40 @@ describe Dossier, type: :model do 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 describe '#build_attestation' do diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index 0b8b3f4ed..971683eb6 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -1813,23 +1813,23 @@ describe Procedure do describe "#attestation_template" do 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 create(:attestation_template, procedure: procedure) - create(:attestation_template, :v2, procedure: procedure) + create(:attestation_template, :v2, :draft, procedure: procedure) end - it { expect(procedure.attestation_template.version).to eq(1) } + it { expect(subject.attestation_template.version).to eq(1) } end - context "when there is a v2 created before v1" do + context "when there is only a v1" do before do - create(:attestation_template, :v2, procedure: procedure) - create(:attestation_template, procedure: procedure, activated: true) + create(:attestation_template, procedure: procedure) end - it { expect(procedure.attestation_template.version).to eq(1) } + it { expect(subject.attestation_template.version).to eq(1) } end context "when there is only a v2" do @@ -1837,7 +1837,23 @@ describe Procedure do create(:attestation_template, :v2, procedure: procedure) 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 diff --git a/spec/services/tiptap_service_spec.rb b/spec/services/tiptap_service_spec.rb index da47220f2..1ec0468b2 100644 --- a/spec/services/tiptap_service_spec.rb +++ b/spec/services/tiptap_service_spec.rb @@ -189,7 +189,7 @@ RSpec.describe TiptapService do describe '#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 diff --git a/spec/services/weasyprint_service_spec.rb b/spec/services/weasyprint_service_spec.rb new file mode 100644 index 000000000..591f88233 --- /dev/null +++ b/spec/services/weasyprint_service_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +describe WeasyprintService do + let(:html) { 'Hello, World!' } + 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 diff --git a/spec/system/administrateurs/procedure_attestation_template_spec.rb b/spec/system/administrateurs/procedure_attestation_template_spec.rb index a54d2e4e3..ce8ae5998 100644 --- a/spec/system/administrateurs/procedure_attestation_template_spec.rb +++ b/spec/system/administrateurs/procedure_attestation_template_spec.rb @@ -14,8 +14,14 @@ describe 'As an administrateur, I want to manage the procedure’s attestation', before { login_as(administrateur.user, scope: :user) } 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 = [ - "a[href=\"#{edit_admin_procedure_attestation_template_path(procedure)}\"]", + "a[href=\"#{attestation_path}\"]", with_nested_selector ].compact.join(" ") page.find(full_selector) @@ -65,6 +71,13 @@ describe 'As an administrateur, I want to manage the procedure’s attestation', end 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 Flipper.enable(:attestation_v2) @@ -81,7 +94,7 @@ describe 'As an administrateur, I want to manage the procedure’s attestation', find("a").click 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") @@ -90,12 +103,12 @@ describe 'As an administrateur, I want to manage the procedure’s attestation', attestation = nil wait_until { - attestation = procedure.reload.attestation_template_v2 + attestation = procedure.reload.attestation_templates.v2.draft.first attestation.present? } + expect(page).to have_content("Attestation enregistrée") expect(attestation.label_logo).to eq("System Test") - expect(attestation.activated?).to be_falsey - expect(page).to have_content("Formulaire enregistré") + expect(attestation.activated?).to be_truthy click_on "date de décision" @@ -129,7 +142,16 @@ describe 'As an administrateur, I want to manage the procedure’s attestation', } 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("L’attestation 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 l’attestation/) end context "tag in error" do @@ -137,7 +159,7 @@ describe 'As an administrateur, I want to manage the procedure’s attestation', tdc = procedure.active_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'age') 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.save! @@ -150,14 +172,14 @@ describe 'As an administrateur, I want to manage the procedure’s attestation', 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 l’attestation » contient la balise \"age\"") page.execute_script("document.getElementById('attestation_template_tiptap_body').type = 'text'") fill_in "attestation_template[tiptap_body]", with: AttestationTemplate::TIPTAP_BODY_DEFAULT.to_json - expect(page).to have_content("Formulaire enregistré") - expect(page).not_to have_content("Formulaire en erreur") + expect(page).to have_content("Attestation enregistrée") + expect(page).not_to have_content("Attestation en erreur") expect(page).not_to have_content("Le champ « Contenu de l’attestation » contient la balise \"age\"") end end