Merge pull request #5995 from betagouv/main

2021-03-18-01
This commit is contained in:
Paul Chavard 2021-03-18 11:16:59 +01:00 committed by GitHub
commit 897064b0f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 176 additions and 33 deletions

View file

@ -210,6 +210,7 @@ module Instructeurs
def telecharger_pjs
return head(:forbidden) if !dossier.attachments_downloadable?
generate_pdf_for_instructeur_export
files = ActiveStorage::DownloadableFile.create_list_from_dossier(dossier)
zipline(files, "dossier-#{dossier.id}.zip")
@ -232,6 +233,12 @@ module Instructeurs
@dossier ||= current_instructeur.dossiers.find(params[:dossier_id])
end
def generate_pdf_for_instructeur_export
@include_infos_administration = true
pdf = render_to_string(file: 'dossiers/show', formats: [:pdf])
dossier.pdf_export_for_instructeur.attach(io: StringIO.open(pdf), filename: "export-#{dossier.id}.pdf", content_type: 'application/pdf')
end
def commentaire_params
params.require(:commentaire).permit(:body, :piece_jointe)
end

View file

@ -395,7 +395,7 @@ type Demarche {
after: String
"""
Si présent, permet de filtrer les dossiers archivés ou non
Seulement les dossiers archivés.
"""
archived: Boolean
@ -419,11 +419,26 @@ type Demarche {
"""
last: Int
"""
Seulement les dossiers pour les révisons avant la révision donnée.
"""
maxRevision: ID
"""
Seulement les dossiers pour les révisons après la révision donnée.
"""
minRevision: ID
"""
Lordre des dossiers.
"""
order: Order = ASC
"""
Seulement les dossiers pour la révision donnée.
"""
revision: ID
"""
Dossiers avec statut.
"""

View file

@ -37,7 +37,10 @@ module Types
argument :created_since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers déposés depuis la date."
argument :updated_since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers mis à jour depuis la date."
argument :state, Types::DossierType::DossierState, required: false, description: "Dossiers avec statut."
argument :archived, Boolean, required: false, description: "Si présent, permet de filtrer les dossiers archivés ou non"
argument :archived, Boolean, required: false, description: "Seulement les dossiers archivés."
argument :revision, ID, required: false, description: "Seulement les dossiers pour la révision donnée."
argument :max_revision, ID, required: false, description: "Seulement les dossiers pour les révisons avant la révision donnée."
argument :min_revision, ID, required: false, description: "Seulement les dossiers pour les révisons après la révision donnée."
end
field :champ_descriptors, [Types::ChampDescriptorType], null: false, method: :types_de_champ
@ -63,7 +66,7 @@ module Types
Loaders::Association.for(object.class, :revisions).load(object)
end
def dossiers(updated_since: nil, created_since: nil, state: nil, archived: nil, order:)
def dossiers(updated_since: nil, created_since: nil, state: nil, archived: nil, revision: nil, max_revision: nil, min_revision: nil, order:)
dossiers = object.dossiers.state_not_brouillon.for_api_v2
if state.present?
@ -74,6 +77,18 @@ module Types
dossiers = dossiers.where(archived: archived)
end
if !revision.nil?
dossiers = dossiers.where(revision: find_revision(revision))
else
if !min_revision.nil?
dossiers = dossiers.joins(:revision).where('procedure_revisions.created_at >= ?', find_revision(min_revision).created_at)
end
if !max_revision.nil?
dossiers = dossiers.joins(:revision).where('procedure_revisions.created_at <= ?', find_revision(max_revision).created_at)
end
end
if updated_since.present?
dossiers = dossiers.updated_since(updated_since).order_by_updated_at(order)
else
@ -90,5 +105,12 @@ module Types
def self.authorized?(object, context)
context.authorized_demarche?(object)
end
private
def find_revision(revision)
revision_id = GraphQL::Schema::UniqueWithinType.decode(revision).second
object.revisions.find(revision_id)
end
end
end

View file

@ -108,9 +108,9 @@ module Types
street_number: object.numero_voie,
street_name: object.nom_voie,
street_address: object.nom_voie.present? ? [object.numero_voie, object.type_voie, object.nom_voie].compact.join(' ') : nil,
postal_code: object.code_postal,
city_name: object.localite,
city_code: object.code_insee_localite
postal_code: object.code_postal.presence || '',
city_name: object.localite.presence || '',
city_code: object.code_insee_localite.presence || ''
}
end

View file

@ -79,6 +79,12 @@ module ApplicationHelper
# rubocop:enable Rails/OutputSafety
end
def focus_element(selector)
# rubocop:disable Rails/OutputSafety
raw("document.querySelector('#{selector}').focus();")
# rubocop:enable Rails/OutputSafety
end
def disable_element(selector)
# rubocop:disable Rails/OutputSafety
raw("document.querySelector('#{selector}').disabled = true;")

View file

@ -25,6 +25,7 @@ export default class ProgressBar {
const element = getDirectUploadElement(id);
if (element) {
element.classList.remove(PENDING_CLASS);
element.focus();
}
}
@ -32,6 +33,7 @@ export default class ProgressBar {
const element = getDirectUploadProgressElement(id);
if (element) {
element.style.width = `${progress}%`;
element.setAttribute('aria-valuenow', progress);
}
}
@ -52,7 +54,7 @@ export default class ProgressBar {
static render(id, filename) {
return `<div id="direct-upload-${id}" class="direct-upload ${PENDING_CLASS}" data-direct-upload-id="${id}">
<div class="direct-upload__progress" style="width: 0%"></div>
<div role="progressbar" aria-valuemin="0" aria-valuemax="100" class="direct-upload__progress" style="width: 0%"></div>
<span class="direct-upload__filename">${filename}</span>
</div>`;
}

View file

@ -16,5 +16,6 @@ delegate('click', TOGGLE_SOURCE_SELECTOR, (evt) => {
const targetElements = document.querySelectorAll(targetSelector);
for (let target of targetElements) {
toggle(target);
target.focus();
}
});

View file

@ -1,24 +1,14 @@
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 |piece_justificative|
files = pjs.map do |piece_justificative|
[
piece_justificative,
self.timestamped_filename(piece_justificative)
]
end
files << [dossier.pdf_export_for_instructeur, self.timestamped_filename(dossier.pdf_export_for_instructeur)]
files
end
private

View file

@ -63,6 +63,7 @@ class Dossier < ApplicationRecord
has_one :france_connect_information, through: :user
has_one_attached :justificatif_motivation
has_one_attached :pdf_export_for_instructeur
has_many :champs, -> { root.public_ordered }, inverse_of: :dossier, dependent: :destroy
has_many :champs_private, -> { root.private_ordered }, class_name: 'Champ', inverse_of: :dossier, dependent: :destroy

View file

@ -4,3 +4,5 @@
<% if attachment.virus_scanner.pending? %>
<%= fire_event('attachment:update', { url: attachment_url(attachment.id, { signed_id: attachment.blob.signed_id, user_can_upload: true }) }.to_json ) %>
<% end %>
<%= focus_element("button[data-toggle-target=\".attachment-input-#{attachment.id}\"]") %>

View file

@ -19,7 +19,7 @@
%ul#print-pj-menu.print-menu.dropdown-content
%li
- if PiecesJustificativesService.pieces_justificatives_total_size(dossier) < Dossier::TAILLE_MAX_ZIP
= link_to "Télécharger toutes les pièces jointes", telecharger_pjs_instructeur_dossier_path(dossier.procedure, dossier), target: "_blank", rel: "noopener", class: "menu-item menu-link"
= link_to "Télécharger le dossier et toutes ses pièces jointes", telecharger_pjs_instructeur_dossier_path(dossier.procedure, dossier), target: "_blank", rel: "noopener", class: "menu-item menu-link"
- else
%p.menu-item Le téléchargement des pièces jointes est désactivé pour les dossiers de plus de #{number_to_human_size Dossier::TAILLE_MAX_ZIP}.

View file

@ -19,7 +19,7 @@
= render partial: "shared/attachment/show", locals: { attachment: attachment, user_can_upload: true }
- if user_can_destroy
.attachment-action
= link_to 'Supprimer', attachment_url(attachment.id, { signed_id: attachment.blob.signed_id }), remote: true, method: :delete, class: 'button small danger', data: { disable: true }
= link_to 'Supprimer', attachment_url(attachment.id, { signed_id: attachment.blob.signed_id }), remote: true, method: :delete, class: 'button small danger', data: { disable: true }, role: 'button'
.attachment-action
= button_tag 'Remplacer', type: 'button', class: 'button small', data: { 'toggle-target': ".attachment-input-#{attachment_id}" }

View file

@ -0,0 +1,4 @@
= format_text_value(champ.to_s)
- if champ.data.present?
Code INSEE :
= champ.data['city_code']

View file

@ -0,0 +1,4 @@
= format_text_value(champ.to_s)
- if champ.external_id.present?
Code INSEE :
= champ.external_id

View file

@ -0,0 +1,4 @@
- if champ.external_id.present?
= format_text_value("#{champ.external_id} - #{champ}")
- else
= format_text_value(champ.to_s)

View file

@ -36,6 +36,12 @@
= render partial: "shared/champs/textarea/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:annuaire_education)
= render partial: "shared/champs/annuaire_education/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:address)
= render partial: "shared/champs/address/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:communes)
= render partial: "shared/champs/communes/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:regions)
= render partial: "shared/champs/regions/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:date)
= c.to_s
- when TypeDeChamp.type_champs.fetch(:datetime)

View file

@ -4,4 +4,13 @@ Sentry.init do |config|
config.enabled_environments = ['production']
config.breadcrumbs_logger = [:active_support_logger]
config.traces_sample_rate = ENV['SENTRY_ENABLED'] == 'enabled' ? 0.001 : nil
config.excluded_exceptions += [
# Ignore exceptions caught by ActiveJob.retry_on
# https://github.com/getsentry/sentry-ruby/issues/1347
'Excon::Error::BadRequest',
'ActiveStorage::IntegrityError',
'VirusScannerJob::FileNotAnalyzedYetError',
'TitreIdentiteWatermarkJob::WatermarkFileNotScannedYetError',
'APIEntreprise::API::Error::TimedOut'
]
end

View file

@ -3,7 +3,7 @@ namespace :after_party do
task remove_invalid_geometries: :environment do
puts "Running deploy task 'remove_invalid_geometries'"
geo_areas = GeoArea.where(source: :selection_utilisateur)
geo_areas = GeoArea.where(source: :selection_utilisateur).includes(champ: [:geo_areas, :type_de_champ])
progress = ProgressReport.new(geo_areas.count)
geo_areas.find_each do |geo_area|
if !geo_area.valid?

View file

@ -234,6 +234,64 @@ describe API::V2::GraphqlController do
end
end
end
context "filter by minRevision" do
let(:query) do
"{
demarche(number: #{procedure.id}) {
id
number
dossiers(minRevision: \"#{procedure.revisions.first.to_typed_id}\") {
nodes {
id
}
}
}
}"
end
it "should be returned" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(demarche: {
id: procedure.to_typed_id,
number: procedure.id,
dossiers: {
nodes: procedure.dossiers.order(:created_at).map do |dossier|
{ id: dossier.to_typed_id }
end
}
})
end
end
context "filter by maxRevision" do
let(:query) do
"{
demarche(number: #{procedure.id}) {
id
number
dossiers(maxRevision: \"#{procedure.revisions.last.to_typed_id}\") {
nodes {
id
}
}
}
}"
end
it "should be returned" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(demarche: {
id: procedure.to_typed_id,
number: procedure.id,
dossiers: {
nodes: procedure.dossiers.order(:created_at).map do |dossier|
{ id: dossier.to_typed_id }
end
}
})
end
end
end
context "dossier" do

View file

@ -201,6 +201,15 @@ FactoryBot.define do
end
end
trait :with_pdf_export do
after(:create) do |dossier, _evaluator|
dossier.pdf_export_for_instructeur.attach(
io: StringIO.new('Hello World'),
filename: 'export.pdf'
)
end
end
trait :with_justificatif do
after(:create) do |dossier, _evaluator|
dossier.justificatif_motivation.attach(

View file

@ -153,7 +153,7 @@ feature 'Instructing a dossier:' do
scenario 'A instructeur can download an archive containing a single attachment' do
find(:css, '.attached').click
click_on 'Télécharger toutes les pièces jointes'
click_on 'Télécharger le dossier et toutes ses pièces jointes'
# For some reason, clicking the download link does not trigger the download in the headless browser ;
# So we need to go to the download link directly
visit telecharger_pjs_instructeur_dossier_path(procedure, dossier)
@ -162,10 +162,11 @@ feature 'Instructing a dossier:' do
files = ZipTricks::FileReader.read_zip_structure(io: File.open(DownloadHelpers.download))
expect(DownloadHelpers.download).to include "dossier-#{dossier.id}.zip"
expect(files.size).to be 2
expect(files.size).to be 3
expect(files[0].filename.include?('piece_justificative_0')).to be_truthy
expect(files[0].uncompressed_size).to be File.size(path)
expect(files[1].filename.include?('horodatage/operation')).to be_truthy
expect(files[2].filename.include?('dossier/export')).to be_truthy
end
scenario 'A instructeur can download an archive containing several identical attachments' do
@ -176,13 +177,14 @@ feature 'Instructing a dossier:' do
files = ZipTricks::FileReader.read_zip_structure(io: File.open(DownloadHelpers.download))
expect(DownloadHelpers.download).to include "dossier-#{dossier.id}.zip"
expect(files.size).to be 3
expect(files.size).to be 4
expect(files[0].filename.include?('piece_justificative_0')).to be_truthy
expect(files[1].filename.include?('piece_justificative_0')).to be_truthy
expect(files[0].filename).not_to eq files[1].filename
expect(files[0].uncompressed_size).to be File.size(path)
expect(files[1].uncompressed_size).to be File.size(path)
expect(files[2].filename.include?('horodatage/operation')).to be_truthy
expect(files[3].filename.include?('dossier/export')).to be_truthy
end
before { DownloadHelpers.clear_downloads }

View file

@ -1,11 +1,12 @@
describe ActiveStorage::DownloadableFile do
let(:dossier) { create(:dossier, :en_construction) }
let(:dossier) { create(:dossier, :en_construction, :with_pdf_export) }
subject(: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([]) }
it { expect(list.length).to eq 1 }
it { expect(list.first[0].record_type).to eq "Dossier" }
end
context 'when there is a piece_justificative' do
@ -13,7 +14,7 @@ describe ActiveStorage::DownloadableFile do
dossier.champs << create(:champ_piece_justificative, :with_piece_justificative_file, dossier: dossier)
end
it { expect(list.length).to eq 1 }
it { expect(list.length).to eq 2 }
end
context 'when there is a private piece_justificative' do
@ -21,7 +22,7 @@ describe ActiveStorage::DownloadableFile do
dossier.champs_private << create(:champ_piece_justificative, :with_piece_justificative_file, private: true, dossier: dossier)
end
it { expect(list.length).to eq 1 }
it { expect(list.length).to eq 2 }
end
context 'when there is a repetition bloc' do
@ -30,7 +31,7 @@ describe ActiveStorage::DownloadableFile do
end
it 'should have 4 piece_justificatives' do
expect(list.size).to eq 4
expect(list.size).to eq 5
end
end
@ -39,7 +40,7 @@ describe ActiveStorage::DownloadableFile do
dossier.commentaires << create(:commentaire, dossier: dossier)
end
it { expect(list.length).to eq 0 }
it { expect(list.length).to eq 1 }
end
context 'when there is a message with an attachment' do
@ -47,7 +48,7 @@ describe ActiveStorage::DownloadableFile do
dossier.commentaires << create(:commentaire, :with_file, dossier: dossier)
end
it { expect(list.length).to eq 1 }
it { expect(list.length).to eq 2 }
end
end
end