Merge pull request #7500 from mfo/US/admin_zip

ETQ admin, je souhaite pouvoir télécharger une archive.zip (mensuelle) des dossiers terminés
This commit is contained in:
mfo 2022-07-04 14:28:43 +02:00 committed by GitHub
commit 6cf397fda4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 270 additions and 135 deletions

View file

@ -2,8 +2,6 @@
@import "constants";
.breadcrumbs {
margin: $default-spacer 0 3 * $default-spacer;
li {
display: inline-block;
font-weight: bold;

View file

@ -0,0 +1,38 @@
module Administrateurs
class ArchivesController < AdministrateurController
before_action :retrieve_procedure, only: [:index, :create]
helper_method :create_archive_url
def index
@average_dossier_weight = @procedure.average_dossier_weight
@count_dossiers_termines_by_month = Traitement.count_dossiers_termines_by_month(all_groupe_instructeurs)
@archives = Archive.for_groupe_instructeur(all_groupe_instructeurs).to_a
end
def create
type = params[:type]
archive = Archive.find_or_create_archive(type, year_month, all_groupe_instructeurs)
if archive.pending?
ArchiveCreationJob.perform_later(@procedure, archive, current_administrateur)
flash[:notice] = "Votre demande a été prise en compte. Selon le nombre de dossiers, cela peut prendre de quelques minutes a plusieurs heures. 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 admin_procedure_archives_path(@procedure)
end
private
def year_month
Date.strptime(params[:year_month], '%Y-%m') if params[:year_month].present?
end
def all_groupe_instructeurs
@procedure.groupe_instructeurs
end
def create_archive_url(procedure, date)
admin_procedure_archives_path(procedure, type: 'monthly', year_month: date.strftime('%Y-%m'))
end
end
end

View file

@ -1,55 +1,44 @@
module Instructeurs
class ArchivesController < InstructeurController
before_action :ensure_procedure_enabled, only: [:create]
before_action :retrieve_procedure, only: [:index, :create]
helper_method :create_archive_url
def index
@procedure = procedure
@average_dossier_weight = procedure.average_dossier_weight
@average_dossier_weight = @procedure.average_dossier_weight
@count_dossiers_termines_by_month = Traitement.count_dossiers_termines_by_month(groupe_instructeurs)
@archives = Archive
.for_groupe_instructeur(groupe_instructeurs)
.to_a
@archives = Archive.for_groupe_instructeur(groupe_instructeurs).to_a
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)
archive = Archive.find_or_create_archive(type, year_month, groupe_instructeurs)
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."
ArchiveCreationJob.perform_later(@procedure, archive, current_instructeur)
flash[:notice] = "Votre demande a été prise en compte. Selon le nombre de dossiers, cela peut prendre de quelques minutes a plusieurs heures. 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)
redirect_to instructeur_archives_path(@procedure)
end
private
def ensure_procedure_enabled
if procedure.brouillon?
flash[:alert] = "L'accès aux archives nest pas disponible pour cette démarche, merci den faire la demande à l'équipe de démarches simplifiees"
return redirect_to instructeur_procedure_path(procedure)
end
def year_month
Date.strptime(params[:year_month], '%Y-%m') if params[:year_month].present?
end
def procedure_id
params[:procedure_id]
def create_archive_url(procedure, date)
instructeur_archives_path(procedure, type: 'monthly', month: date.strftime('%Y-%m'))
end
def groupe_instructeurs
current_instructeur
.groupe_instructeurs
.where(procedure_id: procedure_id)
.where(procedure_id: params[:procedure_id])
end
def procedure
current_instructeur
.procedures
.find(procedure_id)
def retrieve_procedure
@procedure = current_instructeur.procedures.find(params[:procedure_id])
end
end
end

View file

@ -1,13 +1,13 @@
class ArchiveCreationJob < ApplicationJob
queue_as :archives
def perform(procedure, archive, instructeur)
def perform(procedure, archive, administrateur_or_instructeur)
archive.restart! if archive.failed? # restart for AASM
ProcedureArchiveService
.new(procedure)
.make_and_upload_archive(archive, instructeur)
.make_and_upload_archive(archive)
archive.make_available!
InstructeurMailer.send_archive(instructeur, procedure, archive).deliver_later
UserMailer.send_archive(administrateur_or_instructeur, procedure, archive).deliver_later
rescue => e
archive.fail! # fail for observability
raise e # re-raise for retryable behaviour

View file

@ -44,12 +44,4 @@ 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

View file

@ -37,4 +37,22 @@ class UserMailer < ApplicationMailer
subject: subject,
reply_to: CONTACT_EMAIL)
end
def send_archive(administrateur_or_instructeur, procedure, archive)
@archive = archive
@procedure = procedure
@archive_url = case administrateur_or_instructeur
when Instructeur then instructeur_archives_url(@procedure)
when Administrateur then admin_procedure_archives_url(@procedure)
else raise ArgumentError("send_archive expect either an Instructeur or an Administrateur")
end
@procedure_url = case administrateur_or_instructeur
when Instructeur then instructeur_procedure_url(@procedure.id)
when Administrateur then admin_procedure_url(@procedure)
else raise ArgumentError("send_archive expect either an Instructeur or an Administrateur")
end
subject = "Votre archive est disponible"
mail(to: administrateur_or_instructeur.email, subject: subject)
end
end

View file

@ -5,15 +5,7 @@ class ProcedureArchiveService
@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 make_and_upload_archive(archive, instructeur)
def make_and_upload_archive(archive)
dossiers = Dossier.visible_by_administration
.where(groupe_instructeur: archive.groupe_instructeurs)

View file

@ -1,12 +1,13 @@
.sub-header
.container.flex.justify-between.align-baseline.column
%ul.breadcrumbs
%ul.breadcrumbs.mt-1.mb-3
- steps.each do |step|
%li= step
- if defined?(preview) && preview
.mb-2
= link_to "Prévisualiser le formulaire", apercu_admin_procedure_path(@procedure), target: "_blank", rel: "noopener", class: 'button'
= link_to "Continuer >", admin_procedure_path(@procedure), title: 'Vous pourrez revenir ici par la suite', class: 'button accepted'
- if defined?(metadatas)
%ul.admin-metadata
- metadatas.each do |metadata|

View file

@ -0,0 +1,12 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Export et Archives'] }
.container
%h1.mb-2
Archives
= 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 }

View file

@ -4,6 +4,11 @@
metadatas: ["Créée le #{@procedure.created_at.strftime('%d/%m/%Y')} - n° #{@procedure.id}", "#{@procedure.close? ? "Close le #{@procedure.closed_at.strftime('%d/%m/%Y')}" : @procedure.locked? ? "Publiée - #{procedure_lien(@procedure)}" : "Brouillon"}"] }
.container.procedure-admin-container
- if !@procedure.brouillon?
= link_to admin_procedure_archives_path(@procedure), class: 'button', id: "archive-procedure" do
%span.icon.download
Télécharger
= link_to @procedure.active_revision.draft? ? commencer_dossier_vide_test_path(path: @procedure.path) : commencer_dossier_vide_path(path: @procedure.path), target: "_blank", rel: "noopener", class: 'button', id: "pdf-procedure" do
%span.icon.printer
PDF

View file

@ -7,54 +7,5 @@
.container
%h1.mb-2 Archives
.card.featured
.card-title Gestion de vos archives
%p
L'archivage de votre démarche se fait mensuellement. Ainsi pour chaque mois depuis la publication de votre démarche vous pouvez faire une demande de création d'archive.
%p
Cette archive contient uniquement les dossiers terminés, les demandes déposées par l'usager et la liste des pièces justificatives transmises.
%p
Les archives dont le poid est estimé à plus de #{number_to_human_size(Archive::MAX_SIZE)} ne sont pas supportées.
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 darchive.
%table.table.hoverable.archive-table
%thead
%tr
%th &nbsp;
%th.text-right Nombre de dossiers terminés
%th.text-right Poids estimé
%th.center Télécharger
%tbody
- @count_dossiers_termines_by_month.each do |count_by_month|
- month = count_by_month["month"].to_date
- nb_dossiers_termines = count_by_month["count"]
- matching_archive = @archives.find { |archive| archive.time_span_type == 'monthly' && archive.month == month }
- weight = estimate_weight(matching_archive, nb_dossiers_termines, @average_dossier_weight)
%tr
%td
= I18n.l(month, format: "%B %Y").capitalize
%td.text-right
= nb_dossiers_termines
%td.text-right
= number_to_human_size(weight)
%td.center
- 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, scope: [:instructeurs, :procedure], generated_period: time_ago_in_words(matching_archive.updated_at))
- else
%span.icon.retry
= t(:archive_pending_html, scope: [:instructeurs, :procedure], created_period: time_ago_in_words(matching_archive.created_at))
- elsif weight < Archive::MAX_SIZE
= link_to instructeur_archives_path(@procedure, type:'monthly', month: month.strftime('%Y-%m')), method: :post, class: "button" do
%span.icon.new-folder
Demander la création
- else
Archive trop volumineuse
= 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 }

View file

@ -0,0 +1,15 @@
.card.featured
.card-title Gestion de vos archives
%p
L'archivage de votre démarche se fait mensuellement. Ainsi pour chaque mois depuis la publication de votre démarche vous pouvez faire une demande de création d'archive.
%p
Cette archive contient uniquement les dossiers terminés, les demandes déposées par l'usager et la liste des pièces justificatives transmises.
%p
Les archives dont le poid est estimé à plus de #{number_to_human_size(Archive::MAX_SIZE)} ne sont pas supportées.
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 darchive.

View file

@ -0,0 +1,37 @@
%table.table.hoverable.archive-table
%thead
%tr
%th &nbsp;
%th.text-right Nombre de dossiers terminés
%th.text-right Poids estimé
%th.center Télécharger
%tbody
- count_dossiers_termines_by_month.each do |count_by_month|
- month = count_by_month["month"].to_date
- nb_dossiers_termines = count_by_month["count"]
- matching_archive = archives.find { |archive| archive.time_span_type == 'monthly' && archive.month == month }
- weight = estimate_weight(matching_archive, nb_dossiers_termines, average_dossier_weight)
%tr
%td
= I18n.l(month, format: "%B %Y").capitalize
%td.text-right
= nb_dossiers_termines
%td.text-right
= number_to_human_size(weight)
%td.center
- 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, scope: [:instructeurs, :procedure], generated_period: time_ago_in_words(matching_archive.updated_at))
- else
%span.icon.retry
= t(:archive_pending_html, scope: [:instructeurs, :procedure], created_period: time_ago_in_words(matching_archive.created_at))
- elsif weight < Archive::MAX_SIZE
= link_to create_archive_url(procedure, month), method: :post, class: "button" do
%span.icon.new-folder
Demander la création
- else
Archive trop volumineuse

View file

@ -5,12 +5,12 @@
%p
Votre archive pour la démarche
= link_to("#{@procedure.id} #{@procedure.libelle}", instructeur_procedure_url(@procedure.id))
= link_to("#{@procedure.id} #{@procedure.libelle}", @procedure_url)
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)
= round_button('Consulter mes archives', @archive_url, :primary)
%p
Ce fichier est

View file

@ -397,7 +397,7 @@ Rails.application.routes.draw do
end
end
resources :archives, only: [:index, :create, :show], controller: 'archives'
resources :archives, only: [:index, :create]
end
end
end
@ -408,6 +408,7 @@ Rails.application.routes.draw do
scope module: 'administrateurs', path: 'admin', as: 'admin' do
resources :procedures do
resources :archives, only: [:index, :create]
collection do
get 'new_from_existing'
end

View file

@ -0,0 +1,43 @@
describe Administrateurs::ArchivesController, type: :controller do
let(:admin) { create(:administrateur) }
let(:procedure) { create :procedure, administrateur: admin, groupe_instructeurs: [groupe_instructeur1, groupe_instructeur2] }
let(:groupe_instructeur1) { create(:groupe_instructeur) }
let(:groupe_instructeur2) { create(:groupe_instructeur) }
describe 'GET #index' do
subject { get :index, params: { procedure_id: procedure.id } }
context 'when logged out' do
it { is_expected.to have_http_status(302) }
end
context 'when logged in' do
before do
sign_in(admin.user)
end
it { is_expected.to have_http_status(200) }
it 'use all procedure.groupe_instructeurs' do
expect(Archive).to receive(:for_groupe_instructeur).with([groupe_instructeur1, groupe_instructeur2]).and_return([])
subject
end
end
end
describe 'GET #create' do
subject { post :create, params: { procedure_id: procedure.id, month: '22-06', type: 'monthly' } }
context 'when logged out' do
it { is_expected.to have_http_status(302) }
end
context 'when logged in' do
before do
sign_in(admin.user)
end
it { is_expected.to redirect_to(admin_procedure_archives_path(procedure)) }
it 'enqueue the creation job' do
expect { subject }.to have_enqueued_job(ArchiveCreationJob).with(procedure, an_instance_of(Archive), admin)
end
end
end
end

View file

@ -31,14 +31,12 @@ describe Instructeurs::ArchivesController, type: :controller do
describe '#create' do
let(:month) { '21-03' }
let(:date_month) { Date.strptime(month, "%Y-%m") }
let(:archive) { create(:archive) }
let(:subject) do
post :create, params: { procedure_id: procedure1.id, type: 'monthly', month: month }
end
it "performs archive creation job" do
allow_any_instance_of(ProcedureArchiveService).to receive(:create_pending_archive).and_return(archive)
expect { subject }.to have_enqueued_job(ArchiveCreationJob).with(procedure1, archive, instructeur)
expect { subject }.to have_enqueued_job(ArchiveCreationJob).with(procedure1, an_instance_of(Archive), instructeur)
expect(flash.notice).to include("Votre demande a été prise en compte")
end
end

View file

@ -8,7 +8,7 @@ describe ArchiveCreationJob, type: :job do
context 'when it fails' do
let(:status) { :pending }
let(:mailer) { double('mailer', deliver_later: true) }
before { expect(InstructeurMailer).not_to receive(:send_archive) }
before { expect(UserMailer).not_to receive(:send_archive) }
it 'does not send email and forward error for retry' do
allow(DownloadableFileService).to receive(:download_and_zip).and_raise(StandardError, "kaboom")
@ -21,7 +21,7 @@ describe ArchiveCreationJob, type: :job do
let(:mailer) { double('mailer', deliver_later: true) }
before do
allow(DownloadableFileService).to receive(:download_and_zip).and_return(true)
expect(InstructeurMailer).to receive(:send_archive).and_return(mailer)
expect(UserMailer).to receive(:send_archive).and_return(mailer)
end
context 'when archive failed previously' do

View file

@ -16,6 +16,10 @@ class UserMailerPreview < ActionMailer::Preview
UserMailer.france_connect_merge_confirmation('new.exemple.fr', '123456', 15.minutes.from_now)
end
def send_archive
UserMailer.send_archive(Instructeur.first, Procedure.first, Archive.first)
end
private
def user

View file

@ -35,4 +35,24 @@ RSpec.describe UserMailer, type: :mailer do
it { expect(subject.to).to eq([email]) }
it { expect(subject.body).to include(france_connect_particulier_mail_merge_with_existing_account_url(merge_token: code)) }
end
describe '.send_archive' do
let(:procedure) { create(:procedure) }
let(:archive) { create(:archive) }
subject { described_class.send_archive(role, procedure, archive) }
context 'instructeur' do
let(:role) { create(:instructeur) }
it { expect(subject.to).to eq([role.user.email]) }
it { expect(subject.body).to have_link('Consulter mes archives', href: instructeur_archives_url(procedure)) }
it { expect(subject.body).to have_link("#{procedure.id} #{procedure.libelle}", href: instructeur_procedure_url(procedure)) }
end
context 'instructeur' do
let(:role) { create(:administrateur) }
it { expect(subject.to).to eq([role.user.email]) }
it { expect(subject.body).to have_link('Consulter mes archives', href: admin_procedure_archives_url(procedure)) }
it { expect(subject.body).to have_link("#{procedure.id} #{procedure.libelle}", href: admin_procedure_url(procedure)) }
end
end
end

View file

@ -11,28 +11,6 @@ describe ProcedureArchiveService do
procedure.defaut_groupe_instructeur.add(instructeur)
end
describe '#create_pending_archive' do
context 'for a specific month' do
it 'creates a pending archive' do
archive = service.create_pending_archive(instructeur, 'monthly', date_month)
expect(archive.time_span_type).to eq 'monthly'
expect(archive.month).to eq date_month
expect(archive.pending?).to be_truthy
end
end
context 'for all months' do
it 'creates a pending archive' do
archive = service.create_pending_archive(instructeur, 'everything')
expect(archive.time_span_type).to eq 'everything'
expect(archive.month).to eq nil
expect(archive.pending?).to be_truthy
end
end
end
describe '#make_and_upload_archive' do
let!(:dossier) { create_dossier_for_month(year, month) }
let!(:dossier_2020) { create_dossier_for_month(2020, month) }
@ -47,7 +25,7 @@ describe ProcedureArchiveService do
allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io")
VCR.use_cassette('archive/new_file_to_get_200') do
service.make_and_upload_archive(archive, instructeur)
service.make_and_upload_archive(archive)
end
archive.file.open do |f|
@ -69,7 +47,7 @@ describe ProcedureArchiveService do
allow_any_instance_of(ActiveStorage::Attached::One).to receive(:url).and_return("https://www.demarches-simplifiees.fr/error_1")
VCR.use_cassette('archive/new_file_to_get_400.html') do
service.make_and_upload_archive(archive, instructeur)
service.make_and_upload_archive(archive)
end
archive.file.open do |f|
files = ZipTricks::FileReader.read_zip_structure(io: f)
@ -112,11 +90,11 @@ describe ProcedureArchiveService do
end
it 'collect files without raising exception' do
expect { service.make_and_upload_archive(archive, instructeur) }.not_to raise_exception
expect { service.make_and_upload_archive(archive) }.not_to raise_exception
end
it 'add bug report to archive' do
service.make_and_upload_archive(archive, instructeur)
service.make_and_upload_archive(archive)
archive.file.open do |f|
zip_entries = ZipTricks::FileReader.read_zip_structure(io: f)
@ -148,7 +126,7 @@ describe ProcedureArchiveService do
allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/5e61989aecb78e369c93674f877d7bf4ecde378850114a9563cdf8b6a2472536/typhoeus/typhoeus/issues/110")
VCR.use_cassette('archive/old_file_to_get_200') do
service.make_and_upload_archive(archive, instructeur)
service.make_and_upload_archive(archive)
end
archive = Archive.last

View file

@ -0,0 +1,36 @@
require 'system/administrateurs/procedure_spec_helper'
describe 'Creating a new procedure', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur) }
let(:procedure) do
create(:procedure, :with_service, :with_instructeur,
aasm_state: :publiee,
administrateurs: [administrateur],
libelle: 'libellé de la procédure',
path: 'libelle-de-la-procedure')
end
let!(:dossiers) do
create(:dossier, :accepte, procedure: procedure)
end
before { login_as administrateur.user, scope: :user }
scenario "download archive" do
visit admin_procedure_path(id: procedure.id)
# check button
expect(page).to have_selector('#archive-procedure')
click_on "Télécharger"
# check page loading
expect(page).to have_content("Archives")
# check archive
expect {
page.first(".archive-table .button").click
}.to have_enqueued_job(ArchiveCreationJob).with(procedure, an_instance_of(Archive), administrateur)
expect(page).to have_content("Votre demande a été prise en compte. Selon le nombre de dossiers, cela peut prendre de quelques minutes a plusieurs heures. Vous recevrez un courriel lorsque le fichier sera disponible.")
end
end

View file

@ -24,6 +24,10 @@ describe 'administrateurs/procedures/show.html.haml', type: :view do
describe 'procedure path is not customized' do
it { expect(rendered).to have_content('Brouillon') }
end
describe 'archive button' do
it { expect(rendered).not_to have_css('#archive-procedure') }
end
end
end
@ -38,6 +42,9 @@ describe 'administrateurs/procedures/show.html.haml', type: :view do
it { expect(rendered).not_to have_css('#publish-procedure-link') }
it { expect(rendered).to have_css('#close-procedure-link') }
end
describe 'archive button' do
it { expect(rendered).to have_css('#archive-procedure') }
end
end
describe 'procedure is closed' do