Skip to content
Open
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
36 changes: 3 additions & 33 deletions app/components/instructeurs/cell_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def simple_layout
raw_value = raw_value_for_column(@dossier, @column)
return '' if raw_value.nil?

format(raw_value, @column.type)
format(raw_value)
end

def raw_value_for_column(dossier, column)
Expand All @@ -41,38 +41,8 @@ def raw_value_for_column(dossier, column)
@column.value(data)
end

def format(raw_value, type)
case @column.type
when :boolean
if @column.type_de_champ? && @column.tdc_type == 'checkbox'
raw_value ? I18n.t('activerecord.attributes.type_de_champ.type_champs.checkbox_true') : ''
else
raw_value ? I18n.t('utils.yes') : I18n.t('utils.no')
end
when :attachments
raw_value.present? ? 'présent' : 'absent'
when :enum
format_enum(column: @column, raw_value:)
when :enums
format_enums(column: @column, raw_values: raw_value)
when :date
raw_value = Date.parse(raw_value) if raw_value.is_a?(String)
I18n.l(raw_value, format: :short)
when :datetime
raw_value = DateTime.parse(raw_value) if raw_value.is_a?(String)
I18n.l(raw_value, format: :short_with_time)
else
# Escape if it's a string and not already safe
raw_value.html_safe? ? raw_value : html_escape(raw_value.to_s)
end
end

def format_enums(column:, raw_values:)
safe_join(raw_values.map { format_enum(column:, raw_value: _1) }, ', ')
end

def format_enum(column:, raw_value:)
column.label_for_value(raw_value)
def format(raw_value)
ColumnValueFormatter.format(column: @column, raw_value:)
end

def email_and_tiers(dossier)
Expand Down
29 changes: 29 additions & 0 deletions app/components/users/dossier_card_champs_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

class Users::DossierCardChampsComponent < ApplicationComponent
Line = Data.define(:label, :value)

def initialize(columns:, champs_by_stable_id:)
@columns = columns
@champs_by_stable_id = champs_by_stable_id
end

def render? = lines.any?

private

attr_reader :columns, :champs_by_stable_id

def lines
@lines ||= columns.filter_map do |column|
next unless column.champ_column?

champ = champs_by_stable_id[column.stable_id]
raw = column.value(champ)
formatted = ColumnValueFormatter.format(column:, raw_value: raw)
next if formatted.blank?

Line.new(label: column.label, value: formatted)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
en:
label: "%{champ}:"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fr:
label: "%{champ} :"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<% lines.each do |line| %>
<p class="fr-icon--sm fr-icon-info-i fr-mb-0">
<span class="fr-sr-only"><%= t(".label", champ: line.label) %></span>
<%= line.value %>
</p>
<% end %>
24 changes: 24 additions & 0 deletions app/controllers/users/dossiers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def index
Skylight.instrument(title: list_title) do
@dossiers.load
end
load_personnalisation_data(@dossiers)
@corbeille_count = current_user.dossiers.hidden_by_user.or(current_user.dossiers.hidden_by_expired).count
@pending_transfers_count = current_user.dossier_transfers_received_pending.count
@show_simple_list = params[:search].blank? && !@filter.active? && @total_count <= SIMPLE_LIST_THRESHOLD
Expand Down Expand Up @@ -687,6 +688,29 @@ def commentaire_params
params.require(:commentaire).permit(:body, piece_jointe: [])
end

def load_personnalisation_data(dossiers)
@personnalisations_by_procedure_id = {}
@champs_by_dossier_id = {}
return if !feature_enabled?(:dossiers_list_personnalisation)

@personnalisations_by_procedure_id = current_user.dossiers_list_personnalisations
.where(procedure_id: dossiers.map { _1.procedure.id }.uniq)
.index_by(&:procedure_id)
return if @personnalisations_by_procedure_id.empty?

stable_ids = @personnalisations_by_procedure_id.values
.flat_map(&:displayed_columns)
.filter(&:champ_column?)
.map(&:stable_id)
.uniq
return if stable_ids.empty?

Champ.where(dossier_id: dossiers.map(&:id), stable_id: stable_ids, stream: Champ::MAIN_STREAM)
.find_each do |champ|
(@champs_by_dossier_id[champ.dossier_id] ||= {})[champ.stable_id] = champ
end
end

def personnalisation_available?
feature_enabled?(:dossiers_list_personnalisation) &&
current_user.dossiers.visible_by_user.count > SIMPLE_LIST_THRESHOLD
Expand Down
43 changes: 43 additions & 0 deletions app/lib/column_value_formatter.rb

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

quitte à l'extraire ce serait plutôt dans app/services

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module ColumnValueFormatter
module_function

def format(column:, raw_value:)
return if raw_value.nil?

case column.type
when :boolean
if column.type_de_champ? && column.tdc_type == 'checkbox'
raw_value ? I18n.t('activerecord.attributes.type_de_champ.type_champs.checkbox_true') : ''
else
raw_value ? I18n.t('utils.yes') : I18n.t('utils.no')
end
when :attachments
raw_value.present? ? 'présent' : 'absent'
when :enum
format_enum(column:, raw_value:)
when :enums
format_enums(column:, raw_values: raw_value)
when :date
raw_value = Date.parse(raw_value) if raw_value.is_a?(String)
I18n.l(raw_value, format: :short)
when :datetime
raw_value = DateTime.parse(raw_value) if raw_value.is_a?(String)
I18n.l(raw_value, format: :short_with_time)
else
raw_value.html_safe? ? raw_value : ERB::Util.html_escape(raw_value.to_s)
end
end

def format_enums(column:, raw_values:)
ActionController::Base.helpers.safe_join(
raw_values.map { |v| format_enum(column:, raw_value: v) },
', '
)
end

def format_enum(column:, raw_value:)
column.label_for_value(raw_value)
end
end
2 changes: 1 addition & 1 deletion app/models/concerns/columns_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ def moral_columns
end

def types_de_champ_columns
all_revisions_types_de_champ.flat_map { _1.columns(procedure_id: id) }
all_revisions_types_de_champ.filter(&:dynamic_type).flat_map { _1.columns(procedure_id: id) }
end

def dossier_col(**args) = Columns::DossierColumn.new(**(args.merge(procedure_id: id)))
Expand Down
7 changes: 7 additions & 0 deletions app/views/users/dossiers/_dossier_card_content.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@
</p>
<% end %>
<% end %>

<% if local_assigns[:personnalisation].present? %>
<%= render Users::DossierCardChampsComponent.new(
columns: personnalisation.displayed_columns,
champs_by_stable_id: local_assigns[:champs_by_stable_id] || {}
) %>
<% end %>
</div>

<div class="badge_info text-right fr-hidden unhidden-md">
Expand Down
7 changes: 6 additions & 1 deletion app/views/users/dossiers/_dossiers_list.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<% if dossiers.present? %>
<% dossiers.each do |dossier| %>
<div class="card" id="dossier_<%= dossier.id %>">
<%= render partial: 'dossier_card_content', locals: { dossier: dossier, linkable: @statut != "dossiers-supprimes" } %>
<%= render partial: 'dossier_card_content', locals: {
dossier: dossier,
linkable: @statut != "dossiers-supprimes",
personnalisation: @personnalisations_by_procedure_id&.[](dossier.procedure.id),
champs_by_stable_id: @champs_by_dossier_id&.[](dossier.id) || {}
} %>

<% if dossier.procedure.close? %>
<%= render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: "fr-mb-2w") do |c| %>
Expand Down
34 changes: 34 additions & 0 deletions spec/components/users/dossier_card_champs_component_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

RSpec.describe Users::DossierCardChampsComponent, type: :component do
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :text, libelle: 'Titre' }]) }
let(:dossier) { create(:dossier, :en_construction, procedure:, populate_champs: true) }
let(:column) { procedure.personnalisable_columns.first }

it 'renders the formatted value with an info icon when the champ has a value' do
champ = dossier.champs.find { _1.stable_id == column.stable_id }
champ.update(value: 'Presse Océan')

render_inline(described_class.new(columns: [column], champs_by_stable_id: { column.stable_id => champ.reload }))

expect(page).to have_css('.fr-icon-info-i')
expect(page).to have_text('Presse Océan')
end

it 'renders the formatted value for a brouillon dossier' do
brouillon = create(:dossier, :brouillon, procedure:, populate_champs: true)
champ = brouillon.champs.find { _1.stable_id == column.stable_id }
champ.update(value: 'Valeur brouillon')

render_inline(described_class.new(columns: [column], champs_by_stable_id: { column.stable_id => champ.reload }))

expect(page).to have_css('.fr-icon-info-i')
expect(page).to have_text('Valeur brouillon')
end

it 'renders nothing when the champ is missing or empty' do
render_inline(described_class.new(columns: [column], champs_by_stable_id: {}))

expect(page).not_to have_css('.fr-icon-info-i')
end
end
61 changes: 61 additions & 0 deletions spec/controllers/users/dossiers_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2788,6 +2788,67 @@ def announced_region(body)
end
end

describe 'GET #index with personnalisation values' do
render_views

let(:user) { create(:user) }
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :text, libelle: 'Titre' }]) }
let(:column) { procedure.personnalisable_columns.first }

before do
Flipper.enable(:dossiers_list_personnalisation, user)
create(:dossiers_list_personnalisation, user:, procedure:, displayed_columns: [column])
sign_in(user)
end

def query_count
count = 0
callback = -> (*) { count += 1 }
ActiveSupport::Notifications.subscribed(callback, 'sql.active_record') { get :index }
count
end

it 'does not issue more queries as more dossiers are added (no N+1)' do
create_list(:dossier, 6, :en_construction, user:, procedure:, populate_champs: true)
baseline = query_count

create_list(:dossier, 6, :en_construction, user:, procedure:, populate_champs: true)
expect(query_count).to be <= baseline + 3
end
end

describe 'GET #index with personnalisation values – multi-procedure N+1 guard' do
render_views

let(:user) { create(:user) }
let(:procedure1) { create(:procedure, :published, types_de_champ_public: [{ type: :text, libelle: 'Titre' }]) }
let(:procedure2) { create(:procedure, :published, types_de_champ_public: [{ type: :text, libelle: 'Intitulé' }]) }

before do
Flipper.enable(:dossiers_list_personnalisation, user)
create(:dossiers_list_personnalisation, user:, procedure: procedure1, displayed_columns: [procedure1.personnalisable_columns.first])
create(:dossiers_list_personnalisation, user:, procedure: procedure2, displayed_columns: [procedure2.personnalisable_columns.first])
sign_in(user)
end

def query_count
count = 0
callback = -> (*) { count += 1 }
ActiveSupport::Notifications.subscribed(callback, 'sql.active_record') { get :index }
count
end

it 'does not issue more queries per dossier with multiple procedures (no N+1 on Column.find)' do
create_list(:dossier, 3, :en_construction, user:, procedure: procedure1, populate_champs: true)
create_list(:dossier, 3, :en_construction, user:, procedure: procedure2, populate_champs: true)
baseline = query_count

create_list(:dossier, 3, :en_construction, user:, procedure: procedure1, populate_champs: true)
create_list(:dossier, 3, :en_construction, user:, procedure: procedure2, populate_champs: true)
expect(query_count).to be <= baseline + 3
end
end

private

def find_champ_by_stable_id(dossier, stable_id)
Expand Down
36 changes: 36 additions & 0 deletions spec/lib/column_value_formatter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe ColumnValueFormatter do
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :text, libelle: 'Ville' }, { type: :date, libelle: 'Date' }]) }
let(:text_column) { procedure.columns.find { |c| c.respond_to?(:tdc_type) && c.tdc_type == 'text' } }
let(:date_column) { procedure.columns.find { |c| c.respond_to?(:tdc_type) && c.tdc_type == 'date' } }

it 'formats a date with the short format' do
expect(described_class.format(column: date_column, raw_value: Date.new(2026, 3, 18))).to eq(I18n.l(Date.new(2026, 3, 18), format: :short))
end

it 'escapes and returns text values' do
expect(described_class.format(column: text_column, raw_value: 'Presse Océan')).to eq('Presse Océan')
end

it 'returns nil when raw_value is nil' do
expect(described_class.format(column: text_column, raw_value: nil)).to be_nil
end

describe 'format_enums' do
let(:enums_column) { double('column', type: :enums) }

it 'returns an html_safe SafeBuffer and escapes HTML chars in labels' do
allow(enums_column).to receive(:label_for_value).with('a').and_return('A & B')
allow(enums_column).to receive(:label_for_value).with('b').and_return('C')

result = described_class.format(column: enums_column, raw_value: ['a', 'b'])

expect(result).to be_html_safe
expect(result).to include('A &amp; B')
expect(result).to include('C')
end
end
end
20 changes: 20 additions & 0 deletions spec/models/procedure_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2388,6 +2388,26 @@
end
end

describe '#columns' do
it 'does not raise and skips champs with unknown type_champ (legacy value)' do
procedure = create(:procedure, :published, types_de_champ_public: [
{ type: :text, libelle: 'Champ valide' },
{ type: :text, libelle: 'Champ legacy' },
])
valid_tdc = procedure.published_revision.types_de_champ_public.find { _1.libelle == 'Champ valide' }
legacy_tdc_id = procedure.published_revision.types_de_champ_public.find { _1.libelle == 'Champ legacy' }.id
TypeDeChamp.where(id: legacy_tdc_id).update_all(type_champ: 'titre_identite')
expect(TypeDeChamp.find(legacy_tdc_id).dynamic_type).to be_nil
procedure.reload
Current.procedure_columns = nil

expect { procedure.columns }.not_to raise_error
expect(procedure.columns.map(&:label)).not_to include('Champ legacy')
h_id = { procedure_id: procedure.id, column_id: "type_de_champ/#{valid_tdc.stable_id}" }
expect { procedure.find_column(h_id:) }.not_to raise_error
end
end

private

def create_dossier_with_pj_of_size(size, procedure)
Expand Down
14 changes: 14 additions & 0 deletions spec/system/users/dossiers_personnalisation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@
login_as user, scope: :user
end

it 'shows the chosen champ values on the dossier cards' do
perso_procedure = create(:procedure, :published, types_de_champ_public: [{ type: :text, libelle: 'Titre de la publication' }])
column = perso_procedure.personnalisable_columns.first
create(:dossiers_list_personnalisation, user:, procedure: perso_procedure, displayed_columns: [column])
dossiers = create_list(:dossier, 6, :en_construction, user:, procedure: perso_procedure, populate_champs: true)
champ = dossiers.first.champs.find { _1.stable_id == column.stable_id }
champ.update(value: 'Presse Océan')

visit dossiers_path

expect(page).to have_css('.fr-icon-info-i')
expect(page).to have_text('Presse Océan')
end

it 'lets the user pick fields and persists the personnalisation' do
visit dossiers_path

Expand Down
Loading