Merge pull request #10875 from demarches-simplifiees/add_id_to_column_third_part
Utilisation des colonnes dans l'affichage et le filtrage
This commit is contained in:
commit
6862f6c6bd
47 changed files with 1512 additions and 1499 deletions
|
@ -30,7 +30,7 @@ class Dossiers::ExportLinkComponent < ApplicationComponent
|
||||||
end
|
end
|
||||||
|
|
||||||
def export_title(export)
|
def export_title(export)
|
||||||
if export.procedure_presentation_id.nil?
|
if !export.built_from_procedure_presentation?
|
||||||
t(".export_title_everything", export_format: export.format)
|
t(".export_title_everything", export_format: export.format)
|
||||||
elsif export.tous?
|
elsif export.tous?
|
||||||
t(".export_title", export_format: export.format, count: export.count)
|
t(".export_title", export_format: export.format, count: export.count)
|
||||||
|
|
|
@ -30,7 +30,7 @@ class Instructeurs::ColumnFilterComponent < ApplicationComponent
|
||||||
{
|
{
|
||||||
selected_key: column.present? ? column.id : '',
|
selected_key: column.present? ? column.id : '',
|
||||||
items: filterable_columns_options,
|
items: filterable_columns_options,
|
||||||
name: :column,
|
name: "#{prefix}[id]",
|
||||||
id: 'search-filter',
|
id: 'search-filter',
|
||||||
'aria-describedby': 'instructeur-filter-combo-label',
|
'aria-describedby': 'instructeur-filter-combo-label',
|
||||||
form: 'filter-component',
|
form: 'filter-component',
|
||||||
|
@ -39,13 +39,20 @@ class Instructeurs::ColumnFilterComponent < ApplicationComponent
|
||||||
end
|
end
|
||||||
|
|
||||||
def filterable_columns_options
|
def filterable_columns_options
|
||||||
procedure.columns.filter_map do |column|
|
@procedure.columns.filter(&:filterable).map { [_1.label, _1.id] }
|
||||||
next if column.filterable == false
|
|
||||||
|
|
||||||
[column.label, column.id]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def current_filter_tags
|
||||||
|
@procedure_presentation.filters_for(@statut).flat_map do
|
||||||
|
[
|
||||||
|
hidden_field_tag("#{prefix}[id]", _1.column.id, id: nil),
|
||||||
|
hidden_field_tag("#{prefix}[filter]", _1.filter, id: nil)
|
||||||
|
]
|
||||||
|
end.reduce(&:concat)
|
||||||
|
end
|
||||||
|
|
||||||
|
def prefix = "#{procedure_presentation.filters_name_for(@statut)}[]"
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def find_type_de_champ(column)
|
def find_type_de_champ(column)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
= form_tag add_filter_instructeur_procedure_path(procedure), method: :post, class: 'dropdown-form large', id: 'filter-component', data: { turbo: true, controller: 'autosubmit' } do
|
= form_tag add_filter_instructeur_procedure_path(procedure), method: :post, class: 'dropdown-form large', id: 'filter-component', data: { turbo: true, controller: 'autosubmit' } do
|
||||||
|
= current_filter_tags
|
||||||
|
|
||||||
.fr-select-group
|
.fr-select-group
|
||||||
= label_tag :column, t('.column'), class: 'fr-label fr-m-0', id: 'instructeur-filter-combo-label', for: 'search-filter'
|
= label_tag :column, t('.column'), class: 'fr-label fr-m-0', id: 'instructeur-filter-combo-label', for: 'search-filter'
|
||||||
%react-fragment
|
%react-fragment
|
||||||
|
@ -8,9 +10,9 @@
|
||||||
|
|
||||||
= label_tag :value, t('.value'), for: 'value', class: 'fr-label'
|
= label_tag :value, t('.value'), for: 'value', class: 'fr-label'
|
||||||
- if column_type == :enum
|
- if column_type == :enum
|
||||||
= select_tag :value, options_for_select(options_for_select_of_column), id: 'value', name: 'value', class: 'fr-select', data: { no_autosubmit: true }
|
= select_tag :filter, options_for_select(options_for_select_of_column), id: 'value', name: "#{prefix}[filter]", class: 'fr-select', data: { no_autosubmit: true }
|
||||||
- else
|
- else
|
||||||
%input#value.fr-input{ type: column_type, name: :value, maxlength: ProcedurePresentation::FILTERS_VALUE_MAX_LENGTH, disabled: column.nil? ? true : false, data: { no_autosubmit: true } }
|
%input#value.fr-input{ type: column_type, name: "#{prefix}[filter]", maxlength: FilteredColumn::FILTERS_VALUE_MAX_LENGTH, disabled: column.nil? ? true : false, data: { no_autosubmit: true } }
|
||||||
|
|
||||||
= hidden_field_tag :statut, statut
|
= hidden_field_tag :statut, statut
|
||||||
= submit_tag t('.add_filter'), class: 'fr-btn fr-btn--secondary fr-mt-2w'
|
= submit_tag t('.add_filter'), class: 'fr-btn fr-btn--secondary fr-mt-2w'
|
||||||
|
|
|
@ -12,7 +12,7 @@ class Instructeurs::ColumnPickerComponent < ApplicationComponent
|
||||||
def displayable_columns_for_select
|
def displayable_columns_for_select
|
||||||
[
|
[
|
||||||
procedure.columns.filter(&:displayable).map { |column| [column.label, column.id] },
|
procedure.columns.filter(&:displayable).map { |column| [column.label, column.id] },
|
||||||
procedure_presentation.displayed_fields.map { Column.new(**_1.deep_symbolize_keys.merge(procedure_id: procedure.id)).id }
|
procedure_presentation.displayed_columns.map(&:id)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,12 @@ class Instructeurs::ColumnTableHeaderComponent < ApplicationComponent
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def classname(column)
|
||||||
|
return 'status-col' if column.dossier_state?
|
||||||
|
return 'number-col' if column.type == :number
|
||||||
|
return 'sva-col' if column.column == 'sva_svr_decision_on'
|
||||||
|
end
|
||||||
|
|
||||||
def update_sort_path(column)
|
def update_sort_path(column)
|
||||||
id = column.id
|
id = column.id
|
||||||
order = opposite_order_for(column)
|
order = opposite_order_for(column)
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
- @columns.each do |column|
|
- @columns.each do |column|
|
||||||
%th{ aria_sort(column), scope: "col", class: column.classname }
|
%th{ aria_sort(column), scope: "col", class: classname(column) }
|
||||||
= link_to label_and_arrow(column), update_sort_path(column)
|
= link_to label_and_arrow(column), update_sort_path(column)
|
||||||
|
|
|
@ -8,7 +8,7 @@ module Administrateurs
|
||||||
helper_method :create_archive_url
|
helper_method :create_archive_url
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@exports = Export.ante_chronological.by_key(all_groupe_instructeurs.map(&:id), nil)
|
@exports = Export.ante_chronological.by_key(all_groupe_instructeurs.map(&:id))
|
||||||
@average_dossier_weight = @procedure.average_dossier_weight
|
@average_dossier_weight = @procedure.average_dossier_weight
|
||||||
@count_dossiers_termines_by_month = @procedure.dossiers.processed_by_month(all_groupe_instructeurs).count
|
@count_dossiers_termines_by_month = @procedure.dossiers.processed_by_month(all_groupe_instructeurs).count
|
||||||
@archives = Archive.for_groupe_instructeur(all_groupe_instructeurs).to_a
|
@archives = Archive.for_groupe_instructeur(all_groupe_instructeurs).to_a
|
||||||
|
|
|
@ -73,7 +73,7 @@ module Instructeurs
|
||||||
# Setting it here to make clear that it is used by the view
|
# Setting it here to make clear that it is used by the view
|
||||||
@procedure_presentation = procedure_presentation
|
@procedure_presentation = procedure_presentation
|
||||||
|
|
||||||
@current_filters = current_filters
|
@current_filters = procedure_presentation.filters_for(statut)
|
||||||
@counts = current_instructeur
|
@counts = current_instructeur
|
||||||
.dossiers_count_summary(groupe_instructeur_ids)
|
.dossiers_count_summary(groupe_instructeur_ids)
|
||||||
.symbolize_keys
|
.symbolize_keys
|
||||||
|
@ -95,7 +95,7 @@ module Instructeurs
|
||||||
|
|
||||||
@has_export_notification = notify_exports?
|
@has_export_notification = notify_exports?
|
||||||
@last_export = last_export_for(statut)
|
@last_export = last_export_for(statut)
|
||||||
@filtered_sorted_ids = procedure_presentation.filtered_sorted_ids(dossiers, statut, count: dossiers_count)
|
@filtered_sorted_ids = DossierFilterService.filtered_sorted_ids(dossiers, statut, procedure_presentation.filters_for(statut), procedure_presentation.sorted_column, current_instructeur, count: dossiers_count)
|
||||||
page = params[:page].presence || 1
|
page = params[:page].presence || 1
|
||||||
|
|
||||||
@dossiers_count = @filtered_sorted_ids.size
|
@dossiers_count = @filtered_sorted_ids.size
|
||||||
|
@ -104,7 +104,7 @@ module Instructeurs
|
||||||
.page(page)
|
.page(page)
|
||||||
.per(ITEMS_PER_PAGE)
|
.per(ITEMS_PER_PAGE)
|
||||||
|
|
||||||
@projected_dossiers = DossierProjectionService.project(@filtered_sorted_paginated_ids, procedure_presentation.displayed_fields)
|
@projected_dossiers = DossierProjectionService.project(@filtered_sorted_paginated_ids, procedure_presentation.displayed_columns)
|
||||||
@disable_checkbox_all = @projected_dossiers.all? { _1.batch_operation_id.present? }
|
@disable_checkbox_all = @projected_dossiers.all? { _1.batch_operation_id.present? }
|
||||||
|
|
||||||
@batch_operations = BatchOperation.joins(:groupe_instructeurs)
|
@batch_operations = BatchOperation.joins(:groupe_instructeurs)
|
||||||
|
@ -133,9 +133,9 @@ module Instructeurs
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_displayed_fields
|
def update_displayed_fields
|
||||||
values = (params['values'].presence || []).reject(&:empty?)
|
ids = (params['values'].presence || []).reject(&:empty?)
|
||||||
|
|
||||||
procedure_presentation.update_displayed_fields(values)
|
procedure_presentation.update!(displayed_columns: ids)
|
||||||
|
|
||||||
redirect_back(fallback_location: instructeur_procedure_url(procedure))
|
redirect_back(fallback_location: instructeur_procedure_url(procedure))
|
||||||
end
|
end
|
||||||
|
@ -147,8 +147,10 @@ module Instructeurs
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_filter
|
def add_filter
|
||||||
if !procedure_presentation.add_filter(statut, params[:column], params[:value])
|
if !procedure_presentation.update(filter_params)
|
||||||
flash.alert = procedure_presentation.errors.full_messages
|
# complicated way to display inner error messages
|
||||||
|
flash.alert = procedure_presentation.errors
|
||||||
|
.flat_map { _1.detail[:value].flat_map { |c| c.errors.full_messages } }
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_back(fallback_location: instructeur_procedure_url(procedure))
|
redirect_back(fallback_location: instructeur_procedure_url(procedure))
|
||||||
|
@ -158,13 +160,10 @@ module Instructeurs
|
||||||
@statut = statut
|
@statut = statut
|
||||||
@procedure = procedure
|
@procedure = procedure
|
||||||
@procedure_presentation = procedure_presentation
|
@procedure_presentation = procedure_presentation
|
||||||
@column = procedure.find_column(h_id: JSON.parse(params[:column], symbolize_names: true))
|
current_filter = procedure_presentation.filters_name_for(@statut)
|
||||||
end
|
# According to the html, the selected column is the last one
|
||||||
|
h_id = JSON.parse(params[current_filter].last[:id], symbolize_names: true)
|
||||||
def remove_filter
|
@column = procedure.find_column(h_id:)
|
||||||
procedure_presentation.remove_filter(statut, params[:column], params[:value])
|
|
||||||
|
|
||||||
redirect_back(fallback_location: instructeur_procedure_url(procedure))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def download_export
|
def download_export
|
||||||
|
@ -383,10 +382,6 @@ module Instructeurs
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_filters
|
|
||||||
@current_filters ||= procedure_presentation.filters.fetch(statut, [])
|
|
||||||
end
|
|
||||||
|
|
||||||
def bulk_message_params
|
def bulk_message_params
|
||||||
params.require(:bulk_message).permit(:body)
|
params.require(:bulk_message).permit(:body)
|
||||||
end
|
end
|
||||||
|
@ -415,5 +410,11 @@ module Instructeurs
|
||||||
def sorted_column_params
|
def sorted_column_params
|
||||||
params.permit(sorted_column: [:order, :id])
|
params.permit(sorted_column: [:order, :id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
keys = [:tous_filters, :a_suivre_filters, :suivis_filters, :traites_filters, :expirant_filters, :archives_filters, :supprimes_filters]
|
||||||
|
h = keys.index_with { [:id, :filter] }
|
||||||
|
params.permit(h)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,10 +3,14 @@
|
||||||
class RechercheController < ApplicationController
|
class RechercheController < ApplicationController
|
||||||
before_action :authenticate_logged_user!
|
before_action :authenticate_logged_user!
|
||||||
ITEMS_PER_PAGE = 25
|
ITEMS_PER_PAGE = 25
|
||||||
|
|
||||||
|
# the columns are generally procedure specific
|
||||||
|
# but in the search context, we are looking for dossiers from multiple procedures
|
||||||
|
# so we are faking the columns with a random procedure_id
|
||||||
PROJECTIONS = [
|
PROJECTIONS = [
|
||||||
{ "table" => 'procedure', "column" => 'libelle' },
|
Column.new(procedure_id: 666, table: 'procedure', column: 'libelle'),
|
||||||
{ "table" => 'user', "column" => 'email' },
|
Column.new(procedure_id: 666, table: 'user', column: 'email'),
|
||||||
{ "table" => 'procedure', "column" => 'procedure_id' }
|
Column.new(procedure_id: 666, table: 'procedure', column: 'procedure_id')
|
||||||
]
|
]
|
||||||
|
|
||||||
def nav_bar_profile
|
def nav_bar_profile
|
||||||
|
|
|
@ -17,7 +17,6 @@ class ExportDashboard < Administrate::BaseDashboard
|
||||||
job_status: Field::Select.with_options(searchable: false, collection: -> (field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }),
|
job_status: Field::Select.with_options(searchable: false, collection: -> (field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }),
|
||||||
key: Field::Text,
|
key: Field::Text,
|
||||||
procedure_presentation: IdField,
|
procedure_presentation: IdField,
|
||||||
procedure_presentation_snapshot: Field::String.with_options(searchable: false),
|
|
||||||
statut: Field::Select.with_options(searchable: false, collection: -> (field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }),
|
statut: Field::Select.with_options(searchable: false, collection: -> (field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }),
|
||||||
time_span_type: Field::Select.with_options(searchable: false, collection: -> (field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }),
|
time_span_type: Field::Select.with_options(searchable: false, collection: -> (field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }),
|
||||||
created_at: Field::DateTime.with_options(format: "%d/%m %H:%M:%S"),
|
created_at: Field::DateTime.with_options(format: "%d/%m %H:%M:%S"),
|
||||||
|
@ -34,7 +33,7 @@ class ExportDashboard < Administrate::BaseDashboard
|
||||||
|
|
||||||
# SHOW_PAGE_ATTRIBUTES
|
# SHOW_PAGE_ATTRIBUTES
|
||||||
# an array of attributes that will be displayed on the model's show page.
|
# an array of attributes that will be displayed on the model's show page.
|
||||||
SHOW_PAGE_ATTRIBUTES = [:id, :procedure, :job_status, :format, :statut, :file, :groupe_instructeurs, :key, :procedure_presentation, :procedure_presentation_snapshot, :time_span_type, :created_at, :updated_at].freeze
|
SHOW_PAGE_ATTRIBUTES = [:id, :procedure, :job_status, :format, :statut, :file, :groupe_instructeurs, :key, :procedure_presentation, :time_span_type, :created_at, :updated_at].freeze
|
||||||
|
|
||||||
# FORM_ATTRIBUTES
|
# FORM_ATTRIBUTES
|
||||||
# an array of attributes that will be displayed
|
# an array of attributes that will be displayed
|
||||||
|
|
|
@ -10,28 +10,31 @@ class AssignTo < ApplicationRecord
|
||||||
|
|
||||||
def procedure_presentation_or_default_and_errors
|
def procedure_presentation_or_default_and_errors
|
||||||
errors = reset_procedure_presentation_if_invalid
|
errors = reset_procedure_presentation_if_invalid
|
||||||
|
|
||||||
if self.procedure_presentation.nil?
|
if self.procedure_presentation.nil?
|
||||||
self.procedure_presentation = build_procedure_presentation
|
self.procedure_presentation = create_procedure_presentation!
|
||||||
self.procedure_presentation.save if procedure_presentation.valid? && !procedure_presentation.persisted?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
[self.procedure_presentation, errors]
|
[self.procedure_presentation, errors]
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def reset_procedure_presentation_if_invalid
|
def reset_procedure_presentation_if_invalid
|
||||||
if procedure_presentation&.invalid?
|
errors = begin
|
||||||
# This is a last defense against invalid `ProcedurePresentation`s persistently
|
procedure_presentation.errors if procedure_presentation&.invalid?
|
||||||
# hindering instructeurs. Whenever this gets triggered, it means that there is
|
rescue ActiveRecord::RecordNotFound => e
|
||||||
# a bug somewhere else that we need to fix.
|
[e.message]
|
||||||
|
end
|
||||||
|
|
||||||
errors = procedure_presentation.errors
|
if errors.present?
|
||||||
Sentry.capture_message(
|
Sentry.capture_message(
|
||||||
"Destroying invalid ProcedurePresentation",
|
"Destroying invalid ProcedurePresentation",
|
||||||
extra: { procedure_presentation: procedure_presentation.as_json }
|
extra: { procedure_presentation_id: procedure_presentation.id, errors: }
|
||||||
)
|
)
|
||||||
self.procedure_presentation = nil
|
self.procedure_presentation = nil
|
||||||
errors
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
errors
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Column
|
class Column
|
||||||
|
# include validations to enable procedure_presentation.validate_associate,
|
||||||
|
# which enforces the deserialization of columns in the displayed_columns attribute
|
||||||
|
# and raises an error if a column is not found
|
||||||
|
include ActiveModel::Validations
|
||||||
|
|
||||||
TYPE_DE_CHAMP_TABLE = 'type_de_champ'
|
TYPE_DE_CHAMP_TABLE = 'type_de_champ'
|
||||||
|
|
||||||
attr_reader :table, :column, :label, :classname, :type, :scope, :value_column, :filterable, :displayable
|
attr_reader :table, :column, :label, :type, :scope, :value_column, :filterable, :displayable
|
||||||
|
|
||||||
def initialize(procedure_id:, table:, column:, label: nil, type: :text, value_column: :value, filterable: true, displayable: true, classname: '', scope: '')
|
def initialize(procedure_id:, table:, column:, label: nil, type: :text, value_column: :value, filterable: true, displayable: true, scope: '')
|
||||||
@procedure_id = procedure_id
|
@procedure_id = procedure_id
|
||||||
@table = table
|
@table = table
|
||||||
@column = column
|
@column = column
|
||||||
@label = label || I18n.t(column, scope: [:activerecord, :attributes, :procedure_presentation, :fields, table])
|
@label = label || I18n.t(column, scope: [:activerecord, :attributes, :procedure_presentation, :fields, table])
|
||||||
@classname = classname
|
|
||||||
@type = type
|
@type = type
|
||||||
@scope = scope
|
@scope = scope
|
||||||
@value_column = value_column
|
@value_column = value_column
|
||||||
|
@ -29,15 +33,21 @@ class Column
|
||||||
|
|
||||||
def to_json
|
def to_json
|
||||||
{
|
{
|
||||||
table:, column:, label:, classname:, type:, scope:, value_column:, filterable:, displayable:
|
table:, column:, label:, type:, scope:, value_column:, filterable:, displayable:
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def notifications?
|
def notifications? = [table, column] == ['notifications', 'notifications']
|
||||||
table == 'notifications' && column == 'notifications'
|
|
||||||
end
|
def dossier_state? = [table, column] == ['self', 'state']
|
||||||
|
|
||||||
def self.find(h_id)
|
def self.find(h_id)
|
||||||
Procedure.with_discarded.find(h_id[:procedure_id]).find_column(h_id:)
|
begin
|
||||||
|
procedure = Procedure.with_discarded.find(h_id[:procedure_id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
raise ActiveRecord::RecordNotFound.new("Column: unable to find procedure #{h_id[:procedure_id]} from h_id #{h_id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
procedure.find_column(h_id: h_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,7 @@ module ColumnsConcern
|
||||||
column = columns.find { _1.h_id == h_id } if h_id.present?
|
column = columns.find { _1.h_id == h_id } if h_id.present?
|
||||||
column = columns.find { _1.label == label } if label.present?
|
column = columns.find { _1.label == label } if label.present?
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if column.nil?
|
raise ActiveRecord::RecordNotFound.new("Column: unable to find h_id: #{h_id} or label: #{label} for procedure_id #{id}") if column.nil?
|
||||||
|
|
||||||
column
|
column
|
||||||
end
|
end
|
||||||
|
@ -30,7 +30,11 @@ module ColumnsConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
def dossier_id_column
|
def dossier_id_column
|
||||||
Column.new(procedure_id: id, table: 'self', column: 'id', classname: 'number-col', type: :number)
|
Column.new(procedure_id: id, table: 'self', column: 'id', type: :number)
|
||||||
|
end
|
||||||
|
|
||||||
|
def dossier_state_column
|
||||||
|
Column.new(procedure_id: id, table: 'self', column: 'state', type: :enum, scope: 'instructeurs.dossiers.filterable_state', displayable: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def notifications_column
|
def notifications_column
|
||||||
|
@ -46,25 +50,23 @@ module ColumnsConcern
|
||||||
non_displayable_dates = ['updated_since', 'depose_since', 'en_construction_since', 'en_instruction_since', 'processed_since']
|
non_displayable_dates = ['updated_since', 'depose_since', 'en_construction_since', 'en_instruction_since', 'processed_since']
|
||||||
.map { |column| Column.new(procedure_id: id, table: 'self', column:, type: :date, displayable: false) }
|
.map { |column| Column.new(procedure_id: id, table: 'self', column:, type: :date, displayable: false) }
|
||||||
|
|
||||||
states = [Column.new(procedure_id: id, table: 'self', column: 'state', type: :enum, scope: 'instructeurs.dossiers.filterable_state', displayable: false)]
|
states = [dossier_state_column]
|
||||||
|
|
||||||
[common, dates, sva_svr_columns(for_filters: true), non_displayable_dates, states].flatten.compact
|
[common, dates, sva_svr_columns, non_displayable_dates, states].flatten.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def sva_svr_columns(for_filters: false)
|
def sva_svr_columns
|
||||||
return if !sva_svr_enabled?
|
return if !sva_svr_enabled?
|
||||||
|
|
||||||
scope = [:activerecord, :attributes, :procedure_presentation, :fields, :self]
|
scope = [:activerecord, :attributes, :procedure_presentation, :fields, :self]
|
||||||
|
|
||||||
columns = [
|
columns = [
|
||||||
Column.new(procedure_id: id, table: 'self', column: 'sva_svr_decision_on', type: :date,
|
Column.new(procedure_id: id, table: 'self', column: 'sva_svr_decision_on', type: :date,
|
||||||
label: I18n.t("#{sva_svr_decision}_decision_on", scope:), classname: for_filters ? '' : 'sva-col')
|
label: I18n.t("#{sva_svr_decision}_decision_on", scope:))
|
||||||
]
|
]
|
||||||
|
|
||||||
if for_filters
|
columns << Column.new(procedure_id: id, table: 'self', column: 'sva_svr_decision_before', type: :date, displayable: false,
|
||||||
columns << Column.new(procedure_id: id, table: 'self', column: 'sva_svr_decision_before', type: :date, displayable: false,
|
label: I18n.t("#{sva_svr_decision}_decision_before", scope:))
|
||||||
label: I18n.t("#{sva_svr_decision}_decision_before", scope:))
|
|
||||||
end
|
|
||||||
|
|
||||||
columns
|
columns
|
||||||
end
|
end
|
||||||
|
@ -73,11 +75,17 @@ module ColumnsConcern
|
||||||
SortedColumn.new(column: notifications_column, order: 'desc')
|
SortedColumn.new(column: notifications_column, order: 'desc')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def default_displayed_columns = [email_column]
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def email_column
|
||||||
|
Column.new(procedure_id: id, table: 'user', column: 'email')
|
||||||
|
end
|
||||||
|
|
||||||
def standard_columns
|
def standard_columns
|
||||||
[
|
[
|
||||||
Column.new(procedure_id: id, table: 'user', column: 'email'),
|
email_column,
|
||||||
Column.new(procedure_id: id, table: 'followers_instructeurs', column: 'email'),
|
Column.new(procedure_id: id, table: 'followers_instructeurs', column: 'email'),
|
||||||
Column.new(procedure_id: id, table: 'groupe_instructeur', column: 'id', type: :enum),
|
Column.new(procedure_id: id, table: 'groupe_instructeur', column: 'id', type: :enum),
|
||||||
Column.new(procedure_id: id, table: 'avis', column: 'question_answer', filterable: false) # not filterable ?
|
Column.new(procedure_id: id, table: 'avis', column: 'question_answer', filterable: false) # not filterable ?
|
||||||
|
|
|
@ -29,13 +29,13 @@ module DossierFilteringConcern
|
||||||
}
|
}
|
||||||
|
|
||||||
scope :filter_ilike, lambda { |table, column, values|
|
scope :filter_ilike, lambda { |table, column, values|
|
||||||
table_column = ProcedurePresentation.sanitized_column(table, column)
|
table_column = DossierFilterService.sanitized_column(table, column)
|
||||||
q = Array.new(values.count, "(#{table_column} ILIKE ?)").join(' OR ')
|
q = Array.new(values.count, "(#{table_column} ILIKE ?)").join(' OR ')
|
||||||
where(q, *(values.map { |value| "%#{value}%" }))
|
where(q, *(values.map { |value| "%#{value}%" }))
|
||||||
}
|
}
|
||||||
|
|
||||||
scope :filter_enum, lambda { |table, column, values|
|
scope :filter_enum, lambda { |table, column, values|
|
||||||
table_column = ProcedurePresentation.sanitized_column(table, column)
|
table_column = DossierFilterService.sanitized_column(table, column)
|
||||||
q = Array.new(values.count, "(#{table_column} = ?)").join(' OR ')
|
q = Array.new(values.count, "(#{table_column} = ?)").join(' OR ')
|
||||||
where(q, *(values))
|
where(q, *(values))
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
class Export < ApplicationRecord
|
class Export < ApplicationRecord
|
||||||
include TransientModelsWithPurgeableJobConcern
|
include TransientModelsWithPurgeableJobConcern
|
||||||
|
|
||||||
|
self.ignored_columns += ["procedure_presentation_snapshot"]
|
||||||
|
|
||||||
MAX_DUREE_CONSERVATION_EXPORT = 32.hours
|
MAX_DUREE_CONSERVATION_EXPORT = 32.hours
|
||||||
MAX_DUREE_GENERATION = 16.hours
|
MAX_DUREE_GENERATION = 16.hours
|
||||||
|
|
||||||
|
@ -37,6 +39,9 @@ class Export < ApplicationRecord
|
||||||
|
|
||||||
has_one_attached :file
|
has_one_attached :file
|
||||||
|
|
||||||
|
attribute :sorted_column, :sorted_column
|
||||||
|
attribute :filtered_columns, :filtered_column, array: true
|
||||||
|
|
||||||
validates :format, :groupe_instructeurs, :key, presence: true
|
validates :format, :groupe_instructeurs, :key, presence: true
|
||||||
|
|
||||||
scope :ante_chronological, -> { order(updated_at: :desc) }
|
scope :ante_chronological, -> { order(updated_at: :desc) }
|
||||||
|
@ -56,7 +61,6 @@ class Export < ApplicationRecord
|
||||||
|
|
||||||
def compute
|
def compute
|
||||||
self.dossiers_count = dossiers_for_export.count
|
self.dossiers_count = dossiers_for_export.count
|
||||||
load_snapshot!
|
|
||||||
|
|
||||||
file.attach(blob.signed_id) # attaching a blob directly might run identify/virus scanner and wipe it
|
file.attach(blob.signed_id) # attaching a blob directly might run identify/virus scanner and wipe it
|
||||||
end
|
end
|
||||||
|
@ -65,17 +69,16 @@ class Export < ApplicationRecord
|
||||||
time_span_type == Export.time_span_types.fetch(:monthly) ? 30.days.ago : nil
|
time_span_type == Export.time_span_types.fetch(:monthly) ? 30.days.ago : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered?
|
|
||||||
procedure_presentation_id.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.find_or_create_fresh_export(format, groupe_instructeurs, user_profile, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil, export_template: nil)
|
def self.find_or_create_fresh_export(format, groupe_instructeurs, user_profile, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil, export_template: nil)
|
||||||
|
filtered_columns = Array.wrap(procedure_presentation&.filters_for(statut))
|
||||||
|
sorted_column = procedure_presentation&.sorted_column
|
||||||
|
|
||||||
attributes = {
|
attributes = {
|
||||||
format:,
|
format:,
|
||||||
export_template:,
|
export_template:,
|
||||||
time_span_type:,
|
time_span_type:,
|
||||||
statut:,
|
statut:,
|
||||||
key: generate_cache_key(groupe_instructeurs.map(&:id), procedure_presentation)
|
key: generate_cache_key(groupe_instructeurs.map(&:id), filtered_columns, sorted_column)
|
||||||
}
|
}
|
||||||
|
|
||||||
recent_export = pending
|
recent_export = pending
|
||||||
|
@ -87,36 +90,30 @@ class Export < ApplicationRecord
|
||||||
|
|
||||||
create!(**attributes, groupe_instructeurs:,
|
create!(**attributes, groupe_instructeurs:,
|
||||||
user_profile:,
|
user_profile:,
|
||||||
procedure_presentation:,
|
filtered_columns:,
|
||||||
procedure_presentation_snapshot: procedure_presentation&.snapshot)
|
sorted_column:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.for_groupe_instructeurs(groupe_instructeurs_ids)
|
def self.for_groupe_instructeurs(groupe_instructeurs_ids)
|
||||||
joins(:groupe_instructeurs).where(groupe_instructeurs: groupe_instructeurs_ids).distinct(:id)
|
joins(:groupe_instructeurs).where(groupe_instructeurs: groupe_instructeurs_ids).distinct(:id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.by_key(groupe_instructeurs_ids, procedure_presentation)
|
def self.by_key(groupe_instructeurs_ids)
|
||||||
where(key: [
|
where(key: generate_cache_key(groupe_instructeurs_ids))
|
||||||
generate_cache_key(groupe_instructeurs_ids),
|
|
||||||
generate_cache_key(groupe_instructeurs_ids, procedure_presentation)
|
|
||||||
])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.generate_cache_key(groupe_instructeurs_ids, procedure_presentation = nil)
|
def self.generate_cache_key(groupe_instructeurs_ids, filtered_columns = [], sorted_column = nil)
|
||||||
if procedure_presentation.present?
|
columns_key = ([sorted_column] + filtered_columns).compact.map(&:id).sort.join
|
||||||
[
|
|
||||||
groupe_instructeurs_ids.sort.join('-'),
|
[
|
||||||
procedure_presentation.id,
|
groupe_instructeurs_ids.sort.join('-'),
|
||||||
Digest::MD5.hexdigest(procedure_presentation.snapshot.slice(:filters, :sort).to_s)
|
Digest::MD5.hexdigest(columns_key)
|
||||||
].join('--')
|
].join('--')
|
||||||
else
|
|
||||||
groupe_instructeurs_ids.sort.join('-')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def count
|
def count
|
||||||
return dossiers_count if !dossiers_count.nil? # export generated
|
return dossiers_count if !dossiers_count.nil? # export generated
|
||||||
return dossiers_for_export.count if procedure_presentation_id.present?
|
return dossiers_for_export.count if built_from_procedure_presentation?
|
||||||
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
@ -125,23 +122,21 @@ class Export < ApplicationRecord
|
||||||
groupe_instructeurs.first.procedure
|
groupe_instructeurs.first.procedure
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def built_from_procedure_presentation?
|
||||||
|
sorted_column.present? # hack has we know that procedure_presentation always has a sorted_column
|
||||||
def load_snapshot!
|
|
||||||
if procedure_presentation_snapshot.present?
|
|
||||||
procedure_presentation.attributes = procedure_presentation_snapshot
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def dossiers_for_export
|
def dossiers_for_export
|
||||||
@dossiers_for_export ||= begin
|
@dossiers_for_export ||= begin
|
||||||
dossiers = Dossier.where(groupe_instructeur: groupe_instructeurs)
|
dossiers = Dossier.where(groupe_instructeur: groupe_instructeurs)
|
||||||
|
|
||||||
if since.present?
|
if since.present?
|
||||||
dossiers.visible_by_administration.where('dossiers.depose_at > ?', since)
|
dossiers.visible_by_administration.where('dossiers.depose_at > ?', since)
|
||||||
elsif procedure_presentation.present?
|
elsif filtered_columns.present? || sorted_column.present?
|
||||||
filtered_sorted_ids = procedure_presentation
|
instructeur = instructeur_from(user_profile)
|
||||||
.filtered_sorted_ids(dossiers, statut)
|
filtered_sorted_ids = DossierFilterService.filtered_sorted_ids(dossiers, statut, filtered_columns, sorted_column, instructeur)
|
||||||
|
|
||||||
dossiers.where(id: filtered_sorted_ids)
|
dossiers.where(id: filtered_sorted_ids)
|
||||||
else
|
else
|
||||||
|
@ -150,6 +145,15 @@ class Export < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def instructeur_from(user_profile)
|
||||||
|
case user_profile
|
||||||
|
when Administrateur
|
||||||
|
user_profile.instructeur
|
||||||
|
when Instructeur
|
||||||
|
user_profile
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def blob
|
def blob
|
||||||
service = ProcedureExportService.new(procedure, dossiers_for_export, user_profile, export_template)
|
service = ProcedureExportService.new(procedure, dossiers_for_export, user_profile, export_template)
|
||||||
|
|
||||||
|
|
46
app/models/filtered_column.rb
Normal file
46
app/models/filtered_column.rb
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class FilteredColumn
|
||||||
|
include ActiveModel::Validations
|
||||||
|
|
||||||
|
FILTERS_VALUE_MAX_LENGTH = 4048
|
||||||
|
# https://www.postgresql.org/docs/current/datatype-numeric.html
|
||||||
|
PG_INTEGER_MAX_VALUE = 2147483647
|
||||||
|
|
||||||
|
attr_reader :column, :filter
|
||||||
|
|
||||||
|
delegate :label, to: :column
|
||||||
|
|
||||||
|
validate :check_filter_max_length
|
||||||
|
validate :check_filter_max_integer
|
||||||
|
|
||||||
|
def initialize(column:, filter:)
|
||||||
|
@column = column
|
||||||
|
@filter = filter
|
||||||
|
end
|
||||||
|
|
||||||
|
def ==(other)
|
||||||
|
other&.column == column && other.filter == filter
|
||||||
|
end
|
||||||
|
|
||||||
|
def id
|
||||||
|
column.h_id.merge(filter:).sort.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_filter_max_length
|
||||||
|
if @filter.present? && @filter.length.to_i > FILTERS_VALUE_MAX_LENGTH
|
||||||
|
errors.add(
|
||||||
|
:base,
|
||||||
|
"Le filtre #{label} est trop long (maximum: #{FILTERS_VALUE_MAX_LENGTH} caractères)"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_filter_max_integer
|
||||||
|
if @column.column == 'id' && @filter.to_i > PG_INTEGER_MAX_VALUE
|
||||||
|
errors.add(:base, "Le filtre #{label} n'est pas un numéro de dossier possible")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,38 +1,31 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ProcedurePresentation < ApplicationRecord
|
class ProcedurePresentation < ApplicationRecord
|
||||||
TABLE = 'table'
|
|
||||||
COLUMN = 'column'
|
|
||||||
ORDER = 'order'
|
|
||||||
|
|
||||||
SLASH = '/'
|
|
||||||
TYPE_DE_CHAMP = 'type_de_champ'
|
TYPE_DE_CHAMP = 'type_de_champ'
|
||||||
|
|
||||||
FILTERS_VALUE_MAX_LENGTH = 4048
|
|
||||||
# https://www.postgresql.org/docs/current/datatype-numeric.html
|
|
||||||
PG_INTEGER_MAX_VALUE = 2147483647
|
|
||||||
|
|
||||||
belongs_to :assign_to, optional: false
|
belongs_to :assign_to, optional: false
|
||||||
has_many :exports, dependent: :destroy
|
has_many :exports, dependent: :destroy
|
||||||
|
|
||||||
delegate :procedure, :instructeur, to: :assign_to
|
delegate :procedure, :instructeur, to: :assign_to
|
||||||
|
|
||||||
validate :check_allowed_displayed_fields
|
attribute :displayed_columns, :column, array: true
|
||||||
validate :check_allowed_filter_columns
|
|
||||||
validate :check_filters_max_length
|
|
||||||
validate :check_filters_max_integer
|
|
||||||
|
|
||||||
attribute :sorted_column, :sorted_column
|
attribute :sorted_column, :sorted_column
|
||||||
def sorted_column = super || procedure.default_sorted_column # Dummy override to set default value
|
def sorted_column = super || procedure.default_sorted_column # Dummy override to set default value
|
||||||
|
|
||||||
attribute :a_suivre_filters, :jsonb, array: true
|
attribute :a_suivre_filters, :filtered_column, array: true
|
||||||
attribute :suivis_filters, :jsonb, array: true
|
attribute :suivis_filters, :filtered_column, array: true
|
||||||
attribute :traites_filters, :jsonb, array: true
|
attribute :traites_filters, :filtered_column, array: true
|
||||||
attribute :tous_filters, :jsonb, array: true
|
attribute :tous_filters, :filtered_column, array: true
|
||||||
attribute :supprimes_filters, :jsonb, array: true
|
attribute :supprimes_filters, :filtered_column, array: true
|
||||||
attribute :supprimes_recemment_filters, :jsonb, array: true
|
attribute :supprimes_recemment_filters, :filtered_column, array: true
|
||||||
attribute :expirant_filters, :jsonb, array: true
|
attribute :expirant_filters, :filtered_column, array: true
|
||||||
attribute :archives_filters, :jsonb, array: true
|
attribute :archives_filters, :filtered_column, array: true
|
||||||
|
|
||||||
|
before_create { self.displayed_columns = procedure.default_displayed_columns }
|
||||||
|
|
||||||
|
validates_associated :displayed_columns, :sorted_column, :a_suivre_filters, :suivis_filters,
|
||||||
|
:traites_filters, :tous_filters, :supprimes_filters, :expirant_filters, :archives_filters
|
||||||
|
|
||||||
def filters_for(statut)
|
def filters_for(statut)
|
||||||
send(filters_name_for(statut))
|
send(filters_name_for(statut))
|
||||||
|
@ -42,46 +35,35 @@ class ProcedurePresentation < ApplicationRecord
|
||||||
|
|
||||||
def displayed_fields_for_headers
|
def displayed_fields_for_headers
|
||||||
[
|
[
|
||||||
Column.new(procedure_id: procedure.id, table: 'self', column: 'id', classname: 'number-col'),
|
procedure.dossier_id_column,
|
||||||
*displayed_fields.map { Column.new(**_1.deep_symbolize_keys.merge(procedure_id: procedure.id)) },
|
*displayed_columns,
|
||||||
Column.new(procedure_id: procedure.id, table: 'self', column: 'state', classname: 'state-col'),
|
procedure.dossier_state_column,
|
||||||
*procedure.sva_svr_columns
|
*procedure.sva_svr_columns
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_sorted_ids(dossiers, statut, count: nil)
|
def human_value_for_filter(filtered_column)
|
||||||
dossiers_by_statut = dossiers.by_statut(statut, instructeur)
|
if filtered_column.column.table == TYPE_DE_CHAMP
|
||||||
dossiers_sorted_ids = self.sorted_ids(dossiers_by_statut, count || dossiers_by_statut.size)
|
find_type_de_champ(filtered_column.column.column).dynamic_type.filter_to_human(filtered_column.filter)
|
||||||
|
elsif filtered_column.column.column == 'state'
|
||||||
if filters[statut].present?
|
if filtered_column.filter == 'pending_correction'
|
||||||
dossiers_sorted_ids.intersection(filtered_ids(dossiers_by_statut, statut))
|
|
||||||
else
|
|
||||||
dossiers_sorted_ids
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def human_value_for_filter(filter)
|
|
||||||
if filter[TABLE] == TYPE_DE_CHAMP
|
|
||||||
find_type_de_champ(filter[COLUMN]).dynamic_type.filter_to_human(filter['value'])
|
|
||||||
elsif filter['column'] == 'state'
|
|
||||||
if filter['value'] == 'pending_correction'
|
|
||||||
Dossier.human_attribute_name("pending_correction.for_instructeur")
|
Dossier.human_attribute_name("pending_correction.for_instructeur")
|
||||||
else
|
else
|
||||||
Dossier.human_attribute_name("state.#{filter['value']}")
|
Dossier.human_attribute_name("state.#{filtered_column.filter}")
|
||||||
end
|
end
|
||||||
elsif filter['table'] == 'groupe_instructeur' && filter['column'] == 'id'
|
elsif filtered_column.column.table == 'groupe_instructeur' && filtered_column.column.column == 'id'
|
||||||
instructeur.groupe_instructeurs
|
instructeur.groupe_instructeurs
|
||||||
.find { _1.id == filter['value'].to_i }&.label || filter['value']
|
.find { _1.id == filtered_column.filter.to_i }&.label || filtered_column.filter
|
||||||
else
|
else
|
||||||
column = procedure.columns.find { _1.table == filter[TABLE] && _1.column == filter[COLUMN] }
|
column = procedure.columns.find { _1.table == filtered_column.column.table && _1.column == filtered_column.column.column }
|
||||||
|
|
||||||
if column.type == :date
|
if column.type == :date
|
||||||
parsed_date = safe_parse_date(filter['value'])
|
parsed_date = safe_parse_date(filtered_column.filter)
|
||||||
|
|
||||||
return parsed_date.present? ? I18n.l(parsed_date) : nil
|
return parsed_date.present? ? I18n.l(parsed_date) : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
filter['value']
|
filtered_column.filter
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -91,179 +73,8 @@ class ProcedurePresentation < ApplicationRecord
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_filter(statut, column_id, value)
|
|
||||||
h_id = JSON.parse(column_id, symbolize_names: true)
|
|
||||||
|
|
||||||
if value.present?
|
|
||||||
column = procedure.find_column(h_id:)
|
|
||||||
|
|
||||||
case column.table
|
|
||||||
when TYPE_DE_CHAMP
|
|
||||||
value = find_type_de_champ(column.column).dynamic_type.human_to_filter(value)
|
|
||||||
end
|
|
||||||
|
|
||||||
updated_filters = filters.dup
|
|
||||||
updated_filters[statut] << {
|
|
||||||
'label' => column.label,
|
|
||||||
TABLE => column.table,
|
|
||||||
COLUMN => column.column,
|
|
||||||
'value_column' => column.value_column,
|
|
||||||
'value' => value
|
|
||||||
}
|
|
||||||
|
|
||||||
filters_for(statut) << { id: h_id, filter: value }
|
|
||||||
update(filters: updated_filters)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_filter(statut, column_id, value)
|
|
||||||
h_id = JSON.parse(column_id, symbolize_names: true)
|
|
||||||
column = procedure.find_column(h_id:)
|
|
||||||
updated_filters = filters.dup
|
|
||||||
|
|
||||||
updated_filters[statut] = filters[statut].reject do |filter|
|
|
||||||
filter.values_at(TABLE, COLUMN, 'value') == [column.table, column.column, value]
|
|
||||||
end
|
|
||||||
|
|
||||||
collection = filters_for(statut)
|
|
||||||
collection.delete(collection.find { sym_h = _1.deep_symbolize_keys; sym_h[:id] == h_id && sym_h[:filter] == value })
|
|
||||||
|
|
||||||
update!(filters: updated_filters)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_displayed_fields(column_ids)
|
|
||||||
h_ids = Array.wrap(column_ids).map { |id| JSON.parse(id, symbolize_names: true) }
|
|
||||||
columns = h_ids.map { |h_id| procedure.find_column(h_id:) }
|
|
||||||
|
|
||||||
update!(
|
|
||||||
displayed_fields: columns,
|
|
||||||
displayed_columns: columns.map(&:h_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
if !sorted_column.column.in?(columns)
|
|
||||||
update(sorted_column: nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def snapshot
|
|
||||||
slice(:filters, :sort, :displayed_fields)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def sorted_ids(dossiers, count)
|
|
||||||
table = sorted_column.column.table
|
|
||||||
column = sorted_column.column.column
|
|
||||||
order = sorted_column.order
|
|
||||||
|
|
||||||
case table
|
|
||||||
when 'notifications'
|
|
||||||
dossiers_id_with_notification = dossiers.merge(instructeur.followed_dossiers).with_notifications.ids
|
|
||||||
if order == 'desc'
|
|
||||||
dossiers_id_with_notification +
|
|
||||||
(dossiers.order('dossiers.updated_at desc').ids - dossiers_id_with_notification)
|
|
||||||
else
|
|
||||||
(dossiers.order('dossiers.updated_at asc').ids - dossiers_id_with_notification) +
|
|
||||||
dossiers_id_with_notification
|
|
||||||
end
|
|
||||||
when TYPE_DE_CHAMP
|
|
||||||
ids = dossiers
|
|
||||||
.with_type_de_champ(column)
|
|
||||||
.order("champs.value #{order}")
|
|
||||||
.pluck(:id)
|
|
||||||
if ids.size != count
|
|
||||||
rest = dossiers.where.not(id: ids).order(id: order).pluck(:id)
|
|
||||||
order == 'asc' ? ids + rest : rest + ids
|
|
||||||
else
|
|
||||||
ids
|
|
||||||
end
|
|
||||||
when 'followers_instructeurs'
|
|
||||||
assert_supported_column(table, column)
|
|
||||||
# LEFT OUTER JOIN allows to keep dossiers without assigned instructeurs yet
|
|
||||||
dossiers
|
|
||||||
.includes(:followers_instructeurs)
|
|
||||||
.joins('LEFT OUTER JOIN users instructeurs_users ON instructeurs_users.id = instructeurs.user_id')
|
|
||||||
.order("instructeurs_users.email #{order}")
|
|
||||||
.pluck(:id)
|
|
||||||
.uniq
|
|
||||||
when 'avis'
|
|
||||||
dossiers.includes(table)
|
|
||||||
.order("#{self.class.sanitized_column(table, column)} #{order}")
|
|
||||||
.pluck(:id)
|
|
||||||
.uniq
|
|
||||||
when 'self', 'user', 'individual', 'etablissement', 'groupe_instructeur'
|
|
||||||
(table == 'self' ? dossiers : dossiers.includes(table))
|
|
||||||
.order("#{self.class.sanitized_column(table, column)} #{order}")
|
|
||||||
.pluck(:id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def filtered_ids(dossiers, statut)
|
|
||||||
filters.fetch(statut)
|
|
||||||
.group_by { |filter| filter.values_at(TABLE, COLUMN) }
|
|
||||||
.map do |(table, column), filters|
|
|
||||||
values = filters.pluck('value')
|
|
||||||
value_column = filters.pluck('value_column').compact.first || :value
|
|
||||||
dossier_column = procedure.find_column(h_id: { procedure_id: procedure.id, column_id: "#{table}/#{column}" }) # hack to find json path columns
|
|
||||||
if dossier_column.is_a?(Columns::JSONPathColumn)
|
|
||||||
dossier_column.filtered_ids(dossiers, values)
|
|
||||||
else
|
|
||||||
case table
|
|
||||||
when 'self'
|
|
||||||
if dossier_column.type == :date
|
|
||||||
dates = values
|
|
||||||
.filter_map { |v| Time.zone.parse(v).beginning_of_day rescue nil }
|
|
||||||
|
|
||||||
dossiers.filter_by_datetimes(column, dates)
|
|
||||||
elsif dossier_column.column == "state" && values.include?("pending_correction")
|
|
||||||
dossiers.joins(:corrections).where(corrections: DossierCorrection.pending)
|
|
||||||
elsif dossier_column.column == "state" && values.include?("en_construction")
|
|
||||||
dossiers.where("dossiers.#{column} IN (?)", values).includes(:corrections).where.not(corrections: DossierCorrection.pending)
|
|
||||||
else
|
|
||||||
dossiers.where("dossiers.#{column} IN (?)", values)
|
|
||||||
end
|
|
||||||
when TYPE_DE_CHAMP
|
|
||||||
if dossier_column.type == :enum
|
|
||||||
dossiers.with_type_de_champ(column)
|
|
||||||
.filter_enum(:champs, value_column, values)
|
|
||||||
else
|
|
||||||
dossiers.with_type_de_champ(column)
|
|
||||||
.filter_ilike(:champs, value_column, values)
|
|
||||||
end
|
|
||||||
when 'etablissement'
|
|
||||||
if column == 'entreprise_date_creation'
|
|
||||||
dates = values
|
|
||||||
.filter_map { |v| v.to_date rescue nil }
|
|
||||||
|
|
||||||
dossiers
|
|
||||||
.includes(table)
|
|
||||||
.where(table.pluralize => { column => dates })
|
|
||||||
else
|
|
||||||
dossiers
|
|
||||||
.includes(table)
|
|
||||||
.filter_ilike(table, column, values)
|
|
||||||
end
|
|
||||||
when 'followers_instructeurs'
|
|
||||||
assert_supported_column(table, column)
|
|
||||||
dossiers
|
|
||||||
.includes(:followers_instructeurs)
|
|
||||||
.joins('INNER JOIN users instructeurs_users ON instructeurs_users.id = instructeurs.user_id')
|
|
||||||
.filter_ilike('instructeurs_users', :email, values) # ilike OK, user may want to search by *@domain
|
|
||||||
when 'user', 'individual' # user_columns: [email], individual_columns: ['nom', 'prenom', 'gender']
|
|
||||||
dossiers
|
|
||||||
.includes(table)
|
|
||||||
.filter_ilike(table, column, values) # ilike or where column == 'value' are both valid, we opted for ilike
|
|
||||||
when 'groupe_instructeur'
|
|
||||||
assert_supported_column(table, column)
|
|
||||||
|
|
||||||
dossiers
|
|
||||||
.joins(:groupe_instructeur)
|
|
||||||
.where(groupe_instructeur_id: values)
|
|
||||||
end.pluck(:id)
|
|
||||||
end
|
|
||||||
end.reduce(:&)
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_type_de_champ(column)
|
def find_type_de_champ(column)
|
||||||
TypeDeChamp
|
TypeDeChamp
|
||||||
.joins(:revision_types_de_champ)
|
.joins(:revision_types_de_champ)
|
||||||
|
@ -271,83 +82,4 @@ class ProcedurePresentation < ApplicationRecord
|
||||||
.order(created_at: :desc)
|
.order(created_at: :desc)
|
||||||
.find_by(stable_id: column)
|
.find_by(stable_id: column)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_allowed_displayed_fields
|
|
||||||
displayed_fields.each do |field|
|
|
||||||
check_allowed_field(:displayed_fields, field)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_allowed_filter_columns
|
|
||||||
filters.each do |key, columns|
|
|
||||||
return true if key == 'migrated'
|
|
||||||
columns.each do |column|
|
|
||||||
check_allowed_field(:filters, column)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_allowed_field(kind, field, extra_columns = {})
|
|
||||||
table, column = field.values_at(TABLE, COLUMN)
|
|
||||||
if !valid_column?(table, column, extra_columns)
|
|
||||||
errors.add(kind, "#{table}.#{column} n’est pas une colonne permise")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_filters_max_length
|
|
||||||
filters.values.flatten.each do |filter|
|
|
||||||
next if !filter.is_a?(Hash)
|
|
||||||
next if filter['value']&.length.to_i <= FILTERS_VALUE_MAX_LENGTH
|
|
||||||
|
|
||||||
errors.add(:base, "Le filtre #{filter['label']} est trop long (maximum: #{FILTERS_VALUE_MAX_LENGTH} caractères)")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_filters_max_integer
|
|
||||||
filters.values.flatten.each do |filter|
|
|
||||||
next if !filter.is_a?(Hash)
|
|
||||||
next if filter['column'] != 'id'
|
|
||||||
next if filter['value']&.to_i&. < PG_INTEGER_MAX_VALUE
|
|
||||||
|
|
||||||
errors.add(:base, "Le filtre #{filter['label']} n'est pas un numéro de dossier possible")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def valid_column?(table, column, extra_columns = {})
|
|
||||||
valid_columns_for_table(table).include?(column) ||
|
|
||||||
extra_columns[table]&.include?(column)
|
|
||||||
end
|
|
||||||
|
|
||||||
def valid_columns_for_table(table)
|
|
||||||
@column_whitelist ||= procedure.columns
|
|
||||||
.group_by(&:table)
|
|
||||||
.transform_values { |columns| Set.new(columns.map(&:column)) }
|
|
||||||
|
|
||||||
@column_whitelist[table] || []
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.sanitized_column(association, column)
|
|
||||||
table = if association == 'self'
|
|
||||||
Dossier.table_name
|
|
||||||
elsif (association_reflection = Dossier.reflect_on_association(association))
|
|
||||||
association_reflection.klass.table_name
|
|
||||||
else
|
|
||||||
# Allow filtering on a joined table alias (which doesn’t exist
|
|
||||||
# in the ActiveRecord domain).
|
|
||||||
association
|
|
||||||
end
|
|
||||||
|
|
||||||
[table, column]
|
|
||||||
.map { |name| ActiveRecord::Base.connection.quote_column_name(name) }
|
|
||||||
.join('.')
|
|
||||||
end
|
|
||||||
|
|
||||||
def assert_supported_column(table, column)
|
|
||||||
if table == 'followers_instructeurs' && column != 'email'
|
|
||||||
raise ArgumentError, 'Table `followers_instructeurs` only supports the `email` column.'
|
|
||||||
end
|
|
||||||
if table == 'groupe_instructeur' && (column != 'label' && column != 'id')
|
|
||||||
raise ArgumentError, 'Table `groupe_instructeur` only supports the `label` or `id` column.'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SortedColumn
|
class SortedColumn
|
||||||
|
# include validations to enable procedure_presentation.validate_associate,
|
||||||
|
# which enforces the deserialization of columns in the sorted_column attribute
|
||||||
|
# and raises an error if a column is not found
|
||||||
|
include ActiveModel::Validations
|
||||||
|
|
||||||
attr_reader :column, :order
|
attr_reader :column, :order
|
||||||
|
|
||||||
def initialize(column:, order:)
|
def initialize(column:, order:)
|
||||||
|
@ -19,4 +24,8 @@ class SortedColumn
|
||||||
def sort_by_notifications?
|
def sort_by_notifications?
|
||||||
@column.notifications? && @order == 'desc'
|
@column.notifications? && @order == 'desc'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def id
|
||||||
|
column.h_id.merge(order:).sort.to_json
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -54,10 +54,6 @@ class TypesDeChamp::TypeDeChampBase
|
||||||
filter_value
|
filter_value
|
||||||
end
|
end
|
||||||
|
|
||||||
def human_to_filter(human_value)
|
|
||||||
human_value
|
|
||||||
end
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def champ_value(champ)
|
def champ_value(champ)
|
||||||
champ.value.present? ? champ.value.to_s : champ_default_value
|
champ.value.present? ? champ.value.to_s : champ_default_value
|
||||||
|
|
|
@ -11,17 +11,6 @@ class TypesDeChamp::YesNoTypeDeChamp < TypesDeChamp::CheckboxTypeDeChamp
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def human_to_filter(human_value)
|
|
||||||
downcased = human_value.downcase
|
|
||||||
if downcased == "oui"
|
|
||||||
"true"
|
|
||||||
elsif downcased == "non"
|
|
||||||
"false"
|
|
||||||
else
|
|
||||||
downcased
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def champ_value(champ)
|
def champ_value(champ)
|
||||||
champ_formatted_value(champ)
|
champ_formatted_value(champ)
|
||||||
|
|
157
app/services/dossier_filter_service.rb
Normal file
157
app/services/dossier_filter_service.rb
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DossierFilterService
|
||||||
|
TYPE_DE_CHAMP = 'type_de_champ'
|
||||||
|
|
||||||
|
def self.filtered_sorted_ids(dossiers, statut, filters, sorted_column, instructeur, count: nil)
|
||||||
|
dossiers_by_statut = dossiers.by_statut(statut, instructeur)
|
||||||
|
dossiers_sorted_ids = self.sorted_ids(dossiers_by_statut, sorted_column, instructeur, count || dossiers_by_statut.size)
|
||||||
|
|
||||||
|
if filters.present?
|
||||||
|
dossiers_sorted_ids.intersection(filtered_ids(dossiers_by_statut, filters))
|
||||||
|
else
|
||||||
|
dossiers_sorted_ids
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def self.sorted_ids(dossiers, sorted_column, instructeur, count)
|
||||||
|
table = sorted_column.column.table
|
||||||
|
column = sorted_column.column.column
|
||||||
|
order = sorted_column.order
|
||||||
|
|
||||||
|
case table
|
||||||
|
when 'notifications'
|
||||||
|
dossiers_id_with_notification = dossiers.merge(instructeur.followed_dossiers).with_notifications.ids
|
||||||
|
if order == 'desc'
|
||||||
|
dossiers_id_with_notification +
|
||||||
|
(dossiers.order('dossiers.updated_at desc').ids - dossiers_id_with_notification)
|
||||||
|
else
|
||||||
|
(dossiers.order('dossiers.updated_at asc').ids - dossiers_id_with_notification) +
|
||||||
|
dossiers_id_with_notification
|
||||||
|
end
|
||||||
|
when TYPE_DE_CHAMP
|
||||||
|
ids = dossiers
|
||||||
|
.with_type_de_champ(column)
|
||||||
|
.order("champs.value #{order}")
|
||||||
|
.pluck(:id)
|
||||||
|
if ids.size != count
|
||||||
|
rest = dossiers.where.not(id: ids).order(id: order).pluck(:id)
|
||||||
|
order == 'asc' ? ids + rest : rest + ids
|
||||||
|
else
|
||||||
|
ids
|
||||||
|
end
|
||||||
|
when 'followers_instructeurs'
|
||||||
|
assert_supported_column(table, column)
|
||||||
|
# LEFT OUTER JOIN allows to keep dossiers without assigned instructeurs yet
|
||||||
|
dossiers
|
||||||
|
.includes(:followers_instructeurs)
|
||||||
|
.joins('LEFT OUTER JOIN users instructeurs_users ON instructeurs_users.id = instructeurs.user_id')
|
||||||
|
.order("instructeurs_users.email #{order}")
|
||||||
|
.pluck(:id)
|
||||||
|
.uniq
|
||||||
|
when 'avis'
|
||||||
|
dossiers.includes(table)
|
||||||
|
.order("#{sanitized_column(table, column)} #{order}")
|
||||||
|
.pluck(:id)
|
||||||
|
.uniq
|
||||||
|
when 'self', 'user', 'individual', 'etablissement', 'groupe_instructeur'
|
||||||
|
(table == 'self' ? dossiers : dossiers.includes(table))
|
||||||
|
.order("#{sanitized_column(table, column)} #{order}")
|
||||||
|
.pluck(:id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.filtered_ids(dossiers, filters)
|
||||||
|
filters
|
||||||
|
.group_by { |filter| filter.column.then { [_1.table, _1.column] } }
|
||||||
|
.map do |(table, column), filters_for_column|
|
||||||
|
values = filters_for_column.map(&:filter)
|
||||||
|
filtered_column = filters_for_column.first.column
|
||||||
|
value_column = filtered_column.value_column
|
||||||
|
|
||||||
|
if filtered_column.is_a?(Columns::JSONPathColumn)
|
||||||
|
filtered_column.filtered_ids(dossiers, values)
|
||||||
|
else
|
||||||
|
case table
|
||||||
|
when 'self'
|
||||||
|
if filtered_column.type == :date
|
||||||
|
dates = values
|
||||||
|
.filter_map { |v| Time.zone.parse(v).beginning_of_day rescue nil }
|
||||||
|
|
||||||
|
dossiers.filter_by_datetimes(column, dates)
|
||||||
|
elsif filtered_column.column == "state" && values.include?("pending_correction")
|
||||||
|
dossiers.joins(:corrections).where(corrections: DossierCorrection.pending)
|
||||||
|
elsif filtered_column.column == "state" && values.include?("en_construction")
|
||||||
|
dossiers.where("dossiers.#{column} IN (?)", values).includes(:corrections).where.not(corrections: DossierCorrection.pending)
|
||||||
|
else
|
||||||
|
dossiers.where("dossiers.#{column} IN (?)", values)
|
||||||
|
end
|
||||||
|
when TYPE_DE_CHAMP
|
||||||
|
if filtered_column.type == :enum
|
||||||
|
dossiers.with_type_de_champ(column)
|
||||||
|
.filter_enum(:champs, value_column, values)
|
||||||
|
else
|
||||||
|
dossiers.with_type_de_champ(column)
|
||||||
|
.filter_ilike(:champs, value_column, values)
|
||||||
|
end
|
||||||
|
when 'etablissement'
|
||||||
|
if column == 'entreprise_date_creation'
|
||||||
|
dates = values
|
||||||
|
.filter_map { |v| v.to_date rescue nil }
|
||||||
|
|
||||||
|
dossiers
|
||||||
|
.includes(table)
|
||||||
|
.where(table.pluralize => { column => dates })
|
||||||
|
else
|
||||||
|
dossiers
|
||||||
|
.includes(table)
|
||||||
|
.filter_ilike(table, column, values)
|
||||||
|
end
|
||||||
|
when 'followers_instructeurs'
|
||||||
|
assert_supported_column(table, column)
|
||||||
|
dossiers
|
||||||
|
.includes(:followers_instructeurs)
|
||||||
|
.joins('INNER JOIN users instructeurs_users ON instructeurs_users.id = instructeurs.user_id')
|
||||||
|
.filter_ilike('instructeurs_users', :email, values) # ilike OK, user may want to search by *@domain
|
||||||
|
when 'user', 'individual' # user_columns: [email], individual_columns: ['nom', 'prenom', 'gender']
|
||||||
|
dossiers
|
||||||
|
.includes(table)
|
||||||
|
.filter_ilike(table, column, values) # ilike or where column == 'value' are both valid, we opted for ilike
|
||||||
|
when 'groupe_instructeur'
|
||||||
|
assert_supported_column(table, column)
|
||||||
|
|
||||||
|
dossiers
|
||||||
|
.joins(:groupe_instructeur)
|
||||||
|
.where(groupe_instructeur_id: values)
|
||||||
|
end.pluck(:id)
|
||||||
|
end
|
||||||
|
end.reduce(:&)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.sanitized_column(association, column)
|
||||||
|
table = if association == 'self'
|
||||||
|
Dossier.table_name
|
||||||
|
elsif (association_reflection = Dossier.reflect_on_association(association))
|
||||||
|
association_reflection.klass.table_name
|
||||||
|
else
|
||||||
|
# Allow filtering on a joined table alias (which doesn’t exist
|
||||||
|
# in the ActiveRecord domain).
|
||||||
|
association
|
||||||
|
end
|
||||||
|
|
||||||
|
[table, column]
|
||||||
|
.map { |name| ActiveRecord::Base.connection.quote_column_name(name) }
|
||||||
|
.join('.')
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.assert_supported_column(table, column)
|
||||||
|
if table == 'followers_instructeurs' && column != 'email'
|
||||||
|
raise ArgumentError, 'Table `followers_instructeurs` only supports the `email` column.'
|
||||||
|
end
|
||||||
|
if table == 'groupe_instructeur' && (column != 'label' && column != 'id')
|
||||||
|
raise ArgumentError, 'Table `groupe_instructeur` only supports the `label` or `id` column.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -40,8 +40,10 @@ class DossierProjectionService
|
||||||
# Those hashes are needed because:
|
# Those hashes are needed because:
|
||||||
# - the order of the intermediary query results are unknown
|
# - the order of the intermediary query results are unknown
|
||||||
# - some values can be missing (if a revision added or removed them)
|
# - some values can be missing (if a revision added or removed them)
|
||||||
def self.project(dossiers_ids, fields)
|
def self.project(dossiers_ids, columns)
|
||||||
fields = fields.deep_dup
|
fields = columns.map { |c| { TABLE => c.table, COLUMN => c.column } }
|
||||||
|
champ_value = champ_value_formatter(dossiers_ids, fields)
|
||||||
|
|
||||||
state_field = { TABLE => 'self', COLUMN => 'state' }
|
state_field = { TABLE => 'self', COLUMN => 'state' }
|
||||||
archived_field = { TABLE => 'self', COLUMN => 'archived' }
|
archived_field = { TABLE => 'self', COLUMN => 'archived' }
|
||||||
batch_operation_field = { TABLE => 'self', COLUMN => 'batch_operation_id' }
|
batch_operation_field = { TABLE => 'self', COLUMN => 'batch_operation_id' }
|
||||||
|
@ -53,7 +55,7 @@ class DossierProjectionService
|
||||||
individual_last_name = { TABLE => 'individual', COLUMN => 'nom' }
|
individual_last_name = { TABLE => 'individual', COLUMN => 'nom' }
|
||||||
sva_svr_decision_on_field = { TABLE => 'self', COLUMN => 'sva_svr_decision_on' }
|
sva_svr_decision_on_field = { TABLE => 'self', COLUMN => 'sva_svr_decision_on' }
|
||||||
dossier_corrections = { TABLE => 'dossier_corrections', COLUMN => 'resolved_at' }
|
dossier_corrections = { TABLE => 'dossier_corrections', COLUMN => 'resolved_at' }
|
||||||
champ_value = champ_value_formatter(dossiers_ids, fields)
|
|
||||||
([state_field, archived_field, sva_svr_decision_on_field, hidden_by_user_at_field, hidden_by_administration_at_field, hidden_by_reason_field, for_tiers_field, individual_first_name, individual_last_name, batch_operation_field, dossier_corrections] + fields)
|
([state_field, archived_field, sva_svr_decision_on_field, hidden_by_user_at_field, hidden_by_administration_at_field, hidden_by_reason_field, for_tiers_field, individual_first_name, individual_last_name, batch_operation_field, dossier_corrections] + fields)
|
||||||
.each { |f| f[:id_value_h] = {} }
|
.each { |f| f[:id_value_h] = {} }
|
||||||
.group_by { |f| f[TABLE] } # one query per table
|
.group_by { |f| f[TABLE] } # one query per table
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
module Maintenance
|
module Maintenance
|
||||||
# PR: 10774
|
# PR: 10774
|
||||||
# why: postgres does not support integer greater than ProcedurePresentation::PG_INTEGER_MAX_VALUE)
|
# why: postgres does not support integer greater than FilteredColumn::PG_INTEGER_MAX_VALUE)
|
||||||
# it occures when user copypaste the dossier id twice (like missed copy paste,paste)
|
# it occures when user copypaste the dossier id twice (like missed copy paste,paste)
|
||||||
# once this huge integer is saved on procedure presentation, page with this filter can't be loaded
|
# once this huge integer is saved on procedure presentation, page with this filter can't be loaded
|
||||||
# when: run this migration when it appears in your maintenance tasks list, this file fix the data and we added some validations too
|
# when: run this migration when it appears in your maintenance tasks list, this file fix the data and we added some validations too
|
||||||
|
@ -16,7 +16,7 @@ module Maintenance
|
||||||
filters_by_status.reject do |filter|
|
filters_by_status.reject do |filter|
|
||||||
filter.is_a?(Hash) &&
|
filter.is_a?(Hash) &&
|
||||||
filter['column'] == 'id' &&
|
filter['column'] == 'id' &&
|
||||||
(filter['value']&.to_i&. >= ProcedurePresentation::PG_INTEGER_MAX_VALUE)
|
(filter['value']&.to_i&. >= FilteredColumn::PG_INTEGER_MAX_VALUE)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
element.save
|
element.save
|
||||||
|
|
36
app/types/filtered_column_type.rb
Normal file
36
app/types/filtered_column_type.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class FilteredColumnType < 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 FilteredColumn
|
||||||
|
value
|
||||||
|
in NilClass # default value
|
||||||
|
nil
|
||||||
|
# from form (id is a string) or from db (id is a hash)
|
||||||
|
in { id: String|Hash, filter: String } => h
|
||||||
|
FilteredColumn.new(column: ColumnType.new.cast(h[:id]), filter: h[:filter])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# db -> ruby
|
||||||
|
def deserialize(value) = cast(value&.then { JSON.parse(_1) })
|
||||||
|
|
||||||
|
# ruby -> db
|
||||||
|
def serialize(value)
|
||||||
|
case value
|
||||||
|
in NilClass
|
||||||
|
nil
|
||||||
|
in FilteredColumn
|
||||||
|
JSON.generate({
|
||||||
|
id: value.column.h_id,
|
||||||
|
filter: value.filter
|
||||||
|
})
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Invalid value for FilteredColumn serialization: #{value}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,4 +3,4 @@
|
||||||
= t('views.instructeurs.dossiers.filters.title')
|
= t('views.instructeurs.dossiers.filters.title')
|
||||||
|
|
||||||
- menu.with_form do
|
- menu.with_form do
|
||||||
= render Instructeurs::ColumnFilterComponent.new(procedure:, procedure_presentation: @procedure_presentation, statut:)
|
= render Instructeurs::ColumnFilterComponent.new(procedure:, procedure_presentation:, statut:)
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
- if current_filters.count > 0
|
- if current_filters.count > 0
|
||||||
.fr-mb-2w
|
.fr-mb-2w
|
||||||
- current_filters.group_by { |filter| filter['table'] }.each_with_index do |(table, filters), i|
|
- current_filters.group_by { |filter| filter.column.table }.each_with_index do |(table, filters), i|
|
||||||
- if i > 0
|
- if i > 0
|
||||||
= " et "
|
= " et "
|
||||||
- filters.each_with_index do |filter, i|
|
- filters.each_with_index do |filter, i|
|
||||||
- if i > 0
|
- if i > 0
|
||||||
= " ou "
|
= " ou "
|
||||||
= link_to remove_filter_instructeur_procedure_path(procedure, { statut: statut, column: { procedure_id: procedure.id, column_id: filter['table'] + "/" + filter['column'] }.to_json, value: filter['value'] }),
|
= form_tag(add_filter_instructeur_procedure_path(procedure), class: 'inline') do
|
||||||
class: "fr-tag fr-tag--dismiss fr-my-1w", aria: { label: "Retirer le filtre #{filter['column']}" } do
|
- prefix = procedure_presentation.filters_name_for(statut)
|
||||||
= "#{filter['label'].truncate(50)} : #{procedure_presentation.human_value_for_filter(filter)}"
|
= hidden_field_tag "#{prefix}[]", ''
|
||||||
|
- (current_filters - [filter]).each do |f|
|
||||||
|
= hidden_field_tag "#{prefix}[][id]", f.column.id
|
||||||
|
= hidden_field_tag "#{prefix}[][filter]", f.filter
|
||||||
|
|
||||||
|
= button_tag "#{filter.column.label.truncate(50)} : #{procedure_presentation.human_value_for_filter(filter)}",
|
||||||
|
class: 'fr-tag fr-tag--dismiss fr-my-1w'
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
%hr
|
%hr
|
||||||
.flex.align-center
|
.flex.align-center
|
||||||
- if @filtered_sorted_paginated_ids.present? || @current_filters.count > 0
|
- if @filtered_sorted_paginated_ids.present? || @current_filters.count > 0
|
||||||
= render partial: "dossiers_filter_dropdown", locals: { procedure: @procedure, statut: @statut}
|
= render partial: "dossiers_filter_dropdown", locals: { procedure: @procedure, statut: @statut, procedure_presentation: @procedure_presentation }
|
||||||
= render Dossiers::NotifiedToggleComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation) if @statut != 'a-suivre'
|
= render Dossiers::NotifiedToggleComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation) if @statut != 'a-suivre'
|
||||||
|
|
||||||
.fr-ml-auto
|
.fr-ml-auto
|
||||||
|
|
|
@ -44,29 +44,6 @@
|
||||||
],
|
],
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"warning_type": "SQL Injection",
|
|
||||||
"warning_code": 0,
|
|
||||||
"fingerprint": "31693060072e27c02ca4f884f2a07f4f1c1247b7a6f5cc5c724e88e6ca9b4873",
|
|
||||||
"check_name": "SQL",
|
|
||||||
"message": "Possible SQL injection",
|
|
||||||
"file": "app/models/concerns/dossier_filtering_concern.rb",
|
|
||||||
"line": 40,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
||||||
"code": "where(\"#{values.count} OR #{\"(#{ProcedurePresentation.sanitized_column(table, column)} = ?)\"}\", *values)",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "DossierFilteringConcern",
|
|
||||||
"method": null
|
|
||||||
},
|
|
||||||
"user_input": "values.count",
|
|
||||||
"confidence": "Medium",
|
|
||||||
"cwe_id": [
|
|
||||||
89
|
|
||||||
],
|
|
||||||
"note": "filtered by rails query params where(something: ?, values)"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "SQL Injection",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 0,
|
"warning_code": 0,
|
||||||
|
@ -136,6 +113,29 @@
|
||||||
],
|
],
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"warning_type": "SQL Injection",
|
||||||
|
"warning_code": 0,
|
||||||
|
"fingerprint": "91ff8031e7c639c95fe6c244867349a72078ef456d8b3507deaf2bdb9bf62fe2",
|
||||||
|
"check_name": "SQL",
|
||||||
|
"message": "Possible SQL injection",
|
||||||
|
"file": "app/models/concerns/dossier_filtering_concern.rb",
|
||||||
|
"line": 34,
|
||||||
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||||
|
"code": "where(\"#{values.count} OR #{\"(#{DossierFilterService.sanitized_column(table, column)} ILIKE ?)\"}\", *values.map do\n \"%#{value}%\"\n end)",
|
||||||
|
"render_path": null,
|
||||||
|
"location": {
|
||||||
|
"type": "method",
|
||||||
|
"class": "DossierFilteringConcern",
|
||||||
|
"method": null
|
||||||
|
},
|
||||||
|
"user_input": "values.count",
|
||||||
|
"confidence": "Medium",
|
||||||
|
"cwe_id": [
|
||||||
|
89
|
||||||
|
],
|
||||||
|
"note": "filtered by rails query params where(something: ?, values)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"warning_type": "Cross-Site Scripting",
|
"warning_type": "Cross-Site Scripting",
|
||||||
"warning_code": 2,
|
"warning_code": 2,
|
||||||
|
@ -196,13 +196,13 @@
|
||||||
{
|
{
|
||||||
"warning_type": "SQL Injection",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 0,
|
"warning_code": 0,
|
||||||
"fingerprint": "bd1df30f95135357b646e21a03d95498874faffa32e3804fc643e9b6b957ee14",
|
"fingerprint": "aaff41afa7bd5a551cd2e3a385071090cb53c95caa40fad3785cd3d68c9b939c",
|
||||||
"check_name": "SQL",
|
"check_name": "SQL",
|
||||||
"message": "Possible SQL injection",
|
"message": "Possible SQL injection",
|
||||||
"file": "app/models/concerns/dossier_filtering_concern.rb",
|
"file": "app/models/concerns/dossier_filtering_concern.rb",
|
||||||
"line": 34,
|
"line": 40,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||||
"code": "where(\"#{values.count} OR #{\"(#{ProcedurePresentation.sanitized_column(table, column)} ILIKE ?)\"}\", *values.map do\n \"%#{value}%\"\n end)",
|
"code": "where(\"#{values.count} OR #{\"(#{DossierFilterService.sanitized_column(table, column)} = ?)\"}\", *values)",
|
||||||
"render_path": null,
|
"render_path": null,
|
||||||
"location": {
|
"location": {
|
||||||
"type": "method",
|
"type": "method",
|
||||||
|
@ -272,6 +272,6 @@
|
||||||
"note": "Current is not a model"
|
"note": "Current is not a model"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated": "2024-09-24 20:56:24 +0200",
|
"updated": "2024-10-15 15:57:27 +0200",
|
||||||
"brakeman_version": "6.1.2"
|
"brakeman_version": "6.1.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
require Rails.root.join("app/types/column_type")
|
require Rails.root.join("app/types/column_type")
|
||||||
require Rails.root.join("app/types/export_item_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/sorted_column_type")
|
||||||
|
require Rails.root.join("app/types/filtered_column_type")
|
||||||
|
|
||||||
ActiveSupport.on_load(:active_record) do
|
ActiveSupport.on_load(:active_record) do
|
||||||
ActiveRecord::Type.register(:column, ColumnType)
|
ActiveRecord::Type.register(:column, ColumnType)
|
||||||
ActiveRecord::Type.register(:export_item, ExportItemType)
|
ActiveRecord::Type.register(:export_item, ExportItemType)
|
||||||
ActiveRecord::Type.register(:sorted_column, SortedColumnType)
|
ActiveRecord::Type.register(:sorted_column, SortedColumnType)
|
||||||
|
ActiveRecord::Type.register(:filtered_column, FilteredColumnType)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddFilteredAndSortedColumnToExportsTable < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
add_column :exports, :filtered_columns, :jsonb, array: true, default: [], null: false
|
||||||
|
add_column :exports, :sorted_column, :jsonb
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2024_09_29_141825) do
|
ActiveRecord::Schema[7.0].define(version: 2024_10_14_084333) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_buffercache"
|
enable_extension "pg_buffercache"
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
@ -628,12 +628,14 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_29_141825) do
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.integer "dossiers_count"
|
t.integer "dossiers_count"
|
||||||
t.bigint "export_template_id"
|
t.bigint "export_template_id"
|
||||||
|
t.jsonb "filtered_columns", default: [], null: false, array: true
|
||||||
t.string "format", null: false
|
t.string "format", null: false
|
||||||
t.bigint "instructeur_id"
|
t.bigint "instructeur_id"
|
||||||
t.string "job_status", default: "pending", null: false
|
t.string "job_status", default: "pending", null: false
|
||||||
t.text "key", null: false
|
t.text "key", null: false
|
||||||
t.bigint "procedure_presentation_id"
|
t.bigint "procedure_presentation_id"
|
||||||
t.jsonb "procedure_presentation_snapshot"
|
t.jsonb "procedure_presentation_snapshot"
|
||||||
|
t.jsonb "sorted_column"
|
||||||
t.string "statut", default: "tous"
|
t.string "statut", default: "tous"
|
||||||
t.string "time_span_type", default: "everything", null: false
|
t.string "time_span_type", default: "everything", null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
|
|
|
@ -36,9 +36,9 @@ RSpec.describe Dossiers::ExportLinkComponent, type: :component do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when export is for a presentation' do
|
context 'when export is from a presentation' do
|
||||||
before do
|
before do
|
||||||
export.update!(procedure_presentation: procedure_presentation)
|
export.update!(sorted_column: procedure.default_sorted_column)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'display the persisted dossiers count' do
|
it 'display the persisted dossiers count' do
|
||||||
|
@ -48,7 +48,7 @@ RSpec.describe Dossiers::ExportLinkComponent, type: :component do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the export is not available" do
|
context "when the export is not available" do
|
||||||
let(:export) { create(:export, :pending, groupe_instructeurs: [groupe_instructeur], procedure_presentation: procedure_presentation, created_at: 10.minutes.ago) }
|
let(:export) { create(:export, :pending, groupe_instructeurs: [groupe_instructeur], sorted_column: procedure.default_sorted_column, created_at: 10.minutes.ago) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
create_list(:dossier, 3, :en_construction, procedure: procedure, groupe_instructeur: groupe_instructeur)
|
create_list(:dossier, 3, :en_construction, procedure: procedure, groupe_instructeur: groupe_instructeur)
|
||||||
|
|
|
@ -8,27 +8,22 @@ describe Instructeurs::ColumnFilterComponent, type: :component do
|
||||||
let(:procedure_id) { procedure.id }
|
let(:procedure_id) { procedure.id }
|
||||||
let(:procedure_presentation) { nil }
|
let(:procedure_presentation) { nil }
|
||||||
let(:statut) { nil }
|
let(:statut) { nil }
|
||||||
|
let(:column) { nil }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(component).to receive(:current_instructeur).and_return(instructeur)
|
allow(component).to receive(:current_instructeur).and_return(instructeur)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe ".filterable_columns_options" do
|
describe ".filterable_columns_options" do
|
||||||
context 'filders' do
|
let(:filterable_column) { Column.new(procedure_id:, label: 'email', table: 'user', column: 'email') }
|
||||||
let(:column) { nil }
|
let(:non_filterable_column) { Column.new(procedure_id:, label: 'depose_since', table: 'self', column: 'depose_since', filterable: false) }
|
||||||
let(:included_displayable_field) do
|
let(:mocked_columns) { [filterable_column, non_filterable_column] }
|
||||||
[
|
|
||||||
Column.new(procedure_id:, label: 'email', table: 'user', column: 'email'),
|
|
||||||
Column.new(procedure_id:, label: "depose_since", table: "self", column: "depose_since", displayable: false)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
before { allow(procedure).to receive(:columns).and_return(included_displayable_field) }
|
before { allow(procedure).to receive(:columns).and_return(mocked_columns) }
|
||||||
|
|
||||||
subject { component.filterable_columns_options }
|
subject { component.filterable_columns_options }
|
||||||
|
|
||||||
it { is_expected.to eq([["email", included_displayable_field.first.id], ["depose_since", included_displayable_field.second.id]]) }
|
it { is_expected.to eq([[filterable_column.label, filterable_column.id]]) }
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.options_for_select_of_column' do
|
describe '.options_for_select_of_column' do
|
||||||
|
|
|
@ -905,8 +905,8 @@ describe Instructeurs::ProceduresController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
subject do
|
subject do
|
||||||
column = procedure.find_column(label: "Nom").id
|
column = procedure.find_column(label: "Nom")
|
||||||
post :add_filter, params: { procedure_id: procedure.id, column:, value: "n" * 4100, statut: "a-suivre" }
|
post :add_filter, params: { procedure_id: procedure.id, a_suivre_filters: [{ id: column.id, filter: "n" * 4049 }] }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should render the error' do
|
it 'should render the error' do
|
||||||
|
|
|
@ -8,7 +8,10 @@ FactoryBot.define do
|
||||||
groupe_instructeurs { [association(:groupe_instructeur)] }
|
groupe_instructeurs { [association(:groupe_instructeur)] }
|
||||||
|
|
||||||
after(:build) do |export, _evaluator|
|
after(:build) do |export, _evaluator|
|
||||||
export.key = Export.generate_cache_key(export.groupe_instructeurs.map(&:id), export.procedure_presentation)
|
procedure_presentation = export.procedure_presentation
|
||||||
|
filters = Array.wrap(procedure_presentation&.filters_for(export.statut))
|
||||||
|
sorted_column = procedure_presentation&.sorted_column
|
||||||
|
export.key = Export.generate_cache_key(export.groupe_instructeurs.map(&:id), filters, sorted_column)
|
||||||
export.user_profile = export.groupe_instructeurs.first&.instructeurs&.first if export.user_profile.nil?
|
export.user_profile = export.groupe_instructeurs.first&.instructeurs&.first if export.user_profile.nil?
|
||||||
export.dossiers_count = 10 if !export.pending?
|
export.dossiers_count = 10 if !export.pending?
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,10 +38,7 @@ describe '20240920130741_migrate_procedure_presentation_to_columns.rake' do
|
||||||
it 'populates the columns' do
|
it 'populates the columns' do
|
||||||
procedure_id = procedure.id
|
procedure_id = procedure.id
|
||||||
|
|
||||||
expect(procedure_presentation.displayed_columns).to eq([
|
expect(procedure_presentation.displayed_columns.map(&:label)).to eq(["Raison sociale", procedure.active_revision.types_de_champ.first.libelle])
|
||||||
{ "procedure_id" => procedure_id, "column_id" => "etablissement/entreprise_raison_sociale" },
|
|
||||||
{ "procedure_id" => procedure_id, "column_id" => "type_de_champ/#{stable_id}" }
|
|
||||||
])
|
|
||||||
|
|
||||||
order, column_id = procedure_presentation
|
order, column_id = procedure_presentation
|
||||||
.sorted_column
|
.sorted_column
|
||||||
|
@ -52,9 +49,8 @@ describe '20240920130741_migrate_procedure_presentation_to_columns.rake' do
|
||||||
|
|
||||||
expect(procedure_presentation.tous_filters).to eq([])
|
expect(procedure_presentation.tous_filters).to eq([])
|
||||||
|
|
||||||
traites = procedure_presentation.traites_filters
|
traites = procedure_presentation.traites_filters.map { [_1.label, _1.filter] }
|
||||||
.map { [_1['id'], _1['filter']] }
|
|
||||||
|
|
||||||
expect(traites).to eq([[{ "column_id" => "etablissement/libelle_naf", "procedure_id" => procedure_id }, "Administration publique générale"]])
|
expect(traites).to eq([["Libellé NAF", "Administration publique générale"]])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,32 +9,43 @@ describe AssignTo, type: :model do
|
||||||
let(:procedure_presentation_or_default) { procedure_presentation_and_errors.first }
|
let(:procedure_presentation_or_default) { procedure_presentation_and_errors.first }
|
||||||
let(:errors) { procedure_presentation_and_errors.second }
|
let(:errors) { procedure_presentation_and_errors.second }
|
||||||
|
|
||||||
context "without a procedure_presentation" do
|
context "without a preexisting procedure_presentation" do
|
||||||
it { expect(procedure_presentation_or_default).to be_persisted }
|
it 'creates a default pp' do
|
||||||
it { expect(procedure_presentation_or_default).to be_valid }
|
expect(procedure_presentation_or_default).to be_persisted
|
||||||
it { expect(errors).to be_nil }
|
expect(procedure_presentation_or_default).to be_valid
|
||||||
|
expect(errors).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with a procedure_presentation" do
|
context "with a preexisting procedure_presentation" do
|
||||||
let!(:procedure_presentation) { ProcedurePresentation.create(assign_to: assign_to) }
|
let!(:procedure_presentation) { ProcedurePresentation.create(assign_to:) }
|
||||||
|
|
||||||
it { expect(procedure_presentation_or_default).to eq(procedure_presentation) }
|
it 'returns the preexisting pp' do
|
||||||
it { expect(procedure_presentation_or_default).to be_valid }
|
expect(procedure_presentation_or_default).to eq(procedure_presentation)
|
||||||
it { expect(errors).to be_nil }
|
expect(procedure_presentation_or_default).to be_valid
|
||||||
|
expect(errors).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with an invalid procedure_presentation" do
|
context "with an invalid procedure_presentation" do
|
||||||
let!(:procedure_presentation) do
|
let!(:procedure_presentation) do
|
||||||
pp = ProcedurePresentation.new(assign_to: assign_to, displayed_fields: [{ 'table' => 'invalid', 'column' => 'random' }])
|
pp = ProcedurePresentation.create(assign_to: assign_to)
|
||||||
pp.save(validate: false)
|
|
||||||
pp
|
sql = <<-SQL.squish
|
||||||
|
UPDATE procedure_presentations
|
||||||
|
SET displayed_columns = ARRAY['{\"procedure_id\":666}'::jsonb]
|
||||||
|
WHERE id = #{pp.id} ;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
pp.class.connection.execute(sql)
|
||||||
|
|
||||||
|
assign_to.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
it { expect(procedure_presentation_or_default).to be_persisted }
|
|
||||||
it { expect(procedure_presentation_or_default).to be_valid }
|
|
||||||
it { expect(errors).to be_present }
|
|
||||||
it do
|
it do
|
||||||
procedure_presentation_or_default
|
expect(procedure_presentation_or_default).to be_persisted
|
||||||
|
expect(procedure_presentation_or_default).to be_valid
|
||||||
|
expect(errors).to be_present
|
||||||
expect(assign_to.procedure_presentation).not_to be(procedure_presentation)
|
expect(assign_to.procedure_presentation).not_to be(procedure_presentation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,37 +29,37 @@ describe ColumnsConcern do
|
||||||
let(:tdc_private_2) { procedure.active_revision.types_de_champ_private[1] }
|
let(:tdc_private_2) { procedure.active_revision.types_de_champ_private[1] }
|
||||||
let(:expected) {
|
let(:expected) {
|
||||||
[
|
[
|
||||||
{ label: 'Nº dossier', table: 'self', column: 'id', classname: 'number-col', displayable: true, type: :number, scope: '', value_column: :value, filterable: true },
|
{ label: 'Nº dossier', table: 'self', column: 'id', displayable: true, type: :number, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'notifications', table: 'notifications', column: 'notifications', displayable: true, type: :text, scope: '', value_column: :value, filterable: false },
|
{ label: 'notifications', table: 'notifications', column: 'notifications', displayable: true, type: :text, scope: '', value_column: :value, filterable: false },
|
||||||
{ label: 'Créé le', table: 'self', column: 'created_at', classname: '', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: 'Créé le', table: 'self', column: 'created_at', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Mis à jour le', table: 'self', column: 'updated_at', classname: '', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: 'Mis à jour le', table: 'self', column: 'updated_at', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Déposé le', table: 'self', column: 'depose_at', classname: '', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: 'Déposé le', table: 'self', column: 'depose_at', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'En construction le', table: 'self', column: 'en_construction_at', classname: '', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: 'En construction le', table: 'self', column: 'en_construction_at', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'En instruction le', table: 'self', column: 'en_instruction_at', classname: '', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: 'En instruction le', table: 'self', column: 'en_instruction_at', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Terminé le', table: 'self', column: 'processed_at', classname: '', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: 'Terminé le', table: 'self', column: 'processed_at', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: "Mis à jour depuis", table: "self", column: "updated_since", classname: "", displayable: false, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: "Mis à jour depuis", table: "self", column: "updated_since", displayable: false, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: "Déposé depuis", table: "self", column: "depose_since", classname: "", displayable: false, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: "Déposé depuis", table: "self", column: "depose_since", displayable: false, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: "En construction depuis", table: "self", column: "en_construction_since", classname: "", displayable: false, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: "En construction depuis", table: "self", column: "en_construction_since", displayable: false, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: "En instruction depuis", table: "self", column: "en_instruction_since", classname: "", displayable: false, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: "En instruction depuis", table: "self", column: "en_instruction_since", displayable: false, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: "Terminé depuis", table: "self", column: "processed_since", classname: "", displayable: false, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: "Terminé depuis", table: "self", column: "processed_since", displayable: false, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: "Statut", table: "self", column: "state", classname: "", displayable: false, scope: 'instructeurs.dossiers.filterable_state', type: :enum, value_column: :value, filterable: true },
|
{ label: "Statut", table: "self", column: "state", displayable: false, scope: 'instructeurs.dossiers.filterable_state', type: :enum, value_column: :value, filterable: true },
|
||||||
{ label: 'Demandeur', table: 'user', column: 'email', classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: 'Demandeur', table: 'user', column: 'email', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Email instructeur', table: 'followers_instructeurs', column: 'email', classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: 'Email instructeur', table: 'followers_instructeurs', column: 'email', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Groupe instructeur', table: 'groupe_instructeur', column: 'id', classname: '', displayable: true, type: :enum, scope: '', value_column: :value, filterable: true },
|
{ label: 'Groupe instructeur', table: 'groupe_instructeur', column: 'id', displayable: true, type: :enum, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Avis oui/non', table: 'avis', column: 'question_answer', classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: false },
|
{ label: 'Avis oui/non', table: 'avis', column: 'question_answer', displayable: true, type: :text, scope: '', value_column: :value, filterable: false },
|
||||||
{ label: 'SIREN', table: 'etablissement', column: 'entreprise_siren', classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: 'SIREN', table: 'etablissement', column: 'entreprise_siren', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Forme juridique', table: 'etablissement', column: 'entreprise_forme_juridique', classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: 'Forme juridique', table: 'etablissement', column: 'entreprise_forme_juridique', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Nom commercial', table: 'etablissement', column: 'entreprise_nom_commercial', classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: 'Nom commercial', table: 'etablissement', column: 'entreprise_nom_commercial', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Raison sociale', table: 'etablissement', column: 'entreprise_raison_sociale', classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: 'Raison sociale', table: 'etablissement', column: 'entreprise_raison_sociale', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'SIRET siège social', table: 'etablissement', column: 'entreprise_siret_siege_social', classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: 'SIRET siège social', table: 'etablissement', column: 'entreprise_siret_siege_social', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Date de création', table: 'etablissement', column: 'entreprise_date_creation', classname: '', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
{ label: 'Date de création', table: 'etablissement', column: 'entreprise_date_creation', displayable: true, type: :date, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'SIRET', table: 'etablissement', column: 'siret', classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: 'SIRET', table: 'etablissement', column: 'siret', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Libellé NAF', table: 'etablissement', column: 'libelle_naf', classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: 'Libellé NAF', table: 'etablissement', column: 'libelle_naf', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: 'Code postal', table: 'etablissement', column: 'code_postal', classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: 'Code postal', table: 'etablissement', column: 'code_postal', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: tdc_1.libelle, table: 'type_de_champ', column: tdc_1.stable_id.to_s, classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: tdc_1.libelle, table: 'type_de_champ', column: tdc_1.stable_id.to_s, displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: tdc_2.libelle, table: 'type_de_champ', column: tdc_2.stable_id.to_s, classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: tdc_2.libelle, table: 'type_de_champ', column: tdc_2.stable_id.to_s, displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: tdc_private_1.libelle, table: 'type_de_champ', column: tdc_private_1.stable_id.to_s, classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
{ label: tdc_private_1.libelle, table: 'type_de_champ', column: tdc_private_1.stable_id.to_s, displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
|
||||||
{ label: tdc_private_2.libelle, table: 'type_de_champ', column: tdc_private_2.stable_id.to_s, classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true }
|
{ label: tdc_private_2.libelle, table: 'type_de_champ', column: tdc_private_2.stable_id.to_s, displayable: true, type: :text, scope: '', value_column: :value, filterable: true }
|
||||||
].map { Column.new(**_1.merge(procedure_id:)) }
|
].map { Column.new(**_1.merge(procedure_id:)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,9 +84,9 @@ describe ColumnsConcern do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the procedure is for individuals' do
|
context 'when the procedure is for individuals' do
|
||||||
let(:name_field) { Column.new(procedure_id:, label: "Prénom", table: "individual", column: "prenom", classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true) }
|
let(:name_field) { Column.new(procedure_id:, label: "Prénom", table: "individual", column: "prenom", displayable: true, type: :text, scope: '', value_column: :value, filterable: true) }
|
||||||
let(:surname_field) { Column.new(procedure_id:, label: "Nom", table: "individual", column: "nom", classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true) }
|
let(:surname_field) { Column.new(procedure_id:, label: "Nom", table: "individual", column: "nom", displayable: true, type: :text, scope: '', value_column: :value, filterable: true) }
|
||||||
let(:gender_field) { Column.new(procedure_id:, label: "Civilité", table: "individual", column: "gender", classname: '', displayable: true, type: :text, scope: '', value_column: :value, filterable: true) }
|
let(:gender_field) { Column.new(procedure_id:, label: "Civilité", table: "individual", column: "gender", displayable: true, type: :text, scope: '', value_column: :value, filterable: true) }
|
||||||
let(:procedure) { create(:procedure, :for_individual) }
|
let(:procedure) { create(:procedure, :for_individual) }
|
||||||
let(:procedure_id) { procedure.id }
|
let(:procedure_id) { procedure.id }
|
||||||
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) }
|
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) }
|
||||||
|
@ -99,8 +99,8 @@ describe ColumnsConcern do
|
||||||
let(:procedure_id) { procedure.id }
|
let(:procedure_id) { procedure.id }
|
||||||
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) }
|
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) }
|
||||||
|
|
||||||
let(:decision_on) { Column.new(procedure_id:, label: "Date décision SVA", table: "self", column: "sva_svr_decision_on", classname: '', displayable: true, type: :date, scope: '', value_column: :value, filterable: true) }
|
let(:decision_on) { Column.new(procedure_id:, label: "Date décision SVA", table: "self", column: "sva_svr_decision_on", displayable: true, type: :date, scope: '', value_column: :value, filterable: true) }
|
||||||
let(:decision_before_field) { Column.new(procedure_id:, label: "Date décision SVA avant", table: "self", column: "sva_svr_decision_before", classname: '', displayable: false, type: :date, scope: '', value_column: :value, filterable: true) }
|
let(:decision_before_field) { Column.new(procedure_id:, label: "Date décision SVA avant", table: "self", column: "sva_svr_decision_before", displayable: false, type: :date, scope: '', value_column: :value, filterable: true) }
|
||||||
|
|
||||||
it { is_expected.to include(decision_on, decision_before_field) }
|
it { is_expected.to include(decision_on, decision_before_field) }
|
||||||
end
|
end
|
||||||
|
@ -110,8 +110,8 @@ describe ColumnsConcern do
|
||||||
let(:procedure_id) { procedure.id }
|
let(:procedure_id) { procedure.id }
|
||||||
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) }
|
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) }
|
||||||
|
|
||||||
let(:decision_on) { Column.new(procedure_id:, label: "Date décision SVR", table: "self", column: "sva_svr_decision_on", classname: '', displayable: true, type: :date, scope: '', value_column: :value, filterable: true) }
|
let(:decision_on) { Column.new(procedure_id:, label: "Date décision SVR", table: "self", column: "sva_svr_decision_on", displayable: true, type: :date, scope: '', value_column: :value, filterable: true) }
|
||||||
let(:decision_before_field) { Column.new(procedure_id:, label: "Date décision SVR avant", table: "self", column: "sva_svr_decision_before", classname: '', displayable: false, type: :date, scope: '', value_column: :value, filterable: true) }
|
let(:decision_before_field) { Column.new(procedure_id:, label: "Date décision SVR avant", table: "self", column: "sva_svr_decision_before", displayable: false, type: :date, scope: '', value_column: :value, filterable: true) }
|
||||||
|
|
||||||
it { is_expected.to include(decision_on, decision_before_field) }
|
it { is_expected.to include(decision_on, decision_before_field) }
|
||||||
end
|
end
|
||||||
|
|
|
@ -61,31 +61,18 @@ RSpec.describe Export, type: :model do
|
||||||
it { expect(groupe_instructeur.reload).to be_present }
|
it { expect(groupe_instructeur.reload).to be_present }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.find_by groupe_instructeurs' do
|
describe '.by_key groupe_instructeurs' do
|
||||||
let!(:procedure) { create(:procedure) }
|
let!(:procedure) { create(:procedure) }
|
||||||
let!(:gi_1) { create(:groupe_instructeur, procedure: procedure, instructeurs: [create(:instructeur)]) }
|
let!(:gi_1) { create(:groupe_instructeur, procedure: procedure, instructeurs: [create(:instructeur)]) }
|
||||||
let!(:gi_2) { create(:groupe_instructeur, procedure: procedure, instructeurs: [create(:instructeur)]) }
|
let!(:gi_2) { create(:groupe_instructeur, procedure: procedure, instructeurs: [create(:instructeur)]) }
|
||||||
let!(:gi_3) { create(:groupe_instructeur, procedure: procedure, instructeurs: [create(:instructeur)]) }
|
let!(:gi_3) { create(:groupe_instructeur, procedure: procedure, instructeurs: [create(:instructeur)]) }
|
||||||
|
|
||||||
context 'without procedure_presentation' do
|
context 'when an export is made for one groupe instructeur' do
|
||||||
context 'when an export is made for one groupe instructeur' do
|
let!(:export) { create(:export, groupe_instructeurs: [gi_1, gi_2]) }
|
||||||
let!(:export) { create(:export, groupe_instructeurs: [gi_1, gi_2]) }
|
|
||||||
|
|
||||||
it { expect(Export.by_key([gi_1.id], nil)).to be_empty }
|
it { expect(Export.by_key([gi_1.id])).to be_empty }
|
||||||
it { expect(Export.by_key([gi_2.id, gi_1.id], nil)).to eq([export]) }
|
it { expect(Export.by_key([gi_2.id, gi_1.id])).to eq([export]) }
|
||||||
it { expect(Export.by_key([gi_1.id, gi_2.id, gi_3.id], nil)).to be_empty }
|
it { expect(Export.by_key([gi_1.id, gi_2.id, gi_3.id])).to be_empty }
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with procedure_presentation and without' do
|
|
||||||
let!(:export_global) { create(:export, statut: Export.statuts.fetch(:tous), groupe_instructeurs: [gi_1, gi_2], procedure_presentation: nil) }
|
|
||||||
let!(:export_with_filter) { create(:export, statut: Export.statuts.fetch(:suivis), groupe_instructeurs: [gi_1, gi_2], procedure_presentation: create(:procedure_presentation, procedure: procedure, assign_to: gi_1.instructeurs.first.assign_to.first)) }
|
|
||||||
let!(:procedure_presentation) { create(:procedure_presentation, procedure: gi_1.procedure) }
|
|
||||||
|
|
||||||
it 'find global exports as well as filtered one' do
|
|
||||||
expect(Export.by_key([gi_2.id, gi_1.id], export_with_filter.procedure_presentation))
|
|
||||||
.to contain_exactly(export_with_filter, export_global)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -94,7 +81,9 @@ RSpec.describe Export, type: :model do
|
||||||
let(:instructeur) { create(:instructeur) }
|
let(:instructeur) { create(:instructeur) }
|
||||||
let!(:gi_1) { create(:groupe_instructeur, procedure: procedure, instructeurs: [instructeur]) }
|
let!(:gi_1) { create(:groupe_instructeur, procedure: procedure, instructeurs: [instructeur]) }
|
||||||
let!(:pp) { gi_1.instructeurs.first.procedure_presentation_and_errors_for_procedure_id(procedure.id).first }
|
let!(:pp) { gi_1.instructeurs.first.procedure_presentation_and_errors_for_procedure_id(procedure.id).first }
|
||||||
before { pp.add_filter('tous', procedure.find_column(label: 'Créé le').id, '10/12/2021') }
|
let(:created_at_column) { FilteredColumn.new(column: procedure.find_column(label: 'Créé le'), filter: '10/12/2021') }
|
||||||
|
|
||||||
|
before { pp.update(tous_filters: [created_at_column]) }
|
||||||
|
|
||||||
context 'with procedure_presentation having different filters' do
|
context 'with procedure_presentation having different filters' do
|
||||||
it 'works once' do
|
it 'works once' do
|
||||||
|
@ -105,7 +94,10 @@ RSpec.describe Export, type: :model do
|
||||||
it 'works once, changes procedure_presentation, recreate a new' do
|
it 'works once, changes procedure_presentation, recreate a new' do
|
||||||
expect { Export.find_or_create_fresh_export(:zip, [gi_1], instructeur, time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) }
|
expect { Export.find_or_create_fresh_export(:zip, [gi_1], instructeur, time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) }
|
||||||
.to change { Export.count }.by(1)
|
.to change { Export.count }.by(1)
|
||||||
pp.add_filter('tous', procedure.find_column(label: 'Mis à jour le').id, '10/12/2021')
|
|
||||||
|
update_at_column = FilteredColumn.new(column: procedure.find_column(label: 'Mis à jour le'), filter: '10/12/2021')
|
||||||
|
pp.update(tous_filters: [created_at_column, update_at_column])
|
||||||
|
|
||||||
expect { Export.find_or_create_fresh_export(:zip, [gi_1], instructeur, time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) }
|
expect { Export.find_or_create_fresh_export(:zip, [gi_1], instructeur, time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) }
|
||||||
.to change { Export.count }.by(1)
|
.to change { Export.count }.by(1)
|
||||||
end
|
end
|
||||||
|
@ -162,10 +154,16 @@ RSpec.describe Export, type: :model do
|
||||||
let!(:dossier_accepte) { create(:dossier, :accepte, procedure: procedure) }
|
let!(:dossier_accepte) { create(:dossier, :accepte, procedure: procedure) }
|
||||||
|
|
||||||
let(:export) do
|
let(:export) do
|
||||||
create(:export,
|
groupe_instructeurs = [procedure.groupe_instructeurs.first]
|
||||||
groupe_instructeurs: [procedure.groupe_instructeurs.first],
|
user_profile = groupe_instructeurs.first.instructeurs.first
|
||||||
procedure_presentation: procedure_presentation,
|
|
||||||
statut: statut)
|
Export.find_or_create_fresh_export(
|
||||||
|
:csv,
|
||||||
|
groupe_instructeurs,
|
||||||
|
user_profile,
|
||||||
|
procedure_presentation:,
|
||||||
|
statut:
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'without procedure_presentation or since' do
|
context 'without procedure_presentation or since' do
|
||||||
|
@ -179,17 +177,28 @@ RSpec.describe Export, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with procedure_presentation and statut supprimes' do
|
context 'with procedure_presentation and statut tous and filter en_construction' do
|
||||||
let(:statut) { 'supprimes' }
|
let(:statut) { 'tous' }
|
||||||
let(:procedure_presentation) do
|
|
||||||
create(:procedure_presentation,
|
|
||||||
procedure: procedure,
|
|
||||||
assign_to: procedure.groupe_instructeurs.first.assign_tos.first)
|
|
||||||
end
|
|
||||||
let!(:dossier_supprime) { create(:dossier, :accepte, procedure: procedure, hidden_by_administration_at: 2.days.ago) }
|
|
||||||
|
|
||||||
it 'includes supprimes' do
|
let(:procedure_presentation) do
|
||||||
expect(export.send(:dossiers_for_export)).to include(dossier_supprime)
|
statut_column = procedure.find_column(label: 'Statut')
|
||||||
|
en_construction_filter = FilteredColumn.new(column: statut_column, filter: 'en_construction')
|
||||||
|
create(:procedure_presentation,
|
||||||
|
procedure:,
|
||||||
|
assign_to: procedure.groupe_instructeurs.first.assign_tos.first,
|
||||||
|
tous_filters: [en_construction_filter])
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
# ensure the export is generated
|
||||||
|
export
|
||||||
|
|
||||||
|
# change the procedure presentation
|
||||||
|
procedure_presentation.update(tous_filters: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'only includes the en_construction' do
|
||||||
|
expect(export.send(:dossiers_for_export)).to eq([dossier_en_construction])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
59
spec/models/filtered_column_spec.rb
Normal file
59
spec/models/filtered_column_spec.rb
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe FilteredColumn do
|
||||||
|
describe '#check_filters_max_length' do
|
||||||
|
let(:column) { Column.new(procedure_id: 1, table: 'table', column: 'column', label: 'label') }
|
||||||
|
let(:filtered_column) { described_class.new(column:, filter:) }
|
||||||
|
|
||||||
|
before { filtered_column.valid? }
|
||||||
|
|
||||||
|
context 'when the filter is too long' do
|
||||||
|
let(:filter) { 'a' * (FilteredColumn::FILTERS_VALUE_MAX_LENGTH + 1) }
|
||||||
|
|
||||||
|
it 'adds an error' do
|
||||||
|
expect(filtered_column.errors.map(&:message)).to include(/Le filtre label est trop long/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when then filter is not too long' do
|
||||||
|
let(:filter) { 'a' * FilteredColumn::FILTERS_VALUE_MAX_LENGTH }
|
||||||
|
|
||||||
|
it 'does not add an error' do
|
||||||
|
expect(filtered_column.errors).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the filter is empty' do
|
||||||
|
let(:filter) { nil }
|
||||||
|
|
||||||
|
it 'does not add an error' do
|
||||||
|
expect(filtered_column.errors).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#check_filters_max_integer' do
|
||||||
|
context 'when the target column is an id column' do
|
||||||
|
let(:column) { Column.new(procedure_id: 1, table: 'table', column: 'id', label: 'label') }
|
||||||
|
let(:filtered_column) { described_class.new(column:, filter:) }
|
||||||
|
|
||||||
|
before { filtered_column.valid? }
|
||||||
|
|
||||||
|
context 'when the filter is too high' do
|
||||||
|
let(:filter) { (FilteredColumn::PG_INTEGER_MAX_VALUE + 1).to_s }
|
||||||
|
|
||||||
|
it 'adds an error' do
|
||||||
|
expect(filtered_column.errors.map(&:message)).to include(/Le filtre label n'est pas un numéro de dossier possible/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the filter is not too high' do
|
||||||
|
let(:filter) { FilteredColumn::PG_INTEGER_MAX_VALUE.to_s }
|
||||||
|
|
||||||
|
it 'does not add an error' do
|
||||||
|
expect(filtered_column.errors).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -167,20 +167,6 @@ describe Instructeur, type: :model do
|
||||||
it { expect(errors).to be_nil }
|
it { expect(errors).to be_nil }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with invalid presentation' do
|
|
||||||
let(:procedure_id) { procedure.id }
|
|
||||||
before do
|
|
||||||
pp = ProcedurePresentation.create(assign_to: procedure_assign, displayed_fields: [{ 'table' => 'invalid', 'column' => 'random' }])
|
|
||||||
pp.save(:validate => false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'recreates a valid prsentation' do
|
|
||||||
expect(procedure_presentation).to be_persisted
|
|
||||||
end
|
|
||||||
it { expect(procedure_presentation).to be_valid }
|
|
||||||
it { expect(errors).to be_present }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with default presentation' do
|
context 'with default presentation' do
|
||||||
let(:procedure_id) { procedure_2.id }
|
let(:procedure_id) { procedure_2.id }
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,15 @@ describe ProcedurePresentation do
|
||||||
include ActiveSupport::Testing::TimeHelpers
|
include ActiveSupport::Testing::TimeHelpers
|
||||||
|
|
||||||
let(:procedure) { create(:procedure, :published, types_de_champ_public:, types_de_champ_private: [{}]) }
|
let(:procedure) { create(:procedure, :published, types_de_champ_public:, types_de_champ_private: [{}]) }
|
||||||
|
let(:procedure_id) { procedure.id }
|
||||||
let(:types_de_champ_public) { [{}] }
|
let(:types_de_champ_public) { [{}] }
|
||||||
let(:instructeur) { create(:instructeur) }
|
let(:instructeur) { create(:instructeur) }
|
||||||
let(:assign_to) { create(:assign_to, procedure: procedure, instructeur: instructeur) }
|
let(:assign_to) { create(:assign_to, procedure:, instructeur:) }
|
||||||
let(:first_type_de_champ) { assign_to.procedure.active_revision.types_de_champ_public.first }
|
let(:first_type_de_champ) { assign_to.procedure.active_revision.types_de_champ_public.first }
|
||||||
let(:first_type_de_champ_id) { first_type_de_champ.stable_id.to_s }
|
let(:first_type_de_champ_id) { first_type_de_champ.stable_id.to_s }
|
||||||
let(:procedure_presentation) {
|
let(:procedure_presentation) {
|
||||||
create(:procedure_presentation,
|
create(:procedure_presentation,
|
||||||
assign_to: assign_to,
|
assign_to:,
|
||||||
displayed_fields: [
|
displayed_fields: [
|
||||||
{ label: "test1", table: "user", column: "email" },
|
{ label: "test1", table: "user", column: "email" },
|
||||||
{ label: "test2", table: "type_de_champ", column: first_type_de_champ_id }
|
{ label: "test2", table: "type_de_champ", column: first_type_de_champ_id }
|
||||||
|
@ -22,6 +23,8 @@ describe ProcedurePresentation do
|
||||||
let(:procedure_presentation_id) { procedure_presentation.id }
|
let(:procedure_presentation_id) { procedure_presentation.id }
|
||||||
let(:filters) { { "a-suivre" => [], "suivis" => [{ "label" => "label1", "table" => "self", "column" => "created_at" }] } }
|
let(:filters) { { "a-suivre" => [], "suivis" => [{ "label" => "label1", "table" => "self", "column" => "created_at" }] } }
|
||||||
|
|
||||||
|
def to_filter((label, filter)) = FilteredColumn.new(column: procedure.find_column(label: label), filter: filter)
|
||||||
|
|
||||||
describe "#displayed_fields" do
|
describe "#displayed_fields" do
|
||||||
it { expect(procedure_presentation.displayed_fields).to eq([{ "label" => "test1", "table" => "user", "column" => "email" }, { "label" => "test2", "table" => "type_de_champ", "column" => first_type_de_champ_id }]) }
|
it { expect(procedure_presentation.displayed_fields).to eq([{ "label" => "test1", "table" => "user", "column" => "email" }, { "label" => "test2", "table" => "type_de_champ", "column" => first_type_de_champ_id }]) }
|
||||||
end
|
end
|
||||||
|
@ -37,744 +40,27 @@ describe ProcedurePresentation do
|
||||||
describe 'validation' do
|
describe 'validation' do
|
||||||
it { expect(build(:procedure_presentation)).to be_valid }
|
it { expect(build(:procedure_presentation)).to be_valid }
|
||||||
|
|
||||||
context 'of displayed fields' do
|
context 'of displayed columns' do
|
||||||
it { expect(build(:procedure_presentation, displayed_fields: [{ table: "user", column: "reset_password_token", "order" => "asc" }])).to be_invalid }
|
it do
|
||||||
|
pp = build(:procedure_presentation, displayed_columns: [{ table: "user", column: "reset_password_token", procedure_id: }])
|
||||||
|
expect { pp.displayed_columns }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'of filters' do
|
context 'of filters' do
|
||||||
it do
|
it 'validates the filter_column objects' do
|
||||||
expect(build(:procedure_presentation, filters: { "suivis" => [{ table: "user", column: "reset_password_token", "order" => "asc" }] })).to be_invalid
|
expect(build(:procedure_presentation, "suivis_filters": [{ id: { column_id: "user/email", procedure_id: }, "filter": "not so long filter value" }])).to be_valid
|
||||||
expect(build(:procedure_presentation, filters: { "suivis" => [{ table: "user", column: "email", "value" => "exceedingly long filter value" * 1000 }] })).to be_invalid
|
expect(build(:procedure_presentation, "suivis_filters": [{ id: { column_id: "user/email", procedure_id: }, "filter": "exceedingly long filter value" * 400 }])).to be_invalid
|
||||||
end
|
|
||||||
|
|
||||||
describe 'check_filters_max_integer' do
|
|
||||||
it do
|
|
||||||
expect(build(:procedure_presentation, filters: { "suivis" => [{ table: "self", column: "id", "value" => ProcedurePresentation::PG_INTEGER_MAX_VALUE.to_s }] })).to be_invalid
|
|
||||||
expect(build(:procedure_presentation, filters: { "suivis" => [{ table: "self", column: "id", "value" => (ProcedurePresentation::PG_INTEGER_MAX_VALUE - 1).to_s }] })).to be_valid
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#sorted_ids' do
|
|
||||||
let(:instructeur) { create(:instructeur) }
|
|
||||||
let(:assign_to) { create(:assign_to, procedure:, instructeur:) }
|
|
||||||
let(:sorted_column) { SortedColumn.new(column:, order:) }
|
|
||||||
let(:procedure_presentation) { create(:procedure_presentation, assign_to:, sorted_column:) }
|
|
||||||
|
|
||||||
subject { procedure_presentation.send(:sorted_ids, procedure.dossiers, procedure.dossiers.count) }
|
|
||||||
|
|
||||||
context 'for notifications table' do
|
|
||||||
let(:column) { procedure.notifications_column }
|
|
||||||
|
|
||||||
let!(:notified_dossier) { create(:dossier, :en_construction, procedure:) }
|
|
||||||
let!(:recent_dossier) { create(:dossier, :en_construction, procedure:) }
|
|
||||||
let!(:older_dossier) { create(:dossier, :en_construction, procedure:) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
notified_dossier.update!(last_champ_updated_at: Time.zone.local(2018, 9, 20))
|
|
||||||
create(:follow, instructeur: instructeur, dossier: notified_dossier, demande_seen_at: Time.zone.local(2018, 9, 10))
|
|
||||||
notified_dossier.touch(time: Time.zone.local(2018, 9, 20))
|
|
||||||
recent_dossier.touch(time: Time.zone.local(2018, 9, 25))
|
|
||||||
older_dossier.touch(time: Time.zone.local(2018, 5, 13))
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'in ascending order' do
|
|
||||||
let(:order) { 'asc' }
|
|
||||||
|
|
||||||
it { is_expected.to eq([older_dossier, recent_dossier, notified_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'in descending order' do
|
|
||||||
let(:order) { 'desc' }
|
|
||||||
|
|
||||||
it { is_expected.to eq([notified_dossier, recent_dossier, older_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with a dossier terminé' do
|
|
||||||
let!(:notified_dossier) { create(:dossier, :accepte, procedure:) }
|
|
||||||
let(:order) { 'desc' }
|
|
||||||
|
|
||||||
it { is_expected.to eq([notified_dossier, recent_dossier, older_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for self table' do
|
|
||||||
let(:order) { 'asc' } # Desc works the same, no extra test required
|
|
||||||
|
|
||||||
context 'for created_at column' do
|
|
||||||
let!(:column) { procedure.find_column(label: 'Créé le') }
|
|
||||||
let!(:recent_dossier) { Timecop.freeze(Time.zone.local(2018, 10, 17)) { create(:dossier, procedure: procedure) } }
|
|
||||||
let!(:older_dossier) { Timecop.freeze(Time.zone.local(2003, 11, 11)) { create(:dossier, procedure: procedure) } }
|
|
||||||
|
|
||||||
it { is_expected.to eq([older_dossier, recent_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for en_construction_at column' do
|
|
||||||
let!(:column) { procedure.find_column(label: 'En construction le') }
|
|
||||||
let!(:recent_dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: Time.zone.local(2018, 10, 17)) }
|
|
||||||
let!(:older_dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: Time.zone.local(2013, 1, 1)) }
|
|
||||||
|
|
||||||
it { is_expected.to eq([older_dossier, recent_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for updated_at column' do
|
|
||||||
let(:column) { procedure.find_column(label: 'Mis à jour le') }
|
|
||||||
let(:recent_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
let(:older_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
recent_dossier.touch(time: Time.zone.local(2018, 9, 25))
|
|
||||||
older_dossier.touch(time: Time.zone.local(2018, 5, 13))
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to eq([older_dossier, recent_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for type_de_champ table' do
|
|
||||||
context 'with no revisions' do
|
|
||||||
let(:table) { 'type_de_champ' }
|
|
||||||
let(:column) { procedure.find_column(label: first_type_de_champ.libelle) }
|
|
||||||
|
|
||||||
let(:beurre_dossier) { create(:dossier, procedure:) }
|
|
||||||
let(:tartine_dossier) { create(:dossier, procedure:) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
beurre_dossier.project_champs_public.first.update(value: 'beurre')
|
|
||||||
tartine_dossier.project_champs_public.first.update(value: 'tartine')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'asc' do
|
|
||||||
let(:order) { 'asc' }
|
|
||||||
|
|
||||||
it { is_expected.to eq([beurre_dossier, tartine_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'desc' do
|
|
||||||
let(:order) { 'desc' }
|
|
||||||
|
|
||||||
it { is_expected.to eq([tartine_dossier, beurre_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with a revision adding a new type_de_champ' do
|
|
||||||
let!(:tdc) { { type_champ: :text, libelle: 'nouveau champ' } }
|
|
||||||
let(:column) { procedure.find_column(label: 'nouveau champ') }
|
|
||||||
|
|
||||||
let!(:nothing_dossier) { create(:dossier, procedure:) }
|
|
||||||
let!(:beurre_dossier) { create(:dossier, procedure:) }
|
|
||||||
let!(:tartine_dossier) { create(:dossier, procedure:) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
nothing_dossier
|
|
||||||
procedure.draft_revision.add_type_de_champ(tdc)
|
|
||||||
procedure.publish_revision!
|
|
||||||
beurre_dossier.project_champs_public.last.update(value: 'beurre')
|
|
||||||
tartine_dossier.project_champs_public.last.update(value: 'tartine')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'asc' do
|
|
||||||
let(:order) { 'asc' }
|
|
||||||
it { is_expected.to eq([nothing_dossier, beurre_dossier, tartine_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'desc' do
|
|
||||||
let(:order) { 'desc' }
|
|
||||||
it { is_expected.to eq([tartine_dossier, beurre_dossier, nothing_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for type_de_champ_private table' do
|
|
||||||
context 'with no revisions' do
|
|
||||||
let(:column) { procedure.find_column(label: procedure.active_revision.types_de_champ_private.first.libelle) }
|
|
||||||
|
|
||||||
let(:biere_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
let(:vin_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
biere_dossier.project_champs_private.first.update(value: 'biere')
|
|
||||||
vin_dossier.project_champs_private.first.update(value: 'vin')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'asc' do
|
|
||||||
let(:order) { 'asc' }
|
|
||||||
|
|
||||||
it { is_expected.to eq([biere_dossier, vin_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'desc' do
|
|
||||||
let(:order) { 'desc' }
|
|
||||||
|
|
||||||
it { is_expected.to eq([vin_dossier, biere_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for individual table' do
|
|
||||||
let(:order) { 'asc' } # Desc works the same, no extra test required
|
|
||||||
|
|
||||||
let(:procedure) { create(:procedure, :for_individual) }
|
|
||||||
|
|
||||||
let!(:first_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'M', prenom: 'Alain', nom: 'Antonelli')) }
|
|
||||||
let!(:last_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'Mme', prenom: 'Zora', nom: 'Zemmour')) }
|
|
||||||
|
|
||||||
context 'for gender column' do
|
|
||||||
let(:column) { procedure.find_column(label: 'Civilité') }
|
|
||||||
|
|
||||||
it { is_expected.to eq([first_dossier, last_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for prenom column' do
|
|
||||||
let(:column) { procedure.find_column(label: 'Prénom') }
|
|
||||||
|
|
||||||
it { is_expected.to eq([first_dossier, last_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for nom column' do
|
|
||||||
let(:column) { procedure.find_column(label: 'Nom') }
|
|
||||||
|
|
||||||
it { is_expected.to eq([first_dossier, last_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for followers_instructeurs table' do
|
|
||||||
let(:order) { 'asc' } # Desc works the same, no extra test required
|
|
||||||
|
|
||||||
let!(:dossier_z) { create(:dossier, :en_construction, procedure: procedure) }
|
|
||||||
let!(:dossier_a) { create(:dossier, :en_construction, procedure: procedure) }
|
|
||||||
let!(:dossier_without_instructeur) { create(:dossier, :en_construction, procedure: procedure) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
create(:follow, dossier: dossier_z, instructeur: create(:instructeur, email: 'zythum@exemple.fr'))
|
|
||||||
create(:follow, dossier: dossier_a, instructeur: create(:instructeur, email: 'abaca@exemple.fr'))
|
|
||||||
create(:follow, dossier: dossier_a, instructeur: create(:instructeur, email: 'abaca2@exemple.fr'))
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for email column' do
|
|
||||||
let(:column) { procedure.find_column(label: 'Email instructeur') }
|
|
||||||
|
|
||||||
it { is_expected.to eq([dossier_a, dossier_z, dossier_without_instructeur].map(&:id)) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for avis table' do
|
|
||||||
let(:column) { procedure.find_column(label: 'Avis oui/non') }
|
|
||||||
let(:order) { 'asc' }
|
|
||||||
|
|
||||||
let!(:dossier_yes) { create(:dossier, procedure:) }
|
|
||||||
let!(:dossier_no) { create(:dossier, procedure:) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
create_list(:avis, 2, dossier: dossier_yes, question_answer: true)
|
|
||||||
create(:avis, dossier: dossier_no, question_answer: true)
|
|
||||||
create(:avis, dossier: dossier_no, question_answer: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to eq([dossier_no, dossier_yes].map(&:id)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for other tables' do
|
|
||||||
# All other columns and tables work the same so it’s ok to test only one
|
|
||||||
let(:column) { procedure.find_column(label: 'Code postal') }
|
|
||||||
let(:order) { 'asc' } # Desc works the same, no extra test required
|
|
||||||
|
|
||||||
let!(:huitieme_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, code_postal: '75008')) }
|
|
||||||
let!(:vingtieme_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, code_postal: '75020')) }
|
|
||||||
|
|
||||||
it { is_expected.to eq([huitieme_dossier, vingtieme_dossier].map(&:id)) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#filtered_ids' do
|
|
||||||
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to, filters: { "suivis" => filter }) }
|
|
||||||
|
|
||||||
subject { procedure_presentation.send(:filtered_ids, procedure.dossiers.joins(:user), 'suivis') }
|
|
||||||
|
|
||||||
context 'for self table' do
|
|
||||||
context 'for created_at column' do
|
|
||||||
let(:filter) { [{ 'table' => 'self', 'column' => 'created_at', 'value' => '18/9/2018' }] }
|
|
||||||
|
|
||||||
let!(:kept_dossier) { create(:dossier, procedure: procedure, created_at: Time.zone.local(2018, 9, 18, 14, 28)) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, procedure: procedure, created_at: Time.zone.local(2018, 9, 17, 23, 59)) }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for en_construction_at column' do
|
|
||||||
let(:filter) { [{ 'table' => 'self', 'column' => 'en_construction_at', 'value' => '17/10/2018' }] }
|
|
||||||
|
|
||||||
let!(:kept_dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: Time.zone.local(2018, 10, 17)) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: Time.zone.local(2013, 1, 1)) }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for updated_at column' do
|
|
||||||
let(:filter) { [{ 'table' => 'self', 'column' => 'updated_at', 'value' => '18/9/2018' }] }
|
|
||||||
|
|
||||||
let(:kept_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
let(:discarded_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
kept_dossier.touch(time: Time.zone.local(2018, 9, 18, 14, 28))
|
|
||||||
discarded_dossier.touch(time: Time.zone.local(2018, 9, 17, 23, 59))
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for updated_since column' do
|
|
||||||
let(:filter) { [{ 'table' => 'self', 'column' => 'updated_since', 'value' => '18/9/2018' }] }
|
|
||||||
|
|
||||||
let(:kept_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
let(:later_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
let(:discarded_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
kept_dossier.touch(time: Time.zone.local(2018, 9, 18, 14, 28))
|
|
||||||
later_dossier.touch(time: Time.zone.local(2018, 9, 19, 14, 28))
|
|
||||||
discarded_dossier.touch(time: Time.zone.local(2018, 9, 17, 14, 28))
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to match_array([kept_dossier.id, later_dossier.id]) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for sva_svr_decision_before column' do
|
|
||||||
before do
|
|
||||||
travel_to Time.zone.local(2023, 6, 10, 10)
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:procedure) { create(:procedure, :published, :sva, types_de_champ_public: [{}], types_de_champ_private: [{}]) }
|
|
||||||
let(:filter) { [{ 'table' => 'self', 'column' => 'sva_svr_decision_before', 'value' => '15/06/2023' }] }
|
|
||||||
|
|
||||||
let!(:kept_dossier) { create(:dossier, :en_instruction, procedure:, sva_svr_decision_on: Date.current) }
|
|
||||||
let!(:later_dossier) { create(:dossier, :en_instruction, procedure:, sva_svr_decision_on: Date.current + 2.days) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, :en_instruction, procedure:, sva_svr_decision_on: Date.current + 10.days) }
|
|
||||||
let!(:en_construction_dossier) { create(:dossier, :en_construction, procedure:, sva_svr_decision_on: Date.current + 2.days) }
|
|
||||||
let!(:accepte_dossier) { create(:dossier, :accepte, procedure:, sva_svr_decision_on: Date.current + 2.days) }
|
|
||||||
|
|
||||||
it { is_expected.to match_array([kept_dossier.id, later_dossier.id, en_construction_dossier.id]) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'ignore time of day' do
|
|
||||||
let(:filter) { [{ 'table' => 'self', 'column' => 'en_construction_at', 'value' => '17/10/2018 19:30' }] }
|
|
||||||
|
|
||||||
let!(:kept_dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: Time.zone.local(2018, 10, 17, 15, 56)) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: Time.zone.local(2018, 10, 18, 5, 42)) }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for a malformed date' do
|
|
||||||
context 'when its a string' do
|
|
||||||
let(:filter) { [{ 'table' => 'self', 'column' => 'updated_at', 'value' => 'malformed date' }] }
|
|
||||||
|
|
||||||
it { is_expected.to match([]) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when its a number' do
|
|
||||||
let(:filter) { [{ 'table' => 'self', 'column' => 'updated_at', 'value' => '177500' }] }
|
|
||||||
|
|
||||||
it { is_expected.to match([]) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with multiple search values' do
|
|
||||||
let(:filter) do
|
|
||||||
[
|
|
||||||
{ 'table' => 'self', 'column' => 'en_construction_at', 'value' => '17/10/2018' },
|
|
||||||
{ 'table' => 'self', 'column' => 'en_construction_at', 'value' => '19/10/2018' }
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let!(:kept_dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: Time.zone.local(2018, 10, 17)) }
|
|
||||||
let!(:other_kept_dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: Time.zone.local(2018, 10, 19)) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: Time.zone.local(2013, 1, 1)) }
|
|
||||||
|
|
||||||
it 'returns every dossier that matches any of the search criteria for a given column' do
|
|
||||||
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with multiple state filters' do
|
|
||||||
let(:filter) do
|
|
||||||
[
|
|
||||||
{ 'table' => 'self', 'column' => 'state', 'value' => 'en_construction' },
|
|
||||||
{ 'table' => 'self', 'column' => 'state', 'value' => 'en_instruction' }
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let!(:kept_dossier) { create(:dossier, :en_construction, procedure: procedure) }
|
|
||||||
let!(:other_kept_dossier) { create(:dossier, :en_instruction, procedure: procedure) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, :accepte, procedure: procedure) }
|
|
||||||
|
|
||||||
it 'returns every dossier that matches any of the search criteria for a given column' do
|
|
||||||
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with en_construction state filters' do
|
|
||||||
let(:filter) do
|
|
||||||
[
|
|
||||||
{ 'table' => 'self', 'column' => 'state', 'value' => 'en_construction' }
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let!(:en_construction) { create(:dossier, :en_construction, procedure: procedure) }
|
|
||||||
let!(:en_construction_with_correction) { create(:dossier, :en_construction, procedure: procedure) }
|
|
||||||
let!(:correction) { create(:dossier_correction, dossier: en_construction_with_correction) }
|
|
||||||
it 'excludes dossier en construction with pending correction' do
|
|
||||||
is_expected.to contain_exactly(en_construction.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for type_de_champ table' do
|
|
||||||
let(:filter) { [{ 'table' => 'type_de_champ', 'column' => type_de_champ.stable_id.to_s, 'value' => 'keep' }] }
|
|
||||||
|
|
||||||
let(:kept_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
let(:discarded_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
let(:type_de_champ) { procedure.active_revision.types_de_champ_public.first }
|
|
||||||
|
|
||||||
context 'with single value' do
|
|
||||||
before do
|
|
||||||
kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'keep me')
|
|
||||||
discarded_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'discard me')
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with multiple search values' do
|
|
||||||
let(:filter) do
|
|
||||||
[
|
|
||||||
{ 'table' => 'type_de_champ', 'column' => type_de_champ.stable_id.to_s, 'value' => 'keep' },
|
|
||||||
{ 'table' => 'type_de_champ', 'column' => type_de_champ.stable_id.to_s, 'value' => 'and' }
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:other_kept_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'keep me')
|
|
||||||
discarded_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'discard me')
|
|
||||||
other_kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'and me too')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns every dossier that matches any of the search criteria for a given column' do
|
|
||||||
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with yes_no type_de_champ' do
|
|
||||||
let(:filter) { [{ 'table' => 'type_de_champ', 'column' => type_de_champ.stable_id.to_s, 'value' => 'true' }] }
|
|
||||||
let(:types_de_champ_public) { [{ type: :yes_no }] }
|
|
||||||
|
|
||||||
before do
|
|
||||||
kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'true')
|
|
||||||
discarded_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'false')
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with departement type_de_champ' do
|
|
||||||
let(:filter) { [{ 'table' => 'type_de_champ', 'column' => type_de_champ.stable_id.to_s, 'value_column' => :external_id, 'value' => '13' }] }
|
|
||||||
let(:types_de_champ_public) { [{ type: :departements }] }
|
|
||||||
|
|
||||||
before do
|
|
||||||
kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(external_id: '13')
|
|
||||||
discarded_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(external_id: '69')
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with enum type_de_champ' do
|
|
||||||
let(:filter_value) { 'Favorable' }
|
|
||||||
let(:filter) { [{ 'table' => 'type_de_champ', 'column' => type_de_champ.stable_id.to_s, 'value_column' => :value, 'value' => filter_value }] }
|
|
||||||
let(:types_de_champ_public) { [{ type: :drop_down_list, options: ['Favorable', 'Defavorable'] }] }
|
|
||||||
|
|
||||||
before do
|
|
||||||
kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'Favorable')
|
|
||||||
discarded_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(external_id: 'Defavorable')
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for type_de_champ_private table' do
|
|
||||||
let(:filter) { [{ 'table' => 'type_de_champ', 'column' => type_de_champ_private.stable_id.to_s, 'value' => 'keep' }] }
|
|
||||||
|
|
||||||
let(:kept_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
let(:discarded_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
let(:type_de_champ_private) { procedure.active_revision.types_de_champ_private.first }
|
|
||||||
|
|
||||||
before do
|
|
||||||
kept_dossier.champs.find_by(stable_id: type_de_champ_private.stable_id).update(value: 'keep me')
|
|
||||||
discarded_dossier.champs.find_by(stable_id: type_de_champ_private.stable_id).update(value: 'discard me')
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
|
|
||||||
context 'with multiple search values' do
|
|
||||||
let(:filter) do
|
|
||||||
[
|
|
||||||
{ 'table' => 'type_de_champ', 'column' => type_de_champ_private.stable_id.to_s, 'value' => 'keep' },
|
|
||||||
{ 'table' => 'type_de_champ', 'column' => type_de_champ_private.stable_id.to_s, 'value' => 'and' }
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:other_kept_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
other_kept_dossier.champs.find_by(stable_id: type_de_champ_private.stable_id).update(value: 'and me too')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns every dossier that matches any of the search criteria for a given column' do
|
|
||||||
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for type_de_champ using AddressableColumnConcern' do
|
|
||||||
let(:types_de_champ_public) { [{ type: :rna, stable_id: 1 }] }
|
|
||||||
let(:type_de_champ) { procedure.active_revision.types_de_champ.first }
|
|
||||||
let(:available_columns) { type_de_champ.columns(procedure_id: procedure.id) }
|
|
||||||
let(:column) { available_columns.find { _1.value_column == value_column_searched } }
|
|
||||||
let(:filter) { [column.to_json.merge({ "value" => value })] }
|
|
||||||
let(:kept_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
|
|
||||||
context "when searching by postal_code (text)" do
|
|
||||||
let(:value) { "60580" }
|
|
||||||
let(:value_column_searched) { ['postal_code'] }
|
|
||||||
|
|
||||||
before do
|
|
||||||
kept_dossier.project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "postal_code" => value })
|
|
||||||
create(:dossier, procedure: procedure).project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "postal_code" => "unknown" })
|
|
||||||
end
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
it 'describes column' do
|
|
||||||
expect(column.type).to eq(:text)
|
|
||||||
expect(column.options_for_select).to eq([])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when searching by departement_code (enum)" do
|
|
||||||
let(:value) { "99" }
|
|
||||||
let(:value_column_searched) { ['departement_code'] }
|
|
||||||
|
|
||||||
before do
|
|
||||||
kept_dossier.project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "departement_code" => value })
|
|
||||||
create(:dossier, procedure: procedure).project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "departement_code" => "unknown" })
|
|
||||||
end
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
|
|
||||||
it 'describes column' do
|
|
||||||
expect(column.type).to eq(:enum)
|
|
||||||
expect(column.options_for_select.first).to eq(["99 – Etranger", "99"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when searching by region_name" do
|
|
||||||
let(:value) { "60" }
|
|
||||||
let(:value_column_searched) { ['region_name'] }
|
|
||||||
|
|
||||||
before do
|
|
||||||
kept_dossier.project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "region_name" => value })
|
|
||||||
create(:dossier, procedure: procedure).project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "region_name" => "unknown" })
|
|
||||||
end
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
it 'describes column' do
|
|
||||||
expect(column.type).to eq(:enum)
|
|
||||||
expect(column.options_for_select.first).to eq(["Auvergne-Rhône-Alpes", "Auvergne-Rhône-Alpes"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for etablissement table' do
|
|
||||||
context 'for entreprise_date_creation column' do
|
|
||||||
let(:filter) { [{ 'table' => 'etablissement', 'column' => 'entreprise_date_creation', 'value' => '21/6/2018' }] }
|
|
||||||
|
|
||||||
let!(:kept_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, entreprise_date_creation: Time.zone.local(2018, 6, 21))) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, entreprise_date_creation: Time.zone.local(2008, 6, 21))) }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
|
|
||||||
context 'with multiple search values' do
|
|
||||||
let(:filter) do
|
|
||||||
[
|
|
||||||
{ 'table' => 'etablissement', 'column' => 'entreprise_date_creation', 'value' => '21/6/2016' },
|
|
||||||
{ 'table' => 'etablissement', 'column' => 'entreprise_date_creation', 'value' => '21/6/2018' }
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let!(:other_kept_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, entreprise_date_creation: Time.zone.local(2016, 6, 21))) }
|
|
||||||
|
|
||||||
it 'returns every dossier that matches any of the search criteria for a given column' do
|
|
||||||
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for code_postal column' do
|
|
||||||
# All columns except entreprise_date_creation work exacly the same, just testing one
|
|
||||||
|
|
||||||
let(:filter) { [{ 'table' => 'etablissement', 'column' => 'code_postal', 'value' => '75017' }] }
|
|
||||||
|
|
||||||
let!(:kept_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, code_postal: '75017')) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, code_postal: '25000')) }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
|
|
||||||
context 'with multiple search values' do
|
|
||||||
let(:filter) do
|
|
||||||
[
|
|
||||||
{ 'table' => 'etablissement', 'column' => 'code_postal', 'value' => '75017' },
|
|
||||||
{ 'table' => 'etablissement', 'column' => 'code_postal', 'value' => '88100' }
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let!(:other_kept_dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, code_postal: '88100')) }
|
|
||||||
|
|
||||||
it 'returns every dossier that matches any of the search criteria for a given column' do
|
|
||||||
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for user table' do
|
|
||||||
let(:filter) { [{ 'table' => 'user', 'column' => 'email', 'value' => 'keepmail' }] }
|
|
||||||
|
|
||||||
let!(:kept_dossier) { create(:dossier, procedure: procedure, user: create(:user, email: 'me@keepmail.com')) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, procedure: procedure, user: create(:user, email: 'me@discard.com')) }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
|
|
||||||
context 'with multiple search values' do
|
|
||||||
let(:filter) do
|
|
||||||
[
|
|
||||||
{ 'table' => 'user', 'column' => 'email', 'value' => 'keepmail' },
|
|
||||||
{ 'table' => 'user', 'column' => 'email', 'value' => 'beta.gouv.fr' }
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let!(:other_kept_dossier) { create(:dossier, procedure: procedure, user: create(:user, email: 'bazinga@beta.gouv.fr')) }
|
|
||||||
|
|
||||||
it 'returns every dossier that matches any of the search criteria for a given column' do
|
|
||||||
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for individual table' do
|
|
||||||
let(:procedure) { create(:procedure, :for_individual) }
|
|
||||||
let!(:kept_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'Mme', prenom: 'Josephine', nom: 'Baker')) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'M', prenom: 'Jean', nom: 'Tremblay')) }
|
|
||||||
|
|
||||||
context 'for gender column' do
|
|
||||||
let(:filter) { [{ 'table' => 'individual', 'column' => 'gender', 'value' => 'Mme' }] }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for prenom column' do
|
|
||||||
let(:filter) { [{ 'table' => 'individual', 'column' => 'prenom', 'value' => 'Josephine' }] }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for nom column' do
|
|
||||||
let(:filter) { [{ 'table' => 'individual', 'column' => 'nom', 'value' => 'Baker' }] }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with multiple search values' do
|
|
||||||
let(:filter) do
|
|
||||||
[
|
|
||||||
{ 'table' => 'individual', 'column' => 'prenom', 'value' => 'Josephine' },
|
|
||||||
{ 'table' => 'individual', 'column' => 'prenom', 'value' => 'Romuald' }
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let!(:other_kept_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'M', prenom: 'Romuald', nom: 'Pistis')) }
|
|
||||||
|
|
||||||
it 'returns every dossier that matches any of the search criteria for a given column' do
|
|
||||||
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for followers_instructeurs table' do
|
|
||||||
let(:filter) { [{ 'table' => 'followers_instructeurs', 'column' => 'email', 'value' => 'keepmail' }] }
|
|
||||||
|
|
||||||
let!(:kept_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
create(:follow, dossier: kept_dossier, instructeur: create(:instructeur, email: 'me@keepmail.com'))
|
|
||||||
create(:follow, dossier: discarded_dossier, instructeur: create(:instructeur, email: 'me@discard.com'))
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
|
|
||||||
context 'with multiple search values' do
|
|
||||||
let(:filter) do
|
|
||||||
[
|
|
||||||
{ 'table' => 'followers_instructeurs', 'column' => 'email', 'value' => 'keepmail' },
|
|
||||||
{ 'table' => 'followers_instructeurs', 'column' => 'email', 'value' => 'beta.gouv.fr' }
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:other_kept_dossier) { create(:dossier, procedure: procedure) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
create(:follow, dossier: other_kept_dossier, instructeur: create(:instructeur, email: 'bazinga@beta.gouv.fr'))
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns every dossier that matches any of the search criteria for a given column' do
|
|
||||||
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for groupe_instructeur table' do
|
|
||||||
let(:filter) { [{ 'table' => 'groupe_instructeur', 'column' => 'id', 'value' => procedure.defaut_groupe_instructeur.id.to_s }] }
|
|
||||||
|
|
||||||
let!(:gi_2) { create(:groupe_instructeur, label: 'gi2', procedure: procedure) }
|
|
||||||
let!(:gi_3) { create(:groupe_instructeur, label: 'gi3', procedure: procedure) }
|
|
||||||
|
|
||||||
let!(:kept_dossier) { create(:dossier, :en_construction, procedure: procedure) }
|
|
||||||
let!(:discarded_dossier) { create(:dossier, :en_construction, procedure: procedure, groupe_instructeur: gi_2) }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
|
||||||
|
|
||||||
context 'with multiple search values' do
|
|
||||||
let(:filter) do
|
|
||||||
[
|
|
||||||
{ 'table' => 'groupe_instructeur', 'column' => 'id', 'value' => procedure.defaut_groupe_instructeur.id.to_s },
|
|
||||||
{ 'table' => 'groupe_instructeur', 'column' => 'id', 'value' => gi_3.id.to_s }
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let!(:other_kept_dossier) { create(:dossier, procedure: procedure, groupe_instructeur: gi_3) }
|
|
||||||
|
|
||||||
it 'returns every dossier that matches any of the search criteria for a given column' do
|
|
||||||
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#human_value_for_filter" do
|
describe "#human_value_for_filter" do
|
||||||
let(:filters) { { "suivis" => [{ label: "label1", table: "type_de_champ", column: first_type_de_champ_id, "value" => "true" }] } }
|
let(:filtered_column) { to_filter([first_type_de_champ.libelle, "true"]) }
|
||||||
|
|
||||||
subject { procedure_presentation.human_value_for_filter(procedure_presentation.filters["suivis"].first) }
|
subject do
|
||||||
|
procedure_presentation.human_value_for_filter(filtered_column)
|
||||||
|
end
|
||||||
|
|
||||||
context 'when type_de_champ text' do
|
context 'when type_de_champ text' do
|
||||||
it 'should passthrough value' do
|
it 'should passthrough value' do
|
||||||
|
@ -791,7 +77,7 @@ describe ProcedurePresentation do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when filter is state' do
|
context 'when filter is state' do
|
||||||
let(:filters) { { "suivis" => [{ table: "self", column: "state", "value" => "en_construction" }] } }
|
let(:filtered_column) { to_filter(['Statut', "en_construction"]) }
|
||||||
|
|
||||||
it 'should get i18n value' do
|
it 'should get i18n value' do
|
||||||
expect(subject).to eq("En construction")
|
expect(subject).to eq("En construction")
|
||||||
|
@ -799,7 +85,7 @@ describe ProcedurePresentation do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when filter is a date' do
|
context 'when filter is a date' do
|
||||||
let(:filters) { { "suivis" => [{ table: "self", column: "en_instruction_at", "value" => "15/06/2023" }] } }
|
let(:filtered_column) { to_filter(['Créé le', "15/06/2023"]) }
|
||||||
|
|
||||||
it 'should get formatted value' do
|
it 'should get formatted value' do
|
||||||
expect(subject).to eq("15/06/2023")
|
expect(subject).to eq("15/06/2023")
|
||||||
|
@ -807,129 +93,10 @@ describe ProcedurePresentation do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#add_filter" do
|
|
||||||
let(:filters) { { "suivis" => [] } }
|
|
||||||
|
|
||||||
context 'when type_de_champ yes_no' do
|
|
||||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no, libelle: 'oui ou non' }]) }
|
|
||||||
|
|
||||||
it 'should downcase and transform value' do
|
|
||||||
column_id = procedure.find_column(label: 'oui ou non').id
|
|
||||||
procedure_presentation.add_filter("suivis", column_id, "Oui")
|
|
||||||
|
|
||||||
expect(procedure_presentation.filters).to eq({
|
|
||||||
"suivis" =>
|
|
||||||
[
|
|
||||||
{ "label" => first_type_de_champ.libelle, "table" => "type_de_champ", "column" => first_type_de_champ_id, "value" => "true", "value_column" => "value" }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
suivis = procedure_presentation.suivis_filters.map { [_1['id'], _1['filter']] }
|
|
||||||
|
|
||||||
expect(suivis).to eq([[{ "column_id" => "type_de_champ/#{first_type_de_champ_id}", "procedure_id" => procedure.id }, "true"]])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when type_de_champ text' do
|
|
||||||
let(:filters) { { "suivis" => [] } }
|
|
||||||
let(:column_id) { procedure.find_column(label: first_type_de_champ.libelle).id }
|
|
||||||
|
|
||||||
it 'should passthrough value' do
|
|
||||||
procedure_presentation.add_filter("suivis", column_id, "Oui")
|
|
||||||
|
|
||||||
expect(procedure_presentation.filters).to eq({
|
|
||||||
"suivis" => [
|
|
||||||
{ "label" => first_type_de_champ.libelle, "table" => "type_de_champ", "column" => first_type_de_champ_id, "value" => "Oui", "value_column" => "value" }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when type_de_champ departements' do
|
|
||||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :departements }]) }
|
|
||||||
let(:column_id) { procedure.find_column(label: first_type_de_champ.libelle).id }
|
|
||||||
let(:filters) { { "suivis" => [] } }
|
|
||||||
|
|
||||||
it 'should set value_column' do
|
|
||||||
procedure_presentation.add_filter("suivis", column_id, "13")
|
|
||||||
|
|
||||||
expect(procedure_presentation.filters).to eq({
|
|
||||||
"suivis" => [
|
|
||||||
{ "label" => first_type_de_champ.libelle, "table" => "type_de_champ", "column" => first_type_de_champ_id, "value" => "13", "value_column" => "external_id" }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "#remove_filter" do
|
|
||||||
let(:filters) { { "suivis" => [] } }
|
|
||||||
let(:email_column_id) { procedure.find_column(label: 'Demandeur').id }
|
|
||||||
|
|
||||||
before do
|
|
||||||
procedure_presentation.add_filter("suivis", email_column_id, "a@a.com")
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should remove filter' do
|
|
||||||
expect(procedure_presentation.filters).to eq({ "suivis" => [{ "column" => "email", "label" => "Demandeur", "table" => "user", "value" => "a@a.com", "value_column" => "value" }] })
|
|
||||||
expect(procedure_presentation.suivis_filters).to eq([{ "filter" => "a@a.com", "id" => { "column_id" => "user/email", "procedure_id" => procedure.id } }])
|
|
||||||
|
|
||||||
procedure_presentation.remove_filter("suivis", email_column_id, "a@a.com")
|
|
||||||
procedure_presentation.reload
|
|
||||||
|
|
||||||
expect(procedure_presentation.filters).to eq({ "suivis" => [] })
|
|
||||||
expect(procedure_presentation.suivis_filters).to eq([])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#filtered_sorted_ids' do
|
|
||||||
let(:procedure_presentation) { create(:procedure_presentation, assign_to:) }
|
|
||||||
let(:dossier_1) { create(:dossier) }
|
|
||||||
let(:dossier_2) { create(:dossier) }
|
|
||||||
let(:dossier_3) { create(:dossier) }
|
|
||||||
let(:dossiers) { Dossier.where(id: [dossier_1, dossier_2, dossier_3].map(&:id)) }
|
|
||||||
|
|
||||||
let(:sorted_ids) { [dossier_2, dossier_3, dossier_1].map(&:id) }
|
|
||||||
let(:statut) { 'tous' }
|
|
||||||
|
|
||||||
subject { procedure_presentation.filtered_sorted_ids(dossiers, statut) }
|
|
||||||
|
|
||||||
context 'with no filters' do
|
|
||||||
let(:statut) { 'suivis' }
|
|
||||||
let(:dossiers) { procedure.dossiers }
|
|
||||||
|
|
||||||
before do
|
|
||||||
create(:follow, dossier: en_construction_dossier, instructeur: procedure_presentation.instructeur)
|
|
||||||
create(:follow, dossier: accepte_dossier, instructeur: procedure_presentation.instructeur)
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:en_construction_dossier) { create(:dossier, :en_construction, procedure:) }
|
|
||||||
let(:accepte_dossier) { create(:dossier, :accepte, procedure:) }
|
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(en_construction_dossier.id) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with mocked sorted_ids' do
|
|
||||||
before do
|
|
||||||
expect(procedure_presentation).to receive(:sorted_ids).and_return(sorted_ids)
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to eq(sorted_ids) }
|
|
||||||
|
|
||||||
context 'when a filter is present' do
|
|
||||||
let(:filtered_ids) { [dossier_1, dossier_2, dossier_3].map(&:id) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
procedure_presentation.filters['tous'] = 'some_filter'
|
|
||||||
expect(procedure_presentation).to receive(:filtered_ids).and_return(filtered_ids)
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to eq(sorted_ids) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#update_displayed_fields' do
|
describe '#update_displayed_fields' do
|
||||||
|
let(:en_construction_column) { procedure.find_column(label: 'En construction le') }
|
||||||
|
let(:mise_a_jour_column) { procedure.find_column(label: 'Mis à jour le') }
|
||||||
|
|
||||||
let(:procedure_presentation) do
|
let(:procedure_presentation) do
|
||||||
create(:procedure_presentation, assign_to:).tap do |pp|
|
create(:procedure_presentation, assign_to:).tap do |pp|
|
||||||
pp.update(sorted_column: SortedColumn.new(column: procedure.find_column(label: 'Demandeur'), order: 'desc'))
|
pp.update(sorted_column: SortedColumn.new(column: procedure.find_column(label: 'Demandeur'), order: 'desc'))
|
||||||
|
@ -937,24 +104,19 @@ describe ProcedurePresentation do
|
||||||
end
|
end
|
||||||
|
|
||||||
subject do
|
subject do
|
||||||
procedure_presentation.update_displayed_fields([
|
procedure_presentation.update(displayed_columns: [
|
||||||
procedure.find_column(label: 'En construction le').id,
|
en_construction_column.id, mise_a_jour_column.id
|
||||||
procedure.find_column(label: 'Mis à jour le').id
|
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should update displayed_fields' do
|
it 'should update displayed_fields' do
|
||||||
expect(procedure_presentation.displayed_columns).to eq([])
|
expect(procedure_presentation.displayed_columns).to eq(procedure.default_displayed_columns)
|
||||||
|
|
||||||
subject
|
subject
|
||||||
|
|
||||||
expect(procedure_presentation.displayed_columns).to eq([
|
expect(procedure_presentation.displayed_columns).to eq([
|
||||||
{ "column_id" => "self/en_construction_at", "procedure_id" => procedure.id },
|
en_construction_column, mise_a_jour_column
|
||||||
{ "column_id" => "self/updated_at", "procedure_id" => procedure.id }
|
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(procedure_presentation.sorted_column).to eq(procedure.default_sorted_column)
|
|
||||||
expect(procedure_presentation.sorted_column.order).to eq('desc')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
709
spec/services/dossier_filter_service_spec.rb
Normal file
709
spec/services/dossier_filter_service_spec.rb
Normal file
|
@ -0,0 +1,709 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe DossierFilterService do
|
||||||
|
def to_filter((label, filter)) = FilteredColumn.new(column: procedure.find_column(label:), filter:)
|
||||||
|
|
||||||
|
describe '.filtered_sorted_ids' do
|
||||||
|
let(:procedure) { create(:procedure) }
|
||||||
|
let(:instructeur) { create(:instructeur) }
|
||||||
|
let(:dossiers) { procedure.dossiers }
|
||||||
|
let(:statut) { 'suivis' }
|
||||||
|
let(:filters) { [] }
|
||||||
|
let(:sorted_columns) { procedure.default_sorted_column }
|
||||||
|
|
||||||
|
subject { described_class.filtered_sorted_ids(dossiers, statut, filters, sorted_columns, instructeur) }
|
||||||
|
|
||||||
|
context 'with no filters' do
|
||||||
|
let(:en_construction_dossier) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
let(:accepte_dossier) { create(:dossier, :accepte, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:follow, dossier: en_construction_dossier, instructeur:)
|
||||||
|
create(:follow, dossier: accepte_dossier, instructeur:)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(en_construction_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with mocked sorted_ids' do
|
||||||
|
let(:dossier_1) { create(:dossier) }
|
||||||
|
let(:dossier_2) { create(:dossier) }
|
||||||
|
let(:dossier_3) { create(:dossier) }
|
||||||
|
let(:dossiers) { Dossier.where(id: [dossier_1, dossier_2, dossier_3].map(&:id)) }
|
||||||
|
|
||||||
|
let(:sorted_ids) { [dossier_2, dossier_3, dossier_1].map(&:id) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
expect(described_class).to receive(:sorted_ids).and_return(sorted_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to eq(sorted_ids) }
|
||||||
|
|
||||||
|
context 'when a filter is present' do
|
||||||
|
let(:filtered_ids) { [dossier_1, dossier_2, dossier_3].map(&:id) }
|
||||||
|
let(:filters) { [to_filter(['Statut', 'en_construction'])] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
expect(described_class).to receive(:filtered_ids).and_return(filtered_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to eq(sorted_ids) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#sorted_ids' do
|
||||||
|
let(:procedure) { create(:procedure, :published, types_de_champ_public:, types_de_champ_private: [{}]) }
|
||||||
|
let(:types_de_champ_public) { [{}] }
|
||||||
|
let(:first_type_de_champ) { assign_to.procedure.active_revision.types_de_champ_public.first }
|
||||||
|
let(:dossiers) { procedure.dossiers }
|
||||||
|
let(:instructeur) { create(:instructeur) }
|
||||||
|
let(:assign_to) { create(:assign_to, procedure:, instructeur:) }
|
||||||
|
let(:sorted_column) { SortedColumn.new(column:, order:) }
|
||||||
|
|
||||||
|
subject { described_class.send(:sorted_ids, dossiers, sorted_column, instructeur, dossiers.count) }
|
||||||
|
|
||||||
|
context 'for notifications table' do
|
||||||
|
let(:column) { procedure.notifications_column }
|
||||||
|
|
||||||
|
let!(:notified_dossier) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
let!(:recent_dossier) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
let!(:older_dossier) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
notified_dossier.update!(last_champ_updated_at: Time.zone.local(2018, 9, 20))
|
||||||
|
create(:follow, instructeur: instructeur, dossier: notified_dossier, demande_seen_at: Time.zone.local(2018, 9, 10))
|
||||||
|
notified_dossier.touch(time: Time.zone.local(2018, 9, 20))
|
||||||
|
recent_dossier.touch(time: Time.zone.local(2018, 9, 25))
|
||||||
|
older_dossier.touch(time: Time.zone.local(2018, 5, 13))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'in ascending order' do
|
||||||
|
let(:order) { 'asc' }
|
||||||
|
|
||||||
|
it { is_expected.to eq([older_dossier, recent_dossier, notified_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'in descending order' do
|
||||||
|
let(:order) { 'desc' }
|
||||||
|
|
||||||
|
it { is_expected.to eq([notified_dossier, recent_dossier, older_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a dossier terminé' do
|
||||||
|
let!(:notified_dossier) { create(:dossier, :accepte, procedure:) }
|
||||||
|
let(:order) { 'desc' }
|
||||||
|
|
||||||
|
it { is_expected.to eq([notified_dossier, recent_dossier, older_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for self table' do
|
||||||
|
let(:order) { 'asc' } # Desc works the same, no extra test required
|
||||||
|
|
||||||
|
context 'for created_at column' do
|
||||||
|
let!(:column) { procedure.find_column(label: 'Créé le') }
|
||||||
|
let!(:recent_dossier) { Timecop.freeze(Time.zone.local(2018, 10, 17)) { create(:dossier, procedure:) } }
|
||||||
|
let!(:older_dossier) { Timecop.freeze(Time.zone.local(2003, 11, 11)) { create(:dossier, procedure:) } }
|
||||||
|
|
||||||
|
it { is_expected.to eq([older_dossier, recent_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for en_construction_at column' do
|
||||||
|
let!(:column) { procedure.find_column(label: 'En construction le') }
|
||||||
|
let!(:recent_dossier) { create(:dossier, :en_construction, procedure:, en_construction_at: Time.zone.local(2018, 10, 17)) }
|
||||||
|
let!(:older_dossier) { create(:dossier, :en_construction, procedure:, en_construction_at: Time.zone.local(2013, 1, 1)) }
|
||||||
|
|
||||||
|
it { is_expected.to eq([older_dossier, recent_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for updated_at column' do
|
||||||
|
let(:column) { procedure.find_column(label: 'Mis à jour le') }
|
||||||
|
let(:recent_dossier) { create(:dossier, procedure:) }
|
||||||
|
let(:older_dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
recent_dossier.touch(time: Time.zone.local(2018, 9, 25))
|
||||||
|
older_dossier.touch(time: Time.zone.local(2018, 5, 13))
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to eq([older_dossier, recent_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for type_de_champ table' do
|
||||||
|
context 'with no revisions' do
|
||||||
|
let(:column) { procedure.find_column(label: first_type_de_champ.libelle) }
|
||||||
|
|
||||||
|
let(:beurre_dossier) { create(:dossier, procedure:) }
|
||||||
|
let(:tartine_dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
beurre_dossier.project_champs_public.first.update(value: 'beurre')
|
||||||
|
tartine_dossier.project_champs_public.first.update(value: 'tartine')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'asc' do
|
||||||
|
let(:order) { 'asc' }
|
||||||
|
|
||||||
|
it { is_expected.to eq([beurre_dossier, tartine_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'desc' do
|
||||||
|
let(:order) { 'desc' }
|
||||||
|
|
||||||
|
it { is_expected.to eq([tartine_dossier, beurre_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a revision adding a new type_de_champ' do
|
||||||
|
let!(:tdc) { { type_champ: :text, libelle: 'nouveau champ' } }
|
||||||
|
let(:column) { procedure.find_column(label: 'nouveau champ') }
|
||||||
|
|
||||||
|
let!(:nothing_dossier) { create(:dossier, procedure:) }
|
||||||
|
let!(:beurre_dossier) { create(:dossier, procedure:) }
|
||||||
|
let!(:tartine_dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
nothing_dossier
|
||||||
|
procedure.draft_revision.add_type_de_champ(tdc)
|
||||||
|
procedure.publish_revision!
|
||||||
|
beurre_dossier.project_champs_public.last.update(value: 'beurre')
|
||||||
|
tartine_dossier.project_champs_public.last.update(value: 'tartine')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'asc' do
|
||||||
|
let(:order) { 'asc' }
|
||||||
|
it { is_expected.to eq([nothing_dossier, beurre_dossier, tartine_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'desc' do
|
||||||
|
let(:order) { 'desc' }
|
||||||
|
it { is_expected.to eq([tartine_dossier, beurre_dossier, nothing_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for type_de_champ_private table' do
|
||||||
|
context 'with no revisions' do
|
||||||
|
let(:column) { procedure.find_column(label: procedure.active_revision.types_de_champ_private.first.libelle) }
|
||||||
|
|
||||||
|
let(:biere_dossier) { create(:dossier, procedure:) }
|
||||||
|
let(:vin_dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
biere_dossier.project_champs_private.first.update(value: 'biere')
|
||||||
|
vin_dossier.project_champs_private.first.update(value: 'vin')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'asc' do
|
||||||
|
let(:order) { 'asc' }
|
||||||
|
|
||||||
|
it { is_expected.to eq([biere_dossier, vin_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'desc' do
|
||||||
|
let(:order) { 'desc' }
|
||||||
|
|
||||||
|
it { is_expected.to eq([vin_dossier, biere_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for individual table' do
|
||||||
|
let(:order) { 'asc' } # Desc works the same, no extra test required
|
||||||
|
|
||||||
|
let(:procedure) { create(:procedure, :for_individual) }
|
||||||
|
|
||||||
|
let!(:first_dossier) { create(:dossier, procedure:, individual: build(:individual, gender: 'M', prenom: 'Alain', nom: 'Antonelli')) }
|
||||||
|
let!(:last_dossier) { create(:dossier, procedure:, individual: build(:individual, gender: 'Mme', prenom: 'Zora', nom: 'Zemmour')) }
|
||||||
|
|
||||||
|
context 'for gender column' do
|
||||||
|
let(:column) { procedure.find_column(label: 'Civilité') }
|
||||||
|
|
||||||
|
it { is_expected.to eq([first_dossier, last_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for prenom column' do
|
||||||
|
let(:column) { procedure.find_column(label: 'Prénom') }
|
||||||
|
|
||||||
|
it { is_expected.to eq([first_dossier, last_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for nom column' do
|
||||||
|
let(:column) { procedure.find_column(label: 'Nom') }
|
||||||
|
|
||||||
|
it { is_expected.to eq([first_dossier, last_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for followers_instructeurs table' do
|
||||||
|
let(:order) { 'asc' } # Desc works the same, no extra test required
|
||||||
|
|
||||||
|
let!(:dossier_z) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
let!(:dossier_a) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
let!(:dossier_without_instructeur) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:follow, dossier: dossier_z, instructeur: create(:instructeur, email: 'zythum@exemple.fr'))
|
||||||
|
create(:follow, dossier: dossier_a, instructeur: create(:instructeur, email: 'abaca@exemple.fr'))
|
||||||
|
create(:follow, dossier: dossier_a, instructeur: create(:instructeur, email: 'abaca2@exemple.fr'))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for email column' do
|
||||||
|
let(:column) { procedure.find_column(label: 'Email instructeur') }
|
||||||
|
|
||||||
|
it { is_expected.to eq([dossier_a, dossier_z, dossier_without_instructeur].map(&:id)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for avis table' do
|
||||||
|
let(:column) { procedure.find_column(label: 'Avis oui/non') }
|
||||||
|
let(:order) { 'asc' }
|
||||||
|
|
||||||
|
let!(:dossier_yes) { create(:dossier, procedure:) }
|
||||||
|
let!(:dossier_no) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create_list(:avis, 2, dossier: dossier_yes, question_answer: true)
|
||||||
|
create(:avis, dossier: dossier_no, question_answer: true)
|
||||||
|
create(:avis, dossier: dossier_no, question_answer: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to eq([dossier_no, dossier_yes].map(&:id)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for other tables' do
|
||||||
|
# All other columns and tables work the same so it’s ok to test only one
|
||||||
|
let(:column) { procedure.find_column(label: 'Code postal') }
|
||||||
|
let(:order) { 'asc' } # Desc works the same, no extra test required
|
||||||
|
|
||||||
|
let!(:huitieme_dossier) { create(:dossier, procedure:, etablissement: create(:etablissement, code_postal: '75008')) }
|
||||||
|
let!(:vingtieme_dossier) { create(:dossier, procedure:, etablissement: create(:etablissement, code_postal: '75020')) }
|
||||||
|
|
||||||
|
it { is_expected.to eq([huitieme_dossier, vingtieme_dossier].map(&:id)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#filtered_ids' do
|
||||||
|
let(:procedure) { create(:procedure, types_de_champ_public:, types_de_champ_private:) }
|
||||||
|
let(:types_de_champ_public) { [{}] }
|
||||||
|
let(:types_de_champ_private) { [{}] }
|
||||||
|
let(:dossiers) { procedure.dossiers }
|
||||||
|
let(:filtered_columns) { filters.map { to_filter(_1) } }
|
||||||
|
let(:filters) { [filter] }
|
||||||
|
|
||||||
|
subject { described_class.send(:filtered_ids, dossiers.joins(:user), filtered_columns) }
|
||||||
|
|
||||||
|
context 'for self table' do
|
||||||
|
context 'for created_at column' do
|
||||||
|
let(:filter) { ['Créé le', '18/9/2018'] }
|
||||||
|
|
||||||
|
let!(:kept_dossier) { create(:dossier, procedure:, created_at: Time.zone.local(2018, 9, 18, 14, 28)) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, procedure:, created_at: Time.zone.local(2018, 9, 17, 23, 59)) }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for en_construction_at column' do
|
||||||
|
let(:filter) { ['En construction le', '17/10/2018'] }
|
||||||
|
|
||||||
|
let!(:kept_dossier) { create(:dossier, :en_construction, procedure:, en_construction_at: Time.zone.local(2018, 10, 17)) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, :en_construction, procedure:, en_construction_at: Time.zone.local(2013, 1, 1)) }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for updated_at column' do
|
||||||
|
let(:filter) { ['Mis à jour le', '18/9/2018'] }
|
||||||
|
|
||||||
|
let(:kept_dossier) { create(:dossier, procedure:) }
|
||||||
|
let(:discarded_dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
kept_dossier.touch(time: Time.zone.local(2018, 9, 18, 14, 28))
|
||||||
|
discarded_dossier.touch(time: Time.zone.local(2018, 9, 17, 23, 59))
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for updated_since column' do
|
||||||
|
let(:filter) { ['Mis à jour depuis', '18/9/2018'] }
|
||||||
|
|
||||||
|
let(:kept_dossier) { create(:dossier, procedure:) }
|
||||||
|
let(:later_dossier) { create(:dossier, procedure:) }
|
||||||
|
let(:discarded_dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
kept_dossier.touch(time: Time.zone.local(2018, 9, 18, 14, 28))
|
||||||
|
later_dossier.touch(time: Time.zone.local(2018, 9, 19, 14, 28))
|
||||||
|
discarded_dossier.touch(time: Time.zone.local(2018, 9, 17, 14, 28))
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to match_array([kept_dossier.id, later_dossier.id]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for sva_svr_decision_before column' do
|
||||||
|
before do
|
||||||
|
travel_to Time.zone.local(2023, 6, 10, 10)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:procedure) { create(:procedure, :published, :sva, types_de_champ_public: [{}], types_de_champ_private: [{}]) }
|
||||||
|
let(:filter) { ['Date décision SVA avant', '15/06/2023'] }
|
||||||
|
|
||||||
|
let!(:kept_dossier) { create(:dossier, :en_instruction, procedure:, sva_svr_decision_on: Date.current) }
|
||||||
|
let!(:later_dossier) { create(:dossier, :en_instruction, procedure:, sva_svr_decision_on: Date.current + 2.days) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, :en_instruction, procedure:, sva_svr_decision_on: Date.current + 10.days) }
|
||||||
|
let!(:en_construction_dossier) { create(:dossier, :en_construction, procedure:, sva_svr_decision_on: Date.current + 2.days) }
|
||||||
|
let!(:accepte_dossier) { create(:dossier, :accepte, procedure:, sva_svr_decision_on: Date.current + 2.days) }
|
||||||
|
|
||||||
|
it { is_expected.to match_array([kept_dossier.id, later_dossier.id, en_construction_dossier.id]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'ignore time of day' do
|
||||||
|
let(:filter) { ['En construction le', '17/10/2018 19:30'] }
|
||||||
|
|
||||||
|
let!(:kept_dossier) { create(:dossier, :en_construction, procedure:, en_construction_at: Time.zone.local(2018, 10, 17, 15, 56)) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, :en_construction, procedure:, en_construction_at: Time.zone.local(2018, 10, 18, 5, 42)) }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for a malformed date' do
|
||||||
|
context 'when its a string' do
|
||||||
|
let(:filter) { ['Mis à jour le', 'malformed date'] }
|
||||||
|
|
||||||
|
it { is_expected.to match([]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when its a number' do
|
||||||
|
let(:filter) { ['Mis à jour le', '177500'] }
|
||||||
|
|
||||||
|
it { is_expected.to match([]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiple search values' do
|
||||||
|
let(:filters) { [['En construction le', '17/10/2018'], ['En construction le', '19/10/2018']] }
|
||||||
|
|
||||||
|
let!(:kept_dossier) { create(:dossier, :en_construction, procedure:, en_construction_at: Time.zone.local(2018, 10, 17)) }
|
||||||
|
let!(:other_kept_dossier) { create(:dossier, :en_construction, procedure:, en_construction_at: Time.zone.local(2018, 10, 19)) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, :en_construction, procedure:, en_construction_at: Time.zone.local(2013, 1, 1)) }
|
||||||
|
|
||||||
|
it 'returns every dossier that matches any of the search criteria for a given column' do
|
||||||
|
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiple state filters' do
|
||||||
|
let(:filters) { [['Statut', 'en_construction'], ['Statut', 'en_instruction']] }
|
||||||
|
|
||||||
|
let!(:kept_dossier) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
let!(:other_kept_dossier) { create(:dossier, :en_instruction, procedure:) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, :accepte, procedure:) }
|
||||||
|
|
||||||
|
it 'returns every dossier that matches any of the search criteria for a given column' do
|
||||||
|
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with en_construction state filters' do
|
||||||
|
let(:filter) { ['Statut', 'en_construction'] }
|
||||||
|
|
||||||
|
let!(:en_construction) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
let!(:en_construction_with_correction) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
let!(:correction) { create(:dossier_correction, dossier: en_construction_with_correction) }
|
||||||
|
it 'excludes dossier en construction with pending correction' do
|
||||||
|
is_expected.to contain_exactly(en_construction.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for type_de_champ table' do
|
||||||
|
let(:filter) { [type_de_champ.libelle, 'keep'] }
|
||||||
|
|
||||||
|
let(:kept_dossier) { create(:dossier, procedure:) }
|
||||||
|
let(:discarded_dossier) { create(:dossier, procedure:) }
|
||||||
|
let(:type_de_champ) { procedure.active_revision.types_de_champ_public.first }
|
||||||
|
|
||||||
|
context 'with single value' do
|
||||||
|
before do
|
||||||
|
kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'keep me')
|
||||||
|
discarded_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'discard me')
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiple search values' do
|
||||||
|
let(:filters) { [[type_de_champ.libelle, 'keep'], [type_de_champ.libelle, 'and']] }
|
||||||
|
let(:other_kept_dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'keep me')
|
||||||
|
discarded_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'discard me')
|
||||||
|
other_kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'and me too')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns every dossier that matches any of the search criteria for a given column' do
|
||||||
|
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with yes_no type_de_champ' do
|
||||||
|
let(:filter) { [type_de_champ.libelle, 'true'] }
|
||||||
|
let(:types_de_champ_public) { [{ type: :yes_no }] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'true')
|
||||||
|
discarded_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'false')
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with departement type_de_champ' do
|
||||||
|
let(:filter) { [type_de_champ.libelle, '13'] }
|
||||||
|
let(:types_de_champ_public) { [{ type: :departements }] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(external_id: '13')
|
||||||
|
discarded_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(external_id: '69')
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with enum type_de_champ' do
|
||||||
|
let(:filter) { [type_de_champ.libelle, 'Favorable'] }
|
||||||
|
let(:types_de_champ_public) { [{ type: :drop_down_list, options: ['Favorable', 'Defavorable'] }] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
kept_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(value: 'Favorable')
|
||||||
|
discarded_dossier.champs.find_by(stable_id: type_de_champ.stable_id).update(external_id: 'Defavorable')
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for type_de_champ_private table' do
|
||||||
|
let(:filter) { [type_de_champ_private.libelle, 'keep'] }
|
||||||
|
|
||||||
|
let(:kept_dossier) { create(:dossier, procedure:) }
|
||||||
|
let(:discarded_dossier) { create(:dossier, procedure:) }
|
||||||
|
let(:type_de_champ_private) { procedure.active_revision.types_de_champ_private.first }
|
||||||
|
|
||||||
|
before do
|
||||||
|
kept_dossier.champs.find_by(stable_id: type_de_champ_private.stable_id).update(value: 'keep me')
|
||||||
|
discarded_dossier.champs.find_by(stable_id: type_de_champ_private.stable_id).update(value: 'discard me')
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for type_de_champ using AddressableColumnConcern' do
|
||||||
|
let(:column) { filtered_columns.first.column }
|
||||||
|
let(:types_de_champ_public) { [{ type: :rna, stable_id: 1, libelle: 'rna' }] }
|
||||||
|
let(:type_de_champ) { procedure.active_revision.types_de_champ.first }
|
||||||
|
let(:kept_dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
|
context "when searching by postal_code (text)" do
|
||||||
|
let(:value) { "60580" }
|
||||||
|
let(:filter) { ["rna – code postal (5 chiffres)", value] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
kept_dossier.project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "postal_code" => value })
|
||||||
|
create(:dossier, procedure:).project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "postal_code" => "unknown" })
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
|
||||||
|
it 'describes column' do
|
||||||
|
expect(column.type).to eq(:text)
|
||||||
|
expect(column.options_for_select).to eq([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when searching by departement_code (enum)" do
|
||||||
|
let(:value) { "99" }
|
||||||
|
let(:filter) { ["rna – département", value] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
kept_dossier.project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "departement_code" => value })
|
||||||
|
create(:dossier, procedure:).project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "departement_code" => "unknown" })
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
|
||||||
|
it 'describes column' do
|
||||||
|
expect(column.type).to eq(:enum)
|
||||||
|
expect(column.options_for_select.first).to eq(["99 – Etranger", "99"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when searching by region_name" do
|
||||||
|
let(:value) { "60" }
|
||||||
|
let(:filter) { ["rna – region", value] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
kept_dossier.project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "region_name" => value })
|
||||||
|
create(:dossier, procedure:).project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "region_name" => "unknown" })
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
|
||||||
|
it 'describes column' do
|
||||||
|
expect(column.type).to eq(:enum)
|
||||||
|
expect(column.options_for_select.first).to eq(["Auvergne-Rhône-Alpes", "Auvergne-Rhône-Alpes"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for etablissement table' do
|
||||||
|
context 'for entreprise_date_creation column' do
|
||||||
|
let(:filter) { ['Date de création', '21/6/2018'] }
|
||||||
|
|
||||||
|
let!(:kept_dossier) { create(:dossier, procedure:, etablissement: create(:etablissement, entreprise_date_creation: Time.zone.local(2018, 6, 21))) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, procedure:, etablissement: create(:etablissement, entreprise_date_creation: Time.zone.local(2008, 6, 21))) }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
|
||||||
|
context 'with multiple search values' do
|
||||||
|
let(:filters) { [['Date de création', '21/6/2016'], ['Date de création', '21/6/2018']] }
|
||||||
|
|
||||||
|
let!(:other_kept_dossier) { create(:dossier, procedure:, etablissement: create(:etablissement, entreprise_date_creation: Time.zone.local(2016, 6, 21))) }
|
||||||
|
|
||||||
|
it 'returns every dossier that matches any of the search criteria for a given column' do
|
||||||
|
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for code_postal column' do
|
||||||
|
# All columns except entreprise_date_creation work exacly the same, just testing one
|
||||||
|
|
||||||
|
let(:filter) { ['Code postal', '75017'] }
|
||||||
|
|
||||||
|
let!(:kept_dossier) { create(:dossier, procedure:, etablissement: create(:etablissement, code_postal: '75017')) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, procedure:, etablissement: create(:etablissement, code_postal: '25000')) }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
|
||||||
|
context 'with multiple search values' do
|
||||||
|
let(:filters) { [['Code postal', '75017'], ['Code postal', '88100']] }
|
||||||
|
|
||||||
|
let!(:other_kept_dossier) { create(:dossier, procedure:, etablissement: create(:etablissement, code_postal: '88100')) }
|
||||||
|
|
||||||
|
it 'returns every dossier that matches any of the search criteria for a given column' do
|
||||||
|
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for user table' do
|
||||||
|
let(:filter) { ['Demandeur', 'keepmail'] }
|
||||||
|
|
||||||
|
let!(:kept_dossier) { create(:dossier, procedure:, user: create(:user, email: 'me@keepmail.com')) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, procedure:, user: create(:user, email: 'me@discard.com')) }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
|
||||||
|
context 'with multiple search values' do
|
||||||
|
let(:filters) { [['Demandeur', 'keepmail'], ['Demandeur', 'beta.gouv.fr']] }
|
||||||
|
|
||||||
|
let!(:other_kept_dossier) { create(:dossier, procedure:, user: create(:user, email: 'bazinga@beta.gouv.fr')) }
|
||||||
|
|
||||||
|
it 'returns every dossier that matches any of the search criteria for a given column' do
|
||||||
|
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for individual table' do
|
||||||
|
let(:procedure) { create(:procedure, :for_individual) }
|
||||||
|
let!(:kept_dossier) { create(:dossier, procedure:, individual: build(:individual, gender: 'Mme', prenom: 'Josephine', nom: 'Baker')) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, procedure:, individual: build(:individual, gender: 'M', prenom: 'Jean', nom: 'Tremblay')) }
|
||||||
|
|
||||||
|
context 'for gender column' do
|
||||||
|
let(:filter) { ['Civilité', 'Mme'] }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for prenom column' do
|
||||||
|
let(:filter) { ['Prénom', 'Josephine'] }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for nom column' do
|
||||||
|
let(:filter) { ['Nom', 'Baker'] }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiple search values' do
|
||||||
|
let(:filters) { [['Prénom', 'Josephine'], ['Prénom', 'Romuald']] }
|
||||||
|
|
||||||
|
let!(:other_kept_dossier) { create(:dossier, procedure:, individual: build(:individual, gender: 'M', prenom: 'Romuald', nom: 'Pistis')) }
|
||||||
|
|
||||||
|
it 'returns every dossier that matches any of the search criteria for a given column' do
|
||||||
|
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for followers_instructeurs table' do
|
||||||
|
let(:filter) { ['Email instructeur', 'keepmail'] }
|
||||||
|
|
||||||
|
let!(:kept_dossier) { create(:dossier, procedure:) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:follow, dossier: kept_dossier, instructeur: create(:instructeur, email: 'me@keepmail.com'))
|
||||||
|
create(:follow, dossier: discarded_dossier, instructeur: create(:instructeur, email: 'me@discard.com'))
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
|
||||||
|
context 'with multiple search values' do
|
||||||
|
let(:filters) { [['Email instructeur', 'keepmail'], ['Email instructeur', 'beta.gouv.fr']] }
|
||||||
|
|
||||||
|
let(:other_kept_dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:follow, dossier: other_kept_dossier, instructeur: create(:instructeur, email: 'bazinga@beta.gouv.fr'))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns every dossier that matches any of the search criteria for a given column' do
|
||||||
|
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for groupe_instructeur table' do
|
||||||
|
let(:filter) { ['Groupe instructeur', procedure.defaut_groupe_instructeur.id.to_s] }
|
||||||
|
|
||||||
|
let!(:gi_2) { create(:groupe_instructeur, label: 'gi2', procedure:) }
|
||||||
|
let!(:gi_3) { create(:groupe_instructeur, label: 'gi3', procedure:) }
|
||||||
|
|
||||||
|
let!(:kept_dossier) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
let!(:discarded_dossier) { create(:dossier, :en_construction, procedure:, groupe_instructeur: gi_2) }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
|
|
||||||
|
context 'with multiple search values' do
|
||||||
|
let(:filters) { [['Groupe instructeur', procedure.defaut_groupe_instructeur.id.to_s], ['Groupe instructeur', gi_3.id.to_s]] }
|
||||||
|
|
||||||
|
let!(:other_kept_dossier) { create(:dossier, procedure:, groupe_instructeur: gi_3) }
|
||||||
|
|
||||||
|
it 'returns every dossier that matches any of the search criteria for a given column' do
|
||||||
|
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
describe DossierProjectionService do
|
describe DossierProjectionService do
|
||||||
describe '#project' do
|
describe '#project' do
|
||||||
subject { described_class.project(dossiers_ids, fields) }
|
subject { described_class.project(dossiers_ids, columns) }
|
||||||
|
|
||||||
context 'with multiple dossier' do
|
context 'with multiple dossier' do
|
||||||
let!(:procedure) { create(:procedure, types_de_champ_public: [{}, { type: :linked_drop_down_list }]) }
|
let!(:procedure) { create(:procedure, types_de_champ_public: [{}, { type: :linked_drop_down_list }]) }
|
||||||
|
@ -11,12 +11,9 @@ describe DossierProjectionService do
|
||||||
let!(:dossier_3) { create(:dossier, :en_instruction, procedure: procedure) }
|
let!(:dossier_3) { create(:dossier, :en_instruction, procedure: procedure) }
|
||||||
|
|
||||||
let(:dossiers_ids) { [dossier_3.id, dossier_1.id, dossier_2.id] }
|
let(:dossiers_ids) { [dossier_3.id, dossier_1.id, dossier_2.id] }
|
||||||
let(:fields) do
|
let(:columns) do
|
||||||
procedure.active_revision.types_de_champ_public.map do |type_de_champ|
|
procedure.active_revision.types_de_champ_public.map do |type_de_champ|
|
||||||
{
|
procedure.find_column(label: type_de_champ.libelle)
|
||||||
"table" => "type_de_champ",
|
|
||||||
"column" => type_de_champ.stable_id.to_s
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -55,12 +52,9 @@ describe DossierProjectionService do
|
||||||
let!(:dossier) { create(:dossier, procedure:) }
|
let!(:dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
let(:dossiers_ids) { [dossier.id] }
|
let(:dossiers_ids) { [dossier.id] }
|
||||||
let(:fields) do
|
let(:columns) do
|
||||||
[
|
[
|
||||||
{
|
procedure.find_column(label: procedure.active_revision.types_de_champ_public[0].libelle)
|
||||||
"table" => "type_de_champ",
|
|
||||||
"column" => procedure.active_revision.types_de_champ_public[0].stable_id.to_s
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -78,38 +72,37 @@ describe DossierProjectionService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'attributes by attributes' do
|
context 'attributes by attributes' do
|
||||||
let(:fields) { [{ "table" => table, "column" => column }] }
|
let(:procedure) { create(:procedure) }
|
||||||
|
let(:columns) { [procedure.find_column(label:)] }
|
||||||
let(:dossiers_ids) { [dossier.id] }
|
let(:dossiers_ids) { [dossier.id] }
|
||||||
|
|
||||||
subject { super()[0].columns[0] }
|
subject { super()[0].columns[0] }
|
||||||
|
|
||||||
context 'for self table' do
|
context 'for self table' do
|
||||||
let(:table) { 'self' }
|
|
||||||
|
|
||||||
context 'for created_at column' do
|
context 'for created_at column' do
|
||||||
let(:column) { 'created_at' }
|
let(:label) { 'Créé le' }
|
||||||
let(:dossier) { Timecop.freeze(Time.zone.local(1992, 3, 22)) { create(:dossier) } }
|
let(:dossier) { Timecop.freeze(Time.zone.local(1992, 3, 22)) { create(:dossier, procedure:) } }
|
||||||
|
|
||||||
it { is_expected.to eq('22/03/1992') }
|
it { is_expected.to eq('22/03/1992') }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for en_construction_at column' do
|
context 'for en_construction_at column' do
|
||||||
let(:column) { 'en_construction_at' }
|
let(:label) { 'En construction le' }
|
||||||
let(:dossier) { create(:dossier, :en_construction, en_construction_at: Time.zone.local(2018, 10, 17)) }
|
let(:dossier) { create(:dossier, :en_construction, en_construction_at: Time.zone.local(2018, 10, 17), procedure:) }
|
||||||
|
|
||||||
it { is_expected.to eq('17/10/2018') }
|
it { is_expected.to eq('17/10/2018') }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for depose_at column' do
|
context 'for depose_at column' do
|
||||||
let(:column) { 'depose_at' }
|
let(:label) { 'Déposé le' }
|
||||||
let(:dossier) { create(:dossier, :en_construction, depose_at: Time.zone.local(2018, 10, 17)) }
|
let(:dossier) { create(:dossier, :en_construction, depose_at: Time.zone.local(2018, 10, 17), procedure:) }
|
||||||
|
|
||||||
it { is_expected.to eq('17/10/2018') }
|
it { is_expected.to eq('17/10/2018') }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for updated_at column' do
|
context 'for updated_at column' do
|
||||||
let(:column) { 'updated_at' }
|
let(:label) { 'Mis à jour le' }
|
||||||
let(:dossier) { create(:dossier) }
|
let(:dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
before { dossier.touch(time: Time.zone.local(2018, 9, 25)) }
|
before { dossier.touch(time: Time.zone.local(2018, 9, 25)) }
|
||||||
|
|
||||||
|
@ -118,61 +111,56 @@ describe DossierProjectionService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for user table' do
|
context 'for user table' do
|
||||||
let(:table) { 'user' }
|
let(:label) { 'Demandeur' }
|
||||||
let(:column) { 'email' }
|
|
||||||
|
|
||||||
let(:dossier) { create(:dossier, user: create(:user, email: 'bla@yopmail.com')) }
|
let(:dossier) { create(:dossier, user: create(:user, email: 'bla@yopmail.com'), procedure:) }
|
||||||
|
|
||||||
it { is_expected.to eq('bla@yopmail.com') }
|
it { is_expected.to eq('bla@yopmail.com') }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for individual table' do
|
context 'for individual table' do
|
||||||
let(:table) { 'individual' }
|
|
||||||
let(:procedure) { create(:procedure, :for_individual, :with_type_de_champ, :with_type_de_champ_private) }
|
let(:procedure) { create(:procedure, :for_individual, :with_type_de_champ, :with_type_de_champ_private) }
|
||||||
let(:dossier) { create(:dossier, procedure: procedure, individual: build(:individual, nom: 'Martin', prenom: 'Jacques', gender: 'M.')) }
|
let(:dossier) { create(:dossier, procedure:, individual: build(:individual, nom: 'Martin', prenom: 'Jacques', gender: 'M.')) }
|
||||||
|
|
||||||
context 'for prenom column' do
|
context 'for prenom column' do
|
||||||
let(:column) { 'prenom' }
|
let(:label) { 'Prénom' }
|
||||||
|
|
||||||
it { is_expected.to eq('Jacques') }
|
it { is_expected.to eq('Jacques') }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for nom column' do
|
context 'for nom column' do
|
||||||
let(:column) { 'nom' }
|
let(:label) { 'Nom' }
|
||||||
|
|
||||||
it { is_expected.to eq('Martin') }
|
it { is_expected.to eq('Martin') }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for gender column' do
|
context 'for gender column' do
|
||||||
let(:column) { 'gender' }
|
let(:label) { 'Civilité' }
|
||||||
|
|
||||||
it { is_expected.to eq('M.') }
|
it { is_expected.to eq('M.') }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for etablissement table' do
|
context 'for etablissement table' do
|
||||||
let(:table) { 'etablissement' }
|
let(:label) { 'Code postal' }
|
||||||
let(:column) { 'code_postal' } # All other columns work the same, no extra test required
|
|
||||||
|
|
||||||
let!(:dossier) { create(:dossier, etablissement: create(:etablissement, code_postal: '75008')) }
|
let!(:dossier) { create(:dossier, procedure:, etablissement: create(:etablissement, code_postal: '75008')) }
|
||||||
|
|
||||||
it { is_expected.to eq('75008') }
|
it { is_expected.to eq('75008') }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for groupe_instructeur table' do
|
context 'for groupe_instructeur table' do
|
||||||
let(:table) { 'groupe_instructeur' }
|
let(:label) { 'Groupe instructeur' }
|
||||||
let(:column) { 'label' }
|
|
||||||
|
|
||||||
let!(:dossier) { create(:dossier) }
|
let!(:dossier) { create(:dossier, procedure:) }
|
||||||
|
|
||||||
it { is_expected.to eq('défaut') }
|
it { is_expected.to eq('défaut') }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for followers_instructeurs table' do
|
context 'for followers_instructeurs table' do
|
||||||
let(:table) { 'followers_instructeurs' }
|
let(:label) { 'Email instructeur' }
|
||||||
let(:column) { 'email' }
|
|
||||||
|
|
||||||
let(:dossier) { create(:dossier) }
|
let(:dossier) { create(:dossier, procedure:) }
|
||||||
let!(:follow1) { create(:follow, dossier: dossier, instructeur: create(:instructeur, email: 'b@host.fr')) }
|
let!(:follow1) { create(:follow, dossier: dossier, instructeur: create(:instructeur, email: 'b@host.fr')) }
|
||||||
let!(:follow2) { create(:follow, dossier: dossier, instructeur: create(:instructeur, email: 'a@host.fr')) }
|
let!(:follow2) { create(:follow, dossier: dossier, instructeur: create(:instructeur, email: 'a@host.fr')) }
|
||||||
let!(:follow3) { create(:follow, dossier: dossier, instructeur: create(:instructeur, email: 'c@host.fr')) }
|
let!(:follow3) { create(:follow, dossier: dossier, instructeur: create(:instructeur, email: 'c@host.fr')) }
|
||||||
|
@ -181,19 +169,21 @@ describe DossierProjectionService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for type_de_champ table' do
|
context 'for type_de_champ table' do
|
||||||
let(:table) { 'type_de_champ' }
|
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :text }]) }
|
||||||
let(:dossier) { create(:dossier) }
|
let(:dossier) { create(:dossier, procedure:) }
|
||||||
let(:column) { dossier.procedure.active_revision.types_de_champ_public.first.stable_id.to_s }
|
let(:label) { dossier.procedure.active_revision.types_de_champ_public.first.libelle }
|
||||||
|
|
||||||
before { dossier.project_champs_public.first.update(value: 'kale') }
|
before do
|
||||||
|
dossier.project_champs_public.first.update(value: 'kale')
|
||||||
|
end
|
||||||
|
|
||||||
it { is_expected.to eq('kale') }
|
it { is_expected.to eq('kale') }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for type_de_champ_private table' do
|
context 'for type_de_champ_private table' do
|
||||||
let(:table) { 'type_de_champ_private' }
|
let(:procedure) { create(:procedure, types_de_champ_private: [{ type: :text }]) }
|
||||||
let(:dossier) { create(:dossier) }
|
let(:dossier) { create(:dossier, procedure:) }
|
||||||
let(:column) { dossier.procedure.active_revision.types_de_champ_private.first.stable_id.to_s }
|
let(:label) { dossier.procedure.active_revision.types_de_champ_private.first.libelle }
|
||||||
|
|
||||||
before { dossier.project_champs_private.first.update(value: 'quinoa') }
|
before { dossier.project_champs_private.first.update(value: 'quinoa') }
|
||||||
|
|
||||||
|
@ -201,10 +191,9 @@ describe DossierProjectionService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for type_de_champ table and value to.s' do
|
context 'for type_de_champ table and value to.s' do
|
||||||
let(:table) { 'type_de_champ' }
|
|
||||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no }]) }
|
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no }]) }
|
||||||
let(:dossier) { create(:dossier, procedure: procedure) }
|
let(:dossier) { create(:dossier, procedure:) }
|
||||||
let(:column) { dossier.procedure.active_revision.types_de_champ_public.first.stable_id.to_s }
|
let(:label) { dossier.procedure.active_revision.types_de_champ_public.first.libelle }
|
||||||
|
|
||||||
before { dossier.project_champs_public.first.update(value: 'true') }
|
before { dossier.project_champs_public.first.update(value: 'true') }
|
||||||
|
|
||||||
|
@ -212,10 +201,9 @@ describe DossierProjectionService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for type_de_champ table and value to.s which needs data field' do
|
context 'for type_de_champ table and value to.s which needs data field' do
|
||||||
let(:table) { 'type_de_champ' }
|
|
||||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :address }]) }
|
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :address }]) }
|
||||||
let(:dossier) { create(:dossier, procedure: procedure) }
|
let(:dossier) { create(:dossier, procedure:) }
|
||||||
let(:column) { dossier.procedure.active_revision.types_de_champ_public.first.stable_id.to_s }
|
let(:label) { dossier.procedure.active_revision.types_de_champ_public.first.libelle }
|
||||||
|
|
||||||
before { dossier.project_champs_public.first.update(value: '18 a la bonne rue', data: { 'label' => '18 a la bonne rue', 'departement' => 'd' }) }
|
before { dossier.project_champs_public.first.update(value: '18 a la bonne rue', data: { 'label' => '18 a la bonne rue', 'departement' => 'd' }) }
|
||||||
|
|
||||||
|
@ -223,10 +211,9 @@ describe DossierProjectionService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for type_de_champ table: type_de_champ pays which needs external_id field' do
|
context 'for type_de_champ table: type_de_champ pays which needs external_id field' do
|
||||||
let(:table) { 'type_de_champ' }
|
|
||||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :pays }]) }
|
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :pays }]) }
|
||||||
let(:dossier) { create(:dossier, procedure: procedure) }
|
let(:dossier) { create(:dossier, procedure:) }
|
||||||
let(:column) { dossier.procedure.active_revision.types_de_champ_public.first.stable_id.to_s }
|
let(:label) { dossier.procedure.active_revision.types_de_champ_public.first.libelle }
|
||||||
|
|
||||||
around do |example|
|
around do |example|
|
||||||
I18n.with_locale(:fr) do
|
I18n.with_locale(:fr) do
|
||||||
|
@ -254,8 +241,10 @@ describe DossierProjectionService do
|
||||||
context 'for dossier corrections table' do
|
context 'for dossier corrections table' do
|
||||||
let(:table) { 'dossier_corrections' }
|
let(:table) { 'dossier_corrections' }
|
||||||
let(:column) { 'resolved_at' }
|
let(:column) { 'resolved_at' }
|
||||||
let(:dossier) { create(:dossier, :en_construction) }
|
let(:procedure) { create(:procedure) }
|
||||||
subject { described_class.project(dossiers_ids, fields)[0] }
|
let(:columns) { [Column.new(procedure_id: procedure.id, table:, column:)] } # should somehow be present in column concern
|
||||||
|
let(:dossier) { create(:dossier, :en_construction, procedure:) }
|
||||||
|
subject { described_class.project(dossiers_ids, columns)[0] }
|
||||||
|
|
||||||
context "when dossier has pending correction" do
|
context "when dossier has pending correction" do
|
||||||
before { create(:dossier_correction, dossier:) }
|
before { create(:dossier_correction, dossier:) }
|
||||||
|
|
|
@ -224,7 +224,7 @@ describe "procedure filters" do
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_filter(filter_value)
|
def remove_filter(filter_value)
|
||||||
click_link text: filter_value
|
click_button text: filter_value
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_column(column_name)
|
def add_column(column_name)
|
||||||
|
|
|
@ -14,14 +14,14 @@ module Maintenance
|
||||||
before { element.update_column(:filters, filters) }
|
before { element.update_column(:filters, filters) }
|
||||||
|
|
||||||
context 'when filter is valid' do
|
context 'when filter is valid' do
|
||||||
let(:filters) { { "suivis" => [{ 'table' => "self", 'column' => "id", "value" => (ProcedurePresentation::PG_INTEGER_MAX_VALUE - 1).to_s }] } }
|
let(:filters) { { "suivis" => [{ 'table' => "self", 'column' => "id", "value" => (FilteredColumn::PG_INTEGER_MAX_VALUE - 1).to_s }] } }
|
||||||
it 'keeps it filters' do
|
it 'keeps it filters' do
|
||||||
expect { subject }.not_to change { element.reload.filters }
|
expect { subject }.not_to change { element.reload.filters }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when filter is invalid, drop it' do
|
context 'when filter is invalid, drop it' do
|
||||||
let(:filters) { { "suivis" => [{ 'table' => "self", 'column' => "id", "value" => (ProcedurePresentation::PG_INTEGER_MAX_VALUE).to_s }] } }
|
let(:filters) { { "suivis" => [{ 'table' => "self", 'column' => "id", "value" => (FilteredColumn::PG_INTEGER_MAX_VALUE).to_s }] } }
|
||||||
it 'drop invalid filters' do
|
it 'drop invalid filters' do
|
||||||
expect { subject }.to change { element.reload.filters }.to({ "suivis" => [] })
|
expect { subject }.to change { element.reload.filters }.to({ "suivis" => [] })
|
||||||
end
|
end
|
||||||
|
|
65
spec/types/filtered_column_type_spec.rb
Normal file
65
spec/types/filtered_column_type_spec.rb
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe FilteredColumnType do
|
||||||
|
let(:type) { FilteredColumnType.new }
|
||||||
|
|
||||||
|
describe 'cast' do
|
||||||
|
it 'from FilteredColumn' do
|
||||||
|
column = Column.new(procedure_id: 1, table: 'table', column: 'column')
|
||||||
|
filtered_column = FilteredColumn.new(column:, filter: 'filter')
|
||||||
|
expect(type.cast(filtered_column)).to eq(filtered_column)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'from nil' do
|
||||||
|
expect(type.cast(nil)).to eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'from form' do
|
||||||
|
it 'with valid column id' do
|
||||||
|
column = Column.new(procedure_id: 1, table: 'table', column: 'column')
|
||||||
|
h = { filter: 'filter', id: column.id }
|
||||||
|
|
||||||
|
expect(Column).to receive(:find).with(column.h_id).and_return(column)
|
||||||
|
expect(type.cast(h)).to eq(FilteredColumn.new(column:, filter: 'filter'))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'with invalid column id' do
|
||||||
|
h = { filter: 'filter', id: 'invalid' }
|
||||||
|
expect { type.cast(h) }.to raise_error(JSON::ParserError)
|
||||||
|
|
||||||
|
h = { filter: 'filter', id: { procedure_id: 'invalid', column_id: 'nop' }.to_json }
|
||||||
|
expect { type.cast(h) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'deserialize' do
|
||||||
|
context 'with valid value' do
|
||||||
|
it 'works' do
|
||||||
|
column = Column.new(procedure_id: 1, table: 'table', column: 'column')
|
||||||
|
expect(Column).to receive(:find).with(column.h_id).and_return(column)
|
||||||
|
expect(type.deserialize({ id: column.h_id, filter: 'filter' }.to_json)).to eq(FilteredColumn.new(column: column, filter: 'filter'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with nil' do
|
||||||
|
it { expect(type.deserialize(nil)).to eq(nil) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'serialize' do
|
||||||
|
it 'with FilteredColumn' do
|
||||||
|
column = Column.new(procedure_id: 1, table: 'table', column: 'column')
|
||||||
|
sorted_column = FilteredColumn.new(column: column, filter: 'filter')
|
||||||
|
expect(type.serialize(sorted_column)).to eq({ id: column.h_id, filter: 'filter' }.to_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'with nil' do
|
||||||
|
expect(type.serialize(nil)).to eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'with invalid value' do
|
||||||
|
expect { type.serialize('invalid') }.to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue