Merge pull request #11005 from demarches-simplifiees/work_on_json_path_column
ETQ Instructeur, je peux voir et filtrer sur les données retournées par API
This commit is contained in:
commit
4f0713a764
20 changed files with 447 additions and 287 deletions
|
@ -58,12 +58,11 @@ class Instructeurs::ColumnFilterValueComponent < ApplicationComponent
|
||||||
[_1.name, _1.id]
|
[_1.name, _1.id]
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
find_type_de_champ(column.column).options_for_select(column)
|
find_type_de_champ(column.stable_id).options_for_select(column)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_type_de_champ(column)
|
def find_type_de_champ(stable_id)
|
||||||
stable_id = column.to_s.split('->').first
|
|
||||||
TypeDeChamp
|
TypeDeChamp
|
||||||
.joins(:revision_types_de_champ)
|
.joins(:revision_types_de_champ)
|
||||||
.where(revision_types_de_champ: { revision_id: ProcedureRevision.where(procedure_id:) })
|
.where(revision_types_de_champ: { revision_id: ProcedureRevision.where(procedure_id:) })
|
||||||
|
|
|
@ -49,7 +49,7 @@ class Instructeurs::FilterButtonsComponent < ApplicationComponent
|
||||||
column, filter = filter_column.column, filter_column.filter
|
column, filter = filter_column.column, filter_column.filter
|
||||||
|
|
||||||
if column.type_de_champ?
|
if column.type_de_champ?
|
||||||
find_type_de_champ(column.column).dynamic_type.filter_to_human(filter)
|
find_type_de_champ(column.stable_id).dynamic_type.filter_to_human(filter)
|
||||||
elsif column.dossier_state?
|
elsif column.dossier_state?
|
||||||
if filter == 'pending_correction'
|
if filter == 'pending_correction'
|
||||||
Dossier.human_attribute_name("pending_correction.for_instructeur")
|
Dossier.human_attribute_name("pending_correction.for_instructeur")
|
||||||
|
@ -68,9 +68,7 @@ class Instructeurs::FilterButtonsComponent < ApplicationComponent
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_type_de_champ(column)
|
def find_type_de_champ(stable_id)
|
||||||
stable_id = column.to_s.split('->').first
|
|
||||||
|
|
||||||
TypeDeChamp
|
TypeDeChamp
|
||||||
.joins(:revision_types_de_champ)
|
.joins(:revision_types_de_champ)
|
||||||
.where(revision_types_de_champ: { revision_id: @procedure_presentation.procedure.revisions })
|
.where(revision_types_de_champ: { revision_id: @procedure_presentation.procedure.revisions })
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Column
|
||||||
|
|
||||||
# the h_id is a Hash and hold enough information to find the column
|
# the h_id is a Hash and hold enough information to find the column
|
||||||
# in the ColumnType class, aka be able to do the h_id -> column conversion
|
# in the ColumnType class, aka be able to do the h_id -> column conversion
|
||||||
def h_id = { procedure_id: @procedure_id, column_id: "#{table}/#{column}" }
|
def h_id = { procedure_id: @procedure_id, column_id: }
|
||||||
|
|
||||||
def ==(other) = h_id == other.h_id # using h_id instead of id to avoid inversion of keys
|
def ==(other) = h_id == other.h_id # using h_id instead of id to avoid inversion of keys
|
||||||
|
|
||||||
|
@ -53,94 +53,7 @@ class Column
|
||||||
procedure.find_column(h_id: h_id)
|
procedure.find_column(h_id: h_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def value(champ)
|
|
||||||
return if champ.nil?
|
|
||||||
|
|
||||||
value = typed_value(champ)
|
|
||||||
if default_column?
|
|
||||||
cast_value(value, from_type: champ.last_write_column_type, to_type: type)
|
|
||||||
else
|
|
||||||
value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def typed_value(champ)
|
def column_id = "#{table}/#{column}"
|
||||||
value = string_value(champ)
|
|
||||||
parse_value(value, type: champ.last_write_column_type)
|
|
||||||
end
|
|
||||||
|
|
||||||
def string_value(champ) = champ.public_send(value_column)
|
|
||||||
def default_column? = value_column.in?([:value, :external_id])
|
|
||||||
|
|
||||||
def parse_value(value, type:)
|
|
||||||
return if value.blank?
|
|
||||||
|
|
||||||
case type
|
|
||||||
when :boolean
|
|
||||||
parse_boolean(value)
|
|
||||||
when :integer
|
|
||||||
value.to_i
|
|
||||||
when :decimal
|
|
||||||
value.to_f
|
|
||||||
when :datetime
|
|
||||||
parse_datetime(value)
|
|
||||||
when :date
|
|
||||||
parse_datetime(value)&.to_date
|
|
||||||
when :enums
|
|
||||||
parse_enums(value)
|
|
||||||
else
|
|
||||||
value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def cast_value(value, from_type:, to_type:)
|
|
||||||
return if value.blank?
|
|
||||||
return value if from_type == to_type
|
|
||||||
|
|
||||||
case [from_type, to_type]
|
|
||||||
when [:integer, :decimal] # recast numbers automatically
|
|
||||||
value.to_f
|
|
||||||
when [:decimal, :integer] # may lose some data, but who cares ?
|
|
||||||
value.to_i
|
|
||||||
when [:integer, :text], [:decimal, :text] # number to text
|
|
||||||
value.to_s
|
|
||||||
when [:enum, :enums] # single list can become multi
|
|
||||||
[value]
|
|
||||||
when [:enum, :text] # single list can become text
|
|
||||||
value
|
|
||||||
when [:enums, :enum] # multi list can become single list
|
|
||||||
value.first
|
|
||||||
when [:enums, :text] # multi list can become text
|
|
||||||
value.join(', ')
|
|
||||||
when [:date, :datetime] # date <=> datetime
|
|
||||||
value.to_datetime
|
|
||||||
when [:datetime, :date] # may lose some data, but who cares ?
|
|
||||||
value.to_date
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_boolean(value)
|
|
||||||
case value
|
|
||||||
when 'true', 'on', '1'
|
|
||||||
true
|
|
||||||
when 'false'
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_enums(value)
|
|
||||||
JSON.parse(value)
|
|
||||||
rescue JSON::ParserError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_datetime(value)
|
|
||||||
Time.zone.parse(value)
|
|
||||||
rescue ArgumentError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
103
app/models/columns/champ_column.rb
Normal file
103
app/models/columns/champ_column.rb
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Columns::ChampColumn < Column
|
||||||
|
attr_reader :stable_id
|
||||||
|
|
||||||
|
def initialize(procedure_id:, label:, stable_id:, tdc_type:, displayable: true, filterable: true, type: :text, value_column: :value)
|
||||||
|
@stable_id = stable_id
|
||||||
|
@tdc_type = tdc_type
|
||||||
|
|
||||||
|
super(
|
||||||
|
procedure_id:,
|
||||||
|
table: 'type_de_champ',
|
||||||
|
column: stable_id.to_s,
|
||||||
|
label:,
|
||||||
|
type:,
|
||||||
|
value_column:,
|
||||||
|
displayable:,
|
||||||
|
filterable:
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(champ)
|
||||||
|
return if champ.nil?
|
||||||
|
|
||||||
|
# nominal case
|
||||||
|
if @tdc_type == champ.last_write_type_champ
|
||||||
|
typed_value(champ)
|
||||||
|
else
|
||||||
|
cast_value(champ)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def column_id = "type_de_champ/#{stable_id}"
|
||||||
|
|
||||||
|
def string_value(champ) = champ.public_send(value_column)
|
||||||
|
|
||||||
|
def typed_value(champ)
|
||||||
|
value = string_value(champ)
|
||||||
|
|
||||||
|
return if value.blank?
|
||||||
|
|
||||||
|
case type
|
||||||
|
when :boolean
|
||||||
|
parse_boolean(value)
|
||||||
|
when :integer
|
||||||
|
value.to_i
|
||||||
|
when :decimal
|
||||||
|
value.to_f
|
||||||
|
when :datetime
|
||||||
|
parse_datetime(value)
|
||||||
|
when :date
|
||||||
|
parse_datetime(value)&.to_date
|
||||||
|
when :enums
|
||||||
|
parse_enums(value)
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_value(champ)
|
||||||
|
value = string_value(champ)
|
||||||
|
|
||||||
|
return if value.blank?
|
||||||
|
|
||||||
|
case [champ.last_write_type_champ, @tdc_type]
|
||||||
|
when ['integer_number', 'decimal_number'] # recast numbers automatically
|
||||||
|
value.to_f
|
||||||
|
when ['decimal_number', 'integer_number'] # may lose some data, but who cares ?
|
||||||
|
value.to_i
|
||||||
|
when ['integer_number', 'text'], ['decimal_number', 'text'] # number to text
|
||||||
|
value
|
||||||
|
when ['drop_down_list', 'multiple_drop_down_list'] # single list can become multi
|
||||||
|
[value]
|
||||||
|
when ['drop_down_list', 'text'] # single list can become text
|
||||||
|
value
|
||||||
|
when ['multiple_drop_down_list', 'drop_down_list'] # multi list can become single
|
||||||
|
parse_enums(value).first
|
||||||
|
when ['multiple_drop_down_list', 'text'] # single list can become text
|
||||||
|
parse_enums(value).join(', ')
|
||||||
|
when ['date', 'datetime'] # date <=> datetime
|
||||||
|
parse_datetime(value)&.to_datetime
|
||||||
|
when ['datetime', 'date'] # may lose some data, but who cares ?
|
||||||
|
parse_datetime(value)&.to_date
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_boolean(value)
|
||||||
|
case value
|
||||||
|
when 'true', 'on', '1'
|
||||||
|
true
|
||||||
|
when 'false'
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_enums(value) = JSON.parse(value) rescue nil
|
||||||
|
|
||||||
|
def parse_datetime(value) = Time.zone.parse(value) rescue nil
|
||||||
|
end
|
|
@ -1,19 +1,33 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Columns::JSONPathColumn < Column
|
class Columns::JSONPathColumn < Columns::ChampColumn
|
||||||
def column
|
attr_reader :jsonpath
|
||||||
"#{@column}->#{value_column}" # override column otherwise json path facets will have same id as other
|
|
||||||
|
def initialize(procedure_id:, label:, stable_id:, tdc_type:, jsonpath:, displayable:, type: :text)
|
||||||
|
@jsonpath = quote_string(jsonpath)
|
||||||
|
|
||||||
|
super(
|
||||||
|
procedure_id:,
|
||||||
|
label:,
|
||||||
|
stable_id:,
|
||||||
|
tdc_type:,
|
||||||
|
displayable:,
|
||||||
|
type:
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_ids(dossiers, search_occurences)
|
def filtered_ids(dossiers, search_terms)
|
||||||
queries = Array.new(search_occurences.count, "(#{json_path_query_part} ILIKE ?)").join(' OR ')
|
value = quote_string(search_terms.join('|'))
|
||||||
|
|
||||||
|
condition = %{champs.value_json @? '#{jsonpath} ? (@ like_regex "#{value}" flag "i")'}
|
||||||
|
|
||||||
dossiers.with_type_de_champ(stable_id)
|
dossiers.with_type_de_champ(stable_id)
|
||||||
.where(queries, *(search_occurences.map { |value| "%#{value}%" }))
|
.where(condition)
|
||||||
.ids
|
.ids
|
||||||
end
|
end
|
||||||
|
|
||||||
def options_for_select
|
def options_for_select
|
||||||
case value_column.last
|
case jsonpath.split('.').last
|
||||||
when 'departement_code'
|
when 'departement_code'
|
||||||
APIGeoService.departements.map { ["#{_1[:code]} – #{_1[:name]}", _1[:code]] }
|
APIGeoService.departements.map { ["#{_1[:code]} – #{_1[:name]}", _1[:code]] }
|
||||||
when 'region_name'
|
when 'region_name'
|
||||||
|
@ -25,34 +39,11 @@ class Columns::JSONPathColumn < Column
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def column_id = "type_de_champ/#{stable_id}-#{jsonpath}"
|
||||||
|
|
||||||
def typed_value(champ)
|
def typed_value(champ)
|
||||||
champ.value_json&.dig(*value_column)
|
champ.value_json&.dig(*jsonpath.split('.')[1..])
|
||||||
end
|
end
|
||||||
|
|
||||||
def stable_id
|
def quote_string(string) = ActiveRecord::Base.connection.quote_string(string)
|
||||||
@column
|
|
||||||
end
|
|
||||||
|
|
||||||
# given a value_column as ['value_json', 'address', 'postal_code']
|
|
||||||
# build SQL query as 'champs'.'value_json'->'address'->>'postal_code'
|
|
||||||
# see: https://www.postgresql.org/docs/9.5/functions-json.html
|
|
||||||
def json_path_query_part
|
|
||||||
*json_segments, key = value_column
|
|
||||||
|
|
||||||
if json_segments.blank? # not nested, only access using ->> Get JSON array element as text
|
|
||||||
"#{quote_table_column('champs')}.#{quote_table_column('value_json')}->>#{quote_json_segment(key)}"
|
|
||||||
else # nested, have to dig in json using -> Get JSON object field by key
|
|
||||||
field_accessor = json_segments.map(&method(:quote_json_segment)).join('->')
|
|
||||||
|
|
||||||
"#{quote_table_column('champs')}.#{quote_table_column('value_json')}->#{field_accessor}->>#{quote_json_segment(key)}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def quote_table_column(table_or_column)
|
|
||||||
ActiveRecord::Base.connection.quote_column_name(table_or_column)
|
|
||||||
end
|
|
||||||
|
|
||||||
def quote_json_segment(path)
|
|
||||||
"'#{path}'"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Columns::LinkedDropDownColumn < Column
|
class Columns::LinkedDropDownColumn < Columns::ChampColumn
|
||||||
def column
|
attr_reader :path
|
||||||
return super if default_column?
|
|
||||||
"#{@column}->#{value_column}" # override column otherwise json path facets will have same id as other
|
def initialize(procedure_id:, label:, stable_id:, tdc_type:, path:, displayable:, type: :text)
|
||||||
|
@path = path
|
||||||
|
|
||||||
|
super(
|
||||||
|
procedure_id:,
|
||||||
|
label:,
|
||||||
|
stable_id:,
|
||||||
|
tdc_type:,
|
||||||
|
displayable:,
|
||||||
|
type:
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_ids(dossiers, values)
|
def filtered_ids(dossiers, values)
|
||||||
|
@ -14,11 +24,13 @@ class Columns::LinkedDropDownColumn < Column
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def column_id = "type_de_champ/#{stable_id}->#{path}"
|
||||||
|
|
||||||
def typed_value(champ)
|
def typed_value(champ)
|
||||||
return nil if default_column?
|
return nil if path == :value
|
||||||
|
|
||||||
primary_value, secondary_value = unpack_values(champ.value)
|
primary_value, secondary_value = unpack_values(champ.value)
|
||||||
case value_column
|
case path
|
||||||
when :primary
|
when :primary
|
||||||
primary_value
|
primary_value
|
||||||
when :secondary
|
when :secondary
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Columns::TitreIdentiteColumn < Column
|
class Columns::TitreIdentiteColumn < Columns::ChampColumn
|
||||||
private
|
private
|
||||||
|
|
||||||
def typed_value(champ)
|
def typed_value(champ)
|
||||||
|
|
|
@ -6,19 +6,19 @@ module AddressableColumnConcern
|
||||||
included do
|
included do
|
||||||
def columns(procedure_id:, displayable: true, prefix: nil)
|
def columns(procedure_id:, displayable: true, prefix: nil)
|
||||||
super.concat([
|
super.concat([
|
||||||
["code postal (5 chiffres)", ['postal_code'], :text],
|
["code postal (5 chiffres)", '$.postal_code', :text],
|
||||||
["commune", ['city_name'], :text],
|
["commune", '$.city_name', :text],
|
||||||
["département", ['departement_code'], :enum],
|
["département", '$.departement_code', :enum],
|
||||||
["region", ['region_name'], :enum]
|
["region", '$.region_name', :enum]
|
||||||
].map do |(label, value_column, type)|
|
].map do |(label, jsonpath, type)|
|
||||||
Columns::JSONPathColumn.new(
|
Columns::JSONPathColumn.new(
|
||||||
procedure_id:,
|
procedure_id:,
|
||||||
table: Column::TYPE_DE_CHAMP_TABLE,
|
stable_id:,
|
||||||
column: stable_id,
|
tdc_type: type_champ,
|
||||||
label: "#{libelle_with_prefix(prefix)} – #{label}",
|
label: "#{libelle_with_prefix(prefix)} – #{label}",
|
||||||
displayable: false,
|
jsonpath:,
|
||||||
type:,
|
displayable:,
|
||||||
value_column:
|
type:
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
@ -561,7 +561,7 @@ class TypeDeChamp < ApplicationRecord
|
||||||
elsif region?
|
elsif region?
|
||||||
APIGeoService.regions.map { [_1[:name], _1[:code]] }
|
APIGeoService.regions.map { [_1[:name], _1[:code]] }
|
||||||
elsif linked_drop_down_list?
|
elsif linked_drop_down_list?
|
||||||
if column.value_column == :primary
|
if column.path == :primary
|
||||||
primary_options
|
primary_options
|
||||||
else
|
else
|
||||||
secondary_options.values.flatten
|
secondary_options.values.flatten
|
||||||
|
|
|
@ -75,29 +75,29 @@ class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBas
|
||||||
[
|
[
|
||||||
Columns::LinkedDropDownColumn.new(
|
Columns::LinkedDropDownColumn.new(
|
||||||
procedure_id:,
|
procedure_id:,
|
||||||
table: Column::TYPE_DE_CHAMP_TABLE,
|
|
||||||
column: stable_id.to_s,
|
|
||||||
label: libelle_with_prefix(prefix),
|
label: libelle_with_prefix(prefix),
|
||||||
|
stable_id:,
|
||||||
|
tdc_type: type_champ,
|
||||||
type: :text,
|
type: :text,
|
||||||
value_column: :value,
|
path: :value,
|
||||||
displayable:
|
displayable:
|
||||||
),
|
),
|
||||||
Columns::LinkedDropDownColumn.new(
|
Columns::LinkedDropDownColumn.new(
|
||||||
procedure_id:,
|
procedure_id:,
|
||||||
table: Column::TYPE_DE_CHAMP_TABLE,
|
stable_id:,
|
||||||
column: stable_id.to_s,
|
tdc_type: type_champ,
|
||||||
label: "#{libelle_with_prefix(prefix)} (Primaire)",
|
label: "#{libelle_with_prefix(prefix)} (Primaire)",
|
||||||
type: :enum,
|
type: :enum,
|
||||||
value_column: :primary,
|
path: :primary,
|
||||||
displayable: false
|
displayable: false
|
||||||
),
|
),
|
||||||
Columns::LinkedDropDownColumn.new(
|
Columns::LinkedDropDownColumn.new(
|
||||||
procedure_id:,
|
procedure_id:,
|
||||||
table: Column::TYPE_DE_CHAMP_TABLE,
|
stable_id:,
|
||||||
column: stable_id.to_s,
|
tdc_type: type_champ,
|
||||||
label: "#{libelle_with_prefix(prefix)} (Secondaire)",
|
label: "#{libelle_with_prefix(prefix)} (Secondaire)",
|
||||||
type: :enum,
|
type: :enum,
|
||||||
value_column: :secondary,
|
path: :secondary,
|
||||||
displayable: false
|
displayable: false
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -28,8 +28,8 @@ class TypesDeChamp::TitreIdentiteTypeDeChamp < TypesDeChamp::TypeDeChampBase
|
||||||
[
|
[
|
||||||
Columns::TitreIdentiteColumn.new(
|
Columns::TitreIdentiteColumn.new(
|
||||||
procedure_id:,
|
procedure_id:,
|
||||||
table: Column::TYPE_DE_CHAMP_TABLE,
|
stable_id:,
|
||||||
column: stable_id.to_s,
|
tdc_type: type_champ,
|
||||||
label: libelle_with_prefix(prefix),
|
label: libelle_with_prefix(prefix),
|
||||||
type: TypeDeChamp.column_type(type_champ),
|
type: TypeDeChamp.column_type(type_champ),
|
||||||
value_column: TypeDeChamp.value_column(type_champ),
|
value_column: TypeDeChamp.value_column(type_champ),
|
||||||
|
|
|
@ -98,10 +98,10 @@ class TypesDeChamp::TypeDeChampBase
|
||||||
def columns(procedure_id:, displayable: true, prefix: nil)
|
def columns(procedure_id:, displayable: true, prefix: nil)
|
||||||
if fillable?
|
if fillable?
|
||||||
[
|
[
|
||||||
Column.new(
|
Columns::ChampColumn.new(
|
||||||
procedure_id:,
|
procedure_id:,
|
||||||
table: Column::TYPE_DE_CHAMP_TABLE,
|
stable_id:,
|
||||||
column: stable_id.to_s,
|
tdc_type: type_champ,
|
||||||
label: libelle_with_prefix(prefix),
|
label: libelle_with_prefix(prefix),
|
||||||
type: TypeDeChamp.column_type(type_champ),
|
type: TypeDeChamp.column_type(type_champ),
|
||||||
value_column: TypeDeChamp.value_column(type_champ),
|
value_column: TypeDeChamp.value_column(type_champ),
|
||||||
|
|
|
@ -27,6 +27,7 @@ class DossierProjectionService
|
||||||
|
|
||||||
TABLE = 'table'
|
TABLE = 'table'
|
||||||
COLUMN = 'column'
|
COLUMN = 'column'
|
||||||
|
STABLE_ID = 'stable_id'
|
||||||
|
|
||||||
# Returns [DossierProjection(dossier, columns)] ordered by dossiers_ids
|
# Returns [DossierProjection(dossier, columns)] ordered by dossiers_ids
|
||||||
# and the columns orderd by fields.
|
# and the columns orderd by fields.
|
||||||
|
@ -41,7 +42,13 @@ class DossierProjectionService
|
||||||
# - the order of the intermediary query results are unknown
|
# - the order of the intermediary query results are unknown
|
||||||
# - some values can be missing (if a revision added or removed them)
|
# - some values can be missing (if a revision added or removed them)
|
||||||
def self.project(dossiers_ids, columns)
|
def self.project(dossiers_ids, columns)
|
||||||
fields = columns.map { |c| { TABLE => c.table, COLUMN => c.column } }
|
fields = columns.map do |c|
|
||||||
|
if c.is_a?(Columns::ChampColumn)
|
||||||
|
{ TABLE => c.table, STABLE_ID => c.stable_id, original_column: c }
|
||||||
|
else
|
||||||
|
{ TABLE => c.table, COLUMN => c.column }
|
||||||
|
end
|
||||||
|
end
|
||||||
champ_value = champ_value_formatter(dossiers_ids, fields)
|
champ_value = champ_value_formatter(dossiers_ids, fields)
|
||||||
|
|
||||||
state_field = { TABLE => 'self', COLUMN => 'state' }
|
state_field = { TABLE => 'self', COLUMN => 'state' }
|
||||||
|
@ -61,17 +68,20 @@ class DossierProjectionService
|
||||||
.group_by { |f| f[TABLE] } # one query per table
|
.group_by { |f| f[TABLE] } # one query per table
|
||||||
.each do |table, fields|
|
.each do |table, fields|
|
||||||
case table
|
case table
|
||||||
when 'type_de_champ', 'type_de_champ_private'
|
when 'type_de_champ'
|
||||||
Champ
|
Champ
|
||||||
.where(
|
.where(
|
||||||
stable_id: fields.map { |f| f[COLUMN] },
|
stable_id: fields.map { |f| f[STABLE_ID] },
|
||||||
dossier_id: dossiers_ids
|
dossier_id: dossiers_ids
|
||||||
)
|
)
|
||||||
.select(:dossier_id, :value, :stable_id, :type, :external_id, :data, :value_json) # we cannot pluck :value, as we need the champ.to_s method
|
.select(:dossier_id, :value, :stable_id, :type, :external_id, :data, :value_json) # we cannot pluck :value, as we need the champ.to_s method
|
||||||
.group_by(&:stable_id) # the champs are redispatched to their respective fields
|
.group_by(&:stable_id) # the champs are redispatched to their respective fields
|
||||||
.map do |stable_id, champs|
|
.map do |stable_id, champs|
|
||||||
field = fields.find { |f| f[COLUMN] == stable_id.to_s }
|
fields
|
||||||
field[:id_value_h] = champs.to_h { |c| [c.dossier_id, champ_value.(c)] }
|
.filter { |f| f[STABLE_ID] == stable_id }
|
||||||
|
.each do |field|
|
||||||
|
field[:id_value_h] = champs.to_h { |c| [c.dossier_id, champ_value.(c, field[:original_column])] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
when 'self'
|
when 'self'
|
||||||
Dossier
|
Dossier
|
||||||
|
@ -185,7 +195,7 @@ class DossierProjectionService
|
||||||
private
|
private
|
||||||
|
|
||||||
def champ_value_formatter(dossiers_ids, fields)
|
def champ_value_formatter(dossiers_ids, fields)
|
||||||
stable_ids = fields.filter { _1[TABLE].in?(['type_de_champ', 'type_de_champ_private']) }.map { _1[COLUMN] }
|
stable_ids = fields.filter { _1[TABLE].in?(['type_de_champ']) }.map { _1[STABLE_ID] }
|
||||||
revision_ids_by_dossier_ids = Dossier.where(id: dossiers_ids).pluck(:id, :revision_id).to_h
|
revision_ids_by_dossier_ids = Dossier.where(id: dossiers_ids).pluck(:id, :revision_id).to_h
|
||||||
stable_ids_and_types_de_champ_by_revision_ids = ProcedureRevisionTypeDeChamp.includes(:type_de_champ)
|
stable_ids_and_types_de_champ_by_revision_ids = ProcedureRevisionTypeDeChamp.includes(:type_de_champ)
|
||||||
.where(revision_id: revision_ids_by_dossier_ids.values.uniq, type_de_champ: { stable_id: stable_ids })
|
.where(revision_id: revision_ids_by_dossier_ids.values.uniq, type_de_champ: { stable_id: stable_ids })
|
||||||
|
@ -193,10 +203,14 @@ class DossierProjectionService
|
||||||
.group_by(&:first)
|
.group_by(&:first)
|
||||||
.transform_values { _1.map { |_, type_de_champ| [type_de_champ.stable_id, type_de_champ] }.to_h }
|
.transform_values { _1.map { |_, type_de_champ| [type_de_champ.stable_id, type_de_champ] }.to_h }
|
||||||
stable_ids_and_types_de_champ_by_dossier_ids = revision_ids_by_dossier_ids.transform_values { stable_ids_and_types_de_champ_by_revision_ids[_1] }.compact
|
stable_ids_and_types_de_champ_by_dossier_ids = revision_ids_by_dossier_ids.transform_values { stable_ids_and_types_de_champ_by_revision_ids[_1] }.compact
|
||||||
-> (champ) {
|
-> (champ, column) {
|
||||||
type_de_champ = stable_ids_and_types_de_champ_by_dossier_ids.fetch(champ.dossier_id, {})[champ.stable_id]
|
type_de_champ = stable_ids_and_types_de_champ_by_dossier_ids.fetch(champ.dossier_id, {})[champ.stable_id]
|
||||||
if type_de_champ.present? && type_de_champ.type_champ == champ.last_write_type_champ
|
if type_de_champ.present? && type_de_champ.type_champ == champ.last_write_type_champ
|
||||||
type_de_champ.champ_value(champ)
|
if column.is_a?(Columns::JSONPathColumn)
|
||||||
|
column.get_value(champ)
|
||||||
|
else
|
||||||
|
type_de_champ.champ_value(champ)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
''
|
''
|
||||||
end
|
end
|
||||||
|
|
|
@ -67,29 +67,6 @@
|
||||||
],
|
],
|
||||||
"note": "filtered by rails query params where(something: ?, values)"
|
"note": "filtered by rails query params where(something: ?, values)"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"warning_type": "SQL Injection",
|
|
||||||
"warning_code": 0,
|
|
||||||
"fingerprint": "5ba3f5d525b15c710215829e0db49f58e8cca06d68eff5931ebfd7d0ca0e35de",
|
|
||||||
"check_name": "SQL",
|
|
||||||
"message": "Possible SQL injection",
|
|
||||||
"file": "app/models/columns/json_path_column.rb",
|
|
||||||
"line": 11,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
||||||
"code": "dossiers.with_type_de_champ(stable_id).where(\"#{search_occurences.count} OR #{\"(#{json_path_query_part} ILIKE ?)\"}\", *search_occurences.map do\n \"%#{value}%\"\n end)",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "Columns::JSONPathColumn",
|
|
||||||
"method": "filtered_ids"
|
|
||||||
},
|
|
||||||
"user_input": "search_occurences.count",
|
|
||||||
"confidence": "Weak",
|
|
||||||
"cwe_id": [
|
|
||||||
89
|
|
||||||
],
|
|
||||||
"note": "already sanitized"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "SQL Injection",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 0,
|
"warning_code": 0,
|
||||||
|
@ -136,6 +113,29 @@
|
||||||
],
|
],
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"warning_type": "SQL Injection",
|
||||||
|
"warning_code": 0,
|
||||||
|
"fingerprint": "83b5a474065af330c47603d1f60fc31edaab55be162825385d53b77c1c98a6d7",
|
||||||
|
"check_name": "SQL",
|
||||||
|
"message": "Possible SQL injection",
|
||||||
|
"file": "app/models/columns/json_path_column.rb",
|
||||||
|
"line": 24,
|
||||||
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||||
|
"code": "dossiers.with_type_de_champ(stable_id).where(\"champs.value_json @? '#{jsonpath} ? (@ like_regex \\\"#{quote_string(search_terms.join(\"|\"))}\\\" flag \\\"i\\\")'\")",
|
||||||
|
"render_path": null,
|
||||||
|
"location": {
|
||||||
|
"type": "method",
|
||||||
|
"class": "Columns::JSONPathColumn",
|
||||||
|
"method": "filtered_ids"
|
||||||
|
},
|
||||||
|
"user_input": "jsonpath",
|
||||||
|
"confidence": "Weak",
|
||||||
|
"cwe_id": [
|
||||||
|
89
|
||||||
|
],
|
||||||
|
"note": "escaped by hand"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"warning_type": "SQL Injection",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 0,
|
"warning_code": 0,
|
||||||
|
@ -295,6 +295,6 @@
|
||||||
"note": "Current is not a model"
|
"note": "Current is not a model"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated": "2024-10-16 18:07:17 +0200",
|
"updated": "2024-11-04 09:56:55 +0100",
|
||||||
"brakeman_version": "6.1.2"
|
"brakeman_version": "6.1.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ describe Instructeurs::ColumnFilterValueComponent, type: :component do
|
||||||
let(:types_de_champ_public) { [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }] }
|
let(:types_de_champ_public) { [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }] }
|
||||||
let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
|
let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
|
||||||
let(:drop_down_stable_id) { procedure.active_revision.types_de_champ.first.stable_id }
|
let(:drop_down_stable_id) { procedure.active_revision.types_de_champ.first.stable_id }
|
||||||
let(:column) { Column.new(procedure_id:, table: 'type_de_champ', scope: nil, column: drop_down_stable_id) }
|
let(:column) { procedure.find_column(label: 'Votre ville') }
|
||||||
|
|
||||||
it 'find most recent tdc' do
|
it 'find most recent tdc' do
|
||||||
is_expected.to eq(['Paris', 'Lyon', 'Marseille'])
|
is_expected.to eq(['Paris', 'Lyon', 'Marseille'])
|
||||||
|
|
|
@ -1,95 +1,4 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
describe Column do
|
describe Column do
|
||||||
describe 'value' do
|
|
||||||
let(:groupe_instructeur) { create(:groupe_instructeur, instructeurs: [create(:instructeur)]) }
|
|
||||||
|
|
||||||
context 'when dossier columns' do
|
|
||||||
context 'when procedure for individual' do
|
|
||||||
let(:individual) { create(:individual, nom: "Sim", prenom: "Paul", gender: 'M.') }
|
|
||||||
let(:procedure) { create(:procedure, for_individual: true, groupe_instructeurs: [groupe_instructeur]) }
|
|
||||||
let(:dossier) { create(:dossier, individual:, mandataire_first_name: "Martin", mandataire_last_name: "Christophe", for_tiers: true) }
|
|
||||||
|
|
||||||
it 'retrieve individual information' do
|
|
||||||
expect(procedure.find_column(label: "Prénom").value(dossier)).to eq("Paul")
|
|
||||||
expect(procedure.find_column(label: "Nom").value(dossier)).to eq("Sim")
|
|
||||||
expect(procedure.find_column(label: "Civilité").value(dossier)).to eq("M.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when procedure for entreprise' do
|
|
||||||
let(:procedure) { create(:procedure, for_individual: false, groupe_instructeurs: [groupe_instructeur]) }
|
|
||||||
let(:dossier) { create(:dossier, :en_instruction, :with_entreprise, procedure:) }
|
|
||||||
|
|
||||||
it 'retrieve entreprise information' do
|
|
||||||
expect(procedure.find_column(label: "Libellé NAF").value(dossier)).to eq('Transports par conduites')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when sva/svr enabled' do
|
|
||||||
let(:procedure) { create(:procedure, :sva, for_individual: true, groupe_instructeurs: [groupe_instructeur]) }
|
|
||||||
let(:dossier) { create(:dossier, :en_instruction, procedure:) }
|
|
||||||
|
|
||||||
it 'does not fail' do
|
|
||||||
expect(procedure.find_column(label: "Date décision SVA").value(dossier)).to eq(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when champ columns' do
|
|
||||||
let(:procedure) { create(:procedure, :with_all_champs_mandatory, groupe_instructeurs: [groupe_instructeur]) }
|
|
||||||
let(:dossier) { create(:dossier, :with_populated_champs, procedure:) }
|
|
||||||
let(:types_de_champ) { procedure.all_revisions_types_de_champ }
|
|
||||||
|
|
||||||
it 'extracts values for columns and type de champ' do
|
|
||||||
expect_type_de_champ_values('civilite', ["M."])
|
|
||||||
expect_type_de_champ_values('email', ['yoda@beta.gouv.fr'])
|
|
||||||
expect_type_de_champ_values('phone', ['0666666666'])
|
|
||||||
expect_type_de_champ_values('address', ["2 rue des Démarches"])
|
|
||||||
expect_type_de_champ_values('communes', ["Coye-la-Forêt"])
|
|
||||||
expect_type_de_champ_values('departements', ['01'])
|
|
||||||
expect_type_de_champ_values('regions', ['01'])
|
|
||||||
expect_type_de_champ_values('pays', ['France'])
|
|
||||||
expect_type_de_champ_values('epci', [nil])
|
|
||||||
expect_type_de_champ_values('iban', [nil])
|
|
||||||
expect_type_de_champ_values('siret', ["44011762001530", "postal_code", "city_name", "departement_code", "region_name"])
|
|
||||||
expect_type_de_champ_values('text', ['text'])
|
|
||||||
expect_type_de_champ_values('textarea', ['textarea'])
|
|
||||||
expect_type_de_champ_values('number', ['42'])
|
|
||||||
expect_type_de_champ_values('decimal_number', [42.1])
|
|
||||||
expect_type_de_champ_values('integer_number', [42])
|
|
||||||
expect_type_de_champ_values('date', [Time.zone.parse('2019-07-10').to_date])
|
|
||||||
expect_type_de_champ_values('datetime', [Time.zone.parse("1962-09-15T15:35:00+01:00")])
|
|
||||||
expect_type_de_champ_values('checkbox', [true])
|
|
||||||
expect_type_de_champ_values('drop_down_list', ['val1'])
|
|
||||||
expect_type_de_champ_values('multiple_drop_down_list', [["val1", "val2"]])
|
|
||||||
expect_type_de_champ_values('linked_drop_down_list', [nil, "categorie 1", "choix 1"])
|
|
||||||
expect_type_de_champ_values('yes_no', [true])
|
|
||||||
expect_type_de_champ_values('annuaire_education', [nil])
|
|
||||||
expect_type_de_champ_values('carte', [])
|
|
||||||
expect_type_de_champ_values('piece_justificative', [])
|
|
||||||
expect_type_de_champ_values('titre_identite', [true])
|
|
||||||
expect_type_de_champ_values('cnaf', [nil])
|
|
||||||
expect_type_de_champ_values('dgfip', [nil])
|
|
||||||
expect_type_de_champ_values('pole_emploi', [nil])
|
|
||||||
expect_type_de_champ_values('mesri', [nil])
|
|
||||||
expect_type_de_champ_values('cojo', [nil])
|
|
||||||
expect_type_de_champ_values('expression_reguliere', [nil])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def expect_type_de_champ_values(type, values)
|
|
||||||
type_de_champ = types_de_champ.find { _1.type_champ == type }
|
|
||||||
champ = dossier.send(:filled_champ, type_de_champ, nil)
|
|
||||||
columns = type_de_champ.columns(procedure_id: procedure.id)
|
|
||||||
expect(columns.map { _1.value(champ) }).to eq(values)
|
|
||||||
end
|
|
||||||
|
|
||||||
def retrieve_champ(type)
|
|
||||||
type_de_champ = types_de_champ.find { _1.type_champ == type }
|
|
||||||
dossier.send(:filled_champ, type_de_champ, nil)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
134
spec/models/columns/champ_column_spec.rb
Normal file
134
spec/models/columns/champ_column_spec.rb
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe Columns::ChampColumn do
|
||||||
|
describe '#value' do
|
||||||
|
let(:procedure) { create(:procedure, :with_all_champs_mandatory) }
|
||||||
|
|
||||||
|
context 'without any cast' do
|
||||||
|
let(:dossier) { create(:dossier, :with_populated_champs, procedure:) }
|
||||||
|
let(:types_de_champ) { procedure.all_revisions_types_de_champ }
|
||||||
|
|
||||||
|
it 'extracts values for columns and type de champ' do
|
||||||
|
expect_type_de_champ_values('civilite', ["M."])
|
||||||
|
expect_type_de_champ_values('email', ['yoda@beta.gouv.fr'])
|
||||||
|
expect_type_de_champ_values('phone', ['0666666666'])
|
||||||
|
expect_type_de_champ_values('address', ["2 rue des Démarches"])
|
||||||
|
expect_type_de_champ_values('communes', ["Coye-la-Forêt"])
|
||||||
|
expect_type_de_champ_values('departements', ['01'])
|
||||||
|
expect_type_de_champ_values('regions', ['01'])
|
||||||
|
expect_type_de_champ_values('pays', ['France'])
|
||||||
|
expect_type_de_champ_values('epci', [nil])
|
||||||
|
expect_type_de_champ_values('iban', [nil])
|
||||||
|
expect_type_de_champ_values('siret', ["44011762001530", "postal_code", "city_name", "departement_code", "region_name"])
|
||||||
|
expect_type_de_champ_values('text', ['text'])
|
||||||
|
expect_type_de_champ_values('textarea', ['textarea'])
|
||||||
|
expect_type_de_champ_values('number', ['42'])
|
||||||
|
expect_type_de_champ_values('decimal_number', [42.1])
|
||||||
|
expect_type_de_champ_values('integer_number', [42])
|
||||||
|
expect_type_de_champ_values('date', [Time.zone.parse('2019-07-10').to_date])
|
||||||
|
expect_type_de_champ_values('datetime', [Time.zone.parse("1962-09-15T15:35:00+01:00")])
|
||||||
|
expect_type_de_champ_values('checkbox', [true])
|
||||||
|
expect_type_de_champ_values('drop_down_list', ['val1'])
|
||||||
|
expect_type_de_champ_values('multiple_drop_down_list', [["val1", "val2"]])
|
||||||
|
expect_type_de_champ_values('linked_drop_down_list', [nil, "categorie 1", "choix 1"])
|
||||||
|
expect_type_de_champ_values('yes_no', [true])
|
||||||
|
expect_type_de_champ_values('annuaire_education', [nil])
|
||||||
|
expect_type_de_champ_values('carte', [])
|
||||||
|
expect_type_de_champ_values('piece_justificative', [])
|
||||||
|
expect_type_de_champ_values('titre_identite', [true])
|
||||||
|
expect_type_de_champ_values('cnaf', [nil])
|
||||||
|
expect_type_de_champ_values('dgfip', [nil])
|
||||||
|
expect_type_de_champ_values('pole_emploi', [nil])
|
||||||
|
expect_type_de_champ_values('mesri', [nil])
|
||||||
|
expect_type_de_champ_values('cojo', [nil])
|
||||||
|
expect_type_de_champ_values('expression_reguliere', [nil])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with cast' do
|
||||||
|
def column(label) = procedure.find_column(label:)
|
||||||
|
|
||||||
|
context 'from a integer_number' do
|
||||||
|
let(:champ) { double(last_write_type_champ: 'integer_number', value: '42') }
|
||||||
|
|
||||||
|
it do
|
||||||
|
expect(column('decimal_number').value(champ)).to eq(42.0)
|
||||||
|
expect(column('text').value(champ)).to eq('42')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'from a decimal_number' do
|
||||||
|
let(:champ) { double(last_write_type_champ: 'decimal_number', value: '42.1') }
|
||||||
|
|
||||||
|
it do
|
||||||
|
expect(column('integer_number').value(champ)).to eq(42)
|
||||||
|
expect(column('text').value(champ)).to eq('42.1')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'from a date' do
|
||||||
|
let(:champ) { double(last_write_type_champ: 'date', value:) }
|
||||||
|
|
||||||
|
describe 'when the value is valid' do
|
||||||
|
let(:value) { '2019-07-10' }
|
||||||
|
|
||||||
|
it { expect(column('datetime').value(champ)).to eq(Time.zone.parse('2019-07-10')) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when the value is invalid' do
|
||||||
|
let(:value) { 'invalid' }
|
||||||
|
|
||||||
|
it { expect(column('datetime').value(champ)).to be_nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'from a datetime' do
|
||||||
|
let(:champ) { double(last_write_type_champ: 'datetime', value:) }
|
||||||
|
|
||||||
|
describe 'when the value is valid' do
|
||||||
|
let(:value) { '1962-09-15T15:35:00+01:00' }
|
||||||
|
|
||||||
|
it { expect(column('date').value(champ)).to eq('1962-09-15'.to_date) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when the value is invalid' do
|
||||||
|
let(:value) { 'invalid' }
|
||||||
|
|
||||||
|
it { expect(column('date').value(champ)).to be_nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'from a drop_down_list' do
|
||||||
|
let(:champ) { double(last_write_type_champ: 'drop_down_list', value: 'val1') }
|
||||||
|
|
||||||
|
it do
|
||||||
|
expect(column('multiple_drop_down_list').value(champ)).to eq(['val1'])
|
||||||
|
expect(column('text').value(champ)).to eq('val1')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'from a multiple_drop_down_list' do
|
||||||
|
let(:champ) { double(last_write_type_champ: 'multiple_drop_down_list', value: '["val1","val2"]') }
|
||||||
|
|
||||||
|
it do
|
||||||
|
expect(column('simple_drop_down_list').value(champ)).to eq('val1')
|
||||||
|
expect(column('text').value(champ)).to eq('val1, val2')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def expect_type_de_champ_values(type, values)
|
||||||
|
type_de_champ = types_de_champ.find { _1.type_champ == type }
|
||||||
|
champ = dossier.send(:filled_champ, type_de_champ, nil)
|
||||||
|
columns = type_de_champ.columns(procedure_id: procedure.id)
|
||||||
|
expect(columns.map { _1.value(champ) }).to eq(values)
|
||||||
|
end
|
||||||
|
|
||||||
|
def retrieve_champ(type)
|
||||||
|
type_de_champ = types_de_champ.find { _1.type_champ == type }
|
||||||
|
dossier.send(:filled_champ, type_de_champ, nil)
|
||||||
|
end
|
||||||
|
end
|
39
spec/models/columns/dossier_column_spec.rb
Normal file
39
spec/models/columns/dossier_column_spec.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe Columns::DossierColumn do
|
||||||
|
describe 'value' do
|
||||||
|
let(:groupe_instructeur) { create(:groupe_instructeur, instructeurs: [create(:instructeur)]) }
|
||||||
|
|
||||||
|
context 'when dossier columns' do
|
||||||
|
context 'when procedure for individual' do
|
||||||
|
let(:individual) { create(:individual, nom: "Sim", prenom: "Paul", gender: 'M.') }
|
||||||
|
let(:procedure) { create(:procedure, for_individual: true, groupe_instructeurs: [groupe_instructeur]) }
|
||||||
|
let(:dossier) { create(:dossier, individual:, mandataire_first_name: "Martin", mandataire_last_name: "Christophe", for_tiers: true) }
|
||||||
|
|
||||||
|
it 'retrieve individual information' do
|
||||||
|
expect(procedure.find_column(label: "Prénom").value(dossier)).to eq("Paul")
|
||||||
|
expect(procedure.find_column(label: "Nom").value(dossier)).to eq("Sim")
|
||||||
|
expect(procedure.find_column(label: "Civilité").value(dossier)).to eq("M.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when procedure for entreprise' do
|
||||||
|
let(:procedure) { create(:procedure, for_individual: false, groupe_instructeurs: [groupe_instructeur]) }
|
||||||
|
let(:dossier) { create(:dossier, :en_instruction, :with_entreprise, procedure:) }
|
||||||
|
|
||||||
|
it 'retrieve entreprise information' do
|
||||||
|
expect(procedure.find_column(label: "Libellé NAF").value(dossier)).to eq('Transports par conduites')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sva/svr enabled' do
|
||||||
|
let(:procedure) { create(:procedure, :sva, for_individual: true, groupe_instructeurs: [groupe_instructeur]) }
|
||||||
|
let(:dossier) { create(:dossier, :en_instruction, procedure:) }
|
||||||
|
|
||||||
|
it 'does not fail' do
|
||||||
|
expect(procedure.find_column(label: "Date décision SVA").value(dossier)).to eq(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
48
spec/models/columns/json_path_column_spec.rb
Normal file
48
spec/models/columns/json_path_column_spec.rb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe Columns::JSONPathColumn do
|
||||||
|
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :address }]) }
|
||||||
|
let(:dossier) { create(:dossier, procedure:) }
|
||||||
|
let(:champ) { dossier.champs.first }
|
||||||
|
let(:stable_id) { champ.stable_id }
|
||||||
|
let(:tdc_type) { champ.type_champ }
|
||||||
|
let(:column) { described_class.new(procedure_id: procedure.id, label: 'label', stable_id:, tdc_type:, jsonpath:, displayable: true) }
|
||||||
|
|
||||||
|
describe '#value' do
|
||||||
|
let(:jsonpath) { '$.city_name' }
|
||||||
|
|
||||||
|
subject { column.value(champ) }
|
||||||
|
|
||||||
|
context 'when champ has value_json' do
|
||||||
|
before { champ.update(value_json: { city_name: 'Grenoble' }) }
|
||||||
|
|
||||||
|
it { is_expected.to eq('Grenoble') }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when champ has no value_json' do
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#filtered_ids' do
|
||||||
|
let(:jsonpath) { '$.city_name' }
|
||||||
|
|
||||||
|
subject { column.filtered_ids(Dossier.all, ['reno', 'Lyon']) }
|
||||||
|
|
||||||
|
context 'when champ has value_json' do
|
||||||
|
before { champ.update(value_json: { city_name: 'Grenoble' }) }
|
||||||
|
|
||||||
|
it { is_expected.to eq([dossier.id]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when champ has no value_json' do
|
||||||
|
it { is_expected.to eq([]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#initializer' do
|
||||||
|
let(:jsonpath) { %{$.'city_name} }
|
||||||
|
|
||||||
|
it { expect(column.jsonpath).to eq(%{$.''city_name}) }
|
||||||
|
end
|
||||||
|
end
|
|
@ -528,7 +528,7 @@ describe DossierFilterService do
|
||||||
|
|
||||||
context "when searching by postal_code (text)" do
|
context "when searching by postal_code (text)" do
|
||||||
let(:value) { "60580" }
|
let(:value) { "60580" }
|
||||||
let(:filter) { ["rna – code postal (5 chiffres)", value] }
|
let(:filter) { ["rna – code postal (5 chiffres)", value] }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
kept_dossier.project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "postal_code" => value })
|
kept_dossier.project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "postal_code" => value })
|
||||||
|
|
Loading…
Reference in a new issue