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:
commit
e89d1d6ef2
23 changed files with 294 additions and 253 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
---
|
||||
en:
|
||||
max_file_size: "File size limit : %{max_file_size}."
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
---
|
||||
fr:
|
||||
max_file_size: "Taille maximale : %{max_file_size}."
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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 ?
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue