From 4ec61e58ac3895537f337920c39fec694186fdbf Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 16 Jun 2022 15:51:45 +0200 Subject: [PATCH] feat(EditComponent): add max_file_size and content_types to upload buttons inspired by dsfr --- app/assets/stylesheets/utils.scss | 10 +- app/components/attachment/edit_component.rb | 43 ++-- .../edit_component/edit_component.en.yml | 1 + .../edit_component/edit_component.fr.yml | 1 + .../edit_component/edit_component.html.haml | 4 + .../shared/activestorage/auto-upload.ts | 10 +- .../shared/activestorage/uploader.ts | 22 +- app/models/type_de_champ.rb | 3 + .../_informations.html.haml | 4 +- .../groupe_instructeurs/_edit.html.haml | 2 +- .../procedures/_informations.html.haml | 7 +- .../procedures/publication.html.haml | 2 +- app/views/experts/avis/instruction.html.haml | 2 +- .../experts/avis/shared/avis/_form.html.haml | 2 +- app/views/experts/shared/avis/_form.html.haml | 2 +- .../instructeurs/avis/instruction.html.haml | 2 +- .../instructeurs/shared/avis/_form.html.haml | 2 +- .../shared/dossiers/messages/_form.html.haml | 5 +- config/locales/en.yml | 2 - config/locales/fr.yml | 2 - spec/models/type_de_champ_shared_example.rb | 206 ----------------- spec/models/type_de_champ_spec.rb | 207 +++++++++++++++++- .../attachment/_update.html.haml_spec.rb | 6 +- 23 files changed, 294 insertions(+), 253 deletions(-) diff --git a/app/assets/stylesheets/utils.scss b/app/assets/stylesheets/utils.scss index 649bfcc9f..9d28cdced 100644 --- a/app/assets/stylesheets/utils.scss +++ b/app/assets/stylesheets/utils.scss @@ -29,15 +29,19 @@ } .text-sm { - font-size: 14px; + font-size: 14px !important; } .text-lg { font-size: 18px; } -.bold { - font-weight: bold; +.bold-weight-bold { + font-weight: bold !important; +} + +.font-weight-normal { + font-weight: normal !important; } .numbers-delimiter { diff --git a/app/components/attachment/edit_component.rb b/app/components/attachment/edit_component.rb index 82f999afc..dee33be8a 100644 --- a/app/components/attachment/edit_component.rb +++ b/app/components/attachment/edit_component.rb @@ -1,9 +1,8 @@ # Display a widget for uploading, editing and deleting a file attachment class Attachment::EditComponent < ApplicationComponent - def initialize(form:, attached_file:, accept: nil, template: nil, user_can_destroy: false, direct_upload: true, id: nil) + def initialize(form:, attached_file:, template: nil, user_can_destroy: false, direct_upload: true, id: nil) @form = form @attached_file = attached_file - @accept = accept @template = template @user_can_destroy = user_can_destroy @direct_upload = direct_upload @@ -12,16 +11,15 @@ class Attachment::EditComponent < ApplicationComponent attr_reader :template, :form - def self.text(form, file) - new(form: form, attached_file: file, user_can_destroy: true) + def allowed_extensions + content_type_validator.options[:in] + .flat_map { |content_type| MIME::Types[content_type].map(&:extensions) } + .reject(&:blank?) + .flatten end - def self.image(form, file, direct_upload = true) - new(form: form, - attached_file: file, - accept: 'image/png, image/jpg, image/jpeg', - user_can_destroy: true, - direct_upload: direct_upload) + def max_file_size + file_size_validator.options[:less_than] end def user_can_destroy? @@ -55,14 +53,21 @@ class Attachment::EditComponent < ApplicationComponent def file_field_options { class: "attachment-input #{attachment_input_class} #{'hidden' if persisted?}", - accept: @accept, + accept: content_type_validator.options[:in].join(', '), direct_upload: @direct_upload, - id: champ&.input_id || @id, + id: input_id(@id), aria: { describedby: champ&.describedby_id }, - data: { auto_attach_url: helpers.auto_attach_url(form.object) } + data: { + auto_attach_url: helpers.auto_attach_url(form.object), + max_file_size: max_file_size + } } end + def input_id(given_id) + [given_id, champ&.input_id, file_field_name].reject(&:blank?).compact.first + end + def file_field_name @attached_file.name end @@ -90,4 +95,16 @@ class Attachment::EditComponent < ApplicationComponent data: { toggle_target: ".#{attachment_input_class}" } } end + + def file_size_validator + @attached_file.record + ._validators[file_field_name.to_sym] + .find { |validator| validator.class == ActiveStorageValidations::SizeValidator } + end + + def content_type_validator + @attached_file.record + ._validators[file_field_name.to_sym] + .find { |validator| validator.class == ActiveStorageValidations::ContentTypeValidator } + end end diff --git a/app/components/attachment/edit_component/edit_component.en.yml b/app/components/attachment/edit_component/edit_component.en.yml index 1491cafbf..3e584f7ae 100644 --- a/app/components/attachment/edit_component/edit_component.en.yml +++ b/app/components/attachment/edit_component/edit_component.en.yml @@ -1,2 +1,3 @@ --- en: + max_file_size: "File size limit : %{max_file_size}." diff --git a/app/components/attachment/edit_component/edit_component.fr.yml b/app/components/attachment/edit_component/edit_component.fr.yml index 09f6db466..4f38cffa6 100644 --- a/app/components/attachment/edit_component/edit_component.fr.yml +++ b/app/components/attachment/edit_component/edit_component.fr.yml @@ -1,2 +1,3 @@ --- fr: + max_file_size: "Taille maximale : %{max_file_size}." diff --git a/app/components/attachment/edit_component/edit_component.html.haml b/app/components/attachment/edit_component/edit_component.html.haml index ef538b7e4..0c8ec9b02 100644 --- a/app/components/attachment/edit_component/edit_component.html.haml +++ b/app/components/attachment/edit_component/edit_component.html.haml @@ -24,4 +24,8 @@ %span.icon.retry Ré-essayer + + %label.text-sm.font-weight-normal{ for: file_field_options[:id] } + = t('.max_file_size', max_file_size: number_to_human_size(max_file_size)) + = form.file_field(file_field_name, **file_field_options) diff --git a/app/javascript/shared/activestorage/auto-upload.ts b/app/javascript/shared/activestorage/auto-upload.ts index 297a0c313..4bb41a615 100644 --- a/app/javascript/shared/activestorage/auto-upload.ts +++ b/app/javascript/shared/activestorage/auto-upload.ts @@ -24,10 +24,16 @@ export class AutoUpload { #uploader: Uploader; constructor(input: HTMLInputElement, file: File) { - const { directUploadUrl, autoAttachUrl } = input.dataset; + const { directUploadUrl, autoAttachUrl, maxFileSize } = input.dataset; invariant(directUploadUrl, 'Could not find the direct upload URL.'); this.#input = input; - this.#uploader = new Uploader(input, file, directUploadUrl, autoAttachUrl); + this.#uploader = new Uploader( + input, + file, + directUploadUrl, + autoAttachUrl, + maxFileSize + ); } // Create, upload and attach the file. diff --git a/app/javascript/shared/activestorage/uploader.ts b/app/javascript/shared/activestorage/uploader.ts index 0f916f9c9..7b6e6bb25 100644 --- a/app/javascript/shared/activestorage/uploader.ts +++ b/app/javascript/shared/activestorage/uploader.ts @@ -7,6 +7,7 @@ import { ERROR_CODE_ATTACH } from './file-upload-error'; +const BYTES_TO_MB_RATIO = 1_048_576; /** Uploader class is a delegate for DirectUpload instance used to track lifecycle and progress of an upload. @@ -15,16 +16,25 @@ export default class Uploader { directUpload: DirectUpload; progressBar: ProgressBar; autoAttachUrl?: string; + maxFileSize: number; + file: File; constructor( input: HTMLInputElement, file: File, directUploadUrl: string, - autoAttachUrl?: string + autoAttachUrl?: string, + maxFileSize?: string ) { + this.file = file; this.directUpload = new DirectUpload(file, directUploadUrl, this); this.progressBar = new ProgressBar(input, this.directUpload.id + '', file); this.autoAttachUrl = autoAttachUrl; + try { + this.maxFileSize = parseInt(maxFileSize || '0', 10); + } catch (e) { + this.maxFileSize = 0; + } } /** @@ -34,7 +44,12 @@ export default class Uploader { */ async start() { this.progressBar.start(); - + if (this.maxFileSize > 0 && this.file.size > this.maxFileSize) { + throw `La taille du fichier ne peut dépasser + ${this.maxFileSize / BYTES_TO_MB_RATIO} Mo + (in english: File size can't be bigger than + ${this.maxFileSize / BYTES_TO_MB_RATIO} Mo).`; + } try { const blobSignedId = await this.upload(); @@ -89,7 +104,8 @@ export default class Uploader { const errors = (error.jsonBody as { errors: string[] })?.errors; const message = errors && errors[0]; throw new FileUploadError( - message || 'Error attaching file.', + message || + `Impossible d'associer le fichier (in english: error attaching file).'`, error.response?.status, ERROR_CODE_ATTACH ); diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index d715b9179..b1e5514b9 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -17,6 +17,7 @@ class TypeDeChamp < ApplicationRecord self.ignored_columns = [:migrated_parent, :revision_id, :parent_id, :order_place] + FILE_MAX_SIZE = 200.megabytes FEATURE_FLAGS = {} enum type_champs: { @@ -123,6 +124,8 @@ class TypeDeChamp < ApplicationRecord end has_one_attached :piece_justificative_template + validates :piece_justificative_template, size: { less_than: FILE_MAX_SIZE } + validates :piece_justificative_template, content_type: AUTHORIZED_CONTENT_TYPES validates :libelle, presence: true, allow_blank: false, allow_nil: false validates :type_champ, presence: true, allow_blank: false, allow_nil: false diff --git a/app/views/administrateurs/attestation_templates/_informations.html.haml b/app/views/administrateurs/attestation_templates/_informations.html.haml index 397d2c57a..4aea5faa7 100644 --- a/app/views/administrateurs/attestation_templates/_informations.html.haml +++ b/app/views/administrateurs/attestation_templates/_informations.html.haml @@ -25,7 +25,7 @@ = tag[:description] %h3.header-subsection Logo de l'attestation -= render Attachment::EditComponent.image(f, @attestation_template.logo, false) += render Attachment::EditComponent.new(form: f, attached_file: @attestation_template.logo, direct_upload: false, user_can_destroy: true) %p.notice Formats acceptés : JPG / JPEG / PNG. @@ -33,7 +33,7 @@ Dimensions conseillées : au minimum 500 px de largeur ou de hauteur, poids maximum : 0,5 Mo. %h3.header-subsection Tampon de l'attestation -= render Attachment::EditComponent.image(f, @attestation_template.signature, false) += render Attachment::EditComponent.new(form: f, attached_file: @attestation_template.signature, direct_upload: false, user_can_destroy: true) %p.notice Formats acceptés : JPG / JPEG / PNG. diff --git a/app/views/administrateurs/groupe_instructeurs/_edit.html.haml b/app/views/administrateurs/groupe_instructeurs/_edit.html.haml index 97e092ea0..4d79c3813 100644 --- a/app/views/administrateurs/groupe_instructeurs/_edit.html.haml +++ b/app/views/administrateurs/groupe_instructeurs/_edit.html.haml @@ -35,7 +35,7 @@ = file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1" = submit_tag t('.csv_import.import_file'), class: 'button primary send', data: { disable_with: "Envoi..." } - else - %p.mt-4.form.bold.mb-2.text-lg + %p.mt-4.form.font-weight-bold.mb-2.text-lg = t('.csv_import.title') %p.notice = t('.csv_import.import_file_procedure_not_published') diff --git a/app/views/administrateurs/procedures/_informations.html.haml b/app/views/administrateurs/procedures/_informations.html.haml index 5c89e106b..d3cbd0d8d 100644 --- a/app/views/administrateurs/procedures/_informations.html.haml +++ b/app/views/administrateurs/procedures/_informations.html.haml @@ -20,7 +20,7 @@ = f.select :zone_id, grouped_options_for_zone %h3.header-subsection Logo de la démarche -= render Attachment::EditComponent.image(f, @procedure.logo) += render Attachment::EditComponent.new(form: f, attached_file: @procedure.logo, direct_upload: true, user_can_destroy: true) %h3.header-subsection Conservation des données = f.label :duree_conservation_dossiers_dans_ds do @@ -55,7 +55,7 @@ = f.text_field :cadre_juridique, class: 'form-control', placeholder: 'https://www.legifrance.gouv.fr/' = f.label :deliberation, 'Importer le texte' -= render Attachment::EditComponent.text(f, @procedure.deliberation) += render Attachment::EditComponent.new(form: f, attached_file: @procedure.deliberation, user_can_destroy: true) %h3.header-subsection RGPD @@ -84,8 +84,7 @@ = f.label :notice, 'Notice' %p.notice Formats acceptés : .doc, .odt, .pdf, .ppt, .pptx -- notice = @procedure.notice -= render Attachment::EditComponent.text(f, @procedure.notice) += render Attachment::EditComponent.new(form: f, attached_file: @procedure.notice, user_can_destroy: true) - if !@procedure.locked? %h3.header-subsection À qui s’adresse ma démarche ? diff --git a/app/views/administrateurs/procedures/publication.html.haml b/app/views/administrateurs/procedures/publication.html.haml index e88f329a0..8abfb2512 100644 --- a/app/views/administrateurs/procedures/publication.html.haml +++ b/app/views/administrateurs/procedures/publication.html.haml @@ -44,7 +44,7 @@ Profitez de la phase de test pour tester la saisie de dossiers, ainsi que toutes les fonctionnalités associées (instruction, emails automatiques, attestations, etc.). %p Vous pouvez effectuer toutes les modifications que vous souhaitez sur votre démarche pendant cette phase de test. - %p.mb-4.bold + %p.mb-4.font-weight-bold Les dossiers qui seront remplis pendant la phase de test seront automatiquement supprimés lors de la modification ou la publication de votre démarche. %p.center %iframe{ :src =>"https://player.vimeo.com/video/334463514?color=0069CC",:width =>"640",:height =>"360",:frameborder => "0" } diff --git a/app/views/experts/avis/instruction.html.haml b/app/views/experts/avis/instruction.html.haml index 2e827f62a..e5eeb8800 100644 --- a/app/views/experts/avis/instruction.html.haml +++ b/app/views/experts/avis/instruction.html.haml @@ -17,7 +17,7 @@ = form_for @avis, url: expert_avis_path(@avis.procedure, @avis), html: { class: 'form', data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@avis) } } do |f| = f.text_area :answer, rows: 3, placeholder: 'Votre avis', required: true - = render Attachment::EditComponent.text(f, @avis.piece_justificative_file) + = render Attachment::EditComponent.new(form: f, attached_file: @avis.piece_justificative_file, user_can_destroy: true) .flex.justify-between.align-baseline %p.confidentiel.flex diff --git a/app/views/experts/avis/shared/avis/_form.html.haml b/app/views/experts/avis/shared/avis/_form.html.haml index 00df6b934..caaea9dbc 100644 --- a/app/views/experts/avis/shared/avis/_form.html.haml +++ b/app/views/experts/avis/shared/avis/_form.html.haml @@ -7,7 +7,7 @@ = f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true %p.tab-title Ajouter une pièce jointe .form-group - = render Attachment::EditComponent.text(f, avis.introduction_file) + = render Attachment::EditComponent.new(form: f, attached_file: avis.introduction_file, user_can_destroy: true) - if linked_dossiers.present? = f.check_box :invite_linked_dossiers, {}, true, false diff --git a/app/views/experts/shared/avis/_form.html.haml b/app/views/experts/shared/avis/_form.html.haml index 971397e0b..2a39e59aa 100644 --- a/app/views/experts/shared/avis/_form.html.haml +++ b/app/views/experts/shared/avis/_form.html.haml @@ -14,7 +14,7 @@ = f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true %p.tab-title Ajouter une pièce jointe .form-group - = render Attachment::EditComponent.text(f, avis.introduction_file) + = render Attachment::EditComponent.new(form: f, attached_file: avis.introduction_file, user_can_destroy: true) - if linked_dossiers.present? = f.check_box :invite_linked_dossiers, {}, true, false diff --git a/app/views/instructeurs/avis/instruction.html.haml b/app/views/instructeurs/avis/instruction.html.haml index db68c6ed8..8a26dee61 100644 --- a/app/views/instructeurs/avis/instruction.html.haml +++ b/app/views/instructeurs/avis/instruction.html.haml @@ -17,7 +17,7 @@ = form_for @avis, url: instructeur_avis_path(@avis.procedure, @avis), html: { class: 'form' } do |f| = f.text_area :answer, rows: 3, placeholder: 'Votre avis', required: true - = render Attachment::EditComponent.text(f, @avis.piece_justificative_file) + = render Attachment::EditComponent.new(form: f, attached_file: @avis.piece_justificative_file, user_can_destroy: true) .flex.justify-between.align-baseline %p.confidentiel.flex diff --git a/app/views/instructeurs/shared/avis/_form.html.haml b/app/views/instructeurs/shared/avis/_form.html.haml index 637af9dd2..9c37abf61 100644 --- a/app/views/instructeurs/shared/avis/_form.html.haml +++ b/app/views/instructeurs/shared/avis/_form.html.haml @@ -21,7 +21,7 @@ = f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true %p.tab-title Ajouter une pièce jointe .form-group - = render Attachment::EditComponent.text(f, avis.introduction_file) + = render Attachment::EditComponent.new(form: f, attached_file: avis.introduction_file, user_can_destroy: true) - if linked_dossiers.present? = f.check_box :invite_linked_dossiers, {}, true, false diff --git a/app/views/shared/dossiers/messages/_form.html.haml b/app/views/shared/dossiers/messages/_form.html.haml index bbac759cb..265c00b49 100644 --- a/app/views/shared/dossiers/messages/_form.html.haml +++ b/app/views/shared/dossiers/messages/_form.html.haml @@ -8,10 +8,7 @@ - disable_piece_jointe = defined?(disable_piece_jointe) ? disable_piece_jointe : false %div - if !disable_piece_jointe - = f.label :piece_jointe, for: :piece_jointe do - = t('views.shared.dossiers.messages.form.attach_dossier') - %span.notice= t('views.shared.dossiers.messages.form.attachment_size') - = f.file_field :piece_jointe, id: 'piece_jointe', direct_upload: true + = render Attachment::EditComponent.new(form: f, attached_file: commentaire.piece_jointe) %div = f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'button primary send', data: { disable: true } diff --git a/config/locales/en.yml b/config/locales/en.yml index 1cf8a691e..486a73e97 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -129,8 +129,6 @@ en: messages: form: send_message: "Send message" - attachment_size: "(attachment size max : 20 Mo)" - attach_dossier: "Attach a file" write_message_placeholder: "Write your message here" write_message_to_administration_placeholder: "Write your message to the administration here" demande: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index c84ea8e08..b83bf48fd 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -124,8 +124,6 @@ fr: messages: form: send_message: "Envoyer le message" - attachment_size: "(taille max : 20 Mo)" - attach_dossier: "Joindre un document" write_message_placeholder: "Écrivez votre message ici" write_message_to_administration_placeholder: "Écrivez votre message à l’administration ici" demande: diff --git a/spec/models/type_de_champ_shared_example.rb b/spec/models/type_de_champ_shared_example.rb index 8fd12d8c9..8ae7041a0 100644 --- a/spec/models/type_de_champ_shared_example.rb +++ b/spec/models/type_de_champ_shared_example.rb @@ -1,208 +1,2 @@ shared_examples 'type_de_champ_spec' do - describe 'validation' do - context 'libelle' do - it { is_expected.not_to allow_value(nil).for(:libelle) } - it { is_expected.not_to allow_value('').for(:libelle) } - it { is_expected.to allow_value('Montant projet').for(:libelle) } - end - - context 'type' do - it { is_expected.not_to allow_value(nil).for(:type_champ) } - it { is_expected.not_to allow_value('').for(:type_champ) } - - it { is_expected.to allow_value(TypeDeChamp.type_champs.fetch(:text)).for(:type_champ) } - it { is_expected.to allow_value(TypeDeChamp.type_champs.fetch(:textarea)).for(:type_champ) } - it { is_expected.to allow_value(TypeDeChamp.type_champs.fetch(:datetime)).for(:type_champ) } - it { is_expected.to allow_value(TypeDeChamp.type_champs.fetch(:number)).for(:type_champ) } - it { is_expected.to allow_value(TypeDeChamp.type_champs.fetch(:checkbox)).for(:type_champ) } - - it do - TypeDeChamp.type_champs.each do |(type_champ, _)| - type_de_champ = create(:"type_de_champ_#{type_champ}") - champ = type_de_champ.champ.create - - expect(type_de_champ.dynamic_type.class.name).to match(/^TypesDeChamp::/) - expect(champ.class.name).to match(/^Champs::/) - end - end - end - - context 'description' do - it { is_expected.to allow_value(nil).for(:description) } - it { is_expected.to allow_value('').for(:description) } - it { is_expected.to allow_value('blabla').for(:description) } - end - - context 'stable_id' do - it { - type_de_champ = create(:type_de_champ_text) - expect(type_de_champ.id).to eq(type_de_champ.stable_id) - cloned_type_de_champ = type_de_champ.clone - expect(cloned_type_de_champ.stable_id).to eq(type_de_champ.stable_id) - } - end - - context 'changing the type_champ from a piece_justificative' do - context 'when the tdc is piece_justificative' do - let(:template_double) { double('template', attached?: attached, purge_later: true) } - let(:tdc) { create(:type_de_champ_piece_justificative) } - - subject { template_double } - - before do - allow(tdc).to receive(:piece_justificative_template).and_return(template_double) - - tdc.update(type_champ: target_type_champ) - end - - context 'when the target type_champ is not pj' do - let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) } - - context 'calls template.purge_later when a file is attached' do - let(:attached) { true } - - it { is_expected.to have_received(:purge_later) } - end - - context 'does not call template.purge_later when no file is attached' do - let(:attached) { false } - - it { is_expected.not_to have_received(:purge_later) } - end - end - - context 'when the target type_champ is pj' do - let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:piece_justificative) } - - context 'does not call template.purge_later when a file is attached' do - let(:attached) { true } - - it { is_expected.not_to have_received(:purge_later) } - end - end - end - end - - describe 'changing the type_champ from a repetition' do - let!(:procedure) { create(:procedure) } - let(:tdc) { create(:type_de_champ_repetition, :with_types_de_champ, procedure: procedure) } - - before do - tdc.update(type_champ: target_type_champ) - end - - context 'when the target type_champ is not repetition' do - let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) } - - it 'removes the children types de champ' do - expect(procedure.draft_revision.children_of(tdc)).to be_empty - end - end - end - - describe 'changing the type_champ from a drop_down_list' do - let(:tdc) { create(:type_de_champ_drop_down_list) } - - before do - tdc.update(type_champ: target_type_champ) - end - - context 'when the target type_champ is not drop_down_list' do - let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) } - - it { expect(tdc.drop_down_options).to be_nil } - end - - context 'when the target type_champ is linked_drop_down_list' do - let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:linked_drop_down_list) } - - it { expect(tdc.drop_down_options).to be_present } - end - - context 'when the target type_champ is multiple_drop_down_list' do - let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) } - - it { expect(tdc.drop_down_options).to be_present } - end - end - - context 'delegate validation to dynamic type' do - subject { build(:type_de_champ_text) } - let(:dynamic_type) do - Class.new(TypesDeChamp::TypeDeChampBase) do - validate :never_valid - - def never_valid - errors.add(:troll, 'always invalid') - end - end.new(subject) - end - - before { subject.instance_variable_set(:@dynamic_type, dynamic_type) } - - it { is_expected.to be_invalid } - it do - subject.validate - expect(subject.errors.full_messages.to_sentence).to eq('Troll always invalid') - end - end - end - - describe "linked_drop_down_list" do - let(:type_de_champ) { create(:type_de_champ_linked_drop_down_list) } - - it 'should validate without label' do - type_de_champ.drop_down_list_value = 'toto' - expect(type_de_champ.validate).to be_falsey - messages = type_de_champ.errors.full_messages - expect(messages.size).to eq(1) - expect(messages.first.starts_with?("#{type_de_champ.libelle} doit commencer par")).to be_truthy - - type_de_champ.libelle = '' - expect(type_de_champ.validate).to be_falsey - messages = type_de_champ.errors.full_messages - expect(messages.size).to eq(2) - expect(messages.last.starts_with?("La liste doit commencer par")).to be_truthy - end - end - - describe '#drop_down_list_options' do - let(:value) do - <<~EOS - Cohésion sociale - Dév.Eco / Emploi - Cadre de vie / Urb. - Pilotage / Ingénierie - EOS - end - let(:type_de_champ) { create(:type_de_champ_drop_down_list, drop_down_list_value: value) } - - it { expect(type_de_champ.drop_down_list_options).to eq ['', 'Cohésion sociale', 'Dév.Eco / Emploi', 'Cadre de vie / Urb.', 'Pilotage / Ingénierie'] } - - context 'when one value is empty' do - let(:value) do - <<~EOS - Cohésion sociale - Cadre de vie / Urb. - Pilotage / Ingénierie - EOS - end - - it { expect(type_de_champ.drop_down_list_options).to eq ['', 'Cohésion sociale', 'Cadre de vie / Urb.', 'Pilotage / Ingénierie'] } - end - end - - describe 'disabled_options' do - let(:value) do - <<~EOS - tip - --top-- - --troupt-- - ouaich - EOS - end - let(:type_de_champ) { create(:type_de_champ_drop_down_list, drop_down_list_value: value) } - - it { expect(type_de_champ.drop_down_list_disabled_options).to match(['--top--', '--troupt--']) } - end end diff --git a/spec/models/type_de_champ_spec.rb b/spec/models/type_de_champ_spec.rb index 145517153..944add316 100644 --- a/spec/models/type_de_champ_spec.rb +++ b/spec/models/type_de_champ_spec.rb @@ -1,7 +1,210 @@ describe TypeDeChamp do - require 'models/type_de_champ_shared_example' + describe 'validation' do + context 'libelle' do + it { is_expected.not_to allow_value(nil).for(:libelle) } + it { is_expected.not_to allow_value('').for(:libelle) } + it { is_expected.to allow_value('Montant projet').for(:libelle) } + end - it_should_behave_like "type_de_champ_spec" + context 'type' do + it { is_expected.not_to allow_value(nil).for(:type_champ) } + it { is_expected.not_to allow_value('').for(:type_champ) } + + it { is_expected.to allow_value(TypeDeChamp.type_champs.fetch(:text)).for(:type_champ) } + it { is_expected.to allow_value(TypeDeChamp.type_champs.fetch(:textarea)).for(:type_champ) } + it { is_expected.to allow_value(TypeDeChamp.type_champs.fetch(:datetime)).for(:type_champ) } + it { is_expected.to allow_value(TypeDeChamp.type_champs.fetch(:number)).for(:type_champ) } + it { is_expected.to allow_value(TypeDeChamp.type_champs.fetch(:checkbox)).for(:type_champ) } + + it do + TypeDeChamp.type_champs.each do |(type_champ, _)| + type_de_champ = create(:"type_de_champ_#{type_champ}") + champ = type_de_champ.champ.create + + expect(type_de_champ.dynamic_type.class.name).to match(/^TypesDeChamp::/) + expect(champ.class.name).to match(/^Champs::/) + end + end + end + + context 'description' do + it { is_expected.to allow_value(nil).for(:description) } + it { is_expected.to allow_value('').for(:description) } + it { is_expected.to allow_value('blabla').for(:description) } + end + + context 'stable_id' do + it { + type_de_champ = create(:type_de_champ_text) + expect(type_de_champ.id).to eq(type_de_champ.stable_id) + cloned_type_de_champ = type_de_champ.clone + expect(cloned_type_de_champ.stable_id).to eq(type_de_champ.stable_id) + } + end + + context 'changing the type_champ from a piece_justificative' do + context 'when the tdc is piece_justificative' do + let(:template_double) { double('template', attached?: attached, purge_later: true, blob: double(byte_size: 10, content_type: 'text/plain')) } + let(:tdc) { create(:type_de_champ_piece_justificative) } + + subject { template_double } + + before do + allow(tdc).to receive(:piece_justificative_template).and_return(template_double) + + tdc.update(type_champ: target_type_champ) + end + + context 'when the target type_champ is not pj' do + let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) } + + context 'calls template.purge_later when a file is attached' do + let(:attached) { true } + + it { is_expected.to have_received(:purge_later) } + end + + context 'does not call template.purge_later when no file is attached' do + let(:attached) { false } + + it { is_expected.not_to have_received(:purge_later) } + end + end + + context 'when the target type_champ is pj' do + let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:piece_justificative) } + + context 'does not call template.purge_later when a file is attached' do + let(:attached) { true } + + it { is_expected.not_to have_received(:purge_later) } + end + end + end + end + + describe 'changing the type_champ from a repetition' do + let!(:procedure) { create(:procedure) } + let(:tdc) { create(:type_de_champ_repetition, :with_types_de_champ, procedure: procedure) } + + before do + tdc.update(type_champ: target_type_champ) + end + + context 'when the target type_champ is not repetition' do + let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) } + + it 'removes the children types de champ' do + expect(procedure.draft_revision.children_of(tdc)).to be_empty + end + end + end + + describe 'changing the type_champ from a drop_down_list' do + let(:tdc) { create(:type_de_champ_drop_down_list) } + + before do + tdc.update(type_champ: target_type_champ) + end + + context 'when the target type_champ is not drop_down_list' do + let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) } + + it { expect(tdc.drop_down_options).to be_nil } + end + + context 'when the target type_champ is linked_drop_down_list' do + let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:linked_drop_down_list) } + + it { expect(tdc.drop_down_options).to be_present } + end + + context 'when the target type_champ is multiple_drop_down_list' do + let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) } + + it { expect(tdc.drop_down_options).to be_present } + end + end + + context 'delegate validation to dynamic type' do + subject { build(:type_de_champ_text) } + let(:dynamic_type) do + Class.new(TypesDeChamp::TypeDeChampBase) do + validate :never_valid + + def never_valid + errors.add(:troll, 'always invalid') + end + end.new(subject) + end + + before { subject.instance_variable_set(:@dynamic_type, dynamic_type) } + + it { is_expected.to be_invalid } + it do + subject.validate + expect(subject.errors.full_messages.to_sentence).to eq('Troll always invalid') + end + end + end + + describe "linked_drop_down_list" do + let(:type_de_champ) { create(:type_de_champ_linked_drop_down_list) } + + it 'should validate without label' do + type_de_champ.drop_down_list_value = 'toto' + expect(type_de_champ.validate).to be_falsey + messages = type_de_champ.errors.full_messages + expect(messages.size).to eq(1) + expect(messages.first.starts_with?("#{type_de_champ.libelle} doit commencer par")).to be_truthy + + type_de_champ.libelle = '' + expect(type_de_champ.validate).to be_falsey + messages = type_de_champ.errors.full_messages + expect(messages.size).to eq(2) + expect(messages.last.starts_with?("La liste doit commencer par")).to be_truthy + end + end + + describe '#drop_down_list_options' do + let(:value) do + <<~EOS + Cohésion sociale + Dév.Eco / Emploi + Cadre de vie / Urb. + Pilotage / Ingénierie + EOS + end + let(:type_de_champ) { create(:type_de_champ_drop_down_list, drop_down_list_value: value) } + + it { expect(type_de_champ.drop_down_list_options).to eq ['', 'Cohésion sociale', 'Dév.Eco / Emploi', 'Cadre de vie / Urb.', 'Pilotage / Ingénierie'] } + + context 'when one value is empty' do + let(:value) do + <<~EOS + Cohésion sociale + Cadre de vie / Urb. + Pilotage / Ingénierie + EOS + end + + it { expect(type_de_champ.drop_down_list_options).to eq ['', 'Cohésion sociale', 'Cadre de vie / Urb.', 'Pilotage / Ingénierie'] } + end + end + + describe 'disabled_options' do + let(:value) do + <<~EOS + tip + --top-- + --troupt-- + ouaich + EOS + end + let(:type_de_champ) { create(:type_de_champ_drop_down_list, drop_down_list_value: value) } + + it { expect(type_de_champ.drop_down_list_disabled_options).to match(['--top--', '--troupt--']) } + end describe '#public_only' do let(:procedure) { create(:procedure, :with_type_de_champ, :with_type_de_champ_private) } diff --git a/spec/views/shared/attachment/_update.html.haml_spec.rb b/spec/views/shared/attachment/_update.html.haml_spec.rb index 2430cf7cd..6b724e61c 100644 --- a/spec/views/shared/attachment/_update.html.haml_spec.rb +++ b/spec/views/shared/attachment/_update.html.haml_spec.rb @@ -5,7 +5,7 @@ describe 'shared/attachment/_update.html.haml', type: :view do subject do form_for(champ.dossier) do |form| - view.render Attachment::EditComponent.image(form, attached_file) + view.render Attachment::EditComponent.new(form: form, attached_file: attached_file, user_can_destroy: true, direct_upload: true) end end @@ -55,8 +55,8 @@ describe 'shared/attachment/_update.html.haml', type: :view do form_for(champ.dossier) do |form| render Attachment::EditComponent.new(form: form, attached_file: attached_file, - accept: 'image/png', - user_can_destroy: user_can_destroy) + user_can_destroy: user_can_destroy, + direct_upload: true) end end