Merge pull request #7077 from tchak/feat-export-with-filters
feat(exports): add ability to create exports with filters
This commit is contained in:
commit
5629d99a4a
17 changed files with 299 additions and 93 deletions
|
@ -76,10 +76,11 @@ module Instructeurs
|
||||||
@has_termine_notifications = notifications[:termines].present?
|
@has_termine_notifications = notifications[:termines].present?
|
||||||
@not_archived_notifications_dossier_ids = notifications[:en_cours] + notifications[:termines]
|
@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
|
page = params[:page].presence || 1
|
||||||
|
|
||||||
|
@dossiers_count = filtered_sorted_ids.size
|
||||||
@filtered_sorted_paginated_ids = Kaminari
|
@filtered_sorted_paginated_ids = Kaminari
|
||||||
.paginate_array(filtered_sorted_ids)
|
.paginate_array(filtered_sorted_ids)
|
||||||
.page(page)
|
.page(page)
|
||||||
|
@ -137,8 +138,6 @@ module Instructeurs
|
||||||
end
|
end
|
||||||
|
|
||||||
def download_export
|
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 = current_instructeur
|
||||||
.groupe_instructeurs
|
.groupe_instructeurs
|
||||||
.where(procedure: procedure)
|
.where(procedure: procedure)
|
||||||
|
@ -148,17 +147,19 @@ module Instructeurs
|
||||||
.visible_by_administration
|
.visible_by_administration
|
||||||
.exists?(groupe_instructeur_id: groupe_instructeur_ids)
|
.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.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
|
end
|
||||||
|
|
||||||
if export.ready?
|
if export.ready?
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.js do
|
format.js do
|
||||||
@procedure = procedure
|
@procedure = procedure
|
||||||
|
@statut = export_options[:statut]
|
||||||
|
@dossiers_count = export.count
|
||||||
assign_exports
|
assign_exports
|
||||||
flash.notice = "L’export au format \"#{export_format}\" est prêt. Vous pouvez le <a href=\"#{export.file.service_url}\">télécharger</a>"
|
flash.notice = "L’export au format \"#{export_format}\" est prêt. Vous pouvez le <a href=\"#{export.file.service_url}\">télécharger</a>"
|
||||||
end
|
end
|
||||||
|
@ -173,6 +174,8 @@ module Instructeurs
|
||||||
|
|
||||||
format.js do
|
format.js do
|
||||||
@procedure = procedure
|
@procedure = procedure
|
||||||
|
@statut = export_options[:statut]
|
||||||
|
@dossiers_count = export.count
|
||||||
assign_exports
|
assign_exports
|
||||||
if !params[:no_progress_notification]
|
if !params[:no_progress_notification]
|
||||||
flash.notice = notice_message
|
flash.notice = notice_message
|
||||||
|
@ -261,7 +264,7 @@ module Instructeurs
|
||||||
end
|
end
|
||||||
|
|
||||||
def assign_exports
|
def assign_exports
|
||||||
@exports = Export.find_for_groupe_instructeurs(groupe_instructeur_ids)
|
@exports = Export.find_for_groupe_instructeurs(groupe_instructeur_ids, procedure_presentation)
|
||||||
end
|
end
|
||||||
|
|
||||||
def assign_tos
|
def assign_tos
|
||||||
|
@ -280,6 +283,22 @@ module Instructeurs
|
||||||
@statut ||= (params[:statut].presence || 'a-suivre')
|
@statut ||= (params[:statut].presence || 'a-suivre')
|
||||||
end
|
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
|
def procedure_id
|
||||||
params[:procedure_id]
|
params[:procedure_id]
|
||||||
end
|
end
|
||||||
|
|
|
@ -109,9 +109,23 @@ module DossierHelper
|
||||||
"#{base_url}/entreprise/#{siren}"
|
"#{base_url}/entreprise/#{siren}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def exports_list(exports)
|
def exports_list(exports, statut = nil)
|
||||||
Export::FORMATS.map do |(format, time_span_type)|
|
if statut
|
||||||
[format, time_span_type, exports[format] && exports[format][time_span_type]]
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,12 +2,15 @@
|
||||||
#
|
#
|
||||||
# Table name: exports
|
# Table name: exports
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# format :string not null
|
# format :string not null
|
||||||
# key :text not null
|
# key :text not null
|
||||||
# time_span_type :string default("everything"), not null
|
# procedure_presentation_snapshot :jsonb
|
||||||
# created_at :datetime not null
|
# statut :string default("tous")
|
||||||
# updated_at :datetime not null
|
# 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
|
class Export < ApplicationRecord
|
||||||
MAX_DUREE_CONSERVATION_EXPORT = 3.hours
|
MAX_DUREE_CONSERVATION_EXPORT = 3.hours
|
||||||
|
@ -23,7 +26,18 @@ class Export < ApplicationRecord
|
||||||
monthly: 'monthly'
|
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
|
has_and_belongs_to_many :groupe_instructeurs
|
||||||
|
belongs_to :procedure_presentation, optional: true
|
||||||
|
|
||||||
has_one_attached :file
|
has_one_attached :file
|
||||||
|
|
||||||
|
@ -33,19 +47,24 @@ class Export < ApplicationRecord
|
||||||
|
|
||||||
after_create_commit :compute_async
|
after_create_commit :compute_async
|
||||||
|
|
||||||
FORMATS = [:xlsx, :ods, :csv].flat_map do |format|
|
FORMATS_WITH_TIME_SPAN = [:xlsx, :ods, :csv].flat_map do |format|
|
||||||
Export.time_span_types.values.map do |time_span_type|
|
time_span_types.keys.map do |time_span_type|
|
||||||
[format, time_span_type]
|
{ format: format.to_sym, time_span_type: time_span_type }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
FORMATS = [:xlsx, :ods, :csv].map do |format|
|
||||||
|
{ format: format.to_sym }
|
||||||
|
end
|
||||||
|
|
||||||
def compute_async
|
def compute_async
|
||||||
ExportJob.perform_later(self)
|
ExportJob.perform_later(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
def compute
|
def compute
|
||||||
|
load_snapshot!
|
||||||
|
|
||||||
file.attach(
|
file.attach(
|
||||||
io: io(since: since),
|
io: io,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
content_type: content_type,
|
content_type: content_type,
|
||||||
# We generate the exports ourselves, so they are safe
|
# We generate the exports ourselves, so they are safe
|
||||||
|
@ -62,47 +81,109 @@ class Export < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def old?
|
def old?
|
||||||
updated_at < 20.minutes.ago
|
updated_at < 20.minutes.ago || filters_changed?
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.find_or_create_export(format, time_span_type, groupe_instructeurs)
|
def filters_changed?
|
||||||
create_with(groupe_instructeurs: groupe_instructeurs)
|
procedure_presentation&.snapshot != procedure_presentation_snapshot
|
||||||
|
end
|
||||||
|
|
||||||
|
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, procedure_presentation_snapshot: procedure_presentation&.snapshot)
|
||||||
|
.includes(:procedure_presentation)
|
||||||
.create_or_find_by(format: format,
|
.create_or_find_by(format: format,
|
||||||
time_span_type: time_span_type,
|
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
|
end
|
||||||
|
|
||||||
def self.find_for_groupe_instructeurs(groupe_instructeurs_ids)
|
def self.find_for_groupe_instructeurs(groupe_instructeurs_ids, procedure_presentation)
|
||||||
exports = where(key: generate_cache_key(groupe_instructeurs_ids))
|
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|
|
{
|
||||||
[
|
xlsx: {
|
||||||
format,
|
time_span_type: not_filtered.filter(&:xlsx?).index_by(&:time_span_type),
|
||||||
Export.time_span_types.values.map do |time_span_type|
|
statut: filtered.filter(&:xlsx?).index_by(&:statut)
|
||||||
[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
|
ods: {
|
||||||
]
|
time_span_type: not_filtered.filter(&:ods?).index_by(&:time_span_type),
|
||||||
end.filter { |(_, exports)| exports.present? }.to_h
|
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
|
end
|
||||||
|
|
||||||
def self.generate_cache_key(groupe_instructeurs_ids)
|
def self.generate_cache_key(groupe_instructeurs_ids, procedure_presentation_id = nil)
|
||||||
groupe_instructeurs_ids.sort.join('-')
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filename
|
def load_snapshot!
|
||||||
procedure_identifier = procedure.path || "procedure-#{procedure.id}"
|
if procedure_presentation_snapshot.present?
|
||||||
"dossiers_#{procedure_identifier}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M')}.#{format}"
|
procedure_presentation.attributes = procedure_presentation_snapshot
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def io(since: nil)
|
def dossiers_for_export
|
||||||
dossiers = Dossier.visible_by_administration
|
@dossiers_for_export ||= begin
|
||||||
.where(groupe_instructeur: groupe_instructeurs)
|
dossiers = Dossier.where(groupe_instructeur: groupe_instructeurs)
|
||||||
if since.present?
|
|
||||||
dossiers = dossiers.where('dossiers.depose_at > ?', since)
|
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, statut)
|
||||||
|
|
||||||
|
dossiers.where(id: filtered_sorted_ids)
|
||||||
|
else
|
||||||
|
dossiers
|
||||||
|
end
|
||||||
end
|
end
|
||||||
service = ProcedureExportService.new(procedure, dossiers)
|
end
|
||||||
|
|
||||||
|
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
|
case format.to_sym
|
||||||
when :csv
|
when :csv
|
||||||
|
|
|
@ -25,6 +25,8 @@ class ProcedurePresentation < ApplicationRecord
|
||||||
FILTERS_VALUE_MAX_LENGTH = 100
|
FILTERS_VALUE_MAX_LENGTH = 100
|
||||||
|
|
||||||
belongs_to :assign_to, optional: false
|
belongs_to :assign_to, optional: false
|
||||||
|
has_many :exports, dependent: :destroy
|
||||||
|
|
||||||
delegate :procedure, :instructeur, to: :assign_to
|
delegate :procedure, :instructeur, to: :assign_to
|
||||||
|
|
||||||
validate :check_allowed_displayed_fields
|
validate :check_allowed_displayed_fields
|
||||||
|
@ -180,9 +182,9 @@ class ProcedurePresentation < ApplicationRecord
|
||||||
end.reduce(:&)
|
end.reduce(:&)
|
||||||
end
|
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_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?
|
if filters[statut].present?
|
||||||
filtered_ids(dossiers_by_statut, statut).intersection(dossiers_sorted_ids)
|
filtered_ids(dossiers_by_statut, statut).intersection(dossiers_sorted_ids)
|
||||||
|
@ -261,6 +263,10 @@ class ProcedurePresentation < ApplicationRecord
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def snapshot
|
||||||
|
slice(:filters, :sort, :displayed_fields)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def field_id(field)
|
def field_id(field)
|
||||||
|
|
19
app/views/instructeurs/procedures/_dossiers_export.html.haml
Normal file
19
app/views/instructeurs/procedures/_dossiers_export.html.haml
Normal file
|
@ -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}")
|
24
app/views/instructeurs/procedures/_dossiers_filter.html.haml
Normal file
24
app/views/instructeurs/procedures/_dossiers_filter.html.haml
Normal file
|
@ -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)}"
|
|
@ -3,7 +3,10 @@
|
||||||
Télécharger tous les dossiers
|
Télécharger tous les dossiers
|
||||||
#download-menu.dropdown-content.fade-in-down{ style: 'width: 450px' }
|
#download-menu.dropdown-content.fade-in-down{ style: 'width: 450px' }
|
||||||
%ul.dropdown-items
|
%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
|
%li
|
||||||
- if export.nil?
|
- 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
|
= 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
|
= 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
|
.icon.retry
|
||||||
- else
|
- 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])
|
= t("export_#{time_span_type}_pending_html", export_time: time_ago_in_words(export.created_at), export_format: ".#{format}", scope: [:instructeurs, :procedure])
|
||||||
%li
|
%li
|
||||||
= link_to t(:download_archive, scope: [:instructeurs, :procedure]), instructeur_archives_path(procedure)
|
= link_to t(:download_archive, scope: [:instructeurs, :procedure]), instructeur_archives_path(procedure)
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
<% if @can_download_dossiers %>
|
<% 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 %>
|
<% end %>
|
||||||
|
|
||||||
<% @exports.each do |format, exports| %>
|
<% @exports.values.each do |exports| %>
|
||||||
<% exports.each do |time_span_type, export| %>
|
<% if @statut.present? %>
|
||||||
<% if !export.ready? %>
|
<% export = exports[:statut][@statut] %>
|
||||||
<%= 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) %>
|
<% 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 %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -52,34 +52,16 @@
|
||||||
- if @statut == 'expirant'
|
- if @statut == 'expirant'
|
||||||
%p.explication-onglet Les dossiers n'expireront pas avant la période de conservation des données.
|
%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
|
- if @filtered_sorted_paginated_ids.present? || @current_filters.count > 0
|
||||||
- pagination = paginate @filtered_sorted_paginated_ids
|
- pagination = paginate @filtered_sorted_paginated_ids
|
||||||
= pagination
|
= pagination
|
||||||
%span.dropdown
|
.flex
|
||||||
%button.button.dropdown-button{ 'aria-expanded' => 'false', 'aria-controls' => 'filter-menu' }
|
.flex-grow
|
||||||
Filtrer
|
= render partial: "dossiers_filter", locals: { procedure: @procedure, procedure_presentation: @procedure_presentation, current_filters: @current_filters, statut: @statut, displayed_fields_options: @displayed_fields_options }
|
||||||
#filter-menu.dropdown-content.left-aligned.fade-in-down
|
- if @dossiers_count > 0
|
||||||
= form_tag add_filter_instructeur_procedure_path(@procedure), method: :post, class: 'dropdown-form large' do
|
.dossiers-export
|
||||||
= label_tag :field, "Colonne"
|
= render partial: "dossiers_export", locals: { procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count }
|
||||||
= 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)}"
|
|
||||||
%table.table.dossiers-table.hoverable
|
%table.table.dossiers-table.hoverable
|
||||||
%thead
|
%thead
|
||||||
%tr
|
%tr
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
"type": "controller",
|
"type": "controller",
|
||||||
"class": "Users::DossiersController",
|
"class": "Users::DossiersController",
|
||||||
"method": "merci",
|
"method": "merci",
|
||||||
"line": 196,
|
"line": 201,
|
||||||
"file": "app/controllers/users/dossiers_controller.rb",
|
"file": "app/controllers/users/dossiers_controller.rb",
|
||||||
"rendered": {
|
"rendered": {
|
||||||
"name": "users/dossiers/merci",
|
"name": "users/dossiers/merci",
|
||||||
|
@ -115,24 +115,24 @@
|
||||||
{
|
{
|
||||||
"warning_type": "Redirect",
|
"warning_type": "Redirect",
|
||||||
"warning_code": 18,
|
"warning_code": 18,
|
||||||
"fingerprint": "c46b5c9cd6474ffae789f39a2280ba6b5a5a74d3ffa8a38cf8a409f9a027ed0e",
|
"fingerprint": "eae88be293b3849e07be81a18de99c04b08c7b2c045cc4a43ca3624ea178d965",
|
||||||
"check_name": "Redirect",
|
"check_name": "Redirect",
|
||||||
"message": "Possible unprotected redirect",
|
"message": "Possible unprotected redirect",
|
||||||
"file": "app/controllers/instructeurs/procedures_controller.rb",
|
"file": "app/controllers/instructeurs/procedures_controller.rb",
|
||||||
"line": 202,
|
"line": 191,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
|
"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,
|
"render_path": null,
|
||||||
"location": {
|
"location": {
|
||||||
"type": "method",
|
"type": "method",
|
||||||
"class": "Instructeurs::ProceduresController",
|
"class": "Instructeurs::ProceduresController",
|
||||||
"method": "download_export"
|
"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",
|
"confidence": "High",
|
||||||
"note": ""
|
"note": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated": "2022-02-22 15:46:39 +0100",
|
"updated": "2022-04-05 14:21:07 +0200",
|
||||||
"brakeman_version": "5.1.1"
|
"brakeman_version": "5.1.1"
|
||||||
}
|
}
|
||||||
|
|
13
config/locales/views/instructeurs/procedures/fr.yml
Normal file
13
config/locales/views/instructeurs/procedures/fr.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
fr:
|
||||||
|
instructeurs:
|
||||||
|
procedures:
|
||||||
|
dossiers_export:
|
||||||
|
everything_csv_html: Demander un export au format .csv<br>(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é<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:
|
||||||
|
one: Télécharger un dossier
|
||||||
|
other: Télécharger %{count} dossiers
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddProcedurePresentationSnapshotToExports < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :exports, :procedure_presentation_snapshot, :jsonb
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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_163206) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -431,9 +431,13 @@ ActiveRecord::Schema.define(version: 2022_04_05_100354) do
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.string "format", null: false
|
t.string "format", null: false
|
||||||
t.text "key", 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.string "time_span_type", default: "everything", null: false
|
||||||
t.datetime "updated_at", 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
|
end
|
||||||
|
|
||||||
create_table "exports_groupe_instructeurs", force: :cascade do |t|
|
create_table "exports_groupe_instructeurs", force: :cascade do |t|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :export 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) }
|
time_span_type { Export.time_span_types.fetch(:everything) }
|
||||||
groupe_instructeurs { [association(:groupe_instructeur)] }
|
groupe_instructeurs { [association(:groupe_instructeur)] }
|
||||||
|
|
||||||
after(:build) do |export, _evaluator|
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -48,9 +48,9 @@ RSpec.describe Export, type: :model do
|
||||||
context 'when an export is made for one groupe instructeur' do
|
context 'when an export is made for one groupe instructeur' do
|
||||||
let!(:export) { create(:export, groupe_instructeurs: [gi_1, gi_2]) }
|
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_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])).to eq({ csv: { 'everything' => export } }) }
|
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])).to eq({}) }
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -105,7 +105,9 @@ describe 'Instructing a dossier:', js: true do
|
||||||
assert_performed_jobs 1
|
assert_performed_jobs 1
|
||||||
|
|
||||||
click_on "Télécharger tous les dossiers"
|
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('Nous générons cet export.')
|
||||||
expect(page).to have_text('Un export au format .xlsx est en train d’être généré')
|
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('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é')
|
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)
|
perform_enqueued_jobs(only: ExportJob)
|
||||||
assert_performed_jobs 3
|
assert_performed_jobs 4
|
||||||
page.driver.browser.navigate.refresh
|
page.driver.browser.navigate.refresh
|
||||||
|
|
||||||
click_on "Télécharger tous les dossiers"
|
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 au format .xlsx')
|
||||||
expect(page).to have_text('Télécharger l’export des 30 derniers jours 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
|
end
|
||||||
|
|
||||||
scenario 'A instructeur can see the personnes impliquées' do
|
scenario 'A instructeur can see the personnes impliquées' do
|
||||||
|
|
Loading…
Reference in a new issue