Merge pull request #3900 from betagouv/dev

2019-05-28-01
This commit is contained in:
Paul Chavard 2019-05-28 12:09:19 +02:00 committed by GitHub
commit 6fe326ef82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 388 additions and 107 deletions

View file

@ -69,6 +69,7 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian
Administrateurs::ActivateBeforeExpirationJob.set(cron: "0 8 * * *").perform_later Administrateurs::ActivateBeforeExpirationJob.set(cron: "0 8 * * *").perform_later
WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later
GestionnaireEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later GestionnaireEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later
PurgeUnattachedBlobsJob.set("0 0 * * *").perform_later
### Voir les emails envoyés en local ### Voir les emails envoyés en local

View file

@ -231,7 +231,9 @@ class ApplicationController < ActionController::Base
DS_SIGN_IN_COUNT: current_administrateur&.sign_in_count, DS_SIGN_IN_COUNT: current_administrateur&.sign_in_count,
DS_CREATED_AT: current_administrateur&.created_at, DS_CREATED_AT: current_administrateur&.created_at,
DS_ACTIVE: current_administrateur&.active, DS_ACTIVE: current_administrateur&.active,
DS_ID: current_administrateur&.id DS_ID: current_administrateur&.id,
DS_GESTIONNAIRE_ID: current_gestionnaire&.id,
DS_ROLES: logged_user_roles
} }
} }
} }

View file

@ -0,0 +1,9 @@
class AttachmentsController < ApplicationController
before_action :authenticate_logged_user!
include ActiveStorage::SetBlob
def show
@attachment = @blob.attachments.find(params[:id])
@user_can_upload = params[:user_can_upload]
end
end

View file

@ -143,7 +143,7 @@ module Gestionnaires
def purge_champ_piece_justificative def purge_champ_piece_justificative
@champ = dossier.champs_private.find(params[:champ_id]) @champ = dossier.champs_private.find(params[:champ_id])
@champ.piece_justificative_file.purge @champ.piece_justificative_file.purge_later
flash.notice = 'La pièce jointe a bien été supprimée.' flash.notice = 'La pièce jointe a bien été supprimée.'
end end

View file

@ -239,7 +239,7 @@ module Users
def purge_champ_piece_justificative def purge_champ_piece_justificative
@champ = dossier.champs.find(params[:champ_id]) @champ = dossier.champs.find(params[:champ_id])
@champ.piece_justificative_file.purge @champ.piece_justificative_file.purge_later
flash.notice = 'La pièce jointe a bien été supprimée.' flash.notice = 'La pièce jointe a bien été supprimée.'
end end

View file

@ -9,6 +9,7 @@ import ReactUJS from '../shared/react-ujs';
import reactComponents from '../shared/react-components'; import reactComponents from '../shared/react-components';
import '../shared/activestorage/ujs'; import '../shared/activestorage/ujs';
import '../shared/activestorage/attachment-checker';
import '../shared/rails-ujs-fix'; import '../shared/rails-ujs-fix';
import '../shared/safari-11-file-xhr-workaround'; import '../shared/safari-11-file-xhr-workaround';
import '../shared/autocomplete'; import '../shared/autocomplete';

View file

@ -0,0 +1,77 @@
import { ajax, delegate } from '@utils';
addEventListener('turbolinks:load', () => {
checker.deactivate();
const attachments = document.querySelectorAll('[data-attachment-check-url]');
for (let attachment of attachments) {
checker.add(attachment.dataset.attachmentCheckUrl);
}
});
addEventListener('attachment:update', ({ detail: { url } }) => {
checker.add(url);
});
delegate('click', '[data-attachment-refresh]', event => {
event.preventDefault();
checker.check();
});
class AttachmentChecker {
urls = new Set();
timeout;
checks = 0;
constructor(settings = {}) {
this.interval = settings.interval || 5000;
this.maxChecks = settings.maxChecks || 5;
}
get isEnabled() {
return this.checks <= this.maxChecks;
}
get isActive() {
return this.timeout !== undefined;
}
add(url) {
if (this.isEnabled) {
if (!this.isActive) {
this.activate();
}
this.urls.add(url);
}
}
check() {
let urls = this.urls;
this.reset();
for (let url of urls) {
ajax({ url, type: 'get' });
}
}
activate() {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.checks++;
this.check();
}, this.interval);
}
deactivate() {
this.checks = 0;
this.reset();
}
reset() {
clearTimeout(this.timeout);
this.urls = new Set();
this.timeout = undefined;
}
}
const checker = new AttachmentChecker();

View file

@ -3,7 +3,7 @@ import $ from 'jquery';
import debounce from 'debounce'; import debounce from 'debounce';
export { debounce }; export { debounce };
export const { fire } = Rails; export const { fire, ajax } = Rails;
export function show({ classList }) { export function show({ classList }) {
classList.remove('hidden'); classList.remove('hidden');

View file

@ -0,0 +1,9 @@
class PurgeUnattachedBlobsJob < ApplicationJob
queue_as :cron
def perform(*args)
ActiveStorage::Blob.unattached
.where('active_storage_blobs.created_at < ?', 24.hours.ago)
.find_each(&:purge_later)
end
end

View file

@ -40,7 +40,7 @@ class Champs::PieceJustificativeChamp < Champ
end end
if errors.present? if errors.present?
piece_justificative_file.purge piece_justificative_file.purge_later
end end
errors errors

View file

@ -1,3 +1,7 @@
# TODO: once we're using Rails 6, use the hooks on attachments creation
# (rather than on blob creation).
# This will help to avoid cloberring metadata accidentally (as metadata
# are more stable on attachment creation than on blob creation).
module BlobVirusScanner module BlobVirusScanner
extend ActiveSupport::Concern extend ActiveSupport::Concern

View file

@ -75,14 +75,15 @@ class CarrierwaveActiveStorageMigrationService
# but when the first attachment that references this blob is created. # but when the first attachment that references this blob is created.
def make_blob(uploader, created_at, filename: nil, identify: false) def make_blob(uploader, created_at, filename: nil, identify: false)
content_type = uploader.content_type content_type = uploader.content_type
identified = content_type.present? && !identify
ActiveStorage::Blob.create( ActiveStorage::Blob.create(
filename: filename || uploader.filename, filename: filename || uploader.filename,
content_type: uploader.content_type, content_type: uploader.content_type,
identified: content_type.present? && !identify,
byte_size: uploader.size, byte_size: uploader.size,
checksum: checksum(uploader), checksum: checksum(uploader),
created_at: created_at created_at: created_at,
metadata: { identified: identified, virus_scan_result: ActiveStorage::VirusScanner::SAFE }
) )
end end

View file

@ -38,7 +38,8 @@ class DossierSearchService
end end
def self.to_tsquery(search_terms) def self.to_tsquery(search_terms)
search_terms.strip (search_terms || "")
.strip
.gsub(/['?\\:&|!<>\(\)]/, "") # drop disallowed characters .gsub(/['?\\:&|!<>\(\)]/, "") # drop disallowed characters
.split(/\s+/) # split words .split(/\s+/) # split words
.map { |x| "#{x}:*" } # enable prefix matching .map { |x| "#{x}:*" } # enable prefix matching

View file

@ -9,9 +9,18 @@ class PieceJustificativeToChampPieceJointeMigrationService
storage_service.ensure_openstack_copy_possible!(PieceJustificativeUploader) storage_service.ensure_openstack_copy_possible!(PieceJustificativeUploader)
end end
def convert_procedure_pjs_to_champ_pjs(procedure) def procedures_with_pjs_in_range(ids_range)
procedures_with_pj = Procedure.unscope(where: :hidden_at).joins(:types_de_piece_justificative).distinct
procedures_with_pj.where(id: ids_range)
end
def number_of_champs_to_migrate(procedure)
(procedure.types_de_piece_justificative.count + 1) * procedure.dossiers.unscope(where: :hidden_at).count
end
def convert_procedure_pjs_to_champ_pjs(procedure, &progress)
types_de_champ_pj = PiecesJustificativesService.types_pj_as_types_de_champ(procedure) types_de_champ_pj = PiecesJustificativesService.types_pj_as_types_de_champ(procedure)
populate_champs_pjs!(procedure, types_de_champ_pj) populate_champs_pjs!(procedure, types_de_champ_pj, &progress)
# Only destroy the old types PJ once everything has been safely migrated to # Only destroy the old types PJ once everything has been safely migrated to
# champs PJs. Destroying the types PJ will cascade and destroy the PJs, # champs PJs. Destroying the types PJ will cascade and destroy the PJs,
@ -24,7 +33,7 @@ class PieceJustificativeToChampPieceJointeMigrationService
@storage_service ||= CarrierwaveActiveStorageMigrationService.new @storage_service ||= CarrierwaveActiveStorageMigrationService.new
end end
def populate_champs_pjs!(procedure, types_de_champ_pj) def populate_champs_pjs!(procedure, types_de_champ_pj, &progress)
procedure.types_de_champ += types_de_champ_pj procedure.types_de_champ += types_de_champ_pj
# Unscope to make sure all dossiers are migrated, even the soft-deleted ones # Unscope to make sure all dossiers are migrated, even the soft-deleted ones
@ -49,6 +58,8 @@ class PieceJustificativeToChampPieceJointeMigrationService
created_at: dossier.created_at created_at: dossier.created_at
) )
end end
yield if block_given?
end end
end end
rescue rescue

View file

@ -67,7 +67,24 @@
%p %p
Toute personne ayant la connaissance de ce lien pourra ainsi remplir des dossiers de test sur votre démarche. Toute personne ayant la connaissance de ce lien pourra ainsi remplir des dossiers de test sur votre démarche.
%br %br
%h4 Protection des Données personnelles
%p
À ce moment du processus de création, vous devez informer votre Délégué à la Protection des Données personnelles (DPD).
(
%a{ href:'https://www.cnil.fr/fr/protection-des-donnees-les-bons-reflexes', target:'_blank' }
https://www.cnil.fr/fr/protection-des-donnees-les-bons-reflexes
)
Si votre démarche propose de collecter des données personnelles, vous devez informer votre DPD. Chaque organisme en a un.
%p
Ce dernier pourra vous aider dans la finalisation de votre démarche, et vous inviter à vous interroger sur les données collectées, et sur la pertinence de ses dernières.
N'oubliez pas : toutes les démarches qui contiennent des données personnelles doivent être consignées dans un registre des traitements :
%a{ href:'https://www.cnil.fr/fr/RGDP-le-registre-des-activites-de-traitement', target:'_blank' }
https://www.cnil.fr/fr/RGDP-le-registre-des-activites-de-traitement
%p
Comment faire :
vous pouvez soit lui communiquer par email le lien vers la démarche en test, ou bien le nommer « administrateur ». Dans tous les cas, ne publiez votre démarche quaprès avoir eu son avis.
%br
%h4 Ce que vous pouvez faire lorsque vous êtes en test %h4 Ce que vous pouvez faire lorsque vous êtes en test
%p %p
Profitez de la phase de test pour tester la saisie de dossiers, ainsi que toutes les fonctionnalités associées (instruction, emails automatiques, attestations, etc.). Profitez de la phase de test pour tester la saisie de dossiers, ainsi que toutes les fonctionnalités associées (instruction, emails automatiques, attestations, etc.).

View file

@ -0,0 +1,8 @@
<%= render_to_element(".pj-link[data-attachment-id=\"#{@attachment.id}\"]",
partial: 'shared/attachment/show',
outer: true,
locals: { attachment: @attachment, user_can_upload: @user_can_upload }) %>
<% if @attachment.virus_scanner.pending? %>
<%= fire_event('attachment:update', { url: attachment_url(@attachment.id, { signed_id: @attachment.blob.signed_id, user_can_upload: @user_can_upload }) }.to_json ) %>
<% end %>

View file

@ -13,7 +13,7 @@
= form_for @avis, url: gestionnaire_avis_path(@avis), html: { class: 'form' } do |f| = form_for @avis, url: gestionnaire_avis_path(@avis), html: { class: 'form' } do |f|
= f.text_area :answer, rows: 3, placeholder: 'Votre avis', required: true = f.text_area :answer, rows: 3, placeholder: 'Votre avis', required: true
= render partial: "shared/piece_jointe/pj_upload_field", locals: { pj: @avis.piece_justificative_file, object: @avis, form: f } = render partial: "shared/attachment/update", locals: { pj: @avis.piece_justificative_file, object: @avis, form: f }
.flex.justify-between.align-baseline .flex.justify-between.align-baseline
%p.confidentiel.flex %p.confidentiel.flex

View file

@ -60,7 +60,7 @@
- if dossier.motivation.present? - if dossier.motivation.present?
%h4 Motivation %h4 Motivation
%p.dossier-motivation= dossier.motivation %p.dossier-motivation= dossier.motivation
= render partial: 'new_user/dossiers/show/download_justificatif', locals: { dossier: dossier } = render partial: 'users/dossiers/show/download_justificatif', locals: { dossier: dossier }
- if dossier.attestation.present? - if dossier.attestation.present?
%h4 Attestation %h4 Attestation

View file

@ -1,2 +1,7 @@
<%= render_flash %> <%= render_flash %>
<%= render_to_element('.state-button', partial: "state_button", locals: { dossier: dossier }) %> <%= render_to_element('.state-button', partial: "state_button", locals: { dossier: dossier }) %>
<% attachment = dossier.justificatif_motivation.attachment %>
<% if attachment && attachment.virus_scanner.pending? %>
<%= fire_event('attachment:update', { url: attachment_url(attachment.id, { signed_id: attachment.blob.signed_id }) }.to_json ) %>
<% end %>

View file

@ -28,6 +28,7 @@
Réponse donnée le #{l(avis.updated_at, format: '%d/%m/%y à %H:%M')} Réponse donnée le #{l(avis.updated_at, format: '%d/%m/%y à %H:%M')}
- else - else
%span.waiting En attente de réponse %span.waiting En attente de réponse
= render partial: 'shared/piece_jointe/pj_link', locals: { pj: avis.piece_justificative_file, user_can_upload: false } - if avis.piece_justificative_file.attached?
= render partial: 'shared/attachment/show', locals: { attachment: avis.piece_justificative_file.attachment }
.answer-body .answer-body
= simple_format(avis.answer) = simple_format(avis.answer)

View file

@ -36,7 +36,7 @@ as well as a link to its edit page.
<% if page.resource.invitation_expired? %> <% if page.resource.invitation_expired? %>
<%= link_to "renvoyer l'invitation", reinvite_manager_administrateur_path(page.resource), method: :post, class: "button" %> <%= link_to "renvoyer l'invitation", reinvite_manager_administrateur_path(page.resource), method: :post, class: "button" %>
<% end %> <% end %>
<div> </div>
</header> </header>

View file

@ -34,7 +34,7 @@ as well as a link to its edit page.
<% if dossier.hidden_at.nil? %> <% if dossier.hidden_at.nil? %>
<%= link_to 'Supprimer le dossier', hide_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Confirmez vous la suppression du dossier ?" } %> <%= link_to 'Supprimer le dossier', hide_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Confirmez vous la suppression du dossier ?" } %>
<% end %> <% end %>
<div> </div>
</header> </header>
<section class="main-content__body"> <section class="main-content__body">

View file

@ -1,12 +0,0 @@
- if dossier.present?
- justificatif = dossier.justificatif_motivation
- if justificatif.attached?
- if justificatif.virus_scanner.safe?
.action
= link_to (justificatif), target: '_blank', class: 'button primary' do
%span.icon.download
Télécharger le justificatif
- elsif justificatif.virus_scanner.pending?
%p
Un justificatif a été joint. L'analyse antivirus de ce document est en cours.
= link_to "rafraichir", request.path

View file

@ -0,0 +1,26 @@
- should_display_link = attachment.virus_scanner.safe? || !attachment.virus_scanner.started?
- user_can_upload = defined?(user_can_upload) ? user_can_upload : false
- if should_display_link
- attachment_check_url = false
- else
- attachment_check_url = attachment_url(attachment.id, { signed_id: attachment.blob.signed_id, user_can_upload: user_can_upload })
.pj-link{ 'data-attachment-id': attachment.id, 'data-attachment-check-url': attachment_check_url }
- if should_display_link
= link_to url_for(attachment.blob), target: '_blank', rel: 'noopener', title: "Télécharger la pièce jointe" do
%span.icon.attachment
= attachment.filename.to_s
- if !attachment.virus_scanner.started?
(ce fichier na pas été analysé par notre antivirus, téléchargez-le avec précaution)
- else
= attachment.filename.to_s
- if attachment.virus_scanner.pending?
(analyse antivirus en cours
= link_to "rafraichir", request.path, data: { 'attachment-refresh': true }
)
- elsif attachment.virus_scanner.infected?
- if user_can_upload
(virus détecté, merci denvoyer un autre fichier)
- else
(virus détecté, le téléchargement de ce fichier est bloqué)

View file

@ -2,7 +2,7 @@
- if pj.attached? - if pj.attached?
.piece-justificative-actions{ id: "piece_justificative_#{object.id}" } .piece-justificative-actions{ id: "piece_justificative_#{object.id}" }
.piece-justificative-action .piece-justificative-action
= render partial: "shared/piece_jointe/pj_link", locals: { pj: pj, user_can_upload: true } = render partial: "shared/attachment/show", locals: { attachment: pj.attachment, user_can_upload: true }
.piece-justificative-action .piece-justificative-action
= button_tag 'Remplacer', type: 'button', class: 'button small', data: { 'toggle-target': "#champs_#{object.id}" } = button_tag 'Remplacer', type: 'button', class: 'button small', data: { 'toggle-target': "#champs_#{object.id}" }

View file

@ -1,5 +1,5 @@
- pj = champ.piece_justificative_file - pj = champ.piece_justificative_file
- if pj.attached? - if pj.attached?
= render partial: "shared/piece_jointe/pj_link", locals: { pj: pj, user_can_upload: false } = render partial: "shared/attachment/show", locals: { attachment: pj.attachment }
- else - else
Pièce justificative non fournie Pièce justificative non fournie

View file

@ -8,4 +8,4 @@
%th.libelle Justificatif : %th.libelle Justificatif :
%td %td
.action .action
= render partial: 'shared/piece_jointe/pj_link', locals: { object: dossier, pj: dossier.justificatif_motivation } = render partial: 'shared/attachment/show', locals: { attachment: dossier.justificatif_motivation.attachment }

View file

@ -9,7 +9,7 @@
- if pj.attached? - if pj.attached?
.piece-justificative-actions{ id: "piece_justificative_#{champ.id}" } .piece-justificative-actions{ id: "piece_justificative_#{champ.id}" }
.piece-justificative-action .piece-justificative-action
= render partial: "shared/piece_jointe/pj_link", locals: { pj: pj, user_can_upload: true } = render partial: "shared/attachment/show", locals: { attachment: pj.attachment, user_can_upload: true }
.piece-justificative-action .piece-justificative-action
- if champ.private? - if champ.private?
= link_to 'Supprimer', gestionnaire_champ_purge_champ_piece_justificative_path(procedure_id: champ.dossier.procedure_id, dossier_id: champ.dossier_id, champ_id: champ.id), remote: true, method: :delete, class: 'button small danger' = link_to 'Supprimer', gestionnaire_champ_purge_champ_piece_justificative_path(procedure_id: champ.dossier.procedure_id, dossier_id: champ.dossier_id, champ_id: champ.id), remote: true, method: :delete, class: 'button small danger'

View file

@ -1,20 +0,0 @@
- if pj.attached?
.pj-link
- if pj.virus_scanner.safe? || !pj.virus_scanner.started?
= link_to url_for(pj), target: '_blank', rel: 'noopener', title: "Télécharger la pièce jointe" do
%span.icon.attachment
= pj.filename.to_s
- if !pj.virus_scanner.started?
(ce fichier na pas été analysé par notre antivirus, téléchargez-le avec précaution)
- else
= pj.filename.to_s
- if pj.virus_scanner.pending?
(analyse antivirus en cours
= link_to "rafraichir", request.path
)
- elsif pj.virus_scanner.infected?
- if user_can_upload
(virus détecté, merci denvoyer un autre fichier)
- else
(virus détecté, le téléchargement de ce fichier est bloqué)

View file

@ -0,0 +1,2 @@
- if dossier.present? && dossier.justificatif_motivation.attached?
= render partial: "shared/attachment/show", locals: { attachment: dossier.justificatif_motivation.attachment }

View file

@ -50,7 +50,7 @@
%h3 Motif de lacceptation %h3 Motif de lacceptation
%blockquote= dossier.motivation %blockquote= dossier.motivation
= render partial: 'new_user/dossiers/show/download_justificatif', locals: { dossier: dossier } = render partial: 'users/dossiers/show/download_justificatif', locals: { dossier: dossier }
- if dossier.attestation.present? - if dossier.attestation.present?
.action .action
@ -71,7 +71,7 @@
%h3 Motif du refus %h3 Motif du refus
%blockquote= dossier.motivation %blockquote= dossier.motivation
= render partial: 'new_user/dossiers/show/download_justificatif', locals: { dossier: @dossier } = render partial: 'users/dossiers/show/download_justificatif', locals: { dossier: dossier }
.action .action
= link_to 'Envoyer un message à ladministration', messagerie_dossier_url(dossier, anchor: 'new_commentaire'), class: 'button' = link_to 'Envoyer un message à ladministration', messagerie_dossier_url(dossier, anchor: 'new_commentaire'), class: 'button'
@ -83,7 +83,7 @@
= succeed '.' do = succeed '.' do
%strong sans suite %strong sans suite
= render partial: 'new_user/dossiers/show/download_justificatif', locals: { dossier: @dossier } = render partial: 'users/dossiers/show/download_justificatif', locals: { dossier: dossier }
- if dossier.motivation.present? - if dossier.motivation.present?
%h3 Motif du classement sans suite %h3 Motif du classement sans suite

View file

@ -17,4 +17,6 @@
Si vous voyez cette page trop souvent, consultez notre aide : #{link_to 'https://faq.demarches-simplifiees.fr/article/34-je-dois-confirmer-mon-compte-a-chaque-connexion', 'https://faq.demarches-simplifiees.fr/article/34-je-dois-confirmer-mon-compte-a-chaque-connexion', target: '_blank', rel: 'noopener' } Si vous voyez cette page trop souvent, consultez notre aide : #{link_to 'https://faq.demarches-simplifiees.fr/article/34-je-dois-confirmer-mon-compte-a-chaque-connexion', 'https://faq.demarches-simplifiees.fr/article/34-je-dois-confirmer-mon-compte-a-chaque-connexion', target: '_blank', rel: 'noopener' }
%br %br
%br %br
En cas de difficultés, nous restons joignables sur #{link_to CONTACT_EMAIL, "mailto:#{CONTACT_EMAIL}"}. En cas de difficultés, nous restons joignables
= succeed '.' do
= link_to("via ce formulaire", contact_admin_url)

View file

@ -1,21 +1,9 @@
ActiveStorage::Service.url_expires_in = 1.hour ActiveStorage::Service.url_expires_in = 1.hour
# We want to run the virus scan on every ActiveStorage attachment, # In Rails 5.2, we have to hook at `on_load` on the blob themeselves, which is
# regardless of its type (user-uploaded document, instructeur-uploaded attestation, form template, etc.) # not ideal.
# #
# To do this, the best place to work on is the ActiveStorage::Attachment # Rails 6 adds support for `.on_load(:active_storage_attachment)`, which is
# objects themselves. # cleaner (as it allows to enqueue the virus scan on attachment creation, rather
# # than on blob creation).
# We have to monkey patch ActiveStorage in order to always run an analyzer.
# The way analyzable blob interface work is by running the first accepted analyzer.
# This is not what we want for the virus scan. Using analyzer interface is still beneficial
# as it takes care of downloading the blob.
ActiveStorage::Attached::One.class_eval do
def virus_scanner
if attached?
ActiveStorage::VirusScanner.new(attachment.blob)
end
end
end
ActiveSupport.on_load(:active_storage_blob) { include BlobVirusScanner } ActiveSupport.on_load(:active_storage_blob) { include BlobVirusScanner }

View file

@ -133,6 +133,8 @@ Rails.application.routes.draw do
post ':position/repetition', to: 'repetition#show', as: :repetition post ':position/repetition', to: 'repetition#show', as: :repetition
end end
get 'attachments/:id', to: 'attachments#show', as: :attachment
get 'tour-de-france' => 'root#tour_de_france' get 'tour-de-france' => 'root#tour_de_france'
get "patron" => "root#patron" get "patron" => "root#patron"
get "accessibilite" => "root#accessibilite" get "accessibilite" => "root#accessibilite"

View file

@ -1,8 +0,0 @@
namespace :'2019_03_13_migrate_pjs_to_champs' do
task run: :environment do
procedure_id = ENV['PROCEDURE_ID']
service = PieceJustificativeToChampPieceJointeMigrationService.new
service.ensure_correct_storage_configuration!
service.convert_procedure_pjs_to_champ_pjs(Procedure.find(procedure_id))
end
end

View file

@ -0,0 +1,46 @@
require Rails.root.join("lib", "tasks", "task_helper")
namespace :pieces_justificatives do
task migrate_procedure_to_champs: :environment do
procedure_id = ENV['PROCEDURE_ID']
procedure = Procedure.find(procedure_id)
service = PieceJustificativeToChampPieceJointeMigrationService.new
service.ensure_correct_storage_configuration!
progress = ProgressReport.new(service.number_of_champs_to_migrate(procedure))
service.convert_procedure_pjs_to_champ_pjs(procedure) do
progress.inc
end
progress.finish
end
task migrate_procedures_range_to_champs: :environment do
if ENV['RANGE_START'].nil? || ENV['RANGE_END'].nil?
fail "RANGE_START and RANGE_END must be specified"
end
procedures_range = ENV['RANGE_START']..ENV['RANGE_END']
service = PieceJustificativeToChampPieceJointeMigrationService.new
service.ensure_correct_storage_configuration!
procedures_to_migrate = service.procedures_with_pjs_in_range(procedures_range)
total_number_of_champs_to_migrate = procedures_to_migrate
.map { |p| service.number_of_champs_to_migrate(p) }
.sum
progress = ProgressReport.new(total_number_of_champs_to_migrate)
procedures_to_migrate.find_each do |procedure|
rake_puts ''
rake_puts "Migrating procedure #{procedure.id}"
service.convert_procedure_pjs_to_champ_pjs(procedure) do
progress.inc
end
end
progress.finish
end
end

View file

@ -48,5 +48,16 @@ describe Gestionnaires::RechercheController, type: :controller do
end end
end end
end end
context 'with no query param it does not crash' do
subject { get :index, params: {} }
it { is_expected.to have_http_status(200) }
it 'returns 0 dossier' do
subject
expect(assigns(:dossiers).count).to eq(0)
end
end
end end
end end

View file

@ -0,0 +1,51 @@
describe 'pieces_justificatives' do
describe 'migrate_procedure_to_champs' do
let(:rake_task) { Rake::Task['pieces_justificatives:migrate_procedure_to_champs'] }
let(:procedure) { create(:procedure, :with_two_type_de_piece_justificative) }
before do
ENV['PROCEDURE_ID'] = procedure.id.to_s
allow_any_instance_of(PieceJustificativeToChampPieceJointeMigrationService).to receive(:ensure_correct_storage_configuration!)
rake_task.invoke
end
after { rake_task.reenable }
it 'migrates the procedure' do
expect(procedure.reload.types_de_piece_justificative).to be_empty
end
end
describe 'migrate_procedures_range_to_champs' do
let(:rake_task) { Rake::Task['pieces_justificatives:migrate_procedures_range_to_champs'] }
let(:procedure_in_range_1) { create(:procedure, :with_two_type_de_piece_justificative) }
let(:procedure_in_range_2) { create(:procedure, :with_two_type_de_piece_justificative) }
let(:procedure_out_of_range) { create(:procedure, :with_two_type_de_piece_justificative) }
before do
procedure_in_range_1
procedure_in_range_2
procedure_out_of_range
ENV['RANGE_START'] = procedure_in_range_1.id.to_s
ENV['RANGE_END'] = procedure_in_range_2.id.to_s
allow_any_instance_of(PieceJustificativeToChampPieceJointeMigrationService).to receive(:ensure_correct_storage_configuration!)
rake_task.invoke
end
after { rake_task.reenable }
it 'migrates procedures in the ids range' do
expect(procedure_in_range_1.reload.types_de_piece_justificative).to be_empty
expect(procedure_in_range_2.reload.types_de_piece_justificative).to be_empty
end
it 'doesnt migrate procedures not in the range' do
expect(procedure_out_of_range.reload.types_de_piece_justificative).to be_present
end
end
end

View file

@ -390,12 +390,6 @@ describe Champ do
it { expect(champ.piece_justificative_file.virus_scanner.started?).to be_truthy } it { expect(champ.piece_justificative_file.virus_scanner.started?).to be_truthy }
end end
context 'and there is no blob' do
before { champ.save }
it { expect(champ.piece_justificative_file.virus_scanner).to be_nil }
end
end end
end end

View file

@ -1,9 +1,37 @@
require 'spec_helper' require 'spec_helper'
describe CarrierwaveActiveStorageMigrationService do describe CarrierwaveActiveStorageMigrationService do
describe '#hex_to_base64' do let(:service) { CarrierwaveActiveStorageMigrationService.new }
let(:service) { CarrierwaveActiveStorageMigrationService.new }
describe '#hex_to_base64' do
it { expect(service.hex_to_base64('deadbeef')).to eq('3q2+7w==') } it { expect(service.hex_to_base64('deadbeef')).to eq('3q2+7w==') }
end end
describe '.make_blob' do
let(:pj) { create(:piece_justificative, :rib) }
let(:identify) { false }
before do
allow(service).to receive(:checksum).and_return('cafe')
end
subject(:blob) { service.make_blob(pj.content, pj.updated_at.iso8601, filename: pj.original_filename, identify: identify) }
it 'marks the blob as already scanned by the antivirus' do
expect(blob.metadata[:virus_scan_result]).to eq(ActiveStorage::VirusScanner::SAFE)
end
it 'sets the blob MIME type from the file' do
expect(blob.identified).to be true
expect(blob.content_type).to eq 'application/pdf'
end
context 'when asking for explicit MIME type identification' do
let(:identify) { true }
it 'marks the file as needing MIME type detection' do
expect(blob.identified).to be false
end
end
end
end end

View file

@ -9,16 +9,17 @@ describe PieceJustificativeToChampPieceJointeMigrationService do
let(:procedure) { create(:procedure, types_de_piece_justificative: types_pj) } let(:procedure) { create(:procedure, types_de_piece_justificative: types_pj) }
let(:types_pj) { [create(:type_de_piece_justificative)] } let(:types_pj) { [create(:type_de_piece_justificative)] }
let!(:dossier) do let!(:dossier) { make_dossier }
create(
:dossier,
procedure: procedure,
pieces_justificatives: pjs
)
end
let(:pjs) { [] } let(:pjs) { [] }
def make_dossier(hidden: false)
create(:dossier,
procedure: procedure,
pieces_justificatives: pjs,
hidden_at: hidden ? Time.zone.now : nil)
end
def make_pjs def make_pjs
types_pj.map do |tpj| types_pj.map do |tpj|
create(:piece_justificative, :contrat, type_de_piece_justificative: tpj) create(:piece_justificative, :contrat, type_de_piece_justificative: tpj)
@ -31,6 +32,22 @@ describe PieceJustificativeToChampPieceJointeMigrationService do
expect(storage_service).to receive(:make_attachment) expect(storage_service).to receive(:make_attachment)
end end
describe '.number_of_champs_to_migrate' do
let!(:other_dossier) { make_dossier }
it 'reports the numbers of champs to be migrated' do
expect(service.number_of_champs_to_migrate(procedure)).to eq(4)
end
context 'when the procedure has hidden dossiers' do
let!(:hidden_dossier) { make_dossier(hidden: true) }
it 'reports the numbers of champs including those of hidden dossiers' do
expect(service.number_of_champs_to_migrate(procedure)).to eq(6)
end
end
end
context 'when conversion succeeds' do context 'when conversion succeeds' do
context 'for the procedure' do context 'for the procedure' do
it 'types de champ are created for the "pièces jointes" header and for each PJ' do it 'types de champ are created for the "pièces jointes" header and for each PJ' do
@ -114,19 +131,26 @@ describe PieceJustificativeToChampPieceJointeMigrationService do
.to change { dossier.pieces_justificatives.count } .to change { dossier.pieces_justificatives.count }
.to(0) .to(0)
end end
context 'when the procedure has several dossiers' do
let!(:other_dossier) { make_dossier }
it 'sends progress callback for each migrated champ' do
number_of_champs_to_migrate = service.number_of_champs_to_migrate(procedure)
progress_count = 0
service.convert_procedure_pjs_to_champ_pjs(procedure) do
progress_count += 1
end
expect(progress_count).to eq(number_of_champs_to_migrate)
end
end
end end
context 'when the dossier is soft-deleted it still gets converted' do context 'when the dossier is soft-deleted it still gets converted' do
let(:pjs) { make_pjs } let(:pjs) { make_pjs }
let!(:dossier) { make_dossier(hidden: true) }
let!(:dossier) do
create(
:dossier,
procedure: procedure,
pieces_justificatives: pjs,
hidden_at: Time.zone.now
)
end
before { expect_storage_service_to_convert_object } before { expect_storage_service_to_convert_object }

View file

@ -1,6 +1,6 @@
require 'rails_helper' require 'rails_helper'
describe 'shared/piece_jointe/_pj_link.html.haml', type: :view do describe 'shared/attachment/_show.html.haml', type: :view do
let(:champ) { create(:champ_piece_justificative) } let(:champ) { create(:champ_piece_justificative) }
let(:virus_scan_result) { nil } let(:virus_scan_result) { nil }
@ -8,7 +8,7 @@ describe 'shared/piece_jointe/_pj_link.html.haml', type: :view do
champ.piece_justificative_file.blob.update(metadata: champ.piece_justificative_file.blob.metadata.merge(virus_scan_result: virus_scan_result)) champ.piece_justificative_file.blob.update(metadata: champ.piece_justificative_file.blob.metadata.merge(virus_scan_result: virus_scan_result))
end end
subject { render 'shared/piece_jointe/pj_link', pj: champ.piece_justificative_file, user_can_upload: false } subject { render 'shared/attachment/show', attachment: champ.piece_justificative_file.attachment }
context 'when there is no anti-virus scan' do context 'when there is no anti-virus scan' do
let(:virus_scan_result) { nil } let(:virus_scan_result) { nil }