Merge pull request #9507 from demarches-simplifiees/9449-signature-groupe-instructeur

9449 ETQ instructeur ou admin, je peux apposer sur une attestation un tampon dédié à un groupe instructeur
This commit is contained in:
krichtof 2023-09-29 14:20:46 +00:00 committed by GitHub
commit 428ae4a45a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 258 additions and 76 deletions

View file

@ -83,7 +83,7 @@ class Attachment::EditComponent < ApplicationComponent
if champ.present? if champ.present?
auto_attach_url auto_attach_url
else 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
end end
@ -204,12 +204,14 @@ class Attachment::EditComponent < ApplicationComponent
end end
def allowed_formats def allowed_formats
return nil unless champ&.titre_identite?
@allowed_formats ||= begin @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 MiniMime.lookup_by_content_type(content_type)&.extension
end.uniq.sort_by { EXTENSIONS_ORDER.index(_1) || 999 } 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
end end

View file

@ -23,7 +23,7 @@
%p.fr-hint-text.fr-mb-1w %p.fr-hint-text.fr-mb-1w
- if max_file_size.present? - if max_file_size.present?
= t('.max_file_size', max_file_size: number_to_human_size(max_file_size)) = 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(', ')) = t('.allowed_formats', formats: allowed_formats.join(', '))

View file

@ -42,6 +42,8 @@ module Administrateurs
end end
def alert_for_missing_siret_service def alert_for_missing_siret_service
return if flash[:alert].present?
procedures = missing_siret_services procedures = missing_siret_services
if procedures.any? if procedures.any?
errors = [] errors = []
@ -61,6 +63,8 @@ module Administrateurs
end end
def alert_for_missing_service def alert_for_missing_service
return if flash[:alert].present?
procedures = missing_service procedures = missing_service
if procedures.any? if procedures.any?
errors = [] errors = []

View file

@ -1,5 +1,7 @@
module Administrateurs module Administrateurs
class AttestationTemplatesController < AdministrateurController class AttestationTemplatesController < AdministrateurController
include UninterlacePngConcern
before_action :retrieve_procedure before_action :retrieve_procedure
def show def show
@ -63,23 +65,14 @@ module Administrateurs
signature_file = params['attestation_template'].delete('signature') signature_file = params['attestation_template'].delete('signature')
if logo_file.present? if logo_file.present?
@activated_attestation_params[:logo] = uninterlaced_png(logo_file) @activated_attestation_params[:logo] = uninterlace_png(logo_file)
end end
if signature_file.present? if signature_file.present?
@activated_attestation_params[:signature] = uninterlaced_png(signature_file) @activated_attestation_params[:signature] = uninterlace_png(signature_file)
end end
end end
@activated_attestation_params @activated_attestation_params
end 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
end end

View file

@ -2,6 +2,8 @@ module Administrateurs
class GroupeInstructeursController < AdministrateurController class GroupeInstructeursController < AdministrateurController
include ActiveSupport::NumberHelper include ActiveSupport::NumberHelper
include Logic include Logic
include UninterlacePngConcern
include GroupeInstructeursSignatureConcern
before_action :ensure_not_super_admin!, only: [:add_instructeur] before_action :ensure_not_super_admin!, only: [:add_instructeur]
@ -389,6 +391,10 @@ module Administrateurs
params.require(:groupe_instructeur).permit(:label) params.require(:groupe_instructeur).permit(:label)
end end
def signature_params
params.require(:groupe_instructeur).permit(:signature)
end
def paginated_groupe_instructeurs def paginated_groupe_instructeurs
groupes = if params[:q].present? groupes = if params[:q].present?
query = ActiveRecord::Base.sanitize_sql_like(params[:q]) query = ActiveRecord::Base.sanitize_sql_like(params[:q])

View file

@ -6,6 +6,7 @@ class AttachmentsController < ApplicationController
@attachment = @blob.attachments.find(params[:id]) @attachment = @blob.attachments.find(params[:id])
@user_can_edit = cast_bool(params[:user_can_edit]) @user_can_edit = cast_bool(params[:user_can_edit])
@direct_upload = cast_bool(params[:direct_upload])
@view_as = params[:view_as]&.to_sym @view_as = params[:view_as]&.to_sym
@auto_attach_url = params[:auto_attach_url] @auto_attach_url = params[:auto_attach_url]

View file

@ -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 lattestation a bien été ajouté. #{helpers.link_to("Prévisualiser lattestation", preview_path)}"
when :alert
redirect_to redirect_path, alert: "Une erreur a empêché lajout du tampon. Réessayez dans quelques instants."
end
end
end
end

View file

@ -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

View file

@ -1,5 +1,8 @@
module Instructeurs module Instructeurs
class GroupeInstructeursController < InstructeurController class GroupeInstructeursController < InstructeurController
include UninterlacePngConcern
include GroupeInstructeursSignatureConcern
ITEMS_PER_PAGE = 25 ITEMS_PER_PAGE = 25
def index def index

View file

@ -60,16 +60,27 @@ class AttestationTemplate < ApplicationRecord
end end
def render_attributes_for(params = {}) def render_attributes_for(params = {})
dossier = params.fetch(:dossier, false) attributes = {
{
created_at: Time.zone.now, 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), footer: params.fetch(:footer, footer),
logo: params.fetch(:logo, logo.attached? ? logo : nil), logo: params.fetch(:logo, logo.attached? ? logo : nil)
signature: params.fetch(:signature, signature.attached? ? signature : 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 end
def logo_checksum def logo_checksum
@ -90,6 +101,14 @@ class AttestationTemplate < ApplicationRecord
private private
def signature_to_render(groupe_instructeur)
if groupe_instructeur&.signature&.attached?
groupe_instructeur.signature
else
signature
end
end
def used_tags def used_tags
used_tags_for(title) + used_tags_for(body) used_tags_for(title) + used_tags_for(body)
end end

View file

@ -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 :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 :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, presence: true, allow_nil: false
validates :label, uniqueness: { scope: :procedure } validates :label, uniqueness: { scope: :procedure }
validates :closed, acceptance: { accept: [false] }, if: -> { (self == procedure.defaut_groupe_instructeur) } validates :closed, acceptance: { accept: [false] }, if: -> { (self == procedure.defaut_groupe_instructeur) }

View file

@ -24,19 +24,22 @@
= tag[:description] = tag[:description]
%h3.header-subsection Logo de l'attestation %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) = 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) = 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.
- 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) = render Dsfr::InputComponent.new(form: f, attribute: :footer, input_type: :text_field, opts: { maxlength: 190, size: nil }, required: false)

View file

@ -16,3 +16,6 @@
= render partial: 'administrateurs/groupe_instructeurs/contact_information', = render partial: 'administrateurs/groupe_instructeurs/contact_information',
locals: { procedure: @procedure, locals: { procedure: @procedure,
groupe_instructeur: @groupe_instructeur } 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) }

View file

@ -1,5 +1,5 @@
= turbo_stream.replace dom_id(@attachment, :edit) do = turbo_stream.replace dom_id(@attachment, :edit) do
- if @user_can_edit - 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 - else
= render Attachment::ShowComponent.new(attachment: @attachment) = render Attachment::ShowComponent.new(attachment: @attachment)

View file

@ -65,3 +65,6 @@
%p= service.telephone %p= service.telephone
- if service.horaires.present? - if service.horaires.present?
%p= service.horaires %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) }

View file

@ -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 lattestation un tampon (ou signature) dédié à ce groupe dinstructeurs.
Si vous nen 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)

View file

@ -397,6 +397,8 @@ Rails.application.routes.draw do
member do member do
post 'add_instructeur' post 'add_instructeur'
delete 'remove_instructeur' delete 'remove_instructeur'
post 'add_signature'
get 'preview_attestation'
end end
end end
@ -535,6 +537,8 @@ Rails.application.routes.draw do
delete 'remove_instructeur' delete 'remove_instructeur'
get 'reaffecter_dossiers' get 'reaffecter_dossiers'
post 'reaffecter' post 'reaffecter'
post 'add_signature'
get 'preview_attestation'
end end
collection do collection do

View file

@ -58,7 +58,7 @@ describe Administrateurs::AttestationTemplatesController, type: :controller do
expect(assigns(:attestation)).to include(attestation_params) expect(assigns(:attestation)).to include(attestation_params)
expect(assigns(:attestation)[:created_at]).to eq(Time.zone.now) expect(assigns(:attestation)[:created_at]).to eq(Time.zone.now)
expect(assigns(:attestation)[:logo]).to eq(nil) expect(assigns(:attestation)[:logo]).to eq(nil)
expect(assigns(:attestation)[:signature]).to eq(nil) expect(assigns(:attestation)[:signature]).not_to be_attached
end end
it_behaves_like 'rendering a PDF successfully' it_behaves_like 'rendering a PDF successfully'
end end

View file

@ -843,4 +843,22 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do
expect(procedure4.reload.routing_enabled).to be_truthy expect(procedure4.reload.routing_enabled).to be_truthy
end end
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 end

View file

@ -103,4 +103,22 @@ describe Instructeurs::GroupeInstructeursController, type: :controller do
it { expect(response).to redirect_to(instructeur_groupe_path(procedure, gi_1_1)) } it { expect(response).to redirect_to(instructeur_groupe_path(procedure, gi_1_1)) }
end end
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 end

View file

@ -1,45 +1,4 @@
describe AttestationTemplate, type: :model do 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 describe 'validates footer length' do
let(:attestation_template) { build(:attestation_template, footer: footer) } let(:attestation_template) { build(:attestation_template, footer: footer) }
@ -175,4 +134,44 @@ describe AttestationTemplate, type: :model do
end end
end 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 end