Merge pull request #7958 from colinux/feat-pj-multiple
feat(dossier): piece justificative allows multiple attachments
This commit is contained in:
commit
6f5cd5a2ad
102 changed files with 1290 additions and 478 deletions
|
@ -1,59 +1,37 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
margin-bottom: $default-spacer;
|
||||
}
|
||||
.attachment-error,
|
||||
.attachment-upload-error {
|
||||
position: relative;
|
||||
|
||||
.attachment-action {
|
||||
margin-right: $default-spacer;
|
||||
|
||||
.button {
|
||||
text-transform: lowercase;
|
||||
background-image: none; // remove DSFR underline, TODO: switch to DSFR download links https://github.com/betagouv/demarches-simplifiees.fr/issues/7883
|
||||
&::before {
|
||||
box-shadow: inset 2px 0 0 0 var(--border-plain-error);
|
||||
height: 100%;
|
||||
content: "";
|
||||
left: -0.75rem;
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.attachment-link {
|
||||
a:not(:hover) {
|
||||
background-image: none; // remove DSFR underline
|
||||
.attachment-filename {
|
||||
color: var(--text-default-error);
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-error {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
margin-bottom: $default-padding;
|
||||
padding: $default-padding;
|
||||
background: $background-red;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
.fr-error-text {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-error-message {
|
||||
display: inline-block;
|
||||
margin-right: $default-padding;
|
||||
color: $medium-red;
|
||||
}
|
||||
.attachment-multiple:not(.fr-downloads-group) {
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.attachment-error-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.attachment-error-retry {
|
||||
white-space: nowrap;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
li {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-input.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
:not(.fr-downloads-group) > ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: inside;
|
||||
padding-left: $default-padding;
|
||||
|
|
|
@ -77,6 +77,8 @@ $dossier-actions-bar-border-width: 1px;
|
|||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom: none;
|
||||
|
||||
z-index: 10; // above DSFR btn which are at 1
|
||||
}
|
||||
|
||||
.send-dossier-actions-bar {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import "constants";
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
|
||||
|
@ -49,3 +51,7 @@
|
|||
.flex-no-shrink {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flex-gap-2 {
|
||||
gap: 2 * $default-spacer; // scss-lint:disable PropertySpelling
|
||||
}
|
||||
|
|
|
@ -170,8 +170,7 @@
|
|||
input[type=number],
|
||||
input[type=tel],
|
||||
textarea,
|
||||
select,
|
||||
.attachment {
|
||||
select {
|
||||
display: block;
|
||||
margin-bottom: $default-fields-spacer;
|
||||
|
||||
|
@ -386,6 +385,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.editable-champ-titre_identite { // scss-lint:disable SelectorFormat
|
||||
margin-bottom: 2 * $default-padding;
|
||||
}
|
||||
|
||||
.cnaf-inputs,
|
||||
.dgfip-inputs,
|
||||
.pole-emploi-inputs,
|
||||
|
|
|
@ -67,6 +67,8 @@
|
|||
position: -webkit-sticky; // This is needed on Safari (tested on 12.1)
|
||||
// scss-lint:enable VendorPrefix
|
||||
bottom: 0;
|
||||
|
||||
z-index: 10; // above DSFR btn which are at 1
|
||||
}
|
||||
|
||||
html.scroll-margins-for-sticky-footer {
|
||||
|
|
|
@ -29,10 +29,6 @@
|
|||
float: right;
|
||||
}
|
||||
|
||||
.attachment-link {
|
||||
margin-top: $default-spacer;
|
||||
}
|
||||
|
||||
.message-answer-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
padding: 0 $default-padding;
|
||||
background-color: $light-grey;
|
||||
|
||||
input[type=file] {
|
||||
background-color: transparent; // Remove white bg set by DSFR
|
||||
}
|
||||
|
||||
&.no-background {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@import "constants";
|
||||
|
||||
.rich-text {
|
||||
.rich-text:not(.piece_justificative):not(.titre_identite) {
|
||||
i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
|
@ -2,8 +2,11 @@ class ApplicationComponent < ViewComponent::Base
|
|||
include ViewComponent::Translatable
|
||||
include FlipperHelper
|
||||
|
||||
# Takes a Hash of { class_name: boolean }.
|
||||
# Returns truthy class names in an array. Array can be passed as-it in rails helpers,
|
||||
# and is still manipulable if needed.
|
||||
def class_names(class_names)
|
||||
class_names.to_a.filter_map { |(class_name, flag)| class_name if flag }.join(' ')
|
||||
class_names.filter { _2 }.keys
|
||||
end
|
||||
|
||||
def current_user
|
||||
|
|
|
@ -1,113 +1,184 @@
|
|||
# Display a widget for uploading, editing and deleting a file attachment
|
||||
class Attachment::EditComponent < ApplicationComponent
|
||||
def initialize(form:, attached_file:, template: nil, user_can_destroy: false, direct_upload: true, id: nil)
|
||||
@form = form
|
||||
attr_reader :champ
|
||||
attr_reader :attachment
|
||||
attr_reader :user_can_download
|
||||
alias user_can_download? user_can_download
|
||||
attr_reader :user_can_destroy
|
||||
alias user_can_destroy? user_can_destroy
|
||||
attr_reader :as_multiple
|
||||
alias as_multiple? as_multiple
|
||||
|
||||
EXTENSIONS_ORDER = ['jpeg', 'png', 'pdf', 'zip'].freeze
|
||||
|
||||
def initialize(champ: nil, auto_attach_url: nil, attached_file:, direct_upload: true, index: 0, as_multiple: false, user_can_download: false, user_can_destroy: true, **kwargs)
|
||||
@as_multiple = as_multiple
|
||||
@attached_file = attached_file
|
||||
@template = template
|
||||
@user_can_destroy = user_can_destroy
|
||||
@auto_attach_url = auto_attach_url
|
||||
@champ = champ
|
||||
@direct_upload = direct_upload
|
||||
@id = id
|
||||
@index = index
|
||||
@user_can_download = user_can_download
|
||||
@user_can_destroy = user_can_destroy
|
||||
|
||||
# attachment passed by kwarg because we don't want a default (nil) value.
|
||||
@attachment = if kwargs.key?(:attachment)
|
||||
kwargs.delete(:attachment)
|
||||
elsif attached_file.respond_to?(:attachment)
|
||||
attached_file.attachment
|
||||
else
|
||||
fail ArgumentError, "You must pass an `attachment` kwarg when not using as single attachment like in #{attached_file.name}. Set it to nil for a new attachment."
|
||||
end
|
||||
|
||||
# When parent form has nested attributes, pass the form builder object_name
|
||||
# to correctly infer the input attribute name.
|
||||
@form_object_name = kwargs.delete(:form_object_name)
|
||||
|
||||
verify_initialization!(kwargs)
|
||||
end
|
||||
|
||||
attr_reader :template, :form
|
||||
def first?
|
||||
@index.zero?
|
||||
end
|
||||
|
||||
def max_file_size
|
||||
return if file_size_validator.nil?
|
||||
|
||||
file_size_validator.options[:less_than]
|
||||
end
|
||||
|
||||
def user_can_destroy?
|
||||
@user_can_destroy
|
||||
end
|
||||
|
||||
def attachment
|
||||
@attached_file.attachment
|
||||
end
|
||||
|
||||
def attachment_path
|
||||
helpers.attachment_path attachment.id, { signed_id: attachment.blob.signed_id }
|
||||
end
|
||||
|
||||
def attachment_id
|
||||
@attachment_id ||= attachment ? attachment.id : SecureRandom.uuid
|
||||
@attachment_id ||= (attachment&.id || SecureRandom.uuid)
|
||||
end
|
||||
|
||||
def attachment_path(**args)
|
||||
helpers.attachment_path attachment.id, args.merge(signed_id: attachment.blob.signed_id)
|
||||
end
|
||||
|
||||
def destroy_attachment_path
|
||||
attachment_path(champ_id: champ&.id)
|
||||
end
|
||||
|
||||
def attachment_input_class
|
||||
"attachment-input-#{attachment_id}"
|
||||
end
|
||||
|
||||
def persisted?
|
||||
attachment&.persisted?
|
||||
end
|
||||
|
||||
def champ
|
||||
@form.object.is_a?(Champ) ? @form.object : nil
|
||||
end
|
||||
|
||||
def file_field_options
|
||||
track_issue_with_missing_validators if missing_validators?
|
||||
{
|
||||
class: "attachment-input #{attachment_input_class} #{'hidden' if persisted?}",
|
||||
class: "fr-upload attachment-input #{attachment_input_class} #{persisted? ? 'hidden' : ''}",
|
||||
direct_upload: @direct_upload,
|
||||
id: input_id(@id),
|
||||
id: input_id,
|
||||
aria: { describedby: champ&.describedby_id },
|
||||
data: {
|
||||
auto_attach_url: helpers.auto_attach_url(form.object)
|
||||
}.merge(has_file_size_validator? ? { max_file_size: max_file_size } : {})
|
||||
auto_attach_url:
|
||||
}.merge(has_file_size_validator? ? { max_file_size: } : {})
|
||||
}.merge(has_content_type_validator? ? { accept: accept_content_type } : {})
|
||||
end
|
||||
|
||||
def input_id(given_id)
|
||||
[given_id, champ&.input_id, file_field_name].reject(&:blank?).compact.first
|
||||
def poll_url
|
||||
if champ.present?
|
||||
auto_attach_url
|
||||
else
|
||||
attachment_path(user_can_edit: true, user_can_download: @user_can_download, auto_attach_url: @auto_attach_url)
|
||||
end
|
||||
end
|
||||
|
||||
def file_field_name
|
||||
def field_name
|
||||
helpers.field_name(@form_object_name || ActiveModel::Naming.param_key(@attached_file.record), attribute_name)
|
||||
end
|
||||
|
||||
def attribute_name
|
||||
@attached_file.name
|
||||
end
|
||||
|
||||
def remove_button_options
|
||||
{
|
||||
role: 'button',
|
||||
class: 'button small danger',
|
||||
data: { turbo_method: :delete }
|
||||
data: { turbo: "true", turbo_method: :delete }
|
||||
}
|
||||
end
|
||||
|
||||
def retry_button_options
|
||||
{
|
||||
type: 'button',
|
||||
class: 'button attachment-error-retry',
|
||||
class: 'fr-btn fr-btn--sm fr-btn--tertiary fr-mt-1w attachment-upload-error-retry',
|
||||
data: { input_target: ".#{attachment_input_class}", action: 'autosave#onClickRetryButton' }
|
||||
}
|
||||
end
|
||||
|
||||
def replace_button_options
|
||||
{
|
||||
type: 'button',
|
||||
class: 'button small',
|
||||
data: { toggle_target: ".#{attachment_input_class}" }
|
||||
}
|
||||
def persisted?
|
||||
!!attachment&.persisted?
|
||||
end
|
||||
|
||||
def downloadable?
|
||||
return false unless user_can_download?
|
||||
return false if attachment.virus_scanner_error?
|
||||
return false if attachment.watermark_pending?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def error?
|
||||
attachment.virus_scanner_error?
|
||||
end
|
||||
|
||||
def error_message
|
||||
case
|
||||
when attachment.virus_scanner.infected?
|
||||
t(".errors.virus_infected")
|
||||
when attachment.virus_scanner.corrupt?
|
||||
t(".errors.corrupted_file")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def input_id
|
||||
if champ.present?
|
||||
# There is always a single input by champ, its id must match the label "for" attribute.
|
||||
return champ.input_id
|
||||
end
|
||||
|
||||
helpers.field_id(@form_object_name || @attached_file.record, attribute_name)
|
||||
end
|
||||
|
||||
def auto_attach_url
|
||||
return @auto_attach_url if @auto_attach_url.present?
|
||||
|
||||
return helpers.auto_attach_url(@champ) if @champ.present?
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def file_size_validator
|
||||
@attached_file.record
|
||||
._validators[file_field_name.to_sym]
|
||||
._validators[attribute_name.to_sym]
|
||||
.find { |validator| validator.class == ActiveStorageValidations::SizeValidator }
|
||||
end
|
||||
|
||||
def content_type_validator
|
||||
@attached_file.record
|
||||
._validators[file_field_name.to_sym]
|
||||
._validators[attribute_name.to_sym]
|
||||
.find { |validator| validator.class == ActiveStorageValidations::ContentTypeValidator }
|
||||
end
|
||||
|
||||
def accept_content_type
|
||||
list = content_type_validator.options[:in]
|
||||
if list.include?("application/octet-stream")
|
||||
list.push(".acidcsa")
|
||||
end
|
||||
list = content_type_validator.options[:in].dup
|
||||
list << ".acidcsa" if list.include?("application/octet-stream")
|
||||
list.join(', ')
|
||||
end
|
||||
|
||||
def allowed_formats
|
||||
return nil unless champ&.titre_identite?
|
||||
|
||||
@allowed_formats ||= begin
|
||||
content_type_validator.options[:in].filter_map do |content_type|
|
||||
MiniMime.lookup_by_content_type(content_type)&.extension
|
||||
end.uniq.sort_by { EXTENSIONS_ORDER.index(_1) || 999 }
|
||||
end
|
||||
end
|
||||
|
||||
def has_content_type_validator?
|
||||
!content_type_validator.nil?
|
||||
end
|
||||
|
@ -122,12 +193,16 @@ class Attachment::EditComponent < ApplicationComponent
|
|||
return false
|
||||
end
|
||||
|
||||
def verify_initialization!(kwargs)
|
||||
fail ArgumentError, "Unknown kwarg #{kwargs.keys.join(', ')}" unless kwargs.empty?
|
||||
end
|
||||
|
||||
def track_issue_with_missing_validators
|
||||
Sentry.capture_message(
|
||||
"Strange case of missing validator",
|
||||
extra: {
|
||||
champ: champ,
|
||||
file_field_name: file_field_name,
|
||||
field_name: field_name,
|
||||
attachment_id: attachment_id
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
---
|
||||
en:
|
||||
max_file_size: "File size limit : %{max_file_size}."
|
||||
allowed_formats: "Supported formats : %{formats}"
|
||||
retry: Retry
|
||||
delete: Delete
|
||||
errors:
|
||||
uploading: "An error occurred while sending the file."
|
||||
virus_infected: "Virus detected, please send another file."
|
||||
corrupted_file: "The file is corrupted, please send another file."
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
---
|
||||
fr:
|
||||
max_file_size: "Taille maximale : %{max_file_size}."
|
||||
allowed_formats: "Formats supportés : %{formats}"
|
||||
retry: Réessayer
|
||||
delete: Supprimer
|
||||
errors:
|
||||
uploading: "Une erreur s’est produite pendant l’envoi du fichier."
|
||||
virus_infected: "Virus détecté, merci d’envoyer un autre fichier."
|
||||
corrupted_file: "Le fichier est corrompu, merci d’envoyer un autre fichier."
|
||||
|
||||
|
|
|
@ -1,37 +1,39 @@
|
|||
.attachment
|
||||
- if template&.attached?
|
||||
%p.mb-1
|
||||
Veuillez télécharger, remplir et joindre
|
||||
= link_to(url_for(template), download: "", class: "fr-link fr-link--icon-right fr-icon-download-line") do
|
||||
le modèle suivant
|
||||
|
||||
- if helpers.administrateur_signed_in?
|
||||
%span.ml-2.fr-text--xs.fr-text-mention--grey.visible-on-previous-hover
|
||||
%span.fr-text-action-high--blue-france.fr-icon-questionnaire-line{ "aria-hidden": "true" }
|
||||
= t('shared.ephemeral_link')
|
||||
|
||||
.attachment.fr-upload-group{ { id: attachment ? dom_id(attachment, :edit) : nil, class: class_names("fr-mb-2w": !(as_multiple? && downloadable?)) }.compact }
|
||||
- if persisted?
|
||||
.attachment-actions{ id: dom_id(attachment, :actions) }
|
||||
.attachment-action
|
||||
= render Attachment::ShowComponent.new(attachment: attachment, user_can_upload: true)
|
||||
- if user_can_destroy?
|
||||
.attachment-action{ "data-turbo": "true" }
|
||||
= link_to('Supprimer', attachment_path, **remove_button_options)
|
||||
.attachment-action
|
||||
= button_tag('Remplacer', **replace_button_options)
|
||||
%div{ id: dom_id(attachment, :persisted_row) }
|
||||
.flex.flex-gap-2{ class: class_names("attachment-error": attachment.virus_scanner_error?) }
|
||||
- if user_can_destroy?
|
||||
= link_to(t('.delete'), destroy_attachment_path, **remove_button_options, class: "fr-btn fr-btn--tertiary fr-btn--sm fr-icon-delete-line", title: "Supprimer le fichier #{attachment.filename}")
|
||||
|
||||
.attachment-error.hidden
|
||||
.attachment-error-message
|
||||
%p.attachment-error-title
|
||||
Une erreur s’est produite pendant l’envoi du fichier.
|
||||
%p.attachment-error-description
|
||||
Une erreur inconnue s'est produite pendant l'envoi du fichier
|
||||
= button_tag(**retry_button_options) do
|
||||
%span.icon.retry
|
||||
Ré-essayer
|
||||
.fr-py-1v
|
||||
- if downloadable?
|
||||
= render Dsfr::DownloadComponent.new(attachment:)
|
||||
- else
|
||||
%span.attachment-filename.fr-mr-1w-= attachment.filename.to_s
|
||||
|
||||
= render Attachment::ProgressComponent.new(attachment: attachment)
|
||||
|
||||
- if error?
|
||||
%p.fr-error-text= error_message
|
||||
|
||||
- elsif first?
|
||||
%p.fr-text--sm.fr-text-mention--grey.fr-mb-1w
|
||||
- if max_file_size.present?
|
||||
= t('.max_file_size', max_file_size: number_to_human_size(max_file_size))
|
||||
- if allowed_formats
|
||||
= t('.allowed_formats', formats: allowed_formats.join(', '))
|
||||
|
||||
|
||||
%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))
|
||||
- if !as_multiple?
|
||||
= file_field(champ, field_name, **file_field_options)
|
||||
|
||||
- if persisted?
|
||||
- Attachment::PendingPollComponent.new(attachment: attachment, poll_url:).then do |component|
|
||||
.fr-mt-2w
|
||||
= render component
|
||||
|
||||
.attachment-upload-error.hidden
|
||||
%p.fr-error-text= t('.errors.uploading')
|
||||
= button_tag(**retry_button_options) do
|
||||
= t(".retry")
|
||||
|
||||
= form.file_field(file_field_name, **file_field_options)
|
||||
|
|
45
app/components/attachment/multiple_component.rb
Normal file
45
app/components/attachment/multiple_component.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Display a widget for uploading, editing and deleting a file attachment
|
||||
class Attachment::MultipleComponent < ApplicationComponent
|
||||
DEFAULT_MAX_ATTACHMENTS = 10
|
||||
|
||||
renders_one :template
|
||||
|
||||
attr_reader :attached_file
|
||||
attr_reader :attachments
|
||||
attr_reader :champ
|
||||
attr_reader :form_object_name
|
||||
attr_reader :max
|
||||
attr_reader :user_can_destroy
|
||||
attr_reader :user_can_download
|
||||
alias user_can_download? user_can_download
|
||||
|
||||
delegate :count, :empty?, to: :attachments, prefix: true
|
||||
|
||||
def initialize(champ:, attached_file:, form_object_name: nil, user_can_download: false, user_can_destroy: true, max: nil)
|
||||
@champ = champ
|
||||
@attached_file = attached_file
|
||||
@form_object_name = form_object_name
|
||||
@user_can_download = user_can_download
|
||||
@user_can_destroy = user_can_destroy
|
||||
@max = max || DEFAULT_MAX_ATTACHMENTS
|
||||
|
||||
@attachments = attached_file.attachments || []
|
||||
end
|
||||
|
||||
def each_attachment(&block)
|
||||
@attachments.each_with_index(&block)
|
||||
end
|
||||
|
||||
def can_attach_next?
|
||||
@attachments.count < @max
|
||||
end
|
||||
|
||||
def empty_component_id
|
||||
"attachment-multiple-empty-#{champ.id}"
|
||||
end
|
||||
|
||||
def auto_attach_url
|
||||
helpers.auto_attach_url(champ)
|
||||
end
|
||||
alias poll_url auto_attach_url
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
.fr-mb-4w.attachment-multiple{ class: class_names("fr-downloads-group": user_can_download?) }
|
||||
= template
|
||||
|
||||
%ul
|
||||
- each_attachment do |attachment, index|
|
||||
%li{ id: dom_id(attachment) }
|
||||
= render Attachment::EditComponent.new(champ:, attached_file:, attachment:, index:, as_multiple: true, user_can_destroy:, user_can_download:, form_object_name:)
|
||||
|
||||
%div{ id: empty_component_id, class: class_names("hidden": !can_attach_next?) }
|
||||
= render Attachment::EditComponent.new(champ:, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:)
|
||||
|
||||
// single poll and refresh message for all attachments
|
||||
= render Attachment::PendingPollComponent.new(attachments: attachments, poll_url:)
|
33
app/components/attachment/pending_poll_component.rb
Normal file
33
app/components/attachment/pending_poll_component.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
class Attachment::PendingPollComponent < ApplicationComponent
|
||||
def initialize(poll_url:, attachment: nil, attachments: nil)
|
||||
@poll_url = poll_url
|
||||
@attachments = if attachment.present?
|
||||
[attachment]
|
||||
else
|
||||
attachments
|
||||
end
|
||||
end
|
||||
|
||||
def render?
|
||||
@attachments.any? { pending_attachment?(_1) }
|
||||
end
|
||||
|
||||
def long_pending?
|
||||
@attachments.any? do
|
||||
pending_attachment?(_1) && _1.created_at < 30.seconds.ago
|
||||
end
|
||||
end
|
||||
|
||||
def poll_controller_options
|
||||
{
|
||||
controller: 'turbo-poll',
|
||||
turbo_poll_url_value: @poll_url
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pending_attachment?(attachment)
|
||||
attachment.virus_scanner.pending? || attachment.watermark_pending?
|
||||
end
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
en:
|
||||
reload: Reload
|
||||
explanation: Scanning for viruses and processing your attachment(s) takes longer than expected.
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
fr:
|
||||
reload: Recharger
|
||||
explanation: L’analyse antivirus et le traitement de votre ou de vos pièces jointes prend plus de temps que prévu.
|
|
@ -0,0 +1,7 @@
|
|||
%div{ data: poll_controller_options }
|
||||
- if long_pending?
|
||||
= render Dsfr::CalloutComponent.new(title: nil) do |c|
|
||||
- c.with_body do
|
||||
= t(".explanation")
|
||||
- c.with_bottom do
|
||||
= button_tag t(".reload"), type: "button", class: "fr-btn", data: { action: 'click->turbo-poll#refresh' }
|
20
app/components/attachment/progress_component.rb
Normal file
20
app/components/attachment/progress_component.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
class Attachment::ProgressComponent < ApplicationComponent
|
||||
attr_reader :attachment
|
||||
|
||||
def initialize(attachment:)
|
||||
@attachment = attachment
|
||||
end
|
||||
|
||||
def progress_label
|
||||
case
|
||||
when attachment.virus_scanner.pending?
|
||||
t(".antivirus_pending")
|
||||
when attachment.watermark_pending?
|
||||
t(".watermark_pending")
|
||||
end
|
||||
end
|
||||
|
||||
def render?
|
||||
progress_label.present?
|
||||
end
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
en:
|
||||
antivirus_pending: "Antivirus scanning in progress…"
|
||||
watermark_pending: "Processing in progress…"
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
fr:
|
||||
antivirus_pending: "Analyse antivirus en cours…"
|
||||
watermark_pending: "Traitement en cours…"
|
|
@ -0,0 +1,2 @@
|
|||
%p.fr-badge.fr-badge--info.fr-badge--sm.fr-badge--no-icon
|
||||
= progress_label
|
|
@ -1,27 +1,24 @@
|
|||
class Attachment::ShowComponent < ApplicationComponent
|
||||
def initialize(attachment:, user_can_upload: false)
|
||||
def initialize(attachment:)
|
||||
@attachment = attachment
|
||||
@user_can_upload = user_can_upload
|
||||
end
|
||||
|
||||
attr_reader :attachment
|
||||
|
||||
def user_can_upload?
|
||||
@user_can_upload
|
||||
end
|
||||
|
||||
def should_display_link?
|
||||
(attachment.virus_scanner.safe? || !attachment.virus_scanner.started?) && !attachment.watermark_pending?
|
||||
end
|
||||
|
||||
def attachment_path
|
||||
helpers.attachment_path(attachment.id, { signed_id: attachment.blob.signed_id, user_can_upload: user_can_upload? })
|
||||
def error_message
|
||||
case
|
||||
when attachment.virus_scanner.infected?
|
||||
t(".errors.virus_infected")
|
||||
when attachment.virus_scanner.corrupt?
|
||||
t(".errors.corrupted_file")
|
||||
end
|
||||
end
|
||||
|
||||
def poll_controller_options
|
||||
{
|
||||
controller: 'turbo-poll',
|
||||
turbo_poll_url_value: attachment_path
|
||||
}
|
||||
def error?
|
||||
attachment.virus_scanner_error?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
---
|
||||
en:
|
||||
virus_not_analyzed: this file has not been scanned by our antivirus, download it with care
|
||||
errors:
|
||||
virus_infected: Virus detected, download is blocked.
|
||||
corrupted_file: The file is corrupted, the download is blocked.
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
---
|
||||
fr:
|
||||
virus_not_analyzed: ce fichier n’a pas été analysé par notre antivirus, téléchargez-le avec précaution
|
||||
errors:
|
||||
virus_infected: "Virus détecté, le téléchargement est bloqué."
|
||||
corrupted_file: "Le fichier est corrompu, le téléchargement est bloqué."
|
||||
|
|
|
@ -1,30 +1,14 @@
|
|||
.attachment-link{ id: dom_id(attachment, :show) }
|
||||
%div{ id: dom_id(attachment, :show), class: class_names("attachment-error": error?, "fr-mb-2w": !should_display_link?) }
|
||||
- if should_display_link?
|
||||
= link_to url_for(attachment.blob), target: '_blank', rel: 'noopener', title: "Télécharger la pièce jointe" do
|
||||
%span.icon.attached
|
||||
= attachment.filename.to_s
|
||||
- if !attachment.virus_scanner.started?
|
||||
(ce fichier n’a pas été analysé par notre antivirus, téléchargez-le avec précaution)
|
||||
= render Dsfr::DownloadComponent.new(attachment: attachment) do |c|
|
||||
- if !attachment.virus_scanner.started?
|
||||
- c.right do
|
||||
= "(#{t(".virus_not_analyzed")})"
|
||||
|
||||
- else
|
||||
%span{ data: poll_controller_options }
|
||||
= attachment.filename.to_s
|
||||
- if attachment.virus_scanner.pending?
|
||||
(analyse antivirus en cours
|
||||
= link_to "rafraichir", attachment_path, data: { action: 'turbo-poll#refresh' }
|
||||
)
|
||||
- elsif attachment.watermark_pending?
|
||||
(traitement de la pièce en cours
|
||||
= link_to "rafraichir", attachment_path, data: { action: 'turbo-poll#refresh' }
|
||||
)
|
||||
- elsif attachment.virus_scanner.infected?
|
||||
- if user_can_upload?
|
||||
(virus détecté, merci d’envoyer un autre fichier)
|
||||
- else
|
||||
(virus détecté, le téléchargement de ce fichier est bloqué)
|
||||
- elsif attachment.virus_scanner.corrupt?
|
||||
- if user_can_upload?
|
||||
(le fichier est corrompu, merci d’envoyer un autre fichier)
|
||||
- else
|
||||
(le fichier est corrompu, le téléchargement est bloqué)
|
||||
.attachment-filename.fr-mb-1w.fr-mr-1w= attachment.filename.to_s
|
||||
|
||||
= render Attachment::ProgressComponent.new(attachment: attachment)
|
||||
|
||||
- if error?
|
||||
%p.fr-error-text= error_message
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
= t('.delete_button')
|
||||
|
||||
- if commentaire.piece_jointe.attached?
|
||||
.attachment-link
|
||||
.fr-ml-2w
|
||||
= render Attachment::ShowComponent.new(attachment: commentaire.piece_jointe.attachment)
|
||||
|
||||
- if show_reply_button?
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
%div{ class: callout_class }
|
||||
%h3.fr-callout__title= title
|
||||
- if title.present?
|
||||
%h3.fr-callout__title= title
|
||||
%p.fr-callout__text= body
|
||||
= bottom
|
||||
|
|
16
app/components/dsfr/download_component.rb
Normal file
16
app/components/dsfr/download_component.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class Dsfr::DownloadComponent < ApplicationComponent
|
||||
renders_one :right
|
||||
|
||||
attr_reader :attachment
|
||||
attr_reader :html_class
|
||||
attr_reader :name
|
||||
|
||||
def initialize(attachment:, name: nil)
|
||||
@attachment = attachment
|
||||
@name = name || attachment.filename.to_s
|
||||
end
|
||||
|
||||
def title
|
||||
t(".title", filename: attachment.filename.to_s)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
en:
|
||||
title: "Download file %{filename}"
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
fr:
|
||||
title: "Télécharger le fichier %{filename}"
|
|
@ -0,0 +1,8 @@
|
|||
.fr-download
|
||||
%p
|
||||
= link_to url_for(attachment.blob), download: "", class: "fr-download__link", title: title do
|
||||
= name
|
||||
%span.fr-download__detail
|
||||
= helpers.download_details(attachment)
|
||||
|
||||
= right
|
|
@ -18,7 +18,10 @@ class EditableChamp::EditableChampComponent < ApplicationComponent
|
|||
|
||||
def html_options
|
||||
{
|
||||
class: "editable-champ-#{@champ.type_champ} #{'hidden' if !@champ.visible?}",
|
||||
class: class_names(
|
||||
"editable-champ-#{@champ.type_champ}": true,
|
||||
"hidden": !@champ.visible?
|
||||
),
|
||||
id: @champ.input_group_id,
|
||||
data: { controller: stimulus_controller, block: @champ.block? }
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
- elsif has_label?(@champ)
|
||||
= render EditableChamp::ChampLabelComponent.new form: @form, champ: @champ, seen_at: @seen_at
|
||||
- if @champ.type_champ == "titre_identite"
|
||||
%p.notice Carte nationale d’identité (uniquement le recto), passeport, titre de séjour ou autre justificatif d’identité. Formats acceptés : jpg/png
|
||||
%p.notice Carte nationale d’identité (uniquement le recto), passeport, titre de séjour ou autre justificatif d’identité.
|
||||
|
||||
= @form.hidden_field :id, value: @champ.id, data: @champ.block? ? { id: true } : {}
|
||||
= render component_class.new(form: @form, champ: @champ, seen_at: @seen_at)
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
- user_can_destroy = !@champ.mandatory? || @champ.dossier.brouillon?
|
||||
= render Attachment::EditComponent.new(form: @form, attached_file: @champ.piece_justificative_file, template: @champ.type_de_champ.piece_justificative_template, user_can_destroy: user_can_destroy)
|
||||
- user_can_download = !@champ.dossier.brouillon?
|
||||
- max = [true, nil].include?(@champ.procedure&.piece_justificative_multiple?) ? Attachment::MultipleComponent::DEFAULT_MAX_ATTACHMENTS : 1
|
||||
= render Attachment::MultipleComponent.new(champ: @champ, attached_file: @champ.piece_justificative_file, form_object_name: @form.object_name, user_can_destroy:, user_can_download:, max:) do |c|
|
||||
- if @champ.type_de_champ.piece_justificative_template&.attached?
|
||||
- c.with_template do
|
||||
= render partial: "shared/piece_justificative_template", locals: { attachment: @champ.type_de_champ.piece_justificative_template }
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
- user_can_destroy = !@champ.mandatory? || @champ.dossier.brouillon?
|
||||
= render Attachment::EditComponent.new(form: @form, attached_file: @champ.piece_justificative_file, user_can_destroy: user_can_destroy)
|
||||
|
||||
- if @champ.type_de_champ.piece_justificative_template&.attached?
|
||||
= render partial: "shared/piece_justificative_template", locals: { attachment: @champ.type_de_champ.piece_justificative_template }
|
||||
= render Attachment::EditComponent.new(champ: @form.object, attached_file: @champ.piece_justificative_file, attachment: @champ.piece_justificative_file[0], form_object_name: @form.object_name, user_can_destroy:)
|
||||
|
|
|
@ -38,8 +38,7 @@ class TypesDeChampEditor::ChampComponent < ApplicationComponent
|
|||
def form_options
|
||||
{
|
||||
url: admin_procedure_type_de_champ_path(procedure, type_de_champ.stable_id),
|
||||
multipart: true,
|
||||
html: { id: nil, class: 'form width-100' }
|
||||
html: { multipart: true, id: nil, class: 'form width-100' }
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -74,12 +73,11 @@ class TypesDeChampEditor::ChampComponent < ApplicationComponent
|
|||
end
|
||||
end
|
||||
|
||||
def piece_justificative_options(form)
|
||||
def piece_justificative_template_options
|
||||
{
|
||||
form: form,
|
||||
attached_file: type_de_champ.piece_justificative_template,
|
||||
user_can_destroy: true,
|
||||
id: dom_id(type_de_champ, :piece_justificative_template)
|
||||
auto_attach_url: helpers.auto_attach_url(type_de_champ),
|
||||
user_can_download: true
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -57,11 +57,12 @@
|
|||
- if type_de_champ.piece_justificative?
|
||||
.cell
|
||||
= form.label :piece_justificative_template, "Modèle", for: dom_id(type_de_champ, :piece_justificative_template)
|
||||
= render Attachment::EditComponent.new(**piece_justificative_options(form))
|
||||
- if type_de_champ.titre_identite?
|
||||
.cell
|
||||
%p
|
||||
Dans le cadre de la RGPD, le titre d’identité sera supprimé lors de l’acceptation du dossier
|
||||
= render Attachment::EditComponent.new(**piece_justificative_template_options)
|
||||
|
||||
- if type_de_champ.titre_identite?
|
||||
%p Dans le cadre de la RGPD, le titre d’identité sera supprimé lors de l’acceptation du dossier
|
||||
- elsif procedure.piece_justificative_multiple?
|
||||
%p Les usagers pourront envoyer plusieurs fichiers si nécessaire.
|
||||
- if type_de_champ.carte?
|
||||
- type_de_champ.editable_options.each do |slice|
|
||||
.cell
|
||||
|
|
|
@ -396,6 +396,9 @@ module Administrateurs
|
|||
:procedure_expires_when_termine_enabled,
|
||||
:tags
|
||||
]
|
||||
|
||||
editable_params << :piece_justificative_multiple if @procedure && !@procedure.piece_justificative_multiple?
|
||||
|
||||
permited_params = if @procedure&.locked?
|
||||
params.require(:procedure).permit(*editable_params)
|
||||
else
|
||||
|
|
|
@ -375,4 +375,8 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cast_bool(value)
|
||||
ActiveRecord::Type::Boolean.new.deserialize(value)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,10 @@ class AttachmentsController < ApplicationController
|
|||
|
||||
def show
|
||||
@attachment = @blob.attachments.find(params[:id])
|
||||
@user_can_upload = params[:user_can_upload]
|
||||
|
||||
@user_can_edit = cast_bool(params[:user_can_edit])
|
||||
@user_can_download = cast_bool(params[:user_can_download])
|
||||
@auto_attach_url = params[:auto_attach_url]
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
|
@ -17,6 +20,8 @@ class AttachmentsController < ApplicationController
|
|||
@attachment.purge_later
|
||||
flash.notice = 'La pièce jointe a bien été supprimée.'
|
||||
|
||||
@champ_id = params[:champ_id]
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.html { redirect_back(fallback_location: root_url) }
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
class Champs::PieceJustificativeController < ApplicationController
|
||||
before_action :authenticate_logged_user!
|
||||
before_action :set_champ
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.html { redirect_back(fallback_location: root_url) }
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if attach_piece_justificative_or_retry
|
||||
|
@ -11,8 +19,11 @@ class Champs::PieceJustificativeController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def attach_piece_justificative
|
||||
def set_champ
|
||||
@champ = policy_scope(Champ).find(params[:champ_id])
|
||||
end
|
||||
|
||||
def attach_piece_justificative
|
||||
@champ.piece_justificative_file.attach(params[:blob_signed_id])
|
||||
save_succeed = @champ.save
|
||||
@champ.dossier.update(last_champ_updated_at: Time.zone.now.utc) if save_succeed
|
||||
|
|
|
@ -6,14 +6,18 @@ module Extensions
|
|||
attr_reader :attachment_assoc
|
||||
|
||||
def apply
|
||||
# Here we try to define the attachment name:
|
||||
# Here we try to define the ActiveRecord association name:
|
||||
# - it could be set explicitly via extension options
|
||||
# - or we imply that is the same as the field name w/o "_url"
|
||||
# suffix (e.g., "avatar_url" => "avatar")
|
||||
attachment = options&.[](:attachment) || field.original_name.to_s.sub(/_url$/, "")
|
||||
|
||||
# that's the name of the Active Record association
|
||||
@attachment_assoc = "#{attachment}_attachment"
|
||||
@attachment_assoc = if options.key?(:attachment)
|
||||
"#{options[:attachment]}_attachment"
|
||||
elsif options.key?(:attachments)
|
||||
"#{options[:attachments]}_attachments"
|
||||
else
|
||||
attachment = field.original_name.to_s.sub(/_url$/, "")
|
||||
"#{attachment}_attachment"
|
||||
end
|
||||
end
|
||||
|
||||
# This method resolves (as it states) the field itself
|
||||
|
@ -28,8 +32,26 @@ module Extensions
|
|||
# This method is called if the result of the `resolve`
|
||||
# is a lazy value (e.g., a Promise – like in our case)
|
||||
def after_resolve(value:, **_rest)
|
||||
if value&.virus_scanner&.safe? || value&.virus_scanner&.pending?
|
||||
value
|
||||
if value.respond_to?(:map)
|
||||
attachments = value.map { after_resolve_attachment(_1) }
|
||||
|
||||
if options[:flat_first]
|
||||
attachments.first
|
||||
else
|
||||
attachments
|
||||
end
|
||||
else
|
||||
after_resolve_attachment(value)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_resolve_attachment(attachment)
|
||||
return unless attachment
|
||||
|
||||
if attachment.virus_scanner.safe? || attachment.virus_scanner.pending?
|
||||
attachment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2028,7 +2028,8 @@ type PersonnePhysique implements Demandeur {
|
|||
}
|
||||
|
||||
type PieceJustificativeChamp implements Champ {
|
||||
file: File
|
||||
file: File @deprecated(reason: "Utilisez le champ `files` à la place.")
|
||||
files: [File!]
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
|
@ -2429,4 +2430,4 @@ type ValidationError {
|
|||
A description of the error
|
||||
"""
|
||||
message: String!
|
||||
}
|
||||
}
|
|
@ -2,8 +2,12 @@ module Types::Champs
|
|||
class PieceJustificativeChampType < Types::BaseObject
|
||||
implements Types::ChampType
|
||||
|
||||
field :file, Types::File, null: true, extensions: [
|
||||
{ Extensions::Attachment => { attachment: :piece_justificative_file } }
|
||||
field :file, Types::File, null: true, deprecation_reason: "Utilisez le champ `files` à la place.", extensions: [
|
||||
{ Extensions::Attachment => { attachments: :piece_justificative_file, flat_first: true } }
|
||||
]
|
||||
|
||||
field :files, [Types::File], null: true, extensions: [
|
||||
{ Extensions::Attachment => { attachments: :piece_justificative_file } }
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ module ChampHelper
|
|||
|
||||
def auto_attach_url(object)
|
||||
if object.is_a?(Champ)
|
||||
champs_piece_justificative_url(object.id)
|
||||
champs_attach_piece_justificative_url(object.id)
|
||||
elsif object.is_a?(TypeDeChamp)
|
||||
piece_justificative_template_admin_procedure_type_de_champ_url(stable_id: object.stable_id, procedure_id: object.procedure.id)
|
||||
end
|
||||
|
|
39
app/helpers/form_tag_helper.rb
Normal file
39
app/helpers/form_tag_helper.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
module FormTagHelper
|
||||
# from Rails 7 ActionView::Helpers::FormTagHelper
|
||||
# https://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-field_id
|
||||
# Should be removed when we upgrade to Rails 7
|
||||
def field_id(object_name, method_name, *suffixes, index: nil, namespace: nil)
|
||||
if object_name.respond_to?(:model_name)
|
||||
object_name = object_name.model_name.singular
|
||||
end
|
||||
|
||||
sanitized_object_name = object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").delete_suffix("_")
|
||||
|
||||
sanitized_method_name = method_name.to_s.delete_suffix("?")
|
||||
|
||||
[
|
||||
namespace,
|
||||
sanitized_object_name.presence,
|
||||
(index unless sanitized_object_name.empty?),
|
||||
sanitized_method_name,
|
||||
*suffixes
|
||||
].tap(&:compact!).join("_")
|
||||
end
|
||||
|
||||
# from Rails 7 ActionView::Helpers::FormTagHelper
|
||||
# https://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-field_name
|
||||
# Should be removed when we upgrade to Rails 7
|
||||
def field_name(object_name, method_name, *method_names, multiple: false, index: nil)
|
||||
names = method_names.map! { |name| "[#{name}]" }.join
|
||||
|
||||
# a little duplication to construct fewer strings
|
||||
case
|
||||
when object_name.blank?
|
||||
"#{method_name}#{names}#{multiple ? "[]" : ""}"
|
||||
when index
|
||||
"#{object_name}[#{index}][#{method_name}]#{names}#{multiple ? "[]" : ""}"
|
||||
else
|
||||
"#{object_name}[#{method_name}]#{names}#{multiple ? "[]" : ""}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,6 @@ import {
|
|||
|
||||
type ErrorMessage = {
|
||||
title: string;
|
||||
description: string;
|
||||
retry: boolean;
|
||||
};
|
||||
|
||||
|
@ -69,6 +68,8 @@ export class AutoUpload {
|
|||
|
||||
const message = this.messageFromError(error);
|
||||
this.displayErrorMessage(message);
|
||||
|
||||
this.#input.classList.toggle('fr-text-default--error', true);
|
||||
}
|
||||
|
||||
private done() {
|
||||
|
@ -81,20 +82,19 @@ export class AutoUpload {
|
|||
|
||||
if (error.failureReason == FAILURE_CONNECTIVITY) {
|
||||
return {
|
||||
title: 'Le fichier n’a pas pu être envoyé.',
|
||||
description: 'Vérifiez votre connexion à Internet, puis ré-essayez.',
|
||||
title:
|
||||
'Le fichier n’a pas pu être envoyé. Vérifiez votre connexion à Internet, puis ré-essayez.',
|
||||
retry: true
|
||||
};
|
||||
} else if (error.code == ERROR_CODE_READ) {
|
||||
return {
|
||||
title: 'Nous n’arrivons pas à lire ce fichier sur votre appareil.',
|
||||
description: 'Essayez à nouveau, ou sélectionnez un autre fichier.',
|
||||
title:
|
||||
'Nous n’arrivons pas à lire ce fichier sur votre appareil. Essayez à nouveau, ou sélectionnez un autre fichier.',
|
||||
retry: false
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
title: 'Le fichier n’a pas pu être envoyé.',
|
||||
description: message,
|
||||
title: message,
|
||||
retry: !!canRetry
|
||||
};
|
||||
}
|
||||
|
@ -105,7 +105,6 @@ export class AutoUpload {
|
|||
if (errorElement) {
|
||||
show(errorElement);
|
||||
this.errorTitleElement.textContent = message.title || '';
|
||||
this.errorDescriptionElement.textContent = message.description || '';
|
||||
toggle(this.errorRetryButton, message.retry);
|
||||
}
|
||||
}
|
||||
|
@ -118,30 +117,21 @@ export class AutoUpload {
|
|||
}
|
||||
|
||||
get errorElement() {
|
||||
return this.#input.parentElement?.querySelector<HTMLElement>(
|
||||
'.attachment-error'
|
||||
);
|
||||
return this.#input
|
||||
.closest('.attachment')
|
||||
?.querySelector<HTMLElement>('.attachment-upload-error');
|
||||
}
|
||||
|
||||
get errorTitleElement() {
|
||||
const element = this.errorElement?.querySelector<HTMLElement>(
|
||||
'.attachment-error-title'
|
||||
);
|
||||
const element =
|
||||
this.errorElement?.querySelector<HTMLElement>('.fr-error-text');
|
||||
invariant(element, 'Could not find the error title element.');
|
||||
return element;
|
||||
}
|
||||
|
||||
get errorDescriptionElement() {
|
||||
const element = this.errorElement?.querySelector<HTMLElement>(
|
||||
'.attachment-error-description'
|
||||
);
|
||||
invariant(element, 'Could not find the error description element.');
|
||||
return element;
|
||||
}
|
||||
|
||||
get errorRetryButton() {
|
||||
const element = this.errorElement?.querySelector<HTMLButtonElement>(
|
||||
'.attachment-error-retry'
|
||||
'.attachment-upload-error-retry'
|
||||
);
|
||||
invariant(element, 'Could not find the error retry button element.');
|
||||
return element;
|
||||
|
|
|
@ -19,6 +19,7 @@ class VirusScannerJob < ApplicationJob
|
|||
if blob.virus_scanner.done? then return end
|
||||
|
||||
metadata = extract_metadata_via_virus_scanner(blob)
|
||||
|
||||
VirusScannerJob.merge_and_update_metadata(blob, metadata)
|
||||
end
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ class Champ < ApplicationRecord
|
|||
belongs_to :dossier, inverse_of: false, touch: true, optional: false
|
||||
belongs_to :type_de_champ, inverse_of: :champ, optional: false
|
||||
belongs_to :parent, class_name: 'Champ', optional: true
|
||||
has_one_attached :piece_justificative_file
|
||||
has_many_attached :piece_justificative_file
|
||||
|
||||
# We declare champ specific relationships (Champs::CarteChamp, Champs::SiretChamp and Champs::RepetitionChamp)
|
||||
# here because otherwise we can't easily use includes in our queries.
|
||||
|
|
|
@ -43,12 +43,16 @@ class Champs::PieceJustificativeChamp < Champ
|
|||
end
|
||||
|
||||
def for_export
|
||||
piece_justificative_file.filename.to_s if piece_justificative_file.attached?
|
||||
piece_justificative_file.map { _1.filename.to_s }
|
||||
end
|
||||
|
||||
def for_api
|
||||
if piece_justificative_file.attached? && (piece_justificative_file.virus_scanner.safe? || piece_justificative_file.virus_scanner.pending?)
|
||||
piece_justificative_file.service_url
|
||||
return nil unless piece_justificative_file.attached?
|
||||
|
||||
piece_justificative_file.filter_map do |attachment|
|
||||
if attachment.virus_scanner.safe? || attachment.virus_scanner.pending?
|
||||
attachment.service_url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,13 @@ module BlobVirusScannerConcern
|
|||
VirusScannerJob.perform_later(self)
|
||||
end
|
||||
|
||||
def virus_scanner_error?
|
||||
return true if virus_scanner.infected?
|
||||
return true if virus_scanner.corrupt?
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pending
|
||||
|
|
|
@ -261,16 +261,16 @@ class Dossier < ApplicationRecord
|
|||
includes(champs_public: [
|
||||
:type_de_champ,
|
||||
:geo_areas,
|
||||
piece_justificative_file_attachment: :blob,
|
||||
champs: [:type_de_champ, piece_justificative_file_attachment: :blob]
|
||||
piece_justificative_file_attachments: :blob,
|
||||
champs: [:type_de_champ, piece_justificative_file_attachments: :blob]
|
||||
])
|
||||
}
|
||||
scope :with_annotations, -> {
|
||||
includes(champs_private: [
|
||||
:type_de_champ,
|
||||
:geo_areas,
|
||||
piece_justificative_file_attachment: :blob,
|
||||
champs: [:type_de_champ, piece_justificative_file_attachment: :blob]
|
||||
piece_justificative_file_attachments: :blob,
|
||||
champs: [:type_de_champ, piece_justificative_file_attachments: :blob]
|
||||
])
|
||||
}
|
||||
scope :for_api, -> {
|
||||
|
@ -279,7 +279,7 @@ class Dossier < ApplicationRecord
|
|||
.includes(commentaires: { piece_jointe_attachment: :blob },
|
||||
justificatif_motivation_attachment: :blob,
|
||||
attestation: [],
|
||||
avis: { piece_justificative_file_attachment: :blob },
|
||||
avis: { piece_justificative_file_attachments: :blob },
|
||||
traitement: [],
|
||||
etablissement: [],
|
||||
individual: [],
|
||||
|
|
|
@ -35,7 +35,7 @@ class DossierPreloader
|
|||
end
|
||||
|
||||
def load_dossiers(dossiers, pj_template: false)
|
||||
to_include = [piece_justificative_file_attachment: :blob]
|
||||
to_include = [piece_justificative_file_attachments: :blob]
|
||||
|
||||
if pj_template
|
||||
to_include << { type_de_champ: { piece_justificative_template_attachment: :blob } }
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
# opendata :boolean default(TRUE)
|
||||
# organisation :string
|
||||
# path :string not null
|
||||
# piece_justificative_multiple :boolean default(TRUE), not null
|
||||
# procedure_expires_when_termine_enabled :boolean default(TRUE)
|
||||
# published_at :datetime
|
||||
# routing_criteria_name :text default("Votre ville")
|
||||
|
@ -248,9 +249,9 @@ class Procedure < ApplicationRecord
|
|||
:groupe_instructeurs,
|
||||
dossiers: {
|
||||
champs_public: [
|
||||
piece_justificative_file_attachment: :blob,
|
||||
piece_justificative_file_attachments: :blob,
|
||||
champs: [
|
||||
piece_justificative_file_attachment: :blob
|
||||
piece_justificative_file_attachments: :blob
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -776,7 +777,7 @@ class Procedure < ApplicationRecord
|
|||
if dossiers.termine.any?
|
||||
dossiers_sample = dossiers.termine.limit(100)
|
||||
total_size = Champ
|
||||
.includes(piece_justificative_file_attachment: :blob)
|
||||
.includes(piece_justificative_file_attachments: :blob)
|
||||
.where(type: Champs::PieceJustificativeChamp.to_s, dossier: dossiers_sample)
|
||||
.sum('active_storage_blobs.byte_size')
|
||||
|
||||
|
|
|
@ -46,19 +46,23 @@ class PiecesJustificativesService
|
|||
|
||||
def self.serialize_champs_as_pjs(dossier)
|
||||
dossier.champs_public.filter { |champ| champ.type_de_champ.old_pj }.map do |champ|
|
||||
{
|
||||
created_at: champ.created_at&.in_time_zone('UTC'),
|
||||
type_de_piece_justificative_id: champ.type_de_champ.old_pj[:stable_id],
|
||||
content_url: champ.for_api,
|
||||
user: champ.dossier.user
|
||||
}
|
||||
end
|
||||
champ.for_api&.map do |content_url|
|
||||
{
|
||||
created_at: champ.created_at&.in_time_zone('UTC'),
|
||||
type_de_piece_justificative_id: champ.type_de_champ.old_pj[:stable_id],
|
||||
content_url:,
|
||||
user: champ.dossier.user
|
||||
}
|
||||
end
|
||||
end.flatten
|
||||
end
|
||||
|
||||
def self.clone_attachments(original, kopy)
|
||||
case original
|
||||
when Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp
|
||||
clone_attachment(original.piece_justificative_file, kopy.piece_justificative_file)
|
||||
original.piece_justificative_file.attachments.each do |attachment|
|
||||
kopy.piece_justificative_file.attach(attachment.blob)
|
||||
end
|
||||
when TypeDeChamp
|
||||
clone_attachment(original.piece_justificative_template, kopy.piece_justificative_template)
|
||||
when Procedure
|
||||
|
@ -113,7 +117,7 @@ class PiecesJustificativesService
|
|||
|
||||
def self.pjs_for_champs(dossiers, for_expert = false)
|
||||
champs = Champ
|
||||
.joins(:piece_justificative_file_attachment)
|
||||
.joins(:piece_justificative_file_attachments)
|
||||
.where(type: "Champs::PieceJustificativeChamp", dossier: dossiers)
|
||||
|
||||
if for_expert
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
= tag[:description]
|
||||
|
||||
%h3.header-subsection Logo de l'attestation
|
||||
= render Attachment::EditComponent.new(form: f, attached_file: @attestation_template.logo, direct_upload: false, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(attached_file: @attestation_template.logo, direct_upload: false)
|
||||
|
||||
%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.new(form: f, attached_file: @attestation_template.signature, direct_upload: false, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(attached_file: @attestation_template.signature, direct_upload: false)
|
||||
|
||||
%p.notice
|
||||
Formats acceptés : JPG / JPEG / PNG.
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
.procedure-form__columns.container
|
||||
= form_for @attestation_template,
|
||||
url: admin_procedure_attestation_template_path(@procedure),
|
||||
multipart: true,
|
||||
html: { class: 'form procedure-form__column--form' } do |f|
|
||||
html: { multipart: true, class: 'form procedure-form__column--form' } do |f|
|
||||
|
||||
%h1.page-title
|
||||
Délivrance d’attestation
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
- csv_max_size = Administrateurs::GroupeInstructeursController::CSV_MAX_SIZE
|
||||
- if procedure.publiee?
|
||||
= form_tag import_admin_procedure_groupe_instructeurs_path(procedure), method: :post, multipart: true, class: "mt-4 form" do
|
||||
= form_tag import_admin_procedure_groupe_instructeurs_path(procedure), method: :post, html: { multipart: true, class: "mt-4 form" } do
|
||||
= label_tag t('.csv_import.title')
|
||||
%p.notice
|
||||
= t('.csv_import.notice_1')
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
= f.text_area :description, rows: '6', placeholder: 'Description de la démarche, destinataires, etc. ', class: 'form-control'
|
||||
|
||||
%h3.header-subsection Logo de la démarche
|
||||
= render Attachment::EditComponent.new(form: f, attached_file: @procedure.logo, direct_upload: true, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(attached_file: @procedure.logo, user_can_download: true)
|
||||
|
||||
%h3.header-subsection Conservation des données
|
||||
= f.label :duree_conservation_dossiers_dans_ds do
|
||||
|
@ -52,7 +52,7 @@
|
|||
= f.text_field :cadre_juridique, class: 'form-control', placeholder: 'https://www.legifrance.gouv.fr/'
|
||||
|
||||
= f.label :deliberation, 'Importer le texte'
|
||||
= render Attachment::EditComponent.new(form: f, attached_file: @procedure.deliberation, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(attached_file: @procedure.deliberation, user_can_download: true)
|
||||
|
||||
%h3.header-subsection
|
||||
RGPD
|
||||
|
@ -81,7 +81,7 @@
|
|||
= f.label :notice, 'Notice'
|
||||
%p.notice
|
||||
Formats acceptés : .doc, .odt, .pdf, .ppt, .pptx
|
||||
= render Attachment::EditComponent.new(form: f, attached_file: @procedure.notice, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(attached_file: @procedure.notice, user_can_download: true)
|
||||
|
||||
- if !@procedure.locked?
|
||||
%h3.header-subsection À qui s’adresse ma démarche ?
|
||||
|
@ -142,3 +142,12 @@
|
|||
Dans une démarche déclarative, une fois déposé, un dossier ne peut plus être modifié.
|
||||
Soit il passe immédiatement « en instruction » pour être traité soit il est immédiatement « accepté ».
|
||||
= f.select :declarative_with_state, Procedure.declarative_attributes_for_select, { include_blank: 'Non' }, class: 'form-control'
|
||||
|
||||
- if !@procedure.piece_justificative_multiple?
|
||||
.fr-checkbox-group
|
||||
= f.check_box :piece_justificative_multiple
|
||||
= f.label :piece_justificative_multiple, class: 'fr-label' do
|
||||
Champ “Pièce justificative” avec multiples fichiers
|
||||
%p.notice
|
||||
Autorise les usagers à envoyer plusieurs fichiers pour les champs de type “Pièce justificative”. L'activation de cette option est irréversible et peut nécessiter des modifications si vous utilisez des systèmes automatisés pour traiter les dossiers.
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
.procedure-form__columns.container
|
||||
= form_for @procedure,
|
||||
url: url_for({ controller: 'administrateurs/procedures', action: :update, id: @procedure.id }),
|
||||
multipart: true,
|
||||
html: { class: 'form procedure-form__column--form' } do |f|
|
||||
html: { class: 'form procedure-form__column--form',
|
||||
multipart: true } do |f|
|
||||
|
||||
%h1.page-title Description
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
.container
|
||||
%h1
|
||||
= form_for @procedure, url: url_for({ controller: 'administrateurs/procedures', action: :update_monavis }), multipart: true, html: { class: 'form' } do |f|
|
||||
= form_for @procedure, url: url_for({ controller: 'administrateurs/procedures', action: :update_monavis }), html: { class: 'form', multipart: true } do |f|
|
||||
= render partial: 'monavis', locals: { f: f }
|
||||
.text-right
|
||||
= f.button 'Enregistrer', class: 'button primary send'
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
.procedure-form__columns.container
|
||||
= form_for @procedure,
|
||||
url: url_for({ controller: 'administrateurs/procedures', action: :create, id: @procedure.id }),
|
||||
multipart: true,
|
||||
html: { class: 'form procedure-form__column--form' } do |f|
|
||||
html: { class: 'form procedure-form__column--form', multipart: true } do |f|
|
||||
|
||||
%h1.page-title Nouvelle démarche
|
||||
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
.container
|
||||
= form_for @procedure,
|
||||
url: url_for({ controller: 'administrateurs/procedures', action: :update, id: @procedure.id }),
|
||||
multipart: true,
|
||||
html: { class: 'form' } do |f|
|
||||
html: { multipart: true, class: 'form' } do |f|
|
||||
|
||||
%h1.page-title Zones
|
||||
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
= turbo_stream.remove dom_id(@attachment, :actions)
|
||||
= turbo_stream.remove dom_id(@attachment, :persisted_row)
|
||||
|
||||
- if @champ_id
|
||||
= turbo_stream.show "attachment-multiple-empty-#{@champ_id}"
|
||||
|
||||
= turbo_stream.show_all ".attachment-input-#{@attachment.id}"
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
= turbo_stream.replace dom_id(@attachment, :show) do
|
||||
= render Attachment::ShowComponent.new(attachment: @attachment, user_can_upload: @user_can_upload)
|
||||
= turbo_stream.replace dom_id(@attachment, :edit) do
|
||||
- if @user_can_edit
|
||||
= render Attachment::EditComponent.new(attachment: @attachment, attached_file: @attachment.record.public_send(@attachment.name), auto_attach_url: @auto_attach_url, user_can_download: @user_can_download)
|
||||
- else
|
||||
= render Attachment::ShowComponent.new(attachment: @attachment)
|
||||
|
|
|
@ -2,6 +2,5 @@
|
|||
= turbo_stream.morph @champ.input_group_id do
|
||||
= render EditableChamp::EditableChampComponent.new champ: @champ, form: form
|
||||
|
||||
- if @champ.piece_justificative_file.attached?
|
||||
- attachment = @champ.piece_justificative_file.attachment
|
||||
- @champ.piece_justificative_file.attachments.each do |attachment|
|
||||
= turbo_stream.focus_all "button[data-toggle-target=\".attachment-input-#{attachment.id}\"]"
|
||||
|
|
|
@ -15,9 +15,9 @@
|
|||
= render Attachment::ShowComponent.new(attachment: @avis.introduction_file.attachment)
|
||||
%br/
|
||||
|
||||
= 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) }, multipart: true } do |f|
|
||||
= f.text_area :answer, rows: 3, placeholder: 'Votre avis', required: true
|
||||
= render Attachment::EditComponent.new(form: f, attached_file: @avis.piece_justificative_file, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(attached_file: @avis.piece_justificative_file, user_can_download: 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.new(form: f, attached_file: avis.introduction_file, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(attached_file: avis.introduction_file)
|
||||
|
||||
- if linked_dossiers.present?
|
||||
= f.check_box :invite_linked_dossiers, {}, true, false
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
%h1.tab-title Inviter des personnes à donner leur avis
|
||||
%p.avis-notice Les invités pourront consulter le dossier, donner un avis et contribuer au fil de messagerie. Ils ne pourront pas modifier le dossier.
|
||||
|
||||
= form_for avis, url: url, html: { class: 'form', data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@avis.dossier, :avis_by_expert) } } do |f|
|
||||
= form_for avis, url: url, html: { class: 'form', multipart: true, data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@avis.dossier, :avis_by_expert) } } do |f|
|
||||
= hidden_field_tag 'avis[emails]', nil
|
||||
= react_component("ComboMultiple",
|
||||
options: [], selected: [], disabled: [],
|
||||
|
@ -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.new(form: f, attached_file: avis.introduction_file, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(attached_file: avis.introduction_file)
|
||||
|
||||
- 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.new(form: f, attached_file: @avis.piece_justificative_file, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(attached_file: @avis.piece_justificative_file)
|
||||
|
||||
.flex.justify-between.align-baseline
|
||||
%p.confidentiel.flex
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
%span.icon{ class: popup_class }
|
||||
#{popup_title}
|
||||
|
||||
= form_tag(terminer_instructeur_dossier_path(dossier.procedure, dossier), data: { turbo: true, turbo_confirm: confirm }, method: :post, multipart: true, class: 'form') do
|
||||
= form_tag(terminer_instructeur_dossier_path(dossier.procedure, dossier), data: { turbo: true, turbo_confirm: confirm }, method: :post, html: { multipart: true, class: 'form' }) do
|
||||
- if title == 'Accepter'
|
||||
= text_area :dossier, :motivation, class: 'motivation-text-area', placeholder: placeholder, required: false
|
||||
- if dossier.attestation_template&.activated?
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
%p#avis-emails-description.avis-notice
|
||||
Entrez les adresses email des experts à qui vous souhaitez demander un avis
|
||||
|
||||
= form_for avis, url: url, html: { class: 'form', data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@dossier, :avis_by_instructeur) } } do |f|
|
||||
= form_for avis, url: url, html: { class: 'form', multipart: true, data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@dossier, :avis_by_instructeur) } } do |f|
|
||||
= hidden_field_tag 'avis[emails]', nil
|
||||
= react_component("ComboMultiple",
|
||||
options: @dossier.procedure.experts_require_administrateur_invitation ? @experts_emails : [],
|
||||
|
@ -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.new(form: f, attached_file: avis.introduction_file, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(attached_file: avis.introduction_file)
|
||||
|
||||
- if linked_dossiers.present?
|
||||
= f.check_box :invite_linked_dossiers, {}, true, false
|
||||
|
|
|
@ -348,3 +348,53 @@
|
|||
Des marges verticales ont ici été rajoutées.
|
||||
|
||||
|
||||
|
||||
.container
|
||||
%h1.fr-mt-4w Attachment::EditComponent
|
||||
%span.fr-hint-text Note: direct upload, suppression ne marchent pas comme attendu ici.
|
||||
|
||||
- champ = @dossier.champs_public.first
|
||||
- tdc = @dossier.champs_public.find { _1.type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative) }.type_de_champ
|
||||
- avis = Avis.new
|
||||
|
||||
- if attachment = ActiveStorage::Attachment.last
|
||||
- attachment.update(created_at: 1.second.ago)
|
||||
%h3 New attachment
|
||||
= render Attachment::EditComponent.new(champ:, attached_file: champ.piece_justificative_file, attachment: nil)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment
|
||||
= render Attachment::EditComponent.new(champ:, attached_file: champ.piece_justificative_file, attachment:)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment, user can not destroy
|
||||
= render Attachment::EditComponent.new(champ:, attached_file: champ.piece_justificative_file, attachment:, user_can_destroy: false)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment, antivirus in progress
|
||||
- attachment.blob.metadata[:virus_scan_result] = ActiveStorage::VirusScanner::PENDING
|
||||
- attachment.created_at = Time.zone.now
|
||||
= render Attachment::EditComponent.new(champ:, attached_file: Champ.new.piece_justificative_file, attachment:)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment, antivirus in progress since long time
|
||||
- attachment.blob.metadata[:virus_scan_result] = ActiveStorage::VirusScanner::PENDING
|
||||
- attachment.created_at = 2.minutes.ago
|
||||
= render Attachment::EditComponent.new(champ:, attached_file: Champ.new.piece_justificative_file, attachment:)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment, error
|
||||
- attachment.blob.metadata[:virus_scan_result] = ActiveStorage::VirusScanner::INFECTED
|
||||
= render Attachment::EditComponent.new(champ:, attached_file: Champ.new.piece_justificative_file, attachment:)
|
||||
|
||||
|
||||
%h3.fr-mt-4w New attachment on TypeDeChamp
|
||||
= render Attachment::EditComponent.new(auto_attach_url: "/some-auto-attach-path", attached_file: tdc.piece_justificative_template, attachment: nil)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment on TypeDeChamp
|
||||
= render Attachment::EditComponent.new(auto_attach_url: "/some-auto-attach-path", attached_file: tdc.piece_justificative_template, attachment: attachment.reload)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment on TypeDeChamp can download
|
||||
= render Attachment::EditComponent.new(auto_attach_url: "/some-auto-attach-path", attached_file: tdc.piece_justificative_template, attachment: attachment.reload, user_can_download: true)
|
||||
|
||||
%h3.fr-mt-4w New attachment on generic object
|
||||
= render Attachment::EditComponent.new(attached_file: avis.introduction_file)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment on generic object, can download
|
||||
= render Attachment::EditComponent.new(attached_file: avis.introduction_file, attachment: attachment.reload, user_can_download: true)
|
||||
|
||||
|
|
7
app/views/shared/_piece_justificative_template.html.haml
Normal file
7
app/views/shared/_piece_justificative_template.html.haml
Normal file
|
@ -0,0 +1,7 @@
|
|||
= render Dsfr::DownloadComponent.new(attachment: attachment, name: "Modèle à télécharger") do |c|
|
||||
- if administrateur_signed_in?
|
||||
- c.with_right do
|
||||
%span.fr-ml-2w.fr-text--xs.fr-text-mention--grey.visible-on-previous-hover
|
||||
%span.fr-text-action-high--blue-france.fr-icon-questionnaire-line{ "aria-hidden": "true" }
|
||||
= t('shared.ephemeral_link')
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
- pj = champ.piece_justificative_file
|
||||
- if pj.attached?
|
||||
= render Attachment::ShowComponent.new(attachment: pj.attachment)
|
||||
.fr-downloads-group
|
||||
%ul
|
||||
- pj.attachments.each do |attachment|
|
||||
%li= render Attachment::ShowComponent.new(attachment:)
|
||||
- else
|
||||
Pièce justificative non fournie
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
- else
|
||||
%td.libelle{ class: repetition ? 'padded' : '' }
|
||||
= "#{c.libelle} :"
|
||||
%td.rich-text
|
||||
%td.rich-text{ class: c.type_champ }
|
||||
%div{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) }
|
||||
- case c.type_champ
|
||||
- when TypeDeChamp.type_champs.fetch(:carte)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= form_for(commentaire, url: form_url, html: { class: 'form', data: { controller: 'persisted-form', persisted_form_key_value: @dossier.present? ? dom_id(@dossier) : dom_id(@procedure, :bulk_message) } }) do |f|
|
||||
= form_for(commentaire, url: form_url, html: { class: 'form', multipart: true, data: { controller: 'persisted-form', persisted_form_key_value: @dossier.present? ? dom_id(@dossier) : dom_id(@procedure, :bulk_message) } }) do |f|
|
||||
- dossier = commentaire.dossier
|
||||
- placeholder = t('views.shared.dossiers.messages.form.write_message_to_administration_placeholder')
|
||||
- if instructeur_signed_in? || administrateur_signed_in? || expert_signed_in?
|
||||
|
@ -8,7 +8,7 @@
|
|||
- disable_piece_jointe = defined?(disable_piece_jointe) ? disable_piece_jointe : false
|
||||
%div
|
||||
- if !disable_piece_jointe
|
||||
= render Attachment::EditComponent.new(form: f, attached_file: commentaire.piece_jointe)
|
||||
= render Attachment::EditComponent.new(attached_file: commentaire.piece_jointe)
|
||||
|
||||
%div
|
||||
= f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'button primary send', data: { disable: true }
|
||||
|
|
|
@ -166,7 +166,8 @@ Rails.application.routes.draw do
|
|||
patch ':champ_id/carte/features/:id', to: 'carte#update'
|
||||
delete ':champ_id/carte/features/:id', to: 'carte#destroy'
|
||||
|
||||
put ':champ_id/piece_justificative', to: 'piece_justificative#update', as: :piece_justificative
|
||||
get ':champ_id/piece_justificative', to: 'piece_justificative#show', as: :piece_justificative
|
||||
put ':champ_id/piece_justificative', to: 'piece_justificative#update', as: :attach_piece_justificative
|
||||
end
|
||||
|
||||
resources :attachments, only: [:show, :destroy]
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class AddPieceJustificativeMultipleOnProcedures < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
safety_assured do
|
||||
# Only new procedures will have multiple enabled by default
|
||||
add_column :procedures, :piece_justificative_multiple, :boolean, default: false, null: false
|
||||
change_column_default :procedures, :piece_justificative_multiple, from: false, to: true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -664,6 +664,7 @@ ActiveRecord::Schema.define(version: 2022_11_30_113745) do
|
|||
t.string "organisation"
|
||||
t.bigint "parent_procedure_id"
|
||||
t.string "path", null: false
|
||||
t.boolean "piece_justificative_multiple", default: true, null: false
|
||||
t.boolean "procedure_expires_when_termine_enabled", default: true
|
||||
t.datetime "published_at"
|
||||
t.bigint "published_revision_id"
|
||||
|
|
176
spec/components/attachment/edit_component_spec.rb
Normal file
176
spec/components/attachment/edit_component_spec.rb
Normal file
|
@ -0,0 +1,176 @@
|
|||
RSpec.describe Attachment::EditComponent, type: :component do
|
||||
let(:champ) { create(:champ_titre_identite, dossier: create(:dossier)) }
|
||||
let(:attached_file) { champ.piece_justificative_file }
|
||||
let(:attachment) { attached_file.attachments.first }
|
||||
let(:filename) { attachment.filename.to_s }
|
||||
let(:kwargs) { {} }
|
||||
|
||||
let(:component) do
|
||||
described_class.new(
|
||||
champ:,
|
||||
attached_file:,
|
||||
attachment:,
|
||||
**kwargs
|
||||
)
|
||||
end
|
||||
|
||||
subject { render_inline(component).to_html }
|
||||
|
||||
context 'when there is no attachment yet' do
|
||||
let(:attachment) { nil }
|
||||
|
||||
it 'renders a form field for uploading a file' do
|
||||
expect(subject).to have_selector('input[type=file]:not(.hidden)')
|
||||
end
|
||||
|
||||
it 'renders max size' do
|
||||
expect(subject).to have_content(/Taille maximale :\s+20 Mo/)
|
||||
end
|
||||
|
||||
it 'renders allowed formats' do
|
||||
expect(subject).to have_content(/Formats supportés : jpeg, png/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an attachment' do
|
||||
it 'renders the filename' do
|
||||
expect(subject).to have_content(attachment.filename.to_s)
|
||||
end
|
||||
|
||||
it 'hides the file field by default' do
|
||||
expect(subject).to have_selector('input[type=file].hidden')
|
||||
end
|
||||
|
||||
it 'shows the Delete button by default' do
|
||||
expect(subject).to have_selector('[title^="Supprimer le fichier"]')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user cannot destroy the attachment' do
|
||||
let(:kwargs) { { user_can_destroy: false } }
|
||||
|
||||
it 'hides the Delete button' do
|
||||
expect(subject).not_to have_selector("[title^='Supprimer le fichier']")
|
||||
end
|
||||
end
|
||||
|
||||
context 'within multiple attachments' do
|
||||
let(:index) { 0 }
|
||||
let(:component) do
|
||||
described_class.new(
|
||||
champ:,
|
||||
attached_file:,
|
||||
attachment: nil,
|
||||
as_multiple: true,
|
||||
index:
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not render an empty file' do # (is is rendered by MultipleComponent)
|
||||
expect(subject).not_to have_selector('input[type=file]')
|
||||
end
|
||||
|
||||
it 'renders max size for first index' do
|
||||
expect(subject).to have_content(/Taille maximale :\s+20 Mo/)
|
||||
end
|
||||
|
||||
context 'when index is not 0' do
|
||||
let(:index) { 1 }
|
||||
|
||||
it 'renders max size for first index' do
|
||||
expect(subject).not_to have_content('Taille maximale')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user can download' do
|
||||
let(:kwargs) { { user_can_download: true } }
|
||||
|
||||
it 'renders a link to download the file' do
|
||||
expect(subject).to have_link(filename)
|
||||
end
|
||||
|
||||
context 'when watermark is pending' do
|
||||
let(:champ) { create(:champ_titre_identite) }
|
||||
let(:kwargs) { { user_can_download: true } }
|
||||
|
||||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(attachment.watermark_pending?).to be_truthy
|
||||
expect(subject).to have_text(filename)
|
||||
expect(subject).to have_link('Supprimer')
|
||||
expect(subject).to have_no_link(text: filename) # don't match "Delete" link which also include filename in title attribute
|
||||
expect(subject).to have_text('Traitement en cours')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non nominal or final antivirus status' do
|
||||
before do
|
||||
champ.piece_justificative_file[0].blob.update(metadata: attachment.blob.metadata.merge(virus_scan_result: virus_scan_result))
|
||||
end
|
||||
|
||||
context 'when the anti-virus scan is pending' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::PENDING }
|
||||
|
||||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(subject).to have_text(filename)
|
||||
expect(subject).to have_no_link(text: filename)
|
||||
expect(subject).to have_text('Analyse antivirus en cours')
|
||||
end
|
||||
|
||||
it 'setup polling' do
|
||||
expect(subject).to have_selector('[data-controller=turbo-poll]')
|
||||
end
|
||||
|
||||
context "when used as multiple context" do
|
||||
let(:kwargs) { { as_multiple: true } }
|
||||
|
||||
it 'does not setup polling' do
|
||||
expect(subject).to have_no_selector('[data-controller=turbo-poll]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the file is scanned and safe' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::SAFE }
|
||||
|
||||
it 'allows to download the file' do
|
||||
expect(subject).to have_link(filename)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the file is scanned and infected' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::INFECTED }
|
||||
|
||||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(subject).to have_text(champ.piece_justificative_file[0].filename.to_s)
|
||||
expect(subject).to have_no_link(text: filename)
|
||||
expect(subject).to have_text('Virus détecté')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the file is corrupted' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::INTEGRITY_ERROR }
|
||||
|
||||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(subject).to have_text(filename)
|
||||
expect(subject).to have_no_link(text: filename)
|
||||
expect(subject).to have_text('corrompu')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'field name inference' do
|
||||
it "by default generate input name directly form attached file object" do
|
||||
expect(subject).to have_selector("input[name='champs_titre_identite_champ[piece_justificative_file]']")
|
||||
end
|
||||
|
||||
context "when a form object_name is provided" do
|
||||
let(:kwargs) { { form_object_name: "my_form" } }
|
||||
|
||||
it "generate input name from form object name and attached file object" do
|
||||
expect(subject).to have_selector("input[name='my_form[piece_justificative_file]']")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
101
spec/components/attachment/multiple_component_spec.rb
Normal file
101
spec/components/attachment/multiple_component_spec.rb
Normal file
|
@ -0,0 +1,101 @@
|
|||
RSpec.describe Attachment::MultipleComponent, type: :component do
|
||||
let(:champ) { create(:champ_titre_identite) }
|
||||
let(:attached_file) { champ.piece_justificative_file }
|
||||
let(:kwargs) { {} }
|
||||
|
||||
let(:component) do
|
||||
described_class.new(
|
||||
champ:,
|
||||
attached_file:,
|
||||
**kwargs
|
||||
)
|
||||
end
|
||||
|
||||
subject { render_inline(component).to_html }
|
||||
|
||||
context 'when there is no attachment yet' do
|
||||
let(:champ) { create(:champ_titre_identite, skip_default_attachment: true) }
|
||||
|
||||
it 'renders a form field for uploading a file' do
|
||||
expect(subject).to have_no_selector('.hidden input[type=file]')
|
||||
expect(subject).to have_selector('input[type=file]:not(.hidden)')
|
||||
end
|
||||
|
||||
it 'renders max size' do
|
||||
expect(subject).to have_content(/Taille maximale :\s+20 Mo/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a template' do
|
||||
before do
|
||||
component.with_template { "the template to render" }
|
||||
end
|
||||
|
||||
it 'renders the template' do
|
||||
expect(subject).to have_text("the template to render")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an attachment' do
|
||||
before do
|
||||
attached_file.attach(
|
||||
io: StringIO.new("x" * 2),
|
||||
filename: "me.jpg",
|
||||
content_type: "image/jpeg",
|
||||
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
|
||||
)
|
||||
champ.save!
|
||||
end
|
||||
|
||||
it 'renders the filenames' do
|
||||
expect(subject).to have_content(attached_file.attachments[0].filename.to_s)
|
||||
expect(subject).to have_content(attached_file.attachments[1].filename.to_s)
|
||||
end
|
||||
|
||||
it 'shows the Delete button by default' do
|
||||
expect(subject).to have_link(title: "Supprimer le fichier #{attached_file.attachments[0].filename}")
|
||||
expect(subject).to have_link(title: "Supprimer le fichier #{attached_file.attachments[1].filename}")
|
||||
end
|
||||
|
||||
it 'renders a form field for uploading a new file' do
|
||||
expect(subject).to have_selector('input[type=file]:not(.hidden)')
|
||||
end
|
||||
|
||||
it 'does not renders max size anymore' do
|
||||
expect(subject).to have_no_content(/Taille maximale/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user cannot destroy the attachment' do
|
||||
let(:kwargs) { { user_can_destroy: false } }
|
||||
|
||||
it 'hides the Delete button' do
|
||||
expect(subject).to have_no_link(title: "Supprimer le fichier #{attached_file.attachments[0].filename}")
|
||||
end
|
||||
|
||||
it 'still renders the filename' do
|
||||
expect(subject).to have_content(attached_file.attachments[0].filename.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context 'max attachments' do
|
||||
let(:kwargs) { { max: 1 } }
|
||||
|
||||
it 'does not render visible input file where max attachments has been reached' do
|
||||
expect(subject).to have_selector('.hidden input[type=file]')
|
||||
end
|
||||
end
|
||||
|
||||
context 'attachment process in progress' do
|
||||
let(:created_at) { 1.second.ago }
|
||||
|
||||
before do
|
||||
attached_file.attachments[0].blob.update(metadata: { virus_scan_result: ActiveStorage::VirusScanner::PENDING })
|
||||
attached_file.attachments[0].update!(created_at:)
|
||||
end
|
||||
|
||||
it 'setup polling' do
|
||||
expect(subject).to have_selector('[data-controller=turbo-poll]')
|
||||
end
|
||||
end
|
||||
end
|
59
spec/components/attachment/pending_poll_component_spec.rb
Normal file
59
spec/components/attachment/pending_poll_component_spec.rb
Normal file
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Attachment::PendingPollComponent, type: :component do
|
||||
let(:champ) { create(:champ_titre_identite) }
|
||||
let(:attachment) { champ.piece_justificative_file.attachments.first }
|
||||
let(:component) {
|
||||
described_class.new(poll_url: "poll-here", attachment:)
|
||||
}
|
||||
|
||||
subject {
|
||||
render_inline(component).to_html
|
||||
}
|
||||
|
||||
context "when watermark is pending" do
|
||||
it "renders turbo poll attributes" do
|
||||
expect(subject).to have_selector("[data-controller='turbo-poll'][data-turbo-poll-url-value='poll-here']")
|
||||
end
|
||||
|
||||
it "renders" do
|
||||
expect(component).to be_render
|
||||
end
|
||||
|
||||
it "does not render manual reload" do
|
||||
expect(component).not_to have_content("Recharger")
|
||||
end
|
||||
|
||||
context "when watermark is pending for a long time" do
|
||||
before do
|
||||
attachment.created_at = 5.minutes.ago
|
||||
end
|
||||
|
||||
it "renders manual reload" do
|
||||
expect(subject).to have_content("Recharger")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when waterkmark is done" do
|
||||
before do
|
||||
attachment.blob[:metadata] = { watermark: true }
|
||||
end
|
||||
|
||||
it "does not render" do
|
||||
expect(component).not_to be_render
|
||||
end
|
||||
|
||||
context "when antivirus is in progress" do
|
||||
before do
|
||||
attachment.blob[:metadata] = { virus_scan_result: ActiveStorage::VirusScanner::PENDING }
|
||||
end
|
||||
|
||||
it "renders" do
|
||||
expect(component).to be_render
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
67
spec/components/attachment/show_component_spec.rb
Normal file
67
spec/components/attachment/show_component_spec.rb
Normal file
|
@ -0,0 +1,67 @@
|
|||
RSpec.describe Attachment::ShowComponent, type: :component do
|
||||
let(:champ) { create(:champ_piece_justificative) }
|
||||
let(:virus_scan_result) { nil }
|
||||
|
||||
let(:attachment) {
|
||||
champ.piece_justificative_file.attachments.first
|
||||
}
|
||||
|
||||
let(:filename) { attachment.filename.to_s }
|
||||
|
||||
let(:component) do
|
||||
described_class.new(attachment:)
|
||||
end
|
||||
|
||||
subject { render_inline(component).to_html }
|
||||
|
||||
before do
|
||||
champ.piece_justificative_file[0].blob.update(metadata: champ.piece_justificative_file[0].blob.metadata.merge(virus_scan_result: virus_scan_result))
|
||||
end
|
||||
|
||||
context 'when there is no anti-virus scan' do
|
||||
let(:virus_scan_result) { nil }
|
||||
|
||||
it 'allows to download the file' do
|
||||
expect(subject).to have_link(filename)
|
||||
expect(subject).to have_text('ce fichier n’a pas été analysé par notre antivirus')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the anti-virus scan is pending' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::PENDING }
|
||||
|
||||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(subject).to have_text(filename)
|
||||
expect(subject).not_to have_link(filename)
|
||||
expect(subject).to have_text('Analyse antivirus en cours')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the file is scanned and safe' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::SAFE }
|
||||
|
||||
it 'allows to download the file' do
|
||||
expect(subject).to have_link(filename)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the file is scanned and infected' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::INFECTED }
|
||||
|
||||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(subject).to have_text(filename)
|
||||
expect(subject).not_to have_link(filename)
|
||||
expect(subject).to have_text('Virus détecté')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the file is corrupted' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::INTEGRITY_ERROR }
|
||||
|
||||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(subject).to have_text(filename)
|
||||
expect(subject).not_to have_link(filename)
|
||||
expect(subject).to have_text('corrompu')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
describe EditableChamp::PieceJustificativeComponent, type: :component do
|
||||
let(:champ) { create(:champ_piece_justificative, dossier: create(:dossier)) }
|
||||
let(:component) {
|
||||
described_class.new(form: instance_double(ActionView::Helpers::FormBuilder, object_name: "dossier[champs_public_attributes]"), champ:)
|
||||
}
|
||||
|
||||
let(:subject) {
|
||||
render_inline(component).to_html
|
||||
}
|
||||
|
||||
context 'when there is a template' do
|
||||
let(:template) { champ.type_de_champ.piece_justificative_template }
|
||||
let(:profil) { :user }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(ApplicationController).to receive(:administrateur_signed_in?).and_return(profil == :administrateur)
|
||||
end
|
||||
|
||||
it 'renders a link to template' do
|
||||
expect(subject).to have_link('Modèle à télécharger')
|
||||
expect(subject).not_to have_text("éphémère")
|
||||
end
|
||||
|
||||
context 'as an administrator' do
|
||||
let(:profil) { :administrateur }
|
||||
it 'warn about ephemeral template url' do
|
||||
expect(subject).to have_link('Modèle à télécharger')
|
||||
expect(subject).to have_text("éphémère")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -755,11 +755,11 @@ describe API::V2::GraphqlController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "champ" do
|
||||
describe "champ piece_justificative" do
|
||||
let(:champ) { create(:champ_piece_justificative, dossier: dossier) }
|
||||
let(:byte_size) { 2712286911 }
|
||||
|
||||
context "byteSize" do
|
||||
context "with deprecated file field" do
|
||||
let(:query) do
|
||||
"{
|
||||
dossier(number: #{dossier.id}) {
|
||||
|
@ -778,9 +778,28 @@ describe API::V2::GraphqlController do
|
|||
}
|
||||
end
|
||||
|
||||
context "byteSize" do
|
||||
let(:query) do
|
||||
"{
|
||||
dossier(number: #{dossier.id}) {
|
||||
champs(id: \"#{champ.to_typed_id}\") {
|
||||
... on PieceJustificativeChamp {
|
||||
files { byteSize }
|
||||
}
|
||||
}
|
||||
}
|
||||
}"
|
||||
end
|
||||
|
||||
it {
|
||||
expect(gql_errors).to be_nil
|
||||
expect(gql_data).to eq(dossier: { champs: [{ files: [{ byteSize: 4 }] }] })
|
||||
}
|
||||
end
|
||||
|
||||
context "when the file is really big" do
|
||||
before do
|
||||
champ.piece_justificative_file.blob.update(byte_size: byte_size)
|
||||
champ.piece_justificative_file.first.blob.update(byte_size: byte_size)
|
||||
end
|
||||
|
||||
context "byteSize" do
|
||||
|
@ -789,7 +808,7 @@ describe API::V2::GraphqlController do
|
|||
dossier(number: #{dossier.id}) {
|
||||
champs(id: \"#{champ.to_typed_id}\") {
|
||||
... on PieceJustificativeChamp {
|
||||
file { byteSize }
|
||||
files { byteSize }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -807,7 +826,7 @@ describe API::V2::GraphqlController do
|
|||
dossier(number: #{dossier.id}) {
|
||||
champs(id: \"#{champ.to_typed_id}\") {
|
||||
... on PieceJustificativeChamp {
|
||||
file { byteSizeBigInt }
|
||||
files { byteSizeBigInt }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -816,7 +835,7 @@ describe API::V2::GraphqlController do
|
|||
|
||||
it {
|
||||
expect(gql_errors).to be_nil
|
||||
expect(gql_data).to eq(dossier: { champs: [{ file: { byteSizeBigInt: '2712286911' } }] })
|
||||
expect(gql_data).to eq(dossier: { champs: [{ files: [{ byteSizeBigInt: '2712286911' }] }] })
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
describe AttachmentsController, type: :controller do
|
||||
let(:user) { create(:user) }
|
||||
let(:attachment) { champ.piece_justificative_file.attachment }
|
||||
let(:attachment) { champ.piece_justificative_file.attachments.first }
|
||||
let(:dossier) { create(:dossier, user: user) }
|
||||
let(:champ) { create(:champ_piece_justificative, dossier_id: dossier.id) }
|
||||
let(:signed_id) { attachment.blob.signed_id }
|
||||
|
@ -45,7 +45,7 @@ describe AttachmentsController, type: :controller do
|
|||
describe '#destroy' do
|
||||
render_views
|
||||
|
||||
let(:attachment) { champ.piece_justificative_file.attachment }
|
||||
let(:attachment) { champ.piece_justificative_file.attachments.first }
|
||||
let(:dossier) { create(:dossier, user: user) }
|
||||
let(:champ) { create(:champ_piece_justificative, dossier_id: dossier.id) }
|
||||
let(:signed_id) { attachment.blob.signed_id }
|
||||
|
|
|
@ -23,7 +23,7 @@ describe Champs::PieceJustificativeController, type: :controller do
|
|||
subject
|
||||
champ.reload
|
||||
expect(champ.piece_justificative_file.attached?).to be true
|
||||
expect(champ.piece_justificative_file.filename).to eq('piece_justificative_0.pdf')
|
||||
expect(champ.piece_justificative_file[0].filename).to eq('piece_justificative_0.pdf')
|
||||
end
|
||||
|
||||
it 'renders the attachment template as Javascript' do
|
||||
|
|
|
@ -162,8 +162,13 @@ FactoryBot.define do
|
|||
|
||||
factory :champ_titre_identite, class: 'Champs::TitreIdentiteChamp' do
|
||||
type_de_champ { association :type_de_champ_titre_identite, procedure: dossier.procedure }
|
||||
transient do
|
||||
skip_default_attachment { false }
|
||||
end
|
||||
|
||||
after(:build) do |champ, evaluator|
|
||||
next if evaluator.skip_default_attachment
|
||||
|
||||
after(:build) do |champ, _evaluator|
|
||||
champ.piece_justificative_file.attach(
|
||||
io: StringIO.new("toto"),
|
||||
filename: "toto.png",
|
||||
|
|
|
@ -183,6 +183,16 @@ RSpec.describe Types::DossierType, type: :graphql do
|
|||
}
|
||||
end
|
||||
|
||||
describe 'dossier with motivation attachment' do
|
||||
let(:dossier) { create(:dossier, :accepte, :with_motivation, :with_justificatif) }
|
||||
let(:query) { DOSSIER_WITH_MOTIVATION_QUERY }
|
||||
let(:variables) { { number: dossier.id } }
|
||||
|
||||
it {
|
||||
expect(data[:dossier][:motivationAttachment][:url]).not_to be_nil
|
||||
}
|
||||
end
|
||||
|
||||
DOSSIER_QUERY = <<-GRAPHQL
|
||||
query($number: Int!) {
|
||||
dossier(number: $number) {
|
||||
|
@ -219,6 +229,16 @@ RSpec.describe Types::DossierType, type: :graphql do
|
|||
}
|
||||
GRAPHQL
|
||||
|
||||
DOSSIER_WITH_MOTIVATION_QUERY = <<-GRAPHQL
|
||||
query($number: Int!) {
|
||||
dossier(number: $number) {
|
||||
motivationAttachment {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL
|
||||
|
||||
DOSSIER_WITH_CHAMPS_QUERY = <<-GRAPHQL
|
||||
query($number: Int!) {
|
||||
dossier(number: $number) {
|
||||
|
|
|
@ -452,13 +452,13 @@ describe Champ do
|
|||
end
|
||||
|
||||
it 'marks the file as pending virus scan' do
|
||||
expect(subject.piece_justificative_file.virus_scanner.started?).to be_truthy
|
||||
expect(subject.piece_justificative_file.first.virus_scanner.started?).to be_truthy
|
||||
end
|
||||
|
||||
it 'marks the file as safe once the scan completes' do
|
||||
subject
|
||||
perform_enqueued_jobs
|
||||
expect(champ.reload.piece_justificative_file.virus_scanner.safe?).to be_truthy
|
||||
expect(champ.reload.piece_justificative_file.first.virus_scanner.safe?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -467,7 +467,7 @@ describe Champ do
|
|||
describe '#enqueue_watermark_job' do
|
||||
context 'when type_champ is type_de_champ_titre_identite' do
|
||||
let(:type_de_champ) { create(:type_de_champ_titre_identite) }
|
||||
let(:champ) { build(:champ_titre_identite, type_de_champ: type_de_champ) }
|
||||
let(:champ) { build(:champ_titre_identite, type_de_champ: type_de_champ, skip_default_attachment: true) }
|
||||
|
||||
before do
|
||||
allow(ClamavService).to receive(:safe_file?).and_return(true)
|
||||
|
@ -480,14 +480,14 @@ describe Champ do
|
|||
end
|
||||
|
||||
it 'marks the file as needing watermarking' do
|
||||
expect(subject.piece_justificative_file.watermark_pending?).to be_truthy
|
||||
expect(subject.piece_justificative_file.first.watermark_pending?).to be_truthy
|
||||
end
|
||||
|
||||
it 'watermarks the file' do
|
||||
subject
|
||||
perform_enqueued_jobs
|
||||
expect(champ.reload.piece_justificative_file.watermark_pending?).to be_falsy
|
||||
expect(champ.reload.piece_justificative_file.blob.watermark_done?).to be_truthy
|
||||
expect(champ.reload.piece_justificative_file.first.watermark_pending?).to be_falsy
|
||||
expect(champ.reload.piece_justificative_file.first.blob.watermark_done?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,35 +43,35 @@ describe Champs::PieceJustificativeChamp do
|
|||
let(:champ_pj) { create(:champ_piece_justificative) }
|
||||
subject { champ_pj.for_export }
|
||||
|
||||
it { is_expected.to eq('toto.txt') }
|
||||
it { is_expected.to match_array(['toto.txt']) }
|
||||
|
||||
context 'without attached file' do
|
||||
before { champ_pj.piece_justificative_file.purge }
|
||||
it { is_expected.to eq(nil) }
|
||||
it { is_expected.to eq([]) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#for_api' do
|
||||
let(:champ_pj) { create(:champ_piece_justificative) }
|
||||
let(:metadata) { champ_pj.piece_justificative_file.blob.metadata }
|
||||
let(:metadata) { champ_pj.piece_justificative_file.first.blob.metadata }
|
||||
|
||||
before { champ_pj.piece_justificative_file.blob.update(metadata: metadata.merge(virus_scan_result: status)) }
|
||||
before { champ_pj.piece_justificative_file.first.blob.update(metadata: metadata.merge(virus_scan_result: status)) }
|
||||
|
||||
subject { champ_pj.for_api }
|
||||
|
||||
context 'when file is safe' do
|
||||
let(:status) { ActiveStorage::VirusScanner::SAFE }
|
||||
it { is_expected.to include("/rails/active_storage/disk/") }
|
||||
it { is_expected.to match_array([include("/rails/active_storage/disk/")]) }
|
||||
end
|
||||
|
||||
context 'when file is not scanned' do
|
||||
let(:status) { ActiveStorage::VirusScanner::PENDING }
|
||||
it { is_expected.to include("/rails/active_storage/disk/") }
|
||||
it { is_expected.to match_array([include("/rails/active_storage/disk/")]) }
|
||||
end
|
||||
|
||||
context 'when file is infected' do
|
||||
let(:status) { ActiveStorage::VirusScanner::INFECTED }
|
||||
it { is_expected.to be_nil }
|
||||
it { is_expected.to eq([]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1783,7 +1783,7 @@ describe Dossier do
|
|||
let(:dossier) { create(:dossier) }
|
||||
let(:champ_piece_justificative) { create(:champ_piece_justificative, dossier_id: dossier.id) }
|
||||
before { dossier.champs_public << champ_piece_justificative }
|
||||
it { expect(Champs::PieceJustificativeChamp.where(dossier: new_dossier).first.piece_justificative_file.blob).to eq(champ_piece_justificative.piece_justificative_file.blob) }
|
||||
it { expect(Champs::PieceJustificativeChamp.where(dossier: new_dossier).first.piece_justificative_file.first.blob).to eq(champ_piece_justificative.piece_justificative_file.first.blob) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,7 +6,9 @@ describe ChampSerializer do
|
|||
context 'when type champ is piece justificative' do
|
||||
let(:champ) { create(:champ_piece_justificative) }
|
||||
|
||||
it { expect(subject[:value]).to match('/rails/active_storage/disk/') }
|
||||
it {
|
||||
expect(subject[:value]).to match_array([a_string_matching('/rails/active_storage/disk/')])
|
||||
}
|
||||
end
|
||||
|
||||
context 'when type champ is not piece justificative' do
|
||||
|
|
|
@ -20,7 +20,18 @@ describe PiecesJustificativesService do
|
|||
attach_file_to_champ(pj_champ.call(witness))
|
||||
end
|
||||
|
||||
it { expect(subject).to match_array([pj_champ.call(dossier).piece_justificative_file.attachment]) }
|
||||
context 'with a single attachment' do
|
||||
it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) }
|
||||
end
|
||||
|
||||
context 'with a multiple attachments' do
|
||||
before do
|
||||
attach_file_to_champ(pj_champ.call(dossier))
|
||||
end
|
||||
|
||||
it { expect(subject.count).to eq(2) }
|
||||
it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a pj not safe on a champ' do
|
||||
|
@ -46,7 +57,7 @@ describe PiecesJustificativesService do
|
|||
attach_file_to_champ(private_pj_champ.call(witness))
|
||||
end
|
||||
|
||||
it { expect(subject).to match_array([private_pj_champ.call(dossier).piece_justificative_file.attachment]) }
|
||||
it { expect(subject).to match_array(private_pj_champ.call(dossier).piece_justificative_file.attachments) }
|
||||
|
||||
context 'for expert' do
|
||||
let(:for_expert) { true }
|
||||
|
|
|
@ -93,7 +93,7 @@ describe 'The user' do
|
|||
check_selected_value('communes', with: 'Ambléon (01300)')
|
||||
expect(page).to have_field('dossier_link', with: '123')
|
||||
expect(page).to have_text('file.pdf')
|
||||
expect(page).to have_text('analyse antivirus en cours')
|
||||
expect(page).to have_text('Analyse antivirus en cours')
|
||||
end
|
||||
|
||||
let(:procedure_with_repetition) do
|
||||
|
@ -184,7 +184,7 @@ describe 'The user' do
|
|||
find_field('Pièce justificative 2').attach_file(Rails.root + 'spec/fixtures/files/RIB.pdf')
|
||||
|
||||
# Expect the files to be uploaded immediately
|
||||
expect(page).to have_text('analyse antivirus en cours', count: 2, wait: 5)
|
||||
expect(page).to have_text('Analyse antivirus en cours', count: 2, wait: 5)
|
||||
expect(page).to have_text('file.pdf')
|
||||
expect(page).to have_text('RIB.pdf')
|
||||
|
||||
|
@ -206,7 +206,7 @@ describe 'The user' do
|
|||
# Test invalid file type
|
||||
attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/invalid_file_format.json')
|
||||
expect(page).to have_no_text('La pièce justificative n’est pas d’un type accepté')
|
||||
expect(page).to have_text('analyse antivirus en cours', count: 1, wait: 5)
|
||||
expect(page).to have_text('Analyse antivirus en cours', count: 1, wait: 5)
|
||||
end
|
||||
|
||||
scenario 'retry on transcient upload error', js: true do
|
||||
|
@ -216,18 +216,18 @@ describe 'The user' do
|
|||
# Test auto-upload failure
|
||||
# Make the subsequent auto-upload request fail
|
||||
allow_any_instance_of(Champs::PieceJustificativeController).to receive(:update) do |instance|
|
||||
instance.render json: { errors: ['Error'] }, status: :bad_request
|
||||
instance.render json: { errors: ["Une erreur est survenue"] }, status: :bad_request
|
||||
end
|
||||
attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/file.pdf')
|
||||
expect(page).to have_css('p', text: 'Le fichier n’a pas pu être envoyé', visible: :visible, wait: 5)
|
||||
expect(page).to have_button('Ré-essayer', visible: true)
|
||||
expect(page).to have_css('p', text: "Une erreur est survenue", visible: :visible, wait: 5)
|
||||
expect(page).to have_button('Réessayer', visible: true)
|
||||
expect(page).to have_button('Déposer le dossier', disabled: false)
|
||||
|
||||
allow_any_instance_of(Champs::PieceJustificativeController).to receive(:update).and_call_original
|
||||
|
||||
# Test that retrying after a failure works
|
||||
click_on('Ré-essayer', visible: true, wait: 5)
|
||||
expect(page).to have_text('analyse antivirus en cours', wait: 5)
|
||||
click_on('Réessayer', visible: true, wait: 5)
|
||||
expect(page).to have_text('Analyse antivirus en cours', wait: 5)
|
||||
expect(page).to have_text('file.pdf')
|
||||
expect(page).to have_button('Déposer le dossier', disabled: false)
|
||||
|
||||
|
@ -238,6 +238,53 @@ describe 'The user' do
|
|||
expect(page).to have_text('file.pdf')
|
||||
end
|
||||
|
||||
scenario "upload multiple pieces justificatives on same champ", js: true do
|
||||
log_in(user, procedure_with_pjs)
|
||||
fill_individual
|
||||
|
||||
attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/file.pdf')
|
||||
expect(page).to have_text('file.pdf')
|
||||
expect(page).to have_text('Analyse antivirus en cours')
|
||||
|
||||
attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/white.png')
|
||||
expect(page).to have_text('white.png')
|
||||
|
||||
click_on("Supprimer le fichier file.pdf")
|
||||
expect(page).to have_no_text('file.pdf')
|
||||
expect(page).to have_text("La pièce jointe a bien été supprimée")
|
||||
|
||||
attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/black.png')
|
||||
|
||||
# Mark all attachments as safe to test turbo poll
|
||||
# They are not immediately attached in db, so we have to wait a bit before continuing
|
||||
# NOTE: we'res using files not used in other tests to avoid conflicts
|
||||
attachments = Timeout.timeout(5) do
|
||||
filenames = ['white.png', 'black.png']
|
||||
attachments = ActiveStorage::Attachment.where(name: "piece_justificative_file").includes(:blob).filter do |attachment|
|
||||
filenames.include?(attachment.filename.to_s)
|
||||
end
|
||||
|
||||
fail ActiveRecord::RecordNotFound, "Not all attachments where found yet" unless attachments.count == filenames.count
|
||||
|
||||
attachments
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
sleep 0.2
|
||||
retry
|
||||
end
|
||||
|
||||
attachments.each {
|
||||
_1.blob.metadata = { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
|
||||
_1.save!
|
||||
}
|
||||
expect(page).not_to have_text('Analyse antivirus en cours', wait: 10)
|
||||
|
||||
visit current_path
|
||||
|
||||
expect(page).to have_no_text('file.pdf')
|
||||
expect(page).to have_text('white.png')
|
||||
expect(page).to have_text('black.png')
|
||||
end
|
||||
|
||||
context 'with condition' do
|
||||
include Logic
|
||||
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
describe 'shared/attachment/_show.html.haml', type: :view do
|
||||
let(:champ) { create(:champ_piece_justificative) }
|
||||
let(:virus_scan_result) { nil }
|
||||
|
||||
before do
|
||||
champ.piece_justificative_file.blob.update(metadata: champ.piece_justificative_file.blob.metadata.merge(virus_scan_result: virus_scan_result))
|
||||
end
|
||||
|
||||
subject { render Attachment::ShowComponent.new(attachment: champ.piece_justificative_file.attachment) }
|
||||
|
||||
context 'when there is no anti-virus scan' do
|
||||
let(:virus_scan_result) { nil }
|
||||
|
||||
it 'allows to download the file' do
|
||||
expect(subject).to have_link(champ.piece_justificative_file.filename.to_s)
|
||||
expect(subject).to have_text('ce fichier n’a pas été analysé par notre antivirus')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the anti-virus scan is pending' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::PENDING }
|
||||
|
||||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(subject).to have_text(champ.piece_justificative_file.filename.to_s)
|
||||
expect(subject).not_to have_link(champ.piece_justificative_file.filename.to_s)
|
||||
expect(subject).to have_text('analyse antivirus en cours')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when watermark is pending' do
|
||||
let(:champ) { create(:champ_titre_identite) }
|
||||
|
||||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
pp champ.piece_justificative_file.attachment.watermark_pending?
|
||||
expect(subject).to have_text(champ.piece_justificative_file.filename.to_s)
|
||||
expect(subject).not_to have_link(champ.piece_justificative_file.filename.to_s)
|
||||
expect(subject).to have_text('traitement de la pièce en cours')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the file is scanned and safe' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::SAFE }
|
||||
|
||||
it 'allows to download the file' do
|
||||
expect(subject).to have_link(champ.piece_justificative_file.filename.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the file is scanned and infected' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::INFECTED }
|
||||
|
||||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(subject).to have_text(champ.piece_justificative_file.filename.to_s)
|
||||
expect(subject).not_to have_link(champ.piece_justificative_file.filename.to_s)
|
||||
expect(subject).to have_text('virus détecté')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the file is corrupted' do
|
||||
let(:virus_scan_result) { ActiveStorage::VirusScanner::INTEGRITY_ERROR }
|
||||
|
||||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(subject).to have_text(champ.piece_justificative_file.filename.to_s)
|
||||
expect(subject).not_to have_link(champ.piece_justificative_file.filename.to_s)
|
||||
expect(subject).to have_text('corrompu')
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue