download a dossier as zip with all attachments

This commit is contained in:
clemkeirua 2019-07-01 15:55:37 +02:00
parent 5a70ae7a8f
commit 25f81f1d3c
14 changed files with 194 additions and 2 deletions

1
.gitignore vendored
View file

@ -30,6 +30,7 @@ yarn-debug.log*
/public/packs /public/packs
/public/packs-test /public/packs-test
/node_modules /node_modules
vendor/*
/yarn-error.log /yarn-error.log
yarn-debug.log* yarn-debug.log*
.yarn-integrity .yarn-integrity

View file

@ -67,6 +67,7 @@ gem 'turbolinks' # Turbolinks makes following links in your web application fast
gem 'typhoeus' gem 'typhoeus'
gem 'warden' gem 'warden'
gem 'webpacker' gem 'webpacker'
gem 'zipline'
gem 'zxcvbn-ruby', require: 'zxcvbn' gem 'zxcvbn-ruby', require: 'zxcvbn'
group :test do group :test do

View file

@ -170,6 +170,7 @@ GEM
crass (1.0.4) crass (1.0.4)
css_parser (1.6.0) css_parser (1.6.0)
addressable addressable
curb (0.9.10)
daemons (1.3.1) daemons (1.3.1)
database_cleaner (1.7.0) database_cleaner (1.7.0)
datetime_picker_rails (0.0.7) datetime_picker_rails (0.0.7)
@ -674,6 +675,11 @@ GEM
nokogiri (~> 1.8) nokogiri (~> 1.8)
xray-rails (0.3.1) xray-rails (0.3.1)
rails (>= 3.1.0) rails (>= 3.1.0)
zip_tricks (4.7.4)
zipline (1.1.0)
curb (>= 0.8.0, < 0.10)
rails (>= 3.2.1, < 6.1)
zip_tricks (>= 4.2.1, <= 5.0.0)
zxcvbn-ruby (0.1.2) zxcvbn-ruby (0.1.2)
PLATFORMS PLATFORMS
@ -779,6 +785,7 @@ DEPENDENCIES
webmock webmock
webpacker webpacker
xray-rails xray-rails
zipline
zxcvbn-ruby zxcvbn-ruby
BUNDLED WITH BUNDLED WITH

View file

@ -5,6 +5,9 @@ module Gestionnaires
include CreateAvisConcern include CreateAvisConcern
include DossierHelper include DossierHelper
include ActionController::Streaming
include Zipline
after_action :mark_demande_as_read, only: :show after_action :mark_demande_as_read, only: :show
after_action :mark_messagerie_as_read, only: [:messagerie, :create_commentaire] after_action :mark_messagerie_as_read, only: [:messagerie, :create_commentaire]
after_action :mark_avis_as_read, only: [:avis, :create_avis] after_action :mark_avis_as_read, only: [:avis, :create_avis]
@ -172,6 +175,14 @@ module Gestionnaires
render layout: "print" render layout: "print"
end end
def telecharger_pjs
return head(:forbidden) if !Flipflop.download_as_zip_enabled? || !dossier.attachments_downloadable?
files = ActiveStorage::DownloadableFile.create_list_from_dossier(dossier)
zipline(files, "dossier-#{dossier.id}.zip")
end
private private
def dossier def dossier

View file

@ -0,0 +1,29 @@
class ActiveStorage::DownloadableFile
def initialize(attached)
if using_local_backend?
@url = 'file://' + ActiveStorage::Blob.service.path_for(attached.key)
else
@url = attached.service_url
end
end
def url
@url
end
def self.create_list_from_dossier(dossier)
pjs = PiecesJustificativesService.liste_pieces_justificatives(dossier)
pjs.map do |pj|
[
ActiveStorage::DownloadableFile.new(pj.piece_justificative_file),
pj.piece_justificative_file.filename.to_s
]
end
end
private
def using_local_backend?
[:local, :local_test, :test].include?(Rails.application.config.active_storage.service)
end
end

View file

@ -466,6 +466,10 @@ class Dossier < ApplicationRecord
end end
end end
def attachments_downloadable?
!PiecesJustificativesService.liste_pieces_justificatives(self).empty? && PiecesJustificativesService.pieces_justificatives_total_size(self) < 50.megabytes
end
private private
def log_dossier_operation(author, operation, subject = nil) def log_dossier_operation(author, operation, subject = nil)

View file

@ -70,6 +70,17 @@ class PiecesJustificativesService
end end
end end
def self.liste_pieces_justificatives(dossier)
dossier.champs
.select { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative) }
.filter { |pj| pj.piece_justificative_file.attached? }
end
def self.pieces_justificatives_total_size(dossier)
liste_pieces_justificatives(dossier)
.sum { |pj| pj.piece_justificative_file.byte_size }
end
def self.serialize_types_de_champ_as_type_pj(procedure) def self.serialize_types_de_champ_as_type_pj(procedure)
tdcs = procedure.types_de_champ.select { |type_champ| type_champ.old_pj.present? } tdcs = procedure.types_de_champ.select { |type_champ| type_champ.old_pj.present? }
tdcs.map.with_index do |type_champ, order_place| tdcs.map.with_index do |type_champ, order_place|

View file

@ -18,6 +18,14 @@
= link_to "Tout le dossier", print_gestionnaire_dossier_path(dossier.procedure, dossier), target: "_blank", rel: "noopener", class: "menu-item menu-link" = link_to "Tout le dossier", print_gestionnaire_dossier_path(dossier.procedure, dossier), target: "_blank", rel: "noopener", class: "menu-item menu-link"
%li %li
= link_to "Uniquement cet onglet", "#", onclick: "window.print()", class: "menu-item menu-link" = link_to "Uniquement cet onglet", "#", onclick: "window.print()", class: "menu-item menu-link"
- if Flipflop.download_as_zip_enabled? && dossier.attachments_downloadable?
%span.dropdown.print-menu-opener
%button.button.dropdown-button.icon-only
%span.icon.attachment
%ul.print-menu.dropdown-content
%li
= link_to "Télécharger toutes les pièces jointes", telecharger_pjs_gestionnaire_dossier_path(dossier.procedure, dossier), target: "_blank", rel: "noopener", class: "menu-item menu-link"
= render partial: "gestionnaires/procedures/dossier_actions", locals: { procedure: dossier.procedure, dossier: dossier, dossier_is_followed: current_gestionnaire&.follow?(dossier) } = render partial: "gestionnaires/procedures/dossier_actions", locals: { procedure: dossier.procedure, dossier: dossier, dossier_is_followed: current_gestionnaire&.follow?(dossier) }
%span.state-button %span.state-button

View file

@ -18,6 +18,8 @@ Flipflop.configure do
feature :procedure_export_v2_enabled feature :procedure_export_v2_enabled
feature :operation_log_serialize_subject feature :operation_log_serialize_subject
feature :download_as_zip_enabled,
default: false
group :development do group :development do
feature :mini_profiler_enabled, feature :mini_profiler_enabled,

View file

@ -335,6 +335,7 @@ Rails.application.routes.draw do
post 'send-to-instructeurs' => 'dossiers#send_to_instructeurs' post 'send-to-instructeurs' => 'dossiers#send_to_instructeurs'
post 'avis' => 'dossiers#create_avis' post 'avis' => 'dossiers#create_avis'
get 'print' => 'dossiers#print' get 'print' => 'dossiers#print'
get 'telecharger_pjs' => 'dossiers#telecharger_pjs'
end end
end end
end end

View file

@ -524,4 +524,24 @@ describe Gestionnaires::DossiersController, type: :controller do
it { expect(champ_repetition.champs.first.value).to eq('text') } it { expect(champ_repetition.champs.first.value).to eq('text') }
it { expect(response).to redirect_to(annotations_privees_gestionnaire_dossier_path(dossier.procedure, dossier)) } it { expect(response).to redirect_to(annotations_privees_gestionnaire_dossier_path(dossier.procedure, dossier)) }
end end
describe "#telecharger_pjs" do
subject do
get :telecharger_pjs, params: {
procedure_id: procedure.id,
dossier_id: dossier.id
}
end
context 'when zip download is disabled through flipflop' do
before do
Flipflop::FeatureSet.current.test!.switch!(:download_as_zip_enabled, false)
end
it 'is forbidden' do
subject
expect(response).to have_http_status(:forbidden)
end
end
end
end end

View file

@ -0,0 +1,26 @@
describe ActiveStorage::DownloadableFile do
let(:tpjs) { [tpj_not_mandatory] }
let!(:tpj_not_mandatory) do
TypeDePieceJustificative.create(libelle: 'not mandatory', mandatory: false)
end
let(:procedure) { Procedure.create(types_de_piece_justificative: tpjs) }
let(:dossier) { Dossier.create(procedure: procedure) }
let(:procedure) { Procedure.create(types_de_piece_justificative: tpjs) }
let(:dossier) { Dossier.create(procedure: procedure) }
let(:list) { ActiveStorage::DownloadableFile.create_list_from_dossier(dossier) }
describe 'create_list_from_dossier' do
context 'when no piece_justificative is present' do
it { expect(list).to match([]) }
end
context 'when there is a piece_justificative' do
let (:pj) { create(:champ, :piece_justificative, :with_piece_justificative_file) }
before do
dossier.champs = [pj]
end
it { expect(list.length).to be 1 }
end
end
end

View file

@ -99,6 +99,14 @@ describe Dossier do
end end
end end
describe '#types_de_piece_justificative' do
subject { dossier.types_de_piece_justificative }
it 'returns list of required piece justificative' do
expect(subject.size).to eq(2)
expect(subject).to include(TypeDePieceJustificative.find(TypeDePieceJustificative.first.id))
end
end
describe '#retrieve_last_piece_justificative_by_type', vcr: { cassette_name: 'models_dossier_retrieve_last_piece_justificative_by_type' } do describe '#retrieve_last_piece_justificative_by_type', vcr: { cassette_name: 'models_dossier_retrieve_last_piece_justificative_by_type' } do
let(:types_de_pj_dossier) { dossier.procedure.types_de_piece_justificative } let(:types_de_pj_dossier) { dossier.procedure.types_de_piece_justificative }
@ -113,6 +121,20 @@ describe Dossier do
end end
end end
describe '#retrieve_all_piece_justificative_by_type' do
let(:types_de_pj_dossier) { dossier.procedure.types_de_piece_justificative }
subject { dossier.retrieve_all_piece_justificative_by_type types_de_pj_dossier.first }
before do
create :piece_justificative, :rib, dossier: dossier, type_de_piece_justificative: types_de_pj_dossier.first
end
it 'returns a list of the piece justificative' do
expect(subject).not_to be_empty
end
end
describe '#build_default_champs' do describe '#build_default_champs' do
context 'when dossier is linked to a procedure with type_de_champ_public and private' do context 'when dossier is linked to a procedure with type_de_champ_public and private' do
let(:dossier) { create(:dossier, user: user) } let(:dossier) { create(:dossier, user: user) }
@ -1008,4 +1030,32 @@ describe Dossier do
after { Timecop.return } after { Timecop.return }
end end
describe '#attachments_downloadable?' do
let(:dossier) { create(:dossier, user: user) }
# subject { dossier.attachments_downloadable? }
context "no attachments" do
it {
expect(PiecesJustificativesService).to receive(:liste_pieces_justificatives).and_return([])
expect(dossier.attachments_downloadable?).to be false
}
end
context "with a small attachment" do
it {
expect(PiecesJustificativesService).to receive(:liste_pieces_justificatives).and_return([Champ.new])
expect(PiecesJustificativesService).to receive(:pieces_justificatives_total_size).and_return(4.megabytes)
expect(dossier.attachments_downloadable?).to be true
}
end
context "with a too large attachment" do
it {
expect(PiecesJustificativesService).to receive(:liste_pieces_justificatives).and_return([Champ.new])
expect(PiecesJustificativesService).to receive(:pieces_justificatives_total_size).and_return(100.megabytes)
expect(dossier.attachments_downloadable?).to be false
}
end
end
end end

View file

@ -20,6 +20,9 @@ describe PiecesJustificativesService do
let(:errors) { PiecesJustificativesService.upload!(dossier, user, hash) } let(:errors) { PiecesJustificativesService.upload!(dossier, user, hash) }
let(:tpjs) { [tpj_not_mandatory] } let(:tpjs) { [tpj_not_mandatory] }
let(:attachment_list) { PiecesJustificativesService.liste_pieces_justificatives(dossier) }
let(:poids_total) { PiecesJustificativesService.pieces_justificatives_total_size(dossier) }
describe 'self.upload!' do describe 'self.upload!' do
context 'when no params are given' do context 'when no params are given' do
it { expect(errors).to eq([]) } it { expect(errors).to eq([]) }
@ -81,12 +84,30 @@ describe PiecesJustificativesService do
before :each do before :each do
# we are messing around piece_justificative # we are messing around piece_justificative
# because directly doubling carrierwave params seems complicated # because directly doubling carrierwave params seems complicated
piece_justificative_double = double(type_de_piece_justificative: tpj_mandatory) piece_justificative_double = double(type_de_piece_justificative: tpj_mandatory)
expect(dossier).to receive(:pieces_justificatives).and_return([piece_justificative_double]) expect(dossier).to receive(:pieces_justificatives).and_return([piece_justificative_double])
end end
it { expect(errors).to match([]) } it {
expect(errors).to match([])
}
end
end
describe '#attachment_list' do
context 'when no piece_justificative is present' do
it { expect(attachment_list).to match([]) }
it { expect(poids_total).to be 0 }
end
context 'when there is a piece_justificative' do
let (:pj) { create(:champ, :piece_justificative, :with_piece_justificative_file) }
before do
dossier.champs = [pj]
end
it { expect(attachment_list).not_to be_empty }
it { expect(poids_total).to be pj.piece_justificative_file.byte_size }
end end
end end