diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss
index ce83a13bb..6073283bc 100644
--- a/app/assets/stylesheets/forms.scss
+++ b/app/assets/stylesheets/forms.scss
@@ -551,3 +551,22 @@ textarea::placeholder {
.resize-y {
resize: vertical;
}
+
+.checkbox-group-bordered {
+ border: 1px solid var(--border-default-grey);
+ flex: 1 1 100%; // copied from fr-fieldset-element
+ max-width: 100%; // copied from fr-fieldset-element
+}
+
+.fieldset-bordered {
+ position: relative;
+}
+
+.fieldset-bordered::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ border-left: 2px solid var(--border-default-blue-france);
+}
diff --git a/app/assets/stylesheets/instructeur.scss b/app/assets/stylesheets/instructeur.scss
index b42345d6a..1b8105aed 100644
--- a/app/assets/stylesheets/instructeur.scss
+++ b/app/assets/stylesheets/instructeur.scss
@@ -44,8 +44,12 @@
position: relative;
}
-.dropdown-export .dropdown-content {
+.dropdown-export.dropdown-content {
width: 450px;
+
+ a {
+ text-decoration: underline;
+ }
}
.dropdown-label.dropdown-content {
diff --git a/app/components/dossiers/export_dropdown_component.rb b/app/components/dossiers/export_dropdown_component.rb
index c970576e3..9710c3704 100644
--- a/app/components/dossiers/export_dropdown_component.rb
+++ b/app/components/dossiers/export_dropdown_component.rb
@@ -3,13 +3,14 @@
class Dossiers::ExportDropdownComponent < ApplicationComponent
include ApplicationHelper
- def initialize(procedure:, export_templates: nil, statut: nil, count: nil, class_btn: nil, export_url: nil)
+ def initialize(procedure:, export_templates: nil, statut: nil, count: nil, class_btn: nil, export_url: nil, show_export_template_tab: true)
@procedure = procedure
@export_templates = export_templates
@statut = statut
@count = count
@class_btn = class_btn
@export_url = export_url
+ @show_export_template_tab = show_export_template_tab
end
def formats
diff --git a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml
index 441149066..e9c41e5be 100644
--- a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml
+++ b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml
@@ -1,26 +1,68 @@
-= render Dropdown::MenuComponent.new(wrapper: :span, button_options: { class: ['fr-btn--sm', @class_btn.present? ? @class_btn : 'fr-btn--secondary']}, menu_options: { id: @count.nil? ? "download_menu" : "download_all_menu", class: ['dropdown-export'] }) do |menu|
- - menu.with_menu_header_html do
- %p.menu-component-header.fr-px-2w.fr-pt-2w.fr-mb-0
- %span.fr-icon-info-line{ aria: { hidden: true } }
- Des macros ? Lisez la
- = link_to('doc', t('.macros_doc.url'),
- title: t('.macros_doc.title'),
- **external_link_attributes)
-
+= render Dropdown::MenuComponent.new(wrapper: :div, button_options: { class: ['fr-btn--sm', @class_btn.present? ? @class_btn : 'fr-btn--secondary']}, menu_options: { id: @count.nil? ? "download_menu" : "download_all_menu", class: ['dropdown-export'] }) do |menu|
- menu.with_button_inner_html do
= @count.nil? ? t(".download_all") : t(".download", count: @count)
- - formats.each do |format|
- - menu.with_item do
- = link_to download_export_path(export_format: format), role: 'menuitem', data: { turbo_method: :post, turbo: true } do
- = t(".everything_#{format}_html")
+ - menu.with_form do
+ .fr-container
+ .fr-tabs.fr-my-3w
+ %ul.fr-tabs__list{ role: 'tablist' }
+ %li{ role: 'presentation' }
+ %button.fr-tabs__tab.fr-tabs__tab--icon-left{ id: "tabpanel-standard#{@count}", tabindex: "0", role: "tab", "aria-selected": "true", "aria-controls": "tabpanel-standard#{@count}-panel" } Standard
- - if @procedure.feature_enabled?(:export_template)
- - if export_templates.present?
- - export_templates.each do |export_template|
- - menu.with_item do
- = link_to download_export_path(export_template_id: export_template.id), role: 'menuitem', data: { turbo_method: :post, turbo: true } do
- = "Exporter à partir du modèle #{export_template.name}"
- - menu.with_item do
- = link_to [:new, :instructeur, @procedure, :export_template], role: 'menuitem' do
- Ajouter un modèle d'export
+ - if @show_export_template_tab
+ %li{ role: 'presentation' }
+ %button.fr-tabs__tab.fr-tabs__tab--icon-left{ id: "tabpanel-template#{@count}", tabindex: "-1", role: "tab", "aria-selected": "false", "aria-controls": "tabpanel-template#{@count}-panel" } A partir d'un modèle
+
+ .fr-tabs__panel.fr-pb-8w.fr-tabs__panel--selected{ id: "tabpanel-standard#{@count}-panel", role: "tabpanel", "aria-labelledby": "tabpanel-standard#{@count}", tabindex: "0" }
+ = form_with url: download_export_path, namespace: "export#{@count}", data: { turbo_method: :post, turbo: true } do |f|
+ = f.hidden_field :statut, value: @statut
+ %fieldset.fr-fieldset#radio-hint{ "aria-labelledby": "radio-hint-legend" }
+ %legend.fr-fieldset__legend--regular.fr-fieldset__legend#radio-hint-legend Séletionner le format de l'export
+ .fr-fieldset__element
+ .fr-radio-group
+ = f.radio_button :export_format, 'xlsx'
+ = f.label :export_format_xlsx, 'Fichier xlsx'
+ .fr-fieldset__element
+ .fr-radio-group
+ = f.radio_button :export_format, 'ods'
+ = f.label :export_format_ods, 'Fichier ods'
+ .fr-fieldset__element
+ .fr-radio-group
+ = f.radio_button :export_format, 'csv'
+ = f.label :export_format_csv do
+ Fichier csv
+ %span.fr-hint-text Uniquement les dossiers, sans les champs répétables
+ .fr-fieldset__element
+ .fr-radio-group
+ = f.radio_button :export_format, 'zip'
+ = f.label :export_format_zip do
+ Fichier zip
+ %span.fr-hint-text ne contient pas l'horodatage ni le journal de log
+
+ .fr-fieldset__element
+ %ul.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline
+ %li
+ %button.fr-btn.fr-btn--secondary{ type: 'button', "data-action": "click->menu-button#close" } Annuler
+ %li
+ = f.submit "Demander l'export", "data-action": "click->menu-button#close", class: 'fr-btn'
+
+
+ - if @show_export_template_tab
+ .fr-tabs__panel.fr-pr-3w.fr-pb-8w{ id: "tabpanel-template#{@count}-panel", role: "tabpanel", "aria-labelledby": "tabpanel-template", tabindex: "0" }
+ = form_with url: download_export_path, namespace: "export_template_#{@count}", data: { turbo_method: :post, turbo: true } do |f|
+ = f.hidden_field :statut, value: @statut
+ .fr-select-group
+ - if export_templates.present?
+ %label.fr-label{ for: 'select' }
+ Sélectionner le modèle d'export
+ = f.collection_select :export_template_id, export_templates, :id, :name, {}, { class: "fr-select fr-mb-2w" }
+ - else
+ %p
+ %i Aucun modèle configuré
+ %p
+ = link_to "Configurer les modèles d'export", exports_instructeur_procedure_path(procedure_id: params[:procedure_id]), class: 'fr-link'
+ %ul.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline
+ %li
+ %button.fr-btn.fr-btn--secondary{ type: 'button', "data-action": "click->menu-button#close" } Annuler
+ %li
+ = f.submit "Demander l'export", "data-action": "click->menu-button#close", class: 'fr-btn'
diff --git a/app/components/export_template/champs_component.rb b/app/components/export_template/champs_component.rb
new file mode 100644
index 000000000..e12b2d5d9
--- /dev/null
+++ b/app/components/export_template/champs_component.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class ExportTemplate::ChampsComponent < ApplicationComponent
+ attr_reader :export_template, :title
+
+ def initialize(title, export_template, types_de_champ)
+ @title = title
+ @export_template = export_template
+ @types_de_champ = types_de_champ
+ end
+
+ def historical_libelle(column)
+ historical_exported_column = export_template.exported_columns.find { _1.column == column }
+ if historical_exported_column
+ historical_exported_column.libelle
+ else
+ column.label
+ end
+ end
+
+ def sections
+ @types_de_champ
+ .reject { _1.header_section? && _1.header_section_level_value > 1 }
+ .slice_before(&:header_section?)
+ .filter_map do |(head, *rest)|
+ libelle = head.libelle if head.header_section?
+ columns = [head.header_section? ? nil : head, *rest].compact.map { tdc_to_columns(_1) }
+ { libelle:, columns: } if columns.present?
+ end
+ end
+
+ def component_prefix
+ title.parameterize
+ end
+
+ private
+
+ def tdc_to_columns(type_de_champ)
+ prefix = type_de_champ.repetition? ? "Bloc répétable" : nil
+ type_de_champ.columns(procedure: export_template.procedure, prefix:).map do |column|
+ ExportedColumn.new(column:,
+ libelle: historical_libelle(column))
+ end
+ end
+end
diff --git a/app/components/export_template/champs_component/champs_component.html.haml b/app/components/export_template/champs_component/champs_component.html.haml
new file mode 100644
index 000000000..ea6f603cc
--- /dev/null
+++ b/app/components/export_template/champs_component/champs_component.html.haml
@@ -0,0 +1,23 @@
+%fieldset.fr-fieldset{ id: "#{component_prefix}-fieldset", data: { controller: 'checkbox-select-all' } }
+ %legend.fr-fieldset__legend--regular.fr-fieldset__legend.fr-h5.fr-pb-0
+ = title
+ .checkbox-group-bordered.fr-mx-1w.fr-mb-2w
+ .fr-fieldset__element.fr-background-contrast--grey.fr-py-2w.fr-px-4w
+ .fr-checkbox-group
+ = check_box_tag "#{component_prefix}-select-all", "select-all", false, data: { "checkbox-select-all-target": 'checkboxAll' }
+ = label_tag "#{component_prefix}-select-all", "Tout sélectionner"
+ - sections.each.with_index do |section, idx|
+ - if section[:libelle]
+ .fr-fieldset__element.fr-text--bold.fr-px-4w{ class: idx > 0 ? "fr-pt-1w" : "" }= section[:libelle]
+
+ - section[:columns].each do |grouped_columns|
+ - if grouped_columns.many?
+ .fr-fieldset__element
+ .fieldset-bordered.fr-ml-3v
+ - grouped_columns.each do |exported_column|
+ .fr-fieldset__element.fr-px-3v
+ .fr-checkbox-group= render ExportTemplate::CheckboxComponent.new(export_template:, exported_column:)
+ - else
+ - grouped_columns.each do |exported_column|
+ .fr-fieldset__element.fr-px-4w
+ .fr-checkbox-group= render ExportTemplate::CheckboxComponent.new(export_template:, exported_column:)
diff --git a/app/components/export_template/checkbox_component.rb b/app/components/export_template/checkbox_component.rb
new file mode 100644
index 000000000..c5617e71d
--- /dev/null
+++ b/app/components/export_template/checkbox_component.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class ExportTemplate::CheckboxComponent < ApplicationComponent
+ attr_reader :exported_column, :export_template
+
+ def initialize(export_template:, exported_column:)
+ @export_template = export_template
+ @exported_column = exported_column
+ end
+
+ def call
+ safe_join([
+ check_box,
+ label_tag(label_id, exported_column.libelle)
+ ])
+ end
+
+ def check_box
+ check_box_tag(
+ 'export_template[exported_columns][]',
+ exported_column.id,
+ export_template.in_export?(exported_column),
+ class: 'fr-checkbox',
+ id: sanitize_to_id(label_id), # sanitize_to_id is used by rails in label_tag
+ data: { "checkbox-select-all-target": 'checkbox' }
+ )
+ end
+
+ def label_id
+ exported_column.column.id
+ end
+end
diff --git a/app/components/instructeurs/column_table_header_component.rb b/app/components/instructeurs/column_table_header_component.rb
index fa4ff6f90..4cb90dd93 100644
--- a/app/components/instructeurs/column_table_header_component.rb
+++ b/app/components/instructeurs/column_table_header_component.rb
@@ -11,7 +11,7 @@ class Instructeurs::ColumnTableHeaderComponent < ApplicationComponent
def classname(column)
return 'status-col' if column.dossier_state?
- return 'number-col' if column.type == :number
+ return 'number-col' if column.dossier_id?
return 'sva-col' if column.column == 'sva_svr_decision_on'
end
diff --git a/app/controllers/instructeurs/export_templates_controller.rb b/app/controllers/instructeurs/export_templates_controller.rb
index 49d2e3db8..453673b42 100644
--- a/app/controllers/instructeurs/export_templates_controller.rb
+++ b/app/controllers/instructeurs/export_templates_controller.rb
@@ -5,9 +5,10 @@ module Instructeurs
before_action :set_procedure_and_groupe_instructeurs
before_action :set_export_template, only: [:edit, :update, :destroy]
before_action :ensure_legitimate_groupe_instructeur, only: [:create, :update]
+ before_action :set_types_de_champ, only: [:new, :edit]
def new
- @export_template = ExportTemplate.default(groupe_instructeur: @groupe_instructeurs.first)
+ @export_template = export_template
end
def create
@@ -49,9 +50,29 @@ module Instructeurs
private
+ def export_template = @export_template ||= ExportTemplate.default(groupe_instructeur: @groupe_instructeurs.first, kind:)
+
+ def kind = params[:kind] == 'zip' ? 'zip' : 'xlsx'
+
+ def set_types_de_champ
+ if export_template.tabular?
+ @types_de_champ_public = @procedure.all_revisions_types_de_champ(parent: nil, with_header_section: true).public_only
+ @types_de_champ_private = @procedure.all_revisions_types_de_champ(parent: nil, with_header_section: true).private_only
+ end
+ end
+
def export_template_params
- params.require(:export_template)
- .permit(:name, :kind, :groupe_instructeur_id, dossier_folder: [:enabled, :template], export_pdf: [:enabled, :template], pjs: [:stable_id, :enabled, :template])
+ params
+ .require(:export_template)
+ .permit(
+ :name,
+ :kind,
+ :groupe_instructeur_id,
+ dossier_folder: [:enabled, :template],
+ export_pdf: [:enabled, :template],
+ pjs: [:stable_id, :enabled, :template],
+ exported_columns: []
+ )
end
def set_procedure_and_groupe_instructeurs
diff --git a/app/helpers/export_template_helper.rb b/app/helpers/export_template_helper.rb
new file mode 100644
index 000000000..518de7521
--- /dev/null
+++ b/app/helpers/export_template_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ExportTemplateHelper
+ def pretty_kind(kind)
+ icon = kind == 'zip' ? 'archive' : 'table'
+ pretty = tag.span nil, class: "fr-icon-#{icon}-line fr-mr-1v"
+ pretty + kind.upcase
+ end
+end
diff --git a/app/javascript/controllers/checkbox_select_all_controller.ts b/app/javascript/controllers/checkbox_select_all_controller.ts
new file mode 100644
index 000000000..86fbe44d4
--- /dev/null
+++ b/app/javascript/controllers/checkbox_select_all_controller.ts
@@ -0,0 +1,71 @@
+import { ApplicationController } from './application_controller';
+
+export class CheckboxSelectAll extends ApplicationController {
+ declare readonly hasCheckboxAllTarget: boolean;
+ declare readonly checkboxTargets: HTMLInputElement[];
+ declare readonly checkboxAllTarget: HTMLInputElement;
+
+ static targets: string[] = ['checkboxAll', 'checkbox'];
+
+ initialize() {
+ this.toggle = this.toggle.bind(this);
+ this.refresh = this.refresh.bind(this);
+ }
+
+ checkboxAllTargetConnected(checkbox: HTMLInputElement): void {
+ checkbox.addEventListener('change', this.toggle);
+
+ this.refresh();
+ }
+
+ checkboxTargetConnected(checkbox: HTMLInputElement): void {
+ checkbox.addEventListener('change', this.refresh);
+
+ this.refresh();
+ }
+
+ checkboxAllTargetDisconnected(checkbox: HTMLInputElement): void {
+ checkbox.removeEventListener('change', this.toggle);
+
+ this.refresh();
+ }
+
+ checkboxTargetDisconnected(checkbox: HTMLInputElement): void {
+ checkbox.removeEventListener('change', this.refresh);
+
+ this.refresh();
+ }
+
+ toggle(e: Event): void {
+ e.preventDefault();
+
+ this.checkboxTargets.forEach((checkbox) => {
+ // @ts-expect-error faut savoir hein
+ checkbox.checked = e.target.checked;
+ this.triggerInputEvent(checkbox);
+ });
+ }
+
+ refresh(): void {
+ const checkboxesCount = this.checkboxTargets.length;
+ const checkboxesCheckedCount = this.checked.length;
+
+ this.checkboxAllTarget.checked = checkboxesCheckedCount > 0;
+ this.checkboxAllTarget.indeterminate =
+ checkboxesCheckedCount > 0 && checkboxesCheckedCount < checkboxesCount;
+ }
+
+ triggerInputEvent(checkbox: HTMLInputElement): void {
+ const event = new Event('input', { bubbles: false, cancelable: true });
+
+ checkbox.dispatchEvent(event);
+ }
+
+ get checked(): HTMLInputElement[] {
+ return this.checkboxTargets.filter((checkbox) => checkbox.checked);
+ }
+
+ get unchecked(): HTMLInputElement[] {
+ return this.checkboxTargets.filter((checkbox) => !checkbox.checked);
+ }
+}
diff --git a/app/javascript/controllers/menu_button_controller.ts b/app/javascript/controllers/menu_button_controller.ts
index a5edf9b53..b89b9025c 100644
--- a/app/javascript/controllers/menu_button_controller.ts
+++ b/app/javascript/controllers/menu_button_controller.ts
@@ -61,6 +61,12 @@ export class MenuButtonController extends ApplicationController {
});
}
+ close() {
+ this.buttonTarget.setAttribute('aria-expanded', 'false');
+ this.menuTarget.parentElement?.classList.remove('open');
+ this.setFocusToMenuitem(null);
+ }
+
private open(focusMenuItem: 'first' | 'last' = 'first') {
this.buttonTarget.setAttribute('aria-expanded', 'true');
this.menuTarget.parentElement?.classList.add('open');
@@ -75,12 +81,6 @@ export class MenuButtonController extends ApplicationController {
});
}
- private close() {
- this.buttonTarget.setAttribute('aria-expanded', 'false');
- this.menuTarget.parentElement?.classList.remove('open');
- this.setFocusToMenuitem(null);
- }
-
private isClickOutside(target: HTMLElement) {
return (
target.isConnected &&
diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb
index e94bbf518..0e9e8ef64 100644
--- a/app/models/champs/repetition_champ.rb
+++ b/app/models/champs/repetition_champ.rb
@@ -40,11 +40,11 @@ class Champs::RepetitionChamp < Champ
self[attribute]
end
- def spreadsheet_columns(types_de_champ)
+ def spreadsheet_columns(types_de_champ, export_template: nil, format:)
[
['Dossier ID', :dossier_id],
['Ligne', :index]
- ] + dossier.champs_for_export(types_de_champ, row_id)
+ ] + dossier.champ_values_for_export(types_de_champ, row_id:, export_template:, format:)
end
end
end
diff --git a/app/models/column.rb b/app/models/column.rb
index 22fa01a6f..b02feda10 100644
--- a/app/models/column.rb
+++ b/app/models/column.rb
@@ -32,6 +32,7 @@ class Column
def ==(other) = h_id == other.h_id # using h_id instead of id to avoid inversion of keys
def notifications? = [table, column] == ['notifications', 'notifications']
+ def dossier_id? = [table, column] == ['self', 'id']
def dossier_state? = [table, column] == ['self', 'state']
def groupe_instructeur? = [table, column] == ['groupe_instructeur', 'id']
def dossier_labels? = [table, column] == ['dossier_labels', 'label_id']
@@ -47,6 +48,9 @@ class Column
procedure.find_column(h_id: h_id)
end
+ def dossier_column? = false
+ def champ_column? = false
+
private
def column_id = "#{table}/#{column}"
diff --git a/app/models/columns/champ_column.rb b/app/models/columns/champ_column.rb
index c032b3a35..2d3859774 100644
--- a/app/models/columns/champ_column.rb
+++ b/app/models/columns/champ_column.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Columns::ChampColumn < Column
- attr_reader :stable_id
+ attr_reader :stable_id, :tdc_type
def initialize(procedure_id:, label:, stable_id:, tdc_type:, displayable: true, filterable: true, type: :text, options_for_select: [])
@stable_id = stable_id
@@ -45,6 +45,8 @@ class Columns::ChampColumn < Column
end
end
+ def champ_column? = true
+
private
def column_id = "type_de_champ/#{stable_id}"
diff --git a/app/models/columns/dossier_column.rb b/app/models/columns/dossier_column.rb
index 72ec97405..82730b2a9 100644
--- a/app/models/columns/dossier_column.rb
+++ b/app/models/columns/dossier_column.rb
@@ -15,4 +15,6 @@ class Columns::DossierColumn < Column
dossier.followers_instructeurs.map(&:email).join(' ')
end
end
+
+ def dossier_column? = true
end
diff --git a/app/models/columns/linked_drop_down_column.rb b/app/models/columns/linked_drop_down_column.rb
index 1d25d3eea..6399137b0 100644
--- a/app/models/columns/linked_drop_down_column.rb
+++ b/app/models/columns/linked_drop_down_column.rb
@@ -46,10 +46,10 @@ class Columns::LinkedDropDownColumn < Columns::ChampColumn
def column_id = "type_de_champ/#{stable_id}->#{path}"
def typed_value(champ)
- return nil if path == :value
-
primary_value, secondary_value = unpack_values(champ.value)
case path
+ when :value
+ "#{primary_value} / #{secondary_value}"
when :primary
primary_value
when :secondary
diff --git a/app/models/columns/piece_justificative_column.rb b/app/models/columns/piece_justificative_column.rb
new file mode 100644
index 000000000..6b29ae35b
--- /dev/null
+++ b/app/models/columns/piece_justificative_column.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Columns::PieceJustificativeColumn < Column
+ private
+
+ def typed_value(champ)
+ champ.piece_justificative_file.map { _1.blob.filename.to_s }.join(', ')
+ end
+end
diff --git a/app/models/concerns/columns_concern.rb b/app/models/concerns/columns_concern.rb
index 1cf268744..5d61dc982 100644
--- a/app/models/concerns/columns_concern.rb
+++ b/app/models/concerns/columns_concern.rb
@@ -51,7 +51,7 @@ module ColumnsConcern
columns.filter { _1.id.in?(self.columns.map(&:id)) }
end
- def dossier_id_column = dossier_col(table: 'self', column: 'id', type: :number)
+ def dossier_id_column = dossier_col(table: 'self', column: 'id', type: :integer)
def dossier_state_column
options_for_select = I18n.t('instructeurs.dossiers.filterable_state').map(&:to_a).map(&:reverse)
@@ -87,13 +87,13 @@ module ColumnsConcern
def followers_instructeurs_email_column = dossier_col(table: 'followers_instructeurs', column: 'email')
- def dossier_archived_column = dossier_col(table: 'self', column: 'archived', type: :text, displayable: false, filterable: false);
+ def dossier_archived_column = dossier_col(table: 'self', column: 'archived', type: :boolean, displayable: false, filterable: false);
def dossier_motivation_column = dossier_col(table: 'self', column: 'motivation', type: :text, displayable: false, filterable: false);
def user_email_for_display_column = dossier_col(table: 'self', column: 'user_email_for_display', filterable: false, displayable: false)
- def user_france_connected_column = dossier_col(table: 'self', column: 'user_from_france_connect?', filterable: false, displayable: false)
+ def user_france_connected_column = dossier_col(table: 'self', column: 'user_from_france_connect?', type: :boolean, filterable: false, displayable: false)
def dossier_labels_column = dossier_col(table: 'dossier_labels', column: 'label_id', type: :enum, options_for_select: labels.map { [_1.name, _1.id] })
@@ -109,7 +109,7 @@ module ColumnsConcern
def dossier_dates_columns
['created_at', 'updated_at', 'last_champ_updated_at', 'depose_at', 'en_construction_at', 'en_instruction_at', 'processed_at']
- .map { |column| dossier_col(table: 'self', column:, type: :date) }
+ .map { |column| dossier_col(table: 'self', column:, type: :datetime) }
end
def email_column
@@ -140,7 +140,8 @@ module ColumnsConcern
def individual_columns
['gender', 'nom', 'prenom'].map { |column| dossier_col(table: 'individual', column:) }
- .concat ['for_tiers', 'mandataire_last_name', 'mandataire_first_name'].map { |column| dossier_col(table: 'self', column:) }
+ .concat ['mandataire_last_name', 'mandataire_first_name'].map { |column| dossier_col(table: 'self', column:) }
+ .concat ['for_tiers'].map { |column| dossier_col(table: 'self', column:, type: :boolean) }
end
def moral_columns
diff --git a/app/models/concerns/dossier_champs_concern.rb b/app/models/concerns/dossier_champs_concern.rb
index 570b744d8..f6e9baf0a 100644
--- a/app/models/concerns/dossier_champs_concern.rb
+++ b/app/models/concerns/dossier_champs_concern.rb
@@ -82,15 +82,6 @@ module DossierChampsConcern
.map { _1.repetition? ? project_champ(_1, nil) : champ_for_update(_1, nil, updated_by: nil) }
end
- def champs_for_export(types_de_champ, row_id = nil)
- types_de_champ.flat_map do |type_de_champ|
- champ = filled_champ(type_de_champ, row_id)
- type_de_champ.libelles_for_export.map do |(libelle, path)|
- [libelle, type_de_champ.champ_value_for_export(champ, path)]
- end
- end
- end
-
def champ_value_for_tag(type_de_champ, path = :value)
champ = filled_champ(type_de_champ, nil)
type_de_champ.champ_value_for_tag(champ, path)
diff --git a/app/models/concerns/dossier_export_concern.rb b/app/models/concerns/dossier_export_concern.rb
new file mode 100644
index 000000000..887b48207
--- /dev/null
+++ b/app/models/concerns/dossier_export_concern.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+module DossierExportConcern
+ extend ActiveSupport::Concern
+
+ def spreadsheet_columns_csv(types_de_champ:, export_template: nil)
+ spreadsheet_columns(with_etablissement: true, types_de_champ:, export_template:, format: :csv)
+ end
+
+ def spreadsheet_columns_xlsx(types_de_champ:, export_template: nil)
+ spreadsheet_columns(types_de_champ:, export_template:, format: :xlsx)
+ end
+
+ def spreadsheet_columns_ods(types_de_champ:, export_template: nil)
+ spreadsheet_columns(types_de_champ:, export_template:, format: :ods)
+ end
+
+ def champ_values_for_export(types_de_champ, row_id: nil, export_template: nil, format:)
+ types_de_champ.flat_map do |type_de_champ|
+ champ = filled_champ(type_de_champ, row_id)
+ if export_template.present?
+ export_template
+ .columns_for_stable_id(type_de_champ.stable_id)
+ .map { |exported_column| exported_column.libelle_with_value(champ, format:) }
+ else
+ type_de_champ.libelles_for_export.map do |(libelle, path)|
+ [libelle, type_de_champ.champ_value_for_export(champ, path)]
+ end
+ end
+ end
+ end
+
+ def spreadsheet_columns(types_de_champ:, with_etablissement: false, export_template: nil, format: nil)
+ dossier_values_for_export(with_etablissement:, export_template:, format:) + champ_values_for_export(types_de_champ, export_template:, format:)
+ end
+
+ private
+
+ def dossier_values_for_export(with_etablissement: false, export_template: nil, format:)
+ if export_template.present?
+ return export_template.dossier_exported_columns.map { _1.libelle_with_value(self, format:) }
+ end
+
+ columns = [
+ ['ID', id.to_s],
+ ['Email', user_email_for(:display)],
+ ['FranceConnect ?', user_from_france_connect?]
+ ]
+
+ if procedure.for_individual?
+ columns += [
+ ['Civilité', individual&.gender],
+ ['Nom', individual&.nom],
+ ['Prénom', individual&.prenom],
+ ['Dépôt pour un tiers', :for_tiers],
+ ['Nom du mandataire', :mandataire_last_name],
+ ['Prénom du mandataire', :mandataire_first_name]
+ ]
+ if procedure.ask_birthday
+ columns += [['Date de naissance', individual&.birthdate]]
+ end
+ elsif with_etablissement
+ columns += [
+ ['Établissement SIRET', etablissement&.siret],
+ ['Établissement siège social', etablissement&.siege_social],
+ ['Établissement NAF', etablissement&.naf],
+ ['Établissement libellé NAF', etablissement&.libelle_naf],
+ ['Établissement Adresse', etablissement&.adresse],
+ ['Établissement numero voie', etablissement&.numero_voie],
+ ['Établissement type voie', etablissement&.type_voie],
+ ['Établissement nom voie', etablissement&.nom_voie],
+ ['Établissement complément adresse', etablissement&.complement_adresse],
+ ['Établissement code postal', etablissement&.code_postal],
+ ['Établissement localité', etablissement&.localite],
+ ['Établissement code INSEE localité', etablissement&.code_insee_localite],
+ ['Entreprise SIREN', etablissement&.entreprise_siren],
+ ['Entreprise capital social', etablissement&.entreprise_capital_social],
+ ['Entreprise numero TVA intracommunautaire', etablissement&.entreprise_numero_tva_intracommunautaire],
+ ['Entreprise forme juridique', etablissement&.entreprise_forme_juridique],
+ ['Entreprise forme juridique code', etablissement&.entreprise_forme_juridique_code],
+ ['Entreprise nom commercial', etablissement&.entreprise_nom_commercial],
+ ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale],
+ ['Entreprise SIRET siège social', etablissement&.entreprise_siret_siege_social],
+ ['Entreprise code effectif entreprise', etablissement&.entreprise_code_effectif_entreprise],
+ ['Entreprise date de création', etablissement&.entreprise_date_creation],
+ ['Entreprise état administratif', etablissement&.entreprise_etat_administratif],
+ ['Entreprise nom', etablissement&.entreprise_nom],
+ ['Entreprise prénom', etablissement&.entreprise_prenom],
+ ['Association RNA', etablissement&.association_rna],
+ ['Association titre', etablissement&.association_titre],
+ ['Association objet', etablissement&.association_objet],
+ ['Association date de création', etablissement&.association_date_creation],
+ ['Association date de déclaration', etablissement&.association_date_declaration],
+ ['Association date de publication', etablissement&.association_date_publication]
+ ]
+ else
+ columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
+ end
+ if procedure.chorusable? && procedure.chorus_configuration.complete?
+ columns += [
+ ['Domaine Fonctionnel', procedure.chorus_configuration.domaine_fonctionnel&.fetch("code") { '' }],
+ ['Référentiel De Programmation', procedure.chorus_configuration.referentiel_de_programmation&.fetch("code") { '' }],
+ ['Centre De Coût', procedure.chorus_configuration.centre_de_cout&.fetch("code") { '' }]
+ ]
+ end
+ columns += [
+ ['Archivé', :archived],
+ ['État du dossier', Dossier.human_attribute_name("state.#{state}")],
+ ['Dernière mise à jour le', :updated_at],
+ ['Dernière mise à jour du dossier le', :last_champ_updated_at],
+ ['Déposé le', :depose_at],
+ ['Passé en instruction le', :en_instruction_at],
+ procedure.sva_svr_enabled? ? ["Date décision #{procedure.sva_svr_configuration.human_decision}", :sva_svr_decision_on] : nil,
+ ['Traité le', :processed_at],
+ ['Motivation de la décision', :motivation],
+ ['Instructeurs', followers_instructeurs.map(&:email).join(' ')]
+ ].compact
+
+ if procedure.routing_enabled?
+ columns << ['Groupe instructeur', groupe_instructeur.label]
+ end
+
+ columns
+ end
+end
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index d4586e55d..3d4b9ab9b 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -13,6 +13,7 @@ class Dossier < ApplicationRecord
include DossierStateConcern
include DossierChampsConcern
include DossierEmptyConcern
+ include DossierExportConcern
enum state: {
brouillon: 'brouillon',
@@ -944,100 +945,6 @@ class Dossier < ApplicationRecord
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end
- def spreadsheet_columns_csv(types_de_champ:)
- spreadsheet_columns(with_etablissement: true, types_de_champ: types_de_champ)
- end
-
- def spreadsheet_columns_xlsx(types_de_champ:)
- spreadsheet_columns(types_de_champ: types_de_champ)
- end
-
- def spreadsheet_columns_ods(types_de_champ:)
- spreadsheet_columns(types_de_champ: types_de_champ)
- end
-
- def spreadsheet_columns(with_etablissement: false, types_de_champ:)
- columns = [
- ['ID', id.to_s],
- ['Email', user_email_for(:display)],
- ['FranceConnect ?', user_from_france_connect?]
- ]
-
- if procedure.for_individual?
- columns += [
- ['Civilité', individual&.gender],
- ['Nom', individual&.nom],
- ['Prénom', individual&.prenom],
- ['Dépôt pour un tiers', :for_tiers],
- ['Nom du mandataire', :mandataire_last_name],
- ['Prénom du mandataire', :mandataire_first_name]
- ]
- if procedure.ask_birthday
- columns += [['Date de naissance', individual&.birthdate]]
- end
- elsif with_etablissement
- columns += [
- ['Établissement SIRET', etablissement&.siret],
- ['Établissement siège social', etablissement&.siege_social],
- ['Établissement NAF', etablissement&.naf],
- ['Établissement libellé NAF', etablissement&.libelle_naf],
- ['Établissement Adresse', etablissement&.adresse],
- ['Établissement numero voie', etablissement&.numero_voie],
- ['Établissement type voie', etablissement&.type_voie],
- ['Établissement nom voie', etablissement&.nom_voie],
- ['Établissement complément adresse', etablissement&.complement_adresse],
- ['Établissement code postal', etablissement&.code_postal],
- ['Établissement localité', etablissement&.localite],
- ['Établissement code INSEE localité', etablissement&.code_insee_localite],
- ['Entreprise SIREN', etablissement&.entreprise_siren],
- ['Entreprise capital social', etablissement&.entreprise_capital_social],
- ['Entreprise numero TVA intracommunautaire', etablissement&.entreprise_numero_tva_intracommunautaire],
- ['Entreprise forme juridique', etablissement&.entreprise_forme_juridique],
- ['Entreprise forme juridique code', etablissement&.entreprise_forme_juridique_code],
- ['Entreprise nom commercial', etablissement&.entreprise_nom_commercial],
- ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale],
- ['Entreprise SIRET siège social', etablissement&.entreprise_siret_siege_social],
- ['Entreprise code effectif entreprise', etablissement&.entreprise_code_effectif_entreprise],
- ['Entreprise date de création', etablissement&.entreprise_date_creation],
- ['Entreprise état administratif', etablissement&.entreprise_etat_administratif],
- ['Entreprise nom', etablissement&.entreprise_nom],
- ['Entreprise prénom', etablissement&.entreprise_prenom],
- ['Association RNA', etablissement&.association_rna],
- ['Association titre', etablissement&.association_titre],
- ['Association objet', etablissement&.association_objet],
- ['Association date de création', etablissement&.association_date_creation],
- ['Association date de déclaration', etablissement&.association_date_declaration],
- ['Association date de publication', etablissement&.association_date_publication]
- ]
- else
- columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
- end
- if procedure.chorusable? && procedure.chorus_configuration.complete?
- columns += [
- ['Domaine Fonctionnel', procedure.chorus_configuration.domaine_fonctionnel&.fetch("code") { '' }],
- ['Référentiel De Programmation', procedure.chorus_configuration.referentiel_de_programmation&.fetch("code") { '' }],
- ['Centre De Coût', procedure.chorus_configuration.centre_de_cout&.fetch("code") { '' }]
- ]
- end
- columns += [
- ['Archivé', :archived],
- ['État du dossier', Dossier.human_attribute_name("state.#{state}")],
- ['Dernière mise à jour le', :updated_at],
- ['Dernière mise à jour du dossier le', :last_champ_updated_at],
- ['Déposé le', :depose_at],
- ['Passé en instruction le', :en_instruction_at],
- procedure.sva_svr_enabled? ? ["Date décision #{procedure.sva_svr_configuration.human_decision}", :sva_svr_decision_on] : nil,
- ['Traité le', :processed_at],
- ['Motivation de la décision', :motivation],
- ['Instructeurs', followers_instructeurs.map(&:email).join(' ')]
- ].compact
-
- if procedure.routing_enabled?
- columns << ['Groupe instructeur', groupe_instructeur.label]
- end
- columns + champs_for_export(types_de_champ)
- end
-
def linked_dossiers_for(instructeur_or_expert)
dossier_ids = filled_champs.filter(&:dossier_link?).filter_map(&:value)
instructeur_or_expert.dossiers.where(id: dossier_ids)
diff --git a/app/models/export_template.rb b/app/models/export_template.rb
index 1a6f070d7..0328e16b4 100644
--- a/app/models/export_template.rb
+++ b/app/models/export_template.rb
@@ -9,12 +9,14 @@ class ExportTemplate < ApplicationRecord
has_one :procedure, through: :groupe_instructeur
has_many :exports, dependent: :nullify
- enum kind: { zip: "zip" }, _prefix: :template
+ enum kind: { zip: 'zip', csv: 'csv', xlsx: 'xlsx', ods: 'ods' }, _prefix: :template
attribute :dossier_folder, :export_item
attribute :export_pdf, :export_item
attribute :pjs, :export_item, array: true
+ attribute :exported_columns, :exported_column, array: true
+
before_validation :ensure_pjs_are_legit
validates_with ExportTemplateValidator
@@ -28,6 +30,7 @@ class ExportTemplate < ApplicationRecord
end
def self.default(name: nil, kind: 'zip', groupe_instructeur:)
+ # TODO: remove default values for tabular export
dossier_folder = ExportItem.default(prefix: 'dossier')
export_pdf = ExportItem.default(prefix: 'export')
pjs = groupe_instructeur.procedure.exportables_pieces_jointes.map { |tdc| ExportItem.default_pj(tdc) }
@@ -35,6 +38,10 @@ class ExportTemplate < ApplicationRecord
new(name:, kind:, groupe_instructeur:, dossier_folder:, export_pdf:, pjs:)
end
+ def tabular?
+ kind != 'zip'
+ end
+
def tags
tags_categorized.slice(:individual, :etablissement, :dossier).values.flatten
end
@@ -58,6 +65,19 @@ class ExportTemplate < ApplicationRecord
File.join(dossier_folder.path(dossier), file_path) if file_path.present?
end
+ def dossier_exported_columns = exported_columns.filter { _1.column.dossier_column? }
+
+ def columns_for_stable_id(stable_id)
+ exported_columns
+ .filter { _1.column.champ_column? }
+ .filter { _1.column.stable_id == stable_id }
+ end
+
+ def in_export?(exported_column)
+ @template_exported_columns ||= exported_columns.map(&:column)
+ @template_exported_columns.include?(exported_column.column)
+ end
+
private
def ensure_pjs_are_legit
diff --git a/app/models/exported_column.rb b/app/models/exported_column.rb
new file mode 100644
index 000000000..651407a71
--- /dev/null
+++ b/app/models/exported_column.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class ExportedColumn
+ attr_reader :column, :libelle
+
+ def initialize(column:, libelle:)
+ @column = column
+ @libelle = libelle
+ end
+
+ def id = { id: column.id, libelle: }.to_json
+
+ def libelle_with_value(champ_or_dossier, format:)
+ [libelle, ExportedColumnFormatter.format(column:, champ_or_dossier:, format:), spreadsheet_architect_type]
+ end
+
+ def spreadsheet_architect_type
+ case @column.type
+ when :boolean
+ :boolean
+ when :decimal, :integer
+ :float
+ when :datetime
+ :time
+ when :date
+ :date
+ else
+ :string
+ end
+ end
+end
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index 05c3e3346..90344edc9 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -71,10 +71,11 @@ class Procedure < ApplicationRecord
brouillon? ? draft_revision : published_revision
end
- def all_revisions_types_de_champ(parent: nil)
+ def all_revisions_types_de_champ(parent: nil, with_header_section: false)
+ types_de_champ_scope = with_header_section ? TypeDeChamp.with_header_section : TypeDeChamp.fillable
if brouillon?
if parent.nil?
- TypeDeChamp.fillable
+ types_de_champ_scope
.joins(:revision_types_de_champ)
.where(revision_types_de_champ: { revision_id: draft_revision_id, parent_id: nil })
.order(:private, :position)
@@ -82,8 +83,8 @@ class Procedure < ApplicationRecord
draft_revision.children_of(parent)
end
else
- cache_key = ['all_revisions_types_de_champ', published_revision, parent].compact
- Rails.cache.fetch(cache_key, expires_in: 1.month) { published_revisions_types_de_champ(parent) }
+ cache_key = ['all_revisions_types_de_champ', published_revision, parent, with_header_section].compact
+ Rails.cache.fetch(cache_key, expires_in: 1.month) { published_revisions_types_de_champ(parent:, with_header_section:) }
end
end
@@ -875,7 +876,7 @@ class Procedure < ApplicationRecord
@stable_ids_used_by_routing_rules ||= groupe_instructeurs.flat_map { _1.routing_rule&.sources }.compact.uniq
end
- def published_revisions_types_de_champ(parent = nil)
+ def published_revisions_types_de_champ(parent: nil, with_header_section: false)
# all published revisions
revision_ids = revisions.ids - [draft_revision_id]
# fetch all parent types de champ
@@ -889,8 +890,8 @@ class Procedure < ApplicationRecord
# fetch all type_de_champ.stable_id for all the revisions expect draft
# and for each stable_id take the bigger (more recent) type_de_champ.id
- recent_ids = TypeDeChamp
- .fillable
+ types_de_champ_scope = with_header_section ? TypeDeChamp.with_header_section : TypeDeChamp.fillable
+ recent_ids = types_de_champ_scope
.joins(:revision_types_de_champ)
.where(revision_types_de_champ: { revision_id: revision_ids, parent_id: parent_ids })
.group(:stable_id).select('MAX(types_de_champ.id)')
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index 9a4b32d99..0403ac864 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -24,7 +24,6 @@ class TypeDeChamp < ApplicationRecord
TYPE_DE_CHAMP_TO_CATEGORIE = {
engagement_juridique: REFERENTIEL_EXTERNE,
-
header_section: STRUCTURE,
repetition: STRUCTURE,
dossier_link: STRUCTURE,
@@ -171,6 +170,7 @@ class TypeDeChamp < ApplicationRecord
scope :not_repetition, -> { where.not(type_champ: type_champs.fetch(:repetition)) }
scope :not_condition, -> { where(condition: nil) }
scope :fillable, -> { where.not(type_champ: [type_champs.fetch(:header_section), type_champs.fetch(:explication)]) }
+ scope :with_header_section, -> { where.not(type_champ: TypeDeChamp.type_champs[:explication]) }
scope :dubious, -> {
where("unaccent(types_de_champ.libelle) ~* unaccent(?)", DubiousProcedure.forbidden_regexp)
diff --git a/app/models/types_de_champ/commune_type_de_champ.rb b/app/models/types_de_champ/commune_type_de_champ.rb
index 3f2766d6c..2f3d639df 100644
--- a/app/models/types_de_champ/commune_type_de_champ.rb
+++ b/app/models/types_de_champ/commune_type_de_champ.rb
@@ -27,6 +27,31 @@ class TypesDeChamp::CommuneTypeDeChamp < TypesDeChamp::TypeDeChampBase
champ.code_postal? ? "#{champ.name} (#{champ.code_postal})" : champ.name
end
+ def columns(procedure:, displayable: true, prefix: nil)
+ super.concat(
+ [
+ Columns::JSONPathColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: "#{libelle_with_prefix(prefix)} - code postal (5 chiffres)",
+ jsonpath: '$.code_postal',
+ displayable:,
+ type: :text
+ ),
+ Columns::JSONPathColumn.new(
+ procedure_id: procedure.id,
+ stable_id:,
+ tdc_type: type_champ,
+ label: "#{libelle_with_prefix(prefix)} - département",
+ jsonpath: '$.code_departement',
+ displayable:,
+ type: :number
+ )
+ ]
+ )
+ end
+
private
def paths
diff --git a/app/models/types_de_champ/repetition_type_de_champ.rb b/app/models/types_de_champ/repetition_type_de_champ.rb
index 043294933..7470162fd 100644
--- a/app/models/types_de_champ/repetition_type_de_champ.rb
+++ b/app/models/types_de_champ/repetition_type_de_champ.rb
@@ -26,9 +26,11 @@ class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
end
def columns(procedure:, displayable: nil, prefix: nil)
+ prefix = prefix.present? ? "(#{prefix} #{libelle})" : libelle
+
procedure
.all_revisions_types_de_champ(parent: @type_de_champ)
- .flat_map { _1.columns(procedure:, displayable: false, prefix: libelle) }
+ .flat_map { _1.columns(procedure:, displayable: false, prefix:) }
end
def champ_blank?(champ) = champ.dossier.repetition_row_ids(@type_de_champ).blank?
diff --git a/app/services/dossier_filter_service.rb b/app/services/dossier_filter_service.rb
index a322dc8dd..ce32fefc2 100644
--- a/app/services/dossier_filter_service.rb
+++ b/app/services/dossier_filter_service.rb
@@ -81,7 +81,7 @@ class DossierFilterService
else
case table
when 'self'
- if filtered_column.type == :date
+ if filtered_column.type == :date || filtered_column.type == :datetime
dates = values
.filter_map { |v| Time.zone.parse(v).beginning_of_day rescue nil }
diff --git a/app/services/exported_column_formatter.rb b/app/services/exported_column_formatter.rb
new file mode 100644
index 000000000..824b26d29
--- /dev/null
+++ b/app/services/exported_column_formatter.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+class ExportedColumnFormatter
+ def self.format(column:, champ_or_dossier:, format:)
+ return if champ_or_dossier.nil?
+
+ raw_value = column.value(champ_or_dossier)
+
+ case column.type
+ when :boolean
+ format_boolean(column:, raw_value:, format:)
+ when :attachements
+ format_attachments(column:, raw_value:)
+ when :enum
+ format_enum(column:, raw_value:)
+ when :enums
+ format_enums(column:, raw_values: raw_value)
+ else
+ raw_value
+ end
+ end
+
+ private
+
+ def self.format_boolean(column:, raw_value:, format:)
+ if format == :ods
+ raw_value ? 1 : 0
+ else
+ raw_value
+ end
+ end
+
+ def self.format_attachments(column:, raw_value:)
+ case column.tdc_type
+ when TypeDeChamp.type_champs[:titre_identite]
+ raw_value.present? ? 'présent' : 'absent'
+ when TypeDeChamp.type_champs[:piece_justificative]
+ raw_value.map { _1.blob.filename }.join(", ")
+ end
+ end
+
+ def self.format_enums(column:, raw_values:)
+ raw_values.map { format_enum(column:, raw_value: _1) }.join(', ')
+ end
+
+ def self.format_enum(column:, raw_value:)
+ # options for select store ["trad", :enum_value]
+ selected_option = column.options_for_select.find { _1[1].to_s == raw_value }
+
+ selected_option ? selected_option.first : raw_value
+ end
+end
diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb
index 9482cc045..181352d5c 100644
--- a/app/services/procedure_export_service.rb
+++ b/app/services/procedure_export_service.rb
@@ -18,7 +18,7 @@ class ProcedureExportService
def to_xlsx
@dossiers = @dossiers.downloadable_sorted_batch
- tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
+ tables = [:dossiers, :etablissements, :avis] + champs_repetables_options(format: :xlsx)
# We recursively build multi page spreadsheet
io = tables.reduce(nil) do |package, table|
@@ -29,7 +29,7 @@ class ProcedureExportService
def to_ods
@dossiers = @dossiers.downloadable_sorted_batch
- tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
+ tables = [:dossiers, :etablissements, :avis] + champs_repetables_options(format: :ods)
# We recursively build multi page spreadsheet
io = StringIO.new(tables.reduce(nil) do |spreadsheet, table|
@@ -103,7 +103,7 @@ class ProcedureExportService
@avis ||= dossiers.flat_map(&:avis)
end
- def champs_repetables_options
+ def champs_repetables_options(format:)
procedure
.all_revisions_types_de_champ
.repetition
@@ -115,7 +115,7 @@ class ProcedureExportService
{
sheet_name: type_de_champ_repetition.libelle_for_export,
instances: rows,
- spreadsheet_columns: Proc.new { |instance| instance.spreadsheet_columns(types_de_champ) }
+ spreadsheet_columns: Proc.new { |instance| instance.spreadsheet_columns(types_de_champ, export_template: @export_template, format:) }
}
end
end
@@ -152,7 +152,7 @@ class ProcedureExportService
types_de_champ = procedure.types_de_champ_for_procedure_export.to_a
Proc.new do |instance|
- instance.send(:"spreadsheet_columns_#{format}", types_de_champ: types_de_champ)
+ instance.send(:"spreadsheet_columns_#{format}", types_de_champ: types_de_champ, export_template: @export_template)
end
end
end
diff --git a/app/types/export_item_type.rb b/app/types/export_item_type.rb
index e2d1f7014..04c37c6ef 100644
--- a/app/types/export_item_type.rb
+++ b/app/types/export_item_type.rb
@@ -29,7 +29,10 @@ class ExportItemType < ActiveRecord::Type::Value
# ruby -> db
def serialize(value)
- if value.is_a?(ExportItem)
+ case value
+ in NilClass
+ nil
+ in ExportItem
JSON.generate({
template: value.template,
enabled: value.enabled,
diff --git a/app/types/exported_column_type.rb b/app/types/exported_column_type.rb
new file mode 100644
index 000000000..dddd35532
--- /dev/null
+++ b/app/types/exported_column_type.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class ExportedColumnType < ActiveRecord::Type::Value
+ # form_input or setter -> type
+ def cast(value)
+ value = value.deep_symbolize_keys if value.respond_to?(:deep_symbolize_keys)
+
+ case value
+ in ExportedColumn
+ value
+ in NilClass # default value
+ nil
+ # from db
+ in { id: String|Hash, libelle: String } => h
+ ExportedColumn.new(column: ColumnType.new.cast(h[:id]), libelle: h[:libelle])
+ # from form
+ in String
+ h = JSON.parse(value).deep_symbolize_keys
+ ExportedColumn.new(column: ColumnType.new.cast(h[:id]), libelle: h[:libelle])
+ end
+ end
+
+ # db -> ruby
+ def deserialize(value) = cast(value&.then { JSON.parse(_1) })
+
+ # ruby -> db
+ def serialize(value)
+ case value
+ in NilClass
+ nil
+ in ExportedColumn
+ JSON.generate({
+ id: value.column.h_id,
+ libelle: value.libelle
+ })
+ else
+ raise ArgumentError, "Invalid value for ExportedColumn serialization: #{value}"
+ end
+ end
+end
diff --git a/app/validators/export_template_validator.rb b/app/validators/export_template_validator.rb
index 51dc51141..51099b714 100644
--- a/app/validators/export_template_validator.rb
+++ b/app/validators/export_template_validator.rb
@@ -2,6 +2,8 @@
class ExportTemplateValidator < ActiveModel::Validator
def validate(export_template)
+ return if !export_template.template_zip?
+
validate_all_templates(export_template)
return if export_template.errors.any? # no need to continue if the templates are invalid
diff --git a/app/views/administrateurs/archives/index.html.haml b/app/views/administrateurs/archives/index.html.haml
index b50059d6c..86cc6465e 100644
--- a/app/views/administrateurs/archives/index.html.haml
+++ b/app/views/administrateurs/archives/index.html.haml
@@ -4,11 +4,12 @@
['Export et Archives']] }
-.container
- %h1.mb-2
+.container.flex
+ %h1.mb-2.mr-2
Archives
-# index not renderable as administrateur flagged as manager, so render it anyway
- = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_admin_procedure_exports_path))
+ = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_admin_procedure_exports_path), show_export_template_tab: false)
+.container
= render Dossiers::ExportLinkComponent.new(procedure: @procedure, exports: @exports, export_url: method(:download_admin_procedure_exports_path))
= render partial: "shared/archives/notice"
diff --git a/app/views/instructeurs/export_templates/_checkbox_group.html.haml b/app/views/instructeurs/export_templates/_checkbox_group.html.haml
new file mode 100644
index 000000000..80626d857
--- /dev/null
+++ b/app/views/instructeurs/export_templates/_checkbox_group.html.haml
@@ -0,0 +1,14 @@
+%fieldset.fr-fieldset{ id: "#{title.parameterize}-fieldset", data: { controller: 'checkbox-select-all' } }
+ %legend.fr-fieldset__legend--regular.fr-fieldset__legend.fr-h5.fr-pb-0
+ = title
+
+ .checkbox-group-bordered.fr-mx-1w.fr-mb-2w
+ .fr-fieldset__element.fr-background-contrast--grey.fr-py-2w.fr-px-4w
+ .fr-checkbox-group
+ = check_box_tag "#{title.parameterize}-select-all", "select-all", false, data: { "checkbox-select-all-target": 'checkboxAll' }
+ = label_tag "#{title.parameterize}-select-all", "Tout sélectionner"
+
+ - all_columns.each do |column|
+ .fr-fieldset__element.fr-px-4w
+ .fr-checkbox-group
+ = render ExportTemplate::CheckboxComponent.new(export_template:, exported_column: ExportedColumn.new(libelle: column.label, column:))
diff --git a/app/views/instructeurs/export_templates/_form_tabular.html.haml b/app/views/instructeurs/export_templates/_form_tabular.html.haml
new file mode 100644
index 000000000..36712f6f0
--- /dev/null
+++ b/app/views/instructeurs/export_templates/_form_tabular.html.haml
@@ -0,0 +1,57 @@
+#export_template-edit.fr-my-4w
+ .fr-mb-6w
+ = render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur de modèle d'export", heading_level: 'h3') do |c|
+ - c.with_body do
+ = t('.info_html', mailto: mail_to(CONTACT_EMAIL, subject: 'Editeur de modèle d\'export'))
+
+.fr-grid-row.fr-grid-row--gutters
+ .fr-col-12.fr-col-md-8
+ = form_with model: [:instructeur, @procedure, export_template], local: true do |f|
+
+ %h2 Paramètres de l'export
+ = f.hidden_field "[dossier_folder][template]", value: export_template.dossier_folder.template_json
+ = f.hidden_field "[export_pdf][template]", value: export_template.export_pdf.template_json
+
+ = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field)
+
+ - if groupe_instructeurs.many?
+ .fr-input-group
+ = f.label :groupe_instructeur_id, class: 'fr-label' do
+ = f.object.class.human_attribute_name(:groupe_instructeur_id)
+ = render EditableChamp::AsteriskMandatoryComponent.new
+ %span.fr-hint-text
+ Avec quel groupe instructeur souhaitez-vous partager ce modèle d'export ?
+ = f.collection_select :groupe_instructeur_id, groupe_instructeurs, :id, :label, {}, class: 'fr-select'
+ - else
+ = f.hidden_field :groupe_instructeur_id
+
+ %fieldset.fr-fieldset.fr-fieldset--inline
+ %legend#radio-inline-legend.fr-fieldset__legend.fr-text--regular
+ Format export
+ = asterisk
+ .fr-fieldset__element.fr-fieldset__element--inline
+ .fr-radio-group
+ = f.radio_button :kind, "xlsx", id: "xlsx"
+ %label.fr-label{ for: "xlsx" } xlsx
+ .fr-radio-group
+ = f.radio_button :kind, "ods", id: "ods"
+ %label.fr-label{ for: "ods" } ods
+ .fr-radio-group
+ = f.radio_button :kind, "csv", id: "csv"
+ %label.fr-label{ for: "csv" } csv
+
+ %h2 Contenu de l'export
+ %p Sélectionnez les colonnes que vous souhaitez voir affichées dans le tableau de votre export.
+
+ = render partial: 'checkbox_group', locals: { title: 'Informations usager', all_columns: @export_template.procedure.usager_columns_for_export, export_template: @export_template }
+ = render partial: 'checkbox_group', locals: { title: 'Informations dossier', all_columns: @export_template.procedure.dossier_columns_for_export, export_template: @export_template }
+ = render ExportTemplate::ChampsComponent.new("Formulaire usager", @export_template, @types_de_champ_public)
+ = render ExportTemplate::ChampsComponent.new("Annotations privées", @export_template, @types_de_champ_private) if @types_de_champ_private.any?
+
+ .fixed-footer
+ .fr-container
+ %ul.fr-btns-group.fr-btns-group--inline-md
+ %li
+ = link_to "Annuler", instructeur_procedure_path(@procedure), class: "fr-btn fr-btn--secondary"
+ %li
+ = f.submit "Enregistrer", class: "fr-btn", data: @export_template.persisted? ? { confirm: t('.warning') } : {}
diff --git a/app/views/instructeurs/export_templates/edit.html.haml b/app/views/instructeurs/export_templates/edit.html.haml
index ab4c5f8c7..32a80d8d6 100644
--- a/app/views/instructeurs/export_templates/edit.html.haml
+++ b/app/views/instructeurs/export_templates/edit.html.haml
@@ -4,4 +4,7 @@
.fr-container
%h1 Mise à jour modèle d'export
- = render partial: 'form', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }
+ - if @export_template.tabular?
+ = render partial: 'form_tabular', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }
+ - else
+ = render partial: 'form', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }
diff --git a/app/views/instructeurs/export_templates/new.html.haml b/app/views/instructeurs/export_templates/new.html.haml
index 10962b1cd..358bab190 100644
--- a/app/views/instructeurs/export_templates/new.html.haml
+++ b/app/views/instructeurs/export_templates/new.html.haml
@@ -3,4 +3,7 @@
[t('.title')]] }
.fr-container
%h1 Nouveau modèle d'export
- = render partial: 'form', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }
+ - if @export_template.tabular?
+ = render partial: 'form_tabular', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }
+ - else
+ = render partial: 'form', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }
diff --git a/app/views/instructeurs/procedures/exports.html.haml b/app/views/instructeurs/procedures/exports.html.haml
index e63eb4472..3e92cd894 100644
--- a/app/views/instructeurs/procedures/exports.html.haml
+++ b/app/views/instructeurs/procedures/exports.html.haml
@@ -6,41 +6,59 @@
[t('.title')]] }
.fr-container
- %h1= t('.title')
- = render Dsfr::CalloutComponent.new(title: nil) do |c|
- - c.with_body do
- %p= t('.export_description', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i)
+ .fr-tabs.mb-3
+ %ul.fr-tabs__list{ role: 'tablist' }
+ %li{ role: 'presentation' }
+ %button.fr-tabs__tab.fr-tabs__tab--icon-left{ id: "tabpanel-exports", tabindex: "0", role: "tab", "aria-selected": "true", "aria-controls": "tabpanel-exports-panel" } Liste des exports
+ %li{ role: 'presentation' }
+ %button.fr-tabs__tab.fr-tabs__tab--icon-left{ id: "tabpanel-export-templates", tabindex: "-1", role: "tab", "aria-selected": "false", "aria-controls": "tabpanel-export-templates-panel" } Modèles d'export
- - if @exports.present?
- %div{ data: @exports.any?(&:pending?) ? { controller: "turbo-poll", turbo_poll_url_value: "", turbo_poll_interval_value: 10_000, turbo_poll_max_checks_value: 6 } : {} }
- = render Dossiers::ExportLinkComponent.new(procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
-
- - if @exports.any?{_1.format == Export.formats.fetch(:zip)}
- = render Dsfr::AlertComponent.new(title: t('.title_zip'), state: :info, extra_class_names: 'fr-mb-3w') do |c|
+ .fr-tabs__panel.fr-tabs__panel--selected{ id: "tabpanel-exports-panel", role: "tabpanel", "aria-labelledby": "tabpanel-exports", tabindex: "0" }
+ = render Dsfr::CalloutComponent.new(title: nil) do |c|
- c.with_body do
- %p= t('.export_description_zip_html')
+ %p= t('.export_description', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i)
- - else
- = t('.no_export_html', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i )
+ - if @exports.present?
+ %div{ data: @exports.any?(&:pending?) ? { controller: "turbo-poll", turbo_poll_url_value: "", turbo_poll_interval_value: 10_000, turbo_poll_max_checks_value: 6 } : {} }
+ = render Dossiers::ExportLinkComponent.new(procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
- - if @procedure.feature_enabled?(:export_template)
- %h2.fr-mb-1w.fr-mt-8w
- Liste des modèles d'export
- %p.fr-hint-text
- Un modèle d'export permet de personnaliser le nom des fichiers (pour un export au format Zip)
- - if @export_templates.any?
- .fr-table.fr-table--no-caption.fr-mt-5w
- %table
- %thead
- %tr
- %th{ scope: 'col' } Nom du modèle
- %th{ scope: 'col' }= "Groupe instructeur" if @procedure.groupe_instructeurs.many?
- %tbody
- - @export_templates.each do |export_template|
- %tr
- %td= link_to export_template.name, [:edit, :instructeur, @procedure, export_template]
- %td= export_template.groupe_instructeur.label if @procedure.groupe_instructeurs.many?
+ - if @exports.any?{_1.format == Export.formats.fetch(:zip)}
+ = render Dsfr::AlertComponent.new(title: t('.title_zip'), state: :info, extra_class_names: 'fr-mb-3w') do |c|
+ - c.with_body do
+ %p= t('.export_description_zip_html')
- %p
- = link_to [:new, :instructeur, @procedure, :export_template], class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line' do
- Ajouter un modèle d'export
+ - else
+ = t('.no_export_html', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i )
+
+ .fr-tabs__panel.fr-tabs__panel{ id: "tabpanel-export-templates-panel", role: "tabpanel", "aria-labelledby": "tabpanel-export-templates", tabindex: "0" }
+ = render Dsfr::AlertComponent.new(state: :info) do |c|
+ - c.with_body do
+ %p= t('.export_template_list_description_html')
+
+
+ .fr-mt-5w
+ = link_to t('.new_zip_export_template'), new_instructeur_procedure_export_template_path(@procedure), class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line fr-mr-1w"
+ = link_to t('.new_tabular_export_template'), new_instructeur_procedure_export_template_path(@procedure, kind: 'tabular'), class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line"
+
+ .fr-table.fr-table--bordered.fr-table--no-caption.fr-mt-5w
+ .fr-table__wrapper
+ .fr-table__container
+ .fr-table__content
+ %table
+ %thead
+ %tr
+ = tag.th "Nom du modèle", scope: 'col'
+ = tag.th "Format", scope: 'col'
+ = tag.th "Date de création", scope: 'col'
+ = tag.th "Partagé avec (groupe instructeurs)", scope: 'col' if @procedure.groupe_instructeurs.many?
+ = tag.th "Actions", scope: 'col'
+ %tbody
+ - @export_templates.each do |export_template|
+ %tr
+ %td= link_to export_template.name, [:edit, :instructeur, @procedure, export_template]
+ %td= pretty_kind(export_template.kind)
+ %td= l(export_template.created_at)
+ = tag.td export_template.groupe_instructeur.label if @procedure.groupe_instructeurs.many?
+ %td
+ = link_to "Modifier", [:edit, :instructeur, @procedure, export_template], class: "fr-btn fr-btn--icon-left fr-icon-edit-line fr-mr-1w"
+ = link_to "Supprimer", [:instructeur, @procedure, export_template], method: :delete, data: { confirm: "Voulez-vous vraiment supprimer ce modèle ? Il sera supprimé pour tous les instructeurs du groupe"}, class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-delete-line"
diff --git a/config/initializers/types.rb b/config/initializers/types.rb
index 46f40f05d..ecdf990d7 100644
--- a/config/initializers/types.rb
+++ b/config/initializers/types.rb
@@ -4,10 +4,12 @@ require Rails.root.join("app/types/column_type")
require Rails.root.join("app/types/export_item_type")
require Rails.root.join("app/types/sorted_column_type")
require Rails.root.join("app/types/filtered_column_type")
+require Rails.root.join("app/types/exported_column_type")
ActiveSupport.on_load(:active_record) do
ActiveRecord::Type.register(:column, ColumnType)
ActiveRecord::Type.register(:export_item, ExportItemType)
ActiveRecord::Type.register(:sorted_column, SortedColumnType)
ActiveRecord::Type.register(:filtered_column, FilteredColumnType)
+ ActiveRecord::Type.register(:exported_column, ExportedColumnType)
end
diff --git a/config/locales/models/export_templates/en.yml b/config/locales/models/export_templates/en.yml
index 1952e0bc0..bcc3ba867 100644
--- a/config/locales/models/export_templates/en.yml
+++ b/config/locales/models/export_templates/en.yml
@@ -17,3 +17,12 @@ en:
dossier_number_required: "must contain dossier's number"
different_templates: "Files must have different names"
invalid_template: "A file name is invalid"
+ base:
+ invalid: "is invalid"
+ instructeurs:
+ export_templates:
+ form_tabular:
+ info_html: |
+ This page allows you to edit a tabular export template and select fields that you want to export.
+ Try it and let us know what you think by sending an e-mail to %{mailto}.
+ warning: If you modify this template, it will also be modified for all instructors who have access to this template.
diff --git a/config/locales/models/export_templates/fr.yml b/config/locales/models/export_templates/fr.yml
index 9d152bbc5..f2100fe33 100644
--- a/config/locales/models/export_templates/fr.yml
+++ b/config/locales/models/export_templates/fr.yml
@@ -17,3 +17,13 @@ fr:
dossier_number_required: doit contenir le numéro du dossier
different_templates: Les fichiers doivent avoir des noms différents
invalid_template: Un nom de fichier est invalide
+ base:
+ invalid: "est invalide"
+ instructeurs:
+ export_templates:
+ form_tabular:
+ info_html: |
+ Cette page permet d'éditer un modèle d'export tabulaire et ainsi sélectionner les champs que vous souhaitez exporter.
+ Essayez-le et donnez-nous votre avis
+ en nous envoyant un email à %{mailto}.
+ warning: Si vous modifiez ce modèle, il sera également modifié pour tous les instructeurs qui ont accès à ce modèle.
diff --git a/config/locales/views/instructeurs/header/en.yml b/config/locales/views/instructeurs/header/en.yml
index dbec9594d..ad343adb4 100644
--- a/config/locales/views/instructeurs/header/en.yml
+++ b/config/locales/views/instructeurs/header/en.yml
@@ -12,7 +12,7 @@ en:
button_delay_expiration: "Keep for one more month"
notification_management: notification management
administrators_list: administrators list
- exports_list: exports list
+ exports_list: exports and export templates
exports_notification_label: A new export is ready to download
statistics: statistics
instructeurs: instructors
diff --git a/config/locales/views/instructeurs/header/fr.yml b/config/locales/views/instructeurs/header/fr.yml
index c13a4ded0..46b2d77c1 100644
--- a/config/locales/views/instructeurs/header/fr.yml
+++ b/config/locales/views/instructeurs/header/fr.yml
@@ -13,7 +13,7 @@ fr:
button_delay_expiration: "Conserver un mois de plus"
notification_management: Gestion des notifications
administrators_list: Voir les administrateurs
- exports_list: Voir les exports
+ exports_list: Voir les exports et modèles d'export
exports_notification_label: Un nouvel export est prêt à être téléchargé
statistics: Statistiques
instructeurs: instructeurs
diff --git a/config/locales/views/instructeurs/procedures/exports/en.yml b/config/locales/views/instructeurs/procedures/exports/en.yml
index 81b0dcf8e..f1f7bc753 100644
--- a/config/locales/views/instructeurs/procedures/exports/en.yml
+++ b/config/locales/views/instructeurs/procedures/exports/en.yml
@@ -18,3 +18,9 @@ en:
no_export_html: You have no export at the moment.
Can't find an export? It may have expired, exports are deleted after %{expiration_time} hours.
+
+ export_template_list_description_html: |
+ Each instructor can configure an export template to customize exports (attachments name for a zip export, columns selection for a tabular export). It will be made available to all instructors assigned to the procedure.
+ Find out more about export template configuration
+ new_zip_export_template: Create zip export template
+ new_tabular_export_template: Create tabular export template
diff --git a/config/locales/views/instructeurs/procedures/exports/fr.yml b/config/locales/views/instructeurs/procedures/exports/fr.yml
index 030fa8345..fa0945652 100644
--- a/config/locales/views/instructeurs/procedures/exports/fr.yml
+++ b/config/locales/views/instructeurs/procedures/exports/fr.yml
@@ -17,3 +17,8 @@ fr:
Vous n'arrivez pas à extraire un export au format .zip sur un réseau d'entreprise ? Essayer de renommer l'archive avec un nom plus court et ré-essayer de l'extraire.
no_export_html: Vous n'avez pas d'export pour le moment.
Vous ne trouvez pas un export ? Il a peut-être expiré, les exports sont supprimés au bout de %{expiration_time} heures.
+ export_template_list_description_html: |
+ Chaque instructeur a la possibilité de configurer un modèle d'export pour personnaliser les exports (nom des pièces jointes pour un export au format zip, sélection des colonnes pour un export tabulaire). Il sera mis à disposition de l'ensemble des instructeurs affectés à la démarche
+ En savoir plus sur la configuration des modèles d'export
+ new_zip_export_template: Créer un modèle d'export zip
+ new_tabular_export_template: Créer un modèle d'export tabulaire
diff --git a/db/migrate/20241015125024_add_columns_to_export_template.rb b/db/migrate/20241015125024_add_columns_to_export_template.rb
new file mode 100644
index 000000000..2e9e01dc8
--- /dev/null
+++ b/db/migrate/20241015125024_add_columns_to_export_template.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddColumnsToExportTemplate < ActiveRecord::Migration[7.0]
+ def change
+ add_column :export_templates, :exported_columns, :jsonb, array: true, default: [], null: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ca309c9b4..8da54cc84 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -626,6 +626,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_11_12_090128) do
t.datetime "created_at", null: false
t.jsonb "dossier_folder", null: false
t.jsonb "export_pdf", null: false
+ t.jsonb "exported_columns", default: [], null: false, array: true
t.bigint "groupe_instructeur_id", null: false
t.string "kind", null: false
t.string "name", null: false
diff --git a/spec/components/export_template/champs_component_spec.rb b/spec/components/export_template/champs_component_spec.rb
new file mode 100644
index 000000000..23baff354
--- /dev/null
+++ b/spec/components/export_template/champs_component_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+describe ExportTemplate::ChampsComponent, type: :component do
+ let(:groupe_instructeur) { create(:groupe_instructeur, procedure:) }
+ let(:export_template) { build(:export_template, kind: 'csv', groupe_instructeur:) }
+ let(:procedure) { create(:procedure_with_dossiers, :published, types_de_champ_public:, for_individual:) }
+ let(:for_individual) { true }
+ let(:types_de_champ_public) do
+ [
+ { type: :text, libelle: "Ca va ?", mandatory: true, stable_id: 1 },
+ { type: :communes, libelle: "Commune", mandatory: true, stable_id: 17 },
+ { type: :siret, libelle: 'Siret', stable_id: 20 },
+ { type: :repetition, mandatory: true, stable_id: 7, libelle: "Amis", children: [{ type: 'text', libelle: 'Prénom', stable_id: 8 }] }
+ ]
+ end
+ let(:component) { described_class.new("Champs publics", export_template, procedure.all_revisions_types_de_champ(parent: nil, with_header_section: true)) }
+ before { render_inline(component).to_html }
+
+ it 'renders champs within fieldset' do
+ procedure
+ expect(page).to have_unchecked_field "Ca va ?"
+ expect(page).to have_unchecked_field "Commune"
+ expect(page).to have_unchecked_field "Siret"
+ expect(page).to have_unchecked_field "(Bloc répétable Amis) – Prénom"
+ end
+end
diff --git a/spec/controllers/instructeurs/export_templates_controller_spec.rb b/spec/controllers/instructeurs/export_templates_controller_spec.rb
index 4576090fc..32fba6ba8 100644
--- a/spec/controllers/instructeurs/export_templates_controller_spec.rb
+++ b/spec/controllers/instructeurs/export_templates_controller_spec.rb
@@ -86,6 +86,42 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
expect(ExportTemplate.last.pjs).to match_array([])
end
end
+
+ context 'with tabular params' do
+ let(:procedure) do
+ create(
+ :procedure, instructeurs: [instructeur],
+ types_de_champ_public: [{ type: :text, libelle: 'un texte', stable_id: 1 }]
+ )
+ end
+
+ let(:exported_columns) do
+ [
+ { id: procedure.find_column(label: 'Demandeur').id, libelle: 'Demandeur' },
+ { id: procedure.find_column(label: 'Date du dernier évènement').id, libelle: 'Date du dernier évènement' }
+ ].map(&:to_json)
+ end
+
+ let(:create_params) do
+ {
+ name: "ExportODS",
+ kind: "ods",
+ groupe_instructeur_id: groupe_instructeur.id,
+ export_pdf: item_params(text: "export"),
+ dossier_folder: item_params(text: "dossier"),
+ exported_columns:
+ }
+ end
+
+ context 'with valid params' do
+ it 'redirect to some page' do
+ subject
+ expect(response).to redirect_to(exports_instructeur_procedure_path(procedure))
+ expect(flash.notice).to eq "Le modèle d'export ExportODS a bien été créé"
+ expect(ExportTemplate.last.exported_columns.map(&:libelle)).to match_array ['Demandeur', 'Date du dernier évènement']
+ end
+ end
+ end
end
describe '#edit' do
@@ -146,6 +182,35 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
expect(flash.alert).to be_present
end
end
+
+ context 'for tabular' do
+ let(:exported_columns) do
+ [
+ { id: procedure.find_column(label: 'Demandeur').id, libelle: 'Demandeur' },
+ { id: procedure.find_column(label: 'Date du dernier évènement').id, libelle: 'Date du dernier évènement' }
+ ].map(&:to_json)
+ end
+
+ let(:export_template_params) do
+ {
+ name: "ExportODS",
+ kind: "ods",
+ groupe_instructeur_id: groupe_instructeur.id,
+ export_pdf: item_params(text: "export"),
+ dossier_folder: item_params(text: "dossier"),
+ exported_columns:
+ }
+ end
+
+ context 'with valid params' do
+ it 'redirect to some page' do
+ subject
+ expect(response).to redirect_to(exports_instructeur_procedure_path(procedure))
+ expect(flash.notice).to eq "Le modèle d'export ExportODS a bien été modifié"
+ expect(ExportTemplate.last.exported_columns.map(&:libelle)).to match_array ['Demandeur', 'Date du dernier évènement']
+ end
+ end
+ end
end
describe '#destroy' do
diff --git a/spec/factories/champ.rb b/spec/factories/champ.rb
index 2c8796138..943fc5355 100644
--- a/spec/factories/champ.rb
+++ b/spec/factories/champ.rb
@@ -170,15 +170,20 @@ FactoryBot.define do
factory :champ_do_not_use_rna, class: 'Champs::RNAChamp' do
value { 'W173847273' }
+ value_json { AddressProxy::ADDRESS_PARTS.index_by(&:itself) }
end
factory :champ_do_not_use_engagement_juridique, class: 'Champs::EngagementJuridiqueChamp' do
+ value { 'EJ' }
end
factory :champ_do_not_use_cojo, class: 'Champs::COJOChamp' do
end
factory :champ_do_not_use_rnf, class: 'Champs::RNFChamp' do
+ value { '075-FDD-00003-01' }
+ external_id { '075-FDD-00003-01' }
+ value_json { AddressProxy::ADDRESS_PARTS.index_by(&:itself) }
end
factory :champ_do_not_use_expression_reguliere, class: 'Champs::ExpressionReguliereChamp' do
diff --git a/spec/models/columns/champ_column_spec.rb b/spec/models/columns/champ_column_spec.rb
index 82c0fa2bd..ee5e0615c 100644
--- a/spec/models/columns/champ_column_spec.rb
+++ b/spec/models/columns/champ_column_spec.rb
@@ -13,7 +13,7 @@ describe Columns::ChampColumn do
expect_type_de_champ_values('email', eq(['yoda@beta.gouv.fr']))
expect_type_de_champ_values('phone', eq(['0666666666']))
expect_type_de_champ_values('address', eq(["2 rue des Démarches"]))
- expect_type_de_champ_values('communes', eq(["Coye-la-Forêt"]))
+ expect_type_de_champ_values('communes', eq(["Coye-la-Forêt", "60580", "60"]))
expect_type_de_champ_values('departements', eq(['01']))
expect_type_de_champ_values('regions', eq(['01']))
expect_type_de_champ_values('pays', eq(['France']))
@@ -30,7 +30,7 @@ describe Columns::ChampColumn do
expect_type_de_champ_values('checkbox', eq([true]))
expect_type_de_champ_values('drop_down_list', eq(['val1']))
expect_type_de_champ_values('multiple_drop_down_list', eq([["val1", "val2"]]))
- expect_type_de_champ_values('linked_drop_down_list', eq([nil, "primary", "secondary"]))
+ expect_type_de_champ_values('linked_drop_down_list', eq(["primary / secondary", "primary", "secondary"]))
expect_type_de_champ_values('yes_no', eq([true]))
expect_type_de_champ_values('annuaire_education', eq([nil]))
expect_type_de_champ_values('piece_justificative', be_an_instance_of(Array))
diff --git a/spec/models/columns/dossier_column_spec.rb b/spec/models/columns/dossier_column_spec.rb
index 5b8651d2a..6574732c0 100644
--- a/spec/models/columns/dossier_column_spec.rb
+++ b/spec/models/columns/dossier_column_spec.rb
@@ -14,6 +14,9 @@ describe Columns::DossierColumn do
expect(procedure.find_column(label: "Prénom").value(dossier)).to eq("Paul")
expect(procedure.find_column(label: "Nom").value(dossier)).to eq("Sim")
expect(procedure.find_column(label: "Civilité").value(dossier)).to eq("M.")
+ expect(procedure.find_column(label: "Dépôt pour un tiers").value(dossier)).to eq(true)
+ expect(procedure.find_column(label: "Nom du mandataire").value(dossier)).to eq("Christophe")
+ expect(procedure.find_column(label: "Prénom du mandataire").value(dossier)).to eq("Martin")
end
end
@@ -22,7 +25,67 @@ describe Columns::DossierColumn do
let(:dossier) { create(:dossier, :en_instruction, :with_entreprise, procedure:) }
it 'retrieve entreprise information' do
+ expect(procedure.find_column(label: "Nº dossier").value(dossier)).to eq(dossier.id)
+ expect(procedure.find_column(label: "Email").value(dossier)).to eq(dossier.user_email_for(:display))
+ expect(procedure.find_column(label: "France connecté ?").value(dossier)).to eq(false)
+ expect(procedure.find_column(label: "Entreprise forme juridique").value(dossier)).to eq("SA à conseil d'administration (s.a.i.)")
+ expect(procedure.find_column(label: "Entreprise SIREN").value(dossier)).to eq('440117620')
+ expect(procedure.find_column(label: "Entreprise nom commercial").value(dossier)).to eq('GRTGAZ')
+ expect(procedure.find_column(label: "Entreprise raison sociale").value(dossier)).to eq('GRTGAZ')
+ expect(procedure.find_column(label: "Entreprise SIRET siège social").value(dossier)).to eq('44011762001530')
+ expect(procedure.find_column(label: "Date de création").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone)
+ expect(procedure.find_column(label: "Établissement SIRET").value(dossier)).to eq('44011762001530')
expect(procedure.find_column(label: "Libellé NAF").value(dossier)).to eq('Transports par conduites')
+ expect(procedure.find_column(label: "Établissement code postal").value(dossier)).to eq('92270')
+ expect(procedure.find_column(label: "Établissement siège social").value(dossier)).to eq(true)
+ expect(procedure.find_column(label: "Établissement NAF").value(dossier)).to eq('4950Z')
+ expect(procedure.find_column(label: "Établissement Adresse").value(dossier)).to eq("GRTGAZ\r IMMEUBLE BORA\r 6 RUE RAOUL NORDLING\r 92270 BOIS COLOMBES\r")
+ expect(procedure.find_column(label: "Établissement numero voie").value(dossier)).to eq('6')
+ expect(procedure.find_column(label: "Établissement type voie").value(dossier)).to eq('RUE')
+ expect(procedure.find_column(label: "Établissement nom voie").value(dossier)).to eq('RAOUL NORDLING')
+ expect(procedure.find_column(label: "Établissement complément adresse").value(dossier)).to eq('IMMEUBLE BORA')
+ expect(procedure.find_column(label: "Établissement localité").value(dossier)).to eq('BOIS COLOMBES')
+ expect(procedure.find_column(label: "Établissement code INSEE localité").value(dossier)).to eq('92009')
+ expect(procedure.find_column(label: "Entreprise SIREN").value(dossier)).to eq('440117620')
+ expect(procedure.find_column(label: "Entreprise capital social").value(dossier)).to eq(537_100_000)
+ expect(procedure.find_column(label: "Entreprise numero TVA intracommunautaire").value(dossier)).to eq('FR27440117620')
+ expect(procedure.find_column(label: "Entreprise forme juridique code").value(dossier)).to eq('5599')
+ expect(procedure.find_column(label: "Entreprise code effectif entreprise").value(dossier)).to eq('51')
+ expect(procedure.find_column(label: "Entreprise état administratif").value(dossier)).to eq("actif")
+ expect(procedure.find_column(label: "Entreprise nom").value(dossier)).to eq(nil)
+ expect(procedure.find_column(label: "Entreprise prénom").value(dossier)).to eq(nil)
+ expect(procedure.find_column(label: "Association RNA").value(dossier)).to eq(nil)
+ expect(procedure.find_column(label: "Association titre").value(dossier)).to eq(nil)
+ expect(procedure.find_column(label: "Association objet").value(dossier)).to eq(nil)
+ expect(procedure.find_column(label: "Association date de création").value(dossier)).to eq(nil)
+ expect(procedure.find_column(label: "Association date de déclaration").value(dossier)).to eq(nil)
+ expect(procedure.find_column(label: "Association date de publication").value(dossier)).to eq(nil)
+ expect(procedure.find_column(label: "Date de création").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone)
+ expect(procedure.find_column(label: "Date du dernier évènement").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone)
+ expect(procedure.find_column(label: "Date de dépot").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone)
+ expect(procedure.find_column(label: "Date de passage en construction").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone)
+ expect(procedure.find_column(label: "Date de passage en instruction").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone)
+ expect(procedure.find_column(label: "Date de traitement").value(dossier)).to eq(nil)
+ expect(procedure.find_column(label: "État du dossier").value(dossier)).to eq('en_instruction')
+ expect(procedure.find_column(label: "Archivé").value(dossier)).to eq(false)
+ expect(procedure.find_column(label: "Motivation de la décision").value(dossier)).to eq(nil)
+ expect(procedure.find_column(label: "Date de dernière modification (usager)").value(dossier)).to eq(nil)
+ expect(procedure.find_column(label: "Instructeurs").value(dossier)).to eq('')
+ end
+ end
+
+ context 'when procedure for entreprise which is also an association' do
+ let(:procedure) { create(:procedure, for_individual: false, groupe_instructeurs: [groupe_instructeur]) }
+ let(:etablissement) { create(:etablissement, :is_association) }
+ let(:dossier) { create(:dossier, :en_instruction, procedure:, etablissement:) }
+
+ it 'retrieve also association information' do
+ expect(procedure.find_column(label: "Association RNA").value(dossier)).to eq("W072000535")
+ expect(procedure.find_column(label: "Association titre").value(dossier)).to eq("ASSOCIATION POUR LA PROMOTION DE SPECTACLES AU CHATEAU DE ROCHEMAURE")
+ expect(procedure.find_column(label: "Association objet").value(dossier)).to eq("mise en oeuvre et réalisation de spectacles au chateau de rochemaure")
+ expect(procedure.find_column(label: "Association date de création").value(dossier)).to eq(Date.parse("1990-04-24"))
+ expect(procedure.find_column(label: "Association date de déclaration").value(dossier)).to eq(Date.parse("2014-11-28"))
+ expect(procedure.find_column(label: "Association date de publication").value(dossier)).to eq(Date.parse("1990-05-16"))
end
end
diff --git a/spec/models/concerns/dossier_champs_concern_spec.rb b/spec/models/concerns/dossier_champs_concern_spec.rb
index 6b864f28a..1b8a920e8 100644
--- a/spec/models/concerns/dossier_champs_concern_spec.rb
+++ b/spec/models/concerns/dossier_champs_concern_spec.rb
@@ -183,8 +183,8 @@ RSpec.describe DossierChampsConcern do
it { row_id; subject; expect(row_id).not_to be_in(row_ids) }
end
- describe "#champs_for_export" do
- subject { dossier.champs_for_export(dossier.revision.types_de_champ_public) }
+ describe "#champ_values_for_export" do
+ subject { dossier.champ_values_for_export(dossier.revision.types_de_champ_public, format: :xlsx) }
it { expect(subject.size).to eq(4) }
it { expect(subject.first).to eq(["Un champ text", nil]) }
diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb
index 320fe595e..cba6e5188 100644
--- a/spec/models/dossier_spec.rb
+++ b/spec/models/dossier_spec.rb
@@ -1982,7 +1982,7 @@ describe Dossier, type: :model do
end
end
- describe "champs_for_export" do
+ describe "champ_values_for_export" do
context 'with integer_number' do
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :integer_number, libelle: 'c1' }]) }
let(:dossier) { create(:dossier, :with_populated_champs, procedure:) }
@@ -1993,7 +1993,7 @@ describe Dossier, type: :model do
expect {
integer_number_type_de_champ.update(type_champ: :decimal_number)
procedure.update(published_revision: procedure.draft_revision, draft_revision: procedure.create_new_revision)
- }.to change { dossier.reload.champs_for_export(procedure.all_revisions_types_de_champ.not_repetition.to_a) }
+ }.to change { dossier.reload.champ_values_for_export(procedure.all_revisions_types_de_champ.not_repetition.to_a, format: :xlsx) }
.from([["c1", 42]]).to([["c1", 42.0]])
end
end
@@ -2020,8 +2020,8 @@ describe Dossier, type: :model do
let(:repetition_second_revision_champ) { dossier_second_revision.project_champs_public.find(&:repetition?) }
let(:dossier) { create(:dossier, procedure: procedure) }
let(:dossier_second_revision) { create(:dossier, procedure: procedure) }
- let(:dossier_champs_for_export) { dossier.champs_for_export(procedure.types_de_champ_for_procedure_export) }
- let(:dossier_second_revision_champs_for_export) { dossier_second_revision.champs_for_export(procedure.types_de_champ_for_procedure_export) }
+ let(:dossier_champ_values_for_export) { dossier.champ_values_for_export(procedure.types_de_champ_for_procedure_export, format: :xlsx) }
+ let(:dossier_second_revision_champ_values_for_export) { dossier_second_revision.champ_values_for_export(procedure.types_de_champ_for_procedure_export, format: :xlsx) }
context "when procedure published" do
before do
@@ -2040,8 +2040,8 @@ describe Dossier, type: :model do
it "should have champs from all revisions" do
expect(dossier.types_de_champ.map(&:libelle)).to eq([text_type_de_champ.libelle, datetime_type_de_champ.libelle, "Yes/no", explication_type_de_champ.libelle, commune_type_de_champ.libelle, repetition_type_de_champ.libelle])
expect(dossier_second_revision.types_de_champ.map(&:libelle)).to eq([datetime_type_de_champ.libelle, "Updated yes/no", explication_type_de_champ.libelle, 'Commune de naissance', "Repetition", "New text field"])
- expect(dossier_champs_for_export.map { |(libelle)| libelle }).to eq([datetime_type_de_champ.libelle, text_type_de_champ.libelle, "Updated yes/no", "Commune de naissance", "Commune de naissance (Code INSEE)", "Commune de naissance (Département)", "New text field"])
- expect(dossier_champs_for_export).to eq(dossier_second_revision_champs_for_export)
+ expect(dossier_champ_values_for_export.map { |(libelle)| libelle }).to eq([datetime_type_de_champ.libelle, text_type_de_champ.libelle, "Updated yes/no", "Commune de naissance", "Commune de naissance (Code INSEE)", "Commune de naissance (Département)", "New text field"])
+ expect(dossier_champ_values_for_export).to eq(dossier_second_revision_champ_values_for_export)
end
context 'within a repetition having a type de champs commune (multiple values for export)' do
@@ -2056,7 +2056,7 @@ describe Dossier, type: :model do
dossier_test = create(:dossier, procedure: proc_test)
type_champs = proc_test.all_revisions_types_de_champ(parent: tdc_repetition).to_a
expect(type_champs.size).to eq(1)
- expect(dossier.champs_for_export(type_champs).size).to eq(3)
+ expect(dossier.champ_values_for_export(type_champs, format: :xlsx).size).to eq(3)
end
end
end
@@ -2065,7 +2065,7 @@ describe Dossier, type: :model do
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :text }, { type: :explication }]) }
it "should not contain non-exportable types de champ" do
- expect(dossier_champs_for_export.map { |(libelle)| libelle }).to eq([text_type_de_champ.libelle])
+ expect(dossier_champ_values_for_export.map { |(libelle)| libelle }).to eq([text_type_de_champ.libelle])
end
end
end
@@ -2079,7 +2079,7 @@ describe Dossier, type: :model do
let(:text_tdc) { procedure.active_revision.types_de_champ_public.second }
let(:tdcs) { dossier.project_champs_public.map(&:type_de_champ) }
- subject { dossier.champs_for_export(tdcs) }
+ subject { dossier.champ_values_for_export(tdcs, format: :xlsx) }
before do
text_tdc.update(condition: ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
diff --git a/spec/models/export_template_tabular_spec.rb b/spec/models/export_template_tabular_spec.rb
new file mode 100644
index 000000000..4751ffdd2
--- /dev/null
+++ b/spec/models/export_template_tabular_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+describe ExportTemplate do
+ let(:groupe_instructeur) { create(:groupe_instructeur, procedure:) }
+ let(:export_template) { build(:export_template, kind: 'csv', groupe_instructeur:) }
+ let(:tabular_export_template) { build(:tabular_export_template, groupe_instructeur:) }
+ let(:procedure) { create(:procedure_with_dossiers, :published, types_de_champ_public:, for_individual:) }
+ let(:for_individual) { true }
+ let(:types_de_champ_public) do
+ [
+ { type: :text, libelle: "Ca va ?", mandatory: true, stable_id: 1 },
+ { type: :communes, libelle: "Commune", mandatory: true, stable_id: 17 },
+ { type: :siret, libelle: 'siret', stable_id: 20 },
+ { type: :repetition, mandatory: true, stable_id: 7, libelle: "Champ répétable", children: [{ type: 'text', libelle: 'Qqchose à rajouter?', stable_id: 8 }] }
+ ]
+ end
+
+ describe '#exported_columns=' do
+ it 'is assignable/readable with ExportedColumn object' do
+ expect do
+ export_template.exported_columns = [
+ ExportedColumn.new(libelle: 'Ça va ?', column: procedure.find_column(label: "Ca va ?"))
+ ]
+ export_template.save!
+ export_template.exported_columns
+ end.not_to raise_error
+ end
+ it 'create exported_column' do
+ export_template.exported_columns = [
+ ExportedColumn.new(libelle: 'Ça va ?', column: procedure.find_column(label: "Ca va ?"))
+ ]
+ export_template.save!
+ expect(export_template.exported_columns.size).to eq 1
+ end
+
+ context 'when there is a previous revision with a renamed tdc' do
+ context 'with already column in export template' do
+ let(:previous_tdc) { procedure.published_revision.types_de_champ_public.find_by(stable_id: 1) }
+ let(:changed_tdc) { { libelle: "Ca roule ?" } }
+
+ context 'with already column in export template' do
+ before do
+ export_template.exported_columns = [
+ ExportedColumn.new(libelle: 'Ça va ?', column: procedure.find_column(label: "Ca va ?"))
+ ]
+ export_template.save!
+
+ type_de_champ = procedure.draft_revision.find_and_ensure_exclusive_use(previous_tdc.stable_id)
+ type_de_champ.update(changed_tdc)
+ procedure.publish_revision!
+ end
+
+ it 'update columns with original libelle for champs with new revision' do
+ Current.procedure_columns = {}
+ procedure.reload
+ export_template.reload
+ expect(export_template.exported_columns.find { _1.column.stable_id.to_s == "1" }.libelle).to eq('Ça va ?')
+ end
+ end
+ end
+ context 'without columns in export template' do
+ let(:previous_tdc) { procedure.published_revision.types_de_champ_public.find_by(stable_id: 1) }
+ let(:changed_tdc) { { libelle: "Ca roule ?" } }
+
+ before do
+ type_de_champ = procedure.draft_revision.find_and_ensure_exclusive_use(previous_tdc.stable_id)
+ type_de_champ.update(changed_tdc)
+ procedure.publish_revision!
+
+ export_template.exported_columns = [
+ ExportedColumn.new(libelle: 'Ça roule ?', column: procedure.find_column(label: "Ca roule ?"))
+ ]
+ export_template.save!
+ end
+
+ it 'update columns with original libelle for champs with new revision' do
+ Current.procedure_columns = {}
+ procedure.reload
+ export_template.reload
+ expect(export_template.exported_columns.find { _1.column.stable_id.to_s == "1" }.libelle).to eq('Ça roule ?')
+ end
+ end
+ end
+ end
+
+ describe 'dossier_exported_columns' do
+ context 'when exported_columns is empty' do
+ it 'returns an empty array' do
+ expect(export_template.dossier_exported_columns).to eq([])
+ end
+ end
+
+ context 'when exported_columns is not empty' do
+ before do
+ export_template.exported_columns = [
+ ExportedColumn.new(libelle: 'Colonne usager', column: procedure.find_column(label: "Email")),
+ ExportedColumn.new(libelle: 'Ça va ?', column: procedure.find_column(label: "Ca va ?"))
+ ]
+ end
+ it 'returns all columns except tdc columns' do
+ expect(export_template.dossier_exported_columns.size).to eq(1) # exclude tdc
+ expect(export_template.dossier_exported_columns.first.libelle).to eq("Colonne usager")
+ end
+ end
+ end
+
+ describe 'columns_for_stable_id' do
+ before do
+ export_template.exported_columns = procedure.published_revision.types_de_champ.first.columns(procedure: procedure).map do |column|
+ ExportedColumn.new(libelle: column.label, column:)
+ end
+ end
+ context 'when procedure has a TypeDeChamp::Commune' do
+ let(:types_de_champ_public) do
+ [
+ { type: :communes, libelle: "Commune", mandatory: true, stable_id: 17 }
+ ]
+ end
+ it 'is able to resolve stable_id' do
+ expect(export_template.columns_for_stable_id(17).size).to eq(3)
+ end
+ end
+ context 'when procedure has a TypeDeChamp::Siret' do
+ let(:types_de_champ_public) do
+ [
+ { type: :siret, libelle: 'siret', stable_id: 20 }
+ ]
+ end
+ it 'is able to resolve stable_id' do
+ expect(export_template.columns_for_stable_id(20).size).to eq(5)
+ end
+ end
+ context 'when procedure has a TypeDeChamp::Text' do
+ let(:types_de_champ_public) do
+ [
+ { type: :text, libelle: "Text", mandatory: true, stable_id: 15 }
+ ]
+ end
+ it 'is able to resolve stable_id' do
+ expect(export_template.columns_for_stable_id(15).size).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb
index b8724a106..50db50eb1 100644
--- a/spec/models/procedure_spec.rb
+++ b/spec/models/procedure_spec.rb
@@ -1885,6 +1885,49 @@ describe Procedure do
end
end
+ describe '#all_revisions_types_de_champ' do
+ let(:types_de_champ_public) do
+ [
+ { type: :text },
+ { type: :header_section }
+ ]
+ end
+
+ context 'when procedure brouillon' do
+ let(:procedure) { create(:procedure, types_de_champ_public:) }
+
+ it 'returns one type de champ' do
+ expect(procedure.all_revisions_types_de_champ.size).to eq 1
+ end
+
+ it 'returns also section type de champ' do
+ expect(procedure.all_revisions_types_de_champ(with_header_section: true).size).to eq 2
+ end
+
+ it "returns types de champ on draft revision" do
+ procedure.draft_revision.add_type_de_champ(type_champ: :text, libelle: 'onemorechamp')
+ expect(procedure.reload.all_revisions_types_de_champ.size).to eq 2
+ end
+ end
+
+ context 'when procedure is published' do
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
+
+ it 'returns one type de champ' do
+ expect(procedure.all_revisions_types_de_champ.size).to eq 1
+ end
+
+ it 'returns also section type de champ' do
+ expect(procedure.all_revisions_types_de_champ(with_header_section: true).size).to eq 2
+ end
+
+ it "doesn't return types de champ on draft revision" do
+ procedure.draft_revision.add_type_de_champ(type_champ: :text, libelle: 'onemorechamp')
+ expect(procedure.reload.all_revisions_types_de_champ.size).to eq 1
+ end
+ end
+ end
+
private
def create_dossier_with_pj_of_size(size, procedure)
diff --git a/spec/models/types_de_champ/commune_type_de_champ_spec.rb b/spec/models/types_de_champ/commune_type_de_champ_spec.rb
index 18db0b2b5..ccc66bacb 100644
--- a/spec/models/types_de_champ/commune_type_de_champ_spec.rb
+++ b/spec/models/types_de_champ/commune_type_de_champ_spec.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
describe TypesDeChamp::CommuneTypeDeChamp do
- let(:subject) { create(:type_de_champ_communes, libelle: 'Ma commune') }
-
- it { expect(subject.libelles_for_export).to match_array([['Ma commune', :value], ['Ma commune (Code INSEE)', :code], ['Ma commune (Département)', :departement]]) }
+ let(:tdc_commune) { create(:type_de_champ_communes, libelle: 'Ma commune') }
+ it { expect(tdc_commune.libelles_for_export).to match_array([['Ma commune', :value], ['Ma commune (Code INSEE)', :code], ['Ma commune (Département)', :departement]]) }
end
diff --git a/spec/services/procedure_export_service_tabular_spec.rb b/spec/services/procedure_export_service_tabular_spec.rb
new file mode 100644
index 000000000..3bff4c42d
--- /dev/null
+++ b/spec/services/procedure_export_service_tabular_spec.rb
@@ -0,0 +1,282 @@
+# frozen_string_literal: true
+
+require 'csv'
+
+describe ProcedureExportService do
+ let(:instructeur) { create(:instructeur) }
+ let(:procedure) { create(:procedure, types_de_champ_public:, for_individual:, ask_birthday: true, instructeurs: [instructeur]) }
+ let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) }
+ let(:for_individual) { true }
+ let(:types_de_champ_public) do
+ [
+ { type: :text, libelle: "first champ", mandatory: true, stable_id: 1 },
+ { type: :communes, libelle: "Commune", mandatory: true, stable_id: 17 },
+ { type: :piece_justificative, libelle: "PJ", stable_id: 30 },
+ {
+ type: :repetition, mandatory: true, stable_id: 7, libelle: "Champ répétable", children:
+ [
+ { type: 'text', libelle: 'child first champ', stable_id: 8 },
+ { type: 'text', libelle: 'child second champ', stable_id: 9 }
+ ]
+ }
+ ]
+ end
+ let(:exported_columns) { [] }
+
+ describe 'to_xlsx' do
+ let(:kind) { 'xlsx' }
+ let(:export_template) { create(:export_template, kind:, exported_columns:, groupe_instructeur: procedure.defaut_groupe_instructeur) }
+ let(:dossiers_sheet) { subject.sheets.first }
+ let(:etablissements_sheet) { subject.sheets.second }
+ let(:avis_sheet) { subject.sheets.third }
+ let(:repetition_sheet) { subject.sheets.fourth }
+
+ subject do
+ service
+ .to_xlsx
+ .open { |f| SimpleXlsxReader.open(f.path) }
+ end
+
+ describe 'sheets' do
+ it 'should have a sheet for each record type' do
+ expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis'])
+ end
+ end
+
+ describe 'Dossiers sheet' do
+ context 'with multiple columns' do
+ let(:exported_columns) do
+ [
+ ExportedColumn.new(libelle: 'Date du dernier évènement', column: procedure.find_column(label: 'Date du dernier évènement')),
+ ExportedColumn.new(libelle: 'Email', column: procedure.find_column(label: 'Email')),
+ ExportedColumn.new(libelle: 'Groupe instructeur', column: procedure.find_column(label: 'Groupe instructeur')),
+ ExportedColumn.new(libelle: 'État du dossier', column: procedure.dossier_state_column),
+ ExportedColumn.new(libelle: 'first champ', column: procedure.find_column(label: 'first champ')),
+ ExportedColumn.new(libelle: 'Commune', column: procedure.find_column(label: 'Commune')),
+ ExportedColumn.new(libelle: 'PJ', column: procedure.find_column(label: 'PJ'))
+ ]
+ end
+
+ let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) }
+ let(:selected_headers) { ["Email", "first champ", "Commune", "Groupe instructeur", "Date du dernier évènement", "État du dossier", "PJ"] }
+
+ it 'should have only headers from export template' do
+ expect(dossiers_sheet.headers).to match_array(selected_headers)
+ end
+
+ it 'should have data' do
+ expect(procedure.dossiers.count).to eq 1
+ expect(dossiers_sheet.data.size).to eq 1
+
+ expect(dossiers_sheet.data).to match_array([[anything, dossier.user_email_for_display, "défaut", "En instruction", "text", "Coye-la-Forêt", "toto.txt"]])
+ end
+ end
+
+ context 'with multiple groupe instructeur' do
+ let(:exported_columns) { [ExportedColumn.new(libelle: 'Groupe instructeur', column: procedure.find_column(label: 'Groupe instructeur'))] }
+ let(:types_de_champ_public) { [] }
+
+ before do
+ create(:groupe_instructeur, label: '2', procedure:)
+ create(:dossier, :en_instruction, procedure:)
+ end
+
+ it 'find groupe instructeur data' do
+ expect(dossiers_sheet.headers).to include('Groupe instructeur')
+ expect(dossiers_sheet.data[0][dossiers_sheet.headers.index('Groupe instructeur')]).to eq('défaut')
+ end
+ end
+
+ context 'with multiple pjs' do
+ let(:types_de_champ_public) { [{ type: :piece_justificative, libelle: "PJ" }] }
+ let(:exported_columns) { [ExportedColumn.new(libelle: 'PJ', column: procedure.find_column(label: 'PJ'))] }
+ before do
+ dossier = create(:dossier, :en_instruction, :with_populated_champs, procedure:)
+ dossier.filled_champs_public
+ .find { _1.is_a? Champs::PieceJustificativeChamp }
+ .piece_justificative_file
+ .attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain")
+ end
+ it { expect(dossiers_sheet.data.last.last).to eq "toto.txt, toto.txt" }
+ end
+
+ context 'with TypeDeChamp::MutlipleDropDownListTypeDeChamp' do
+ let(:types_de_champ_public) { [{ type: :multiple_drop_down_list, libelle: "multiple_drop_down_list", mandatory: true }] }
+ let(:exported_columns) { [ExportedColumn.new(libelle: 'Date du dernier évènement', column: procedure.find_column(label: 'multiple_drop_down_list'))] }
+ before { create(:dossier, :with_populated_champs, procedure:) }
+ it { expect(dossiers_sheet.data.last.last).to eq "val1, val2" }
+ end
+
+ context 'with TypeDeChamp:YesNoTypeDeChamp' do
+ let(:types_de_champ_public) { [{ type: :yes_no, libelle: "yes_no", mandatory: true }] }
+ let(:exported_columns) { [ExportedColumn.new(libelle: 'yes_no', column: procedure.find_column(label: 'yes_no'))] }
+ before { create(:dossier, :with_populated_champs, procedure:) }
+ it { expect(dossiers_sheet.data.last.last).to eq true }
+ end
+
+ context 'with TypeDeChamp:CheckboxTypeDeChamp' do
+ let(:types_de_champ_public) { [{ type: :checkbox, libelle: "checkbox", mandatory: true }] }
+ let(:exported_columns) { [ExportedColumn.new(libelle: 'checkbox', column: procedure.find_column(label: 'checkbox'))] }
+ before { create(:dossier, :with_populated_champs, procedure:) }
+ it { expect(dossiers_sheet.data.last.last).to eq true }
+ end
+
+ context 'with TypeDeChamp:DecimalNumberTypeDeChamp' do
+ let(:types_de_champ_public) { [{ type: :decimal_number, libelle: "decimal", mandatory: true }] }
+ let(:exported_columns) { [ExportedColumn.new(libelle: 'decimal', column: procedure.find_column(label: 'decimal'))] }
+ before { create(:dossier, :with_populated_champs, procedure:) }
+ it { expect(dossiers_sheet.data.last.last).to eq 42.1 }
+ end
+
+ context 'with TypeDeChamp:IntegerNumberTypeDeChamp' do
+ let(:types_de_champ_public) { [{ type: :integer_number, libelle: "integer", mandatory: true }] }
+ let(:exported_columns) { [ExportedColumn.new(libelle: 'integer', column: procedure.find_column(label: 'integer'))] }
+ before { create(:dossier, :with_populated_champs, procedure:) }
+ it { expect(dossiers_sheet.data.last.last).to eq 42.0 }
+ end
+
+ context 'with TypesDeChamp::LinkedDropDownListTypeDeChamp' do
+ let(:types_de_champ_public) { [{ type: :linked_drop_down_list, libelle: "linked_drop_down_list", mandatory: true }] }
+ let(:exported_columns) { [ExportedColumn.new(libelle: 'linked_drop_down_list', column: procedure.find_column(label: 'linked_drop_down_list'))] }
+ before { create(:dossier, :with_populated_champs, procedure:) }
+ it { expect(dossiers_sheet.data.last.last).to eq "primary / secondary" }
+ end
+
+ context 'with TypesDeChamp::DateTimeTypeDeChamp' do
+ let(:types_de_champ_public) { [{ type: :datetime, libelle: "datetime", mandatory: true }] }
+ let(:exported_columns) { [ExportedColumn.new(libelle: 'datetime', column: procedure.find_column(label: 'datetime'))] }
+ let(:dossier) { create(:dossier, :with_populated_champs, procedure:) }
+ before { dossier }
+ it do
+ champ_value = Time.zone.parse(dossier.champs.first.value)
+ offset = champ_value.utc_offset
+ sheet_value = Time.zone.at(dossiers_sheet.data.last.last - offset.seconds)
+ expect(sheet_value).to eq(champ_value.round)
+ end
+ end
+
+ context 'with TypesDeChamp::Date' do
+ let(:types_de_champ_public) { [{ type: :date, libelle: "date", mandatory: true }] }
+ let(:exported_columns) { [ExportedColumn.new(libelle: 'date', column: procedure.find_column(label: 'date'))] }
+ before { create(:dossier, :with_populated_champs, procedure:) }
+ it { expect(dossiers_sheet.data.last.last).to be_an_instance_of(Date) }
+ end
+
+ context 'with DossierColumn as datetime' do
+ let(:types_de_champ_public) { [] }
+ let(:exported_columns) { [ExportedColumn.new(libelle: 'Date de passage en instruction', column: procedure.find_column(label: 'Date de passage en instruction'))] }
+ before { create(:dossier, :en_instruction, :with_populated_champs, procedure:) }
+ it { expect(dossiers_sheet.data.last.last).to be_an_instance_of(Time) }
+ end
+ end
+
+ describe 'Etablissement sheet' do
+ let(:types_de_champ_public) { [{ type: :siret, libelle: 'siret', stable_id: 40 }] }
+ let(:exported_columns) do
+ [
+ ExportedColumn.new(libelle: "Nº dossier", column: procedure.find_column(label: "Nº dossier")),
+ ExportedColumn.new(libelle: "Demandeur", column: procedure.find_column(label: "Demandeur")),
+ ExportedColumn.new(libelle: "siret", column: procedure.find_column(label: "siret"))
+ ]
+ end
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
+ let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_entreprise, procedure: procedure) }
+
+ let(:dossier_etablissement) { etablissements_sheet.data[1] }
+ let(:champ_etablissement) { etablissements_sheet.data[0] }
+
+ it 'should have siret header in dossiers sheet' do
+ expect(dossiers_sheet.headers).to include('siret')
+ end
+
+ it 'should have headers in etablissement sheet' do
+ expect(etablissements_sheet.headers).to eq([
+ "Dossier ID",
+ "Champ",
+ "Établissement SIRET",
+ "Etablissement enseigne",
+ "Établissement siège social",
+ "Établissement NAF",
+ "Établissement libellé NAF",
+ "Établissement Adresse",
+ "Établissement numero voie",
+ "Établissement type voie",
+ "Établissement nom voie",
+ "Établissement complément adresse",
+ "Établissement code postal",
+ "Établissement localité",
+ "Établissement code INSEE localité",
+ "Entreprise SIREN",
+ "Entreprise capital social",
+ "Entreprise numero TVA intracommunautaire",
+ "Entreprise forme juridique",
+ "Entreprise forme juridique code",
+ "Entreprise nom commercial",
+ "Entreprise raison sociale",
+ "Entreprise SIRET siège social",
+ "Entreprise code effectif entreprise",
+ "Entreprise date de création",
+ "Entreprise état administratif",
+ "Entreprise nom",
+ "Entreprise prénom",
+ "Association RNA",
+ "Association titre",
+ "Association objet",
+ "Association date de création",
+ "Association date de déclaration",
+ "Association date de publication"
+ ])
+ end
+ end
+
+ describe 'Avis sheet' do
+ let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) }
+ let!(:avis) { create(:avis, :with_answer, dossier: dossier) }
+
+ it 'should have headers and data' do
+ expect(avis_sheet.headers).to eq([
+ "Dossier ID",
+ "Introduction",
+ "Réponse",
+ "Question",
+ "Réponse oui/non",
+ "Créé le",
+ "Répondu le",
+ "Instructeur",
+ "Expert"
+ ])
+ expect(avis_sheet.data.size).to eq(1)
+ end
+ end
+
+ describe 'Repetitions sheet' do
+ let(:exported_columns) do
+ [
+ ExportedColumn.new(libelle: "Champ répétable – child second champ", column: procedure.find_column(label: "Champ répétable – child second champ"))
+ ]
+ end
+ let!(:dossiers) do
+ [
+ create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure),
+ create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure)
+ ]
+ end
+
+ describe 'sheets' do
+ it 'should have a sheet for repetition' do
+ expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', '(7) Champ repetable'])
+ end
+ end
+
+ it 'should have headers' do
+ expect(repetition_sheet.headers).to eq([
+ "Dossier ID", "Ligne", "Champ répétable – child second champ"
+ ])
+ end
+
+ it 'should have data' do
+ expect(repetition_sheet.data.size).to eq 4
+ end
+ end
+ end
+end
diff --git a/spec/system/administrateurs/procedure_archive_and_export_spec.rb b/spec/system/administrateurs/procedure_archive_and_export_spec.rb
index 3b1b85c9c..eb868f788 100644
--- a/spec/system/administrateurs/procedure_archive_and_export_spec.rb
+++ b/spec/system/administrateurs/procedure_archive_and_export_spec.rb
@@ -41,7 +41,10 @@ describe 'Creating a new procedure', js: true do
click_on "Télécharger tous les dossiers"
expect {
- click_on "Demander un export au format .xlsx"
+ within(:css, '#tabpanel-standard-panel') do
+ choose "Fichier xlsx", allow_label_click: true
+ click_on "Demander l'export"
+ end
expect(page).to have_content("Nous générons cet export. Veuillez revenir dans quelques minutes pour le télécharger.")
}.to have_enqueued_job(ExportJob).with(an_instance_of(Export))
end
diff --git a/spec/system/instructeurs/instruction_spec.rb b/spec/system/instructeurs/instruction_spec.rb
index 098878767..bfd9447d2 100644
--- a/spec/system/instructeurs/instruction_spec.rb
+++ b/spec/system/instructeurs/instruction_spec.rb
@@ -132,13 +132,14 @@ describe 'Instructing a dossier:', js: true do
test_statut_bar(a_suivre: 1, tous_les_dossiers: 1)
click_on "Télécharger un dossier"
- within(:css, '.dossiers-export') do
- click_on "Demander un export au format .csv"
+ within(:css, '#tabpanel-standard1-panel') do
+ choose "Fichier csv", allow_label_click: true
+ click_on "Demander l'export"
end
expect(page).to have_text('Nous générons cet export.')
- click_on "Voir les exports"
+ click_on "Voir les exports et modèles d'export"
expect(page).to have_text("Export .csv d’un dossier « à suivre » demandé il y a moins d'une minute")
expect(page).to have_text("En préparation")
diff --git a/spec/system/instructeurs/procedure_export_tabular_spec.rb b/spec/system/instructeurs/procedure_export_tabular_spec.rb
new file mode 100644
index 000000000..326bdbc30
--- /dev/null
+++ b/spec/system/instructeurs/procedure_export_tabular_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+describe "procedure exports" do
+ let(:instructeur) { create(:instructeur) }
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:, instructeurs: [instructeur]) }
+ let(:types_de_champ_public) { [{ type: :text }] }
+ before { login_as(instructeur.user, scope: :user) }
+
+ scenario "create an export_template tabular and u", js: true do
+ Flipper.enable(:export_template, procedure)
+ visit instructeur_procedure_path(procedure)
+
+ click_on "Voir les exports et modèles d'export"
+
+ click_on "Modèles d'export"
+
+ click_on "Créer un modèle d'export tabulaire"
+
+ fill_in "Nom du modèle", with: "Mon modèle"
+
+ find("#informations-usager-fieldset label", text: "Tout sélectionner").click
+ within '#informations-usager-fieldset' do
+ expect(all('input[type=checkbox]').all?(&:checked?)).to be_truthy
+ end
+
+ find("#informations-dossier-fieldset label", text: "Tout sélectionner").click
+ within '#informations-dossier-fieldset' do
+ expect(all('input[type=checkbox]').all?(&:checked?)).to be_truthy
+ end
+
+ click_on "Enregistrer"
+
+ find("#tabpanel-export-templates", wait: 5, visible: true)
+ find("#tabpanel-export-templates").click
+
+ within 'table' do
+ expect(page).to have_content('Mon modèle')
+ end
+
+ # check if all usager colonnes are selected
+ #
+ click_on 'Mon modèle'
+
+ within '#informations-usager-fieldset' do
+ expect(all('input[type=checkbox]').all?(&:checked?)).to be_truthy
+ end
+
+ within '#informations-dossier-fieldset' do
+ expect(all('input[type=checkbox]').all?(&:checked?)).to be_truthy
+ end
+
+ # uncheck checkboxes
+ find("#informations-dossier-fieldset label", text: "Tout sélectionner").click
+ within '#informations-dossier-fieldset' do
+ expect(all('input[type=checkbox]').none?(&:checked?)).to be_truthy
+ end
+ end
+end
diff --git a/spec/system/routing/rules_full_scenario_spec.rb b/spec/system/routing/rules_full_scenario_spec.rb
index b38bbba68..56e1c7942 100644
--- a/spec/system/routing/rules_full_scenario_spec.rb
+++ b/spec/system/routing/rules_full_scenario_spec.rb
@@ -209,7 +209,7 @@ describe 'The routing with rules', js: true do
## on the dossiers list
click_on procedure.libelle
expect(page).to have_current_path(instructeur_procedure_path(procedure))
- expect(find('.fr-tabs')).to have_css('span.notifications')
+ expect(find('nav.fr-tabs')).to have_css('span.notifications')
## on the dossier itself
click_on 'suivi'
diff --git a/spec/tasks/maintenance/populate_rna_json_value_task_spec.rb b/spec/tasks/maintenance/populate_rna_json_value_task_spec.rb
index bdb3fd559..df727227c 100644
--- a/spec/tasks/maintenance/populate_rna_json_value_task_spec.rb
+++ b/spec/tasks/maintenance/populate_rna_json_value_task_spec.rb
@@ -20,7 +20,7 @@ module Maintenance
end
it 'updates value_json' do
expect { subject }.to change { element.reload.value_json }
- .from(nil)
+ .from(anything)
.to({
"street_number" => "33",
"street_name" => "de Modagor",
diff --git a/spec/tasks/maintenance/populate_rnf_json_value_task_spec.rb b/spec/tasks/maintenance/populate_rnf_json_value_task_spec.rb
index a71eb25ad..f74ed99a1 100644
--- a/spec/tasks/maintenance/populate_rnf_json_value_task_spec.rb
+++ b/spec/tasks/maintenance/populate_rnf_json_value_task_spec.rb
@@ -53,7 +53,7 @@ module Maintenance
it 'updates value_json' do
expect { subject }.to change { element.reload.value_json }
- .from(nil)
+ .from(anything)
.to({
"street_number" => "16",
"street_name" => "Rue du Général de Boissieu",
@@ -79,7 +79,7 @@ module Maintenance
it 'updates value_json' do
expect { subject }.to change { element.reload.value_json }
- .from(nil)
+ .from(anything)
.to({
"street_number" => "16",
"street_name" => "Rue du Général de Boissieu",