Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 102 additions & 10 deletions app/components/attachment/edit_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ def first?
end

def max_file_size
if champ.present?
return TypeDeChamp::IDENTITY_FILE_MAX_SIZE if champ.titre_identite? || champ.titre_identite_nature?
return champ.max_file_size_bytes
end

return if file_size_validator.nil?

file_size_validator.options[:less_than]
Expand Down Expand Up @@ -87,7 +92,7 @@ def file_field_options
auto_attach_url:,
turbo_force: :server,
'enable-submit-if-uploaded-target': 'input'
}.merge(has_file_size_validator? ? { max_file_size: max_file_size } : {})
}.merge(max_file_size.present? ? { max_file_size: max_file_size } : {})
}

describedby = []
Expand All @@ -97,7 +102,8 @@ def file_field_options

options[:aria] = { describedby: describedby.join(' ') }

options.merge!(has_content_type_validator? ? { accept: accept_content_type } : {})
accept = accept_from_type_de_champ || accept_from_attached_type_de_champ || (has_content_type_validator? ? accept_content_type : nil)
options.merge!(accept.present? ? { accept: accept } : {})
options[:multiple] = true if as_multiple?
options[:disabled] = true if (@max && @index >= @max) || persisted?

Expand Down Expand Up @@ -222,16 +228,102 @@ def accept_content_type
list.join(', ')
end

def accept_from_type_de_champ
return nil if champ.blank?

if champ.titre_identite_nature?
return ['.jpg', '.jpeg', '.png'].join(', ')
end

extensions = champ.type_de_champ.send(:allowed_extensions)
return nil if extensions.blank?

extensions.join(', ')
end

def accept_from_attached_type_de_champ
record = @attached_file.record
return nil if champ.present?

tdc = if record.is_a?(TypeDeChamp)
record
elsif record.respond_to?(:type_de_champ)
record.type_de_champ
end

extensions = tdc&.send(:allowed_extensions).presence
extensions&.join(', ')
end

def allowed_formats
@allowed_formats ||= begin
formats = 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 }

# When too many formats are allowed, consider instead manually indicating
# above the input a more comprehensive of formats allowed, like "any image", or a simplified list.
formats.size > 5 ? [] : formats
end
if identity_context?
tdc = champ.present? ? champ.type_de_champ : @attached_file.record
return tdc.send(:allowed_extensions).map { _1.delete_prefix('.') }
end

if champ.blank?
record = @attached_file.record
tdc = record.is_a?(TypeDeChamp) ? record : (record.respond_to?(:type_de_champ) ? record.type_de_champ : nil)
if tdc.is_a?(TypeDeChamp)
return tdc.send(:allowed_extensions).map { _1.delete_prefix('.') } if tdc.rib_nature?

# Pour les pièces jointes avec des formats limités, on affiche les formats autorisés
if tdc.piece_justificative? && tdc.pj_limit_formats? && tdc.pj_format_families.present?
return tdc.send(:allowed_extensions).map { _1.delete_prefix('.') }
end
end
end

raw = if champ.present?
champ.piece_justificative? ? champ.allowed_content_types : (has_content_type_validator? ? content_type_validator.options[:in] : [])
elsif has_content_type_validator?
content_type_validator.options[:in]
else
[]
end

extensions = raw.filter_map { |ct| MiniMime.lookup_by_content_type(ct)&.extension }.uniq

sorted_extensions = extensions.sort_by { |e| EXTENSIONS_ORDER.index(e) || 999 }
sorted_extensions.size > 5 ? (sorted_extensions.first(5) + ['…']) : sorted_extensions
end
end

def allowed_families_with_examples
tdc = if champ.present?
champ.type_de_champ
else
record = @attached_file.record
record.is_a?(TypeDeChamp) ? record : (record.respond_to?(:type_de_champ) ? record.type_de_champ : nil)
end
return nil unless tdc

families =
if tdc.titre_identite_nature?
[:image_scan]
elsif tdc.rib_nature?
[:document_texte, :image_scan]
elsif tdc.piece_justificative? && tdc.pj_limit_formats? && tdc.pj_format_families.present?
Array.wrap(tdc.pj_format_families).map(&:to_sym)
else
nil
end

return nil if families.blank?

families.map do |family|
label = I18n.t("activerecord.attributes.type_de_champ.format_families.#{family}", default: family.to_s.humanize)
examples = FORMAT_FAMILY_EXAMPLES[family]
examples.present? ? "#{label} (#{examples})" : label
end
end

def identity_context?
return false if champ.blank? && !@attached_file.record.is_a?(TypeDeChamp)

tdc = champ || @attached_file.record
tdc.titre_identite? || tdc.titre_identite_nature?
end

def has_content_type_validator?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
---
fr:
max_file_size: "Taille maximale autorisée : %{max_file_size}."
allowed_formats: "Formats supportés : %{formats}"
allowed_formats: "Formats acceptés : %{formats}"
identity_expected: "Pièce attendue : Carte nationale d’identité (uniquement le recto), passeport, titre de séjour ou autre justificatif d’identité."
retry: Réessayer
delete: Supprimer
delete_file: Supprimer le fichier %{filename}
Expand All @@ -14,4 +15,3 @@ fr:
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."

Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,15 @@

- if show_hint?
%p.fr-hint-text.fr-mb-1w{ id: describedby_hint_id }
- if champ&.titre_identite_nature?
= t('.identity_expected')
%br/
- if max_file_size.present?
= t('.max_file_size', max_file_size: number_to_human_size(max_file_size))
- if allowed_formats.present?
- families = allowed_families_with_examples
- if families.present?
= t('.allowed_formats', formats: families.join(', '))
- elsif allowed_formats.present?
= t('.allowed_formats', formats: allowed_formats.join(', '))
- if as_multiple?
= t('.multiple_files')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ fr:
update_start_date: La date de début pour le champ « %{label} » a été modifiée. Le date est désormais %{to}.
remove_end_date: La date de fin pour le champ « %{label} » a été supprimée.
update_end_date: La date de fin pour le champ « %{label} » a été modifiée. La date est désormais %{to}.
<<: *update_referentiel
update_pj_limit_formats: "La limitation des formats du champ « %{label} » a été modifiée."
update_pj_format_families: "Les familles de formats acceptés du champ « %{label} » ont été modifiées."
update_pj_auto_purge: "La suppression automatique du champ « %{label} » a été modifiée."

private:
add: L’annotation privée « %{label} » a été ajoutée.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,16 @@
= t(".#{prefix}.update_piece_justificative_template", label: change.label)
- when :nature
- list.with_item do
= t(".#{prefix}.update_nature", label: change.label, to: change.to)
= t(".#{prefix}.update_nature", label: change.label, to: t("activerecord.attributes.type_de_champ.natures.#{change.to}", default: change.to))
- when :pj_limit_formats
- list.with_item do
= t(".#{prefix}.update_pj_limit_formats", label: change.label)
- when :pj_format_families
- list.with_item do
= t(".#{prefix}.update_pj_format_families", label: change.label)
- when :pj_auto_purge
- list.with_item do
= t(".#{prefix}.update_pj_auto_purge", label: change.label)
- when :notice_explicative
- list.with_item do
= t(".#{prefix}.update_notice_explicative", label: change.label)
Expand Down
16 changes: 16 additions & 0 deletions app/components/types_de_champ_editor/champ_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ def filter_featured_type_champ(type_champ)
end

def filter_type_champ(type_champ)
if type_champ == TypeDeChamp.type_champs.fetch(:titre_identite)
return type_de_champ.titre_identite?
end

case type_champ
when TypeDeChamp.type_champs.fetch(:number)
has_legacy_number?
Expand All @@ -140,6 +144,18 @@ def filter_type_champ(type_champ)
end
end

def format_families_for_select
return [] if !defined?(FORMAT_FAMILIES)

FORMAT_FAMILIES.keys.map do |key|
[
key,
I18n.t("activerecord.attributes.type_de_champ.format_families.#{key}", default: key.to_s.humanize),
(defined?(FORMAT_FAMILY_EXAMPLES) && FORMAT_FAMILY_EXAMPLES[key])
]
end
end

def has_legacy_number?
revision.types_de_champ.any?(&:number?)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,24 @@ en:
labels:
simple: Simple mode
advanced: Advanced mode
identity_notice:
title: "File analysis"
desc_html: "The file uploaded by the user will be automatically analyzed to extract and display to the instructor identity information, for example: <strong>Gender, First name(s), Birth name, Birth date, Birth place, Nationality, Issue date, Expiry date, Document number</strong>."
identity_formats:
title: "ACCEPTED FORMATS"
desc_html: "The user may upload an identity card (front side only), passport, residence permit or another identity document."
rule_html: "Accepted formats for this document are: <strong>image / scan</strong> (<strong>.jpg, .jpeg, .png</strong>) <strong>with a 20 MB maximum size.</strong>"
identity_deletion:
title: "ATTACHMENT DELETION"
desc_html: "As part of GDPR, the identity document will be <strong>watermarked</strong> and <strong>automatically deleted once the file is processed</strong> (accepted, refused or closed)."
note_html: "Note: the identity document will not be available in dossier ZIP exports nor downloadable via API."
rib_notice:
desc_html: "The file uploaded by the user will be automatically analyzed to extract and display to the instructor the following information: <strong>Account holder name, Account holder address</strong> (if present on the file), <strong>IBAN, BIC code, Bank name</strong>."
rib_formats:
title: "ACCEPTED FORMATS"
desc_html: "Accepted formats for this document are: <strong>text document</strong> (.pdf, .doc, .docx, .odt, .txt) and <strong>image / scan</strong> (.jpg, .jpeg, .png)."
limit_formats_label: "Limit to specific file formats"
auto_purge_label: "Automatically delete the file after dossier processing"
template_label: "Template file proposed to the user"
families_legend: "ACCEPTED FORMAT(S)"
tooltip_examples: "Examples: %{examples}"
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,24 @@ fr:
labels:
simple: 'Mode simple'
advanced: 'Mode avancé'
identity_notice:
title: "Analyse du fichier"
desc_html: "Le contenu du fichier joint par l’usager sera analysé automatiquement afin de récupérer et retranscrire à l’instructeur les informations d’identité, par exemple : <strong>Sexe, Prénom(s), Nom de naissance, Date de naissance, Lieu de naissance, Nationalité, Date de délivrance, Date d’expiration, Numéro de la pièce</strong>."
identity_formats:
title: "FORMATS ACCEPTÉS"
desc_html: "L’usager pourra joindre une carte d’identité (uniquement le recto), passeport, titre de séjour ou autre justificatif d’identité."
rule_html: "Les formats acceptés pour cette pièce seront uniquement : <strong>image / scan</strong> (<strong>.jpg, .jpeg, .png</strong>) <strong>uniquement de 20 Mo max.</strong>"
identity_deletion:
title: "SUPPRESSION DE LA PIÈCE JOINTE"
desc_html: "Dans le cadre du RGPD, le titre d’identité sera <strong>filigrané</strong> et <strong>automatiquement supprimé une fois le dossier traité</strong> (accepté, refusé ou classé sans suite)."
note_html: "À noter : le titre d’identité ne sera ni disponible dans les zip de dossiers, ni téléchargeable par API."
rib_notice:
desc_html: "Le contenu du fichier joint par l’usager sera analysé automatiquement afin de récupérer et retranscrire à l’instructeur les informations suivantes : <strong>Nom du titulaire, Adresse du titulaire</strong> (si présente sur le fichier), <strong>IBAN, Code BIC, Nom de la banque</strong>."
rib_formats:
title: "FORMATS ACCEPTÉS"
desc_html: "Les formats acceptés pour cette pièce seront uniquement : <strong>document texte</strong> (.pdf, .doc, .docx, .odt, .txt) et <strong>image / scan</strong> (.jpg, .jpeg, .png)."
limit_formats_label: "Limiter à certains formats de fichier"
auto_purge_label: "Supprimer automatiquement la pièce après traitement du dossier"
template_label: "Modèle de fichier proposé à l’usager"
families_legend: "FORMAT(S) ACCEPTÉ(S)"
tooltip_examples: "Exemples : %{examples}"
Original file line number Diff line number Diff line change
Expand Up @@ -54,30 +54,90 @@
= form.label :notice_explicative, "Notice explicative", for: dom_id(type_de_champ, :notice_explicative)
= render Attachment::EditComponent.new(**notice_explicative_options)

- if type_de_champ.piece_justificative? && procedure.feature_enabled?(:ocr)
- if type_de_champ.piece_justificative?
.cell.fr-mt-1w
.fr-hr.fr-mt-1w
= form.label :nature, "Nature de la pièce", for: dom_id(type_de_champ, :nature)
- natures = TypeDeChamp.natures.keys.map { |k| [t("activerecord.attributes.type_de_champ.natures.#{k}", default: k.to_s.humanize), k] }
= form.select :nature,
TypeDeChamp.natures.to_a + [["Générique", nil]],
natures,
{ },
class: 'fr-select small-margin small inline width-100',
id: dom_id(type_de_champ, :nature)
- nature_dom_id = dom_id(type_de_champ, :nature)
- if type_de_champ.titre_identite_nature?
.cell.fr-mt-2w
= render Dsfr::NoticeComponent.new(state: 'info') do |c|
- c.with_title do
= t('.identity_notice.title')
- c.with_desc do
= t('.identity_notice.desc_html')

.fr-hr.fr-mt-2w
.cell
= form.label :formats_accepted, t('.identity_formats.title'), for: nature_dom_id, class: 'fake-label'
%p.fr-mb-0= t('.identity_formats.desc_html')
%p.fr-mt-0= t('.identity_formats.rule_html')
.fr-hr.fr-mt-1w
.cell
= form.label :deletion_title, t('.identity_deletion.title'), for: nature_dom_id, class: 'fake-label'
%p.fr-mb-0= t('.identity_deletion.desc_html')
%p.fr-mt-0= t('.identity_deletion.note_html')
- elsif type_de_champ.rib_nature?
.cell.fr-mt-2w
= render Dsfr::NoticeComponent.new(state: 'info') do |c|
- c.with_title do
= t('.identity_notice.title')
- c.with_desc do
= t('.rib_notice.desc_html')
.fr-hr.fr-mt-2w
.cell
= form.label :formats_accepted, t('.rib_formats.title'), for: nature_dom_id, class: 'fake-label'
%p.fr-mb-0= t('.rib_formats.desc_html')
- else
.fr-hr.fr-mt-2w
.cell{ data: { controller: 'hide-target' } }
.fr-toggle{ class: class_names('fr-mb-2w': form.object.pj_limit_formats?) }
= form.check_box :pj_limit_formats,
id: dom_id(type_de_champ, :pj_limit_formats),
class: 'fr-toggle__input',
data: { 'hide-target-target': 'source' },
checked: form.object.pj_limit_formats?
= form.label :pj_limit_formats, t('.limit_formats_label'), for: dom_id(type_de_champ, :pj_limit_formats), class: 'fr-toggle__label fr-label'

- families = format_families_for_select
- grid_hidden_class = form.object.pj_limit_formats? ? nil : 'fr-hidden'
.cell{ class: grid_hidden_class, data: { 'hide-target-target': 'toHide' } }
%fieldset.fr-fieldset.fr-mx-0.fr-px-0
%legend.fr-fieldset__legend.fr-fieldset__legend--regular.fr-mx-0.fr-pl-0.fr-mb-1w
= t('.families_legend')

- prechecked = Array.wrap(form.object.pj_format_families).map(&:to_s)
- families.each do |(value, label, examples)|
.fr-fieldset__element.fr-fieldset__element--inline.fr-pl-0
.fr-checkbox-group
- checked_opt = prechecked.include?(value.to_s)
= form.check_box :pj_format_families, { multiple: true, id: dom_id(type_de_champ, "pj_format_#{value}"), checked: checked_opt }, value, nil
- if examples.present?
- tip_id = "#{dom_id(type_de_champ, "pj_format_#{value}")}-tip"
= form.label "pj_format_families_#{value}", label, for: dom_id(type_de_champ, "pj_format_#{value}"), class: 'fr-label', tabindex: 0, 'aria-describedby': tip_id
%span.fr-tooltip.fr-placement{ id: tip_id, role: 'tooltip', 'aria-hidden': 'true' }= t('.tooltip_examples', examples: examples)
- else
= form.label "pj_format_families_#{value}", label, for: dom_id(type_de_champ, "pj_format_#{value}"), class: 'fr-label'

- if !type_de_champ.titre_identite_nature?
.fr-hr.fr-mt-1w
.cell
.fr-toggle
= form.check_box :pj_auto_purge, id: dom_id(type_de_champ, :pj_auto_purge), class: 'fr-toggle__input'
= form.label :pj_auto_purge, t('.auto_purge_label'), for: dom_id(type_de_champ, :pj_auto_purge), class: 'fr-toggle__label fr-label'

- if type_de_champ.piece_justificative_or_titre_identite?
.cell.fr-mt-1w
= form.label :piece_justificative_template, "Modèle", for: dom_id(type_de_champ, :piece_justificative_template)
.fr-hr.fr-mt-1w
= form.label :piece_justificative_template, t('.template_label'), for: dom_id(type_de_champ, :piece_justificative_template)
= render Attachment::EditComponent.new(**piece_justificative_template_options)

- if type_de_champ.titre_identite?
.cell.fr-mt-1w
= render Dsfr::AlertComponent.new(state: :info, heading_level: 'p') do |c|
- c.with_body do
Dans le cadre de la RGPD, le titre d’identité sera supprimé lors de l’acceptation, du refus ou du classement sans suite du dossier.<br />
Aussi, pour des raisons de sécurité, un filigrane est automatiquement ajouté aux images.<br />
Finalement, le titre d’identité ne sera ni disponible dans les zip de dossiers, ni téléchargeable par API.
- elsif procedure.piece_justificative_multiple?
.cell.fr-mt-1w
%p Les usagers pourront envoyer plusieurs fichiers si nécessaire.

- if type_de_champ.integer_number? || type_de_champ.decimal_number?
.border-left-dark.fr-mt-2w
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/administrateurs/types_de_champ_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ def type_de_champ_update_params
:libelle,
:description,
:mandatory,
:pj_limit_formats,
:pj_auto_purge,
{ pj_format_families: [] },
:drop_down_options_from_text,
:drop_down_other,
:drop_down_secondary_libelle,
Expand Down
Loading
Loading