[#3479] les instructeurs peuvent filtrer les dossiers avec plusieurs critères sur une même colonne. Les critères sont combinés avec un "OU".

This commit is contained in:
Frederic Merizen 2019-03-11 18:58:10 +01:00 committed by GitHub
commit cccaf54e2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 399 additions and 176 deletions

View file

@ -87,7 +87,7 @@ module NewGestionnaire
@dossiers = @dossiers.where(id: filtered_sorted_paginated_ids)
eager_load_displayed_fields
@dossiers = procedure_presentation.eager_load_displayed_fields(@dossiers)
@dossiers = @dossiers.sort_by { |d| filtered_sorted_paginated_ids.index(d.id) }
@ -102,17 +102,13 @@ module NewGestionnaire
end
fields = values.map do |value|
table, column = value.split("/")
procedure_presentation.fields.find do |field|
field['table'] == table && field['column'] == column
end
find_field(*value.split('/'))
end
procedure_presentation.update(displayed_fields: fields)
current_sort = procedure_presentation.sort
if !values.include?("#{current_sort['table']}/#{current_sort['column']}")
if !values.include?(field_id(current_sort))
procedure_presentation.update(sort: Procedure.default_sort)
end
@ -145,7 +141,7 @@ module NewGestionnaire
if params[:value].present?
filters = procedure_presentation.filters
table, column = params[:field].split('/')
label = procedure_presentation.fields.find { |c| c['table'] == table && c['column'] == column }['label']
label = find_field(table, column)['label']
filters[statut] << {
'label' => label,
@ -162,11 +158,9 @@ module NewGestionnaire
def remove_filter
filters = procedure_presentation.filters
filter_to_remove = current_filters.find do |filter|
filter['table'] == params[:table] && filter['column'] == params[:column]
end
filters[statut] = filters[statut] - [filter_to_remove]
to_remove = params.values_at(:table, :column, :value)
filters[statut].reject! { |filter| filter.values_at('table', 'column', 'value') == to_remove }
procedure_presentation.update(filters: filters)
@ -194,6 +188,14 @@ module NewGestionnaire
private
def find_field(table, column)
procedure_presentation.fields.find { |c| c['table'] == table && c['column'] == column }
end
def field_id(field)
field.values_at('table', 'column').join('/')
end
def statut
@statut ||= (params[:statut].presence || 'a-suivre')
end
@ -228,9 +230,7 @@ module NewGestionnaire
end
def displayed_fields_values
procedure_presentation.displayed_fields.map do |field|
"#{field['table']}/#{field['column']}"
end
procedure_presentation.displayed_fields.map { |field| field_id(field) }
end
def current_filters
@ -238,44 +238,7 @@ module NewGestionnaire
end
def available_fields_to_filters
current_filters_fields_ids = current_filters.map do |field|
"#{field['table']}/#{field['column']}"
end
procedure_presentation.fields_for_select.reject do |field|
current_filters_fields_ids.include?(field[1])
end
end
def eager_load_displayed_fields
procedure_presentation.displayed_fields
.reject { |field| field['table'] == 'self' }
.group_by do |field|
if ['type_de_champ', 'type_de_champ_private'].include?(field['table'])
'type_de_champ_group'
else
field['table']
end
end.each do |group_key, fields|
case group_key
when 'type_de_champ_group'
if fields.any? { |field| field['table'] == 'type_de_champ' }
@dossiers = @dossiers.includes(:champs).references(:champs)
end
if fields.any? { |field| field['table'] == 'type_de_champ_private' }
@dossiers = @dossiers.includes(:champs_private).references(:champs_private)
end
where_conditions = fields.map do |field|
"champs.type_de_champ_id = #{field['column']}"
end.join(" OR ")
@dossiers = @dossiers.where(where_conditions)
else
@dossiers = @dossiers.includes(fields.first['table'])
end
end
procedure_presentation.fields_for_select
end
def kaminarize(current_page, total)

View file

@ -0,0 +1,21 @@
module DossierFilteringConcern
extend ActiveSupport::Concern
included do
scope :filter_by_datetimes, lambda { |column, dates|
if dates.present?
dates
.map { |date| self.where(column => date..(date + 1.day)) }
.reduce(:or)
else
none
end
}
scope :filter_ilike, lambda { |table, column, values|
table_column = ProcedurePresentation.sanitized_column(table, column)
q = Array.new(values.count, "(#{table_column} ILIKE ?)").join(' OR ')
where(q, *(values.map { |value| "%#{value}%" }))
}
end
end

View file

@ -1,4 +1,6 @@
class Dossier < ApplicationRecord
include DossierFilteringConcern
enum state: {
brouillon: 'brouillon',
en_construction: 'en_construction',

View file

@ -74,9 +74,7 @@ class ProcedurePresentation < ApplicationRecord
def sorted_ids(dossiers, gestionnaire)
dossiers.each { |dossier| assert_matching_procedure(dossier) }
table = sort['table']
column = sanitized_column(sort)
order = sort['order']
table, column, order = sort.values_at('table', 'column', 'order')
case table
when 'notifications'
@ -88,80 +86,85 @@ class ProcedurePresentation < ApplicationRecord
return (dossiers.order('dossiers.updated_at asc').ids - dossiers_id_with_notification) +
dossiers_id_with_notification
end
when 'self'
return dossiers
.order("#{column} #{order}")
.pluck(:id)
when 'type_de_champ', 'type_de_champ_private'
return dossiers
.includes(table == 'type_de_champ' ? :champs : :champs_private)
.where("champs.type_de_champ_id = #{sort['column'].to_i}")
.where("champs.type_de_champ_id = #{column.to_i}")
.order("champs.value #{order}")
.pluck(:id)
when 'user', 'individual', 'etablissement'
return dossiers
.includes(table)
.order("#{column} #{order}")
when 'self', 'user', 'individual', 'etablissement'
return (table == 'self' ? dossiers : dossiers.includes(table))
.order("#{self.class.sanitized_column(table, column)} #{order}")
.pluck(:id)
end
end
def filtered_ids(dossiers, statut)
dossiers.each { |dossier| assert_matching_procedure(dossier) }
filters[statut].map do |filter|
table = filter['table']
column = sanitized_column(filter)
filters[statut].group_by { |filter| filter.values_at('table', 'column') } .map do |(table, column), filters|
values = filters.pluck('value')
case table
when 'self'
date = Time.zone.parse(filter['value']).beginning_of_day rescue nil
if date.present?
dossiers.where("#{column} BETWEEN ? AND ?", date, date + 1.day)
else
[]
end
dates = values
.map { |v| Time.zone.parse(v).beginning_of_day rescue nil }
.compact
dossiers.filter_by_datetimes(column, dates)
when 'type_de_champ', 'type_de_champ_private'
relation = table == 'type_de_champ' ? :champs : :champs_private
dossiers
.includes(relation)
.where("champs.type_de_champ_id = ?", filter['column'].to_i)
.where("champs.value ILIKE ?", "%#{filter['value']}%")
.where("champs.type_de_champ_id = ?", column.to_i)
.filter_ilike(:champ, :value, values)
when 'etablissement'
if filter['column'] == 'entreprise_date_creation'
date = filter['value'].to_date rescue nil
if column == 'entreprise_date_creation'
dates = values
.map { |v| v.to_date rescue nil }
.compact
dossiers
.includes(table)
.where("#{column} = ?", date)
.where(table.pluralize => { column => dates })
else
dossiers
.includes(table)
.where("#{column} ILIKE ?", "%#{filter['value']}%")
.filter_ilike(table, column, values)
end
when 'user', 'individual'
dossiers
.includes(table)
.where("#{column} ILIKE ?", "%#{filter['value']}%")
.filter_ilike(table, column, values)
end.pluck(:id)
end.reduce(:&)
end
def eager_load_displayed_fields(dossiers)
relations_to_include = displayed_fields
.pluck('table')
.reject { |table| table == 'self' }
.map do |table|
case table
when 'type_de_champ'
:champs
when 'type_de_champ_private'
:champs_private
else
table
end
end
.uniq
dossiers.includes(relations_to_include)
end
private
def check_allowed_displayed_fields
displayed_fields.each do |field|
table = field['table']
column = field['column']
if !valid_column?(table, column)
errors.add(:filters, "#{table}.#{column} nest pas une colonne permise")
end
check_allowed_field(:displayed_fields, field)
end
end
def check_allowed_sort_column
table = sort['table']
column = sort['column']
if !valid_sort_column?(table, column)
errors.add(:sort, "#{table}.#{column} nest pas une colonne permise")
end
check_allowed_field(:sort, sort, EXTRA_SORT_COLUMNS)
end
def check_allowed_sort_order
@ -174,15 +177,18 @@ class ProcedurePresentation < ApplicationRecord
def check_allowed_filter_columns
filters.each do |_, columns|
columns.each do |column|
table = column['table']
column = column['column']
if !valid_column?(table, column)
errors.add(:filters, "#{table}.#{column} nest pas une colonne permise")
end
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} nest pas une colonne permise")
end
end
def assert_matching_procedure(dossier)
if dossier.procedure != procedure
raise "Procedure mismatch (expected #{procedure.id}, got #{dossier.procedure.id})"
@ -210,32 +216,27 @@ class ProcedurePresentation < ApplicationRecord
}
end
def valid_column?(table, column)
valid_columns_for_table(table).include?(column)
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 ||= fields
.group_by { |field| field['table'] }
.map { |table, fields| [table, Set.new(fields.map { |field| field['column'] })] }
.map { |table, fields| [table, Set.new(fields.pluck('column'))] }
.to_h
@column_whitelist[table] || []
end
def sanitized_column(field)
table = field['table']
table = ActiveRecord::Base.connection.quote_column_name((table == 'self' ? 'dossier' : table).pluralize)
column = ActiveRecord::Base.connection.quote_column_name(field['column'])
table + '.' + column
def self.sanitized_column(table, column)
[(table == 'self' ? 'dossier' : table.to_s).pluralize, column]
.map { |name| ActiveRecord::Base.connection.quote_column_name(name) }
.join('.')
end
def dossier_field_service
@dossier_field_service ||= DossierFieldService.new
end
def valid_sort_column?(table, column)
valid_column?(table, column) || EXTRA_SORT_COLUMNS[table]&.include?(column)
end
end

View file

@ -57,11 +57,16 @@
%br
= submit_tag "Ajouter le filtre", class: 'button'
- @current_filters.each do |filter|
%span.filter
= link_to remove_filter_gestionnaire_procedure_path(@procedure, statut: @statut, table: filter['table'], column: filter['column']) do
%img.close-icon{ src: image_url("close.svg") }
= "#{filter['label'].truncate(50)} : #{filter['value']}"
- @current_filters.group_by { |filter| filter['table'] }.each_with_index do |(table, filters), i|
- if i > 0
et
- filters.each_with_index do |filter, i|
- if i > 0
ou
%span.filter
= link_to remove_filter_gestionnaire_procedure_path(@procedure, statut: @statut, table: filter['table'], column: filter['column'], value: filter['value']) do
%img.close-icon{ src: image_url("close.svg") }
= "#{filter['label'].truncate(50)} : #{filter['value']}"
%table.table.dossiers-table.hoverable
%thead
%tr

View file

@ -1,85 +1,66 @@
{
"ignored_warnings": [
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "0d61a1267d264f1e61cc2398a2683703ac60878129dc9515542f246a80ad575b",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/champs/carto/show.js.erb",
"line": 5,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "geo_data((Champ.joins(:dossier).where(:dossiers => ({ :user_id => logged_user_ids })).find_by(:id => params.permit(:champ_id)) or CartoChamp.new))",
"render_path": [{"type":"controller","class":"Champs::CartoController","method":"show","line":48,"file":"app/controllers/champs/carto_controller.rb"}],
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "bd1df30f95135357b646e21a03d95498874faffa32e3804fc643e9b6b957ee14",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/concerns/dossier_filtering_concern.rb",
"line": 18,
"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)",
"render_path": null,
"location": {
"type": "template",
"template": "champs/carto/show"
"type": "method",
"class": "DossierFilteringConcern",
"method": null
},
"user_input": "Champ.joins(:dossier).where(:dossiers => ({ :user_id => logged_user_ids }))",
"confidence": "Weak",
"note": "Not an injection because logged_user_ids have no user input"
"user_input": "values.count",
"confidence": "Medium",
"note": "The table and column are escaped, which should make this safe"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "1840f5340630814ea86311e850ebd91b966e6bccd0b6856133528e7745c0695a",
"fingerprint": "e6f09095e3d381bcf6280d2f9b06c239946be3e440330136934f34611bc2b2d9",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/procedure_presentation.rb",
"line": 90,
"line": 97,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "dossiers.order(\"#{sanitized_column(sort)} #{sort[\"order\"]}\")",
"code": "((\"self\" == \"self\") ? (dossiers) : (dossiers.includes(\"self\"))).order(\"#{self.class.sanitized_column(\"self\", column)} #{order}\")",
"render_path": null,
"location": {
"type": "method",
"class": "ProcedurePresentation",
"method": "sorted_ids"
},
"user_input": "sanitized_column(sort)",
"user_input": "self.class.sanitized_column(\"self\", column)",
"confidence": "Weak",
"note": "Not an injection because of `sanitized_column`"
"note": "`table`, `column` and `order` come from the model, which is validated to prevent injection attacks. Furthermore, `table` and `column` are escaped."
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "b2feda5e5ae668cdbf0653f134c40bcb9e45499c1b607450e43a0166c4098364",
"fingerprint": "f85ed20c14a223884f624d744ff99070f6fc0697d918f54a08e7786ad70bb243",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/procedure_presentation.rb",
"line": 96,
"line": 93,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "dossiers.includes(((\"type_de_champ\" == \"type_de_champ\") ? (:champs) : (:champs_private))).where(\"champs.type_de_champ_id = #{sort[\"column\"].to_i}\").order(\"champs.value #{sort[\"order\"]}\")",
"code": "dossiers.includes(((\"type_de_champ\" == \"type_de_champ\") ? (:champs) : (:champs_private))).where(\"champs.type_de_champ_id = #{column.to_i}\").order(\"champs.value #{order}\")",
"render_path": null,
"location": {
"type": "method",
"class": "ProcedurePresentation",
"method": "sorted_ids"
},
"user_input": "sort[\"order\"]",
"user_input": "order",
"confidence": "Weak",
"note": "Not an injection because `sort[\"order\"]` has passed `check_allowed_sort_order`"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "e0e5b55126891df8fe144835ea99367ffd7a92ae6d7227a923fe79f4a79f67f4",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/procedure_presentation.rb",
"line": 101,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "dossiers.includes(\"user\").order(\"#{sanitized_column(sort)} #{sort[\"order\"]}\")",
"render_path": null,
"location": {
"type": "method",
"class": "ProcedurePresentation",
"method": "sorted_ids"
},
"user_input": "sanitized_column(sort)",
"confidence": "Weak",
"note": "Not an injection because of `sanitized_column`"
"note": "`column` and `order` come from the model, which is validated to prevent injection attacks. Furthermore, the sql injection attack on `column` would need to survive the `to_i`"
}
],
"updated": "2018-10-16 11:28:34 +0300",
"updated": "2019-03-04 11:59:49 +0100",
"brakeman_version": "4.3.1"
}

View file

@ -312,7 +312,7 @@ Rails.application.routes.draw do
patch 'update_displayed_fields'
get 'update_sort/:table/:column' => 'procedures#update_sort', as: 'update_sort'
post 'add_filter'
get 'remove_filter/:statut/:table/:column' => 'procedures#remove_filter', as: 'remove_filter'
get 'remove_filter/:statut/:table/:column/:value' => 'procedures#remove_filter', as: 'remove_filter'
get 'download_dossiers'
resources :dossiers, only: [:show], param: :dossier_id do

View file

@ -7,9 +7,11 @@ feature "procedure filters" do
let!(:new_unfollow_dossier) { create(:dossier, procedure: procedure, state: Dossier.states.fetch(:en_instruction)) }
let!(:champ) { Champ.find_by(type_de_champ_id: type_de_champ.id, dossier_id: new_unfollow_dossier.id) }
let!(:new_unfollow_dossier_2) { create(:dossier, procedure: procedure, state: Dossier.states.fetch(:en_instruction)) }
let!(:champ_2) { Champ.find_by(type_de_champ_id: type_de_champ.id, dossier_id: new_unfollow_dossier_2.id) }
before do
champ.update(value: "Mon champ rempli")
champ_2.update(value: "Mon autre champ rempli différemment")
login_as gestionnaire, scope: :gestionnaire
visit gestionnaire_procedure_path(procedure)
end
@ -66,7 +68,7 @@ feature "procedure filters" do
expect(page).not_to have_link(new_unfollow_dossier_2.user.email)
end
remove_filter
remove_filter(champ.value)
within ".dossiers-table" do
expect(page).to have_link(new_unfollow_dossier.id)
@ -77,8 +79,43 @@ feature "procedure filters" do
end
end
def remove_filter
find(:xpath, "//span[contains(@class, 'filter')]/a").click
scenario "should be able to add and remove two filters for the same field", js: true do
add_filter(type_de_champ.libelle, champ.value)
add_filter(type_de_champ.libelle, champ_2.value)
expect(page).to have_content("#{type_de_champ.libelle} : #{champ.value}")
within ".dossiers-table" do
expect(page).to have_link(new_unfollow_dossier.id, exact: true)
expect(page).to have_link(new_unfollow_dossier.user.email)
expect(page).to have_link(new_unfollow_dossier_2.id, exact: true)
expect(page).to have_link(new_unfollow_dossier_2.user.email)
end
remove_filter(champ.value)
within ".dossiers-table" do
expect(page).not_to have_link(new_unfollow_dossier.id)
expect(page).not_to have_link(new_unfollow_dossier.user.email)
expect(page).to have_link(new_unfollow_dossier_2.id)
expect(page).to have_link(new_unfollow_dossier_2.user.email)
end
remove_filter(champ_2.value)
within ".dossiers-table" do
expect(page).to have_link(new_unfollow_dossier.id)
expect(page).to have_link(new_unfollow_dossier.user.email)
expect(page).to have_link(new_unfollow_dossier_2.id)
expect(page).to have_link(new_unfollow_dossier_2.user.email)
end
end
def remove_filter(filter_value)
find(:xpath, "(//span[contains(@class, 'filter')]/a[contains(@href, '#{URI.encode(filter_value)}')])[1]").click
end
def add_filter(column_name, filter_value)

View file

@ -377,25 +377,28 @@ describe ProcedurePresentation do
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)) }
let(:filter) { [{ 'table' => 'self', 'column' => 'created_at', 'value' => '18/9/2018' }] }
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)) }
let(:filter) { [{ 'table' => 'self', 'column' => 'en_construction_at', 'value' => '17/10/2018' }] }
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) }
let(:filter) { [{ 'table' => 'self', 'column' => 'updated_at', 'value' => '18/9/2018' }] }
before do
kept_dossier.touch(time: Time.zone.local(2018, 9, 18, 14, 28))
@ -406,9 +409,10 @@ describe ProcedurePresentation do
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)) }
let(:filter) { [{ 'table' => 'self', 'column' => 'en_construction_at', 'value' => '17/10/2018 19:30' }] }
it { is_expected.to contain_exactly(kept_dossier.id) }
end
@ -416,6 +420,7 @@ describe ProcedurePresentation do
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
@ -425,13 +430,31 @@ describe ProcedurePresentation do
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
end
context 'for type_de_champ table' do
let(:filter) { [{ 'table' => 'type_de_champ', 'column' => type_de_champ.id.to_s, 'value' => 'keep' }] }
let(:kept_dossier) { create(:dossier, procedure: procedure) }
let(:discarded_dossier) { create(:dossier, procedure: procedure) }
let(:type_de_champ) { procedure.types_de_champ.first }
let(:filter) { [{ 'table' => 'type_de_champ', 'column' => type_de_champ.id.to_s, 'value' => 'keep' }] }
before do
type_de_champ.champ.create(dossier: kept_dossier, value: 'keep me')
@ -439,13 +462,33 @@ describe ProcedurePresentation do
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.id.to_s, 'value' => 'keep' },
{ 'table' => 'type_de_champ', 'column' => type_de_champ.id.to_s, 'value' => 'and' }
]
end
let(:other_kept_dossier) { create(:dossier, procedure: procedure) }
before do
type_de_champ.champ.create(dossier: other_kept_dossier, 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_private table' do
let(:filter) { [{ 'table' => 'type_de_champ_private', 'column' => type_de_champ_private.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.types_de_champ_private.first }
let(:filter) { [{ 'table' => 'type_de_champ_private', 'column' => type_de_champ_private.id.to_s, 'value' => 'keep' }] }
before do
type_de_champ_private.champ.create(dossier: kept_dossier, value: 'keep me')
@ -453,34 +496,101 @@ describe ProcedurePresentation do
end
it { is_expected.to contain_exactly(kept_dossier.id) }
context 'with multiple search values' do
let(:filter) do
[
{ 'table' => 'type_de_champ_private', 'column' => type_de_champ_private.id.to_s, 'value' => 'keep' },
{ 'table' => 'type_de_champ_private', 'column' => type_de_champ_private.id.to_s, 'value' => 'and' }
]
end
let(:other_kept_dossier) { create(:dossier, procedure: procedure) }
before do
type_de_champ_private.champ.create(dossier: other_kept_dossier, 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 etablissement table' do
context 'for entreprise_date_creation column' do
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))) }
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!(: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')) }
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!(: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')) }
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
@ -505,6 +615,109 @@ describe ProcedurePresentation do
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: create(: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
end
describe '#eager_load_displayed_fields' do
let(:procedure_presentation) { ProcedurePresentation.create(assign_to: assign_to, displayed_fields: [{ 'table' => table, 'column' => column }]) }
let!(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
let(:displayed_dossier) { procedure_presentation.eager_load_displayed_fields(procedure.dossiers).first }
context 'for type de champ' do
let(:table) { 'type_de_champ' }
let(:column) { procedure.types_de_champ.first.id }
it 'preloads the champs relation' do
# Ideally, we would only preload the champs for the matching column
expect(displayed_dossier.association(:champs)).to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
end
end
context 'for type de champ private' do
let(:table) { 'type_de_champ_private' }
let(:column) { procedure.types_de_champ_private.first.id }
it 'preloads the champs relation' do
# Ideally, we would only preload the champs for the matching column
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
end
end
context 'for user' do
let(:table) { 'user' }
let(:column) { 'email' }
it 'preloads the user relation' do
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
end
end
context 'for individual' do
let(:table) { 'individual' }
let(:column) { 'nom' }
it 'preloads the individual relation' do
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
end
end
context 'for etablissement' do
let(:table) { 'etablissement' }
let(:column) { 'siret' }
it 'preloads the etablissement relation' do
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).to be_loaded
end
end
context 'for self' do
let(:table) { 'self' }
let(:column) { 'created_at' }
it 'does not preload anything' do
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
end
end
end
end