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
|
||||
|
||||
def export_title(export)
|
||||
if export.procedure_presentation_id.nil?
|
||||
if !export.built_from_procedure_presentation?
|
||||
t(".export_title_everything", export_format: export.format)
|
||||
elsif export.tous?
|
||||
t(".export_title", export_format: export.format, count: export.count)
|
||||
|
|
|
@ -30,7 +30,7 @@ class Instructeurs::ColumnFilterComponent < ApplicationComponent
|
|||
{
|
||||
selected_key: column.present? ? column.id : '',
|
||||
items: filterable_columns_options,
|
||||
name: :column,
|
||||
name: "#{prefix}[id]",
|
||||
id: 'search-filter',
|
||||
'aria-describedby': 'instructeur-filter-combo-label',
|
||||
form: 'filter-component',
|
||||
|
@ -39,13 +39,20 @@ class Instructeurs::ColumnFilterComponent < ApplicationComponent
|
|||
end
|
||||
|
||||
def filterable_columns_options
|
||||
procedure.columns.filter_map do |column|
|
||||
next if column.filterable == false
|
||||
@procedure.columns.filter(&:filterable).map { [_1.label, _1.id] }
|
||||
end
|
||||
|
||||
[column.label, column.id]
|
||||
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
|
||||
|
||||
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
|
||||
= current_filter_tags
|
||||
|
||||
.fr-select-group
|
||||
= label_tag :column, t('.column'), class: 'fr-label fr-m-0', id: 'instructeur-filter-combo-label', for: 'search-filter'
|
||||
%react-fragment
|
||||
|
@ -8,9 +10,9 @@
|
|||
|
||||
= label_tag :value, t('.value'), for: 'value', class: 'fr-label'
|
||||
- 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
|
||||
%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
|
||||
= 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
|
||||
[
|
||||
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
|
||||
|
|
|
@ -9,6 +9,12 @@ class Instructeurs::ColumnTableHeaderComponent < ApplicationComponent
|
|||
|
||||
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)
|
||||
id = column.id
|
||||
order = opposite_order_for(column)
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
- @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)
|
||||
|
|
|
@ -8,7 +8,7 @@ module Administrateurs
|
|||
helper_method :create_archive_url
|
||||
|
||||
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
|
||||
@count_dossiers_termines_by_month = @procedure.dossiers.processed_by_month(all_groupe_instructeurs).count
|
||||
@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
|
||||
@procedure_presentation = procedure_presentation
|
||||
|
||||
@current_filters = current_filters
|
||||
@current_filters = procedure_presentation.filters_for(statut)
|
||||
@counts = current_instructeur
|
||||
.dossiers_count_summary(groupe_instructeur_ids)
|
||||
.symbolize_keys
|
||||
|
@ -95,7 +95,7 @@ module Instructeurs
|
|||
|
||||
@has_export_notification = notify_exports?
|
||||
@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
|
||||
|
||||
@dossiers_count = @filtered_sorted_ids.size
|
||||
|
@ -104,7 +104,7 @@ module Instructeurs
|
|||
.page(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? }
|
||||
|
||||
@batch_operations = BatchOperation.joins(:groupe_instructeurs)
|
||||
|
@ -133,9 +133,9 @@ module Instructeurs
|
|||
end
|
||||
|
||||
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))
|
||||
end
|
||||
|
@ -147,8 +147,10 @@ module Instructeurs
|
|||
end
|
||||
|
||||
def add_filter
|
||||
if !procedure_presentation.add_filter(statut, params[:column], params[:value])
|
||||
flash.alert = procedure_presentation.errors.full_messages
|
||||
if !procedure_presentation.update(filter_params)
|
||||
# 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
|
||||
|
||||
redirect_back(fallback_location: instructeur_procedure_url(procedure))
|
||||
|
@ -158,13 +160,10 @@ module Instructeurs
|
|||
@statut = statut
|
||||
@procedure = procedure
|
||||
@procedure_presentation = procedure_presentation
|
||||
@column = procedure.find_column(h_id: JSON.parse(params[:column], symbolize_names: true))
|
||||
end
|
||||
|
||||
def remove_filter
|
||||
procedure_presentation.remove_filter(statut, params[:column], params[:value])
|
||||
|
||||
redirect_back(fallback_location: instructeur_procedure_url(procedure))
|
||||
current_filter = procedure_presentation.filters_name_for(@statut)
|
||||
# According to the html, the selected column is the last one
|
||||
h_id = JSON.parse(params[current_filter].last[:id], symbolize_names: true)
|
||||
@column = procedure.find_column(h_id:)
|
||||
end
|
||||
|
||||
def download_export
|
||||
|
@ -383,10 +382,6 @@ module Instructeurs
|
|||
end
|
||||
end
|
||||
|
||||
def current_filters
|
||||
@current_filters ||= procedure_presentation.filters.fetch(statut, [])
|
||||
end
|
||||
|
||||
def bulk_message_params
|
||||
params.require(:bulk_message).permit(:body)
|
||||
end
|
||||
|
@ -415,5 +410,11 @@ module Instructeurs
|
|||
def sorted_column_params
|
||||
params.permit(sorted_column: [:order, :id])
|
||||
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
|
||||
|
|
|
@ -3,10 +3,14 @@
|
|||
class RechercheController < ApplicationController
|
||||
before_action :authenticate_logged_user!
|
||||
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 = [
|
||||
{ "table" => 'procedure', "column" => 'libelle' },
|
||||
{ "table" => 'user', "column" => 'email' },
|
||||
{ "table" => 'procedure', "column" => 'procedure_id' }
|
||||
Column.new(procedure_id: 666, table: 'procedure', column: 'libelle'),
|
||||
Column.new(procedure_id: 666, table: 'user', column: 'email'),
|
||||
Column.new(procedure_id: 666, table: 'procedure', column: 'procedure_id')
|
||||
]
|
||||
|
||||
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 }),
|
||||
key: Field::Text,
|
||||
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 }),
|
||||
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"),
|
||||
|
@ -34,7 +33,7 @@ class ExportDashboard < Administrate::BaseDashboard
|
|||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# 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
|
||||
# an array of attributes that will be displayed
|
||||
|
|
|
@ -10,28 +10,31 @@ class AssignTo < ApplicationRecord
|
|||
|
||||
def procedure_presentation_or_default_and_errors
|
||||
errors = reset_procedure_presentation_if_invalid
|
||||
|
||||
if self.procedure_presentation.nil?
|
||||
self.procedure_presentation = build_procedure_presentation
|
||||
self.procedure_presentation.save if procedure_presentation.valid? && !procedure_presentation.persisted?
|
||||
self.procedure_presentation = create_procedure_presentation!
|
||||
end
|
||||
|
||||
[self.procedure_presentation, errors]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_procedure_presentation_if_invalid
|
||||
if procedure_presentation&.invalid?
|
||||
# This is a last defense against invalid `ProcedurePresentation`s persistently
|
||||
# hindering instructeurs. Whenever this gets triggered, it means that there is
|
||||
# a bug somewhere else that we need to fix.
|
||||
errors = begin
|
||||
procedure_presentation.errors if procedure_presentation&.invalid?
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
[e.message]
|
||||
end
|
||||
|
||||
errors = procedure_presentation.errors
|
||||
if errors.present?
|
||||
Sentry.capture_message(
|
||||
"Destroying invalid ProcedurePresentation",
|
||||
extra: { procedure_presentation: procedure_presentation.as_json }
|
||||
extra: { procedure_presentation_id: procedure_presentation.id, errors: }
|
||||
)
|
||||
self.procedure_presentation = nil
|
||||
end
|
||||
|
||||
errors
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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'
|
||||
|
||||
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
|
||||
@table = table
|
||||
@column = column
|
||||
@label = label || I18n.t(column, scope: [:activerecord, :attributes, :procedure_presentation, :fields, table])
|
||||
@classname = classname
|
||||
@type = type
|
||||
@scope = scope
|
||||
@value_column = value_column
|
||||
|
@ -29,15 +33,21 @@ class Column
|
|||
|
||||
def to_json
|
||||
{
|
||||
table:, column:, label:, classname:, type:, scope:, value_column:, filterable:, displayable:
|
||||
table:, column:, label:, type:, scope:, value_column:, filterable:, displayable:
|
||||
}
|
||||
end
|
||||
|
||||
def notifications?
|
||||
table == 'notifications' && column == 'notifications'
|
||||
end
|
||||
def notifications? = [table, column] == ['notifications', 'notifications']
|
||||
|
||||
def dossier_state? = [table, column] == ['self', 'state']
|
||||
|
||||
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
|
||||
|
|
|
@ -12,7 +12,7 @@ module ColumnsConcern
|
|||
column = columns.find { _1.h_id == h_id } if h_id.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
|
||||
end
|
||||
|
@ -30,7 +30,11 @@ module ColumnsConcern
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def notifications_column
|
||||
|
@ -46,25 +50,23 @@ module ColumnsConcern
|
|||
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) }
|
||||
|
||||
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
|
||||
|
||||
def sva_svr_columns(for_filters: false)
|
||||
def sva_svr_columns
|
||||
return if !sva_svr_enabled?
|
||||
|
||||
scope = [:activerecord, :attributes, :procedure_presentation, :fields, :self]
|
||||
|
||||
columns = [
|
||||
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,
|
||||
label: I18n.t("#{sva_svr_decision}_decision_before", scope:))
|
||||
end
|
||||
|
||||
columns
|
||||
end
|
||||
|
@ -73,11 +75,17 @@ module ColumnsConcern
|
|||
SortedColumn.new(column: notifications_column, order: 'desc')
|
||||
end
|
||||
|
||||
def default_displayed_columns = [email_column]
|
||||
|
||||
private
|
||||
|
||||
def email_column
|
||||
Column.new(procedure_id: id, table: 'user', column: 'email')
|
||||
end
|
||||
|
||||
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: 'groupe_instructeur', column: 'id', type: :enum),
|
||||
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|
|
||||
table_column = ProcedurePresentation.sanitized_column(table, column)
|
||||
table_column = DossierFilterService.sanitized_column(table, column)
|
||||
q = Array.new(values.count, "(#{table_column} ILIKE ?)").join(' OR ')
|
||||
where(q, *(values.map { |value| "%#{value}%" }))
|
||||
}
|
||||
|
||||
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 ')
|
||||
where(q, *(values))
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
class Export < ApplicationRecord
|
||||
include TransientModelsWithPurgeableJobConcern
|
||||
|
||||
self.ignored_columns += ["procedure_presentation_snapshot"]
|
||||
|
||||
MAX_DUREE_CONSERVATION_EXPORT = 32.hours
|
||||
MAX_DUREE_GENERATION = 16.hours
|
||||
|
||||
|
@ -37,6 +39,9 @@ class Export < ApplicationRecord
|
|||
|
||||
has_one_attached :file
|
||||
|
||||
attribute :sorted_column, :sorted_column
|
||||
attribute :filtered_columns, :filtered_column, array: true
|
||||
|
||||
validates :format, :groupe_instructeurs, :key, presence: true
|
||||
|
||||
scope :ante_chronological, -> { order(updated_at: :desc) }
|
||||
|
@ -56,7 +61,6 @@ class Export < ApplicationRecord
|
|||
|
||||
def compute
|
||||
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
|
||||
end
|
||||
|
@ -65,17 +69,16 @@ class Export < ApplicationRecord
|
|||
time_span_type == Export.time_span_types.fetch(:monthly) ? 30.days.ago : nil
|
||||
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)
|
||||
filtered_columns = Array.wrap(procedure_presentation&.filters_for(statut))
|
||||
sorted_column = procedure_presentation&.sorted_column
|
||||
|
||||
attributes = {
|
||||
format:,
|
||||
export_template:,
|
||||
time_span_type:,
|
||||
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
|
||||
|
@ -87,36 +90,30 @@ class Export < ApplicationRecord
|
|||
|
||||
create!(**attributes, groupe_instructeurs:,
|
||||
user_profile:,
|
||||
procedure_presentation:,
|
||||
procedure_presentation_snapshot: procedure_presentation&.snapshot)
|
||||
filtered_columns:,
|
||||
sorted_column:)
|
||||
end
|
||||
|
||||
def self.for_groupe_instructeurs(groupe_instructeurs_ids)
|
||||
joins(:groupe_instructeurs).where(groupe_instructeurs: groupe_instructeurs_ids).distinct(:id)
|
||||
end
|
||||
|
||||
def self.by_key(groupe_instructeurs_ids, procedure_presentation)
|
||||
where(key: [
|
||||
generate_cache_key(groupe_instructeurs_ids),
|
||||
generate_cache_key(groupe_instructeurs_ids, procedure_presentation)
|
||||
])
|
||||
def self.by_key(groupe_instructeurs_ids)
|
||||
where(key: generate_cache_key(groupe_instructeurs_ids))
|
||||
end
|
||||
|
||||
def self.generate_cache_key(groupe_instructeurs_ids, procedure_presentation = nil)
|
||||
if procedure_presentation.present?
|
||||
def self.generate_cache_key(groupe_instructeurs_ids, filtered_columns = [], sorted_column = nil)
|
||||
columns_key = ([sorted_column] + filtered_columns).compact.map(&:id).sort.join
|
||||
|
||||
[
|
||||
groupe_instructeurs_ids.sort.join('-'),
|
||||
procedure_presentation.id,
|
||||
Digest::MD5.hexdigest(procedure_presentation.snapshot.slice(:filters, :sort).to_s)
|
||||
Digest::MD5.hexdigest(columns_key)
|
||||
].join('--')
|
||||
else
|
||||
groupe_instructeurs_ids.sort.join('-')
|
||||
end
|
||||
end
|
||||
|
||||
def count
|
||||
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
|
||||
end
|
||||
|
@ -125,13 +122,11 @@ class Export < ApplicationRecord
|
|||
groupe_instructeurs.first.procedure
|
||||
end
|
||||
|
||||
private
|
||||
def built_from_procedure_presentation?
|
||||
sorted_column.present? # hack has we know that procedure_presentation always has a sorted_column
|
||||
end
|
||||
|
||||
def load_snapshot!
|
||||
if procedure_presentation_snapshot.present?
|
||||
procedure_presentation.attributes = procedure_presentation_snapshot
|
||||
end
|
||||
end
|
||||
private
|
||||
|
||||
def dossiers_for_export
|
||||
@dossiers_for_export ||= begin
|
||||
|
@ -139,9 +134,9 @@ class Export < ApplicationRecord
|
|||
|
||||
if since.present?
|
||||
dossiers.visible_by_administration.where('dossiers.depose_at > ?', since)
|
||||
elsif procedure_presentation.present?
|
||||
filtered_sorted_ids = procedure_presentation
|
||||
.filtered_sorted_ids(dossiers, statut)
|
||||
elsif filtered_columns.present? || sorted_column.present?
|
||||
instructeur = instructeur_from(user_profile)
|
||||
filtered_sorted_ids = DossierFilterService.filtered_sorted_ids(dossiers, statut, filtered_columns, sorted_column, instructeur)
|
||||
|
||||
dossiers.where(id: filtered_sorted_ids)
|
||||
else
|
||||
|
@ -150,6 +145,15 @@ class Export < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def instructeur_from(user_profile)
|
||||
case user_profile
|
||||
when Administrateur
|
||||
user_profile.instructeur
|
||||
when Instructeur
|
||||
user_profile
|
||||
end
|
||||
end
|
||||
|
||||
def blob
|
||||
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
|
||||
|
||||
class ProcedurePresentation < ApplicationRecord
|
||||
TABLE = 'table'
|
||||
COLUMN = 'column'
|
||||
ORDER = 'order'
|
||||
|
||||
SLASH = '/'
|
||||
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
|
||||
has_many :exports, dependent: :destroy
|
||||
|
||||
delegate :procedure, :instructeur, to: :assign_to
|
||||
|
||||
validate :check_allowed_displayed_fields
|
||||
validate :check_allowed_filter_columns
|
||||
validate :check_filters_max_length
|
||||
validate :check_filters_max_integer
|
||||
attribute :displayed_columns, :column, array: true
|
||||
|
||||
attribute :sorted_column, :sorted_column
|
||||
def sorted_column = super || procedure.default_sorted_column # Dummy override to set default value
|
||||
|
||||
attribute :a_suivre_filters, :jsonb, array: true
|
||||
attribute :suivis_filters, :jsonb, array: true
|
||||
attribute :traites_filters, :jsonb, array: true
|
||||
attribute :tous_filters, :jsonb, array: true
|
||||
attribute :supprimes_filters, :jsonb, array: true
|
||||
attribute :supprimes_recemment_filters, :jsonb, array: true
|
||||
attribute :expirant_filters, :jsonb, array: true
|
||||
attribute :archives_filters, :jsonb, array: true
|
||||
attribute :a_suivre_filters, :filtered_column, array: true
|
||||
attribute :suivis_filters, :filtered_column, array: true
|
||||
attribute :traites_filters, :filtered_column, array: true
|
||||
attribute :tous_filters, :filtered_column, array: true
|
||||
attribute :supprimes_filters, :filtered_column, array: true
|
||||
attribute :supprimes_recemment_filters, :filtered_column, array: true
|
||||
attribute :expirant_filters, :filtered_column, 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)
|
||||
send(filters_name_for(statut))
|
||||
|
@ -42,46 +35,35 @@ class ProcedurePresentation < ApplicationRecord
|
|||
|
||||
def displayed_fields_for_headers
|
||||
[
|
||||
Column.new(procedure_id: procedure.id, table: 'self', column: 'id', classname: 'number-col'),
|
||||
*displayed_fields.map { Column.new(**_1.deep_symbolize_keys.merge(procedure_id: procedure.id)) },
|
||||
Column.new(procedure_id: procedure.id, table: 'self', column: 'state', classname: 'state-col'),
|
||||
procedure.dossier_id_column,
|
||||
*displayed_columns,
|
||||
procedure.dossier_state_column,
|
||||
*procedure.sva_svr_columns
|
||||
]
|
||||
end
|
||||
|
||||
def filtered_sorted_ids(dossiers, statut, count: nil)
|
||||
dossiers_by_statut = dossiers.by_statut(statut, instructeur)
|
||||
dossiers_sorted_ids = self.sorted_ids(dossiers_by_statut, count || dossiers_by_statut.size)
|
||||
|
||||
if filters[statut].present?
|
||||
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'
|
||||
def human_value_for_filter(filtered_column)
|
||||
if filtered_column.column.table == TYPE_DE_CHAMP
|
||||
find_type_de_champ(filtered_column.column.column).dynamic_type.filter_to_human(filtered_column.filter)
|
||||
elsif filtered_column.column.column == 'state'
|
||||
if filtered_column.filter == 'pending_correction'
|
||||
Dossier.human_attribute_name("pending_correction.for_instructeur")
|
||||
else
|
||||
Dossier.human_attribute_name("state.#{filter['value']}")
|
||||
Dossier.human_attribute_name("state.#{filtered_column.filter}")
|
||||
end
|
||||
elsif filter['table'] == 'groupe_instructeur' && filter['column'] == 'id'
|
||||
elsif filtered_column.column.table == 'groupe_instructeur' && filtered_column.column.column == 'id'
|
||||
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
|
||||
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
|
||||
parsed_date = safe_parse_date(filter['value'])
|
||||
parsed_date = safe_parse_date(filtered_column.filter)
|
||||
|
||||
return parsed_date.present? ? I18n.l(parsed_date) : nil
|
||||
end
|
||||
|
||||
filter['value']
|
||||
filtered_column.filter
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -91,179 +73,8 @@ class ProcedurePresentation < ApplicationRecord
|
|||
nil
|
||||
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
|
||||
|
||||
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)
|
||||
TypeDeChamp
|
||||
.joins(:revision_types_de_champ)
|
||||
|
@ -271,83 +82,4 @@ class ProcedurePresentation < ApplicationRecord
|
|||
.order(created_at: :desc)
|
||||
.find_by(stable_id: column)
|
||||
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
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
|
||||
def initialize(column:, order:)
|
||||
|
@ -19,4 +24,8 @@ class SortedColumn
|
|||
def sort_by_notifications?
|
||||
@column.notifications? && @order == 'desc'
|
||||
end
|
||||
|
||||
def id
|
||||
column.h_id.merge(order:).sort.to_json
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,10 +54,6 @@ class TypesDeChamp::TypeDeChampBase
|
|||
filter_value
|
||||
end
|
||||
|
||||
def human_to_filter(human_value)
|
||||
human_value
|
||||
end
|
||||
|
||||
class << self
|
||||
def champ_value(champ)
|
||||
champ.value.present? ? champ.value.to_s : champ_default_value
|
||||
|
|
|
@ -11,17 +11,6 @@ class TypesDeChamp::YesNoTypeDeChamp < TypesDeChamp::CheckboxTypeDeChamp
|
|||
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
|
||||
def champ_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:
|
||||
# - the order of the intermediary query results are unknown
|
||||
# - some values can be missing (if a revision added or removed them)
|
||||
def self.project(dossiers_ids, fields)
|
||||
fields = fields.deep_dup
|
||||
def self.project(dossiers_ids, columns)
|
||||
fields = columns.map { |c| { TABLE => c.table, COLUMN => c.column } }
|
||||
champ_value = champ_value_formatter(dossiers_ids, fields)
|
||||
|
||||
state_field = { TABLE => 'self', COLUMN => 'state' }
|
||||
archived_field = { TABLE => 'self', COLUMN => 'archived' }
|
||||
batch_operation_field = { TABLE => 'self', COLUMN => 'batch_operation_id' }
|
||||
|
@ -53,7 +55,7 @@ class DossierProjectionService
|
|||
individual_last_name = { TABLE => 'individual', COLUMN => 'nom' }
|
||||
sva_svr_decision_on_field = { TABLE => 'self', COLUMN => 'sva_svr_decision_on' }
|
||||
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)
|
||||
.each { |f| f[:id_value_h] = {} }
|
||||
.group_by { |f| f[TABLE] } # one query per table
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Maintenance
|
||||
# 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)
|
||||
# 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
|
||||
|
@ -16,7 +16,7 @@ module Maintenance
|
|||
filters_by_status.reject do |filter|
|
||||
filter.is_a?(Hash) &&
|
||||
filter['column'] == 'id' &&
|
||||
(filter['value']&.to_i&. >= ProcedurePresentation::PG_INTEGER_MAX_VALUE)
|
||||
(filter['value']&.to_i&. >= FilteredColumn::PG_INTEGER_MAX_VALUE)
|
||||
end
|
||||
end
|
||||
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')
|
||||
|
||||
- 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
|
||||
.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
|
||||
= " et "
|
||||
- filters.each_with_index do |filter, i|
|
||||
- if i > 0
|
||||
= " 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'] }),
|
||||
class: "fr-tag fr-tag--dismiss fr-my-1w", aria: { label: "Retirer le filtre #{filter['column']}" } do
|
||||
= "#{filter['label'].truncate(50)} : #{procedure_presentation.human_value_for_filter(filter)}"
|
||||
= form_tag(add_filter_instructeur_procedure_path(procedure), class: 'inline') do
|
||||
- prefix = procedure_presentation.filters_name_for(statut)
|
||||
= 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
|
||||
.flex.align-center
|
||||
- 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'
|
||||
|
||||
.fr-ml-auto
|
||||
|
|
|
@ -44,29 +44,6 @@
|
|||
],
|
||||
"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_code": 0,
|
||||
|
@ -136,6 +113,29 @@
|
|||
],
|
||||
"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_code": 2,
|
||||
|
@ -196,13 +196,13 @@
|
|||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
"fingerprint": "bd1df30f95135357b646e21a03d95498874faffa32e3804fc643e9b6b957ee14",
|
||||
"fingerprint": "aaff41afa7bd5a551cd2e3a385071090cb53c95caa40fad3785cd3d68c9b939c",
|
||||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/concerns/dossier_filtering_concern.rb",
|
||||
"line": 34,
|
||||
"line": 40,
|
||||
"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,
|
||||
"location": {
|
||||
"type": "method",
|
||||
|
@ -272,6 +272,6 @@
|
|||
"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"
|
||||
}
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
require Rails.root.join("app/types/column_type")
|
||||
require Rails.root.join("app/types/export_item_type")
|
||||
require Rails.root.join("app/types/sorted_column_type")
|
||||
require Rails.root.join("app/types/filtered_column_type")
|
||||
|
||||
ActiveSupport.on_load(:active_record) do
|
||||
ActiveRecord::Type.register(:column, ColumnType)
|
||||
ActiveRecord::Type.register(:export_item, ExportItemType)
|
||||
ActiveRecord::Type.register(:sorted_column, SortedColumnType)
|
||||
ActiveRecord::Type.register(:filtered_column, FilteredColumnType)
|
||||
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.
|
||||
|
||||
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
|
||||
enable_extension "pg_buffercache"
|
||||
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.integer "dossiers_count"
|
||||
t.bigint "export_template_id"
|
||||
t.jsonb "filtered_columns", default: [], null: false, array: true
|
||||
t.string "format", null: false
|
||||
t.bigint "instructeur_id"
|
||||
t.string "job_status", default: "pending", null: false
|
||||
t.text "key", null: false
|
||||
t.bigint "procedure_presentation_id"
|
||||
t.jsonb "procedure_presentation_snapshot"
|
||||
t.jsonb "sorted_column"
|
||||
t.string "statut", default: "tous"
|
||||
t.string "time_span_type", default: "everything", null: false
|
||||
t.datetime "updated_at", precision: nil, null: false
|
||||
|
|
|
@ -36,9 +36,9 @@ RSpec.describe Dossiers::ExportLinkComponent, type: :component do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when export is for a presentation' do
|
||||
context 'when export is from a presentation' do
|
||||
before do
|
||||
export.update!(procedure_presentation: procedure_presentation)
|
||||
export.update!(sorted_column: procedure.default_sorted_column)
|
||||
end
|
||||
|
||||
it 'display the persisted dossiers count' do
|
||||
|
@ -48,7 +48,7 @@ RSpec.describe Dossiers::ExportLinkComponent, type: :component do
|
|||
end
|
||||
|
||||
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
|
||||
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_presentation) { nil }
|
||||
let(:statut) { nil }
|
||||
let(:column) { nil }
|
||||
|
||||
before do
|
||||
allow(component).to receive(:current_instructeur).and_return(instructeur)
|
||||
end
|
||||
|
||||
describe ".filterable_columns_options" do
|
||||
context 'filders' do
|
||||
let(:column) { nil }
|
||||
let(:included_displayable_field) do
|
||||
[
|
||||
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
|
||||
let(:filterable_column) { Column.new(procedure_id:, label: 'email', table: 'user', column: 'email') }
|
||||
let(:non_filterable_column) { Column.new(procedure_id:, label: 'depose_since', table: 'self', column: 'depose_since', filterable: false) }
|
||||
let(:mocked_columns) { [filterable_column, non_filterable_column] }
|
||||
|
||||
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 }
|
||||
|
||||
it { is_expected.to eq([["email", included_displayable_field.first.id], ["depose_since", included_displayable_field.second.id]]) }
|
||||
end
|
||||
it { is_expected.to eq([[filterable_column.label, filterable_column.id]]) }
|
||||
end
|
||||
|
||||
describe '.options_for_select_of_column' do
|
||||
|
|
|
@ -905,8 +905,8 @@ describe Instructeurs::ProceduresController, type: :controller do
|
|||
end
|
||||
|
||||
subject do
|
||||
column = procedure.find_column(label: "Nom").id
|
||||
post :add_filter, params: { procedure_id: procedure.id, column:, value: "n" * 4100, statut: "a-suivre" }
|
||||
column = procedure.find_column(label: "Nom")
|
||||
post :add_filter, params: { procedure_id: procedure.id, a_suivre_filters: [{ id: column.id, filter: "n" * 4049 }] }
|
||||
end
|
||||
|
||||
it 'should render the error' do
|
||||
|
|
|
@ -8,7 +8,10 @@ FactoryBot.define do
|
|||
groupe_instructeurs { [association(:groupe_instructeur)] }
|
||||
|
||||
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.dossiers_count = 10 if !export.pending?
|
||||
end
|
||||
|
|
|
@ -38,10 +38,7 @@ describe '20240920130741_migrate_procedure_presentation_to_columns.rake' do
|
|||
it 'populates the columns' do
|
||||
procedure_id = procedure.id
|
||||
|
||||
expect(procedure_presentation.displayed_columns).to eq([
|
||||
{ "procedure_id" => procedure_id, "column_id" => "etablissement/entreprise_raison_sociale" },
|
||||
{ "procedure_id" => procedure_id, "column_id" => "type_de_champ/#{stable_id}" }
|
||||
])
|
||||
expect(procedure_presentation.displayed_columns.map(&:label)).to eq(["Raison sociale", procedure.active_revision.types_de_champ.first.libelle])
|
||||
|
||||
order, column_id = procedure_presentation
|
||||
.sorted_column
|
||||
|
@ -52,9 +49,8 @@ describe '20240920130741_migrate_procedure_presentation_to_columns.rake' do
|
|||
|
||||
expect(procedure_presentation.tous_filters).to eq([])
|
||||
|
||||
traites = procedure_presentation.traites_filters
|
||||
.map { [_1['id'], _1['filter']] }
|
||||
traites = procedure_presentation.traites_filters.map { [_1.label, _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
|
||||
|
|
|
@ -9,32 +9,43 @@ describe AssignTo, type: :model do
|
|||
let(:procedure_presentation_or_default) { procedure_presentation_and_errors.first }
|
||||
let(:errors) { procedure_presentation_and_errors.second }
|
||||
|
||||
context "without a procedure_presentation" do
|
||||
it { expect(procedure_presentation_or_default).to be_persisted }
|
||||
it { expect(procedure_presentation_or_default).to be_valid }
|
||||
it { expect(errors).to be_nil }
|
||||
context "without a preexisting procedure_presentation" do
|
||||
it 'creates a default pp' do
|
||||
expect(procedure_presentation_or_default).to be_persisted
|
||||
expect(procedure_presentation_or_default).to be_valid
|
||||
expect(errors).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "with a procedure_presentation" do
|
||||
let!(:procedure_presentation) { ProcedurePresentation.create(assign_to: assign_to) }
|
||||
context "with a preexisting procedure_presentation" do
|
||||
let!(:procedure_presentation) { ProcedurePresentation.create(assign_to:) }
|
||||
|
||||
it { expect(procedure_presentation_or_default).to eq(procedure_presentation) }
|
||||
it { expect(procedure_presentation_or_default).to be_valid }
|
||||
it { expect(errors).to be_nil }
|
||||
it 'returns the preexisting pp' do
|
||||
expect(procedure_presentation_or_default).to eq(procedure_presentation)
|
||||
expect(procedure_presentation_or_default).to be_valid
|
||||
expect(errors).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "with an invalid procedure_presentation" do
|
||||
let!(:procedure_presentation) do
|
||||
pp = ProcedurePresentation.new(assign_to: assign_to, displayed_fields: [{ 'table' => 'invalid', 'column' => 'random' }])
|
||||
pp.save(validate: false)
|
||||
pp
|
||||
pp = ProcedurePresentation.create(assign_to: assign_to)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,37 +29,37 @@ describe ColumnsConcern do
|
|||
let(:tdc_private_2) { procedure.active_revision.types_de_champ_private[1] }
|
||||
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: 'Créé le', table: 'self', column: 'created_at', classname: '', 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: 'Déposé le', table: 'self', column: 'depose_at', classname: '', 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 instruction le', table: 'self', column: 'en_instruction_at', classname: '', 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: "Mis à jour depuis", table: "self", column: "updated_since", classname: "", 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: "En construction depuis", table: "self", column: "en_construction_since", classname: "", 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: "Terminé depuis", table: "self", column: "processed_since", classname: "", 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: 'Demandeur', table: 'user', column: 'email', classname: '', 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: 'Groupe instructeur', table: 'groupe_instructeur', column: 'id', classname: '', 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: 'SIREN', table: 'etablissement', column: 'entreprise_siren', classname: '', 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: 'Nom commercial', table: 'etablissement', column: 'entreprise_nom_commercial', classname: '', 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: 'SIRET siège social', table: 'etablissement', column: 'entreprise_siret_siege_social', classname: '', 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: 'SIRET', table: 'etablissement', column: 'siret', classname: '', 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: 'Code postal', table: 'etablissement', column: 'code_postal', 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, 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, 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, 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, classname: '', displayable: true, type: :text, 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', 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', 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', displayable: true, 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", 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", 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", displayable: false, scope: 'instructeurs.dossiers.filterable_state', type: :enum, 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', displayable: true, type: :text, 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', displayable: true, type: :text, scope: '', value_column: :value, filterable: false },
|
||||
{ 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', 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', 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', displayable: true, type: :date, 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', 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, 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, 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:)) }
|
||||
}
|
||||
|
||||
|
@ -84,9 +84,9 @@ describe ColumnsConcern do
|
|||
end
|
||||
|
||||
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(:surname_field) { Column.new(procedure_id:, label: "Nom", table: "individual", column: "nom", classname: '', 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(: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", 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_id) { procedure.id }
|
||||
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) }
|
||||
|
@ -99,8 +99,8 @@ describe ColumnsConcern do
|
|||
let(:procedure_id) { procedure.id }
|
||||
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_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_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", displayable: false, type: :date, scope: '', value_column: :value, filterable: true) }
|
||||
|
||||
it { is_expected.to include(decision_on, decision_before_field) }
|
||||
end
|
||||
|
@ -110,8 +110,8 @@ describe ColumnsConcern do
|
|||
let(:procedure_id) { procedure.id }
|
||||
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_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_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", displayable: false, type: :date, scope: '', value_column: :value, filterable: true) }
|
||||
|
||||
it { is_expected.to include(decision_on, decision_before_field) }
|
||||
end
|
||||
|
|
|
@ -61,31 +61,18 @@ RSpec.describe Export, type: :model do
|
|||
it { expect(groupe_instructeur.reload).to be_present }
|
||||
end
|
||||
|
||||
describe '.find_by groupe_instructeurs' do
|
||||
describe '.by_key groupe_instructeurs' do
|
||||
let!(:procedure) { create(:procedure) }
|
||||
let!(:gi_1) { 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)]) }
|
||||
|
||||
context 'without procedure_presentation' do
|
||||
context 'when an export is made for one groupe instructeur' do
|
||||
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_2.id, gi_1.id], nil)).to eq([export]) }
|
||||
it { expect(Export.by_key([gi_1.id, gi_2.id, gi_3.id], nil)).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
|
||||
it { expect(Export.by_key([gi_1.id])).to be_empty }
|
||||
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])).to be_empty }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -94,7 +81,9 @@ RSpec.describe Export, type: :model do
|
|||
let(:instructeur) { create(: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 }
|
||||
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
|
||||
it 'works once' do
|
||||
|
@ -105,7 +94,10 @@ RSpec.describe Export, type: :model 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) }
|
||||
.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) }
|
||||
.to change { Export.count }.by(1)
|
||||
end
|
||||
|
@ -162,10 +154,16 @@ RSpec.describe Export, type: :model do
|
|||
let!(:dossier_accepte) { create(:dossier, :accepte, procedure: procedure) }
|
||||
|
||||
let(:export) do
|
||||
create(:export,
|
||||
groupe_instructeurs: [procedure.groupe_instructeurs.first],
|
||||
procedure_presentation: procedure_presentation,
|
||||
statut: statut)
|
||||
groupe_instructeurs = [procedure.groupe_instructeurs.first]
|
||||
user_profile = groupe_instructeurs.first.instructeurs.first
|
||||
|
||||
Export.find_or_create_fresh_export(
|
||||
:csv,
|
||||
groupe_instructeurs,
|
||||
user_profile,
|
||||
procedure_presentation:,
|
||||
statut:
|
||||
)
|
||||
end
|
||||
|
||||
context 'without procedure_presentation or since' do
|
||||
|
@ -179,17 +177,28 @@ RSpec.describe Export, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with procedure_presentation and statut supprimes' do
|
||||
let(:statut) { 'supprimes' }
|
||||
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) }
|
||||
context 'with procedure_presentation and statut tous and filter en_construction' do
|
||||
let(:statut) { 'tous' }
|
||||
|
||||
it 'includes supprimes' do
|
||||
expect(export.send(:dossiers_for_export)).to include(dossier_supprime)
|
||||
let(:procedure_presentation) do
|
||||
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
|
||||
|
|
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 }
|
||||
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
|
||||
let(:procedure_id) { procedure_2.id }
|
||||
|
||||
|
|
|
@ -4,14 +4,15 @@ describe ProcedurePresentation do
|
|||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
let(:procedure) { create(:procedure, :published, types_de_champ_public:, types_de_champ_private: [{}]) }
|
||||
let(:procedure_id) { procedure.id }
|
||||
let(:types_de_champ_public) { [{}] }
|
||||
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_id) { first_type_de_champ.stable_id.to_s }
|
||||
let(:procedure_presentation) {
|
||||
create(:procedure_presentation,
|
||||
assign_to: assign_to,
|
||||
assign_to:,
|
||||
displayed_fields: [
|
||||
{ label: "test1", table: "user", column: "email" },
|
||||
{ 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(: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
|
||||
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
|
||||
|
@ -37,744 +40,27 @@ describe ProcedurePresentation do
|
|||
describe 'validation' do
|
||||
it { expect(build(:procedure_presentation)).to be_valid }
|
||||
|
||||
context 'of displayed fields' do
|
||||
it { expect(build(:procedure_presentation, displayed_fields: [{ table: "user", column: "reset_password_token", "order" => "asc" }])).to be_invalid }
|
||||
context 'of displayed columns' do
|
||||
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
|
||||
|
||||
context 'of filters' do
|
||||
it do
|
||||
expect(build(:procedure_presentation, filters: { "suivis" => [{ table: "user", column: "reset_password_token", "order" => "asc" }] })).to be_invalid
|
||||
expect(build(:procedure_presentation, filters: { "suivis" => [{ table: "user", column: "email", "value" => "exceedingly long filter value" * 1000 }] })).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
|
||||
it 'validates the filter_column objects' do
|
||||
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, "suivis_filters": [{ id: { column_id: "user/email", procedure_id: }, "filter": "exceedingly long filter value" * 400 }])).to be_invalid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
it 'should passthrough value' do
|
||||
|
@ -791,7 +77,7 @@ describe ProcedurePresentation do
|
|||
end
|
||||
|
||||
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
|
||||
expect(subject).to eq("En construction")
|
||||
|
@ -799,7 +85,7 @@ describe ProcedurePresentation do
|
|||
end
|
||||
|
||||
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
|
||||
expect(subject).to eq("15/06/2023")
|
||||
|
@ -807,129 +93,10 @@ describe ProcedurePresentation do
|
|||
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
|
||||
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
|
||||
create(:procedure_presentation, assign_to:).tap do |pp|
|
||||
pp.update(sorted_column: SortedColumn.new(column: procedure.find_column(label: 'Demandeur'), order: 'desc'))
|
||||
|
@ -937,24 +104,19 @@ describe ProcedurePresentation do
|
|||
end
|
||||
|
||||
subject do
|
||||
procedure_presentation.update_displayed_fields([
|
||||
procedure.find_column(label: 'En construction le').id,
|
||||
procedure.find_column(label: 'Mis à jour le').id
|
||||
procedure_presentation.update(displayed_columns: [
|
||||
en_construction_column.id, mise_a_jour_column.id
|
||||
])
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
expect(procedure_presentation.displayed_columns).to eq([
|
||||
{ "column_id" => "self/en_construction_at", "procedure_id" => procedure.id },
|
||||
{ "column_id" => "self/updated_at", "procedure_id" => procedure.id }
|
||||
en_construction_column, mise_a_jour_column
|
||||
])
|
||||
|
||||
expect(procedure_presentation.sorted_column).to eq(procedure.default_sorted_column)
|
||||
expect(procedure_presentation.sorted_column.order).to eq('desc')
|
||||
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 '#project' do
|
||||
subject { described_class.project(dossiers_ids, fields) }
|
||||
subject { described_class.project(dossiers_ids, columns) }
|
||||
|
||||
context 'with multiple dossier' do
|
||||
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(: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|
|
||||
{
|
||||
"table" => "type_de_champ",
|
||||
"column" => type_de_champ.stable_id.to_s
|
||||
}
|
||||
procedure.find_column(label: type_de_champ.libelle)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -55,12 +52,9 @@ describe DossierProjectionService do
|
|||
let!(:dossier) { create(:dossier, procedure:) }
|
||||
|
||||
let(:dossiers_ids) { [dossier.id] }
|
||||
let(:fields) do
|
||||
let(:columns) do
|
||||
[
|
||||
{
|
||||
"table" => "type_de_champ",
|
||||
"column" => procedure.active_revision.types_de_champ_public[0].stable_id.to_s
|
||||
}
|
||||
procedure.find_column(label: procedure.active_revision.types_de_champ_public[0].libelle)
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -78,38 +72,37 @@ describe DossierProjectionService do
|
|||
end
|
||||
|
||||
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] }
|
||||
|
||||
subject { super()[0].columns[0] }
|
||||
|
||||
context 'for self table' do
|
||||
let(:table) { 'self' }
|
||||
|
||||
context 'for created_at column' do
|
||||
let(:column) { 'created_at' }
|
||||
let(:dossier) { Timecop.freeze(Time.zone.local(1992, 3, 22)) { create(:dossier) } }
|
||||
let(:label) { 'Créé le' }
|
||||
let(:dossier) { Timecop.freeze(Time.zone.local(1992, 3, 22)) { create(:dossier, procedure:) } }
|
||||
|
||||
it { is_expected.to eq('22/03/1992') }
|
||||
end
|
||||
|
||||
context 'for en_construction_at column' do
|
||||
let(:column) { 'en_construction_at' }
|
||||
let(:dossier) { create(:dossier, :en_construction, en_construction_at: Time.zone.local(2018, 10, 17)) }
|
||||
let(:label) { 'En construction le' }
|
||||
let(:dossier) { create(:dossier, :en_construction, en_construction_at: Time.zone.local(2018, 10, 17), procedure:) }
|
||||
|
||||
it { is_expected.to eq('17/10/2018') }
|
||||
end
|
||||
|
||||
context 'for depose_at column' do
|
||||
let(:column) { 'depose_at' }
|
||||
let(:dossier) { create(:dossier, :en_construction, depose_at: Time.zone.local(2018, 10, 17)) }
|
||||
let(:label) { 'Déposé le' }
|
||||
let(:dossier) { create(:dossier, :en_construction, depose_at: Time.zone.local(2018, 10, 17), procedure:) }
|
||||
|
||||
it { is_expected.to eq('17/10/2018') }
|
||||
end
|
||||
|
||||
context 'for updated_at column' do
|
||||
let(:column) { 'updated_at' }
|
||||
let(:dossier) { create(:dossier) }
|
||||
let(:label) { 'Mis à jour le' }
|
||||
let(:dossier) { create(:dossier, procedure:) }
|
||||
|
||||
before { dossier.touch(time: Time.zone.local(2018, 9, 25)) }
|
||||
|
||||
|
@ -118,61 +111,56 @@ describe DossierProjectionService do
|
|||
end
|
||||
|
||||
context 'for user table' do
|
||||
let(:table) { 'user' }
|
||||
let(:column) { 'email' }
|
||||
let(:label) { 'Demandeur' }
|
||||
|
||||
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') }
|
||||
end
|
||||
|
||||
context 'for individual table' do
|
||||
let(:table) { 'individual' }
|
||||
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
|
||||
let(:column) { 'prenom' }
|
||||
let(:label) { 'Prénom' }
|
||||
|
||||
it { is_expected.to eq('Jacques') }
|
||||
end
|
||||
|
||||
context 'for nom column' do
|
||||
let(:column) { 'nom' }
|
||||
let(:label) { 'Nom' }
|
||||
|
||||
it { is_expected.to eq('Martin') }
|
||||
end
|
||||
|
||||
context 'for gender column' do
|
||||
let(:column) { 'gender' }
|
||||
let(:label) { 'Civilité' }
|
||||
|
||||
it { is_expected.to eq('M.') }
|
||||
end
|
||||
end
|
||||
|
||||
context 'for etablissement table' do
|
||||
let(:table) { 'etablissement' }
|
||||
let(:column) { 'code_postal' } # All other columns work the same, no extra test required
|
||||
let(:label) { 'Code postal' }
|
||||
|
||||
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') }
|
||||
end
|
||||
|
||||
context 'for groupe_instructeur table' do
|
||||
let(:table) { 'groupe_instructeur' }
|
||||
let(:column) { 'label' }
|
||||
let(:label) { 'Groupe instructeur' }
|
||||
|
||||
let!(:dossier) { create(:dossier) }
|
||||
let!(:dossier) { create(:dossier, procedure:) }
|
||||
|
||||
it { is_expected.to eq('défaut') }
|
||||
end
|
||||
|
||||
context 'for followers_instructeurs table' do
|
||||
let(:table) { 'followers_instructeurs' }
|
||||
let(:column) { 'email' }
|
||||
let(:label) { 'Email instructeur' }
|
||||
|
||||
let(:dossier) { create(:dossier) }
|
||||
let(:dossier) { create(:dossier, procedure:) }
|
||||
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!(:follow3) { create(:follow, dossier: dossier, instructeur: create(:instructeur, email: 'c@host.fr')) }
|
||||
|
@ -181,19 +169,21 @@ describe DossierProjectionService do
|
|||
end
|
||||
|
||||
context 'for type_de_champ table' do
|
||||
let(:table) { 'type_de_champ' }
|
||||
let(:dossier) { create(:dossier) }
|
||||
let(:column) { dossier.procedure.active_revision.types_de_champ_public.first.stable_id.to_s }
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :text }]) }
|
||||
let(:dossier) { create(:dossier, procedure:) }
|
||||
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') }
|
||||
end
|
||||
|
||||
context 'for type_de_champ_private table' do
|
||||
let(:table) { 'type_de_champ_private' }
|
||||
let(:dossier) { create(:dossier) }
|
||||
let(:column) { dossier.procedure.active_revision.types_de_champ_private.first.stable_id.to_s }
|
||||
let(:procedure) { create(:procedure, types_de_champ_private: [{ type: :text }]) }
|
||||
let(:dossier) { create(:dossier, procedure:) }
|
||||
let(:label) { dossier.procedure.active_revision.types_de_champ_private.first.libelle }
|
||||
|
||||
before { dossier.project_champs_private.first.update(value: 'quinoa') }
|
||||
|
||||
|
@ -201,10 +191,9 @@ describe DossierProjectionService do
|
|||
end
|
||||
|
||||
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(:dossier) { create(:dossier, procedure: procedure) }
|
||||
let(:column) { dossier.procedure.active_revision.types_de_champ_public.first.stable_id.to_s }
|
||||
let(:dossier) { create(:dossier, procedure:) }
|
||||
let(:label) { dossier.procedure.active_revision.types_de_champ_public.first.libelle }
|
||||
|
||||
before { dossier.project_champs_public.first.update(value: 'true') }
|
||||
|
||||
|
@ -212,10 +201,9 @@ describe DossierProjectionService do
|
|||
end
|
||||
|
||||
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(:dossier) { create(:dossier, procedure: procedure) }
|
||||
let(:column) { dossier.procedure.active_revision.types_de_champ_public.first.stable_id.to_s }
|
||||
let(:dossier) { create(:dossier, procedure:) }
|
||||
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' }) }
|
||||
|
||||
|
@ -223,10 +211,9 @@ describe DossierProjectionService do
|
|||
end
|
||||
|
||||
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(:dossier) { create(:dossier, procedure: procedure) }
|
||||
let(:column) { dossier.procedure.active_revision.types_de_champ_public.first.stable_id.to_s }
|
||||
let(:dossier) { create(:dossier, procedure:) }
|
||||
let(:label) { dossier.procedure.active_revision.types_de_champ_public.first.libelle }
|
||||
|
||||
around do |example|
|
||||
I18n.with_locale(:fr) do
|
||||
|
@ -254,8 +241,10 @@ describe DossierProjectionService do
|
|||
context 'for dossier corrections table' do
|
||||
let(:table) { 'dossier_corrections' }
|
||||
let(:column) { 'resolved_at' }
|
||||
let(:dossier) { create(:dossier, :en_construction) }
|
||||
subject { described_class.project(dossiers_ids, fields)[0] }
|
||||
let(:procedure) { create(:procedure) }
|
||||
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
|
||||
before { create(:dossier_correction, dossier:) }
|
||||
|
|
|
@ -224,7 +224,7 @@ describe "procedure filters" do
|
|||
end
|
||||
|
||||
def remove_filter(filter_value)
|
||||
click_link text: filter_value
|
||||
click_button text: filter_value
|
||||
end
|
||||
|
||||
def add_column(column_name)
|
||||
|
|
|
@ -14,14 +14,14 @@ module Maintenance
|
|||
before { element.update_column(:filters, filters) }
|
||||
|
||||
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
|
||||
expect { subject }.not_to change { element.reload.filters }
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
expect { subject }.to change { element.reload.filters }.to({ "suivis" => [] })
|
||||
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