Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tech: simplifie le service de projection des colonnes #11041

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions app/components/instructeurs/cell_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# frozen_string_literal: true

class Instructeurs::CellComponent < ApplicationComponent
def initialize(dossier:, column:)
@dossier = dossier
@column = column
end

def call
tag.span(value)
end

private

def value
custom_format = if @column.email?
email_and_tiers(@dossier)
elsif @column.dossier_labels?
helpers.tags_label(@dossier.labels)
elsif @column.avis?
sum_up_avis(@dossier.avis)
# needed ?
elsif @column.column == 'sva_svr_decision_on'
raise
@column.value(@dossier)
end

return custom_format if custom_format.present?

value = @column.champ_column? ? @column.value(champ_for(@column)) : @column.value(@dossier)

return '' if value.nil?

case @column.type
when :boolean
if @column.tdc_type == 'checkbox'
value ? 'coché' : ''
else
value ? 'Oui' : 'Non'
end
when :attachements
value.present? ? 'présent' : 'absent'
when :enum
format_enum(column: @column, value:)
when :enums
format_enums(column: @column, values: value)
when :datetime, :date
I18n.l(value)
else
value
end
end

def format_enums(column:, values:)
values.map { format_enum(column:, value: _1) }.join(', ')
end

def format_enum(column:, value:)
# options for select store ["trad", :enum_value]
selected_option = @column.options_for_select.find { _1[1].to_s == value }

selected_option ? selected_option.first : value
end

def email_and_tiers(dossier)
email = dossier&.user&.email

if dossier.for_tiers
prenom, nom = dossier&.individual&.prenom, dossier&.individual&.nom
"#{email} #{I18n.t('views.instructeurs.dossiers.acts_on_behalf')} #{prenom} #{nom}"
else
email
end
end

def sum_up_avis(avis)
avis.map(&:question_answer)&.compact&.tally
&.map { |k, v| I18n.t("helpers.label.question_answer_with_count.#{k}", count: v) }
&.join(' / ')
end

def champ_for(column) = @dossier.champs.find { _1.stable_id == column.stable_id }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ça, ça marche ici par chance car il n'y a pas de répétitions possibles dans le tableau de bord

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Et dans tous les cas, il ne faut plus passer par le dossier.champs. Jamais.

end
1 change: 1 addition & 0 deletions app/controllers/instructeurs/procedures_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def show
# Technically, procedure_presentation already sets the attribute.
# Setting it here to make clear that it is used by the view
@procedure_presentation = procedure_presentation
@displayed_columns = procedure_presentation.displayed_columns

@current_filters = procedure_presentation.filters_for(statut)
@counts = current_instructeur
Expand Down
12 changes: 6 additions & 6 deletions app/helpers/dossier_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,14 @@ def correction_resolved_badge(html_class: nil)
tag.span(Dossier.human_attribute_name("pending_correction.resolved"), class: ['fr-badge fr-badge--sm fr-badge--success super', html_class], role: 'status')
end

def tags_label(tags)
if tags.count > 1
def tags_label(labels)
if labels.size > 1
tag.ul(class: 'fr-tags-group') do
safe_join(tags.map { |t| tag.li(tag_label(t[1], t[2])) })
safe_join(labels.map { |l| tag.li(tag_label(l.name, l.color)) })
end
else
tag = tags.first
tag_label(tag[1], tag[2])
elsif labels.one?
label = labels.first
label_label(label.name, label.color)
end
end

Expand Down
2 changes: 2 additions & 0 deletions app/models/column.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def dossier_id? = [table, column] == ['self', 'id']
def dossier_state? = [table, column] == ['self', 'state']
def groupe_instructeur? = [table, column] == ['groupe_instructeur', 'id']
def dossier_labels? = [table, column] == ['dossier_labels', 'label_id']
def email? = [table, column] == ['user', 'email']
def avis? = [table, column] == ['avis', 'question_answer']
def type_de_champ? = table == TYPE_DE_CHAMP_TABLE

def self.find(h_id)
Expand Down
230 changes: 23 additions & 207 deletions app/services/dossier_projection_service.rb
Original file line number Diff line number Diff line change
@@ -1,214 +1,30 @@
# frozen_string_literal: true

class DossierProjectionService
class DossierProjection < Struct.new(:dossier_id, :state, :archived, :hidden_by_user_at, :hidden_by_administration_at, :hidden_by_reason, :for_tiers, :prenom, :nom, :batch_operation_id, :sva_svr_decision_on, :corrections, :columns) do
def pending_correction?
return false if corrections.blank?

corrections.any? { _1[:resolved_at].nil? }
end

def resolved_corrections?
return false if corrections.blank?

corrections.all? { _1[:resolved_at].present? }
end
end
end

def self.for_tiers_translation(array)
for_tiers, email, first_name, last_name = array
if for_tiers == true
"#{email} #{I18n.t('views.instructeurs.dossiers.acts_on_behalf')} #{first_name} #{last_name}"
else
email
end
end

TABLE = 'table'
COLUMN = 'column'
STABLE_ID = 'stable_id'

# Returns [DossierProjection(dossier, columns)] ordered by dossiers_ids
# and the columns orderd by fields.
#
# It tries to be fast by using `pluck` (or at least `select`)
# to avoid deserializing entire records.
#
# It stores its intermediary queries results in an hash in the corresponding field.
# ex: field_email[:id_value_h] = { dossier_id_1: email_1, dossier_id_3: email_3 }
#
# Those hashes are needed because:
# - the order of the intermediary query results are unknown
# - some values can be missing (if a revision added or removed them)
def self.project(dossiers_ids, columns)
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 }
def self.project(dossier_ids, columns)
to_include = columns.map(&:table).uniq.map(&:to_sym).map do |sym|
case sym
when :self
nil
when :type_de_champ
:champs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il faut ajouter les attachements ici

when :user
[:user, :individual]
when :individual
:individual
when :etablissement
:etablissement
when :groupe_instructeur
:groupe_instructeur
when :followers_instructeurs
:followers_instructeurs
when :avis
:avis
when :dossier_labels
:labels
end
end
champ_value = champ_value_formatter(dossiers_ids, fields)

state_field = { TABLE => 'self', COLUMN => 'state' }
archived_field = { TABLE => 'self', COLUMN => 'archived' }
batch_operation_field = { TABLE => 'self', COLUMN => 'batch_operation_id' }
hidden_by_user_at_field = { TABLE => 'self', COLUMN => 'hidden_by_user_at' }
hidden_by_administration_at_field = { TABLE => 'self', COLUMN => 'hidden_by_administration_at' }
hidden_by_reason_field = { TABLE => 'self', COLUMN => 'hidden_by_reason' }
for_tiers_field = { TABLE => 'self', COLUMN => 'for_tiers' }
individual_first_name = { TABLE => 'individual', COLUMN => 'prenom' }
individual_last_name = { TABLE => 'individual', COLUMN => 'nom' }
sva_svr_decision_on_field = { TABLE => 'self', COLUMN => 'sva_svr_decision_on' }
dossier_corrections = { TABLE => 'dossier_corrections', COLUMN => 'resolved_at' }

([state_field, archived_field, sva_svr_decision_on_field, hidden_by_user_at_field, hidden_by_administration_at_field, hidden_by_reason_field, for_tiers_field, individual_first_name, individual_last_name, batch_operation_field, dossier_corrections] + fields)
.each { |f| f[:id_value_h] = {} }
.group_by { |f| f[TABLE] } # one query per table
.each do |table, fields|
case table
when 'type_de_champ'
Champ
.where(
stable_id: fields.map { |f| f[STABLE_ID] },
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
.group_by(&:stable_id) # the champs are redispatched to their respective fields
.map do |stable_id, champs|
fields
.filter { |f| f[STABLE_ID] == stable_id }
.each do |field|
column = field[:original_column]
field[:id_value_h] = champs.to_h { [_1.dossier_id, column.is_a?(Columns::JSONPathColumn) ? column.value(_1) : champ_value.(_1)] }
end
end
when 'self'
Dossier
.where(id: dossiers_ids)
.pluck(:id, *fields.map { |f| f[COLUMN].to_sym })
.each do |id, *columns|
fields.zip(columns).each do |field, value|
# SVA must remain a date: in other column we compute remaining delay with it
field[:id_value_h][id] = if value.respond_to?(:strftime) && field != sva_svr_decision_on_field
I18n.l(value.to_date)
else
value
end
end
end
when 'individual'
Individual
.where(dossier_id: dossiers_ids)
.pluck(:dossier_id, *fields.map { |f| f[COLUMN].to_sym })
.each { |id, *columns| fields.zip(columns).each { |field, value| field[:id_value_h][id] = value } }
when 'etablissement'
Etablissement
.where(dossier_id: dossiers_ids)
.pluck(:dossier_id, *fields.map { |f| f[COLUMN].to_sym })
.each { |id, *columns| fields.zip(columns).each { |field, value| field[:id_value_h][id] = value } }
when 'user'

fields[0][:id_value_h] = Dossier # there is only one field available for user table
.joins(:user)
.includes(:individual)
.where(id: dossiers_ids)
.pluck('dossiers.id, dossiers.for_tiers, users.email, individuals.prenom, individuals.nom')
.to_h { |dossier_id, *array| [dossier_id, for_tiers_translation(array)] }
when 'groupe_instructeur'
fields[0][:id_value_h] = Dossier
.joins(:groupe_instructeur)
.where(id: dossiers_ids)
.pluck('dossiers.id, groupe_instructeurs.label')
.to_h
when 'dossier_corrections'
columns = fields.map { _1[COLUMN].to_sym }

id_value_h = DossierCorrection.where(dossier_id: dossiers_ids)
.pluck(:dossier_id, *columns)
.group_by(&:first) # group corrections by dossier_id
.transform_values do |values| # build each correction has an hash column => value
values.map { Hash[columns.zip(_1[1..-1])] }
end

fields[0][:id_value_h] = id_value_h

when 'dossier_labels'
columns = fields.map { _1[COLUMN].to_sym }

id_value_h =
DossierLabel
.includes(:label)
.where(dossier_id: dossiers_ids)
.pluck('dossier_id, labels.name, labels.color')
.group_by { |dossier_id, _| dossier_id }

fields[0][:id_value_h] = id_value_h.transform_values { |v| { value: v, type: :label } }

when 'procedure'
Dossier
.joins(:procedure)
.where(id: dossiers_ids)
.pluck(:id, *fields.map { |f| f[COLUMN].to_sym })
.each { |id, *columns| fields.zip(columns).each { |field, value| field[:id_value_h][id] = value } }
when 'followers_instructeurs'
# rubocop:disable Style/HashTransformValues
fields[0][:id_value_h] = Follow
.active
.joins(instructeur: :user)
.where(dossier_id: dossiers_ids)
.pluck('dossier_id, users.email')
.group_by { |dossier_id, _| dossier_id }
.to_h { |dossier_id, dossier_id_emails| [dossier_id, dossier_id_emails.sort.map { |_, email| email }&.join(', ')] }
# rubocop:enable Style/HashTransformValues
when 'avis'
# rubocop:disable Style/HashTransformValues
fields[0][:id_value_h] = Avis
.where(dossier_id: dossiers_ids)
.pluck('dossier_id', 'question_answer')
.group_by { |dossier_id, _| dossier_id }
.to_h { |dossier_id, question_answer| [dossier_id, question_answer.map { |_, answer| answer }&.compact&.tally&.map { |k, v| I18n.t("helpers.label.question_answer_with_count.#{k}", count: v) }&.join(' / ')] }
# rubocop:enable Style/HashTransformValues
end
end

dossiers_ids.map do |dossier_id|
DossierProjection.new(
dossier_id,
state_field[:id_value_h][dossier_id],
archived_field[:id_value_h][dossier_id],
hidden_by_user_at_field[:id_value_h][dossier_id],
hidden_by_administration_at_field[:id_value_h][dossier_id],
hidden_by_reason_field[:id_value_h][dossier_id],
for_tiers_field[:id_value_h][dossier_id],
individual_first_name[:id_value_h][dossier_id],
individual_last_name[:id_value_h][dossier_id],
batch_operation_field[:id_value_h][dossier_id],
sva_svr_decision_on_field[:id_value_h][dossier_id],
dossier_corrections[:id_value_h][dossier_id],
fields.map { |f| f[:id_value_h][dossier_id] }
)
end
end

class << self
private
end.flatten.uniq

def champ_value_formatter(dossiers_ids, fields)
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
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 })
.map { [_1.revision_id, _1.type_de_champ] }
.group_by(&:first)
.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
-> (champ) {
type_de_champ = stable_ids_and_types_de_champ_by_dossier_ids
.fetch(champ.dossier_id, {})[champ.stable_id]
type_de_champ&.champ_value(champ)
}
end
Dossier.includes(:corrections, :pending_corrections, *to_include).find(dossier_ids)
end
end
Loading
Loading