From 77d825ae63d3fd63bdb6156a00449267b9df7dac Mon Sep 17 00:00:00 2001 From: Benjamin-Doberset <36986044+Benjamin-Doberset@users.noreply.github.com> Date: Thu, 11 Jul 2019 14:04:13 +0200 Subject: [PATCH 01/11] Update _estimated_delay.html.haml --- app/views/users/dossiers/show/_estimated_delay.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/users/dossiers/show/_estimated_delay.html.haml b/app/views/users/dossiers/show/_estimated_delay.html.haml index 2bea129cf..40654f096 100644 --- a/app/views/users/dossiers/show/_estimated_delay.html.haml +++ b/app/views/users/dossiers/show/_estimated_delay.html.haml @@ -6,4 +6,4 @@ - cache(procedure.id, expires_in: 1.day) do - if procedure.usual_traitement_time && show_time_means %p - Habituellement, les dossiers de cette démarche sont traités dans un délai de #{distance_of_time_in_words(procedure.usual_traitement_time)}. + Habituellement, les dossiers de cette démarche sont traités dans un délai de #{distance_of_time_in_words(procedure.usual_traitement_time)}. Ce délai est le résultat d'un calcul automatique à partir des délais d'instruction précedemment constatés. Le délai réel peut être différent, en fonction du type de démarche : appels à projet avec date de décision fixe par exemple. From 077082c381cce01eb1084a706876fa80e3771521 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Thu, 11 Jul 2019 13:29:19 +0000 Subject: [PATCH 02/11] dossier: improve estimated delay wording --- app/views/users/dossiers/show/_estimated_delay.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/users/dossiers/show/_estimated_delay.html.haml b/app/views/users/dossiers/show/_estimated_delay.html.haml index 40654f096..eabc51bb3 100644 --- a/app/views/users/dossiers/show/_estimated_delay.html.haml +++ b/app/views/users/dossiers/show/_estimated_delay.html.haml @@ -6,4 +6,6 @@ - cache(procedure.id, expires_in: 1.day) do - if procedure.usual_traitement_time && show_time_means %p - Habituellement, les dossiers de cette démarche sont traités dans un délai de #{distance_of_time_in_words(procedure.usual_traitement_time)}. Ce délai est le résultat d'un calcul automatique à partir des délais d'instruction précedemment constatés. Le délai réel peut être différent, en fonction du type de démarche : appels à projet avec date de décision fixe par exemple. + Habituellement, les dossiers de cette démarche sont traités dans un délai de #{distance_of_time_in_words(procedure.usual_traitement_time)}. + %p + Cette estimation est calculée automatiquement à partir des délais d’instruction constatés précédemment. Le délai réel peut être différent, en fonction du type de démarche (par exemple pour un appel à projet avec date de décision fixe). From 8ea8e524310aa37f94d20f01c7220bbf0c5b97a9 Mon Sep 17 00:00:00 2001 From: Benjamin-Doberset <36986044+Benjamin-Doberset@users.noreply.github.com> Date: Thu, 11 Jul 2019 14:16:32 +0200 Subject: [PATCH 03/11] Update _status_overview.html.haml --- .../dossiers/show/_status_overview.html.haml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/views/users/dossiers/show/_status_overview.html.haml b/app/views/users/dossiers/show/_status_overview.html.haml index b5504237f..600ae48ba 100644 --- a/app/views/users/dossiers/show/_status_overview.html.haml +++ b/app/views/users/dossiers/show/_status_overview.html.haml @@ -16,26 +16,23 @@ .status-explanation - if dossier.brouillon? .brouillon - %p Vous pouvez remplir votre dossier tranquillement : il n’est pas encore visible par l’administration. - %p Quand vous aurez terminé, soumettez votre dossier pour qu’il soit examiné. + %p Votre dossier n’est pas encore visible par l’administration. + %p Vous pouvez "soumettre votre dossier" afin de le transmettre à l'administration. Une fois soumis, vous pourrez toujours modifier votre dossier. - elsif dossier.en_construction? .en-construction - %p Un instructeur de l’administration est en train de vérifier que votre dossier est bien complet. Si des modifications sont nécessaires, vous recevrez un message avec les modifications à effectuer. + %p Votre dossier est en construction cela signifie que vous pouvez encore le modifier. Vous pouvez aussi directement échanger avec l'administration via le fil de messagerie. %p - Sinon, + Vous ne pourrez plus modifier, = succeed '.' do - %strong votre dossier passera directement en instruction + %strong votre dossier lorsque l'administration le passera "en instruction". = render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure } - elsif dossier.en_instruction? .en-instruction - %p Votre dossier est complet. Il est en cours d’examen par les instructeurs de l’administration. + %p Votre dossier est en cours d'instruction par l'administration. Vous ne pouvez plus le modifier. %p - Dès que l’administration aura statué sur votre dossier, - %strong - vous recevrez un email - avec le résultat. + = render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure } - elsif dossier.accepte? From d9c01c0af632f06f3963b83ceccadd759861ec51 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Mon, 15 Jul 2019 15:45:04 +0200 Subject: [PATCH 04/11] status_overview: improve wording --- .../dossiers/show/_status_overview.html.haml | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/views/users/dossiers/show/_status_overview.html.haml b/app/views/users/dossiers/show/_status_overview.html.haml index 600ae48ba..be7eaeb02 100644 --- a/app/views/users/dossiers/show/_status_overview.html.haml +++ b/app/views/users/dossiers/show/_status_overview.html.haml @@ -1,5 +1,3 @@ - - .status-overview - if !dossier.termine? %ul.status-timeline @@ -17,24 +15,36 @@ - if dossier.brouillon? .brouillon %p Votre dossier n’est pas encore visible par l’administration. - %p Vous pouvez "soumettre votre dossier" afin de le transmettre à l'administration. Une fois soumis, vous pourrez toujours modifier votre dossier. + %p Vous pouvez « Soumettre votre dossier » afin de le transmettre à l’administration. Une fois soumis, vous pourrez toujours modifier votre dossier. - elsif dossier.en_construction? .en-construction - %p Votre dossier est en construction cela signifie que vous pouvez encore le modifier. Vous pouvez aussi directement échanger avec l'administration via le fil de messagerie. %p - Vous ne pourrez plus modifier, + Votre dossier est en construction. Cela signifie que = succeed '.' do - %strong votre dossier lorsque l'administration le passera "en instruction". + %strong vous pouvez encore le modifier + %p Vous ne pourrez plus modifier votre dossier lorsque l’administration le passera « en instruction ». + = render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure } + %p + %strong Vous avez une question ? + Utilisez la messagerie pour + = succeed '.' do + = link_to 'contacter l’administration directement', messagerie_dossier_url(dossier) + - elsif dossier.en_instruction? .en-instruction - %p Votre dossier est en cours d'instruction par l'administration. Vous ne pouvez plus le modifier. - %p + %p Votre dossier est en cours d’instruction par l’administration. Vous ne pouvez plus le modifier. = render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure } + %p + %strong Vous avez une question ? + Utilisez la messagerie pour + = succeed '.' do + = link_to 'contacter l’administration directement', messagerie_dossier_url(dossier) + - elsif dossier.accepte? .accepte %p.decision From 862b08427bb06f161da4e7404c934bff813e3249 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Mon, 15 Jul 2019 15:55:25 +0200 Subject: [PATCH 05/11] status_overview: remove extraneous line --- app/views/users/dossiers/show/_status_overview.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/users/dossiers/show/_status_overview.html.haml b/app/views/users/dossiers/show/_status_overview.html.haml index be7eaeb02..796c80e8c 100644 --- a/app/views/users/dossiers/show/_status_overview.html.haml +++ b/app/views/users/dossiers/show/_status_overview.html.haml @@ -23,7 +23,7 @@ Votre dossier est en construction. Cela signifie que = succeed '.' do %strong vous pouvez encore le modifier - %p Vous ne pourrez plus modifier votre dossier lorsque l’administration le passera « en instruction ». + Vous ne pourrez plus modifier votre dossier lorsque l’administration le passera « en instruction ». = render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure } From 85027a8f1e3e184b1f933254c74135200da74cd5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2019 14:24:59 +0000 Subject: [PATCH 06/11] build(deps): bump lodash from 4.17.11 to 4.17.14 Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1b73ba830..c3b8f9511 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5114,9 +5114,9 @@ lodash.uniq@^4.5.0: integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@~4.17.10: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + version "4.17.14" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" + integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== loglevel@^1.6.3: version "1.6.3" From 36eca3d059e4524ca07763d5ac39da81a8599c0f Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Mon, 15 Jul 2019 16:10:34 +0200 Subject: [PATCH 07/11] dossier: rename the "Submit" action --- app/views/invites/_form.html.haml | 2 +- app/views/shared/dossiers/_edit.html.haml | 4 ++-- app/views/users/dossiers/show/_status_overview.html.haml | 2 +- spec/features/users/brouillon_spec.rb | 4 ++-- spec/features/users/invite_spec.rb | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/invites/_form.html.haml b/app/views/invites/_form.html.haml index 4268d522d..a35dbe5f9 100644 --- a/app/views/invites/_form.html.haml +++ b/app/views/invites/_form.html.haml @@ -6,7 +6,7 @@ %li= invite.email %p Ces personnes peuvent modifier ce dossier. - if dossier.brouillon? - %p Une fois le dossier complet, vous devez le soumettre vous-même. + %p Une fois le dossier complet, vous devez le déposer vous-même. - else %p Vous pouvez inviter quelqu’un à remplir ce dossier avec vous. diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml index 97f0b494d..ed34765e0 100644 --- a/app/views/shared/dossiers/_edit.html.haml +++ b/app/views/shared/dossiers/_edit.html.haml @@ -80,7 +80,7 @@ data: { 'disable-with': "Envoi en cours…" } - if dossier.can_transition_to_en_construction? - = f.button 'Soumettre le dossier', + = f.button 'Déposer le dossier', class: 'button send primary', disabled: !current_user.owns?(dossier), data: { 'disable-with': "Envoi en cours…" } @@ -92,6 +92,6 @@ - if dossier.brouillon? && !current_user.owns?(dossier) .send-notice.invite-cannot-submit - En tant qu’invité, vous pouvez remplir ce formulaire – mais le titulaire du dossier doit le soumettre lui-même. + En tant qu’invité, vous pouvez remplir ce formulaire – mais le titulaire du dossier doit le déposer lui-même. = render partial: "shared/dossiers/submit_is_over", locals: { dossier: dossier } diff --git a/app/views/users/dossiers/show/_status_overview.html.haml b/app/views/users/dossiers/show/_status_overview.html.haml index 796c80e8c..945b18e63 100644 --- a/app/views/users/dossiers/show/_status_overview.html.haml +++ b/app/views/users/dossiers/show/_status_overview.html.haml @@ -15,7 +15,7 @@ - if dossier.brouillon? .brouillon %p Votre dossier n’est pas encore visible par l’administration. - %p Vous pouvez « Soumettre votre dossier » afin de le transmettre à l’administration. Une fois soumis, vous pourrez toujours modifier votre dossier. + %p Vous pouvez déposer votre dossier afin de le transmettre à l’administration. Une fois soumis, vous pourrez toujours modifier votre dossier. - elsif dossier.en_construction? .en-construction diff --git a/spec/features/users/brouillon_spec.rb b/spec/features/users/brouillon_spec.rb index a0eef004f..6788c9b41 100644 --- a/spec/features/users/brouillon_spec.rb +++ b/spec/features/users/brouillon_spec.rb @@ -137,14 +137,14 @@ feature 'The user' do expect(page).to have_current_path(brouillon_dossier_path(user_dossier)) # Check an incomplete dossier cannot be submitted when mandatory fields are missing - click_on 'Soumettre le dossier' + click_on 'Déposer le dossier' expect(user_dossier.reload.brouillon?).to be(true) expect(page).to have_current_path(brouillon_dossier_path(user_dossier)) # Check a dossier can be submitted when all mandatory fields are filled fill_in('texte obligatoire', with: 'super texte') - click_on 'Soumettre le dossier' + click_on 'Déposer le dossier' expect(user_dossier.reload.en_construction?).to be(true) expect(champ_value_for('texte obligatoire')).to eq('super texte') expect(page).to have_current_path(merci_dossier_path(user_dossier)) diff --git a/spec/features/users/invite_spec.rb b/spec/features/users/invite_spec.rb index c8706dc7d..e1f8c58b2 100644 --- a/spec/features/users/invite_spec.rb +++ b/spec/features/users/invite_spec.rb @@ -63,7 +63,7 @@ feature 'Invitations' do navigate_to_invited_dossier(invite) expect(page).to have_current_path(brouillon_dossier_path(dossier)) - expect(page).to have_button('Soumettre le dossier', disabled: true) + expect(page).to have_button('Déposer le dossier', disabled: true) expect(page).to have_selector('.invite-cannot-submit') end end From 76335511c8523142a2f697fd1bedccb9cb4ced08 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Mon, 15 Jul 2019 14:35:24 +0000 Subject: [PATCH 08/11] omniauth: protect against CSRF See https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284 --- Gemfile | 1 + Gemfile.lock | 4 ++++ app/views/administrations/sessions/new.html.haml | 2 +- config/initializers/omniauth.rb | 4 ++++ 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 config/initializers/omniauth.rb diff --git a/Gemfile b/Gemfile index be3d60fb6..d46ee525c 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,7 @@ gem 'lograge' gem 'logstash-event' gem 'mailjet' gem 'omniauth-github' +gem 'omniauth-rails_csrf_protection', '~> 0.1' gem 'openid_connect' gem 'openstack' gem 'pg' diff --git a/Gemfile.lock b/Gemfile.lock index 4e80820c2..76d7ca1fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -386,6 +386,9 @@ GEM omniauth-oauth2 (1.6.0) oauth2 (~> 1.1) omniauth (~> 1.9) + omniauth-rails_csrf_protection (0.1.2) + actionpack (>= 4.2) + omniauth (>= 1.3.1) open4 (1.3.4) openid_connect (1.1.6) activemodel @@ -731,6 +734,7 @@ DEPENDENCIES mailjet mina! omniauth-github + omniauth-rails_csrf_protection (~> 0.1) openid_connect openstack pg diff --git a/app/views/administrations/sessions/new.html.haml b/app/views/administrations/sessions/new.html.haml index 8ed5fb4d9..3176e8927 100644 --- a/app/views/administrations/sessions/new.html.haml +++ b/app/views/administrations/sessions/new.html.haml @@ -1,6 +1,6 @@ .super-admin.flex.justify-center %div %h2 Espace Admin - = link_to administration_github_omniauth_authorize_path, class: "button large" do + = link_to administration_github_omniauth_authorize_path, method: :post, class: "button large" do %span.icon.lock Connexion avec GitHub diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb new file mode 100644 index 000000000..1c1946a5c --- /dev/null +++ b/config/initializers/omniauth.rb @@ -0,0 +1,4 @@ +# OmniAuth GET requests may be vulnerable to CSRF. +# Ensure that OmniAuth only uses POST requests. +# See https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284 +OmniAuth.config.allowed_request_methods = [:post] From 25f81f1d3c5e0eea68d00f45a399215a1493e5c3 Mon Sep 17 00:00:00 2001 From: clemkeirua Date: Mon, 1 Jul 2019 15:55:37 +0200 Subject: [PATCH 09/11] download a dossier as zip with all attachments --- .gitignore | 1 + Gemfile | 1 + Gemfile.lock | 7 +++ .../gestionnaires/dossiers_controller.rb | 11 ++++ app/lib/active_storage/downloadable_file.rb | 29 +++++++++++ app/models/dossier.rb | 4 ++ app/services/pieces_justificatives_service.rb | 11 ++++ .../gestionnaires/dossiers/_header.html.haml | 8 +++ config/features.rb | 2 + config/routes.rb | 1 + .../gestionnaires/dossiers_controller_spec.rb | 20 ++++++++ .../active_storage/downloadable_file_spec.rb | 26 ++++++++++ spec/models/dossier_spec.rb | 50 +++++++++++++++++++ .../pieces_justificatives_service_spec.rb | 25 +++++++++- 14 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 app/lib/active_storage/downloadable_file.rb create mode 100644 spec/lib/active_storage/downloadable_file_spec.rb diff --git a/.gitignore b/.gitignore index 9a9dd9ca4..4af928a07 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ yarn-debug.log* /public/packs /public/packs-test /node_modules +vendor/* /yarn-error.log yarn-debug.log* .yarn-integrity diff --git a/Gemfile b/Gemfile index d46ee525c..7ffa37c1c 100644 --- a/Gemfile +++ b/Gemfile @@ -67,6 +67,7 @@ gem 'turbolinks' # Turbolinks makes following links in your web application fast gem 'typhoeus' gem 'warden' gem 'webpacker' +gem 'zipline' gem 'zxcvbn-ruby', require: 'zxcvbn' group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 76d7ca1fc..b925daa7a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -170,6 +170,7 @@ GEM crass (1.0.4) css_parser (1.6.0) addressable + curb (0.9.10) daemons (1.3.1) database_cleaner (1.7.0) datetime_picker_rails (0.0.7) @@ -674,6 +675,11 @@ GEM nokogiri (~> 1.8) xray-rails (0.3.1) 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) PLATFORMS @@ -779,6 +785,7 @@ DEPENDENCIES webmock webpacker xray-rails + zipline zxcvbn-ruby BUNDLED WITH diff --git a/app/controllers/gestionnaires/dossiers_controller.rb b/app/controllers/gestionnaires/dossiers_controller.rb index d58fed8fb..adbc9e237 100644 --- a/app/controllers/gestionnaires/dossiers_controller.rb +++ b/app/controllers/gestionnaires/dossiers_controller.rb @@ -5,6 +5,9 @@ module Gestionnaires include CreateAvisConcern include DossierHelper + include ActionController::Streaming + include Zipline + after_action :mark_demande_as_read, only: :show after_action :mark_messagerie_as_read, only: [:messagerie, :create_commentaire] after_action :mark_avis_as_read, only: [:avis, :create_avis] @@ -172,6 +175,14 @@ module Gestionnaires render layout: "print" 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 def dossier diff --git a/app/lib/active_storage/downloadable_file.rb b/app/lib/active_storage/downloadable_file.rb new file mode 100644 index 000000000..3ef733e8d --- /dev/null +++ b/app/lib/active_storage/downloadable_file.rb @@ -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 diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 81c426824..28682f056 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -466,6 +466,10 @@ class Dossier < ApplicationRecord end end + def attachments_downloadable? + !PiecesJustificativesService.liste_pieces_justificatives(self).empty? && PiecesJustificativesService.pieces_justificatives_total_size(self) < 50.megabytes + end + private def log_dossier_operation(author, operation, subject = nil) diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index 411e11124..da7d53325 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -70,6 +70,17 @@ class PiecesJustificativesService 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) tdcs = procedure.types_de_champ.select { |type_champ| type_champ.old_pj.present? } tdcs.map.with_index do |type_champ, order_place| diff --git a/app/views/gestionnaires/dossiers/_header.html.haml b/app/views/gestionnaires/dossiers/_header.html.haml index 186d4c4d6..61afc1361 100644 --- a/app/views/gestionnaires/dossiers/_header.html.haml +++ b/app/views/gestionnaires/dossiers/_header.html.haml @@ -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" %li = 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) } %span.state-button diff --git a/config/features.rb b/config/features.rb index 1f7816d14..d4bb54e21 100644 --- a/config/features.rb +++ b/config/features.rb @@ -18,6 +18,8 @@ Flipflop.configure do feature :procedure_export_v2_enabled feature :operation_log_serialize_subject + feature :download_as_zip_enabled, + default: false group :development do feature :mini_profiler_enabled, diff --git a/config/routes.rb b/config/routes.rb index bb9785886..6abccf8e2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -335,6 +335,7 @@ Rails.application.routes.draw do post 'send-to-instructeurs' => 'dossiers#send_to_instructeurs' post 'avis' => 'dossiers#create_avis' get 'print' => 'dossiers#print' + get 'telecharger_pjs' => 'dossiers#telecharger_pjs' end end end diff --git a/spec/controllers/gestionnaires/dossiers_controller_spec.rb b/spec/controllers/gestionnaires/dossiers_controller_spec.rb index cbf5b775a..0f327221a 100644 --- a/spec/controllers/gestionnaires/dossiers_controller_spec.rb +++ b/spec/controllers/gestionnaires/dossiers_controller_spec.rb @@ -524,4 +524,24 @@ describe Gestionnaires::DossiersController, type: :controller do it { expect(champ_repetition.champs.first.value).to eq('text') } it { expect(response).to redirect_to(annotations_privees_gestionnaire_dossier_path(dossier.procedure, dossier)) } 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 diff --git a/spec/lib/active_storage/downloadable_file_spec.rb b/spec/lib/active_storage/downloadable_file_spec.rb new file mode 100644 index 000000000..b802a90c9 --- /dev/null +++ b/spec/lib/active_storage/downloadable_file_spec.rb @@ -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 diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index d1ac82b16..67ad2a39a 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -99,6 +99,14 @@ describe Dossier do 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 let(:types_de_pj_dossier) { dossier.procedure.types_de_piece_justificative } @@ -113,6 +121,20 @@ describe Dossier do 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 context 'when dossier is linked to a procedure with type_de_champ_public and private' do let(:dossier) { create(:dossier, user: user) } @@ -1008,4 +1030,32 @@ describe Dossier do after { Timecop.return } 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 diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index 3d8eb6d09..6988e3007 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -20,6 +20,9 @@ describe PiecesJustificativesService do let(:errors) { PiecesJustificativesService.upload!(dossier, user, hash) } 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 context 'when no params are given' do it { expect(errors).to eq([]) } @@ -81,12 +84,30 @@ describe PiecesJustificativesService do before :each do # we are messing around piece_justificative # because directly doubling carrierwave params seems complicated - piece_justificative_double = double(type_de_piece_justificative: tpj_mandatory) expect(dossier).to receive(:pieces_justificatives).and_return([piece_justificative_double]) 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 From a8354bd103485fd71f09c471004c788776c03864 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Mon, 15 Jul 2019 14:00:49 +0000 Subject: [PATCH 10/11] dossiers: unify deletion of dossiers between manager and user The code paths for deleting a dossier were different, depending on whether the dossier was deleted by the user, or from the Manager. This commit unifies the two code paths into one. This has the effect of: - An operation log is now recorded when an user deletes its own dossier; - Gestionnaires are now notified even when the dossier is deleted from the Manager; - The `support:delete_user_account` task now requires the email address of the author. --- .../manager/dossiers_controller.rb | 4 +-- app/controllers/users/dossiers_controller.rb | 2 +- app/models/dossier.rb | 12 +++------ lib/tasks/support.rake | 19 +++++++++----- spec/models/dossier_spec.rb | 25 ++++++------------- 5 files changed, 26 insertions(+), 36 deletions(-) diff --git a/app/controllers/manager/dossiers_controller.rb b/app/controllers/manager/dossiers_controller.rb index 31867443f..35b7c785c 100644 --- a/app/controllers/manager/dossiers_controller.rb +++ b/app/controllers/manager/dossiers_controller.rb @@ -22,10 +22,10 @@ module Manager def hide dossier = Dossier.find(params[:id]) - dossier.hide!(current_administration) + dossier.delete_and_keep_track(current_administration) logger.info("Le dossier #{dossier.id} est supprimé par #{current_administration.email}") - flash[:notice] = "Le dossier #{dossier.id} est supprimé" + flash[:notice] = "Le dossier #{dossier.id} a été supprimé." redirect_to manager_dossier_path(dossier) end diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 3e13bbc46..4479e5a18 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -193,7 +193,7 @@ module Users dossier = current_user.dossiers.includes(:user, procedure: :administrateurs).find(params[:id]) if dossier.can_be_deleted_by_user? - dossier.delete_and_keep_track + dossier.delete_and_keep_track(current_user) flash.notice = 'Votre dossier a bien été supprimé.' redirect_to dossiers_path else diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 28682f056..a0251397b 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -317,7 +317,7 @@ class Dossier < ApplicationRecord end end - def delete_and_keep_track + def delete_and_keep_track(author) deleted_dossier = DeletedDossier.create_from_dossier(self) update(hidden_at: deleted_dossier.deleted_at) @@ -328,6 +328,8 @@ class Dossier < ApplicationRecord end end DossierMailer.notify_deletion_to_user(deleted_dossier, user.email).deliver_later + + log_dossier_operation(author, :supprimer, self) end def after_passer_en_instruction(gestionnaire) @@ -409,14 +411,6 @@ class Dossier < ApplicationRecord log_dossier_operation(gestionnaire, :classer_sans_suite, self) end - def hide!(administration) - update(hidden_at: Time.zone.now) - - deleted_dossier = DeletedDossier.create_from_dossier(self) - DossierMailer.notify_deletion_to_user(deleted_dossier, user.email).deliver_later - log_dossier_operation(administration, :supprimer, self) - end - def check_mandatory_champs (champs + champs.select(&:repetition?).flat_map(&:champs)) .select(&:mandatory_and_blank?) diff --git a/lib/tasks/support.rake b/lib/tasks/support.rake index 9cdb35adb..7d96ac8e2 100644 --- a/lib/tasks/support.rake +++ b/lib/tasks/support.rake @@ -2,19 +2,26 @@ require Rails.root.join("lib", "tasks", "task_helper") namespace :support do desc <<~EOD - Delete the user account for a given USER_EMAIL. + Delete the user account for a given USER_EMAIL on the behalf of ADMIN_EMAIL. Only works if the user has no dossier where the instruction has started. EOD task delete_user_account: :environment do user_email = ENV['USER_EMAIL'] - if user_email.nil? - fail "Must specify a USER_EMAIL" - end - user = User.find_by(email: user_email) + fail "Must specify a USER_EMAIL" if user_email.nil? + + administration_email = ENV['ADMIN_EMAIL'] + fail "Must specify the ADMIN_EMAIL of the operator performing the deletion (yourself)" if administration_email.nil? + + user = User.find_by!(email: user_email) + administration = Administration.find_by!(email: administration_email) + if user.dossiers.state_instruction_commencee.any? fail "Cannot delete this user because instruction has started for some dossiers" end - user.dossiers.each(&:delete_and_keep_track) + + user.dossiers.each do |dossier| + dossier.delete_and_keep_track(administration) + end user.destroy end diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index 67ad2a39a..312e5d9ab 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -632,13 +632,14 @@ describe Dossier do describe "#delete_and_keep_track" do let(:dossier) { create(:dossier) } let(:deleted_dossier) { DeletedDossier.find_by!(dossier_id: dossier.id) } + let(:last_operation) { dossier.dossier_operation_logs.last } before do allow(DossierMailer).to receive(:notify_deletion_to_user).and_return(double(deliver_later: nil)) allow(DossierMailer).to receive(:notify_deletion_to_administration).and_return(double(deliver_later: nil)) end - subject! { dossier.delete_and_keep_track } + subject! { dossier.delete_and_keep_track(dossier.user) } it 'hides the dossier' do expect(dossier.hidden_at).to be_present @@ -655,6 +656,11 @@ describe Dossier do expect(DossierMailer).to have_received(:notify_deletion_to_user).with(deleted_dossier, dossier.user.email) end + it 'records the operation in the log' do + expect(last_operation.operation).to eq("supprimer") + expect(last_operation.automatic_operation?).to be_falsey + end + context 'where gestionnaires are following the dossier' do let(:dossier) { create(:dossier, :en_construction, :followed) } let!(:non_following_gestionnaire) do @@ -990,23 +996,6 @@ describe Dossier do end end - describe '#hide!' do - let(:dossier) { create(:dossier) } - let(:administration) { create(:administration) } - let(:last_operation) { dossier.dossier_operation_logs.last } - - before do - Timecop.freeze - dossier.hide!(administration) - end - - after { Timecop.return } - - it { expect(dossier.hidden_at).to eq(Time.zone.now) } - it { expect(last_operation.operation).to eq('supprimer') } - it { expect(last_operation.automatic_operation?).to be_falsey } - end - describe '#repasser_en_instruction!' do let(:dossier) { create(:dossier, :refuse, :with_attestation) } let!(:gestionnaire) { create(:gestionnaire) } From 80efe27ff255ad46041327e090cac8f87211e6bd Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Tue, 16 Jul 2019 17:51:29 +0200 Subject: [PATCH 11/11] pj migration: retry when changing the content type fails --- ...erwave_active_storage_migration_service.rb | 12 ++++++++- ...e_active_storage_migration_service_spec.rb | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/services/carrierwave_active_storage_migration_service.rb b/app/services/carrierwave_active_storage_migration_service.rb index 1183cf8f9..d5a4c59a5 100644 --- a/app/services/carrierwave_active_storage_migration_service.rb +++ b/app/services/carrierwave_active_storage_migration_service.rb @@ -154,10 +154,20 @@ class CarrierwaveActiveStorageMigrationService attachment end - def fix_content_type(blob) + def fix_content_type(blob, retry_delay: 5) + retries ||= 0 # In OpenStack, ActiveStorage cannot inject the MIME type on the fly during direct # download. Instead, the MIME type needs to be stored statically on the file object # in OpenStack. This is what this call does. blob.service.change_content_type(blob.key, blob.content_type) + rescue + # When we quickly create a new attachment, and then change its content type, + # the Object Storage may not be synchronized yet. It this cas, it will return a + # "409 Conflict" error. + # + # Wait for a while, then try again twice (before giving up). + sleep(retry_delay) + retry if (retries += 1) < 3 + raise end end diff --git a/spec/services/carrierwave_active_storage_migration_service_spec.rb b/spec/services/carrierwave_active_storage_migration_service_spec.rb index fb70b0d6f..abf7ff1ca 100644 --- a/spec/services/carrierwave_active_storage_migration_service_spec.rb +++ b/spec/services/carrierwave_active_storage_migration_service_spec.rb @@ -69,4 +69,31 @@ describe CarrierwaveActiveStorageMigrationService do end end end + + describe '.fix_content_type' do + let(:pj) { create(:piece_justificative, :rib, updated_at: Time.zone.local(2019, 01, 01, 12, 00)) } + let(:blob) { service.make_empty_blob(pj.content, pj.updated_at.iso8601, filename: pj.original_filename) } + + context 'when the request is ok' do + it 'succeeds' do + expect(blob.service).to receive(:change_content_type).and_return(true) + expect { service.fix_content_type(blob) }.not_to raise_error + end + end + + context 'when the request fails initially' do + it 'retries the request' do + expect(blob.service).to receive(:change_content_type).and_raise(StandardError).ordered + expect(blob.service).to receive(:change_content_type).and_return(true).ordered + expect { service.fix_content_type(blob, retry_delay: 0.01) }.not_to raise_error + end + end + + context 'when the request fails too many times' do + it 'gives up' do + expect(blob.service).to receive(:change_content_type).and_raise(StandardError).thrice + expect { service.fix_content_type(blob, retry_delay: 0.01) }.to raise_error(StandardError) + end + end + end end