Merge pull request #3818 from tchak/migrate-message-attachement-to-active-storage

Migrate message attachements to use active_storage
This commit is contained in:
Paul Chavard 2019-07-10 16:07:41 +02:00 committed by GitHub
commit 5ed992fd47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 138 additions and 66 deletions

View file

@ -138,7 +138,7 @@ module Gestionnaires
end end
def commentaire_params def commentaire_params
params.require(:commentaire).permit(:body, :file) params.require(:commentaire).permit(:body, :piece_jointe)
end end
end end
end end

View file

@ -166,7 +166,7 @@ module Gestionnaires
end end
def commentaire_params def commentaire_params
params.require(:commentaire).permit(:body, :file) params.require(:commentaire).permit(:body, :piece_jointe)
end end
def champs_private_params def champs_private_params

View file

@ -52,7 +52,7 @@ class SupportController < ApplicationController
email: email, email: email,
phone: params[:phone], phone: params[:phone],
text: params[:text], text: params[:text],
file: params[:file], file: params[:piece_jointe],
dossier_id: dossier&.id, dossier_id: dossier&.id,
browser: browser_name, browser: browser_name,
tags: tags tags: tags
@ -61,7 +61,7 @@ class SupportController < ApplicationController
def create_commentaire def create_commentaire
attributes = { attributes = {
file: params[:file], piece_jointe: params[:piece_jointe],
body: "[#{params[:subject]}]<br><br>#{params[:text]}" body: "[#{params[:subject]}]<br><br>#{params[:text]}"
} }
commentaire = CommentaireService.build(current_user, dossier, attributes) commentaire = CommentaireService.build(current_user, dossier, attributes)

View file

@ -354,7 +354,7 @@ module Users
end end
def commentaire_params def commentaire_params
params.require(:commentaire).permit(:body, :file) params.require(:commentaire).permit(:body, :piece_jointe)
end end
def passage_en_construction? def passage_en_construction?

View file

@ -5,9 +5,10 @@ class Commentaire < ApplicationRecord
belongs_to :gestionnaire belongs_to :gestionnaire
mount_uploader :file, CommentaireFileUploader mount_uploader :file, CommentaireFileUploader
validates :file, file_size: { maximum: 20.megabytes, message: "La taille du fichier doit être inférieure à 20 Mo" }
validate :is_virus_free?
validate :messagerie_available?, on: :create validate :messagerie_available?, on: :create
has_one_attached :piece_jointe
validates :body, presence: { message: "Votre message ne peut être vide" } validates :body, presence: { message: "Votre message ne peut être vide" }
default_scope { order(created_at: :asc) } default_scope { order(created_at: :asc) }
@ -47,10 +48,15 @@ class Commentaire < ApplicationRecord
end end
def file_url def file_url
if Flipflop.remote_storage? if piece_jointe.attached?
if piece_jointe.virus_scanner.safe?
Rails.application.routes.url_helpers.url_for(piece_jointe)
end
elsif Flipflop.remote_storage?
RemoteDownloader.new(file.path).url RemoteDownloader.new(file.path).url
else elsif file&.url
file.url # FIXME: this is horrible but used only in dev and will be removed after migration
File.join(LOCAL_DOWNLOAD_URL, file.url)
end end
end end
@ -74,12 +80,6 @@ class Commentaire < ApplicationRecord
DossierMailer.notify_new_answer(dossier).deliver_later DossierMailer.notify_new_answer(dossier).deliver_later
end end
def is_virus_free?
if file.present? && file_changed? && !ClamavService.safe_file?(file.path)
errors.add(:file, "Virus détecté dans le fichier joint, merci de changer de fichier")
end
end
def messagerie_available? def messagerie_available?
return if sent_by_system? return if sent_by_system?
if dossier.present? && !dossier.messagerie_available? if dossier.present? && !dossier.messagerie_available?

View file

@ -117,7 +117,7 @@ class Dossier < ApplicationRecord
scope :nearing_end_of_retention, -> (duration = '1 month') { joins(:procedure).where("en_instruction_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - now() < interval ?", duration) } scope :nearing_end_of_retention, -> (duration = '1 month') { joins(:procedure).where("en_instruction_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - now() < interval ?", duration) }
scope :since, -> (since) { where('dossiers.en_construction_at >= ?', since) } scope :since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
scope :for_api, -> { scope :for_api, -> {
includes(commentaires: [], includes(commentaires: { piece_jointe_attachment: :blob },
champs: [ champs: [
:geo_areas, :geo_areas,
:etablissement, :etablissement,

View file

@ -13,6 +13,11 @@ class CommentaireService
def build_with_email(email, dossier, params) def build_with_email(email, dossier, params)
attributes = params.merge(email: email, dossier: dossier) attributes = params.merge(email: email, dossier: dossier)
# For some reason ActiveStorage trows an error in tests if we passe an empty string here.
# I suspect it could be resolved in rails 6 by using explicit `attach()`
if attributes[:piece_jointe].blank?
attributes.delete(:piece_jointe)
end
Commentaire.new(attributes) Commentaire.new(attributes)
end end
end end

View file

@ -14,7 +14,7 @@ class LocalDownloader
end end
def url def url
@url ||= File.join(TPS::Application::URL, 'downloads', random_folder_name, "#{@filename_suffix}.#{@extension}") @url ||= File.join(LOCAL_DOWNLOAD_URL, 'downloads', random_folder_name, "#{@filename_suffix}.#{@extension}")
end end
protected protected

View file

@ -61,7 +61,7 @@
.messagerie .messagerie
%ul.messages-list %ul.messages-list
- @dossier.commentaires.each do |commentaire| - @dossier.commentaires.with_attached_piece_jointe.each do |commentaire|
%li %li
= render partial: "shared/dossiers/messages/message", locals: { commentaire: commentaire, connected_user: current_gestionnaire, messagerie_seen_at: nil } = render partial: "shared/dossiers/messages/message", locals: { commentaire: commentaire, connected_user: current_gestionnaire, messagerie_seen_at: nil }

View file

@ -1,6 +1,6 @@
.messagerie.container .messagerie.container
%ul.messages-list %ul.messages-list
- dossier.commentaires.each do |commentaire| - dossier.commentaires.with_attached_piece_jointe.each do |commentaire|
%li.message{ class: commentaire_is_from_me_class(commentaire, connected_user) } %li.message{ class: commentaire_is_from_me_class(commentaire, connected_user) }
= render partial: "shared/dossiers/messages/message", locals: { commentaire: commentaire, connected_user: connected_user, messagerie_seen_at: messagerie_seen_at } = render partial: "shared/dossiers/messages/message", locals: { commentaire: commentaire, connected_user: connected_user, messagerie_seen_at: messagerie_seen_at }

View file

@ -2,8 +2,8 @@
= f.text_area :body, rows: 5, placeholder: 'Répondre ici', required: true, class: 'message-textarea' = f.text_area :body, rows: 5, placeholder: 'Répondre ici', required: true, class: 'message-textarea'
.flex.justify-between.wrap .flex.justify-between.wrap
%div %div
= f.file_field :file, id: :file, accept: commentaire.file.accept_extension_list = f.file_field :piece_jointe, id: 'piece_jointe', direct_upload: true
%label{ for: :file } %label{ for: :piece_jointe }
.notice .notice
(taille max : 20 Mo) (taille max : 20 Mo)

View file

@ -10,7 +10,10 @@
= commentaire_date(commentaire) = commentaire_date(commentaire)
.rich-text= sanitize(simple_format(commentaire.body)) .rich-text= sanitize(simple_format(commentaire.body))
- if commentaire.file.present? - if commentaire.piece_jointe.attached?
.attachment-link
= render partial: "shared/attachment/show", locals: { attachment: commentaire.piece_jointe.attachment }
- elsif commentaire.file.present?
.attachment-link .attachment-link
= link_to commentaire.file_url, class: "button", target: "_blank", rel: "noopener", title: "Télécharger" do = link_to commentaire.file_url, class: "button", target: "_blank", rel: "noopener", title: "Télécharger" do
%span.icon.attachment %span.icon.attachment

View file

@ -76,7 +76,7 @@
Une capture décran peut nous aider à identifier plus facilement lendroit à améliorer. Une capture décran peut nous aider à identifier plus facilement lendroit à améliorer.
.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AUTRE } } .notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AUTRE } }
Une capture décran peut nous aider à identifier plus facilement le problème. Une capture décran peut nous aider à identifier plus facilement le problème.
= file_field_tag :file = file_field_tag :piece_jointe
= hidden_field_tag :tags, @tags&.join(',') = hidden_field_tag :tags, @tags&.join(',')

View file

@ -31,8 +31,6 @@ module TPS
config.assets.paths << Rails.root.join('app', 'assets', 'fonts') config.assets.paths << Rails.root.join('app', 'assets', 'fonts')
config.assets.precompile += ['.woff'] config.assets.precompile += ['.woff']
URL = ENV['APP_HOST'] || "http://localhost:3000/"
config.active_job.queue_adapter = :delayed_job config.active_job.queue_adapter = :delayed_job
config.action_view.sanitized_allowed_tags = ActionView::Base.sanitized_allowed_tags + ['u'] config.action_view.sanitized_allowed_tags = ActionView::Base.sanitized_allowed_tags + ['u']

View file

@ -28,3 +28,6 @@ FAQ_ADMIN_URL = "https://faq.demarches-simplifiees.fr/collection/1-administrateu
COMMENT_TROUVER_MA_DEMARCHE_URL = [FAQ_URL, "article", "59-comment-trouver-ma-demarche"].join("/") COMMENT_TROUVER_MA_DEMARCHE_URL = [FAQ_URL, "article", "59-comment-trouver-ma-demarche"].join("/")
STATUS_PAGE_URL = "https://status.demarches-simplifiees.fr" STATUS_PAGE_URL = "https://status.demarches-simplifiees.fr"
MATOMO_IFRAME_URL = "https://stats.data.gouv.fr/index.php?module=CoreAdminHome&action=optOut&language=fr&&fontColor=333333&fontSize=16px&fontFamily=Muli" MATOMO_IFRAME_URL = "https://stats.data.gouv.fr/index.php?module=CoreAdminHome&action=optOut&language=fr&&fontColor=333333&fontSize=16px&fontFamily=Muli"
# FIXME: This is only used in dev in couple of places and should be removed after PJ migration
LOCAL_DOWNLOAD_URL = "http://#{ENV.fetch('APP_HOST', 'localhost:3000')}"

View file

@ -0,0 +1,37 @@
namespace :'2019_05_29_migrate_commentaire_pj' do
task run: :environment do
commentaires = Commentaire.where
.not(file: nil)
.left_joins(:piece_jointe_attachment)
.where('active_storage_attachments.id IS NULL')
.order(:created_at)
limit = ENV['LIMIT']
if limit
commentaires.limit!(limit.to_i)
end
progress = ProgressReport.new(commentaires.count)
commentaires.find_each do |commentaire|
if commentaire.file.present?
uri = URI.parse(URI.escape(commentaire.file_url))
response = Typhoeus.get(uri)
if response.success?
filename = commentaire.file.filename || commentaire.file_identifier
updated_at = commentaire.updated_at
dossier_updated_at = commentaire.dossier.updated_at
commentaire.piece_jointe.attach(
io: StringIO.new(response.body),
filename: filename,
content_type: commentaire.file.content_type,
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
)
commentaire.update_column(:updated_at, updated_at)
commentaire.dossier.update_column(:updated_at, dossier_updated_at)
end
end
progress.inc
end
progress.finish
end
end

View file

@ -92,7 +92,7 @@ describe Gestionnaires::AvisController, type: :controller do
let(:file) { nil } let(:file) { nil }
let(:scan_result) { true } let(:scan_result) { true }
subject { post :create_commentaire, params: { id: avis_without_answer.id, commentaire: { body: 'commentaire body', file: file } } } subject { post :create_commentaire, params: { id: avis_without_answer.id, commentaire: { body: 'commentaire body', piece_jointe: file } } }
before do before do
allow(ClamavService).to receive(:safe_file?).and_return(scan_result) allow(ClamavService).to receive(:safe_file?).and_return(scan_result)
@ -110,16 +110,10 @@ describe Gestionnaires::AvisController, type: :controller do
it do it do
subject subject
expect(Commentaire.last.file.path).to include("piece_justificative_0.pdf") expect(Commentaire.last.piece_jointe.filename).to eq("piece_justificative_0.pdf")
end end
it { expect { subject }.to change(Commentaire, :count).by(1) } it { expect { subject }.to change(Commentaire, :count).by(1) }
context "and a virus" do
let(:scan_result) { false }
it { expect { subject }.not_to change(Commentaire, :count) }
end
end end
end end

View file

@ -350,15 +350,15 @@ describe Gestionnaires::DossiersController, type: :controller do
expect(flash.notice).to be_present expect(flash.notice).to be_present
end end
context "when the commentaire creation fails" do context "when the commentaire created with virus file" do
let(:scan_result) { false } let(:scan_result) { false }
it "renders the messagerie page with the invalid commentaire" do it "creates a commentaire (shows message that file have a virus)" do
expect { subject }.not_to change(Commentaire, :count) expect { subject }.to change(Commentaire, :count).by(1)
expect(gestionnaire.followed_dossiers).to include(dossier)
expect(response).to render_template :messagerie expect(response).to redirect_to(messagerie_gestionnaire_dossier_path(dossier.procedure, dossier))
expect(flash.alert).to be_present expect(flash.notice).to be_present
expect(assigns(:commentaire).body).to eq("avant\napres")
end end
end end
end end

View file

@ -806,7 +806,7 @@ describe Users::DossiersController, type: :controller do
id: dossier.id, id: dossier.id,
commentaire: { commentaire: {
body: body, body: body,
file: file piece_jointe: file
} }
} }
} }
@ -822,18 +822,6 @@ describe Users::DossiersController, type: :controller do
expect(response).to redirect_to(messagerie_dossier_path(dossier)) expect(response).to redirect_to(messagerie_dossier_path(dossier))
expect(flash.notice).to be_present expect(flash.notice).to be_present
end end
context "when the commentaire creation fails" do
let(:scan_result) { false }
it "renders the messagerie page with the invalid commentaire" do
expect { subject }.not_to change(Commentaire, :count)
expect(response).to render_template :messagerie
expect(flash.alert).to be_present
expect(assigns(:commentaire).body).to eq("avant\napres")
end
end
end end
describe '#ask_deletion' do describe '#ask_deletion' do

View file

@ -7,5 +7,9 @@ FactoryBot.define do
commentaire.dossier = create :dossier, :en_construction commentaire.dossier = create :dossier, :en_construction
end end
end end
trait :with_file do
file { Rack::Test::UploadedFile.new("./spec/fixtures/files/logo_test_procedure.png", 'application/pdf') }
end
end end
end end

View file

@ -0,0 +1,42 @@
describe '2019_05_29_migrate_commentaire_pj.rake' do
let(:rake_task) { Rake::Task['2019_05_29_migrate_commentaire_pj:run'] }
let!(:commentaires) do
create(:commentaire)
create(:commentaire, :with_file)
create(:commentaire, :with_file)
end
before do
Commentaire.all.each do |commentaire|
if commentaire.file.present?
stub_request(:get, commentaire.file_url)
.to_return(status: 200, body: File.read(commentaire.file.path))
end
end
end
after do
ENV['LIMIT'] = nil
rake_task.reenable
end
it 'should migrate pj' do
comment_updated_at = Commentaire.last.updated_at
dossier_updated_at = Commentaire.last.dossier.updated_at
expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, false, false])
rake_task.invoke
expect(Commentaire.where(file: nil).count).to eq(1)
expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, true, true])
expect(Commentaire.last.updated_at).to eq(comment_updated_at)
expect(Commentaire.last.dossier.updated_at).to eq(dossier_updated_at)
end
it 'should migrate pj within limit' do
expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, false, false])
ENV['LIMIT'] = '1'
rake_task.invoke
expect(Commentaire.where(file: nil).count).to eq(1)
expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, true, false])
end
end

View file

@ -1,24 +1,21 @@
require 'spec_helper' require 'spec_helper'
describe CommentaireService do describe CommentaireService do
include ActiveJob::TestHelper
describe '.create' do describe '.create' do
let(:dossier) { create :dossier, :en_construction } let(:dossier) { create :dossier, :en_construction }
let(:sender) { dossier.user } let(:sender) { dossier.user }
let(:body) { 'Contenu du message.' } let(:body) { 'Contenu du message.' }
let(:file) { nil } let(:file) { nil }
let(:scan_result) { true }
subject(:commentaire) { CommentaireService.build(sender, dossier, { body: body, file: file }) } subject(:commentaire) { CommentaireService.build(sender, dossier, { body: body, piece_jointe: file }) }
before do
allow(ClamavService).to receive(:safe_file?).and_return(scan_result)
end
it 'creates a new valid commentaire' do it 'creates a new valid commentaire' do
expect(commentaire.email).to eq sender.email expect(commentaire.email).to eq sender.email
expect(commentaire.dossier).to eq dossier expect(commentaire.dossier).to eq dossier
expect(commentaire.body).to eq 'Contenu du message.' expect(commentaire.body).to eq 'Contenu du message.'
expect(commentaire.file).to be_blank expect(commentaire.piece_jointe.attached?).to be_falsey
expect(commentaire).to be_valid expect(commentaire).to be_valid
end end
@ -34,14 +31,15 @@ describe CommentaireService do
context 'when it has a file' do context 'when it has a file' do
let(:file) { Rack::Test::UploadedFile.new("./spec/fixtures/files/piece_justificative_0.pdf", 'application/pdf') } let(:file) { Rack::Test::UploadedFile.new("./spec/fixtures/files/piece_justificative_0.pdf", 'application/pdf') }
it 'saves the attached file' do before do
expect(commentaire.file).to be_present expect(ClamavService).to receive(:safe_file?).and_return(true)
expect(commentaire).to be_valid
end end
context 'and a virus' do it 'saves the attached file' do
let(:scan_result) { false } perform_enqueued_jobs do
it { expect(commentaire).not_to be_valid } commentaire.save
expect(commentaire.piece_jointe.attached?).to be_truthy
end
end end
end end
end end