[#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)
|
||||
|
||||
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)
|
||||
|
|
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
|
||||
include DossierFilteringConcern
|
||||
|
||||
enum state: {
|
||||
brouillon: 'brouillon',
|
||||
en_construction: 'en_construction',
|
||||
|
|
|
@ -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} n’est 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} n’est 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} n’est 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} n’est 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue