From 7c0e8e406bdfc8bd771213fdc8f01530a47029d1 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 6 Apr 2022 16:07:25 +0200 Subject: [PATCH 01/10] fix(users/profiles#update): allow people from @assurance-maladie.fr to be a target email when user change his email --- config/initializers/legit_admin_domains.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/legit_admin_domains.rb b/config/initializers/legit_admin_domains.rb index c064dfe4b..5df4e341f 100644 --- a/config/initializers/legit_admin_domains.rb +++ b/config/initializers/legit_admin_domains.rb @@ -1,2 +1,2 @@ -domains = ["gouv.fr", "sante.fr", "cnafmail.fr", "cnamts.fr", "cci.fr", "caf.fr", "msa.fr"] +domains = ["gouv.fr", "sante.fr", "cnafmail.fr", "cnamts.fr", "cci.fr", "caf.fr", "msa.fr", "assurance-maladie.fr"] LEGIT_ADMIN_DOMAINS = ENV["LEGIT_ADMIN_DOMAINS"]&.split(';') || domains From be1a2f916d1c0847796aaf942fbf29a8dedfe267 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 5 Apr 2022 15:53:15 +0200 Subject: [PATCH 02/10] feat(exports): add statut and procedure_presentation to exports --- app/models/export.rb | 25 ++++++++++++++----- app/models/procedure_presentation.rb | 2 ++ ...edure_presentation_and_state_to_exports.rb | 10 ++++++++ db/schema.rb | 7 ++++-- spec/factories/export.rb | 5 ++-- 5 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb diff --git a/app/models/export.rb b/app/models/export.rb index caef9ce48..05d8f04a7 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -2,12 +2,14 @@ # # Table name: exports # -# id :bigint not null, primary key -# format :string not null -# key :text not null -# time_span_type :string default("everything"), not null -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint not null, primary key +# format :string not null +# key :text not null +# statut :string default("tous") +# time_span_type :string default("everything"), not null +# created_at :datetime not null +# updated_at :datetime not null +# procedure_presentation_id :bigint # class Export < ApplicationRecord MAX_DUREE_CONSERVATION_EXPORT = 3.hours @@ -23,7 +25,18 @@ class Export < ApplicationRecord monthly: 'monthly' } + enum statut: { + 'a-suivre': 'a-suivre', + suivis: 'suivis', + traites: 'traites', + tous: 'tous', + supprimes_recemment: 'supprimes_recemment', + archives: 'archives', + expirant: 'expirant' + } + has_and_belongs_to_many :groupe_instructeurs + belongs_to :procedure_presentation, optional: true has_one_attached :file diff --git a/app/models/procedure_presentation.rb b/app/models/procedure_presentation.rb index 1c39669d5..5eaf64138 100644 --- a/app/models/procedure_presentation.rb +++ b/app/models/procedure_presentation.rb @@ -25,6 +25,8 @@ class ProcedurePresentation < ApplicationRecord FILTERS_VALUE_MAX_LENGTH = 100 belongs_to :assign_to, optional: false + has_many :exports, dependent: :destroy + delegate :procedure, :instructeur, to: :assign_to validate :check_allowed_displayed_fields diff --git a/db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb b/db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb new file mode 100644 index 000000000..01921efb4 --- /dev/null +++ b/db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb @@ -0,0 +1,10 @@ +class AddProcedurePresentationAndStateToExports < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_reference :exports, :procedure_presentation, null: true, index: { algorithm: :concurrently } + add_column :exports, :statut, :string, default: 'tous' + remove_index :exports, [:format, :time_span_type, :key] + add_index :exports, [:format, :time_span_type, :statut, :key], unique: true, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 11feb6627..36b33644d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_04_05_100354) do +ActiveRecord::Schema.define(version: 2022_04_05_110354) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -431,9 +431,12 @@ ActiveRecord::Schema.define(version: 2022_04_05_100354) do t.datetime "created_at", null: false t.string "format", null: false t.text "key", null: false + t.bigint "procedure_presentation_id" + t.string "statut", default: "tous" t.string "time_span_type", default: "everything", null: false t.datetime "updated_at", null: false - t.index ["format", "time_span_type", "key"], name: "index_exports_on_format_and_time_span_type_and_key", unique: true + t.index ["format", "time_span_type", "statut", "key"], name: "index_exports_on_format_and_time_span_type_and_statut_and_key", unique: true + t.index ["procedure_presentation_id"], name: "index_exports_on_procedure_presentation_id" end create_table "exports_groupe_instructeurs", force: :cascade do |t| diff --git a/spec/factories/export.rb b/spec/factories/export.rb index 3069ca238..c7ebda9b4 100644 --- a/spec/factories/export.rb +++ b/spec/factories/export.rb @@ -1,11 +1,12 @@ FactoryBot.define do factory :export do - format { :csv } + format { Export.formats.fetch(:csv) } + statut { Export.statuts.fetch(:tous) } time_span_type { Export.time_span_types.fetch(:everything) } groupe_instructeurs { [association(:groupe_instructeur)] } after(:build) do |export, _evaluator| - export.key = Export.generate_cache_key(export.groupe_instructeurs.map(&:id)) + export.key = Export.generate_cache_key(export.groupe_instructeurs.map(&:id), export.procedure_presentation&.id) end end end From e82dc9c8b5283d5785f94c318030c1fa47b366b1 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 5 Apr 2022 15:57:19 +0200 Subject: [PATCH 03/10] feat(exports): add ability to create exports with filters --- .../instructeurs/procedures_controller.rb | 34 ++++-- app/helpers/dossier_helper.rb | 20 +++- app/models/export.rb | 113 +++++++++++++----- .../procedures/_dossiers_export.html.haml | 19 +++ .../procedures/_dossiers_filter.html.haml | 24 ++++ .../procedures/_download_dossiers.html.haml | 7 +- .../procedures/download_export.js.erb | 21 +++- .../instructeurs/procedures/show.html.haml | 29 +---- config/brakeman.ignore | 12 +- .../views/instructeurs/procedures/fr.yml | 13 ++ spec/models/export_spec.rb | 6 +- spec/system/instructeurs/instruction_spec.rb | 18 ++- 12 files changed, 234 insertions(+), 82 deletions(-) create mode 100644 app/views/instructeurs/procedures/_dossiers_export.html.haml create mode 100644 app/views/instructeurs/procedures/_dossiers_filter.html.haml create mode 100644 config/locales/views/instructeurs/procedures/fr.yml diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 7dbdc2365..73d70eda6 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -63,7 +63,7 @@ module Instructeurs @can_download_dossiers = (@counts[:tous] + @counts[:archives]) > 0 dossiers = Dossier.where(groupe_instructeur_id: groupe_instructeur_ids) - dossiers_count = @counts[statut.underscore.to_sym] + @dossiers_count = @counts[statut.underscore.to_sym] @followed_dossiers_id = current_instructeur .followed_dossiers @@ -76,7 +76,7 @@ module Instructeurs @has_termine_notifications = notifications[:termines].present? @not_archived_notifications_dossier_ids = notifications[:en_cours] + notifications[:termines] - filtered_sorted_ids = procedure_presentation.filtered_sorted_ids(dossiers, dossiers_count, statut) + filtered_sorted_ids = procedure_presentation.filtered_sorted_ids(dossiers, @dossiers_count, statut) page = params[:page].presence || 1 @@ -137,8 +137,6 @@ module Instructeurs end def download_export - export_format = params[:export_format] - time_span_type = params[:time_span_type] || Export.time_span_types.fetch(:everything) groupe_instructeurs = current_instructeur .groupe_instructeurs .where(procedure: procedure) @@ -148,17 +146,19 @@ module Instructeurs .visible_by_administration .exists?(groupe_instructeur_id: groupe_instructeur_ids) - export = Export.find_or_create_export(export_format, time_span_type, groupe_instructeurs) + export = Export.find_or_create_export(export_format, groupe_instructeurs, **export_options) - if export.ready? && export.old? && params[:force_export] + if export.ready? && export.old? && force_export? export.destroy - export = Export.find_or_create_export(export_format, time_span_type, groupe_instructeurs) + export = Export.find_or_create_export(export_format, groupe_instructeurs, **export_options) end if export.ready? respond_to do |format| format.js do @procedure = procedure + @statut = export_options[:statut] + @dossiers_count = export.count assign_exports flash.notice = "L’export au format \"#{export_format}\" est prêt. Vous pouvez le télécharger" end @@ -173,6 +173,8 @@ module Instructeurs format.js do @procedure = procedure + @statut = export_options[:statut] + @dossiers_count = export.count assign_exports if !params[:no_progress_notification] flash.notice = notice_message @@ -261,7 +263,7 @@ module Instructeurs end def assign_exports - @exports = Export.find_for_groupe_instructeurs(groupe_instructeur_ids) + @exports = Export.find_for_groupe_instructeurs(groupe_instructeur_ids, procedure_presentation) end def assign_tos @@ -280,6 +282,22 @@ module Instructeurs @statut ||= (params[:statut].presence || 'a-suivre') end + def export_format + @export_format ||= params[:export_format] + end + + def force_export? + @force_export ||= params[:force_export].present? + end + + def export_options + @export_options ||= { + time_span_type: params[:time_span_type], + statut: params[:statut], + procedure_presentation: params[:statut].present? ? procedure_presentation : nil + }.compact + end + def procedure_id params[:procedure_id] end diff --git a/app/helpers/dossier_helper.rb b/app/helpers/dossier_helper.rb index 8a2582100..a0d9f1905 100644 --- a/app/helpers/dossier_helper.rb +++ b/app/helpers/dossier_helper.rb @@ -109,9 +109,23 @@ module DossierHelper "#{base_url}/entreprise/#{siren}" end - def exports_list(exports) - Export::FORMATS.map do |(format, time_span_type)| - [format, time_span_type, exports[format] && exports[format][time_span_type]] + def exports_list(exports, statut = nil) + if statut + Export::FORMATS.map do |item| + export = exports + .fetch(item.fetch(:format)) + .fetch(:statut) + .fetch(statut, nil) + item.merge(export: export) + end + else + Export::FORMATS_WITH_TIME_SPAN.map do |item| + export = exports + .fetch(item.fetch(:format)) + .fetch(:time_span_type) + .fetch(item.fetch(:time_span_type), nil) + item.merge(export: export) + end end end end diff --git a/app/models/export.rb b/app/models/export.rb index 05d8f04a7..f97673248 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -46,11 +46,14 @@ class Export < ApplicationRecord after_create_commit :compute_async - FORMATS = [:xlsx, :ods, :csv].flat_map do |format| - Export.time_span_types.values.map do |time_span_type| - [format, time_span_type] + FORMATS_WITH_TIME_SPAN = [:xlsx, :ods, :csv].flat_map do |format| + time_span_types.keys.map do |time_span_type| + { format: format.to_sym, time_span_type: time_span_type } end end + FORMATS = [:xlsx, :ods, :csv].map do |format| + { format: format.to_sym } + end def compute_async ExportJob.perform_later(self) @@ -58,7 +61,7 @@ class Export < ApplicationRecord def compute file.attach( - io: io(since: since), + io: io, filename: filename, content_type: content_type, # We generate the exports ourselves, so they are safe @@ -78,44 +81,96 @@ class Export < ApplicationRecord updated_at < 20.minutes.ago end - def self.find_or_create_export(format, time_span_type, groupe_instructeurs) - create_with(groupe_instructeurs: groupe_instructeurs) + def filtered? + procedure_presentation_id.present? + end + + def xlsx? + format == self.class.formats.fetch(:xlsx) + end + + def ods? + format == self.class.formats.fetch(:ods) + end + + def csv? + format == self.class.formats.fetch(:csv) + end + + def self.find_or_create_export(format, groupe_instructeurs, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil) + create_with(groupe_instructeurs: groupe_instructeurs, procedure_presentation: procedure_presentation) + .includes(:procedure_presentation) .create_or_find_by(format: format, time_span_type: time_span_type, - key: generate_cache_key(groupe_instructeurs.map(&:id))) + statut: statut, + key: generate_cache_key(groupe_instructeurs.map(&:id), procedure_presentation&.id)) end - def self.find_for_groupe_instructeurs(groupe_instructeurs_ids) - exports = where(key: generate_cache_key(groupe_instructeurs_ids)) + def self.find_for_groupe_instructeurs(groupe_instructeurs_ids, procedure_presentation) + exports = if procedure_presentation.present? + where(key: generate_cache_key(groupe_instructeurs_ids)) + .or(where(key: generate_cache_key(groupe_instructeurs_ids, procedure_presentation.id))) + else + where(key: generate_cache_key(groupe_instructeurs_ids)) + end + filtered, not_filtered = exports.partition(&:filtered?) - [:xlsx, :csv, :ods].map do |format| - [ - format, - Export.time_span_types.values.map do |time_span_type| - [time_span_type, exports.find { |export| export.format == format.to_s && export.time_span_type == time_span_type }] - end.filter { |(_, export)| export.present? }.to_h - ] - end.filter { |(_, exports)| exports.present? }.to_h + { + xlsx: { + time_span_type: not_filtered.filter(&:xlsx?).index_by(&:time_span_type), + statut: filtered.filter(&:xlsx?).index_by(&:statut) + }, + ods: { + time_span_type: not_filtered.filter(&:ods?).index_by(&:time_span_type), + statut: filtered.filter(&:ods?).index_by(&:statut) + }, + csv: { + time_span_type: not_filtered.filter(&:csv?).index_by(&:time_span_type), + statut: filtered.filter(&:csv?).index_by(&:statut) + } + } end - def self.generate_cache_key(groupe_instructeurs_ids) - groupe_instructeurs_ids.sort.join('-') + def self.generate_cache_key(groupe_instructeurs_ids, procedure_presentation_id = nil) + if procedure_presentation_id.present? + "#{groupe_instructeurs_ids.sort.join('-')}--#{procedure_presentation_id}" + else + groupe_instructeurs_ids.sort.join('-') + end + end + + def count + if procedure_presentation_id.present? + dossiers_for_export.size + end end private - def filename - procedure_identifier = procedure.path || "procedure-#{procedure.id}" - "dossiers_#{procedure_identifier}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M')}.#{format}" + def dossiers_for_export + @dossiers_for_export ||= begin + dossiers = Dossier.where(groupe_instructeur: groupe_instructeurs) + + if since.present? + dossiers.visible_by_administration.where('dossiers.depose_at > ?', since) + elsif procedure_presentation.present? + filtered_sorted_ids = procedure_presentation + .filtered_sorted_ids(dossiers, dossiers.size, statut) + + dossiers.where(id: filtered_sorted_ids) + else + dossiers + end + end end - def io(since: nil) - dossiers = Dossier.visible_by_administration - .where(groupe_instructeur: groupe_instructeurs) - if since.present? - dossiers = dossiers.where('dossiers.depose_at > ?', since) - end - service = ProcedureExportService.new(procedure, dossiers) + def filename + procedure_identifier = procedure.path || "procedure-#{procedure.id}" + "dossiers_#{procedure_identifier}_#{statut}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M')}.#{format}" + end + + def io + service = ProcedureExportService.new(procedure, dossiers_for_export) case format.to_sym when :csv diff --git a/app/views/instructeurs/procedures/_dossiers_export.html.haml b/app/views/instructeurs/procedures/_dossiers_export.html.haml new file mode 100644 index 000000000..45f0754b5 --- /dev/null +++ b/app/views/instructeurs/procedures/_dossiers_export.html.haml @@ -0,0 +1,19 @@ +%span.dropdown + %button.button.dropdown-button{ 'aria-expanded' => 'false', 'aria-controls' => 'download-menu' } + = t(".download", count: count) + #download-menu.dropdown-content.fade-in-down{ style: 'width: 450px' } + %ul.dropdown-items + - exports_list(exports, statut).each do |item| + - format = item[:format] + - export = item[:export] + %li + - if export.nil? + = link_to t(".everything_#{format}_html"), download_export_instructeur_procedure_path(procedure, statut: statut, export_format: format), remote: true + - elsif export.ready? + = link_to t(".everything_ready_html", export_time: time_ago_in_words(export.updated_at), export_format: ".#{format}"), export.file.service_url, target: "_blank", rel: "noopener" + - if export.old? + = button_to download_export_instructeur_procedure_path(procedure, export_format: format, statut: statut, force_export: true), class: "button small", style: "padding-right: 2px", title: t(".everything_short", export_format: ".#{format}"), remote: true, method: :get, params: { export_format: format, statut: statut, force_export: true } do + .icon.retry + - else + %span{ 'data-export-poll-url': download_export_instructeur_procedure_path(procedure, export_format: format, statut: statut, no_progress_notification: true) } + = t(".everything_pending_html", export_time: time_ago_in_words(export.created_at), export_format: ".#{format}") diff --git a/app/views/instructeurs/procedures/_dossiers_filter.html.haml b/app/views/instructeurs/procedures/_dossiers_filter.html.haml new file mode 100644 index 000000000..ac7c88839 --- /dev/null +++ b/app/views/instructeurs/procedures/_dossiers_filter.html.haml @@ -0,0 +1,24 @@ +%span.dropdown + %button.button.dropdown-button{ 'aria-expanded' => 'false', 'aria-controls' => 'filter-menu' } + Filtrer + #filter-menu.dropdown-content.left-aligned.fade-in-down + = form_tag add_filter_instructeur_procedure_path(procedure), method: :post, class: 'dropdown-form large' do + = label_tag :field, "Colonne" + = select_tag :field, options_for_select(displayed_fields_options) + %br + = label_tag :value, "Valeur" + = text_field_tag :value, nil, maxlength: ProcedurePresentation::FILTERS_VALUE_MAX_LENGTH + = hidden_field_tag :statut, statut + %br + = submit_tag "Ajouter le filtre", class: 'button' + +- current_filters.group_by { |filter| filter['table'] }.each_with_index do |(table, filters), i| + - if i > 0 + et + - filters.each_with_index do |filter, i| + - if i > 0 + ou + %span.filter + = link_to remove_filter_instructeur_procedure_path(procedure, { statut: statut, field: "#{filter['table']}/#{filter['column']}", value: filter['value'] }) do + %img.close-icon{ src: image_url("close.svg") } + = "#{filter['label'].truncate(50)} : #{procedure_presentation.human_value_for_filter(filter)}" diff --git a/app/views/instructeurs/procedures/_download_dossiers.html.haml b/app/views/instructeurs/procedures/_download_dossiers.html.haml index c07b51811..11e83b75e 100644 --- a/app/views/instructeurs/procedures/_download_dossiers.html.haml +++ b/app/views/instructeurs/procedures/_download_dossiers.html.haml @@ -3,7 +3,10 @@ Télécharger tous les dossiers #download-menu.dropdown-content.fade-in-down{ style: 'width: 450px' } %ul.dropdown-items - - exports_list(exports).each do |(format, time_span_type, export)| + - exports_list(exports).each do |item| + - format = item[:format] + - time_span_type = item[:time_span_type] + - export = item[:export] %li - if export.nil? = link_to t("#{time_span_type}_#{format}_html", scope: [:instructeurs, :procedure, :export_stale]), download_export_instructeur_procedure_path(procedure, time_span_type: time_span_type, export_format: format), remote: true @@ -13,7 +16,7 @@ = button_to download_export_instructeur_procedure_path(procedure, export_format: format, time_span_type: time_span_type, force_export: true), class: "button small", style: "padding-right: 2px", title: t("#{time_span_type}_short", export_format: ".#{format}", scope: [:instructeurs, :procedure, :export_stale]), remote: true, method: :get, params: { export_format: format, time_span_type: time_span_type, force_export: true } do .icon.retry - else - %span{ 'data-export-poll-url': download_export_instructeur_procedure_path(procedure, export_format: format, no_progress_notification: true) } + %span{ 'data-export-poll-url': download_export_instructeur_procedure_path(procedure, export_format: format, time_span_type: time_span_type, no_progress_notification: true) } = t("export_#{time_span_type}_pending_html", export_time: time_ago_in_words(export.created_at), export_format: ".#{format}", scope: [:instructeurs, :procedure]) %li = link_to t(:download_archive, scope: [:instructeurs, :procedure]), instructeur_archives_path(procedure) diff --git a/app/views/instructeurs/procedures/download_export.js.erb b/app/views/instructeurs/procedures/download_export.js.erb index aab5dbf9f..ab489f92d 100644 --- a/app/views/instructeurs/procedures/download_export.js.erb +++ b/app/views/instructeurs/procedures/download_export.js.erb @@ -1,11 +1,22 @@ <% if @can_download_dossiers %> - <%= render_to_element('.procedure-actions', partial: "download_dossiers", locals: { procedure: @procedure, exports: @exports }) %> + <% if @statut.present? %> + <%= render_to_element('.dossiers-export', partial: "dossiers_export", locals: { procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count }) %> + <% else %> + <%= render_to_element('.procedure-actions', partial: "download_dossiers", locals: { procedure: @procedure, exports: @exports }) %> + <% end %> <% end %> -<% @exports.each do |format, exports| %> - <% exports.each do |time_span_type, export| %> - <% if !export.ready? %> - <%= fire_event('export:update', { url: download_export_instructeur_procedure_path(@procedure, export_format: format, time_span_type: time_span_type, no_progress_notification: true) }.to_json) %> +<% @exports.values.each do |exports| %> + <% if @statut.present? %> + <% export = exports[:statut][@statut] %> + <% if export && !export.ready? %> + <%= fire_event('export:update', { url: download_export_instructeur_procedure_path(@procedure, export_format: export.format, statut: export.statut, no_progress_notification: true) }.to_json) %> + <% end %> + <% else %> + <% exports[:time_span_type].values.each do |export| %> + <% if !export.ready? %> + <%= fire_event('export:update', { url: download_export_instructeur_procedure_path(@procedure, export_format: export.format, time_span_type: export.time_span_type, no_progress_notification: true) }.to_json) %> + <% end %> <% end %> <% end %> <% end %> diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index ac166b19d..213c33151 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -52,34 +52,15 @@ - if @statut == 'expirant' %p.explication-onglet Les dossiers n'expireront pas avant la période de conservation des données. - - if @filtered_sorted_paginated_ids.present? || @current_filters.count > 0 - pagination = paginate @filtered_sorted_paginated_ids = pagination - %span.dropdown - %button.button.dropdown-button{ 'aria-expanded' => 'false', 'aria-controls' => 'filter-menu' } - Filtrer - #filter-menu.dropdown-content.left-aligned.fade-in-down - = form_tag add_filter_instructeur_procedure_path(@procedure), method: :post, class: 'dropdown-form large' do - = label_tag :field, "Colonne" - = select_tag :field, options_for_select(@displayed_fields_options) - %br - = label_tag :value, "Valeur" - = text_field_tag :value, nil, maxlength: ProcedurePresentation::FILTERS_VALUE_MAX_LENGTH - = hidden_field_tag :statut, @statut - %br - = submit_tag "Ajouter le filtre", class: 'button' + .flex + .flex-grow + = render partial: "dossiers_filter", locals: { procedure: @procedure, procedure_presentation: @procedure_presentation, current_filters: @current_filters, statut: @statut, displayed_fields_options: @displayed_fields_options } + .dossiers-export + = render partial: "dossiers_export", locals: { procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count } - - @current_filters.group_by { |filter| filter['table'] }.each_with_index do |(table, filters), i| - - if i > 0 - et - - filters.each_with_index do |filter, i| - - if i > 0 - ou - %span.filter - = link_to remove_filter_instructeur_procedure_path(@procedure, { statut: @statut, field: "#{filter['table']}/#{filter['column']}", value: filter['value'] }) do - %img.close-icon{ src: image_url("close.svg") } - = "#{filter['label'].truncate(50)} : #{@procedure_presentation.human_value_for_filter(filter)}" %table.table.dossiers-table.hoverable %thead %tr diff --git a/config/brakeman.ignore b/config/brakeman.ignore index ac201cf2d..692e07c58 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -15,7 +15,7 @@ "type": "controller", "class": "Users::DossiersController", "method": "merci", - "line": 196, + "line": 201, "file": "app/controllers/users/dossiers_controller.rb", "rendered": { "name": "users/dossiers/merci", @@ -115,24 +115,24 @@ { "warning_type": "Redirect", "warning_code": 18, - "fingerprint": "c46b5c9cd6474ffae789f39a2280ba6b5a5a74d3ffa8a38cf8a409f9a027ed0e", + "fingerprint": "eae88be293b3849e07be81a18de99c04b08c7b2c045cc4a43ca3624ea178d965", "check_name": "Redirect", "message": "Possible unprotected redirect", "file": "app/controllers/instructeurs/procedures_controller.rb", - "line": 202, + "line": 191, "link": "https://brakemanscanner.org/docs/warning_types/redirect/", - "code": "redirect_to(Export.find_or_create_export(params[:export_format], (params[:time_span_type] or \"everything\"), current_instructeur.groupe_instructeurs.where(:procedure => procedure)).file.service_url)", + "code": "redirect_to(Export.find_or_create_export(export_format, current_instructeur.groupe_instructeurs.where(:procedure => procedure), **export_options).file.service_url)", "render_path": null, "location": { "type": "method", "class": "Instructeurs::ProceduresController", "method": "download_export" }, - "user_input": "Export.find_or_create_export(params[:export_format], (params[:time_span_type] or \"everything\"), current_instructeur.groupe_instructeurs.where(:procedure => procedure)).file.service_url", + "user_input": "Export.find_or_create_export(export_format, current_instructeur.groupe_instructeurs.where(:procedure => procedure), **export_options).file.service_url", "confidence": "High", "note": "" } ], - "updated": "2022-02-22 15:46:39 +0100", + "updated": "2022-04-05 14:21:07 +0200", "brakeman_version": "5.1.1" } diff --git a/config/locales/views/instructeurs/procedures/fr.yml b/config/locales/views/instructeurs/procedures/fr.yml new file mode 100644 index 000000000..9cf758507 --- /dev/null +++ b/config/locales/views/instructeurs/procedures/fr.yml @@ -0,0 +1,13 @@ +fr: + instructeurs: + procedures: + dossiers_export: + everything_csv_html: Demander un export au format .csv
(uniquement les dossiers, sans les champs répétables) + everything_xlsx_html: Demander un export au format .xlsx + everything_ods_html: Demander un export au format .ods + everything_short: Demander un export au format %{export_format} + everything_pending_html: Un export au format %{export_format} est en train d’être généré
(demandé il y a %{export_time}) + everything_ready_html: Télécharger l’export au format %{export_format}
(généré il y a %{export_time}) + download: + one: Télécharger un dossier + other: Télécharger %{count} dossiers diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb index e074999b7..2bfdec18a 100644 --- a/spec/models/export_spec.rb +++ b/spec/models/export_spec.rb @@ -48,9 +48,9 @@ RSpec.describe Export, type: :model do context 'when an export is made for one groupe instructeur' do let!(:export) { create(:export, groupe_instructeurs: [gi_1, gi_2]) } - it { expect(Export.find_for_groupe_instructeurs([gi_1.id])).to eq({}) } - it { expect(Export.find_for_groupe_instructeurs([gi_2.id, gi_1.id])).to eq({ csv: { 'everything' => export } }) } - it { expect(Export.find_for_groupe_instructeurs([gi_1.id, gi_2.id, gi_3.id])).to eq({}) } + it { expect(Export.find_for_groupe_instructeurs([gi_1.id], nil)).to eq({ csv: { statut: {}, time_span_type: {} }, xlsx: { statut: {}, time_span_type: {} }, ods: { statut: {}, time_span_type: {} } }) } + it { expect(Export.find_for_groupe_instructeurs([gi_2.id, gi_1.id], nil)).to eq({ csv: { statut: {}, time_span_type: { 'everything' => export } }, xlsx: { statut: {}, time_span_type: {} }, ods: { statut: {}, time_span_type: {} } }) } + it { expect(Export.find_for_groupe_instructeurs([gi_1.id, gi_2.id, gi_3.id], nil)).to eq({ csv: { statut: {}, time_span_type: {} }, xlsx: { statut: {}, time_span_type: {} }, ods: { statut: {}, time_span_type: {} } }) } end end end diff --git a/spec/system/instructeurs/instruction_spec.rb b/spec/system/instructeurs/instruction_spec.rb index 032397f8e..9af7c53aa 100644 --- a/spec/system/instructeurs/instruction_spec.rb +++ b/spec/system/instructeurs/instruction_spec.rb @@ -105,7 +105,9 @@ describe 'Instructing a dossier:', js: true do assert_performed_jobs 1 click_on "Télécharger tous les dossiers" - click_on "Demander un export au format .xlsx" + within(:css, '.procedure-actions') do + click_on "Demander un export au format .xlsx" + end expect(page).to have_text('Nous générons cet export.') expect(page).to have_text('Un export au format .xlsx est en train d’être généré') @@ -114,13 +116,25 @@ describe 'Instructing a dossier:', js: true do expect(page).to have_text('Nous générons cet export.') expect(page).to have_text('Un export des 30 derniers jours au format .xlsx est en train d’être généré') + click_on "Télécharger un dossier" + within(:css, '.dossiers-export') do + click_on "Demander un export au format .csv" + end + expect(page).to have_text('Nous générons cet export.') + expect(page).to have_text('Un export au format .csv est en train d’être généré') + perform_enqueued_jobs(only: ExportJob) - assert_performed_jobs 3 + assert_performed_jobs 4 page.driver.browser.navigate.refresh click_on "Télécharger tous les dossiers" expect(page).to have_text('Télécharger l’export au format .xlsx') expect(page).to have_text('Télécharger l’export des 30 derniers jours au format .xlsx') + # close dropdown menu + click_on "Télécharger tous les dossiers" + + click_on "Télécharger un dossier" + expect(page).to have_text('Télécharger l’export au format .xlsx') end scenario 'A instructeur can see the personnes impliquées' do From 9904dabffefbc3ddf010c21d9b820ba429e1b76c Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 5 Apr 2022 18:11:12 +0200 Subject: [PATCH 04/10] feat(procedure_presentation): pass filters as serialized snapshot --- app/models/export.rb | 33 +++++++++++++------ app/models/procedure_presentation.rb | 4 +++ ...cedure_presentation_snapshot_to_exports.rb | 5 +++ db/schema.rb | 3 +- 4 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 db/migrate/20220405163206_add_procedure_presentation_snapshot_to_exports.rb diff --git a/app/models/export.rb b/app/models/export.rb index f97673248..78e595546 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -2,14 +2,15 @@ # # Table name: exports # -# id :bigint not null, primary key -# format :string not null -# key :text not null -# statut :string default("tous") -# time_span_type :string default("everything"), not null -# created_at :datetime not null -# updated_at :datetime not null -# procedure_presentation_id :bigint +# id :bigint not null, primary key +# format :string not null +# key :text not null +# procedure_presentation_snapshot :jsonb +# statut :string default("tous") +# time_span_type :string default("everything"), not null +# created_at :datetime not null +# updated_at :datetime not null +# procedure_presentation_id :bigint # class Export < ApplicationRecord MAX_DUREE_CONSERVATION_EXPORT = 3.hours @@ -60,6 +61,8 @@ class Export < ApplicationRecord end def compute + load_snapshot! + file.attach( io: io, filename: filename, @@ -78,7 +81,11 @@ class Export < ApplicationRecord end def old? - updated_at < 20.minutes.ago + updated_at < 20.minutes.ago || filters_changed? + end + + def filters_changed? + procedure_presentation&.snapshot != procedure_presentation_snapshot end def filtered? @@ -98,7 +105,7 @@ class Export < ApplicationRecord end def self.find_or_create_export(format, groupe_instructeurs, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil) - create_with(groupe_instructeurs: groupe_instructeurs, procedure_presentation: procedure_presentation) + create_with(groupe_instructeurs: groupe_instructeurs, procedure_presentation: procedure_presentation, procedure_presentation_snapshot: procedure_presentation&.snapshot) .includes(:procedure_presentation) .create_or_find_by(format: format, time_span_type: time_span_type, @@ -147,6 +154,12 @@ class Export < ApplicationRecord private + def load_snapshot! + if procedure_presentation_snapshot.present? + procedure_presentation.attributes = procedure_presentation_snapshot + end + end + def dossiers_for_export @dossiers_for_export ||= begin dossiers = Dossier.where(groupe_instructeur: groupe_instructeurs) diff --git a/app/models/procedure_presentation.rb b/app/models/procedure_presentation.rb index 5eaf64138..4382a02b5 100644 --- a/app/models/procedure_presentation.rb +++ b/app/models/procedure_presentation.rb @@ -263,6 +263,10 @@ class ProcedurePresentation < ApplicationRecord }) end + def snapshot + slice(:filters, :sort, :displayed_fields) + end + private def field_id(field) diff --git a/db/migrate/20220405163206_add_procedure_presentation_snapshot_to_exports.rb b/db/migrate/20220405163206_add_procedure_presentation_snapshot_to_exports.rb new file mode 100644 index 000000000..2ac3dd06b --- /dev/null +++ b/db/migrate/20220405163206_add_procedure_presentation_snapshot_to_exports.rb @@ -0,0 +1,5 @@ +class AddProcedurePresentationSnapshotToExports < ActiveRecord::Migration[6.1] + def change + add_column :exports, :procedure_presentation_snapshot, :jsonb + end +end diff --git a/db/schema.rb b/db/schema.rb index 36b33644d..4cff9290a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_04_05_110354) do +ActiveRecord::Schema.define(version: 2022_04_05_163206) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -432,6 +432,7 @@ ActiveRecord::Schema.define(version: 2022_04_05_110354) do t.string "format", null: false t.text "key", null: false t.bigint "procedure_presentation_id" + t.jsonb "procedure_presentation_snapshot" t.string "statut", default: "tous" t.string "time_span_type", default: "everything", null: false t.datetime "updated_at", null: false From 23e2429d0d557fbb5c8de73c8b92f9d5977cee94 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 6 Apr 2022 09:12:15 +0200 Subject: [PATCH 05/10] fix(export): make count argument optional --- app/controllers/instructeurs/procedures_controller.rb | 2 +- app/models/export.rb | 2 +- app/models/procedure_presentation.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 73d70eda6..504cf671a 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -76,7 +76,7 @@ module Instructeurs @has_termine_notifications = notifications[:termines].present? @not_archived_notifications_dossier_ids = notifications[:en_cours] + notifications[:termines] - filtered_sorted_ids = procedure_presentation.filtered_sorted_ids(dossiers, @dossiers_count, statut) + filtered_sorted_ids = procedure_presentation.filtered_sorted_ids(dossiers, statut, count: @dossiers_count) page = params[:page].presence || 1 diff --git a/app/models/export.rb b/app/models/export.rb index 78e595546..a62872c5b 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -168,7 +168,7 @@ class Export < ApplicationRecord dossiers.visible_by_administration.where('dossiers.depose_at > ?', since) elsif procedure_presentation.present? filtered_sorted_ids = procedure_presentation - .filtered_sorted_ids(dossiers, dossiers.size, statut) + .filtered_sorted_ids(dossiers, statut) dossiers.where(id: filtered_sorted_ids) else diff --git a/app/models/procedure_presentation.rb b/app/models/procedure_presentation.rb index 4382a02b5..c2edecc70 100644 --- a/app/models/procedure_presentation.rb +++ b/app/models/procedure_presentation.rb @@ -182,9 +182,9 @@ class ProcedurePresentation < ApplicationRecord end.reduce(:&) end - def filtered_sorted_ids(dossiers, count, statut) + def filtered_sorted_ids(dossiers, statut, count: nil) dossiers_by_statut = dossiers.by_statut(instructeur, statut) - dossiers_sorted_ids = self.sorted_ids(dossiers_by_statut, count) + dossiers_sorted_ids = self.sorted_ids(dossiers_by_statut, count || dossiers_by_statut.size) if filters[statut].present? filtered_ids(dossiers_by_statut, statut).intersection(dossiers_sorted_ids) From 5bfcae1c0f50404c4abcdbf6a50b9a0a265bc746 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 6 Apr 2022 17:08:22 +0200 Subject: [PATCH 06/10] fix(dossiers): show correct count on download button --- app/controllers/instructeurs/procedures_controller.rb | 5 +++-- app/views/instructeurs/procedures/show.html.haml | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 504cf671a..172891bc8 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -63,7 +63,7 @@ module Instructeurs @can_download_dossiers = (@counts[:tous] + @counts[:archives]) > 0 dossiers = Dossier.where(groupe_instructeur_id: groupe_instructeur_ids) - @dossiers_count = @counts[statut.underscore.to_sym] + dossiers_count = @counts[statut.underscore.to_sym] @followed_dossiers_id = current_instructeur .followed_dossiers @@ -76,10 +76,11 @@ module Instructeurs @has_termine_notifications = notifications[:termines].present? @not_archived_notifications_dossier_ids = notifications[:en_cours] + notifications[:termines] - filtered_sorted_ids = procedure_presentation.filtered_sorted_ids(dossiers, statut, count: @dossiers_count) + filtered_sorted_ids = procedure_presentation.filtered_sorted_ids(dossiers, statut, count: dossiers_count) page = params[:page].presence || 1 + @dossiers_count = filtered_sorted_ids.size @filtered_sorted_paginated_ids = Kaminari .paginate_array(filtered_sorted_ids) .page(page) diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index 213c33151..feae02087 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -58,8 +58,9 @@ .flex .flex-grow = render partial: "dossiers_filter", locals: { procedure: @procedure, procedure_presentation: @procedure_presentation, current_filters: @current_filters, statut: @statut, displayed_fields_options: @displayed_fields_options } - .dossiers-export - = render partial: "dossiers_export", locals: { procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count } + - if @dossiers_count > 0 + .dossiers-export + = render partial: "dossiers_export", locals: { procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count } %table.table.dossiers-table.hoverable %thead From a9769ae277cae3e779d73db5c3433dd191831b8a Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 6 Apr 2022 15:01:07 +0200 Subject: [PATCH 07/10] fix(instructeur/dossiers#create_avis): as an instructeur, when I ask an avis, i hope to be notified when the expert give his avis --- spec/controllers/instructeurs/dossiers_controller_spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index bd3c80f20..c0642a768 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -509,6 +509,13 @@ describe Instructeurs::DossiersController, type: :controller do end end + context 'as an instructeur, i auto follow the dossier so I get the notifications' do + it 'works' do + subject + expect(instructeur.follows.where(dossier: dossier).count).to eq(1) + end + end + context 'email sending' do before do subject From 9bac5c65bafef1ce1e2f076a73e2eac19159ed14 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 6 Apr 2022 15:05:03 +0200 Subject: [PATCH 08/10] feat(instructeurs/dossier#create_avis): only followed dossier receive notifications, so when an instructeur ask for an avis, he follows the dossier Update app/controllers/concerns/create_avis_concern.rb Co-authored-by: Paul Chavard Update spec/controllers/instructeurs/dossiers_controller_spec.rb Co-authored-by: Paul Chavard --- app/controllers/concerns/create_avis_concern.rb | 3 +++ spec/controllers/instructeurs/dossiers_controller_spec.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/concerns/create_avis_concern.rb b/app/controllers/concerns/create_avis_concern.rb index 8b7e55865..74965976c 100644 --- a/app/controllers/concerns/create_avis_concern.rb +++ b/app/controllers/concerns/create_avis_concern.rb @@ -17,6 +17,9 @@ module CreateAvisConcern allowed_dossiers += dossier.linked_dossiers_for(instructeur_or_expert) end + if (instructeur_or_expert.is_a?(Instructeur)) && !instructeur_or_expert.follows.exists?(dossier: dossier) + instructeur_or_expert.follow(dossier) + end create_results = Avis.create( expert_emails.flat_map do |email| user = User.create_or_promote_to_expert(email, SecureRandom.hex) diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index c0642a768..4962ac9d4 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -512,7 +512,7 @@ describe Instructeurs::DossiersController, type: :controller do context 'as an instructeur, i auto follow the dossier so I get the notifications' do it 'works' do subject - expect(instructeur.follows.where(dossier: dossier).count).to eq(1) + expect(instructeur.followed_dossiers).to match_array([dossier]) end end From 21945b6df085f6b8978a2a459e450f9662a6ce5c Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 7 Apr 2022 09:31:44 +0200 Subject: [PATCH 09/10] =?UTF-8?q?fix(exports):=20disable=20strong=20migrat?= =?UTF-8?q?ion=20checks=20because=20we=20are=20operating=20on=20a=20?= =?UTF-8?q?=E2=80=9Csmall=E2=80=9D=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...323143325_add_procedure_presentation_and_state_to_exports.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb b/db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb index 01921efb4..61f166723 100644 --- a/db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb +++ b/db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb @@ -3,7 +3,9 @@ class AddProcedurePresentationAndStateToExports < ActiveRecord::Migration[6.1] def change add_reference :exports, :procedure_presentation, null: true, index: { algorithm: :concurrently } + StrongMigrations.disable_check(:add_column) add_column :exports, :statut, :string, default: 'tous' + StrongMigrations.enable_check(:add_column) remove_index :exports, [:format, :time_span_type, :key] add_index :exports, [:format, :time_span_type, :statut, :key], unique: true, algorithm: :concurrently end From f84c7ac5e4fb56084328e39e31b84894234d3da0 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 7 Apr 2022 10:18:12 +0200 Subject: [PATCH 10/10] fix(migration): apply what strong migration recommends --- ..._add_procedure_presentation_and_state_to_exports.rb | 5 ++--- db/migrate/20220407081538_backfill_export_status.rb | 10 ++++++++++ db/schema.rb | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20220407081538_backfill_export_status.rb diff --git a/db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb b/db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb index 61f166723..3b2ac9406 100644 --- a/db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb +++ b/db/migrate/20220323143325_add_procedure_presentation_and_state_to_exports.rb @@ -3,9 +3,8 @@ class AddProcedurePresentationAndStateToExports < ActiveRecord::Migration[6.1] def change add_reference :exports, :procedure_presentation, null: true, index: { algorithm: :concurrently } - StrongMigrations.disable_check(:add_column) - add_column :exports, :statut, :string, default: 'tous' - StrongMigrations.enable_check(:add_column) + add_column :exports, :statut, :string + change_column_default :exports, :statut, "tous" remove_index :exports, [:format, :time_span_type, :key] add_index :exports, [:format, :time_span_type, :statut, :key], unique: true, algorithm: :concurrently end diff --git a/db/migrate/20220407081538_backfill_export_status.rb b/db/migrate/20220407081538_backfill_export_status.rb new file mode 100644 index 000000000..721789437 --- /dev/null +++ b/db/migrate/20220407081538_backfill_export_status.rb @@ -0,0 +1,10 @@ +class BackfillExportStatus < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + Export.in_batches do |relation| + relation.update_all statut: "tous" + sleep(0.01) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4cff9290a..d59c95dd9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_04_05_163206) do +ActiveRecord::Schema.define(version: 2022_04_07_081538) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql"