From c25f3c79d9041ba20ed287d4bfc25c17e9ff83ce Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Mon, 29 Mar 2021 17:07:46 +0200 Subject: [PATCH 01/14] add archive model --- app/models/archive.rb | 51 +++++++++++++++++++ app/models/instructeur.rb | 1 + ...8_create_archive_for_groupe_instructeur.rb | 17 +++++++ db/schema.rb | 19 +++++++ spec/factories/archive.rb | 14 +++++ spec/models/archive_spec.rb | 51 +++++++++++++++++++ 6 files changed, 153 insertions(+) create mode 100644 app/models/archive.rb create mode 100644 db/migrate/20201104163658_create_archive_for_groupe_instructeur.rb create mode 100644 spec/factories/archive.rb create mode 100644 spec/models/archive_spec.rb diff --git a/app/models/archive.rb b/app/models/archive.rb new file mode 100644 index 000000000..7be847b3a --- /dev/null +++ b/app/models/archive.rb @@ -0,0 +1,51 @@ +# == Schema Information +# +# Table name: archives +# +# id :bigint not null, primary key +# content_type :string not null +# month :date +# status :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 content_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 +end diff --git a/app/models/instructeur.rb b/app/models/instructeur.rb index 1b773297e..a9afa9516 100644 --- a/app/models/instructeur.rb +++ b/app/models/instructeur.rb @@ -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 diff --git a/db/migrate/20201104163658_create_archive_for_groupe_instructeur.rb b/db/migrate/20201104163658_create_archive_for_groupe_instructeur.rb new file mode 100644 index 000000000..51793681c --- /dev/null +++ b/db/migrate/20201104163658_create_archive_for_groupe_instructeur.rb @@ -0,0 +1,17 @@ +class CreateArchiveForGroupeInstructeur < ActiveRecord::Migration[6.0] + def change + create_table :archives do |t| + t.string :status, null: false + t.date :month + t.string :content_type, null: false + t.timestamps + end + + create_table "archives_groupe_instructeurs", force: :cascade do |t| + t.belongs_to :archive, foreign_key: true, null: false + t.belongs_to :groupe_instructeur, foreign_key: true, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 956e5a302..3b5bcf32d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -81,6 +81,23 @@ ActiveRecord::Schema.define(version: 2021_04_27_120002) do t.index ["procedure_id"], name: "index_administrateurs_procedures_on_procedure_id" end + create_table "archives", force: :cascade do |t| + t.string "status", null: false + t.date "month" + t.string "content_type", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + + create_table "archives_groupe_instructeurs", force: :cascade do |t| + t.bigint "archive_id", null: false + t.bigint "groupe_instructeur_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["archive_id"], name: "index_archives_groupe_instructeurs_on_archive_id" + t.index ["groupe_instructeur_id"], name: "index_archives_groupe_instructeurs_on_groupe_instructeur_id" + end + create_table "assign_tos", id: :serial, force: :cascade do |t| t.integer "instructeur_id" t.datetime "created_at" @@ -735,6 +752,8 @@ ActiveRecord::Schema.define(version: 2021_04_27_120002) do end add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "archives_groupe_instructeurs", "archives" + add_foreign_key "archives_groupe_instructeurs", "groupe_instructeurs" add_foreign_key "assign_tos", "groupe_instructeurs" add_foreign_key "attestation_templates", "procedures" add_foreign_key "attestations", "dossiers" diff --git a/spec/factories/archive.rb b/spec/factories/archive.rb new file mode 100644 index 000000000..16a302219 --- /dev/null +++ b/spec/factories/archive.rb @@ -0,0 +1,14 @@ +FactoryBot.define do + factory :archive do + content_type { 'everything' } + groupe_instructeurs { [association(:groupe_instructeur)] } + + trait :pending do + status { 'pending' } + end + + trait :generated do + status { 'generated' } + end + end +end diff --git a/spec/models/archive_spec.rb b/spec/models/archive_spec.rb new file mode 100644 index 000000000..5753340d3 --- /dev/null +++ b/spec/models/archive_spec.rb @@ -0,0 +1,51 @@ +describe Dossier do + include ActiveJob::TestHelper + + before { Timecop.freeze(Time.zone.now) } + after { Timecop.return } + + let(:archive) { create(:archive) } + + describe 'scopes' do + describe 'staled' do + let(:recent_archive) { create(:archive) } + let(:staled_archive) { create(:archive, updated_at: (Archive::RETENTION_DURATION + 2).days.ago) } + + subject do + archive; recent_archive; staled_archive + Archive.stale + end + + it { is_expected.to match_array([staled_archive]) } + end + end + + describe '.status' do + it { expect(archive.status).to eq('pending') } + end + + describe '#make_available!' do + before { archive.make_available! } + it { expect(archive.status).to eq('generated') } + end + + describe '#available?' do + subject { archive.available? } + context 'without attachment' do + let(:archive) { create(:archive, file: nil) } + it { is_expected.to eq(false) } + end + + context 'with an attachment' do + context 'when the attachment was created but the process was not over' do + let(:archive) { create(:archive, :pending, file: Rack::Test::UploadedFile.new('spec/fixtures/files/file.pdf', 'application/pdf')) } + it { is_expected.to eq(false) } + end + + context 'when the attachment was created but the process was not over' do + let(:archive) { create(:archive, :generated, file: Rack::Test::UploadedFile.new('spec/fixtures/files/file.pdf', 'application/pdf')) } + it { is_expected.to eq(true) } + end + end + end +end From fba0d78153735c1b149a53e81d1b105725a42870 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Mon, 29 Mar 2021 21:45:46 +0200 Subject: [PATCH 02/14] generate pdf export inside pieces_justificatives_service --- .../instructeurs/dossiers_controller.rb | 7 ------- app/lib/active_storage/downloadable_file.rb | 20 ++++++++++--------- app/services/pieces_justificatives_service.rb | 14 ++++++++++++- .../instructeurs/dossiers_controller_spec.rb | 7 ------- spec/features/instructeurs/expert_spec.rb | 2 ++ .../features/instructeurs/instruction_spec.rb | 18 ++++++++--------- .../pieces_justificatives_service_spec.rb | 7 ++++++- 7 files changed, 41 insertions(+), 34 deletions(-) diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 37b7e1d4c..e6fb16d21 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -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 diff --git a/app/lib/active_storage/downloadable_file.rb b/app/lib/active_storage/downloadable_file.rb index db86efc73..d8ca5cf4c 100644 --- a/app/lib/active_storage/downloadable_file.rb +++ b/app/lib/active_storage/downloadable_file.rb @@ -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 diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index a5903531b..f3cd8303c 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -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 diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index b31cb81c0..ab6d9ff5a 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -712,13 +712,6 @@ describe Instructeurs::DossiersController, type: :controller do dossier_id: dossier.id } end - - context 'when zip download is disabled through flipflop' do - it 'is forbidden' do - subject - expect(response).to have_http_status(:forbidden) - end - end end describe "#delete_dossier" do diff --git a/spec/features/instructeurs/expert_spec.rb b/spec/features/instructeurs/expert_spec.rb index d361b3cf3..a384687b6 100644 --- a/spec/features/instructeurs/expert_spec.rb +++ b/spec/features/instructeurs/expert_spec.rb @@ -12,6 +12,8 @@ feature 'Inviting an expert:', js: true do context 'as an Instructeur' do scenario 'I can invite an expert' do + allow(ClamavService).to receive(:safe_file?).and_return(true) + # assign instructeur to linked dossier instructeur.assign_to_procedure(linked_dossier.procedure) diff --git a/spec/features/instructeurs/instruction_spec.rb b/spec/features/instructeurs/instruction_spec.rb index b0c9aeef6..3fdadf27d 100644 --- a/spec/features/instructeurs/instruction_spec.rb +++ b/spec/features/instructeurs/instruction_spec.rb @@ -165,10 +165,10 @@ feature 'Instructing a dossier:', js: true do expect(DownloadHelpers.download).to include "dossier-#{dossier.id}.zip" expect(files.size).to be 3 - expect(files[0].filename.include?('piece_justificative_0')).to be_truthy - expect(files[0].uncompressed_size).to be File.size(path) - expect(files[1].filename.include?('horodatage/operation')).to be_truthy - expect(files[2].filename.include?('dossier/export')).to be_truthy + expect(files[0].filename.include?('export')).to be_truthy + expect(files[1].filename.include?('piece_justificative_0')).to be_truthy + expect(files[1].uncompressed_size).to be File.size(path) + expect(files[2].filename.include?('horodatage/operation')).to be_truthy end scenario 'A instructeur can download an archive containing several identical attachments' do @@ -180,13 +180,13 @@ feature 'Instructing a dossier:', js: true do expect(DownloadHelpers.download).to include "dossier-#{dossier.id}.zip" expect(files.size).to be 4 - expect(files[0].filename.include?('piece_justificative_0')).to be_truthy + expect(files[0].filename.include?('export')).to be_truthy expect(files[1].filename.include?('piece_justificative_0')).to be_truthy - expect(files[0].filename).not_to eq files[1].filename - expect(files[0].uncompressed_size).to be File.size(path) + expect(files[2].filename.include?('piece_justificative_0')).to be_truthy + expect(files[1].filename).not_to eq files[2].filename expect(files[1].uncompressed_size).to be File.size(path) - expect(files[2].filename.include?('horodatage/operation')).to be_truthy - expect(files[3].filename.include?('dossier/export')).to be_truthy + expect(files[2].uncompressed_size).to be File.size(path) + expect(files[3].filename.include?('horodatage/operation')).to be_truthy end before { DownloadHelpers.clear_downloads } diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index d0ad55499..fc151f908 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -16,7 +16,12 @@ describe PiecesJustificativesService do # to be exported it 'ensures no titre identite is given' do expect(champ_identite.piece_justificative_file).to be_attached - expect(subject).to eq([]) + expect(subject.any? { |piece| piece.name == 'piece_justificative_file' }).to be_falsy + end + + it 'returns export pdf of the dossier' do + expect(champ_identite.piece_justificative_file).to be_attached + expect(subject.any? { |piece| piece.name == 'pdf_export_for_instructeur' }).to be_truthy end end end From 07cc4fa97f486aae383b0a7eb4b56f295e6eaec8 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Mon, 29 Mar 2021 22:15:49 +0200 Subject: [PATCH 03/14] add service to create procedure archive --- app/mailers/instructeur_mailer.rb | 10 +++ app/models/archive.rb | 8 ++ app/models/dossier.rb | 7 +- app/services/procedure_archive_service.rb | 74 +++++++++++++++++++ .../instructeur_mailer/send_archive.html.haml | 21 ++++++ .../procedure_archive_service_spec.rb | 63 ++++++++++++++++ 6 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 app/services/procedure_archive_service.rb create mode 100644 app/views/instructeur_mailer/send_archive.html.haml create mode 100644 spec/services/procedure_archive_service_spec.rb diff --git a/app/mailers/instructeur_mailer.rb b/app/mailers/instructeur_mailer.rb index 89c58864c..55332cf38 100644 --- a/app/mailers/instructeur_mailer.rb +++ b/app/mailers/instructeur_mailer.rb @@ -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 diff --git a/app/models/archive.rb b/app/models/archive.rb index 7be847b3a..082176066 100644 --- a/app/models/archive.rb +++ b/app/models/archive.rb @@ -48,4 +48,12 @@ class Archive < ApplicationRecord def available? status == 'generated' && file.attached? end + + def filename(procedure) + if content_type == 'everything' + "procedure-#{procedure.id}.zip" + else + "procedure-#{procedure.id}-mois-#{I18n.l(month, format: '%Y-%m')}.zip" + end + end end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index a0add0355..8b8a1e60e 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -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, diff --git a/app/services/procedure_archive_service.rb b/app/services/procedure_archive_service.rb new file mode 100644 index 000000000..66862ff1d --- /dev/null +++ b/app/services/procedure_archive_service.rb @@ -0,0 +1,74 @@ +require 'tempfile' + +class ProcedureArchiveService + def initialize(procedure) + @procedure = procedure + end + + def create_archive(instructeur, type, month = nil) + groupe_instructeurs = instructeur + .groupe_instructeurs + .where(procedure: @procedure) + + if type == 'everything' + dossiers = @procedure.dossiers.state_termine + else + dossiers = @procedure.dossiers.processed_in_month(month) + end + + files = create_list_of_attachments(dossiers) + + archive = Archive.create!( + content_type: type, + month: month, + groupe_instructeurs: groupe_instructeurs + ) + + 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 diff --git a/app/views/instructeur_mailer/send_archive.html.haml b/app/views/instructeur_mailer/send_archive.html.haml new file mode 100644 index 000000000..11857c69f --- /dev/null +++ b/app/views/instructeur_mailer/send_archive.html.haml @@ -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" diff --git a/spec/services/procedure_archive_service_spec.rb b/spec/services/procedure_archive_service_spec.rb new file mode 100644 index 000000000..48dd2ba67 --- /dev/null +++ b/spec/services/procedure_archive_service_spec.rb @@ -0,0 +1,63 @@ +describe ProcedureArchiveService do + describe '#create_archive' do + let(:procedure) { create(:procedure, :published) } + let(:instructeur) { create(:instructeur) } + let(:service) { ProcedureArchiveService.new(procedure) } + let(:year) { 2020 } + let(:month) { 3 } + let(:date_month) { Date.strptime("#{year}-#{month}", "%Y-%m") } + + before do + create_dossier_for_month(year, month) + create_dossier_for_month(2020, month) + end + + after { Timecop.return } + + context 'for a specific month' do + let(:year) { 2021 } + let(:mailer) { double('mailer', deliver_later: true) } + + it 'creates a monthly archive' do + expect(InstructeurMailer).to receive(:send_archive).and_return(mailer) + + service.create_archive(instructeur, 'monthly', date_month) + + archive = Archive.last + archive.file.open do |f| + files = ZipTricks::FileReader.read_zip_structure(io: f) + expect(files.size).to be 2 + expect(files.first.filename).to include("export") + expect(files.last.filename).to include("attestation") + end + expect(archive.content_type).to eq 'monthly' + expect(archive.file.attached?).to be_truthy + end + end + + context 'for all months' do + let(:mailer) { double('mailer', deliver_later: true) } + + it 'creates a everything archive' do + expect(InstructeurMailer).to receive(:send_archive).and_return(mailer) + + service.create_archive(instructeur, 'everything') + + archive = Archive.last + archive.file.open do |f| + files = ZipTricks::FileReader.read_zip_structure(io: f) + expect(files.size).to be 4 + end + expect(archive.content_type).to eq 'everything' + expect(archive.file.attached?).to be_truthy + end + end + end + + private + + def create_dossier_for_month(year, month) + Timecop.freeze(Time.zone.local(year, month, 5)) + create(:dossier, :accepte, :with_attestation, procedure: procedure) + end +end From 609a1aa45b08c14f1d4a4ffb97461045ad87e706 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 30 Mar 2021 10:12:18 +0200 Subject: [PATCH 04/14] add archive creation and purge archives jobs --- app/jobs/archive_creation_job.rb | 7 +++++++ app/jobs/purge_stale_archives_job.rb | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 app/jobs/archive_creation_job.rb create mode 100644 app/jobs/purge_stale_archives_job.rb diff --git a/app/jobs/archive_creation_job.rb b/app/jobs/archive_creation_job.rb new file mode 100644 index 000000000..79b774777 --- /dev/null +++ b/app/jobs/archive_creation_job.rb @@ -0,0 +1,7 @@ +class ArchiveCreationJob < ApplicationJob + def perform(procedure, instructeur, type, month) + ProcedureArchiveService + .new(procedure) + .create_archive(instructeur, type, month) + end +end diff --git a/app/jobs/purge_stale_archives_job.rb b/app/jobs/purge_stale_archives_job.rb new file mode 100644 index 000000000..979224ad2 --- /dev/null +++ b/app/jobs/purge_stale_archives_job.rb @@ -0,0 +1,7 @@ +class PurgeStaleArchivesJob < CronJob + self.schedule_expression = "every 5 minutes" + + def perform + Archive.stale.destroy_all + end +end From 8b2849408c7f8aecc00789bb3d559308b06cf2e1 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 30 Mar 2021 10:46:21 +0200 Subject: [PATCH 05/14] instructeurs can create and download archives --- .../instructeurs/archives_controller.rb | 49 ++++++++++ app/helpers/archive_helper.rb | 5 + app/models/procedure.rb | 14 +++ .../instructeurs/archives/create.js.haml | 1 + .../instructeurs/archives/index.html.haml | 91 +++++++++++++++++++ .../procedures/_download_dossiers.html.haml | 2 + config/locales/views/instructeurs/fr.yml | 1 + config/routes.rb | 2 + .../instructeurs/archives_controller_spec.rb | 54 +++++++++++ .../_download_dossiers.html.haml_spec.rb | 10 ++ 10 files changed, 229 insertions(+) create mode 100644 app/controllers/instructeurs/archives_controller.rb create mode 100644 app/helpers/archive_helper.rb create mode 100644 app/views/instructeurs/archives/create.js.haml create mode 100644 app/views/instructeurs/archives/index.html.haml create mode 100644 spec/controllers/instructeurs/archives_controller_spec.rb diff --git a/app/controllers/instructeurs/archives_controller.rb b/app/controllers/instructeurs/archives_controller.rb new file mode 100644 index 000000000..308f51b80 --- /dev/null +++ b/app/controllers/instructeurs/archives_controller.rb @@ -0,0 +1,49 @@ +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? + + ArchiveCreationJob.perform_later(procedure, current_instructeur, type, month) + 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." + end + + private + + def ensure_procedure_enabled + if !procedure.publiee? + 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 diff --git a/app/helpers/archive_helper.rb b/app/helpers/archive_helper.rb new file mode 100644 index 000000000..b349b8793 --- /dev/null +++ b/app/helpers/archive_helper.rb @@ -0,0 +1,5 @@ +module ArchiveHelper + def can_generate_archive?(dossiers_termines, poids_total) + dossiers_termines.count < 100 && poids_total < 1.gigabyte + end +end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index ea0b4e570..b4760c353 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -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 diff --git a/app/views/instructeurs/archives/create.js.haml b/app/views/instructeurs/archives/create.js.haml new file mode 100644 index 000000000..7fe9f7f0b --- /dev/null +++ b/app/views/instructeurs/archives/create.js.haml @@ -0,0 +1 @@ += render_flash(sticky: true) diff --git a/app/views/instructeurs/archives/index.html.haml b/app/views/instructeurs/archives/index.html.haml new file mode 100644 index 000000000..057385e69 --- /dev/null +++ b/app/views/instructeurs/archives/index.html.haml @@ -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   + %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(content_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élécharger + - elsif matching_archive.try(&:pending?) + %span.icon.retry + Archive en cours de création + - elsif @dossiers_termines.count > 0 + = link_to instructeur_archives_path(@procedure, type: 'everything'), method: :post, class: "button", remote: true 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(content_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élécharger + - else + %span.icon.retry + Archive en cours de création + - else + = link_to instructeur_archives_path(@procedure, type:'monthly', month: month.strftime('%Y-%m')), method: :post, class: "button", remote: true do + %span.icon.new-folder + Démander la création + - else + Rien à télécharger ! + diff --git a/app/views/instructeurs/procedures/_download_dossiers.html.haml b/app/views/instructeurs/procedures/_download_dossiers.html.haml index 87c2c7417..1e0993605 100644 --- a/app/views/instructeurs/procedures/_download_dossiers.html.haml +++ b/app/views/instructeurs/procedures/_download_dossiers.html.haml @@ -16,3 +16,5 @@ - 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]) + %li + = link_to t(:download_archive, scope: [:instructeurs, :procedure]), instructeur_archives_path(procedure) diff --git a/config/locales/views/instructeurs/fr.yml b/config/locales/views/instructeurs/fr.yml index 3cc5fabdc..17c7ef429 100644 --- a/config/locales/views/instructeurs/fr.yml +++ b/config/locales/views/instructeurs/fr.yml @@ -8,3 +8,4 @@ fr: ods_html: Demander un export au format .ods export_ready_html: Télécharger l’export au format %{export_format}
(généré il y a %{export_time}) export_pending_html: Un export au format %{export_format} est en train d’être généré
(demandé il y a %{export_time}) + download_archive: Télécharger une archive au format .zip de tous les dossiers et leurs pièces jointes diff --git a/config/routes.rb b/config/routes.rb index bb953ffa4..acdac88ea 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -383,6 +383,8 @@ Rails.application.routes.draw do get 'telecharger_pjs' => 'dossiers#telecharger_pjs' end end + + resources :archives, only: [:index, :create, :show], controller: 'archives' end end get "recherche" => "recherche#index" diff --git a/spec/controllers/instructeurs/archives_controller_spec.rb b/spec/controllers/instructeurs/archives_controller_spec.rb new file mode 100644 index 000000000..d698c95bb --- /dev/null +++ b/spec/controllers/instructeurs/archives_controller_spec.rb @@ -0,0 +1,54 @@ +describe Instructeurs::ArchivesController, type: :controller do + let(:procedure1) { create(:procedure, :published, groupe_instructeurs: [gi1]) } + let(:procedure2) { create(:procedure, :published, groupe_instructeurs: [gi2]) } + let!(:instructeur) { create(:instructeur, groupe_instructeurs: [gi1, gi2]) } + let!(:archive1) { create(:archive, :generated, groupe_instructeurs: [gi1]) } + let!(:archive2) { create(:archive, :generated, groupe_instructeurs: [gi2]) } + let(:gi1) { create(:groupe_instructeur) } + let(:gi2) { create(:groupe_instructeur) } + + before do + sign_in(instructeur.user) + end + + after { Timecop.return } + + describe '#index' do + before do + create_dossier_for_month(procedure1, 2021, 3) + create_dossier_for_month(procedure1, 2021, 3) + create_dossier_for_month(procedure1, 2021, 2) + Timecop.freeze(Time.zone.local(2021, 3, 5)) + end + + it 'displays archives' do + get :index, { params: { procedure_id: procedure1.id } } + + expect(assigns(:dossiers_termines).size).to eq(3) + expect(assigns(:archives)).to eq([archive1]) + end + end + + describe '#create' do + let(:month) { '21-03' } + let(:date_month) { Date.strptime(month, "%Y-%m") } + let(:subject) do + post :create, { + xhr: true, + params: { procedure_id: procedure1.id, type: 'monthly', month: month } + } + end + + it "performs archive creation job" do + expect { subject }.to have_enqueued_job(ArchiveCreationJob).with(procedure1, instructeur, 'monthly', date_month) + expect(flash.notice).to include("Votre demande a été prise en compte") + end + end + + private + + def create_dossier_for_month(procedure, year, month) + Timecop.freeze(Time.zone.local(year, month, 5)) + create(:dossier, :accepte, :with_attestation, procedure: procedure) + end +end diff --git a/spec/views/instructeur/procedures/_download_dossiers.html.haml_spec.rb b/spec/views/instructeur/procedures/_download_dossiers.html.haml_spec.rb index 45b522742..a796905d9 100644 --- a/spec/views/instructeur/procedures/_download_dossiers.html.haml_spec.rb +++ b/spec/views/instructeur/procedures/_download_dossiers.html.haml_spec.rb @@ -16,5 +16,15 @@ describe 'instructeurs/procedures/_download_dossiers.html.haml', type: :view do context "when procedure has at least 1 dossier en construction" do let!(:dossier) { create(:dossier, :en_construction, procedure: procedure) } it { is_expected.to include("Télécharger tous les dossiers") } + + context "With zip archive enabled" do + before { Flipper.enable(:archive_zip_globale, procedure) } + it { is_expected.to include("Télécharger une archive au format .zip") } + end + + context "With zip archive disabled" do + before { Flipper.disable(:archive_zip_globale, procedure) } + it { is_expected.not_to include("Télécharger une archive au format .zip") } + end end end From 71a5e41e5b8efe5f9920c0925291bf5018978812 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 30 Mar 2021 10:56:29 +0200 Subject: [PATCH 06/14] add archive_zip_globale feature flag --- app/controllers/instructeurs/archives_controller.rb | 2 +- .../instructeurs/procedures/_download_dossiers.html.haml | 5 +++-- spec/controllers/instructeurs/archives_controller_spec.rb | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/instructeurs/archives_controller.rb b/app/controllers/instructeurs/archives_controller.rb index 308f51b80..d4e7400d7 100644 --- a/app/controllers/instructeurs/archives_controller.rb +++ b/app/controllers/instructeurs/archives_controller.rb @@ -23,7 +23,7 @@ module Instructeurs private def ensure_procedure_enabled - if !procedure.publiee? + if !procedure.feature_enabled?(:archive_zip_globale) || !procedure.publiee? 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 diff --git a/app/views/instructeurs/procedures/_download_dossiers.html.haml b/app/views/instructeurs/procedures/_download_dossiers.html.haml index 1e0993605..b6e5dedf6 100644 --- a/app/views/instructeurs/procedures/_download_dossiers.html.haml +++ b/app/views/instructeurs/procedures/_download_dossiers.html.haml @@ -16,5 +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]) - %li - = link_to t(:download_archive, scope: [:instructeurs, :procedure]), instructeur_archives_path(procedure) + - if procedure.feature_enabled?(:archive_zip_globale) + %li + = link_to t(:download_archive, scope: [:instructeurs, :procedure]), instructeur_archives_path(procedure) diff --git a/spec/controllers/instructeurs/archives_controller_spec.rb b/spec/controllers/instructeurs/archives_controller_spec.rb index d698c95bb..7229f5720 100644 --- a/spec/controllers/instructeurs/archives_controller_spec.rb +++ b/spec/controllers/instructeurs/archives_controller_spec.rb @@ -9,6 +9,7 @@ describe Instructeurs::ArchivesController, type: :controller do before do sign_in(instructeur.user) + Flipper.enable(:archive_zip_globale, procedure1) end after { Timecop.return } From fa4f6190c4aff402fde196b35ed74e364db2a68e Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Thu, 15 Apr 2021 11:15:53 +0200 Subject: [PATCH 07/14] =?UTF-8?q?exporte=20aussi=20les=20d=C3=A9marches=20?= =?UTF-8?q?archiv=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/instructeurs/archives_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/instructeurs/archives_controller.rb b/app/controllers/instructeurs/archives_controller.rb index d4e7400d7..2757dbb33 100644 --- a/app/controllers/instructeurs/archives_controller.rb +++ b/app/controllers/instructeurs/archives_controller.rb @@ -23,7 +23,7 @@ module Instructeurs private def ensure_procedure_enabled - if !procedure.feature_enabled?(:archive_zip_globale) || !procedure.publiee? + 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 From 59a08ad307db192c6e6ce737618c58200e2d91a4 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Mon, 19 Apr 2021 15:59:48 +0200 Subject: [PATCH 08/14] =?UTF-8?q?affiche=20quand=20l'archive=20a=20=C3=A9t?= =?UTF-8?q?=C3=A9=20g=C3=A9n=C3=A9r=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/instructeurs/archives/index.html.haml | 10 +++++----- config/locales/views/instructeurs/fr.yml | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/views/instructeurs/archives/index.html.haml b/app/views/instructeurs/archives/index.html.haml index 057385e69..3faf237dc 100644 --- a/app/views/instructeurs/archives/index.html.haml +++ b/app/views/instructeurs/archives/index.html.haml @@ -47,10 +47,10 @@ - if matching_archive.try(&:available?) = link_to url_for(matching_archive.file), class: 'button primary' do %span.icon.download-white - Télécharger + = 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 - Archive en cours de création + = 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", remote: true do %span.icon.new-folder @@ -78,12 +78,12 @@ - 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élécharger + = t(:archive_ready_html, generated_period: time_ago_in_words(matching_archive.updated_at), scope: [:instructeurs, :procedure]) - else %span.icon.retry - Archive en cours de création + = 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", remote: true do + = 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 diff --git a/config/locales/views/instructeurs/fr.yml b/config/locales/views/instructeurs/fr.yml index 17c7ef429..ef2b1987b 100644 --- a/config/locales/views/instructeurs/fr.yml +++ b/config/locales/views/instructeurs/fr.yml @@ -9,3 +9,5 @@ fr: export_ready_html: Télécharger l’export au format %{export_format}
(généré il y a %{export_time}) export_pending_html: Un export au format %{export_format} est en train d’être généré
(demandé il y a %{export_time}) download_archive: Télécharger une archive au format .zip de tous les dossiers et leurs pièces jointes + archive_pending_html: Archive en cours de création
(demandée il y a %{created_period}) + archive_ready_html: Télécharger l'archive
(demandée il y a %{generated_period}) From 8bee53fe77cbc44bf8a811817651580edd008320 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Wed, 21 Apr 2021 16:54:15 +0200 Subject: [PATCH 09/14] =?UTF-8?q?rend=20plus=20robuste=20la=20cr=C3=A9atio?= =?UTF-8?q?n=20de=20l'archive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../instructeurs/archives_controller.rb | 10 ++++- app/jobs/archive_creation_job.rb | 4 +- app/services/procedure_archive_service.rb | 28 +++++++++----- .../instructeurs/archives/index.html.haml | 2 +- .../instructeurs/archives_controller_spec.rb | 5 ++- .../procedure_archive_service_spec.rb | 37 +++++++++++++++---- 6 files changed, 62 insertions(+), 24 deletions(-) diff --git a/app/controllers/instructeurs/archives_controller.rb b/app/controllers/instructeurs/archives_controller.rb index 2757dbb33..2917520be 100644 --- a/app/controllers/instructeurs/archives_controller.rb +++ b/app/controllers/instructeurs/archives_controller.rb @@ -16,8 +16,14 @@ module Instructeurs type = params[:type] month = Date.strptime(params[:month], '%Y-%m') if params[:month].present? - ArchiveCreationJob.perform_later(procedure, current_instructeur, type, month) - 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." + 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 diff --git a/app/jobs/archive_creation_job.rb b/app/jobs/archive_creation_job.rb index 79b774777..255af07db 100644 --- a/app/jobs/archive_creation_job.rb +++ b/app/jobs/archive_creation_job.rb @@ -1,7 +1,7 @@ class ArchiveCreationJob < ApplicationJob - def perform(procedure, instructeur, type, month) + def perform(procedure, archive, instructeur) ProcedureArchiveService .new(procedure) - .create_archive(instructeur, type, month) + .collect_files_archive(archive, instructeur) end end diff --git a/app/services/procedure_archive_service.rb b/app/services/procedure_archive_service.rb index 66862ff1d..6de4f2359 100644 --- a/app/services/procedure_archive_service.rb +++ b/app/services/procedure_archive_service.rb @@ -5,25 +5,35 @@ class ProcedureArchiveService @procedure = procedure end - def create_archive(instructeur, type, month = nil) + def create_pending_archive(instructeur, type, month=nil) groupe_instructeurs = instructeur .groupe_instructeurs .where(procedure: @procedure) - if type == 'everything' + + archive = Archive.for_groupe_instructeur(groupe_instructeurs).find_by( + content_type: type, + month: month + ) + if archive.nil? + archive = Archive.create!( + content_type: type, + month: month, + groupe_instructeurs: groupe_instructeurs + ) + end + archive + end + + def collect_files_archive(archive, instructeur) + if archive.content_type == 'everything' dossiers = @procedure.dossiers.state_termine else - dossiers = @procedure.dossiers.processed_in_month(month) + dossiers = @procedure.dossiers.processed_in_month(archive.month) end files = create_list_of_attachments(dossiers) - archive = Archive.create!( - content_type: type, - month: month, - groupe_instructeurs: groupe_instructeurs - ) - tmp_file = Tempfile.new(['tc', '.zip']) Zip::OutputStream.open(tmp_file) do |zipfile| diff --git a/app/views/instructeurs/archives/index.html.haml b/app/views/instructeurs/archives/index.html.haml index 3faf237dc..6219cc242 100644 --- a/app/views/instructeurs/archives/index.html.haml +++ b/app/views/instructeurs/archives/index.html.haml @@ -52,7 +52,7 @@ %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", remote: true do + = link_to instructeur_archives_path(@procedure, type: 'everything'), method: :post, class: "button" do %span.icon.new-folder Demander la création - else diff --git a/spec/controllers/instructeurs/archives_controller_spec.rb b/spec/controllers/instructeurs/archives_controller_spec.rb index 7229f5720..2de3579b5 100644 --- a/spec/controllers/instructeurs/archives_controller_spec.rb +++ b/spec/controllers/instructeurs/archives_controller_spec.rb @@ -33,15 +33,16 @@ 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, { - xhr: true, params: { procedure_id: procedure1.id, type: 'monthly', month: month } } end it "performs archive creation job" do - expect { subject }.to have_enqueued_job(ArchiveCreationJob).with(procedure1, instructeur, 'monthly', date_month) + 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(flash.notice).to include("Votre demande a été prise en compte") end end diff --git a/spec/services/procedure_archive_service_spec.rb b/spec/services/procedure_archive_service_spec.rb index 48dd2ba67..367230c25 100644 --- a/spec/services/procedure_archive_service_spec.rb +++ b/spec/services/procedure_archive_service_spec.rb @@ -1,12 +1,34 @@ describe ProcedureArchiveService do - describe '#create_archive' do let(:procedure) { create(:procedure, :published) } let(:instructeur) { create(:instructeur) } let(:service) { ProcedureArchiveService.new(procedure) } let(:year) { 2020 } let(:month) { 3 } let(:date_month) { Date.strptime("#{year}-#{month}", "%Y-%m") } + 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.content_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.content_type).to eq 'everything' + expect(archive.month).to eq nil + expect(archive.pending?).to be_truthy + end + end + end + + describe '#collect_files_archive' do before do create_dossier_for_month(year, month) create_dossier_for_month(2020, month) @@ -15,40 +37,39 @@ describe ProcedureArchiveService do after { Timecop.return } context 'for a specific month' do + let(:archive) { create(:archive, content_type: 'monthly', status: 'pending', month: date_month) } let(:year) { 2021 } let(:mailer) { double('mailer', deliver_later: true) } - it 'creates a monthly archive' do + it 'collect files' do expect(InstructeurMailer).to receive(:send_archive).and_return(mailer) - service.create_archive(instructeur, 'monthly', date_month) + service.collect_files_archive(archive, instructeur) - archive = Archive.last archive.file.open do |f| files = ZipTricks::FileReader.read_zip_structure(io: f) expect(files.size).to be 2 expect(files.first.filename).to include("export") expect(files.last.filename).to include("attestation") end - expect(archive.content_type).to eq 'monthly' expect(archive.file.attached?).to be_truthy end end context 'for all months' do + let(:archive) { create(:archive, content_type: 'everything', status: 'pending') } let(:mailer) { double('mailer', deliver_later: true) } - it 'creates a everything archive' do + it 'collect files' do expect(InstructeurMailer).to receive(:send_archive).and_return(mailer) - service.create_archive(instructeur, 'everything') + service.collect_files_archive(archive, instructeur) archive = Archive.last archive.file.open do |f| files = ZipTricks::FileReader.read_zip_structure(io: f) expect(files.size).to be 4 end - expect(archive.content_type).to eq 'everything' expect(archive.file.attached?).to be_truthy end end From aa407d5607881208e346925dc64c79f2f58a48f2 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Wed, 21 Apr 2021 18:16:22 +0200 Subject: [PATCH 10/14] =?UTF-8?q?place=20le=20cronjob=20PurgeStaleArchives?= =?UTF-8?q?=20dans=20le=20bon=20r=C3=A9pertoire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/jobs/{ => cron}/purge_stale_archives_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/jobs/{ => cron}/purge_stale_archives_job.rb (67%) diff --git a/app/jobs/purge_stale_archives_job.rb b/app/jobs/cron/purge_stale_archives_job.rb similarity index 67% rename from app/jobs/purge_stale_archives_job.rb rename to app/jobs/cron/purge_stale_archives_job.rb index 979224ad2..4ce48784a 100644 --- a/app/jobs/purge_stale_archives_job.rb +++ b/app/jobs/cron/purge_stale_archives_job.rb @@ -1,4 +1,4 @@ -class PurgeStaleArchivesJob < CronJob +class Cron::PurgeStaleArchivesJob < Cron::CronJob self.schedule_expression = "every 5 minutes" def perform From 9134114c2e391894c039e8cbc86d1c8893ffbe70 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Wed, 21 Apr 2021 18:19:55 +0200 Subject: [PATCH 11/14] fix rubocop offenses --- app/services/procedure_archive_service.rb | 3 +-- .../instructeurs/archives_controller_spec.rb | 2 +- spec/services/procedure_archive_service_spec.rb | 13 ++++++------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/services/procedure_archive_service.rb b/app/services/procedure_archive_service.rb index 6de4f2359..83b9ce97e 100644 --- a/app/services/procedure_archive_service.rb +++ b/app/services/procedure_archive_service.rb @@ -5,12 +5,11 @@ class ProcedureArchiveService @procedure = procedure end - def create_pending_archive(instructeur, type, month=nil) + def create_pending_archive(instructeur, type, month = nil) groupe_instructeurs = instructeur .groupe_instructeurs .where(procedure: @procedure) - archive = Archive.for_groupe_instructeur(groupe_instructeurs).find_by( content_type: type, month: month diff --git a/spec/controllers/instructeurs/archives_controller_spec.rb b/spec/controllers/instructeurs/archives_controller_spec.rb index 2de3579b5..7ce7e4d22 100644 --- a/spec/controllers/instructeurs/archives_controller_spec.rb +++ b/spec/controllers/instructeurs/archives_controller_spec.rb @@ -36,7 +36,7 @@ describe Instructeurs::ArchivesController, type: :controller do let(:archive) { create(:archive) } let(:subject) do post :create, { - params: { procedure_id: procedure1.id, type: 'monthly', month: month } + params: { procedure_id: procedure1.id, type: 'monthly', month: month } } end diff --git a/spec/services/procedure_archive_service_spec.rb b/spec/services/procedure_archive_service_spec.rb index 367230c25..b6840b832 100644 --- a/spec/services/procedure_archive_service_spec.rb +++ b/spec/services/procedure_archive_service_spec.rb @@ -1,12 +1,11 @@ describe ProcedureArchiveService do - let(:procedure) { create(:procedure, :published) } - let(:instructeur) { create(:instructeur) } - let(:service) { ProcedureArchiveService.new(procedure) } - let(:year) { 2020 } - let(:month) { 3 } - let(:date_month) { Date.strptime("#{year}-#{month}", "%Y-%m") } + let(:procedure) { create(:procedure, :published) } + let(:instructeur) { create(:instructeur) } + let(:service) { ProcedureArchiveService.new(procedure) } + let(:year) { 2020 } + let(:month) { 3 } + let(:date_month) { Date.strptime("#{year}-#{month}", "%Y-%m") } 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) From dfbe004122ff52f91d349bb0b1c56b116bd6f5e0 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 27 Apr 2021 18:54:58 +0200 Subject: [PATCH 12/14] rename content_type to time_span_type for archives --- app/models/archive.rb | 16 ++++++++-------- app/services/procedure_archive_service.rb | 4 ++-- app/views/instructeurs/archives/index.html.haml | 4 ++-- ...ent_type_to_to_time_span_type_for_archives.rb | 5 +++++ db/schema.rb | 4 ++-- spec/factories/archive.rb | 2 +- spec/services/procedure_archive_service_spec.rb | 8 ++++---- 7 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 db/migrate/20210427112642_rename_content_type_to_to_time_span_type_for_archives.rb diff --git a/app/models/archive.rb b/app/models/archive.rb index 082176066..2f2f5c1b8 100644 --- a/app/models/archive.rb +++ b/app/models/archive.rb @@ -2,12 +2,12 @@ # # Table name: archives # -# id :bigint not null, primary key -# content_type :string not null -# month :date -# status :string not null -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint not null, primary key +# 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 @@ -26,7 +26,7 @@ class Archive < ApplicationRecord ) } - enum content_type: { + enum time_span_type: { everything: 'everything', monthly: 'monthly' } @@ -50,7 +50,7 @@ class Archive < ApplicationRecord end def filename(procedure) - if content_type == 'everything' + if time_span_type == 'everything' "procedure-#{procedure.id}.zip" else "procedure-#{procedure.id}-mois-#{I18n.l(month, format: '%Y-%m')}.zip" diff --git a/app/services/procedure_archive_service.rb b/app/services/procedure_archive_service.rb index 83b9ce97e..1b5bd86e3 100644 --- a/app/services/procedure_archive_service.rb +++ b/app/services/procedure_archive_service.rb @@ -11,7 +11,7 @@ class ProcedureArchiveService .where(procedure: @procedure) archive = Archive.for_groupe_instructeur(groupe_instructeurs).find_by( - content_type: type, + time_span_type: type, month: month ) if archive.nil? @@ -25,7 +25,7 @@ class ProcedureArchiveService end def collect_files_archive(archive, instructeur) - if archive.content_type == 'everything' + if archive.time_span_type == 'everything' dossiers = @procedure.dossiers.state_termine else dossiers = @procedure.dossiers.processed_in_month(archive.month) diff --git a/app/views/instructeurs/archives/index.html.haml b/app/views/instructeurs/archives/index.html.haml index 6219cc242..82ec92f1d 100644 --- a/app/views/instructeurs/archives/index.html.haml +++ b/app/views/instructeurs/archives/index.html.haml @@ -32,7 +32,7 @@ %tbody - if can_generate_archive?(@dossiers_termines, @poids_total) %tr - - matching_archive = @archives.find_by(content_type: 'everything') + - matching_archive = @archives.find_by(time_span_type: 'everything') %td Tous les dossiers %td @@ -60,7 +60,7 @@ - @archivable_months.each do |month| - dossiers_termines = @procedure.dossiers.processed_in_month(month) - nb_dossiers_termines = dossiers_termines.count - - matching_archive = @archives.find_by(content_type: 'monthly', month: month) + - matching_archive = @archives.find_by(time_span_type: 'monthly', month: month) %tr %td = I18n.l(month, format: "%B %Y") diff --git a/db/migrate/20210427112642_rename_content_type_to_to_time_span_type_for_archives.rb b/db/migrate/20210427112642_rename_content_type_to_to_time_span_type_for_archives.rb new file mode 100644 index 000000000..be6d5a404 --- /dev/null +++ b/db/migrate/20210427112642_rename_content_type_to_to_time_span_type_for_archives.rb @@ -0,0 +1,5 @@ +class RenameContentTypeToToTimeSpanTypeForArchives < ActiveRecord::Migration[6.1] + def change + rename_column :archives, :content_type, :time_span_type + end +end diff --git a/db/schema.rb b/db/schema.rb index 3b5bcf32d..04a730a00 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_04_27_120002) do +ActiveRecord::Schema.define(version: 2021_04_27_112642) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -84,7 +84,7 @@ ActiveRecord::Schema.define(version: 2021_04_27_120002) do create_table "archives", force: :cascade do |t| t.string "status", null: false t.date "month" - t.string "content_type", null: false + t.string "time_span_type", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end diff --git a/spec/factories/archive.rb b/spec/factories/archive.rb index 16a302219..8bdbb877e 100644 --- a/spec/factories/archive.rb +++ b/spec/factories/archive.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory :archive do - content_type { 'everything' } + time_span_type { 'everything' } groupe_instructeurs { [association(:groupe_instructeur)] } trait :pending do diff --git a/spec/services/procedure_archive_service_spec.rb b/spec/services/procedure_archive_service_spec.rb index b6840b832..80e013f74 100644 --- a/spec/services/procedure_archive_service_spec.rb +++ b/spec/services/procedure_archive_service_spec.rb @@ -10,7 +10,7 @@ describe ProcedureArchiveService do it 'creates a pending archive' do archive = service.create_pending_archive(instructeur, 'monthly', date_month) - expect(archive.content_type).to eq 'monthly' + expect(archive.time_span_type).to eq 'monthly' expect(archive.month).to eq date_month expect(archive.pending?).to be_truthy end @@ -20,7 +20,7 @@ describe ProcedureArchiveService do it 'creates a pending archive' do archive = service.create_pending_archive(instructeur, 'everything') - expect(archive.content_type).to eq 'everything' + expect(archive.time_span_type).to eq 'everything' expect(archive.month).to eq nil expect(archive.pending?).to be_truthy end @@ -36,7 +36,7 @@ describe ProcedureArchiveService do after { Timecop.return } context 'for a specific month' do - let(:archive) { create(:archive, content_type: 'monthly', status: 'pending', month: date_month) } + let(:archive) { create(:archive, time_span_type: 'monthly', status: 'pending', month: date_month) } let(:year) { 2021 } let(:mailer) { double('mailer', deliver_later: true) } @@ -56,7 +56,7 @@ describe ProcedureArchiveService do end context 'for all months' do - let(:archive) { create(:archive, content_type: 'everything', status: 'pending') } + let(:archive) { create(:archive, time_span_type: 'everything', status: 'pending') } let(:mailer) { double('mailer', deliver_later: true) } it 'collect files' do From 09870c918d4a6635dcf89a629245e2caa5832f59 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 27 Apr 2021 18:57:13 +0200 Subject: [PATCH 13/14] prevent race conditions when creating archives --- app/models/archive.rb | 12 ++++++++++++ app/services/procedure_archive_service.rb | 13 +------------ db/migrate/20210427124500_add_key_to_archives.rb | 6 ++++++ db/schema.rb | 4 +++- spec/factories/archive.rb | 1 + 5 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 db/migrate/20210427124500_add_key_to_archives.rb diff --git a/app/models/archive.rb b/app/models/archive.rb index 2f2f5c1b8..7ee93720f 100644 --- a/app/models/archive.rb +++ b/app/models/archive.rb @@ -3,6 +3,7 @@ # 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 @@ -56,4 +57,15 @@ class Archive < ApplicationRecord "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 diff --git a/app/services/procedure_archive_service.rb b/app/services/procedure_archive_service.rb index 1b5bd86e3..b26757210 100644 --- a/app/services/procedure_archive_service.rb +++ b/app/services/procedure_archive_service.rb @@ -10,18 +10,7 @@ class ProcedureArchiveService .groupe_instructeurs .where(procedure: @procedure) - archive = Archive.for_groupe_instructeur(groupe_instructeurs).find_by( - time_span_type: type, - month: month - ) - if archive.nil? - archive = Archive.create!( - content_type: type, - month: month, - groupe_instructeurs: groupe_instructeurs - ) - end - archive + Archive.find_or_create_archive(type, month, groupe_instructeurs) end def collect_files_archive(archive, instructeur) diff --git a/db/migrate/20210427124500_add_key_to_archives.rb b/db/migrate/20210427124500_add_key_to_archives.rb new file mode 100644 index 000000000..08559a0dc --- /dev/null +++ b/db/migrate/20210427124500_add_key_to_archives.rb @@ -0,0 +1,6 @@ +class AddKeyToArchives < ActiveRecord::Migration[6.1] + def change + add_column :archives, :key, :text, null: false + add_index :archives, [:key, :time_span_type, :month], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 04a730a00..eefe0112f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_04_27_112642) do +ActiveRecord::Schema.define(version: 2021_04_27_124500) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -87,6 +87,8 @@ ActiveRecord::Schema.define(version: 2021_04_27_112642) do t.string "time_span_type", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.text "key", null: false + t.index ["key", "time_span_type", "month"], name: "index_archives_on_key_and_time_span_type_and_month", unique: true end create_table "archives_groupe_instructeurs", force: :cascade do |t| diff --git a/spec/factories/archive.rb b/spec/factories/archive.rb index 8bdbb877e..41fbc2542 100644 --- a/spec/factories/archive.rb +++ b/spec/factories/archive.rb @@ -2,6 +2,7 @@ FactoryBot.define do factory :archive do time_span_type { 'everything' } groupe_instructeurs { [association(:groupe_instructeur)] } + key { 'unique-key' } trait :pending do status { 'pending' } From 85b907807a7ebe85c1cc56f444fa5e460c4fb170 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 23 Apr 2021 12:45:04 +0100 Subject: [PATCH 14/14] Improuve ComboMultipleDropdownList style --- app/assets/stylesheets/forms.scss | 69 ++++++++++++------- app/assets/stylesheets/procedure_show.scss | 17 +++-- .../components/ComboMultipleDropdownList.jsx | 23 +++++-- app/javascript/new_design/dropdown.js | 2 +- .../instructeurs/procedures/show.html.haml | 2 +- package.json | 1 + .../features/instructeurs/instruction_spec.rb | 4 +- .../instructeurs/procedure_filters_spec.rb | 11 +-- spec/features/users/brouillon_spec.rb | 29 ++------ spec/support/feature_helpers.rb | 44 ++++++++---- yarn.lock | 5 ++ 11 files changed, 128 insertions(+), 79 deletions(-) diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index c473aca8d..cfe13af7d 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -176,7 +176,7 @@ margin-bottom: $default-fields-spacer; &.small-margin { - margin-bottom: $default-padding / 2; + margin-bottom: $default-spacer; } } @@ -186,7 +186,7 @@ input[type=checkbox] { &.small-margin { - margin-bottom: $default-padding / 2; + margin-bottom: $default-spacer; } } @@ -203,7 +203,7 @@ padding: $default-padding; &.small { - padding: $default-padding / 2; + padding: $default-spacer; } &:disabled { @@ -292,12 +292,13 @@ [data-reach-combobox-input] { - padding: 16px; width: 100%; min-width: 50%; max-width: 100%; min-height: 62px; - margin-bottom: 40px; + border-radius: 4px; + border: solid 1px $border-grey; + padding: $default-padding; } [data-reach-combobox-input]:focus { @@ -305,25 +306,31 @@ } [data-reach-combobox-token-list] { - padding: $default-padding; + padding: $default-spacer; display: flex; + align-items: center; + list-style: none; } [data-reach-combobox-token] { border: solid 1px $border-grey; color: $black; - margin-top: $default-padding; - margin-bottom: $default-padding; - margin-right: 0.5 * $default-padding; border-radius: 4px; - padding: $default-padding; + padding: $default-spacer; + margin-right: $default-spacer; cursor: pointer; - list-style: none; + display: flex; + align-items: center; } [data-reach-combobox-token]:focus { background-color: $black; color: $white; + + [data-combobox-remove-token] { + background-color: $black; + color: $white; + } } .editable-champ { @@ -388,7 +395,7 @@ .explication { margin-bottom: $default-fields-spacer; - padding: $default-padding / 2; + padding: $default-spacer; background-color: $light-grey; p:not(:last-child) { @@ -467,23 +474,26 @@ } } +[data-react-class]:not([data-react-class="ComboMultipleDropdownList"]) { + [data-reach-combobox-input] { + margin-bottom: $default-fields-spacer; + } +} + [data-react-class="ComboMultipleDropdownList"] { margin-bottom: $default-fields-spacer; [data-reach-combobox-input] { - outline: none; - border: none; flex-grow: 1; - margin: 0.25rem; background-image: image-url("icons/chevron-down"); background-size: 14px; background-repeat: no-repeat; background-position: right 10px center; - } - - [data-reach-combobox-input]:focus { - outline: solid; - outline-color: $light-blue; + border-radius: 4px; + border: solid 1px $border-grey; + padding: $default-padding; + margin: $default-spacer; + margin-top: 0; } } @@ -507,15 +517,26 @@ [data-combobox-separator] { font-size: 16px; color: $dark-grey; - margin-top: 6px; + background: $light-grey; + padding: $default-spacer; } [data-combobox-remove-token] { - color: $dark-grey; - padding-right: 4px; + cursor: pointer; + background-color: transparent; + background-image: none; + border: none; + line-height: 1; + padding: 0; + margin-right: 4px; + display: flex; + align-items: center !important; } - [data-reach-combobox-input]:focus { outline-color: $light-blue; } + +[data-reach-combobox-popover] { + z-index: 20; +} diff --git a/app/assets/stylesheets/procedure_show.scss b/app/assets/stylesheets/procedure_show.scss index c05969248..62d31ba71 100644 --- a/app/assets/stylesheets/procedure_show.scss +++ b/app/assets/stylesheets/procedure_show.scss @@ -79,24 +79,33 @@ padding: 0.25 * $default-padding; cursor: pointer; list-style: none; + display: flex; + align-items: center; } [data-reach-combobox-token]:focus { background-color: $black; color: $white; + + [data-combobox-remove-token] { + background-color: $black; + color: $white; + } } [data-reach-combobox-input] { outline: none; - border: none; flex-grow: 1; - margin: 0.25rem; + margin: $default-spacer; + padding: $default-spacer; + border-radius: 4px; + border: solid 1px $border-grey; + margin-top: 0; } [data-reach-combobox-input]:focus { - outline: solid; - outline-color: $light-blue; + border-color: $blue; } } } diff --git a/app/javascript/components/ComboMultipleDropdownList.jsx b/app/javascript/components/ComboMultipleDropdownList.jsx index 90eada298..a10c74215 100644 --- a/app/javascript/components/ComboMultipleDropdownList.jsx +++ b/app/javascript/components/ComboMultipleDropdownList.jsx @@ -18,6 +18,7 @@ import { import '@reach/combobox/styles.css'; import { matchSorter } from 'match-sorter'; import { fire } from '@utils'; +import { XIcon } from '@heroicons/react/outline'; const Context = createContext(); @@ -152,7 +153,7 @@ function ComboMultipleDropdownList({ /> {results && ( - + {results.length === 0 && (

Aucun résultat{' '} @@ -160,11 +161,17 @@ function ComboMultipleDropdownList({

)} - {results.map(([label], index) => { + {results.map(([label, value], index) => { if (label.startsWith('--')) { return ; } - return ; + return ( + + ); })}
@@ -226,15 +233,17 @@ function ComboboxToken({ value, ...props }) { }} {...props} > - { onRemove(value); }} > - x - + + Désélectionner + {value} ); diff --git a/app/javascript/new_design/dropdown.js b/app/javascript/new_design/dropdown.js index c92a8f896..dc948c983 100644 --- a/app/javascript/new_design/dropdown.js +++ b/app/javascript/new_design/dropdown.js @@ -1,7 +1,7 @@ import { delegate } from '@utils'; delegate('click', 'body', (event) => { - if (!event.target.closest('.dropdown')) { + if (!event.target.closest('.dropdown, [data-reach-combobox-popover]')) { [...document.querySelectorAll('.dropdown')].forEach((element) => { const button = element.querySelector('.dropdown-button'); button.setAttribute('aria-expanded', false); diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index 90d95fcb9..1c8eaa0e6 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -121,7 +121,7 @@ %button.button.dropdown-button{ 'aria-expanded' => 'false', 'aria-controls' => 'custom-menu' } Personnaliser #custom-menu.dropdown-content.fade-in-down - = form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form columns-form' do + = form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do - hidden_field_id = SecureRandom.uuid = hidden_field_tag :values, nil, data: { uuid: hidden_field_id } = react_component("ComboMultipleDropdownList", options: @displayed_fields_options, selected: @displayed_fields_selected, disabled: [], hiddenFieldId: hidden_field_id, label: 'colonne') diff --git a/package.json b/package.json index 326f941e6..3c562fe07 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "@fortawesome/fontawesome-svg-core": "^1.2.34", "@fortawesome/free-solid-svg-icons": "^5.15.2", "@fortawesome/react-fontawesome": "^0.1.14", + "@heroicons/react": "^1.0.1", "@mapbox/mapbox-gl-draw": "^1.2.0", "@rails/actiontext": "^6.0.3", "@rails/activestorage": "^6.0.3", diff --git a/spec/features/instructeurs/instruction_spec.rb b/spec/features/instructeurs/instruction_spec.rb index 3fdadf27d..257ca2a73 100644 --- a/spec/features/instructeurs/instruction_spec.rb +++ b/spec/features/instructeurs/instruction_spec.rb @@ -130,8 +130,8 @@ feature 'Instructing a dossier:', js: true do click_on 'Personnes impliquées' - select_multi('email instructeur', instructeur_2.email) - select_multi('email instructeur', instructeur_3.email) + select_multi_combobox('email instructeur', instructeur_2.email, instructeur_2.id) + select_multi_combobox('email instructeur', instructeur_3.email, instructeur_3.id) click_on 'Envoyer' diff --git a/spec/features/instructeurs/procedure_filters_spec.rb b/spec/features/instructeurs/procedure_filters_spec.rb index 2e3ab6519..342151059 100644 --- a/spec/features/instructeurs/procedure_filters_spec.rb +++ b/spec/features/instructeurs/procedure_filters_spec.rb @@ -32,7 +32,7 @@ feature "procedure filters" do end scenario "should add be able to add created_at column", js: true do - add_column("Créé le") + add_column("Créé le", "self/created_at") within ".dossiers-table" do expect(page).to have_link("Créé le") expect(page).to have_link(new_unfollow_dossier.created_at.strftime('%d/%m/%Y')) @@ -40,7 +40,7 @@ feature "procedure filters" do end scenario "should add be able to add and remove custom type_de_champ column", js: true do - add_column(type_de_champ.libelle) + add_column(type_de_champ.libelle, "type_de_champ/#{type_de_champ.stable_id}") within ".dossiers-table" do expect(page).to have_link(type_de_champ.libelle) expect(page).to have_link(champ.value) @@ -123,15 +123,16 @@ feature "procedure filters" do click_button "Ajouter le filtre" end - def add_column(column_name) + def add_column(column_name, column_path) click_on 'Personnaliser' - select_multi('colonne', column_name) + select_multi_combobox('colonne', column_name, column_path) click_button "Enregistrer" end def remove_column(column_name) click_on 'Personnaliser' - find(:xpath, "//li[contains(text(), '#{column_name}')]/span[contains(text(), 'x')]").click + find(:xpath, ".//li[contains(text(), \"#{column_name}\")]/button", text: 'Désélectionner').click + find("body").native.send_key("Escape") click_button "Enregistrer" end end diff --git a/spec/features/users/brouillon_spec.rb b/spec/features/users/brouillon_spec.rb index 9f3b9fa0f..2c6450ae7 100644 --- a/spec/features/users/brouillon_spec.rb +++ b/spec/features/users/brouillon_spec.rb @@ -25,16 +25,13 @@ feature 'The user' do check('val1') check('val3') select('bravo', from: form_id_for('simple_choice_drop_down_list_long')) - select_multi('multiple_choice_drop_down_list_long', 'alpha') - select_multi('multiple_choice_drop_down_list_long', 'charly') + select_multi_combobox('multiple_choice_drop_down_list_long', 'alp', 'alpha') + select_multi_combobox('multiple_choice_drop_down_list_long', 'cha', 'charly') - select_champ_geo('pays', 'aust', 'AUSTRALIE') - - select_champ_geo('regions', 'Ma', 'Martinique') - - select_champ_geo('departements', 'Ai', '02 - Aisne') - - select_champ_geo('communes', 'Ambl', 'Ambléon (01300)') + select_combobox('pays', 'aust', 'AUSTRALIE') + select_combobox('regions', 'Ma', 'Martinique') + select_combobox('departements', 'Ai', '02 - Aisne') + select_combobox('communes', 'Ambl', 'Ambléon (01300)') check('engagement') fill_in('dossier_link', with: '123') @@ -320,10 +317,6 @@ feature 'The user' do e.sibling('.datetime').first('select')[:id][0..-4] end - def have_hidden_field(libelle, with:) - have_css("##{form_id_for(libelle)}[value=\"#{with}\"]") - end - def champ_value_for(libelle) champs = user_dossier.champs champs.find { |c| c.libelle == libelle }.value @@ -352,14 +345,4 @@ feature 'The user' do expect(page).to have_selected_value("#{field}_4i", selected: date.strftime('%H')) expect(page).to have_selected_value("#{field}_5i", selected: date.strftime('%M')) end - - def select_champ_geo(champ, fill_with, value) - input = find("input[aria-label=#{champ}") - input.click - input.fill_in with: fill_with - selector = "li[data-option-value=\"#{value}\"]" - find(selector).click - expect(page).to have_css(selector) - expect(page).to have_hidden_field(champ, with: value) - end end diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb index 8973ba57b..6e2302735 100644 --- a/spec/support/feature_helpers.rb +++ b/spec/support/feature_helpers.rb @@ -103,23 +103,43 @@ module FeatureHelpers end end - def select_multi(champ, with) - input = find("input[aria-label='#{champ}'") + def select_combobox(champ, fill_with, value) + input = find("input[aria-label=\"#{champ}\"") input.click + input.fill_in with: fill_with + selector = "li[data-option-value=\"#{value}\"]" + find(selector).click + expect(page).to have_css(selector) + expect(page).to have_hidden_field(champ, with: value) + end - # hack because for unknown reason, the click on input doesn't show combobox-popover with selenium driver - script = "document.evaluate(\"//input[@aria-label='#{champ}']//ancestor::div[@data-reach-combobox]/div[@data-reach-combobox-popover]\", document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null).iterateNext().removeAttribute(\"hidden\")" - execute_script(script) - - element = find(:xpath, "//input[@aria-label='#{champ}']/ancestor::div[@data-reach-combobox]//div[@data-reach-combobox-popover]//li/span[normalize-space(text())='#{with}']") - element.click + def select_multi_combobox(champ, fill_with, value) + input = find("input[aria-label=\"#{champ}\"") + input.click + input.fill_in with: fill_with + selector = "li[data-option-value=\"#{value}\"]" + find(selector).click + check_selected_value(champ, value) end def check_selected_values(champ, values) - combobox = find(:xpath, "//input[@aria-label='#{champ}']/ancestor::div[@data-react-class='ComboMultipleDropdownList']") - hiddenFieldId = JSON.parse(combobox["data-react-props"])["hiddenFieldId"] - hiddenField = find("input[data-uuid='#{hiddenFieldId}']") - expect(values.sort).to eq(JSON.parse(hiddenField.value).sort) + combobox = find(:xpath, "//input[@aria-label=\"#{champ}\"]/ancestor::div[@data-react-class='ComboMultipleDropdownList']") + hidden_field_id = JSON.parse(combobox["data-react-props"])["hiddenFieldId"] + hidden_field = find("input[data-uuid=\"#{hidden_field_id}\"]") + hidden_field_values = JSON.parse(hidden_field.value) + expect(values.sort).to eq(hidden_field_values.sort) + end + + def check_selected_value(champ, value) + combobox = find(:xpath, "//input[@aria-label=\"#{champ}\"]/ancestor::div[@data-react-class='ComboMultipleDropdownList']") + hidden_field_id = JSON.parse(combobox["data-react-props"])["hiddenFieldId"] + hidden_field = find("input[data-uuid=\"#{hidden_field_id}\"]") + hidden_field_values = JSON.parse(hidden_field.value) + expect(hidden_field_values).to include(value) + end + + def have_hidden_field(libelle, with:) + have_css("##{form_id_for(libelle)}[value=\"#{with}\"]") end # Keep the brower window open after a test success of failure, to diff --git a/yarn.lock b/yarn.lock index 59c13c45b..18389b691 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1031,6 +1031,11 @@ dependencies: prop-types "^15.7.2" +"@heroicons/react@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.1.tgz#66d25f6441920bd5c2146ea27fd33995885452dd" + integrity sha512-uikw2gKCmqnvjVxitecWfFLMOKyL9BTFcU4VM3hHj9OMwpkCr5Ke+MRMyY2/aQVmsYs4VTq7NCFX05MYwAHi3g== + "@jest/types@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59"