[#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:
commit
cccaf54e2e
9 changed files with 399 additions and 176 deletions
|
@ -87,7 +87,7 @@ module NewGestionnaire
|
||||||
|
|
||||||
@dossiers = @dossiers.where(id: filtered_sorted_paginated_ids)
|
@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) }
|
@dossiers = @dossiers.sort_by { |d| filtered_sorted_paginated_ids.index(d.id) }
|
||||||
|
|
||||||
|
@ -102,17 +102,13 @@ module NewGestionnaire
|
||||||
end
|
end
|
||||||
|
|
||||||
fields = values.map do |value|
|
fields = values.map do |value|
|
||||||
table, column = value.split("/")
|
find_field(*value.split('/'))
|
||||||
|
|
||||||
procedure_presentation.fields.find do |field|
|
|
||||||
field['table'] == table && field['column'] == column
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
procedure_presentation.update(displayed_fields: fields)
|
procedure_presentation.update(displayed_fields: fields)
|
||||||
|
|
||||||
current_sort = procedure_presentation.sort
|
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)
|
procedure_presentation.update(sort: Procedure.default_sort)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -145,7 +141,7 @@ module NewGestionnaire
|
||||||
if params[:value].present?
|
if params[:value].present?
|
||||||
filters = procedure_presentation.filters
|
filters = procedure_presentation.filters
|
||||||
table, column = params[:field].split('/')
|
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] << {
|
filters[statut] << {
|
||||||
'label' => label,
|
'label' => label,
|
||||||
|
@ -162,11 +158,9 @@ module NewGestionnaire
|
||||||
|
|
||||||
def remove_filter
|
def remove_filter
|
||||||
filters = procedure_presentation.filters
|
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)
|
procedure_presentation.update(filters: filters)
|
||||||
|
|
||||||
|
@ -194,6 +188,14 @@ module NewGestionnaire
|
||||||
|
|
||||||
private
|
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
|
def statut
|
||||||
@statut ||= (params[:statut].presence || 'a-suivre')
|
@statut ||= (params[:statut].presence || 'a-suivre')
|
||||||
end
|
end
|
||||||
|
@ -228,9 +230,7 @@ module NewGestionnaire
|
||||||
end
|
end
|
||||||
|
|
||||||
def displayed_fields_values
|
def displayed_fields_values
|
||||||
procedure_presentation.displayed_fields.map do |field|
|
procedure_presentation.displayed_fields.map { |field| field_id(field) }
|
||||||
"#{field['table']}/#{field['column']}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_filters
|
def current_filters
|
||||||
|
@ -238,44 +238,7 @@ module NewGestionnaire
|
||||||
end
|
end
|
||||||
|
|
||||||
def available_fields_to_filters
|
def available_fields_to_filters
|
||||||
current_filters_fields_ids = current_filters.map do |field|
|
procedure_presentation.fields_for_select
|
||||||
"#{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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def kaminarize(current_page, total)
|
def kaminarize(current_page, total)
|
||||||
|
|
21
app/models/concerns/dossier_filtering_concern.rb
Normal file
21
app/models/concerns/dossier_filtering_concern.rb
Normal 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
|
|
@ -1,4 +1,6 @@
|
||||||
class Dossier < ApplicationRecord
|
class Dossier < ApplicationRecord
|
||||||
|
include DossierFilteringConcern
|
||||||
|
|
||||||
enum state: {
|
enum state: {
|
||||||
brouillon: 'brouillon',
|
brouillon: 'brouillon',
|
||||||
en_construction: 'en_construction',
|
en_construction: 'en_construction',
|
||||||
|
|
|
@ -74,9 +74,7 @@ class ProcedurePresentation < ApplicationRecord
|
||||||
|
|
||||||
def sorted_ids(dossiers, gestionnaire)
|
def sorted_ids(dossiers, gestionnaire)
|
||||||
dossiers.each { |dossier| assert_matching_procedure(dossier) }
|
dossiers.each { |dossier| assert_matching_procedure(dossier) }
|
||||||
table = sort['table']
|
table, column, order = sort.values_at('table', 'column', 'order')
|
||||||
column = sanitized_column(sort)
|
|
||||||
order = sort['order']
|
|
||||||
|
|
||||||
case table
|
case table
|
||||||
when 'notifications'
|
when 'notifications'
|
||||||
|
@ -88,80 +86,85 @@ class ProcedurePresentation < ApplicationRecord
|
||||||
return (dossiers.order('dossiers.updated_at asc').ids - dossiers_id_with_notification) +
|
return (dossiers.order('dossiers.updated_at asc').ids - dossiers_id_with_notification) +
|
||||||
dossiers_id_with_notification
|
dossiers_id_with_notification
|
||||||
end
|
end
|
||||||
when 'self'
|
|
||||||
return dossiers
|
|
||||||
.order("#{column} #{order}")
|
|
||||||
.pluck(:id)
|
|
||||||
when 'type_de_champ', 'type_de_champ_private'
|
when 'type_de_champ', 'type_de_champ_private'
|
||||||
return dossiers
|
return dossiers
|
||||||
.includes(table == 'type_de_champ' ? :champs : :champs_private)
|
.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}")
|
.order("champs.value #{order}")
|
||||||
.pluck(:id)
|
.pluck(:id)
|
||||||
when 'user', 'individual', 'etablissement'
|
when 'self', 'user', 'individual', 'etablissement'
|
||||||
return dossiers
|
return (table == 'self' ? dossiers : dossiers.includes(table))
|
||||||
.includes(table)
|
.order("#{self.class.sanitized_column(table, column)} #{order}")
|
||||||
.order("#{column} #{order}")
|
|
||||||
.pluck(:id)
|
.pluck(:id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_ids(dossiers, statut)
|
def filtered_ids(dossiers, statut)
|
||||||
dossiers.each { |dossier| assert_matching_procedure(dossier) }
|
dossiers.each { |dossier| assert_matching_procedure(dossier) }
|
||||||
filters[statut].map do |filter|
|
filters[statut].group_by { |filter| filter.values_at('table', 'column') } .map do |(table, column), filters|
|
||||||
table = filter['table']
|
values = filters.pluck('value')
|
||||||
column = sanitized_column(filter)
|
|
||||||
case table
|
case table
|
||||||
when 'self'
|
when 'self'
|
||||||
date = Time.zone.parse(filter['value']).beginning_of_day rescue nil
|
dates = values
|
||||||
if date.present?
|
.map { |v| Time.zone.parse(v).beginning_of_day rescue nil }
|
||||||
dossiers.where("#{column} BETWEEN ? AND ?", date, date + 1.day)
|
.compact
|
||||||
else
|
dossiers.filter_by_datetimes(column, dates)
|
||||||
[]
|
|
||||||
end
|
|
||||||
when 'type_de_champ', 'type_de_champ_private'
|
when 'type_de_champ', 'type_de_champ_private'
|
||||||
relation = table == 'type_de_champ' ? :champs : :champs_private
|
relation = table == 'type_de_champ' ? :champs : :champs_private
|
||||||
dossiers
|
dossiers
|
||||||
.includes(relation)
|
.includes(relation)
|
||||||
.where("champs.type_de_champ_id = ?", filter['column'].to_i)
|
.where("champs.type_de_champ_id = ?", column.to_i)
|
||||||
.where("champs.value ILIKE ?", "%#{filter['value']}%")
|
.filter_ilike(:champ, :value, values)
|
||||||
when 'etablissement'
|
when 'etablissement'
|
||||||
if filter['column'] == 'entreprise_date_creation'
|
if column == 'entreprise_date_creation'
|
||||||
date = filter['value'].to_date rescue nil
|
dates = values
|
||||||
|
.map { |v| v.to_date rescue nil }
|
||||||
|
.compact
|
||||||
dossiers
|
dossiers
|
||||||
.includes(table)
|
.includes(table)
|
||||||
.where("#{column} = ?", date)
|
.where(table.pluralize => { column => dates })
|
||||||
else
|
else
|
||||||
dossiers
|
dossiers
|
||||||
.includes(table)
|
.includes(table)
|
||||||
.where("#{column} ILIKE ?", "%#{filter['value']}%")
|
.filter_ilike(table, column, values)
|
||||||
end
|
end
|
||||||
when 'user', 'individual'
|
when 'user', 'individual'
|
||||||
dossiers
|
dossiers
|
||||||
.includes(table)
|
.includes(table)
|
||||||
.where("#{column} ILIKE ?", "%#{filter['value']}%")
|
.filter_ilike(table, column, values)
|
||||||
end.pluck(:id)
|
end.pluck(:id)
|
||||||
end.reduce(:&)
|
end.reduce(:&)
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def check_allowed_displayed_fields
|
def check_allowed_displayed_fields
|
||||||
displayed_fields.each do |field|
|
displayed_fields.each do |field|
|
||||||
table = field['table']
|
check_allowed_field(:displayed_fields, field)
|
||||||
column = field['column']
|
|
||||||
if !valid_column?(table, column)
|
|
||||||
errors.add(:filters, "#{table}.#{column} n’est pas une colonne permise")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_allowed_sort_column
|
def check_allowed_sort_column
|
||||||
table = sort['table']
|
check_allowed_field(:sort, sort, EXTRA_SORT_COLUMNS)
|
||||||
column = sort['column']
|
|
||||||
if !valid_sort_column?(table, column)
|
|
||||||
errors.add(:sort, "#{table}.#{column} n’est pas une colonne permise")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_allowed_sort_order
|
def check_allowed_sort_order
|
||||||
|
@ -174,15 +177,18 @@ class ProcedurePresentation < ApplicationRecord
|
||||||
def check_allowed_filter_columns
|
def check_allowed_filter_columns
|
||||||
filters.each do |_, columns|
|
filters.each do |_, columns|
|
||||||
columns.each do |column|
|
columns.each do |column|
|
||||||
table = column['table']
|
check_allowed_field(:filters, column)
|
||||||
column = column['column']
|
|
||||||
if !valid_column?(table, column)
|
|
||||||
errors.add(:filters, "#{table}.#{column} n’est pas une colonne permise")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
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 assert_matching_procedure(dossier)
|
def assert_matching_procedure(dossier)
|
||||||
if dossier.procedure != procedure
|
if dossier.procedure != procedure
|
||||||
raise "Procedure mismatch (expected #{procedure.id}, got #{dossier.procedure.id})"
|
raise "Procedure mismatch (expected #{procedure.id}, got #{dossier.procedure.id})"
|
||||||
|
@ -210,32 +216,27 @@ class ProcedurePresentation < ApplicationRecord
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_column?(table, column)
|
def valid_column?(table, column, extra_columns = {})
|
||||||
valid_columns_for_table(table).include?(column)
|
valid_columns_for_table(table).include?(column) ||
|
||||||
|
extra_columns[table]&.include?(column)
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_columns_for_table(table)
|
def valid_columns_for_table(table)
|
||||||
@column_whitelist ||= fields
|
@column_whitelist ||= fields
|
||||||
.group_by { |field| field['table'] }
|
.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
|
.to_h
|
||||||
|
|
||||||
@column_whitelist[table] || []
|
@column_whitelist[table] || []
|
||||||
end
|
end
|
||||||
|
|
||||||
def sanitized_column(field)
|
def self.sanitized_column(table, column)
|
||||||
table = field['table']
|
[(table == 'self' ? 'dossier' : table.to_s).pluralize, column]
|
||||||
table = ActiveRecord::Base.connection.quote_column_name((table == 'self' ? 'dossier' : table).pluralize)
|
.map { |name| ActiveRecord::Base.connection.quote_column_name(name) }
|
||||||
column = ActiveRecord::Base.connection.quote_column_name(field['column'])
|
.join('.')
|
||||||
|
|
||||||
table + '.' + column
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def dossier_field_service
|
def dossier_field_service
|
||||||
@dossier_field_service ||= DossierFieldService.new
|
@dossier_field_service ||= DossierFieldService.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_sort_column?(table, column)
|
|
||||||
valid_column?(table, column) || EXTRA_SORT_COLUMNS[table]&.include?(column)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,11 +57,16 @@
|
||||||
%br
|
%br
|
||||||
= submit_tag "Ajouter le filtre", class: 'button'
|
= submit_tag "Ajouter le filtre", class: 'button'
|
||||||
|
|
||||||
- @current_filters.each do |filter|
|
- @current_filters.group_by { |filter| filter['table'] }.each_with_index do |(table, filters), i|
|
||||||
%span.filter
|
- if i > 0
|
||||||
= link_to remove_filter_gestionnaire_procedure_path(@procedure, statut: @statut, table: filter['table'], column: filter['column']) do
|
et
|
||||||
%img.close-icon{ src: image_url("close.svg") }
|
- filters.each_with_index do |filter, i|
|
||||||
= "#{filter['label'].truncate(50)} : #{filter['value']}"
|
- 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
|
%table.table.dossiers-table.hoverable
|
||||||
%thead
|
%thead
|
||||||
%tr
|
%tr
|
||||||
|
|
|
@ -1,85 +1,66 @@
|
||||||
{
|
{
|
||||||
"ignored_warnings": [
|
"ignored_warnings": [
|
||||||
{
|
{
|
||||||
"warning_type": "Cross-Site Scripting",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 2,
|
"warning_code": 0,
|
||||||
"fingerprint": "0d61a1267d264f1e61cc2398a2683703ac60878129dc9515542f246a80ad575b",
|
"fingerprint": "bd1df30f95135357b646e21a03d95498874faffa32e3804fc643e9b6b957ee14",
|
||||||
"check_name": "CrossSiteScripting",
|
"check_name": "SQL",
|
||||||
"message": "Unescaped model attribute",
|
"message": "Possible SQL injection",
|
||||||
"file": "app/views/champs/carto/show.js.erb",
|
"file": "app/models/concerns/dossier_filtering_concern.rb",
|
||||||
"line": 5,
|
"line": 18,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||||
"code": "geo_data((Champ.joins(:dossier).where(:dossiers => ({ :user_id => logged_user_ids })).find_by(:id => params.permit(:champ_id)) or CartoChamp.new))",
|
"code": "where(\"#{values.count} OR #{\"(#{ProcedurePresentation.sanitized_column(table, column)} ILIKE ?)\"}\", *values.map do\n \"%#{value}%\"\n end)",
|
||||||
"render_path": [{"type":"controller","class":"Champs::CartoController","method":"show","line":48,"file":"app/controllers/champs/carto_controller.rb"}],
|
"render_path": null,
|
||||||
"location": {
|
"location": {
|
||||||
"type": "template",
|
"type": "method",
|
||||||
"template": "champs/carto/show"
|
"class": "DossierFilteringConcern",
|
||||||
|
"method": null
|
||||||
},
|
},
|
||||||
"user_input": "Champ.joins(:dossier).where(:dossiers => ({ :user_id => logged_user_ids }))",
|
"user_input": "values.count",
|
||||||
"confidence": "Weak",
|
"confidence": "Medium",
|
||||||
"note": "Not an injection because logged_user_ids have no user input"
|
"note": "The table and column are escaped, which should make this safe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"warning_type": "SQL Injection",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 0,
|
"warning_code": 0,
|
||||||
"fingerprint": "1840f5340630814ea86311e850ebd91b966e6bccd0b6856133528e7745c0695a",
|
"fingerprint": "e6f09095e3d381bcf6280d2f9b06c239946be3e440330136934f34611bc2b2d9",
|
||||||
"check_name": "SQL",
|
"check_name": "SQL",
|
||||||
"message": "Possible SQL injection",
|
"message": "Possible SQL injection",
|
||||||
"file": "app/models/procedure_presentation.rb",
|
"file": "app/models/procedure_presentation.rb",
|
||||||
"line": 90,
|
"line": 97,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
"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,
|
"render_path": null,
|
||||||
"location": {
|
"location": {
|
||||||
"type": "method",
|
"type": "method",
|
||||||
"class": "ProcedurePresentation",
|
"class": "ProcedurePresentation",
|
||||||
"method": "sorted_ids"
|
"method": "sorted_ids"
|
||||||
},
|
},
|
||||||
"user_input": "sanitized_column(sort)",
|
"user_input": "self.class.sanitized_column(\"self\", column)",
|
||||||
"confidence": "Weak",
|
"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_type": "SQL Injection",
|
||||||
"warning_code": 0,
|
"warning_code": 0,
|
||||||
"fingerprint": "b2feda5e5ae668cdbf0653f134c40bcb9e45499c1b607450e43a0166c4098364",
|
"fingerprint": "f85ed20c14a223884f624d744ff99070f6fc0697d918f54a08e7786ad70bb243",
|
||||||
"check_name": "SQL",
|
"check_name": "SQL",
|
||||||
"message": "Possible SQL injection",
|
"message": "Possible SQL injection",
|
||||||
"file": "app/models/procedure_presentation.rb",
|
"file": "app/models/procedure_presentation.rb",
|
||||||
"line": 96,
|
"line": 93,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
"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,
|
"render_path": null,
|
||||||
"location": {
|
"location": {
|
||||||
"type": "method",
|
"type": "method",
|
||||||
"class": "ProcedurePresentation",
|
"class": "ProcedurePresentation",
|
||||||
"method": "sorted_ids"
|
"method": "sorted_ids"
|
||||||
},
|
},
|
||||||
"user_input": "sort[\"order\"]",
|
"user_input": "order",
|
||||||
"confidence": "Weak",
|
"confidence": "Weak",
|
||||||
"note": "Not an injection because `sort[\"order\"]` has passed `check_allowed_sort_order`"
|
"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`"
|
||||||
},
|
|
||||||
{
|
|
||||||
"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`"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated": "2018-10-16 11:28:34 +0300",
|
"updated": "2019-03-04 11:59:49 +0100",
|
||||||
"brakeman_version": "4.3.1"
|
"brakeman_version": "4.3.1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -312,7 +312,7 @@ Rails.application.routes.draw do
|
||||||
patch 'update_displayed_fields'
|
patch 'update_displayed_fields'
|
||||||
get 'update_sort/:table/:column' => 'procedures#update_sort', as: 'update_sort'
|
get 'update_sort/:table/:column' => 'procedures#update_sort', as: 'update_sort'
|
||||||
post 'add_filter'
|
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'
|
get 'download_dossiers'
|
||||||
|
|
||||||
resources :dossiers, only: [:show], param: :dossier_id do
|
resources :dossiers, only: [:show], param: :dossier_id do
|
||||||
|
|
|
@ -7,9 +7,11 @@ feature "procedure filters" do
|
||||||
let!(:new_unfollow_dossier) { create(:dossier, procedure: procedure, state: Dossier.states.fetch(:en_instruction)) }
|
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!(: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!(: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
|
before do
|
||||||
champ.update(value: "Mon champ rempli")
|
champ.update(value: "Mon champ rempli")
|
||||||
|
champ_2.update(value: "Mon autre champ rempli différemment")
|
||||||
login_as gestionnaire, scope: :gestionnaire
|
login_as gestionnaire, scope: :gestionnaire
|
||||||
visit gestionnaire_procedure_path(procedure)
|
visit gestionnaire_procedure_path(procedure)
|
||||||
end
|
end
|
||||||
|
@ -66,7 +68,7 @@ feature "procedure filters" do
|
||||||
expect(page).not_to have_link(new_unfollow_dossier_2.user.email)
|
expect(page).not_to have_link(new_unfollow_dossier_2.user.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
remove_filter
|
remove_filter(champ.value)
|
||||||
|
|
||||||
within ".dossiers-table" do
|
within ".dossiers-table" do
|
||||||
expect(page).to have_link(new_unfollow_dossier.id)
|
expect(page).to have_link(new_unfollow_dossier.id)
|
||||||
|
@ -77,8 +79,43 @@ feature "procedure filters" do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_filter
|
scenario "should be able to add and remove two filters for the same field", js: true do
|
||||||
find(:xpath, "//span[contains(@class, 'filter')]/a").click
|
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
|
end
|
||||||
|
|
||||||
def add_filter(column_name, filter_value)
|
def add_filter(column_name, filter_value)
|
||||||
|
|
|
@ -377,25 +377,28 @@ describe ProcedurePresentation do
|
||||||
|
|
||||||
context 'for self table' do
|
context 'for self table' do
|
||||||
context 'for created_at column' 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!(: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!(: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) }
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for en_construction_at column' do
|
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!(: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!(: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) }
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for updated_at column' do
|
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(:kept_dossier) { create(:dossier, procedure: procedure) }
|
||||||
let(:discarded_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
|
before do
|
||||||
kept_dossier.touch(time: Time.zone.local(2018, 9, 18, 14, 28))
|
kept_dossier.touch(time: Time.zone.local(2018, 9, 18, 14, 28))
|
||||||
|
@ -406,9 +409,10 @@ describe ProcedurePresentation do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'ignore time of day' do
|
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!(: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!(: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) }
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
end
|
end
|
||||||
|
@ -416,6 +420,7 @@ describe ProcedurePresentation do
|
||||||
context 'for a malformed date' do
|
context 'for a malformed date' do
|
||||||
context 'when its a string' do
|
context 'when its a string' do
|
||||||
let(:filter) { [{ 'table' => 'self', 'column' => 'updated_at', 'value' => 'malformed date' }] }
|
let(:filter) { [{ 'table' => 'self', 'column' => 'updated_at', 'value' => 'malformed date' }] }
|
||||||
|
|
||||||
it { is_expected.to match([]) }
|
it { is_expected.to match([]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -425,13 +430,31 @@ describe ProcedurePresentation do
|
||||||
it { is_expected.to match([]) }
|
it { is_expected.to match([]) }
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
context 'for type_de_champ table' do
|
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(:kept_dossier) { create(:dossier, procedure: procedure) }
|
||||||
let(:discarded_dossier) { create(:dossier, procedure: procedure) }
|
let(:discarded_dossier) { create(:dossier, procedure: procedure) }
|
||||||
let(:type_de_champ) { procedure.types_de_champ.first }
|
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
|
before do
|
||||||
type_de_champ.champ.create(dossier: kept_dossier, value: 'keep me')
|
type_de_champ.champ.create(dossier: kept_dossier, value: 'keep me')
|
||||||
|
@ -439,13 +462,33 @@ describe ProcedurePresentation do
|
||||||
end
|
end
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
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
|
end
|
||||||
|
|
||||||
context 'for type_de_champ_private table' do
|
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(:kept_dossier) { create(:dossier, procedure: procedure) }
|
||||||
let(:discarded_dossier) { create(:dossier, procedure: procedure) }
|
let(:discarded_dossier) { create(:dossier, procedure: procedure) }
|
||||||
let(:type_de_champ_private) { procedure.types_de_champ_private.first }
|
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
|
before do
|
||||||
type_de_champ_private.champ.create(dossier: kept_dossier, value: 'keep me')
|
type_de_champ_private.champ.create(dossier: kept_dossier, value: 'keep me')
|
||||||
|
@ -453,34 +496,101 @@ describe ProcedurePresentation do
|
||||||
end
|
end
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
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
|
end
|
||||||
|
|
||||||
context 'for etablissement table' do
|
context 'for etablissement table' do
|
||||||
context 'for entreprise_date_creation column' 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(: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) }
|
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
|
end
|
||||||
|
|
||||||
context 'for code_postal column' do
|
context 'for code_postal column' do
|
||||||
# All columns except entreprise_date_creation work exacly the same, just testing one
|
# 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(: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) }
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for user table' do
|
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(: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) }
|
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
|
end
|
||||||
|
|
||||||
context 'for individual table' do
|
context 'for individual table' do
|
||||||
|
@ -505,6 +615,109 @@ describe ProcedurePresentation do
|
||||||
|
|
||||||
it { is_expected.to contain_exactly(kept_dossier.id) }
|
it { is_expected.to contain_exactly(kept_dossier.id) }
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue