From e6cf07b810446887d4a5e55fcd66c051f7943a6b Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Wed, 2 Feb 2022 08:17:43 +0000 Subject: [PATCH 1/4] stats: move date formatting out of the Stat model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit, the monthly dossiers count was serialized into the Stat record using human-formatted dates, as: ```ruby s.dossiers_in_the_last_4_months = { "octobre 2021"=>409592, "novembre 2021"=>497823, "décembre 2021"=>38170, "janvier 2022"=>0 } ``` Turns out the ordering of keys in a serialized hash is not guaranteed. After a round-trip to the database, the keys will be wrongly sorted. Instead we want to save raw Date objects, which will preserve the ordering. The date formatting can be applied at display-time by the controller. Fix #6848 --- app/controllers/stats_controller.rb | 13 ++++-- app/models/stat.rb | 6 +-- spec/models/stat_spec.rb | 68 ++++++++++++++++++----------- 3 files changed, 53 insertions(+), 34 deletions(-) diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index 1202df9c8..3032e67c7 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -31,7 +31,7 @@ class StatsController < ApplicationController @procedures_in_the_last_4_months = last_four_months_serie(procedures, :published_at) @dossiers_cumulative = stat.dossiers_cumulative - @dossiers_in_the_last_4_months = stat.dossiers_in_the_last_4_months + @dossiers_in_the_last_4_months = format_keys_as_months(stat.dossiers_in_the_last_4_months) end def download @@ -136,11 +136,18 @@ class StatsController < ApplicationController end end + def format_keys_as_months(series) + series.transform_keys do |k| + date = k.is_a?(Date) ? k : (Date.parse(k) rescue k) + l(date, format: "%B %Y") + end + end + def last_four_months_serie(association, date_attribute) - association + series = association .group_by_month(date_attribute, last: 4, current: super_admin_signed_in?) .count - .transform_keys { |date| l(date, format: "%B %Y") } + format_keys_as_months(series) end def cumulative_month_serie(association, date_attribute) diff --git a/app/models/stat.rb b/app/models/stat.rb index ce0109c5b..097c0c592 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -80,11 +80,7 @@ class Stat < ApplicationRecord association.group_by_month(date_attribute, last: 4, current: false).count end - month_serie(sum_hashes(*timeseries)) - end - - def month_serie(date_serie) - date_serie.keys.sort.each_with_object({}) { |date, h| h[I18n.l(date, format: "%B %Y")] = date_serie[date] } + sum_hashes(*timeseries).sort.to_h end def cumulative_month_serie(associations_with_date_attribute) diff --git a/spec/models/stat_spec.rb b/spec/models/stat_spec.rb index e9fc2edc2..41138d59a 100644 --- a/spec/models/stat_spec.rb +++ b/spec/models/stat_spec.rb @@ -57,24 +57,30 @@ describe Stat do create(:dossier, state: :en_construction, depose_at: i.months.ago) create(:deleted_dossier, dossier_id: i + 100, state: :en_construction, deleted_at: i.month.ago) end - rs = Stat.send(:cumulative_month_serie, [ - [Dossier.state_not_brouillon, :depose_at], - [DeletedDossier.where.not(state: :brouillon), :deleted_at] + s = Stat.new({ + dossiers_cumulative: + Stat.send(:cumulative_month_serie, [ + [Dossier.state_not_brouillon, :depose_at], + [DeletedDossier.where.not(state: :brouillon), :deleted_at] + ]) + }) + s.save! + s.reload + # Use `Hash#to_a` to also test the key ordering + expect(s.dossiers_cumulative.to_a).to eq([ + [formatted_n_months_ago(12), 2], + [formatted_n_months_ago(11), 4], + [formatted_n_months_ago(10), 6], + [formatted_n_months_ago(9), 8], + [formatted_n_months_ago(8), 10], + [formatted_n_months_ago(7), 12], + [formatted_n_months_ago(6), 14], + [formatted_n_months_ago(5), 16], + [formatted_n_months_ago(4), 18], + [formatted_n_months_ago(3), 20], + [formatted_n_months_ago(2), 22], + [formatted_n_months_ago(1), 24] ]) - expect(rs).to eq({ - 12 => 2, - 11 => 4, - 10 => 6, - 9 => 8, - 8 => 10, - 7 => 12, - 6 => 14, - 5 => 16, - 4 => 18, - 3 => 20, - 2 => 22, - 1 => 24 - }.transform_keys { |i| i.months.ago.beginning_of_month.to_date }) end end @@ -85,16 +91,22 @@ describe Stat do create(:dossier, state: :en_construction, depose_at: i.months.ago) create(:deleted_dossier, dossier_id: i + 100, state: :en_construction, deleted_at: i.month.ago) end - rs = Stat.send(:last_four_months_serie, [ - [Dossier.state_not_brouillon, :depose_at], - [DeletedDossier.where.not(state: :brouillon), :deleted_at] - ]) - expect(rs).to eq({ - "juillet 2021" => 2, - "août 2021" => 2, - "septembre 2021" => 2, - "octobre 2021" => 2 + s = Stat.new({ + dossiers_in_the_last_4_months: + Stat.send(:last_four_months_serie, [ + [Dossier.state_not_brouillon, :depose_at], + [DeletedDossier.where.not(state: :brouillon), :deleted_at] + ]) }) + s.save! + s.reload + # Use `Hash#to_a` to also test the key ordering + expect(s.dossiers_in_the_last_4_months.to_a).to eq([ + ['2021-07-01', 2], + ['2021-08-01', 2], + ['2021-09-01', 2], + ['2021-10-01', 2] + ]) end end end @@ -104,4 +116,8 @@ describe Stat do expect(Stat.send(:sum_hashes, *[{ a: 1, b: 2, d: 5 }, { a: 2, b: 3, c: 5 }])).to eq({ a: 3, b: 5, c: 5, d: 5 }) end end + + def formatted_n_months_ago(i) + i.months.ago.beginning_of_month.to_date.to_s + end end From 447612abdfcc16cecb1d168a3434e8b1158bf55e Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Wed, 2 Feb 2022 18:03:24 +0100 Subject: [PATCH 2/4] fix a11y-8.9.1 no p tag when Champ text with no value --- app/helpers/string_to_html_helper.rb | 1 + app/views/shared/dossiers/_champ_row.html.haml | 2 +- spec/helpers/string_to_html_helper_spec.rb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/helpers/string_to_html_helper.rb b/app/helpers/string_to_html_helper.rb index 8806863f6..8fc99684f 100644 --- a/app/helpers/string_to_html_helper.rb +++ b/app/helpers/string_to_html_helper.rb @@ -1,5 +1,6 @@ module StringToHtmlHelper def string_to_html(str, wrapper_tag = 'p') + return nil if str.blank? html_formatted = simple_format(str, {}, { wrapper_tag: wrapper_tag }) with_links = Anchored::Linker.auto_link(html_formatted, target: '_blank', rel: 'noopener') sanitize(with_links, attributes: ['target', 'rel', 'href']) diff --git a/app/views/shared/dossiers/_champ_row.html.haml b/app/views/shared/dossiers/_champ_row.html.haml index 4392289d8..edb7f0788 100644 --- a/app/views/shared/dossiers/_champ_row.html.haml +++ b/app/views/shared/dossiers/_champ_row.html.haml @@ -57,7 +57,7 @@ - when TypeDeChamp.type_champs.fetch(:number) = number_with_html_delimiter(c.to_s) - else - = format_text_value(c.to_s) + = format_text_value(c.to_s) unless c.blank? - if c.type_champ != TypeDeChamp.type_champs.fetch(:header_section) %td.updated-at diff --git a/spec/helpers/string_to_html_helper_spec.rb b/spec/helpers/string_to_html_helper_spec.rb index 847b6def7..545bc0cbb 100644 --- a/spec/helpers/string_to_html_helper_spec.rb +++ b/spec/helpers/string_to_html_helper_spec.rb @@ -28,7 +28,7 @@ RSpec.describe StringToHtmlHelper, type: :helper do context "with empty decription" do let(:description) { nil } - it { is_expected.to eq('

') } + it { is_expected.to eq nil } end context "with a bad script" do From b80ec845521fd5ac478f9d23ccf173701b03988c Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Wed, 2 Feb 2022 18:04:41 +0100 Subject: [PATCH 3/4] fix a11y-8.9.1 for attachement description error --- app/views/shared/attachment/_edit.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/shared/attachment/_edit.html.haml b/app/views/shared/attachment/_edit.html.haml index d61f555b9..124680296 100644 --- a/app/views/shared/attachment/_edit.html.haml +++ b/app/views/shared/attachment/_edit.html.haml @@ -29,6 +29,7 @@ %p.attachment-error-title Une erreur s’est produite pendant l’envoi du fichier. %p.attachment-error-description + Une erreur inconnue s'est produite pendant l'envoi du fichier = button_tag type: 'button', class: 'button attachment-error-retry', data: { 'input-target': ".attachment-input-#{attachment_id}" } do %span.icon.retry Ré-essayer From 5d10158fa6abd9ca3b86e2c0fa5ff1d6d4a4f6fb Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Wed, 2 Feb 2022 19:34:00 +0100 Subject: [PATCH 4/4] =?UTF-8?q?Instructeur=20:=20ne=20peut=20plus=20clique?= =?UTF-8?q?r=20sur=20un=20dossier=20supprim=C3=A9=20dans=20la=20recherche?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/recherche_controller.rb | 3 +-- app/services/dossier_projection_service.rb | 9 ++++---- .../dossiers/_header_actions.html.haml | 3 +-- .../procedures/_dossier_actions.html.haml | 8 +------ .../instructeurs/procedures/show.html.haml | 21 ++++++++++++------- app/views/recherche/_hidden_dossier.html.haml | 17 +++++++++++++++ app/views/recherche/index.html.haml | 20 +++++++++++++----- config/locales/en.yml | 3 ++- config/locales/fr.yml | 3 ++- 9 files changed, 57 insertions(+), 30 deletions(-) create mode 100644 app/views/recherche/_hidden_dossier.html.haml diff --git a/app/controllers/recherche_controller.rb b/app/controllers/recherche_controller.rb index 00626af60..f2d96ffb6 100644 --- a/app/controllers/recherche_controller.rb +++ b/app/controllers/recherche_controller.rb @@ -4,8 +4,7 @@ class RechercheController < ApplicationController PROJECTIONS = [ { "table" => 'procedure', "column" => 'libelle' }, { "table" => 'user', "column" => 'email' }, - { "table" => 'procedure', "column" => 'procedure_id' }, - { "table" => 'dossier', "column" => 'hidden_by_administration_at' } + { "table" => 'procedure', "column" => 'procedure_id' } ] def index diff --git a/app/services/dossier_projection_service.rb b/app/services/dossier_projection_service.rb index bb03493ba..52865c6ca 100644 --- a/app/services/dossier_projection_service.rb +++ b/app/services/dossier_projection_service.rb @@ -1,5 +1,5 @@ class DossierProjectionService - class DossierProjection < Struct.new(:dossier_id, :state, :archived, :hidden_by_user_at, :columns) + class DossierProjection < Struct.new(:dossier_id, :state, :archived, :hidden_by_user_at, :hidden_by_administration_at, :columns) end TABLE = 'table' @@ -21,8 +21,8 @@ class DossierProjectionService state_field = { TABLE => 'self', COLUMN => 'state' } archived_field = { TABLE => 'self', COLUMN => 'archived' } hidden_by_user_at_field = { TABLE => 'self', COLUMN => 'hidden_by_user_at' } - - ([state_field, archived_field, hidden_by_user_at_field] + fields) # the view needs state and archived dossier attributes + hidden_by_administration_at_field = { TABLE => 'self', COLUMN => 'hidden_by_administration_at' } + ([state_field, archived_field, hidden_by_user_at_field, hidden_by_administration_at_field] + fields) # the view needs state and archived dossier attributes .each { |f| f[:id_value_h] = {} } .group_by { |f| f[TABLE] } # one query per table .each do |table, fields| @@ -46,7 +46,7 @@ class DossierProjectionService .pluck(:id, *fields.map { |f| f[COLUMN].to_sym }) .each do |id, *columns| fields.zip(columns).each do |field, value| - if [state_field, archived_field, hidden_by_user_at_field].include?(field) + if [state_field, archived_field, hidden_by_user_at_field, hidden_by_administration_at_field].include?(field) field[:id_value_h][id] = value else field[:id_value_h][id] = value&.strftime('%d/%m/%Y') # other fields are datetime @@ -100,6 +100,7 @@ class DossierProjectionService 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], fields.map { |f| f[:id_value_h][dossier_id] } ) end diff --git a/app/views/instructeurs/dossiers/_header_actions.html.haml b/app/views/instructeurs/dossiers/_header_actions.html.haml index 9bfb488fa..47f7b4893 100644 --- a/app/views/instructeurs/dossiers/_header_actions.html.haml +++ b/app/views/instructeurs/dossiers/_header_actions.html.haml @@ -28,8 +28,7 @@ state: dossier.state, archived: dossier.archived, dossier_is_followed: current_instructeur&.follow?(dossier), - close_to_expiration: dossier.close_to_expiration?, - recently_deleted: dossier.hidden_by_administration? } + close_to_expiration: dossier.close_to_expiration? } .state-button diff --git a/app/views/instructeurs/procedures/_dossier_actions.html.haml b/app/views/instructeurs/procedures/_dossier_actions.html.haml index 49a142173..aab58893a 100644 --- a/app/views/instructeurs/procedures/_dossier_actions.html.haml +++ b/app/views/instructeurs/procedures/_dossier_actions.html.haml @@ -21,13 +21,7 @@ %span.icon.archive .dropdown-description Archiver le dossier - - if recently_deleted - %li.danger - = link_to restore_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, data: { confirm: "Voulez vous vraiment restaurer le dossier #{dossier_id}" } do - %span.icon.reply - .dropdown-description - = t('views.instructeurs.dossiers.restore') - - else + %li.danger = link_to supprimer_dossier_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, data: { confirm: "Voulez vous vraiment supprimer le dossier #{dossier_id} ? Cette action est irréversible. \nNous vous suggérons de télécharger le dossier au format PDF au préalable." } do %span.icon.delete diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index edfda99b3..6734bd8de 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -132,14 +132,19 @@ %td.status-col %a.cell-link{ href: path }= status_badge(p.state) - %td.action-col.follow-col= render partial: 'dossier_actions', - locals: { procedure_id: @procedure.id, - dossier_id: p.dossier_id, - state: p.state, - archived: p.archived, - dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id), - close_to_expiration: @statut == 'expirant', - recently_deleted: @statut == 'supprimes_recemment' } + - if @statut == 'supprimes_recemment' + %td.action-col.follow-col + = link_to restore_instructeur_dossier_path(@procedure, p.dossier_id), method: :patch, class: "button primary" do + = t('views.instructeurs.dossiers.restore') + + - else + %td.action-col.follow-col= render partial: 'dossier_actions', + locals: { procedure_id: @procedure.id, + dossier_id: p.dossier_id, + state: p.state, + archived: p.archived, + dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id), + close_to_expiration: @statut == 'expirant' } = pagination - else diff --git a/app/views/recherche/_hidden_dossier.html.haml b/app/views/recherche/_hidden_dossier.html.haml new file mode 100644 index 000000000..7a5de7011 --- /dev/null +++ b/app/views/recherche/_hidden_dossier.html.haml @@ -0,0 +1,17 @@ +%td.folder-col + %p.cell-link + %span.icon.folder + +%td.number-col + %p.cell-link= p.dossier_id + +%td + %p.cell-link= procedure_libelle + +%td + %p.cell-link + = user_email + = "- #{t('views.instructeurs.dossiers.deleted_by_administration')}" if p.hidden_by_administration_at.present? + +%td.status-col + %p.cell-link= status_badge(p.state) diff --git a/app/views/recherche/index.html.haml b/app/views/recherche/index.html.haml index d84be0712..72c91916e 100644 --- a/app/views/recherche/index.html.haml +++ b/app/views/recherche/index.html.haml @@ -20,13 +20,14 @@ %th.action-col.follow-col %tbody - @projected_dossiers.each do |p| - - procedure_libelle, user_email, procedure_id, hidden_by_administration = p.columns + - procedure_libelle, user_email, procedure_id = p.columns - instructeur_dossier = @instructeur_dossiers_ids.include?(p.dossier_id) - expert_dossier = @dossier_avis_ids_h[p.dossier_id].present? + - hidden_by_administration = p.hidden_by_administration_at.present? - instructeur_and_expert_dossier = instructeur_dossier && expert_dossier - path = instructeur_dossier ? instructeur_dossier_path(procedure_id, p.dossier_id) : expert_avis_path(procedure_id, @dossier_avis_ids_h[p.dossier_id]) - %tr + %tr{ class: [p.hidden_by_administration_at.present? && "file-hidden-by-user"] } - if instructeur_and_expert_dossier %td.folder-col.cell-link %span.icon.folder @@ -39,7 +40,11 @@ %td.status-col .cell-link= status_badge(p.state) + - elsif hidden_by_administration + = render partial: "recherche/hidden_dossier", locals: {p: p, procedure_libelle: procedure_libelle, user_email: user_email} + - else + %td.folder-col %a.cell-link{ href: path } %span.icon.folder @@ -76,14 +81,19 @@ Donner mon avis - elsif instructeur_dossier - %td.action-col.follow-col= render partial: "instructeurs/procedures/dossier_actions", + - if hidden_by_administration + %td.action-col.follow-col + = link_to restore_instructeur_dossier_path(procedure_id, p.dossier_id), method: :patch, class: "button primary" do + = t('views.instructeurs.dossiers.restore') + + - else + %td.action-col.follow-col= render partial: "instructeurs/procedures/dossier_actions", locals: { procedure_id: procedure_id, dossier_id: p.dossier_id, state: p.state, archived: p.archived, dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id), - close_to_expiration: nil, - recently_deleted: hidden_by_administration.blank? } + close_to_expiration: nil } - else %td diff --git a/config/locales/en.yml b/config/locales/en.yml index e6668fac8..4a482a470 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -140,7 +140,8 @@ en: archived_dossier: "This file will be kept for an additional month" delete_dossier: "Delete file" deleted_by_user: "File deleted by user" - restore: "Restore the file" + deleted_by_administration: "File deleted by administration" + restore: "Restore" avis: introduction_file_explaination: "File attached to the request for advice" users: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 4c230ae37..b66c9e56f 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -137,7 +137,8 @@ fr: archived_dossier: "Le dossier sera conservé 1 mois supplémentaire" delete_dossier: "Supprimer le dossier" deleted_by_user: "Dossier supprimé par l'usager" - restore: "Restaurer le dossier" + deleted_by_administration: "Dossier supprimé par l'administration" + restore: "Restaurer" avis: introduction_file_explaination: "Fichier joint à la demande d’avis" users: