Revert "Revert "Export de tous les dossier d'une démarche""

This reverts commit d9a588b52e.
This commit is contained in:
Christophe Robillard 2021-04-29 17:29:47 +02:00
parent 533fa903bc
commit f40d96fbd2
32 changed files with 675 additions and 36 deletions

View file

@ -0,0 +1,55 @@
module Instructeurs
class ArchivesController < InstructeurController
before_action :ensure_procedure_enabled
def index
@procedure = procedure
@archivable_months = archivable_months
@dossiers_termines = @procedure.dossiers.state_termine
@poids_total = ProcedureArchiveService.procedure_files_size(@procedure)
groupe_instructeur = current_instructeur.groupe_instructeurs.where(procedure: @procedure.id).first
@archives = Archive.for_groupe_instructeur(groupe_instructeur)
end
def create
type = params[:type]
month = Date.strptime(params[:month], '%Y-%m') if params[:month].present?
archive = ProcedureArchiveService.new(procedure).create_pending_archive(current_instructeur, type, month)
if archive.pending?
ArchiveCreationJob.perform_later(procedure, archive, current_instructeur)
flash[:notice] = "Votre demande a été prise en compte. Selon le nombre de dossiers, cela peut prendre quelques minutes. Vous recevrez un courriel lorsque le fichier sera disponible."
else
flash[:notice] = "Cette archive a déjà été générée."
end
redirect_to instructeur_archives_path(procedure)
end
private
def ensure_procedure_enabled
if !procedure.feature_enabled?(:archive_zip_globale) || procedure.brouillon?
flash[:alert] = "L'accès aux archives n'est pas disponible pour cette démarche, merci d'en faire la demande à l'équipe de démarches simplifiees"
return redirect_to instructeur_procedure_path(procedure)
end
end
def archivable_months
start_date = procedure.published_at.to_date
end_date = Time.zone.now.to_date
(start_date...end_date)
.map(&:beginning_of_month)
.uniq
.reverse
end
def procedure
current_instructeur
.procedures
.for_download
.find(params[:procedure_id])
end
end
end

View file

@ -213,7 +213,6 @@ module Instructeurs
def telecharger_pjs
return head(:forbidden) if !dossier.attachments_downloadable?
generate_pdf_for_instructeur_export
files = ActiveStorage::DownloadableFile.create_list_from_dossier(dossier)
zipline(files, "dossier-#{dossier.id}.zip")
@ -239,12 +238,6 @@ module Instructeurs
.find(params[:dossier_id])
end
def generate_pdf_for_instructeur_export
@include_infos_administration = true
pdf = render_to_string(template: 'dossiers/show', formats: [:pdf])
dossier.pdf_export_for_instructeur.attach(io: StringIO.open(pdf), filename: "export-#{dossier.id}.pdf", content_type: 'application/pdf')
end
def commentaire_params
params.require(:commentaire).permit(:body, :piece_jointe)
end

View file

@ -0,0 +1,5 @@
module ArchiveHelper
def can_generate_archive?(dossiers_termines, poids_total)
dossiers_termines.count < 100 && poids_total < 1.gigabyte
end
end

View file

@ -0,0 +1,7 @@
class ArchiveCreationJob < ApplicationJob
def perform(procedure, archive, instructeur)
ProcedureArchiveService
.new(procedure)
.collect_files_archive(archive, instructeur)
end
end

View file

@ -0,0 +1,7 @@
class Cron::PurgeStaleArchivesJob < Cron::CronJob
self.schedule_expression = "every 5 minutes"
def perform
Archive.stale.destroy_all
end
end

View file

@ -1,14 +1,12 @@
class ActiveStorage::DownloadableFile
def self.create_list_from_dossier(dossier)
pjs = PiecesJustificativesService.liste_pieces_justificatives(dossier)
files = pjs.map do |piece_justificative|
pjs.map do |piece_justificative|
[
piece_justificative,
self.timestamped_filename(piece_justificative)
"dossier-#{dossier.id}/#{self.timestamped_filename(piece_justificative)}"
]
end
files << [dossier.pdf_export_for_instructeur, self.timestamped_filename(dossier.pdf_export_for_instructeur)]
files
end
private
@ -22,19 +20,23 @@ class ActiveStorage::DownloadableFile
timestamp = attachment.created_at.strftime("%d-%m-%Y-%H-%M")
id = attachment.id % 10000
"#{folder}/#{basename}-#{timestamp}-#{id}#{extension}"
[folder, "#{basename}-#{timestamp}-#{id}#{extension}"].join
end
def self.folder(attachment)
if attachment.name == 'pdf_export_for_instructeur'
return ''
end
case attachment.record_type
when 'Dossier'
'dossier'
'dossier/'
when 'DossierOperationLog', 'BillSignature'
'horodatage'
'horodatage/'
when 'Commentaire'
'messagerie'
'messagerie/'
else
'pieces_justificatives'
'pieces_justificatives/'
end
end

View file

@ -1,5 +1,7 @@
# Preview all emails at http://localhost:3000/rails/mailers/instructeur_mailer
class InstructeurMailer < ApplicationMailer
helper MailerHelper
layout 'mailers/layout'
def user_to_instructeur(email)
@ -42,4 +44,12 @@ class InstructeurMailer < ApplicationMailer
mail(to: instructeur.email, subject: subject)
end
def send_archive(instructeur, procedure, archive)
@archive = archive
@procedure = procedure
subject = "Votre archive est disponible"
mail(to: instructeur.email, subject: subject)
end
end

71
app/models/archive.rb Normal file
View file

@ -0,0 +1,71 @@
# == Schema Information
#
# Table name: archives
#
# id :bigint not null, primary key
# key :text not null
# month :date
# status :string not null
# time_span_type :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
class Archive < ApplicationRecord
include AASM
RETENTION_DURATION = 1.week
has_and_belongs_to_many :groupe_instructeurs
has_one_attached :file
scope :stale, -> { where('updated_at < ?', (Time.zone.now - RETENTION_DURATION)) }
scope :for_groupe_instructeur, -> (groupe_instructeur) {
joins(:archives_groupe_instructeurs)
.where(
archives_groupe_instructeurs: { groupe_instructeur: groupe_instructeur }
)
}
enum time_span_type: {
everything: 'everything',
monthly: 'monthly'
}
enum status: {
pending: 'pending',
generated: 'generated'
}
aasm whiny_persistence: true, column: :status, enum: true do
state :pending, initial: true
state :generated
event :make_available do
transitions from: :pending, to: :generated
end
end
def available?
status == 'generated' && file.attached?
end
def filename(procedure)
if time_span_type == 'everything'
"procedure-#{procedure.id}.zip"
else
"procedure-#{procedure.id}-mois-#{I18n.l(month, format: '%Y-%m')}.zip"
end
end
def self.find_or_create_archive(time_span_type, month, groupe_instructeurs)
create_with(groupe_instructeurs: groupe_instructeurs)
.create_or_find_by(time_span_type: time_span_type, month: month, key: generate_cache_key(groupe_instructeurs))
end
private
def self.generate_cache_key(groupe_instructeurs)
groupe_instructeurs.map(&:id).sort.join('-')
end
end

View file

@ -172,7 +172,12 @@ class Dossier < ApplicationRecord
scope :en_construction, -> { not_archived.state_en_construction }
scope :en_instruction, -> { not_archived.state_en_instruction }
scope :termine, -> { not_archived.state_termine }
scope :downloadable_sorted, -> {
scope :processed_in_month, -> (month) do
state_termine
.joins(:traitements)
.where(traitements: { processed_at: month.beginning_of_month..month.end_of_month })
end
scope :downloadable_sorted, -> {
state_not_brouillon
.includes(
:user,

View file

@ -25,6 +25,7 @@ class Instructeur < ApplicationRecord
has_many :followed_dossiers, through: :follows, source: :dossier
has_many :previously_followed_dossiers, -> { distinct }, through: :previous_follows, source: :dossier
has_many :trusted_device_tokens, dependent: :destroy
has_many :archives
has_one :user, dependent: :nullify

View file

@ -156,6 +156,20 @@ class Procedure < ApplicationRecord
includes(:draft_revision, :published_revision, administrateurs: :user)
}
scope :for_download, -> {
includes(
:groupe_instructeurs,
dossiers: {
champs: [
piece_justificative_file_attachment: :blob,
champs: [
piece_justificative_file_attachment: :blob
]
]
}
)
}
validates :libelle, presence: true, allow_blank: false, allow_nil: false
validates :description, presence: true, allow_blank: false, allow_nil: false
validates :administrateurs, presence: true

View file

@ -1,10 +1,11 @@
class PiecesJustificativesService
def self.liste_pieces_justificatives(dossier)
dossier_export = generate_dossier_export(dossier)
pjs_champs = pjs_for_champs(dossier)
pjs_commentaires = pjs_for_commentaires(dossier)
pjs_dossier = pjs_for_dossier(dossier)
(pjs_champs + pjs_commentaires + pjs_dossier)
([dossier_export] + pjs_champs + pjs_commentaires + pjs_dossier)
.filter(&:attached?)
end
@ -43,6 +44,17 @@ class PiecesJustificativesService
private
def self.generate_dossier_export(dossier)
pdf = ApplicationController
.render(template: 'dossiers/show', formats: [:pdf],
assigns: {
include_infos_administration: true,
dossier: dossier
})
dossier.pdf_export_for_instructeur.attach(io: StringIO.open(pdf), filename: "export-#{dossier.id}.pdf", content_type: 'application/pdf')
dossier.pdf_export_for_instructeur
end
def self.pjs_for_champs(dossier)
allowed_champs = dossier.champs + dossier.champs_private

View file

@ -0,0 +1,72 @@
require 'tempfile'
class ProcedureArchiveService
def initialize(procedure)
@procedure = procedure
end
def create_pending_archive(instructeur, type, month = nil)
groupe_instructeurs = instructeur
.groupe_instructeurs
.where(procedure: @procedure)
Archive.find_or_create_archive(type, month, groupe_instructeurs)
end
def collect_files_archive(archive, instructeur)
if archive.time_span_type == 'everything'
dossiers = @procedure.dossiers.state_termine
else
dossiers = @procedure.dossiers.processed_in_month(archive.month)
end
files = create_list_of_attachments(dossiers)
tmp_file = Tempfile.new(['tc', '.zip'])
Zip::OutputStream.open(tmp_file) do |zipfile|
files.each do |attachment, pj_filename|
zipfile.put_next_entry(pj_filename)
zipfile.puts(attachment.download)
end
end
archive.file.attach(io: File.open(tmp_file), filename: archive.filename(@procedure))
tmp_file.delete
archive.make_available!
InstructeurMailer.send_archive(instructeur, @procedure, archive).deliver_later
end
def self.procedure_files_size(procedure)
dossiers_files_size(procedure.dossiers)
end
def self.dossiers_files_size(dossiers)
dossiers.map do |dossier|
liste_pieces_justificatives_for_archive(dossier).sum(&:byte_size)
end.sum
end
private
def create_list_of_attachments(dossiers)
dossiers.flat_map do |dossier|
ActiveStorage::DownloadableFile.create_list_from_dossier(dossier)
end
end
def self.attachments_from_champs_piece_justificative(champs)
champs
.filter { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative) }
.filter { |pj| pj.piece_justificative_file.attached? }
.map(&:piece_justificative_file)
end
def self.liste_pieces_justificatives_for_archive(dossier)
champs_blocs_repetables = dossier.champs
.filter { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:repetition) }
.flat_map(&:champs)
attachments_from_champs_piece_justificative(champs_blocs_repetables + dossier.champs)
end
end

View file

@ -0,0 +1,21 @@
- content_for(:title, 'Votre archive est disponible')
%p
Bonjour,
%p
Votre archive pour la démarche
= link_to("#{@procedure.id} #{@procedure.libelle}", instructeur_procedure_url(@procedure.id))
est disponible.
Vous pouvez la télécharger dans votre espace de gestion des archives.
%p
= round_button('Consulter mes archives', instructeur_archives_url(@procedure), :primary)
%p
Ce fichier est
%b valide une semaine
et peut-être téléchargé
%b plusieurs fois.
= render partial: "layouts/mailers/signature"

View file

@ -0,0 +1 @@
= render_flash(sticky: true)

View file

@ -0,0 +1,91 @@
- content_for(:title, "Archives pour #{@procedure.libelle}")
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to(@procedure.libelle, instructeur_procedure_path(@procedure)),
'Archives'] }
.container
%h1 Archives
.card.featured
.card-title Gestion de vos archives
%p
Vous pouvez télécharger les archives des dossiers terminés depuis la publication de la procédure au format Zip.
%p
Cet export contient les demande déposée par l'usager et la liste des pièces justificatives transmises.
%p
Cet export n'est pas possible pour le moment pour les démarches à forte volumétrie.
Nous vous invitons à regarder
= link_to 'la documentation', ARCHIVAGE_DOC_URL
afin de voir les options à votre disposition pour mettre en place un système d'archive.
%table.table.hoverable
%thead
%tr
%th &nbsp;
%th Nombre de dossiers terminés
%th Poids estimé
%th Télécharger
%tbody
- if can_generate_archive?(@dossiers_termines, @poids_total)
%tr
- matching_archive = @archives.find_by(time_span_type: 'everything')
%td
Tous les dossiers
%td
= @dossiers_termines.count
%td
- if matching_archive.present? && matching_archive.available?
- weight = matching_archive.file.byte_size
- else
- weight = @poids_total
= number_to_human_size(weight)
%td
- if matching_archive.try(&:available?)
= link_to url_for(matching_archive.file), class: 'button primary' do
%span.icon.download-white
= t(:archive_ready_html, generated_period: time_ago_in_words(matching_archive.updated_at), scope: [:instructeurs, :procedure])
- elsif matching_archive.try(&:pending?)
%span.icon.retry
= t(:archive_pending_html, created_period: time_ago_in_words(matching_archive.created_at), scope: [:instructeurs, :procedure])
- elsif @dossiers_termines.count > 0
= link_to instructeur_archives_path(@procedure, type: 'everything'), method: :post, class: "button" do
%span.icon.new-folder
Demander la création
- else
Rien à télécharger !
- @archivable_months.each do |month|
- dossiers_termines = @procedure.dossiers.processed_in_month(month)
- nb_dossiers_termines = dossiers_termines.count
- matching_archive = @archives.find_by(time_span_type: 'monthly', month: month)
%tr
%td
= I18n.l(month, format: "%B %Y")
%td
= nb_dossiers_termines
%td
- if matching_archive.present? && matching_archive.available?
- weight = matching_archive.file.byte_size
- else
- weight = ProcedureArchiveService::dossiers_files_size(dossiers_termines)
= number_to_human_size(weight)
%td
- if nb_dossiers_termines > 0
- if matching_archive.present?
- if matching_archive.status == 'generated' && matching_archive.file.attached?
= link_to url_for(matching_archive.file), class: 'button primary' do
%span.icon.download-white
= t(:archive_ready_html, generated_period: time_ago_in_words(matching_archive.updated_at), scope: [:instructeurs, :procedure])
- else
%span.icon.retry
= t(:archive_pending_html, created_period: time_ago_in_words(matching_archive.created_at), scope: [:instructeurs, :procedure])
- else
= link_to instructeur_archives_path(@procedure, type:'monthly', month: month.strftime('%Y-%m')), method: :post, class: "button" do
%span.icon.new-folder
Démander la création
- else
Rien à télécharger !

View file

@ -16,3 +16,6 @@
- else
%span{ 'data-export-poll-url': download_export_instructeur_procedure_path(procedure, export_format: format, no_progress_notification: true) }
= t(:export_pending_html, export_time: time_ago_in_words(export.created_at), export_format: ".#{format}", scope: [:instructeurs, :procedure])
- if procedure.feature_enabled?(:archive_zip_globale)
%li
= link_to t(:download_archive, scope: [:instructeurs, :procedure]), instructeur_archives_path(procedure)