feat(EditComponent): add max_file_size and content_types to upload buttons inspired by dsfr

This commit is contained in:
Martin 2022-06-16 15:51:45 +02:00 committed by mfo
parent 3891b57473
commit 4ec61e58ac
23 changed files with 294 additions and 253 deletions

View file

@ -29,15 +29,19 @@
} }
.text-sm { .text-sm {
font-size: 14px; font-size: 14px !important;
} }
.text-lg { .text-lg {
font-size: 18px; font-size: 18px;
} }
.bold { .bold-weight-bold {
font-weight: bold; font-weight: bold !important;
}
.font-weight-normal {
font-weight: normal !important;
} }
.numbers-delimiter { .numbers-delimiter {

View file

@ -1,9 +1,8 @@
# Display a widget for uploading, editing and deleting a file attachment # Display a widget for uploading, editing and deleting a file attachment
class Attachment::EditComponent < ApplicationComponent 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 @form = form
@attached_file = attached_file @attached_file = attached_file
@accept = accept
@template = template @template = template
@user_can_destroy = user_can_destroy @user_can_destroy = user_can_destroy
@direct_upload = direct_upload @direct_upload = direct_upload
@ -12,16 +11,15 @@ class Attachment::EditComponent < ApplicationComponent
attr_reader :template, :form attr_reader :template, :form
def self.text(form, file) def allowed_extensions
new(form: form, attached_file: file, user_can_destroy: true) content_type_validator.options[:in]
.flat_map { |content_type| MIME::Types[content_type].map(&:extensions) }
.reject(&:blank?)
.flatten
end end
def self.image(form, file, direct_upload = true) def max_file_size
new(form: form, file_size_validator.options[:less_than]
attached_file: file,
accept: 'image/png, image/jpg, image/jpeg',
user_can_destroy: true,
direct_upload: direct_upload)
end end
def user_can_destroy? def user_can_destroy?
@ -55,14 +53,21 @@ class Attachment::EditComponent < ApplicationComponent
def file_field_options def file_field_options
{ {
class: "attachment-input #{attachment_input_class} #{'hidden' if persisted?}", class: "attachment-input #{attachment_input_class} #{'hidden' if persisted?}",
accept: @accept, accept: content_type_validator.options[:in].join(', '),
direct_upload: @direct_upload, direct_upload: @direct_upload,
id: champ&.input_id || @id, id: input_id(@id),
aria: { describedby: champ&.describedby_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 end
def input_id(given_id)
[given_id, champ&.input_id, file_field_name].reject(&:blank?).compact.first
end
def file_field_name def file_field_name
@attached_file.name @attached_file.name
end end
@ -90,4 +95,16 @@ class Attachment::EditComponent < ApplicationComponent
data: { toggle_target: ".#{attachment_input_class}" } data: { toggle_target: ".#{attachment_input_class}" }
} }
end 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 end

View file

@ -1,2 +1,3 @@
--- ---
en: en:
max_file_size: "File size limit : %{max_file_size}."

View file

@ -1,2 +1,3 @@
--- ---
fr: fr:
max_file_size: "Taille maximale : %{max_file_size}."

View file

@ -24,4 +24,8 @@
%span.icon.retry %span.icon.retry
Ré-essayer 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) = form.file_field(file_field_name, **file_field_options)

View file

@ -24,10 +24,16 @@ export class AutoUpload {
#uploader: Uploader; #uploader: Uploader;
constructor(input: HTMLInputElement, file: File) { 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.'); invariant(directUploadUrl, 'Could not find the direct upload URL.');
this.#input = input; 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. // Create, upload and attach the file.

View file

@ -7,6 +7,7 @@ import {
ERROR_CODE_ATTACH ERROR_CODE_ATTACH
} from './file-upload-error'; } from './file-upload-error';
const BYTES_TO_MB_RATIO = 1_048_576;
/** /**
Uploader class is a delegate for DirectUpload instance Uploader class is a delegate for DirectUpload instance
used to track lifecycle and progress of an upload. used to track lifecycle and progress of an upload.
@ -15,16 +16,25 @@ export default class Uploader {
directUpload: DirectUpload; directUpload: DirectUpload;
progressBar: ProgressBar; progressBar: ProgressBar;
autoAttachUrl?: string; autoAttachUrl?: string;
maxFileSize: number;
file: File;
constructor( constructor(
input: HTMLInputElement, input: HTMLInputElement,
file: File, file: File,
directUploadUrl: string, directUploadUrl: string,
autoAttachUrl?: string autoAttachUrl?: string,
maxFileSize?: string
) { ) {
this.file = file;
this.directUpload = new DirectUpload(file, directUploadUrl, this); this.directUpload = new DirectUpload(file, directUploadUrl, this);
this.progressBar = new ProgressBar(input, this.directUpload.id + '', file); this.progressBar = new ProgressBar(input, this.directUpload.id + '', file);
this.autoAttachUrl = autoAttachUrl; 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() { async start() {
this.progressBar.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 { try {
const blobSignedId = await this.upload(); const blobSignedId = await this.upload();
@ -89,7 +104,8 @@ export default class Uploader {
const errors = (error.jsonBody as { errors: string[] })?.errors; const errors = (error.jsonBody as { errors: string[] })?.errors;
const message = errors && errors[0]; const message = errors && errors[0];
throw new FileUploadError( throw new FileUploadError(
message || 'Error attaching file.', message ||
`Impossible d'associer le fichier (in english: error attaching file).'`,
error.response?.status, error.response?.status,
ERROR_CODE_ATTACH ERROR_CODE_ATTACH
); );

View file

@ -17,6 +17,7 @@
class TypeDeChamp < ApplicationRecord class TypeDeChamp < ApplicationRecord
self.ignored_columns = [:migrated_parent, :revision_id, :parent_id, :order_place] self.ignored_columns = [:migrated_parent, :revision_id, :parent_id, :order_place]
FILE_MAX_SIZE = 200.megabytes
FEATURE_FLAGS = {} FEATURE_FLAGS = {}
enum type_champs: { enum type_champs: {
@ -123,6 +124,8 @@ class TypeDeChamp < ApplicationRecord
end end
has_one_attached :piece_justificative_template 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 :libelle, presence: true, allow_blank: false, allow_nil: false
validates :type_champ, presence: true, allow_blank: false, allow_nil: false validates :type_champ, presence: true, allow_blank: false, allow_nil: false

View file

@ -25,7 +25,7 @@
= tag[:description] = tag[:description]
%h3.header-subsection Logo de l'attestation %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 %p.notice
Formats acceptés : JPG / JPEG / PNG. 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. 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 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 %p.notice
Formats acceptés : JPG / JPEG / PNG. Formats acceptés : JPG / JPEG / PNG.

View file

@ -35,7 +35,7 @@
= file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1" = 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..." } = submit_tag t('.csv_import.import_file'), class: 'button primary send', data: { disable_with: "Envoi..." }
- else - 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') = t('.csv_import.title')
%p.notice %p.notice
= t('.csv_import.import_file_procedure_not_published') = t('.csv_import.import_file_procedure_not_published')

View file

@ -20,7 +20,7 @@
= f.select :zone_id, grouped_options_for_zone = f.select :zone_id, grouped_options_for_zone
%h3.header-subsection Logo de la démarche %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 %h3.header-subsection Conservation des données
= f.label :duree_conservation_dossiers_dans_ds do = 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.text_field :cadre_juridique, class: 'form-control', placeholder: 'https://www.legifrance.gouv.fr/'
= f.label :deliberation, 'Importer le texte' = 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 %h3.header-subsection
RGPD RGPD
@ -84,8 +84,7 @@
= f.label :notice, 'Notice' = f.label :notice, 'Notice'
%p.notice %p.notice
Formats acceptés : .doc, .odt, .pdf, .ppt, .pptx Formats acceptés : .doc, .odt, .pdf, .ppt, .pptx
- notice = @procedure.notice = render Attachment::EditComponent.new(form: f, attached_file: @procedure.notice, user_can_destroy: true)
= render Attachment::EditComponent.text(f, @procedure.notice)
- if !@procedure.locked? - if !@procedure.locked?
%h3.header-subsection À qui sadresse ma démarche ? %h3.header-subsection À qui sadresse ma démarche ?

View file

@ -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.). 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 %p
Vous pouvez effectuer toutes les modifications que vous souhaitez sur votre démarche pendant cette phase de test. 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. 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 %p.center
%iframe{ :src =>"https://player.vimeo.com/video/334463514?color=0069CC",:width =>"640",:height =>"360",:frameborder => "0" } %iframe{ :src =>"https://player.vimeo.com/video/334463514?color=0069CC",:width =>"640",:height =>"360",:frameborder => "0" }

View file

@ -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| = 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 = 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 .flex.justify-between.align-baseline
%p.confidentiel.flex %p.confidentiel.flex

View file

@ -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 = 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 %p.tab-title Ajouter une pièce jointe
.form-group .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? - if linked_dossiers.present?
= f.check_box :invite_linked_dossiers, {}, true, false = f.check_box :invite_linked_dossiers, {}, true, false

View file

@ -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 = 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 %p.tab-title Ajouter une pièce jointe
.form-group .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? - if linked_dossiers.present?
= f.check_box :invite_linked_dossiers, {}, true, false = f.check_box :invite_linked_dossiers, {}, true, false

View file

@ -17,7 +17,7 @@
= form_for @avis, url: instructeur_avis_path(@avis.procedure, @avis), html: { class: 'form' } do |f| = 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 = 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 .flex.justify-between.align-baseline
%p.confidentiel.flex %p.confidentiel.flex

View file

@ -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 = 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 %p.tab-title Ajouter une pièce jointe
.form-group .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? - if linked_dossiers.present?
= f.check_box :invite_linked_dossiers, {}, true, false = f.check_box :invite_linked_dossiers, {}, true, false

View file

@ -8,10 +8,7 @@
- disable_piece_jointe = defined?(disable_piece_jointe) ? disable_piece_jointe : false - disable_piece_jointe = defined?(disable_piece_jointe) ? disable_piece_jointe : false
%div %div
- if !disable_piece_jointe - if !disable_piece_jointe
= f.label :piece_jointe, for: :piece_jointe do = render Attachment::EditComponent.new(form: f, attached_file: commentaire.piece_jointe)
= 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
%div %div
= f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'button primary send', data: { disable: true } = f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'button primary send', data: { disable: true }

View file

@ -129,8 +129,6 @@ en:
messages: messages:
form: form:
send_message: "Send message" 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_placeholder: "Write your message here"
write_message_to_administration_placeholder: "Write your message to the administration here" write_message_to_administration_placeholder: "Write your message to the administration here"
demande: demande:

View file

@ -124,8 +124,6 @@ fr:
messages: messages:
form: form:
send_message: "Envoyer le message" 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_placeholder: "Écrivez votre message ici"
write_message_to_administration_placeholder: "Écrivez votre message à ladministration ici" write_message_to_administration_placeholder: "Écrivez votre message à ladministration ici"
demande: demande:

View file

@ -1,208 +1,2 @@
shared_examples 'type_de_champ_spec' do 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 end

View file

@ -1,7 +1,210 @@
describe TypeDeChamp do 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 describe '#public_only' do
let(:procedure) { create(:procedure, :with_type_de_champ, :with_type_de_champ_private) } let(:procedure) { create(:procedure, :with_type_de_champ, :with_type_de_champ_private) }

View file

@ -5,7 +5,7 @@ describe 'shared/attachment/_update.html.haml', type: :view do
subject do subject do
form_for(champ.dossier) do |form| 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
end end
@ -55,8 +55,8 @@ describe 'shared/attachment/_update.html.haml', type: :view do
form_for(champ.dossier) do |form| form_for(champ.dossier) do |form|
render Attachment::EditComponent.new(form: form, render Attachment::EditComponent.new(form: form,
attached_file: attached_file, attached_file: attached_file,
accept: 'image/png', user_can_destroy: user_can_destroy,
user_can_destroy: user_can_destroy) direct_upload: true)
end end
end end