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 {
|
.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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
---
|
---
|
||||||
en:
|
en:
|
||||||
|
max_file_size: "File size limit : %{max_file_size}."
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
---
|
---
|
||||||
fr:
|
fr:
|
||||||
|
max_file_size: "Taille maximale : %{max_file_size}."
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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 s’adresse ma démarche ?
|
%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.).
|
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" }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 à l’administration ici"
|
write_message_to_administration_placeholder: "Écrivez votre message à l’administration ici"
|
||||||
demande:
|
demande:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue