Merge pull request #7471 from betagouv/US/edit_component_max_file_size

feat(EditComponent): for uploaded files, display and validate max_file_size and content_type for attached files on the front
This commit is contained in:
mfo 2022-06-28 17:50:00 +02:00 committed by GitHub
commit e89d1d6ef2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 294 additions and 253 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 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.).
%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" }

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

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

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

View file

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

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

View file

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

View file

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

View file

@ -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 à ladministration ici"
demande:

View file

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

View file

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

View file

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