Merge pull request #9473 from demarches-simplifiees/create-export-page-V2-ldu
[Export] Créer une page d'export et sortir les liens des dropdowns
This commit is contained in:
commit
f942610d32
40 changed files with 577 additions and 256 deletions
|
@ -1,73 +0,0 @@
|
|||
class Dossiers::ExportComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
|
||||
def initialize(procedure:, exports:, statut: nil, count: nil, class_btn: nil, export_url: nil)
|
||||
@procedure = procedure
|
||||
@exports = exports
|
||||
@statut = statut
|
||||
@count = count
|
||||
@class_btn = class_btn
|
||||
@export_url = export_url
|
||||
end
|
||||
|
||||
def exports
|
||||
if @statut
|
||||
Export::FORMATS.filter(&method(:allowed_format?)).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
|
||||
|
||||
def allowed_format?(item)
|
||||
item.fetch(:format) != :json || @procedure.active_revision.carte?
|
||||
end
|
||||
|
||||
def download_export_path(export_format:, force_export: false, no_progress_notification: nil)
|
||||
@export_url.call(@procedure,
|
||||
export_format: export_format,
|
||||
statut: @statut,
|
||||
force_export: force_export,
|
||||
no_progress_notification: no_progress_notification)
|
||||
end
|
||||
|
||||
def refresh_button_options(export)
|
||||
{
|
||||
title: t(".everything_short", export_format: ".#{export.format}"),
|
||||
class: "button small",
|
||||
style: "padding-right: 2px"
|
||||
}
|
||||
end
|
||||
|
||||
def ready_link_label(export)
|
||||
t(".everything_ready_html",
|
||||
export_time: helpers.time_ago_in_words(export.updated_at),
|
||||
export_format: ".#{export.format}")
|
||||
end
|
||||
|
||||
def pending_label(export)
|
||||
t(".everything_pending_html",
|
||||
export_time: time_ago_in_words(export.created_at),
|
||||
export_format: ".#{export.format}")
|
||||
end
|
||||
|
||||
def poll_controller_options(export)
|
||||
{
|
||||
controller: 'turbo-poll',
|
||||
turbo_poll_url_value: download_export_path(export_format: export.format, no_progress_notification: true),
|
||||
turbo_poll_interval_value: 6000,
|
||||
turbo_poll_max_checks_value: 10
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,29 +0,0 @@
|
|||
= render Dropdown::MenuComponent.new(wrapper: :span, button_options: { class: ['fr-btn--sm', @class_btn.present? ? @class_btn : 'fr-btn--secondary']}, menu_options: { id: @count.nil? ? "download_menu" : "download_all_menu", class: ['dropdown-export'] }) do |menu|
|
||||
- menu.with_menu_header_html do
|
||||
%p.menu-component-header.fr-px-2w.fr-pt-2w.fr-mb-0
|
||||
%span.fr-icon-info-line{ aria: { hidden: true } }
|
||||
Des macros ? Lisez la
|
||||
= link_to('doc', t('.macros_doc.url'),
|
||||
title: t('.macros_doc.title'),
|
||||
**external_link_attributes)
|
||||
|
||||
- menu.with_button_inner_html do
|
||||
= @count.nil? ? t(".download_all") : t(".download", count: @count)
|
||||
- exports.each do |item|
|
||||
- export = item[:export]
|
||||
|
||||
- if export.nil?
|
||||
- menu.with_item do
|
||||
= link_to download_export_path(export_format: item[:format]), role: 'menuitem', data: { turbo_method: :post, turbo: true } do
|
||||
= t(".everything_#{item[:format]}_html")
|
||||
- elsif export.available?
|
||||
- menu.with_item do
|
||||
%div
|
||||
= link_to ready_link_label(export), export.file.url, target: "_blank", rel: "noopener", role: 'menuitem'
|
||||
- if export.old?
|
||||
= button_to download_export_path(export_format: export.format, force_export: true), refresh_button_options(export).merge(role: 'menuitem') do
|
||||
.icon.retry
|
||||
- else
|
||||
- menu.with_item(aria: {disabled:"true"}, class: 'selected') do
|
||||
%span{ data: poll_controller_options(export) }
|
||||
= pending_label(export)
|
30
app/components/dossiers/export_dropdown_component.rb
Normal file
30
app/components/dossiers/export_dropdown_component.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
class Dossiers::ExportDropdownComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
|
||||
def initialize(procedure:, statut: nil, count: nil, class_btn: nil, export_url: nil)
|
||||
@procedure = procedure
|
||||
@statut = statut
|
||||
@count = count
|
||||
@class_btn = class_btn
|
||||
@export_url = export_url
|
||||
end
|
||||
|
||||
def formats
|
||||
if @statut
|
||||
Export::FORMATS.filter(&method(:allowed_format?))
|
||||
else
|
||||
Export::FORMATS_WITH_TIME_SPAN
|
||||
end.map { _1[:format] }
|
||||
end
|
||||
|
||||
def allowed_format?(item)
|
||||
item.fetch(:format) != :json || @procedure.active_revision.carte?
|
||||
end
|
||||
|
||||
def download_export_path(export_format:, no_progress_notification: nil)
|
||||
@export_url.call(@procedure,
|
||||
export_format: export_format,
|
||||
statut: @statut,
|
||||
no_progress_notification: no_progress_notification)
|
||||
end
|
||||
end
|
|
@ -6,8 +6,6 @@ en:
|
|||
everything_zip_html: Request an export in .zip format<br>(does not contains timestamp nor operation logs)
|
||||
everything_json_html: Request an export in .json format (GeoJSON)
|
||||
everything_short: Request an export in %{export_format} format
|
||||
everything_pending_html: An export in %{export_format} format is being generated<br>(ask %{export_time} ago)
|
||||
everything_ready_html: Download the export in %{export_format} format<br>(generated %{export_time} ago)
|
||||
download_all: Download all files
|
||||
download:
|
||||
one: Download a file
|
|
@ -6,8 +6,6 @@ fr:
|
|||
everything_zip_html: Demander un export au format .zip<br>(ne contient pas l'horodatage ni le journal de log)
|
||||
everything_json_html: Demander un export au format .json (GeoJSON)
|
||||
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é<br>(demandé il y a %{export_time})
|
||||
everything_ready_html: Télécharger l’export au format %{export_format}<br>(généré il y a %{export_time})
|
||||
download_all: Télécharger tous les dossiers
|
||||
download:
|
||||
one: Télécharger un dossier
|
|
@ -0,0 +1,16 @@
|
|||
= render Dropdown::MenuComponent.new(wrapper: :span, button_options: { class: ['fr-btn--sm', @class_btn.present? ? @class_btn : 'fr-btn--secondary']}, menu_options: { id: @count.nil? ? "download_menu" : "download_all_menu", class: ['dropdown-export'] }) do |menu|
|
||||
- menu.with_menu_header_html do
|
||||
%p.menu-component-header.fr-px-2w.fr-pt-2w.fr-mb-0
|
||||
%span.fr-icon-info-line{ aria: { hidden: true } }
|
||||
Des macros ? Lisez la
|
||||
= link_to('doc', t('.macros_doc.url'),
|
||||
title: t('.macros_doc.title'),
|
||||
**external_link_attributes)
|
||||
|
||||
- menu.with_button_inner_html do
|
||||
= @count.nil? ? t(".download_all") : t(".download", count: @count)
|
||||
|
||||
- formats.each do |format|
|
||||
- menu.with_item do
|
||||
= link_to download_export_path(export_format: format), role: 'menuitem', data: { turbo_method: :post, turbo: true } do
|
||||
= t(".everything_#{format}_html")
|
70
app/components/dossiers/export_link_component.rb
Normal file
70
app/components/dossiers/export_link_component.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
class Dossiers::ExportLinkComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
include TabsHelper
|
||||
|
||||
def initialize(procedure:, exports:, statut: nil, count: nil, class_btn: nil, export_url: nil)
|
||||
@procedure = procedure
|
||||
@exports = exports
|
||||
@statut = statut
|
||||
@count = count
|
||||
@class_btn = class_btn
|
||||
@export_url = export_url
|
||||
end
|
||||
|
||||
def download_export_path(export_format:, statut:, no_progress_notification: nil)
|
||||
@export_url.call(@procedure,
|
||||
export_format: export_format,
|
||||
statut: statut,
|
||||
no_progress_notification: no_progress_notification)
|
||||
end
|
||||
|
||||
def time_info(export)
|
||||
if export.available?
|
||||
t(".ready_link_label_time_info", export_time: helpers.time_ago_in_words(export.updated_at))
|
||||
else
|
||||
t(".not_ready_link_label_time_info", export_time: helpers.time_ago_in_words(export.created_at))
|
||||
end
|
||||
end
|
||||
|
||||
def export_title(export)
|
||||
if export.procedure_presentation_id.nil?
|
||||
t(".export_title_everything", export_format: export.format)
|
||||
elsif export.tous?
|
||||
t(".export_title", export_format: export.format, count: export.count)
|
||||
else
|
||||
t(".export_title_with_tab", export_tabs: human_export_status(export), export_format: export.format, count: export.count)
|
||||
end
|
||||
end
|
||||
|
||||
def human_export_status(export)
|
||||
key = tab_i18n_key_from_status(export.statut)
|
||||
|
||||
t(key, count: export.count) || export.statut
|
||||
end
|
||||
|
||||
def badge(export)
|
||||
if export.available?
|
||||
content_tag(:span, t(".success_label"), { class: "fr-badge fr-badge--success fr-text-right" })
|
||||
elsif export.failed?
|
||||
content_tag(:span, t(".failed_label"), { class: "fr-badge fr-badge--warning fr-text-right" })
|
||||
else
|
||||
content_tag(:span, t(".pending_label"), { class: "fr-badge fr-badge--info fr-text-right" })
|
||||
end
|
||||
end
|
||||
|
||||
def export_button(export)
|
||||
if export.available?
|
||||
render Dsfr::DownloadComponent.new(attachment: export.file, name: t('.download_export'))
|
||||
elsif export.pending?
|
||||
content_tag(:a, t('.refresh_page'), { href: "", class: 'fr-btn fr-btn--sm fr-btn--tertiary' })
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_button_options(export)
|
||||
{
|
||||
title: t(".refresh_old_export"),
|
||||
"aria-label" => t(".refresh_old_export"),
|
||||
class: "fr-btn fr-btn--sm fr-icon-refresh-line fr-btn--tertiary fr-btn--icon-left"
|
||||
}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
en:
|
||||
download_export: Download export
|
||||
refresh_old_export: Regenerate this export
|
||||
success_label: Ready
|
||||
failed_label: Failed
|
||||
pending_label: In progress
|
||||
refresh_page: Refresh page
|
||||
export_title_everything: Export .%{export_format} of all files
|
||||
export_title_with_tab:
|
||||
one: Export .%{export_format} of 1 file « %{export_tabs} »
|
||||
other: Export .%{export_format} of %{count} files « %{export_tabs} »
|
||||
export_title:
|
||||
one: Export .%{export_format} of 1 file
|
||||
other: Export .%{export_format} of %{count} files
|
||||
ready_link_label_time_info: " generated %{export_time} ago"
|
||||
not_ready_link_label_time_info: " asked %{export_time} ago"
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
fr:
|
||||
download_export: Télécharger l’export
|
||||
refresh_old_export: Regénérer cet export
|
||||
success_label: Prêt
|
||||
failed_label: Erreur
|
||||
pending_label: En préparation
|
||||
refresh_page: Recharger la page
|
||||
export_title_everything: "Export .%{export_format} de tous les dossiers"
|
||||
export_title_with_tab:
|
||||
one: "Export .%{export_format} d’un dossier « %{export_tabs} »"
|
||||
other: "Export .%{export_format} de %{count} dossiers « %{export_tabs} »"
|
||||
export_title:
|
||||
one: "Export .%{export_format} d’un dossier"
|
||||
other: "Export .%{export_format} de %{count} dossiers"
|
||||
ready_link_label_time_info: " généré il y a %{export_time}"
|
||||
not_ready_link_label_time_info: " demandé il y a %{export_time}"
|
|
@ -0,0 +1,18 @@
|
|||
%ul#exports-list.fr-raw-list
|
||||
- @exports.each do |export|
|
||||
%li.fr-mb-3w
|
||||
.flex
|
||||
%span
|
||||
%strong
|
||||
= export_title(export)
|
||||
%span.fr-text-mention--grey.fr-mb-1w
|
||||
= time_info(export)
|
||||
.fr-ml-auto
|
||||
= badge(export)
|
||||
|
||||
.flex.flex-gap-2
|
||||
= export_button(export)
|
||||
|
||||
- if export.failed?
|
||||
= button_to refresh_button_options(export)[:title], download_export_path(export_format: export.format, statut: export.statut), refresh_button_options(export)
|
||||
|
|
@ -6,7 +6,7 @@ module Administrateurs
|
|||
helper_method :create_archive_url
|
||||
|
||||
def index
|
||||
@exports = Export.find_for_groupe_instructeurs(all_groupe_instructeurs.map(&:id), nil)
|
||||
@exports = Export.ante_chronological.by_key(all_groupe_instructeurs.map(&:id), nil)
|
||||
@average_dossier_weight = @procedure.average_dossier_weight
|
||||
@count_dossiers_termines_by_month = @procedure.dossiers.processed_by_month(all_groupe_instructeurs).count
|
||||
@archives = Archive.for_groupe_instructeur(all_groupe_instructeurs).to_a
|
||||
|
|
|
@ -4,14 +4,13 @@ module Administrateurs
|
|||
before_action :ensure_not_super_admin!
|
||||
|
||||
def download
|
||||
export = Export.find_or_create_export(export_format, all_groupe_instructeurs, force: force_export?, **export_options)
|
||||
export = Export.find_or_create_fresh_export(export_format, all_groupe_instructeurs, **export_options)
|
||||
@dossiers_count = export.count
|
||||
assign_exports
|
||||
|
||||
if export.available?
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
flash.notice = export.flash_message
|
||||
flash.notice = t('administrateurs.exports.export_available_html', file_format: export.format, file_url: export.file.url)
|
||||
end
|
||||
|
||||
format.html do
|
||||
|
@ -22,11 +21,11 @@ module Administrateurs
|
|||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
if !params[:no_progress_notification]
|
||||
flash.notice = export.flash_message
|
||||
flash.notice = t('administrateurs.exports.export_pending')
|
||||
end
|
||||
end
|
||||
format.html do
|
||||
redirect_to admin_procedure_archives_url(@procedure), notice: export.flash_message
|
||||
redirect_to admin_procedure_archives_url(@procedure), notice: t('administrateurs.exports.export_pending')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -38,10 +37,6 @@ module Administrateurs
|
|||
@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],
|
||||
|
@ -53,9 +48,5 @@ module Administrateurs
|
|||
def all_groupe_instructeurs
|
||||
@procedure.groupe_instructeurs
|
||||
end
|
||||
|
||||
def assign_exports
|
||||
@exports = Export.find_for_groupe_instructeurs(all_groupe_instructeurs.map(&:id), nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Instructeurs
|
||||
class ProceduresController < InstructeurController
|
||||
before_action :ensure_ownership!, except: [:index]
|
||||
before_action :ensure_not_super_admin!, only: [:download_export]
|
||||
before_action :ensure_not_super_admin!, only: [:download_export, :exports]
|
||||
|
||||
ITEMS_PER_PAGE = 25
|
||||
BATCH_SELECTION_LIMIT = 500
|
||||
|
@ -90,6 +90,8 @@ module Instructeurs
|
|||
@has_termine_notifications = notifications[:termines].present?
|
||||
@not_archived_notifications_dossier_ids = notifications[:en_cours] + notifications[:termines]
|
||||
|
||||
@has_export_notification = notify_exports?
|
||||
|
||||
@filtered_sorted_ids = procedure_presentation.filtered_sorted_ids(dossiers, statut, count: dossiers_count)
|
||||
|
||||
page = params[:page].presence || 1
|
||||
|
@ -103,7 +105,6 @@ module Instructeurs
|
|||
@projected_dossiers = DossierProjectionService.project(@filtered_sorted_paginated_ids, procedure_presentation.displayed_fields)
|
||||
@disable_checkbox_all = @projected_dossiers.all? { _1.batch_operation_id.present? }
|
||||
|
||||
assign_exports
|
||||
@batch_operations = BatchOperation.joins(:groupe_instructeurs)
|
||||
.where(groupe_instructeurs: current_instructeur.groupe_instructeurs.where(procedure_id: @procedure.id))
|
||||
.where(seen_at: nil)
|
||||
|
@ -127,8 +128,6 @@ module Instructeurs
|
|||
@has_termine_notifications = notifications[:termines].present?
|
||||
|
||||
@statut = 'supprime'
|
||||
|
||||
assign_exports
|
||||
end
|
||||
|
||||
def update_displayed_fields
|
||||
|
@ -173,17 +172,16 @@ module Instructeurs
|
|||
.visible_by_administration
|
||||
.exists?(groupe_instructeur_id: groupe_instructeur_ids) && !instructeur_as_manager?
|
||||
|
||||
export = Export.find_or_create_export(export_format, groupe_instructeurs, force: force_export?, **export_options)
|
||||
export = Export.find_or_create_fresh_export(export_format, groupe_instructeurs, **export_options)
|
||||
|
||||
@procedure = procedure
|
||||
@statut = export_options[:statut]
|
||||
@dossiers_count = export.count
|
||||
assign_exports
|
||||
|
||||
if export.available?
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
flash.notice = export.flash_message
|
||||
flash.notice = t('instructeurs.procedures.export_available_html', file_format: export.format, file_url: export.file.url)
|
||||
end
|
||||
|
||||
format.html do
|
||||
|
@ -194,11 +192,11 @@ module Instructeurs
|
|||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
if !params[:no_progress_notification]
|
||||
flash.notice = export.flash_message
|
||||
flash.notice = t('instructeurs.procedures.export_pending_html', url: exports_instructeur_procedure_path(procedure))
|
||||
end
|
||||
end
|
||||
format.html do
|
||||
redirect_to instructeur_procedure_url(procedure), notice: export.flash_message
|
||||
redirect_to exports_instructeur_procedure_path(procedure), notice: t('instructeurs.procedures.export_pending_html', url: exports_instructeur_procedure_path(procedure))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -226,6 +224,20 @@ module Instructeurs
|
|||
@usual_traitement_time_by_month = @procedure.stats_usual_traitement_time_by_month_in_days
|
||||
end
|
||||
|
||||
def exports
|
||||
@procedure = procedure
|
||||
@exports = Export.for_groupe_instructeurs(groupe_instructeur_ids).ante_chronological
|
||||
cookies.encrypted[cookies_export_key] = {
|
||||
value: DateTime.current,
|
||||
expires: Export::MAX_DUREE_GENERATION + Export::MAX_DUREE_CONSERVATION_EXPORT
|
||||
}
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.html
|
||||
end
|
||||
end
|
||||
|
||||
def email_usagers
|
||||
@procedure = procedure
|
||||
@bulk_messages = BulkMessage.includes(:groupe_instructeurs).where(groupe_instructeurs: { procedure: procedure })
|
||||
|
@ -277,10 +289,6 @@ module Instructeurs
|
|||
.permit(:instant_expert_avis_email_notifications_enabled, :instant_email_dossier_notifications_enabled, :instant_email_message_notifications_enabled, :daily_email_notifications_enabled, :weekly_email_notifications_enabled)
|
||||
end
|
||||
|
||||
def assign_exports
|
||||
@exports = Export.find_for_groupe_instructeurs(groupe_instructeur_ids, procedure_presentation)
|
||||
end
|
||||
|
||||
def assign_tos
|
||||
@assign_tos ||= current_instructeur
|
||||
.assign_to
|
||||
|
@ -301,10 +309,6 @@ module Instructeurs
|
|||
@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],
|
||||
|
@ -356,5 +360,22 @@ module Instructeurs
|
|||
def bulk_message_params
|
||||
params.require(:bulk_message).permit(:body)
|
||||
end
|
||||
|
||||
def notify_exports?
|
||||
last_seen_at = begin
|
||||
DateTime.parse(cookies.encrypted[cookies_export_key])
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
scope = Export.generated.for_groupe_instructeurs(groupe_instructeur_ids)
|
||||
scope = scope.where(updated_at: last_seen_at...) if last_seen_at
|
||||
|
||||
scope.exists?
|
||||
end
|
||||
|
||||
def cookies_export_key
|
||||
"exports_#{@procedure.id}_seen_at"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,25 @@
|
|||
module TabsHelper
|
||||
def tab_i18n_key_from_status(status)
|
||||
case status
|
||||
when 'a-suivre'
|
||||
'views.instructeurs.dossiers.tab_steps.to_follow' # i18n-tasks-use t('views.instructeurs.dossiers.tab_steps.to_follow')
|
||||
when 'suivis'
|
||||
'pluralize.followed'
|
||||
when 'traites'
|
||||
'pluralize.processed'
|
||||
when 'tous'
|
||||
'views.instructeurs.dossiers.tab_steps.total' # i18n-tasks-use t('views.instructeurs.dossiers.tab_steps.total')
|
||||
when 'supprimes_recemment'
|
||||
'pluralize.dossiers_supprimes_recemment'
|
||||
when 'expirant'
|
||||
'pluralize.dossiers_close_to_expiration'
|
||||
when 'archives'
|
||||
'pluralize.archived'
|
||||
else
|
||||
fail ArgumentError, "Unknown tab status: #{status}"
|
||||
end
|
||||
end
|
||||
|
||||
def tab_item(label, url, active: false, badge: nil, notification: false)
|
||||
render partial: 'shared/tab_item', locals: {
|
||||
label: label,
|
||||
|
|
|
@ -34,6 +34,8 @@ class Export < ApplicationRecord
|
|||
|
||||
validates :format, :groupe_instructeurs, :key, presence: true
|
||||
|
||||
scope :ante_chronological, -> { order(updated_at: :desc) }
|
||||
|
||||
after_create_commit :compute_async
|
||||
|
||||
FORMATS_WITH_TIME_SPAN = [:xlsx, :ods, :csv].flat_map do |format|
|
||||
|
@ -57,77 +59,39 @@ class Export < ApplicationRecord
|
|||
time_span_type == Export.time_span_types.fetch(:monthly) ? 30.days.ago : nil
|
||||
end
|
||||
|
||||
def old?
|
||||
updated_at < 10.minutes.ago || filters_changed?
|
||||
end
|
||||
|
||||
def filters_changed?
|
||||
procedure_presentation&.snapshot != procedure_presentation_snapshot
|
||||
end
|
||||
|
||||
def filtered?
|
||||
procedure_presentation_id.present?
|
||||
end
|
||||
|
||||
def flash_message
|
||||
if available?
|
||||
"L’export au format \"#{format}\" est prêt. Vous pouvez le <a href=\"#{file.url}\">télécharger</a>"
|
||||
else
|
||||
"Nous générons cet export. Veuillez revenir dans quelques minutes pour le télécharger."
|
||||
end
|
||||
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, force: false)
|
||||
export = create_or_find_export(format, groupe_instructeurs, time_span_type: time_span_type, statut: statut, procedure_presentation: procedure_presentation)
|
||||
|
||||
if export.available? && export.old? && force
|
||||
export.destroy
|
||||
create_or_find_export(format, groupe_instructeurs, time_span_type: time_span_type, statut: statut, procedure_presentation: procedure_presentation)
|
||||
else
|
||||
export
|
||||
end
|
||||
end
|
||||
|
||||
def self.find_for_groupe_instructeurs(groupe_instructeurs_ids, procedure_presentation)
|
||||
exports = if procedure_presentation.present?
|
||||
where(key: generate_cache_key(groupe_instructeurs_ids, procedure_presentation))
|
||||
.or(where(key: generate_cache_key(groupe_instructeurs_ids)))
|
||||
else
|
||||
where(key: generate_cache_key(groupe_instructeurs_ids))
|
||||
end
|
||||
filtered, not_filtered = exports.partition(&:filtered?)
|
||||
|
||||
{
|
||||
xlsx: {
|
||||
time_span_type: not_filtered.filter(&:format_xlsx?).index_by(&:time_span_type),
|
||||
statut: filtered.filter(&:format_xlsx?).index_by(&:statut)
|
||||
},
|
||||
ods: {
|
||||
time_span_type: not_filtered.filter(&:format_ods?).index_by(&:time_span_type),
|
||||
statut: filtered.filter(&:format_ods?).index_by(&:statut)
|
||||
},
|
||||
csv: {
|
||||
time_span_type: not_filtered.filter(&:format_csv?).index_by(&:time_span_type),
|
||||
statut: filtered.filter(&:format_csv?).index_by(&:statut)
|
||||
},
|
||||
zip: {
|
||||
time_span_type: {},
|
||||
statut: filtered.filter(&:format_zip?).index_by(&:statut)
|
||||
},
|
||||
json: {
|
||||
time_span_type: {},
|
||||
statut: filtered.filter(&:format_json?).index_by(&:statut)
|
||||
}
|
||||
def self.find_or_create_fresh_export(format, groupe_instructeurs, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil)
|
||||
attributes = {
|
||||
format:,
|
||||
time_span_type:,
|
||||
statut:,
|
||||
key: generate_cache_key(groupe_instructeurs.map(&:id), procedure_presentation)
|
||||
}
|
||||
|
||||
recent_export = pending
|
||||
.or(generated.where(updated_at: (5.minutes.ago)..))
|
||||
.includes(:procedure_presentation)
|
||||
.find_by(attributes)
|
||||
|
||||
return recent_export if recent_export.present?
|
||||
|
||||
create!(**attributes, groupe_instructeurs:,
|
||||
procedure_presentation:,
|
||||
procedure_presentation_snapshot: procedure_presentation&.snapshot)
|
||||
end
|
||||
|
||||
def self.create_or_find_export(format, groupe_instructeurs, time_span_type:, statut:, 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,
|
||||
statut: statut,
|
||||
key: generate_cache_key(groupe_instructeurs.map(&:id), procedure_presentation))
|
||||
def self.for_groupe_instructeurs(groupe_instructeurs_ids)
|
||||
joins(:groupe_instructeurs).where(groupe_instructeurs: groupe_instructeurs_ids).distinct(:id)
|
||||
end
|
||||
|
||||
def self.by_key(groupe_instructeurs_ids, procedure_presentation)
|
||||
where(key: [
|
||||
generate_cache_key(groupe_instructeurs_ids),
|
||||
generate_cache_key(groupe_instructeurs_ids, procedure_presentation)
|
||||
])
|
||||
end
|
||||
|
||||
def self.generate_cache_key(groupe_instructeurs_ids, procedure_presentation = nil)
|
||||
|
@ -144,7 +108,7 @@ class Export < ApplicationRecord
|
|||
|
||||
def count
|
||||
if procedure_presentation_id.present?
|
||||
dossiers_for_export.size
|
||||
dossiers_for_export.count
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
%h1.mb-2
|
||||
Archives
|
||||
-# index not renderable as administrateur flagged as manager, so render it anyway
|
||||
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, export_url: method(:download_admin_procedure_exports_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_admin_procedure_exports_path))
|
||||
= render Dossiers::ExportLinkComponent.new(procedure: @procedure, exports: @exports, export_url: method(:download_admin_procedure_exports_path))
|
||||
|
||||
= render partial: "shared/archives/notice"
|
||||
= render partial: "shared/archives/table", locals: {count_dossiers_termines_by_month: @count_dossiers_termines_by_month, archives: @archives, average_dossier_weight: @average_dossier_weight, procedure: @procedure }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
-# not renderable as administrateur flagged as manager, so render it anyway
|
||||
- if @can_download_dossiers
|
||||
= turbo_stream.update_all '.procedure-actions' do
|
||||
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, count: @dossiers_count, export_url: method(:admin_procedure_exports_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, count: @dossiers_count, export_url: method(:admin_procedure_exports_path))
|
||||
|
|
|
@ -22,3 +22,7 @@
|
|||
|
||||
|
|
||||
= link_to t('instructeurs.dossiers.header.banner.administrators_list'), administrateurs_instructeur_procedure_path(procedure), class: 'header-link'
|
||||
|
|
||||
= link_to t('instructeurs.dossiers.header.banner.exports_list'), exports_instructeur_procedure_path(procedure), class: 'header-link'
|
||||
- if @has_export_notification
|
||||
%span.notifications{ 'aria-label': t('instructeurs.dossiers.header.banner.exports_notification_label') }
|
||||
|
|
|
@ -1,40 +1,39 @@
|
|||
%nav.tabs.mt-3
|
||||
%ul
|
||||
= tab_item(t('views.instructeurs.dossiers.tab_steps.to_follow'),
|
||||
= tab_item(t(tab_i18n_key_from_status('a-suivre')),
|
||||
instructeur_procedure_path(procedure, statut: 'a-suivre'),
|
||||
active: statut == 'a-suivre',
|
||||
badge: number_with_html_delimiter(a_suivre_count))
|
||||
|
||||
= tab_item(t('pluralize.followed', count: suivis_count),
|
||||
= tab_item(t(tab_i18n_key_from_status('suivis'), count: suivis_count),
|
||||
instructeur_procedure_path(procedure, statut: 'suivis'),
|
||||
active: statut == 'suivis',
|
||||
badge: number_with_html_delimiter(suivis_count),
|
||||
notification: has_en_cours_notifications)
|
||||
|
||||
= tab_item(t('pluralize.processed', count: traites_count),
|
||||
= tab_item(t(tab_i18n_key_from_status('traites'), count: traites_count),
|
||||
instructeur_procedure_path(procedure, statut: 'traites'),
|
||||
active: statut == 'traites',
|
||||
badge: number_with_html_delimiter(traites_count),
|
||||
notification: has_termine_notifications)
|
||||
|
||||
= tab_item(t('views.instructeurs.dossiers.tab_steps.total'),
|
||||
= tab_item(t(tab_i18n_key_from_status('tous')),
|
||||
instructeur_procedure_path(procedure, statut: 'tous'),
|
||||
active: statut == 'tous',
|
||||
badge: number_with_html_delimiter(tous_count))
|
||||
|
||||
= tab_item(t('pluralize.dossiers_supprimes_recemment', count: supprimes_recemment_count),
|
||||
= tab_item(t(tab_i18n_key_from_status('supprimes_recemment'), count: supprimes_recemment_count),
|
||||
instructeur_procedure_path(procedure, statut: 'supprimes_recemment'),
|
||||
active: statut == 'supprimes_recemment',
|
||||
badge: number_with_html_delimiter(supprimes_recemment_count))
|
||||
|
||||
- if procedure.procedure_expires_when_termine_enabled
|
||||
= tab_item(t('pluralize.dossiers_close_to_expiration', count: expirant_count),
|
||||
= tab_item(t(tab_i18n_key_from_status('expirant'), count: expirant_count),
|
||||
instructeur_procedure_path(procedure, statut: 'expirant'),
|
||||
active: statut == 'expirant',
|
||||
badge: number_with_html_delimiter(expirant_count))
|
||||
|
||||
= tab_item(t('pluralize.archived', count: archives_count),
|
||||
= tab_item(t(tab_i18n_key_from_status('archives'), count: archives_count),
|
||||
instructeur_procedure_path(procedure, statut: 'archives'),
|
||||
active: statut == 'archives',
|
||||
badge: number_with_html_delimiter(archives_count))
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
.procedure-actions
|
||||
- if @can_download_dossiers
|
||||
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, export_url: method(:download_export_instructeur_procedure_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, exports: @exports, export_url: method(:download_export_instructeur_procedure_path))
|
||||
|
||||
.fr-container.flex= render partial: "tabs", locals: { procedure: @procedure,
|
||||
statut: @statut,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
- if @can_download_dossiers
|
||||
- if @statut.nil?
|
||||
= turbo_stream.update_all '.procedure-actions' do
|
||||
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, export_url: method(:download_export_instructeur_procedure_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_export_instructeur_procedure_path))
|
||||
- else
|
||||
= turbo_stream.update_all '.dossiers-export' do
|
||||
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count, export_url: method(:download_export_instructeur_procedure_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, statut: @statut, count: @dossiers_count, export_url: method(:download_export_instructeur_procedure_path))
|
||||
|
|
19
app/views/instructeurs/procedures/exports.html.haml
Normal file
19
app/views/instructeurs/procedures/exports.html.haml
Normal file
|
@ -0,0 +1,19 @@
|
|||
- title = "Exports · #{@procedure.libelle}"
|
||||
- content_for(:title, title)
|
||||
|
||||
= render partial: 'administrateurs/breadcrumbs',
|
||||
locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)],
|
||||
[t('.title')]] }
|
||||
|
||||
.fr-container
|
||||
%h1= t('.title')
|
||||
= render Dsfr::CalloutComponent.new(title: nil) do |c|
|
||||
- c.with_body do
|
||||
%p= t('.export_description', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i)
|
||||
|
||||
- if @exports.present?
|
||||
%div{ data: @exports.any?(&:pending?) ? { controller: "turbo-poll", turbo_poll_url_value: "", turbo_poll_interval_value: 10_000, turbo_poll_max_checks_value: 6 } : {} }
|
||||
= render Dossiers::ExportLinkComponent.new(procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
|
||||
|
||||
- else
|
||||
= t('.no_export_html', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i )
|
|
@ -0,0 +1,3 @@
|
|||
= turbo_stream.replace "exports-list" do
|
||||
- if @exports.present?
|
||||
= render Dossiers::ExportLinkComponent.new(procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
.procedure-actions
|
||||
- if @can_download_dossiers
|
||||
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, export_url: method(:download_export_instructeur_procedure_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_export_instructeur_procedure_path))
|
||||
|
||||
.fr-container.flex= render partial: "tabs", locals: { procedure: @procedure,
|
||||
statut: @statut,
|
||||
|
@ -72,7 +72,7 @@
|
|||
|
||||
- if @dossiers_count > 0
|
||||
%span.dossiers-export
|
||||
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
|
||||
|
||||
- if @filtered_sorted_paginated_ids.present? || @current_filters.count > 0
|
||||
= render partial: "dossiers_filter_tags", locals: { procedure: @procedure, procedure_presentation: @procedure_presentation, current_filters: @current_filters, statut: @statut }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
- if flash.any?
|
||||
= turbo_stream.replace 'flash_messages', partial: 'layouts/flash_messages'
|
||||
= turbo_stream.show 'flash_messages'
|
||||
= turbo_stream.hide 'flash_messages', delay: 30000
|
||||
- flash.clear
|
||||
|
||||
|
|
|
@ -13,19 +13,20 @@
|
|||
%li
|
||||
%span.fr-badge des répertoires dossier-{id}/
|
||||
Les répertoires de chaque dossiers terminés (acceptés, refusés et classés sans suite). Chacun de ces répertoires est structuré ainsi :
|
||||
%ul
|
||||
%li
|
||||
%span.fr-badge un repertoire horodatage/
|
||||
contenant le journal d'outil de chaque opération (un fichier terminant en .json) effectuée sur ce dossier
|
||||
%li
|
||||
%span.fr-badge un répertoire dossier/
|
||||
contenant l'attestation de la décision finale sur ce dossier
|
||||
%li
|
||||
%span.fr-badge un répertoire pieces_justificatives/
|
||||
contenant les pièces justificatives associées à ce dossier
|
||||
%li
|
||||
%span.fr-badge le dossier
|
||||
exporté au format PDF
|
||||
%li.list-style-type-none
|
||||
%ul
|
||||
%li
|
||||
%span.fr-badge un repertoire horodatage/
|
||||
contenant le journal d'outil de chaque opération (un fichier terminant en .json) effectuée sur ce dossier
|
||||
%li
|
||||
%span.fr-badge un répertoire dossier/
|
||||
contenant l'attestation de la décision finale sur ce dossier
|
||||
%li
|
||||
%span.fr-badge un répertoire pieces_justificatives/
|
||||
contenant les pièces justificatives associées à ce dossier
|
||||
%li
|
||||
%span.fr-badge le dossier
|
||||
exporté au format PDF
|
||||
%li
|
||||
%span.fr-badge un répertoire bills/
|
||||
contenant l'horodatage signé de toute la plateforme pour chaque jour où une opération a été effectuée sur l'un des dossiers présent dans l'archive
|
||||
|
|
5
config/locales/views/administrateurs/exports/en.yml
Normal file
5
config/locales/views/administrateurs/exports/en.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
en:
|
||||
administrateurs:
|
||||
exports:
|
||||
export_available_html: The export in %{file_format} format is ready. You can <a href="%{file_url}">download</a>
|
||||
export_pending: We generate this export. Please come back in a few minutes to download it.
|
5
config/locales/views/administrateurs/exports/fr.yml
Normal file
5
config/locales/views/administrateurs/exports/fr.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
fr:
|
||||
administrateurs:
|
||||
exports:
|
||||
export_available_html: L’export au format %{file_format} est prêt. Vous pouvez le <a href="%{file_url}">télécharger</a>
|
||||
export_pending: Nous générons cet export. Veuillez revenir dans quelques minutes pour le télécharger.
|
|
@ -12,6 +12,8 @@ en:
|
|||
button_delay_expiration: "Keep for one more month"
|
||||
notification_management: notification management
|
||||
administrators_list: administrators list
|
||||
exports_list: exports list
|
||||
exports_notification_label: A new export is ready to download
|
||||
statistics: statistics
|
||||
instructeurs: instructors
|
||||
contact_users: contact users (draft)
|
||||
|
|
|
@ -12,6 +12,8 @@ fr:
|
|||
button_delay_expiration: "Conserver un mois de plus"
|
||||
notification_management: gestion des notifications
|
||||
administrators_list: voir les administrateurs
|
||||
exports_list: voir les exports
|
||||
exports_notification_label: Un nouvel export est prêt à être téléchargé
|
||||
statistics: statistiques
|
||||
instructeurs: instructeurs
|
||||
contact_users: contacter les usagers (brouillon)
|
||||
|
|
|
@ -23,3 +23,5 @@ en:
|
|||
title: "%{procedure_libelle} - n°%{procedure_id} - administrators"
|
||||
stats:
|
||||
title: Statistics
|
||||
exports:
|
||||
title: Exports
|
||||
|
|
11
config/locales/views/instructeurs/procedures/exports/en.yml
Normal file
11
config/locales/views/instructeurs/procedures/exports/en.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
en:
|
||||
instructeurs:
|
||||
procedures:
|
||||
export_available_html: The export in %{file_format} format is ready. You can <a href="%{file_url}">download</a>
|
||||
export_pending_html: We generate this export. You will be able to download it in a few minutes from <a href="%{url}">the exports list</a>.
|
||||
exports:
|
||||
title: Exports list
|
||||
export_description: |
|
||||
This list of exports contains the last exports you requested as well as those requested by instructors belonging to the same group.
|
||||
They are available for %{expiration_time} hours after generation.
|
||||
no_export_html: You have no export at the moment. <br> Can't find an export? It may have expired, exports are deleted after %{expiration_time} hours.
|
11
config/locales/views/instructeurs/procedures/exports/fr.yml
Normal file
11
config/locales/views/instructeurs/procedures/exports/fr.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
fr:
|
||||
instructeurs:
|
||||
procedures:
|
||||
export_available_html: L’export au format %{file_format} est prêt. Vous pouvez le <a href="%{file_url}">télécharger</a>
|
||||
export_pending_html: Nous générons cet export. Vous pourrez le télécharger dans quelques minutes depuis <a href="%{url}">la liste des exports</a>.
|
||||
exports:
|
||||
title: Liste des exports
|
||||
export_description: |
|
||||
Cette liste d'exports contient les derniers exports que vous avez demandés ainsi que ceux demandés par les instructeurs appartenant au même groupe.
|
||||
Ils sont disponibles pendant %{expiration_time} heures après leur génération.
|
||||
no_export_html: Vous n'avez pas d'export pour le moment. <br> Vous ne trouvez pas un export ? Il a peut-être expiré, les exports sont supprimés au bout de %{expiration_time} heures.
|
|
@ -415,6 +415,7 @@ Rails.application.routes.draw do
|
|||
get 'download_export'
|
||||
post 'download_export'
|
||||
get 'stats'
|
||||
get 'exports'
|
||||
get 'email_notifications'
|
||||
get 'administrateurs'
|
||||
patch 'update_email_notifications'
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class RemoveExportsUnicityConstraint < ActiveRecord::Migration[7.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
remove_index :exports, ["format", "time_span_type", "statut", "key"], unique: true
|
||||
|
||||
add_index :exports, "key", unique: false, algorithm: :concurrently
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_08_14_091648) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_09_28_083809) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
|
@ -539,7 +539,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_14_091648) do
|
|||
t.string "statut", default: "tous"
|
||||
t.string "time_span_type", default: "everything", null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
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 ["key"], name: "index_exports_on_key"
|
||||
t.index ["procedure_presentation_id"], name: "index_exports_on_procedure_presentation_id"
|
||||
end
|
||||
|
||||
|
|
55
spec/components/dossiers/export_link_component_spec.rb
Normal file
55
spec/components/dossiers/export_link_component_spec.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
RSpec.describe Dossiers::ExportLinkComponent, type: :component do
|
||||
let(:procedure) { create(:procedure) }
|
||||
let(:groupe_instructeur) { create(:groupe_instructeur, procedure: procedure) }
|
||||
let(:export) { create(:export, groupe_instructeurs: [groupe_instructeur], updated_at: 5.minutes.ago, created_at: 10.minutes.ago) }
|
||||
let(:export_url) { double("ExportUrl", call: "/some/fake/path") }
|
||||
|
||||
let(:component) { described_class.new(procedure:, exports: [export], export_url:) }
|
||||
|
||||
describe "rendering" do
|
||||
subject { render_inline(component).to_html }
|
||||
|
||||
context "when the export is available" do
|
||||
before do
|
||||
allow(export).to receive(:available?).and_return(true)
|
||||
attachment = ActiveStorage::Attachment.new(name: "export", record: export, blob: ActiveStorage::Blob.new(byte_size: 10.kilobytes, content_type: "text/csv", filename: "export.csv"))
|
||||
allow(export).to receive(:file).and_return(attachment)
|
||||
end
|
||||
|
||||
it "displays the time info" do
|
||||
expect(subject).to include("généré il y a 5 minutes")
|
||||
end
|
||||
|
||||
it "displays the download button with the correct label" do
|
||||
expect(subject).to include("Télécharger")
|
||||
expect(subject).to include("CSV")
|
||||
expect(subject).to include("10 ko")
|
||||
end
|
||||
end
|
||||
|
||||
context "when the export is not available" do
|
||||
before do
|
||||
allow(export).to receive(:available?).and_return(false)
|
||||
allow(export).to receive(:failed?).and_return(false)
|
||||
end
|
||||
|
||||
it "displays the pending label" do
|
||||
expect(subject).to include("demandé il y a 10 minutes")
|
||||
end
|
||||
|
||||
it "displays a refresh page button" do
|
||||
expect(subject).to include("Recharger")
|
||||
end
|
||||
end
|
||||
|
||||
context "when the export has failed" do
|
||||
before do
|
||||
allow(export).to receive(:failed?).and_return(true)
|
||||
end
|
||||
|
||||
it "displays the refresh old export button" do
|
||||
expect(subject).to include("Regénérer")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -466,6 +466,48 @@ describe Instructeurs::ProceduresController, type: :controller do
|
|||
it { expect(assigns(:filtered_sorted_paginated_ids)).to match_array([archived_dossier].map(&:id)) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'exports notification' do
|
||||
context 'without generated export' do
|
||||
before do
|
||||
create(:export, :pending, groupe_instructeurs: [gi_2])
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it { expect(assigns(:has_export_notification)).to be(false) }
|
||||
end
|
||||
|
||||
context 'with generated export' do
|
||||
render_views
|
||||
before do
|
||||
create(:export, :generated, groupe_instructeurs: [gi_2], updated_at: 1.minute.ago)
|
||||
|
||||
if exports_seen_at
|
||||
cookies.encrypted["exports_#{procedure.id}_seen_at"] = exports_seen_at.to_datetime.to_s
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'without cookie' do
|
||||
let(:exports_seen_at) { nil }
|
||||
it { expect(assigns(:has_export_notification)).to be(true) }
|
||||
end
|
||||
|
||||
context 'with cookie in past' do
|
||||
let(:exports_seen_at) { 1.hour.ago }
|
||||
it { expect(assigns(:has_export_notification)).to be(true) }
|
||||
|
||||
it { expect(response.body).to match(/Un nouvel export est prêt/) }
|
||||
end
|
||||
|
||||
context 'with cookie set after last generated export' do
|
||||
let(:exports_seen_at) { 10.seconds.ago }
|
||||
it { expect(assigns(:has_export_notification)).to be(false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -586,9 +628,9 @@ describe Instructeurs::ProceduresController, type: :controller do
|
|||
get :download_export, params: { export_format: :csv, procedure_id: procedure.id }
|
||||
end
|
||||
|
||||
context 'when the export is does not exist' do
|
||||
context 'when the export does not exist' do
|
||||
it 'displays an notice' do
|
||||
is_expected.to redirect_to(instructeur_procedure_url(procedure))
|
||||
is_expected.to redirect_to(exports_instructeur_procedure_url(procedure))
|
||||
expect(flash.notice).to be_present
|
||||
end
|
||||
|
||||
|
@ -601,7 +643,7 @@ describe Instructeurs::ProceduresController, type: :controller do
|
|||
end
|
||||
|
||||
it 'displays an notice' do
|
||||
is_expected.to redirect_to(instructeur_procedure_url(procedure))
|
||||
is_expected.to redirect_to(exports_instructeur_procedure_url(procedure))
|
||||
expect(flash.notice).to be_present
|
||||
end
|
||||
end
|
||||
|
@ -627,7 +669,7 @@ describe Instructeurs::ProceduresController, type: :controller do
|
|||
end
|
||||
|
||||
it 'displays an notice' do
|
||||
is_expected.to redirect_to(instructeur_procedure_url(procedure))
|
||||
is_expected.to redirect_to(exports_instructeur_procedure_url(procedure))
|
||||
expect(flash.notice).to be_present
|
||||
end
|
||||
end
|
||||
|
@ -650,4 +692,41 @@ describe Instructeurs::ProceduresController, type: :controller do
|
|||
it { is_expected.to have_http_status(:forbidden) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#exports' do
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
let!(:procedure) { create(:procedure) }
|
||||
let!(:assign_to) { create(:assign_to, instructeur: instructeur, groupe_instructeur: build(:groupe_instructeur, procedure: procedure), manager: manager) }
|
||||
let!(:gi_0) { assign_to.groupe_instructeur }
|
||||
let!(:gi_1) { create(:groupe_instructeur, label: 'gi_1', procedure: procedure, instructeurs: [instructeur]) }
|
||||
let(:manager) { false }
|
||||
before { sign_in(instructeur.user) }
|
||||
|
||||
subject do
|
||||
get :exports, params: { procedure_id: procedure.id }
|
||||
end
|
||||
|
||||
context 'when there is one export in the instructeurs group' do
|
||||
let!(:export) { create(:export, groupe_instructeurs: [gi_1]) }
|
||||
it 'retrieves the export' do
|
||||
subject
|
||||
expect(assigns(:exports)).to eq([export])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is one export in another instructeurs group' do
|
||||
let!(:instructeur_2) { create(:instructeur) }
|
||||
let!(:gi_2) { create(:groupe_instructeur, label: 'gi_2', procedure: procedure, instructeurs: [instructeur_2]) }
|
||||
let!(:export) { create(:export, groupe_instructeurs: [gi_2]) }
|
||||
it 'does not retrieved the export' do
|
||||
subject
|
||||
expect(assigns(:exports)).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when logged in through super admin' do
|
||||
let(:manager) { true }
|
||||
it { is_expected.to have_http_status(:forbidden) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -61,9 +61,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], nil)).to eq({ csv: { statut: {}, time_span_type: {} }, xlsx: { statut: {}, time_span_type: {} }, ods: { statut: {}, time_span_type: {} }, zip: { statut: {}, time_span_type: {} }, json: { 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: {} }, zip: { statut: {}, time_span_type: {} }, json: { 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: {} }, zip: { statut: {}, time_span_type: {} }, json: { statut: {}, time_span_type: {} } }) }
|
||||
it { expect(Export.by_key([gi_1.id], nil)).to be_empty }
|
||||
it { expect(Export.by_key([gi_2.id, gi_1.id], nil)).to eq([export]) }
|
||||
it { expect(Export.by_key([gi_1.id, gi_2.id, gi_3.id], nil)).to be_empty }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -73,19 +73,13 @@ RSpec.describe Export, type: :model do
|
|||
let!(:procedure_presentation) { create(:procedure_presentation, procedure: gi_1.procedure) }
|
||||
|
||||
it 'find global exports as well as filtered one' do
|
||||
expect(Export.find_for_groupe_instructeurs([gi_2.id, gi_1.id], export_with_filter.procedure_presentation))
|
||||
.to eq({
|
||||
csv: { statut: { Export.statuts.fetch(:suivis) => export_with_filter }, time_span_type: { 'everything' => export_global } },
|
||||
xlsx: { statut: {}, time_span_type: {} },
|
||||
ods: { statut: {}, time_span_type: {} },
|
||||
zip: { statut: {}, time_span_type: {} },
|
||||
json: { statut: {}, time_span_type: {} }
|
||||
})
|
||||
expect(Export.by_key([gi_2.id, gi_1.id], export_with_filter.procedure_presentation))
|
||||
.to contain_exactly(export_with_filter, export_global)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.find_or_create_export' do
|
||||
describe '.find_or_create_fresh_export' do
|
||||
let!(:procedure) { create(:procedure) }
|
||||
let!(:gi_1) { create(:groupe_instructeur, procedure: procedure, instructeurs: [create(:instructeur)]) }
|
||||
let!(:pp) { gi_1.instructeurs.first.procedure_presentation_and_errors_for_procedure_id(procedure.id).first }
|
||||
|
@ -93,18 +87,50 @@ RSpec.describe Export, type: :model do
|
|||
|
||||
context 'with procedure_presentation having different filters' do
|
||||
it 'works once' do
|
||||
expect { Export.find_or_create_export(:zip, [gi_1], time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) }
|
||||
expect { Export.find_or_create_fresh_export(:zip, [gi_1], time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) }
|
||||
.to change { Export.count }.by(1)
|
||||
end
|
||||
|
||||
it 'works once, changes procedure_presentation, recreate a new' do
|
||||
expect { Export.find_or_create_export(:zip, [gi_1], time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) }
|
||||
expect { Export.find_or_create_fresh_export(:zip, [gi_1], time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) }
|
||||
.to change { Export.count }.by(1)
|
||||
pp.add_filter('tous', 'self/updated_at', '10/12/2021')
|
||||
expect { Export.find_or_create_export(:zip, [gi_1], time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) }
|
||||
expect { Export.find_or_create_fresh_export(:zip, [gi_1], time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) }
|
||||
.to change { Export.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with existing matching export' do
|
||||
def find_or_create =
|
||||
Export.find_or_create_fresh_export(:zip, [gi_1], time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp)
|
||||
|
||||
context 'freshly generate export' do
|
||||
before { find_or_create.update!(job_status: :generated, updated_at: 1.second.ago) }
|
||||
|
||||
it 'returns current pending export' do
|
||||
current_export = find_or_create
|
||||
|
||||
expect(find_or_create).to eq(current_export)
|
||||
end
|
||||
end
|
||||
|
||||
context 'old generated export' do
|
||||
before { find_or_create.update!(job_status: :generated, updated_at: 1.hour.ago) }
|
||||
|
||||
it 'returns a new export' do
|
||||
expect { find_or_create }.to change { Export.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'pending export' do
|
||||
before { find_or_create.update!(updated_at: 1.hour.ago) }
|
||||
|
||||
it 'returns current pending export' do
|
||||
current_export = find_or_create
|
||||
expect(find_or_create).to eq(current_export)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.dossiers_for_export' do
|
||||
|
@ -147,4 +173,28 @@ RSpec.describe Export, type: :model do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.for_groupe_instructeurs' do
|
||||
let!(:groupe_instructeur1) { create(:groupe_instructeur) }
|
||||
let!(:groupe_instructeur2) { create(:groupe_instructeur) }
|
||||
let!(:groupe_instructeur3) { create(:groupe_instructeur) }
|
||||
|
||||
let!(:export1) { create(:export, groupe_instructeurs: [groupe_instructeur1, groupe_instructeur2]) }
|
||||
let!(:export2) { create(:export, groupe_instructeurs: [groupe_instructeur2]) }
|
||||
let!(:export3) { create(:export, groupe_instructeurs: [groupe_instructeur3]) }
|
||||
|
||||
it 'returns exports for the specified groupe instructeurs' do
|
||||
expect(Export.for_groupe_instructeurs([groupe_instructeur1.id, groupe_instructeur2.id]))
|
||||
.to match_array([export1, export2])
|
||||
end
|
||||
|
||||
it 'does not return exports not associated with the specified groupe instructeurs' do
|
||||
expect(Export.for_groupe_instructeurs([groupe_instructeur1.id])).not_to include(export2, export3)
|
||||
end
|
||||
|
||||
it 'returns unique exports even if they belong to multiple matching groupe instructeurs' do
|
||||
results = Export.for_groupe_instructeurs([groupe_instructeur1.id])
|
||||
expect(results.count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -120,15 +120,17 @@ describe 'Instructing a dossier:', js: true, retry: 3 do
|
|||
end
|
||||
|
||||
expect(page).to have_text('Nous générons cet export.')
|
||||
click_on "Télécharger un dossier"
|
||||
expect(page).to have_text('Un export au format .csv est en train d’être généré')
|
||||
|
||||
click_on "voir les exports"
|
||||
expect(page).to have_text("Export .csv d’un dossier « à suivre » demandé il y a moins d'une minute")
|
||||
expect(page).to have_text("En préparation")
|
||||
|
||||
assert_performed_jobs 2 do
|
||||
perform_enqueued_jobs(only: ExportJob)
|
||||
end
|
||||
page.driver.browser.navigate.refresh
|
||||
|
||||
click_on "Télécharger un dossier"
|
||||
expect(page).to have_text('Télécharger l’export au format .csv')
|
||||
page.driver.browser.navigate.refresh
|
||||
expect(page).to have_text('Télécharger l’export')
|
||||
end
|
||||
|
||||
scenario 'A instructeur can see the personnes impliquées' do
|
||||
|
|
Loading…
Reference in a new issue