diff --git a/app/components/attachment/edit_component.rb b/app/components/attachment/edit_component.rb index ba8b786cd28..faee823c415 100644 --- a/app/components/attachment/edit_component.rb +++ b/app/components/attachment/edit_component.rb @@ -83,7 +83,7 @@ def poll_url if champ.present? auto_attach_url else - attachment_path(user_can_edit: true, view_as: @view_as, auto_attach_url: @auto_attach_url) + attachment_path(user_can_edit: true, view_as: @view_as, auto_attach_url: @auto_attach_url, direct_upload: @direct_upload) end end @@ -204,12 +204,14 @@ def accept_content_type end def allowed_formats - return nil unless champ&.titre_identite? - @allowed_formats ||= begin - content_type_validator.options[:in].filter_map do |content_type| + formats = content_type_validator.options[:in].filter_map do |content_type| MiniMime.lookup_by_content_type(content_type)&.extension end.uniq.sort_by { EXTENSIONS_ORDER.index(_1) || 999 } + + # When too many formats are allowed, consider instead manually indicating + # above the input a more comprehensive of formats allowed, like "any image", or a simplified list. + formats.size > 5 ? [] : formats end end diff --git a/app/components/attachment/edit_component/edit_component.html.haml b/app/components/attachment/edit_component/edit_component.html.haml index 116271c19ce..d0d88b590b4 100644 --- a/app/components/attachment/edit_component/edit_component.html.haml +++ b/app/components/attachment/edit_component/edit_component.html.haml @@ -23,7 +23,7 @@ %p.fr-text--sm.fr-text-mention--grey.fr-mb-1w - if max_file_size.present? = t('.max_file_size', max_file_size: number_to_human_size(max_file_size)) - - if allowed_formats + - if allowed_formats.present? = t('.allowed_formats', formats: allowed_formats.join(', ')) diff --git a/app/controllers/administrateurs/administrateur_controller.rb b/app/controllers/administrateurs/administrateur_controller.rb index ff93420d9be..149d87bc80e 100644 --- a/app/controllers/administrateurs/administrateur_controller.rb +++ b/app/controllers/administrateurs/administrateur_controller.rb @@ -42,6 +42,8 @@ def administrateur_as_manager? end def alert_for_missing_siret_service + return if flash[:alert].present? + procedures = missing_siret_services if procedures.any? errors = [] @@ -61,6 +63,8 @@ def missing_siret_services end def alert_for_missing_service + return if flash[:alert].present? + procedures = missing_service if procedures.any? errors = [] diff --git a/app/controllers/administrateurs/attestation_templates_controller.rb b/app/controllers/administrateurs/attestation_templates_controller.rb index 3308eb36db9..27daeac2128 100644 --- a/app/controllers/administrateurs/attestation_templates_controller.rb +++ b/app/controllers/administrateurs/attestation_templates_controller.rb @@ -1,5 +1,7 @@ module Administrateurs class AttestationTemplatesController < AdministrateurController + include UninterlacePngConcern + before_action :retrieve_procedure def show @@ -63,23 +65,14 @@ def activated_attestation_params signature_file = params['attestation_template'].delete('signature') if logo_file.present? - @activated_attestation_params[:logo] = uninterlaced_png(logo_file) + @activated_attestation_params[:logo] = uninterlace_png(logo_file) end if signature_file.present? - @activated_attestation_params[:signature] = uninterlaced_png(signature_file) + @activated_attestation_params[:signature] = uninterlace_png(signature_file) end end @activated_attestation_params end - - def uninterlaced_png(uploaded_file) - if uploaded_file&.content_type == 'image/png' - chunky_img = ChunkyPNG::Image.from_io(uploaded_file.to_io) - chunky_img.save(uploaded_file.tempfile.to_path, interlace: false) - uploaded_file.tempfile.reopen(uploaded_file.tempfile.to_path, 'rb') - end - uploaded_file - end end end diff --git a/app/controllers/administrateurs/groupe_instructeurs_controller.rb b/app/controllers/administrateurs/groupe_instructeurs_controller.rb index 8eb719e3c36..c714c712b92 100644 --- a/app/controllers/administrateurs/groupe_instructeurs_controller.rb +++ b/app/controllers/administrateurs/groupe_instructeurs_controller.rb @@ -2,6 +2,8 @@ module Administrateurs class GroupeInstructeursController < AdministrateurController include ActiveSupport::NumberHelper include Logic + include UninterlacePngConcern + include GroupeInstructeursSignatureConcern before_action :ensure_not_super_admin!, only: [:add_instructeur] @@ -389,6 +391,10 @@ def groupe_instructeur_params params.require(:groupe_instructeur).permit(:label) end + def signature_params + params.require(:groupe_instructeur).permit(:signature) + end + def paginated_groupe_instructeurs groupes = if params[:q].present? query = ActiveRecord::Base.sanitize_sql_like(params[:q]) diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index ee5d20d79c1..410c6421290 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -6,6 +6,7 @@ def show @attachment = @blob.attachments.find(params[:id]) @user_can_edit = cast_bool(params[:user_can_edit]) + @direct_upload = cast_bool(params[:direct_upload]) @view_as = params[:view_as]&.to_sym @auto_attach_url = params[:auto_attach_url] diff --git a/app/controllers/concerns/groupe_instructeurs_signature_concern.rb b/app/controllers/concerns/groupe_instructeurs_signature_concern.rb new file mode 100644 index 00000000000..5ff13f1e8c9 --- /dev/null +++ b/app/controllers/concerns/groupe_instructeurs_signature_concern.rb @@ -0,0 +1,63 @@ +module GroupeInstructeursSignatureConcern + extend ActiveSupport::Concern + + included do + def add_signature + @procedure = procedure + @groupe_instructeur = groupe_instructeur + @instructeurs = paginated_instructeurs + + signature_file = params[:groupe_instructeur][:signature] + + if params[:groupe_instructeur].nil? || signature_file.blank? + if respond_to?(:available_instructeur_emails) + @available_instructeur_emails = available_instructeur_emails + end + + flash[:alert] = "Aucun fichier joint pour le tampon de l'attestation" + render :show + else + signature = uninterlace_png(signature_file) + + if @groupe_instructeur.signature.attach(signature) + handle_redirect :success + else + handle_redirect :alert + end + end + end + + def preview_attestation + attestation_template = procedure.attestation_template || procedure.build_attestation_template + @attestation = attestation_template.render_attributes_for({ groupe_instructeur: groupe_instructeur }) + + render 'administrateurs/attestation_templates/show', formats: [:pdf] + end + + private + + def handle_redirect(status) + redirect, preview = if self.class.module_parent_name == "Administrateurs" + [ + :admin_procedure_groupe_instructeur_path, + :preview_attestation_admin_procedure_groupe_instructeur_path + ] + else + [ + :instructeur_groupe_path, + :preview_attestation_instructeur_groupe_path + ] + end + + redirect_path = method(redirect).call(@procedure, @groupe_instructeur) + preview_path = method(preview).call(@procedure, @groupe_instructeur) + + case status + when :success + redirect_to redirect_path, notice: "Le tampon de l’attestation a bien été ajouté. #{helpers.link_to("Prévisualiser l’attestation", preview_path)}" + when :alert + redirect_to redirect_path, alert: "Une erreur a empêché l’ajout du tampon. Réessayez dans quelques instants." + end + end + end +end diff --git a/app/controllers/concerns/uninterlace_png_concern.rb b/app/controllers/concerns/uninterlace_png_concern.rb new file mode 100644 index 00000000000..8e9d06251d8 --- /dev/null +++ b/app/controllers/concerns/uninterlace_png_concern.rb @@ -0,0 +1,19 @@ +module UninterlacePngConcern + extend ActiveSupport::Concern + + private + + def uninterlace_png(uploaded_file) + if uploaded_file&.content_type == 'image/png' && interlaced?(uploaded_file.tempfile.to_path) + chunky_img = ChunkyPNG::Image.from_io(uploaded_file.to_io) + chunky_img.save(uploaded_file.tempfile.to_path, interlace: false) + uploaded_file.tempfile.reopen(uploaded_file.tempfile.to_path, 'rb') + end + uploaded_file + end + + def interlaced?(png_path) + png = MiniMagick::Image.open(png_path) + png.data["interlace"] != "None" + end +end diff --git a/app/controllers/instructeurs/groupe_instructeurs_controller.rb b/app/controllers/instructeurs/groupe_instructeurs_controller.rb index 08dd61ee823..3a6b52feef6 100644 --- a/app/controllers/instructeurs/groupe_instructeurs_controller.rb +++ b/app/controllers/instructeurs/groupe_instructeurs_controller.rb @@ -1,5 +1,8 @@ module Instructeurs class GroupeInstructeursController < InstructeurController + include UninterlacePngConcern + include GroupeInstructeursSignatureConcern + ITEMS_PER_PAGE = 25 def index diff --git a/app/models/attestation_template.rb b/app/models/attestation_template.rb index 8b1e9d792ba..153169d1145 100644 --- a/app/models/attestation_template.rb +++ b/app/models/attestation_template.rb @@ -60,16 +60,27 @@ def signature_url end def render_attributes_for(params = {}) - dossier = params.fetch(:dossier, false) - - { + attributes = { created_at: Time.zone.now, - title: dossier ? replace_tags(title, dossier) : params.fetch(:title, title), - body: dossier ? replace_tags(body, dossier) : params.fetch(:body, body), footer: params.fetch(:footer, footer), - logo: params.fetch(:logo, logo.attached? ? logo : nil), - signature: params.fetch(:signature, signature.attached? ? signature : nil) + logo: params.fetch(:logo, logo.attached? ? logo : nil) } + + dossier = params[:dossier] + + if dossier.present? + attributes.merge({ + title: replace_tags(title, dossier), + body: replace_tags(body, dossier), + signature: signature_to_render(dossier.groupe_instructeur) + }) + else + attributes.merge({ + title: params.fetch(:title, title), + body: params.fetch(:body, body), + signature: signature_to_render(params[:groupe_instructeur]) + }) + end end def logo_checksum @@ -90,6 +101,14 @@ def signature_filename private + def signature_to_render(groupe_instructeur) + if groupe_instructeur&.signature&.attached? + groupe_instructeur.signature + else + signature + end + end + def used_tags used_tags_for(title) + used_tags_for(body) end diff --git a/app/models/groupe_instructeur.rb b/app/models/groupe_instructeur.rb index 1d0af0c7bb5..d1443458669 100644 --- a/app/models/groupe_instructeur.rb +++ b/app/models/groupe_instructeur.rb @@ -15,6 +15,11 @@ class GroupeInstructeur < ApplicationRecord has_one :defaut_procedure, -> { with_discarded }, class_name: 'Procedure', foreign_key: :defaut_groupe_instructeur_id, dependent: :nullify, inverse_of: :defaut_groupe_instructeur has_one :contact_information + has_one_attached :signature + + SIGNATURE_MAX_SIZE = 1.megabytes + validates :signature, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: { less_than: SIGNATURE_MAX_SIZE } + validates :label, presence: true, allow_nil: false validates :label, uniqueness: { scope: :procedure } validates :closed, acceptance: { accept: [false] }, if: -> { (self == procedure.defaut_groupe_instructeur) } diff --git a/app/views/administrateurs/attestation_templates/_informations.html.haml b/app/views/administrateurs/attestation_templates/_informations.html.haml index fc9fe38b51e..139c4ff53a9 100644 --- a/app/views/administrateurs/attestation_templates/_informations.html.haml +++ b/app/views/administrateurs/attestation_templates/_informations.html.haml @@ -24,19 +24,22 @@ = tag[:description] %h3.header-subsection Logo de l'attestation +%p.fr-text--sm.fr-text-mention--grey.fr-mb-0 + Dimensions conseillées : au minimum 500px de largeur ou de hauteur. = render Attachment::EditComponent.new(attached_file: @attestation_template.logo, direct_upload: false) -%p.notice - Formats acceptés : JPG / JPEG / PNG. - %br - Dimensions conseillées : au minimum 500 px de largeur ou de hauteur, poids maximum : 0,5 Mo. -%h3.header-subsection Tampon de l'attestation +%h3.header-subsection.fr-mt-5w Tampon de l'attestation +%p.fr-text--sm.fr-text-mention--grey.fr-mb-0 + Dimensions conseillées : au minimum 500px de largeur ou de hauteur. = render Attachment::EditComponent.new(attached_file: @attestation_template.signature, direct_upload: false) -%p.notice - Formats acceptés : JPG / JPEG / PNG. - %br - Dimensions conseillées : au minimum 500 px de largeur ou de hauteur, poids maximum : 0,5 Mo. -= render Dsfr::InputComponent.new(form: f, attribute: :footer, input_type: :text_field, opts: { maxlength: 190, size: nil }, required: false) +- if @attestation_template.procedure.routing_enabled? + %p.fr-text--sm.fr-text-mention--grey + À noter : chaque groupe instructeur peut apposer son propre tampon à la place de celui-ci. + + +.fr-mt-4w + = render Dsfr::InputComponent.new(form: f, attribute: :footer, input_type: :text_field, opts: { maxlength: 190, size: nil }, required: false) + diff --git a/app/views/administrateurs/groupe_instructeurs/show.html.haml b/app/views/administrateurs/groupe_instructeurs/show.html.haml index d61e1743338..d1d9eb8f6bf 100644 --- a/app/views/administrateurs/groupe_instructeurs/show.html.haml +++ b/app/views/administrateurs/groupe_instructeurs/show.html.haml @@ -16,3 +16,6 @@ = render partial: 'administrateurs/groupe_instructeurs/contact_information', locals: { procedure: @procedure, groupe_instructeur: @groupe_instructeur } + + = render partial: "shared/groupe_instructeurs/signature_form", locals: { groupe_instructeur: @groupe_instructeur, + preview_path: preview_attestation_admin_procedure_groupe_instructeur_path(@groupe_instructeur.procedure, @groupe_instructeur) } diff --git a/app/views/attachments/show.turbo_stream.haml b/app/views/attachments/show.turbo_stream.haml index 440cf52b250..c7cb617690f 100644 --- a/app/views/attachments/show.turbo_stream.haml +++ b/app/views/attachments/show.turbo_stream.haml @@ -1,5 +1,5 @@ = turbo_stream.replace dom_id(@attachment, :edit) do - if @user_can_edit - = render Attachment::EditComponent.new(attachment: @attachment, attached_file: @attachment.record.public_send(@attachment.name), auto_attach_url: @auto_attach_url, view_as: @view_as) + = render Attachment::EditComponent.new(attachment: @attachment, attached_file: @attachment.record.public_send(@attachment.name), auto_attach_url: @auto_attach_url, view_as: @view_as, direct_upload: @direct_upload) - else = render Attachment::ShowComponent.new(attachment: @attachment) diff --git a/app/views/instructeurs/groupe_instructeurs/show.html.haml b/app/views/instructeurs/groupe_instructeurs/show.html.haml index 1cabdabf303..78d13fcff53 100644 --- a/app/views/instructeurs/groupe_instructeurs/show.html.haml +++ b/app/views/instructeurs/groupe_instructeurs/show.html.haml @@ -65,3 +65,6 @@ %p= service.telephone - if service.horaires.present? %p= service.horaires + + = render partial: "shared/groupe_instructeurs/signature_form", locals: { groupe_instructeur: @groupe_instructeur, + preview_path: preview_attestation_instructeur_groupe_path(@groupe_instructeur.procedure, @groupe_instructeur) } diff --git a/app/views/shared/groupe_instructeurs/_signature_form.html.haml b/app/views/shared/groupe_instructeurs/_signature_form.html.haml new file mode 100644 index 00000000000..09a8e672b50 --- /dev/null +++ b/app/views/shared/groupe_instructeurs/_signature_form.html.haml @@ -0,0 +1,19 @@ +.card.mt-2 + = render NestedForms::FormOwnerComponent.new + = form_with url: { action: :add_signature }, method: :post, html: { multipart: true } do |f| + .card-title Tampon de l'attestation + + %p.fr-text--sm.fr-text-mention--grey + Vous pouvez apposer sur l’attestation un tampon (ou signature) dédié à ce groupe d’instructeurs. + Si vous n’en fournissez pas, celui de la démarche sera utilisé, le cas échéant. + + .fr-upload-group.fr-mb-4w + %p.fr-text--sm.fr-text-mention--grey.fr-mb-1w + Dimensions conseillées : au minimum 500px de largeur ou de hauteur. + = render Attachment::EditComponent.new(attached_file: groupe_instructeur.signature, direct_upload: false) + + .fr-btns-group.fr-btns-group--inline + = f.submit 'Ajouter le tampon', class: 'fr-btn' + + - if @groupe_instructeur.signature.persisted? + = link_to("Prévisualiser", preview_path, class: "fr-btn fr-btn--secondary", **external_link_attributes) diff --git a/config/routes.rb b/config/routes.rb index 0d906dacfc5..66b75ef67ab 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -395,6 +395,8 @@ member do post 'add_instructeur' delete 'remove_instructeur' + post 'add_signature' + get 'preview_attestation' end end @@ -532,6 +534,8 @@ delete 'remove_instructeur' get 'reaffecter_dossiers' post 'reaffecter' + post 'add_signature' + get 'preview_attestation' end collection do diff --git a/spec/controllers/administrateurs/attestation_templates_controller_spec.rb b/spec/controllers/administrateurs/attestation_templates_controller_spec.rb index 0e670de3580..a1930f1d5cc 100644 --- a/spec/controllers/administrateurs/attestation_templates_controller_spec.rb +++ b/spec/controllers/administrateurs/attestation_templates_controller_spec.rb @@ -58,7 +58,7 @@ expect(assigns(:attestation)).to include(attestation_params) expect(assigns(:attestation)[:created_at]).to eq(Time.zone.now) expect(assigns(:attestation)[:logo]).to eq(nil) - expect(assigns(:attestation)[:signature]).to eq(nil) + expect(assigns(:attestation)[:signature]).not_to be_attached end it_behaves_like 'rendering a PDF successfully' end diff --git a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb index c6901eb446d..c6737c284bc 100644 --- a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb +++ b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb @@ -843,4 +843,22 @@ def remove_instructeur(instructeur) expect(procedure4.reload.routing_enabled).to be_truthy end end + + describe '#add_signature' do + let(:signature) { fixture_file_upload('spec/fixtures/files/black.png', 'image/png') } + + before { + post :add_signature, + params: { + procedure_id: procedure.id, + id: gi_1_1.id, + groupe_instructeur: { + signature: signature + } + } + } + + it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_1)) } + it { expect(gi_1_1.signature).to be_attached } + end end diff --git a/spec/controllers/instructeurs/groupe_instructeurs_controller_spec.rb b/spec/controllers/instructeurs/groupe_instructeurs_controller_spec.rb index 6452b8f261e..fe0a1e2d48f 100644 --- a/spec/controllers/instructeurs/groupe_instructeurs_controller_spec.rb +++ b/spec/controllers/instructeurs/groupe_instructeurs_controller_spec.rb @@ -103,4 +103,22 @@ def remove_instructeur(instructeur) it { expect(response).to redirect_to(instructeur_groupe_path(procedure, gi_1_1)) } end end + + describe '#add_signature' do + let(:signature) { fixture_file_upload('spec/fixtures/files/black.png', 'image/png') } + + before do + post :add_signature, + params: { + procedure_id: procedure.id, + id: gi_1_2.id, + groupe_instructeur: { + signature: signature + } + } + end + + it { expect(response).to redirect_to(instructeur_groupe_path(procedure, gi_1_2)) } + it { expect(gi_1_2.reload.signature).to be_attached } + end end diff --git a/spec/models/attestation_template_spec.rb b/spec/models/attestation_template_spec.rb index 3f307c2f8a9..e47f596a75a 100644 --- a/spec/models/attestation_template_spec.rb +++ b/spec/models/attestation_template_spec.rb @@ -1,45 +1,4 @@ describe AttestationTemplate, type: :model do - # describe 'validate' do - # let(:logo_size) { AttestationTemplate::FILE_MAX_SIZE_IN_MB.megabyte } - # let(:signature_size) { AttestationTemplate::FILE_MAX_SIZE_IN_MB.megabyte } - # let(:fake_logo) { double(AttestationTemplateLogoUploader, file: double(size: logo_size)) } - # let(:fake_signature) { double(AttestationTemplateSignatureUploader, file: double(size: signature_size)) } - # let(:attestation_template) { AttestationTemplate.new } - - # before do - # allow(attestation_template).to receive(:logo).and_return(fake_logo) - # allow(attestation_template).to receive(:signature).and_return(fake_signature) - # attestation_template.validate - # end - - # subject { attestation_template.errors.details } - - # context 'when no files are present' do - # let(:fake_logo) { nil } - # let(:fake_signature) { nil } - - # it { is_expected.to match({}) } - # end - - # context 'when the logo and the signature have the right size' do - # it { is_expected.to match({}) } - # end - - # context 'when the logo and the signature are too heavy' do - # let(:logo_size) { AttestationTemplate::FILE_MAX_SIZE_IN_MB.megabyte + 1 } - # let(:signature_size) { AttestationTemplate::FILE_MAX_SIZE_IN_MB.megabyte + 1 } - - # it do - # expected = { - # signature: [{ error: ' : vous ne pouvez pas charger une image de plus de 0,5 Mo' }], - # logo: [{ error: ' : vous ne pouvez pas charger une image de plus de 0,5 Mo' }] - # } - - # is_expected.to match(expected) - # end - # end - # end - describe 'validates footer length' do let(:attestation_template) { build(:attestation_template, footer: footer) } @@ -175,4 +134,44 @@ end end end + + describe '#render_attributes_for' do + context 'signature' do + let(:dossier) { create(:dossier, procedure: attestation.procedure, groupe_instructeur: groupe_instructeur) } + + subject { attestation.render_attributes_for(dossier: dossier)[:signature] } + + context 'procedure with signature' do + let(:attestation) { create(:attestation_template, signature: Rack::Test::UploadedFile.new('spec/fixtures/files/logo_test_procedure.png', 'image/png')) } + + context "groupe instructeur without signature" do + let(:groupe_instructeur) { create(:groupe_instructeur, signature: nil) } + + it { expect(subject.blob.filename).to eq("logo_test_procedure.png") } + end + + context 'groupe instructeur with signature' do + let(:groupe_instructeur) { create(:groupe_instructeur, signature: Rack::Test::UploadedFile.new('spec/fixtures/files/black.png', 'image/png')) } + + it { expect(subject.blob.filename).to eq("black.png") } + end + end + + context 'procedure without signature' do + let(:attestation) { create(:attestation_template, signature: nil) } + + context "groupe instructeur without signature" do + let(:groupe_instructeur) { create(:groupe_instructeur, signature: nil) } + + it { expect(subject.attached?).to be_falsey } + end + + context 'groupe instructeur with signature' do + let(:groupe_instructeur) { create(:groupe_instructeur, signature: Rack::Test::UploadedFile.new('spec/fixtures/files/black.png', 'image/png')) } + + it { expect(subject.blob.filename).to eq("black.png") } + end + end + end + end end