Merge pull request #4499 from betagouv/dev

2019-11-07-02
This commit is contained in:
LeSim 2019-11-07 17:03:36 +01:00 committed by GitHub
commit 7c82eefbd1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 321 additions and 24 deletions

View file

@ -1,4 +1,4 @@
module Mutations module Mutations
class BaseMutation < GraphQL::Schema::Mutation class BaseMutation < GraphQL::Schema::RelayClassicMutation
end end
end end

View file

@ -0,0 +1,41 @@
module Mutations
class CreateDirectUpload < Mutations::BaseMutation
description "File information required to prepare a direct upload"
argument :dossier_id, ID, "Dossier ID", required: true, loads: Types::DossierType
argument :filename, String, "Original file name", required: true
argument :byte_size, Int, "File size (bytes)", required: true
argument :checksum, String, "MD5 file checksum as base64", required: true
argument :content_type, String, "File content type", required: true
class DirectUpload < Types::BaseObject
description "Represents direct upload credentials"
field :url, String, "Upload URL", null: false
field :headers, String, "HTTP request headers (JSON-encoded)", null: false
field :blob_id, ID, "Created blob record ID", null: false
field :signed_blob_id, ID, "Created blob record signed ID", null: false
end
field :direct_upload, DirectUpload, null: false
def resolve(filename:, byte_size:, checksum:, content_type:, dossier:)
blob = ActiveStorage::Blob.create_before_direct_upload!(
filename: filename,
byte_size: byte_size,
checksum: checksum,
content_type: content_type
)
{
direct_upload: {
url: blob.service_url_for_direct_upload,
# NOTE: we pass headers as JSON since they have no schema
headers: blob.service_headers_for_direct_upload.to_json,
blob_id: blob.id,
signed_blob_id: blob.signed_id
}
}
end
end
end

View file

@ -40,6 +40,52 @@ GeoJSON coordinates
""" """
scalar Coordinates scalar Coordinates
"""
Autogenerated input type of CreateDirectUpload
"""
input CreateDirectUploadInput {
"""
File size (bytes)
"""
byteSize: Int!
"""
MD5 file checksum as base64
"""
checksum: String!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
File content type
"""
contentType: String!
"""
Dossier ID
"""
dossierId: ID!
"""
Original file name
"""
filename: String!
}
"""
Autogenerated return type of CreateDirectUpload
"""
type CreateDirectUploadPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
directUpload: DirectUpload!
}
type DateChamp implements Champ { type DateChamp implements Champ {
id: ID! id: ID!
label: String! label: String!
@ -131,6 +177,31 @@ enum DemarcheState {
publiee publiee
} }
"""
Represents direct upload credentials
"""
type DirectUpload {
"""
Created blob record ID
"""
blobId: ID!
"""
HTTP request headers (JSON-encoded)
"""
headers: String!
"""
Created blob record signed ID
"""
signedBlobId: ID!
"""
Upload URL
"""
url: String!
}
""" """
Un dossier Un dossier
""" """
@ -324,6 +395,10 @@ type MultipleDropDownListChamp implements Champ {
} }
type Mutation { type Mutation {
"""
File information required to prepare a direct upload
"""
createDirectUpload(input: CreateDirectUploadInput!): CreateDirectUploadPayload
} }
""" """

View file

@ -1,4 +1,5 @@
module Types module Types
class MutationType < Types::BaseObject class MutationType < Types::BaseObject
field :create_direct_upload, mutation: Mutations::CreateDirectUpload
end end
end end

View file

@ -56,7 +56,9 @@ function addTypeDeChamp(state, typeDeChamps, insertAfter, done) {
state.flash.success(); state.flash.success();
done(); done();
if (insertAfter) { if (insertAfter) {
scrollToComponent(insertAfter.target.nextElementSibling); scrollToComponent(insertAfter.target.nextElementSibling, {
duration: 300
});
} }
}) })
.catch(message => state.flash.error(message)); .catch(message => state.flash.error(message));
@ -219,7 +221,7 @@ function getUpdateHandler(typeDeChamp, { queue, flash }) {
} }
function findItemToInsertAfter() { function findItemToInsertAfter() {
const target = getFirstTarget(); const target = getLastVisibleTypeDeChamp();
return { return {
target, target,
@ -227,8 +229,10 @@ function findItemToInsertAfter() {
}; };
} }
function getFirstTarget() { function getLastVisibleTypeDeChamp() {
const [target] = document.querySelectorAll('[data-in-view]'); const typeDeChamps = document.querySelectorAll('[data-in-view]');
const target = typeDeChamps[typeDeChamps.length - 1];
if (target) { if (target) {
const parentTarget = target.closest('[data-repetition]'); const parentTarget = target.closest('[data-repetition]');
if (parentTarget) { if (parentTarget) {

View file

@ -33,6 +33,12 @@ class Champs::RepetitionChamp < Champ
end end
end end
# We have to truncate the label here as spreadsheets have a (30 char) limit on length.
def libelle_for_export
str = "(#{type_de_champ.stable_id}) #{libelle}"
ActiveStorage::Filename.new(str).sanitized.truncate(30)
end
class Row < Hashie::Dash class Row < Hashie::Dash
property :index property :index
property :dossier_id property :dossier_id

View file

@ -472,7 +472,19 @@ class Dossier < ApplicationRecord
log_dossier_operation(avis.claimant, :demander_un_avis, avis) log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end end
def spreadsheet_columns def spreadsheet_columns_csv
spreadsheet_columns(with_etablissement: true)
end
def spreadsheet_columns_xlsx
spreadsheet_columns
end
def spreadsheet_columns_ods
spreadsheet_columns
end
def spreadsheet_columns(with_etablissement: false)
columns = [ columns = [
['ID', id.to_s], ['ID', id.to_s],
['Email', user.email] ['Email', user.email]
@ -485,6 +497,39 @@ class Dossier < ApplicationRecord
['Prénom', individual&.prenom], ['Prénom', individual&.prenom],
['Date de naissance', individual&.birthdate] ['Date de naissance', individual&.birthdate]
] ]
elsif with_etablissement
columns += [
['Établissement SIRET', etablissement&.siret],
['Établissement siège social', etablissement&.siege_social],
['Établissement NAF', etablissement&.naf],
['Établissement libellé NAF', etablissement&.libelle_naf],
['Établissement Adresse', etablissement&.adresse],
['Établissement numero voie', etablissement&.numero_voie],
['Établissement type voie', etablissement&.type_voie],
['Établissement nom voie', etablissement&.nom_voie],
['Établissement complément adresse', etablissement&.complement_adresse],
['Établissement code postal', etablissement&.code_postal],
['Établissement localité', etablissement&.localite],
['Établissement code INSEE localité', etablissement&.code_insee_localite],
['Entreprise SIREN', etablissement&.entreprise_siren],
['Entreprise capital social', etablissement&.entreprise_capital_social],
['Entreprise numero TVA intracommunautaire', etablissement&.entreprise_numero_tva_intracommunautaire],
['Entreprise forme juridique', etablissement&.entreprise_forme_juridique],
['Entreprise forme juridique code', etablissement&.entreprise_forme_juridique_code],
['Entreprise nom commercial', etablissement&.entreprise_nom_commercial],
['Entreprise raison sociale', etablissement&.entreprise_raison_sociale],
['Entreprise SIRET siège social', etablissement&.entreprise_siret_siege_social],
['Entreprise code effectif entreprise', etablissement&.entreprise_code_effectif_entreprise],
['Entreprise date de création', etablissement&.entreprise_date_creation],
['Entreprise nom', etablissement&.entreprise_nom],
['Entreprise prénom', etablissement&.entreprise_prenom],
['Association RNA', etablissement&.association_rna],
['Association titre', etablissement&.association_titre],
['Association objet', etablissement&.association_objet],
['Association date de création', etablissement&.association_date_creation],
['Association date de déclaration', etablissement&.association_date_declaration],
['Association date de publication', etablissement&.association_date_publication]
]
else else
columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale] columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
end end

View file

@ -16,21 +16,21 @@ class ProcedureExportV2Service
@tables = [:dossiers, :etablissements, :avis] + champs_repetables_options @tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
end end
def to_csv(table = :dossiers) def to_csv
SpreadsheetArchitect.to_csv(options_for(table)) SpreadsheetArchitect.to_csv(options_for(:dossiers, :csv))
end end
def to_xlsx def to_xlsx
# We recursively build multi page spreadsheet # We recursively build multi page spreadsheet
@tables.reduce(nil) do |package, table| @tables.reduce(nil) do |package, table|
SpreadsheetArchitect.to_axlsx_package(options_for(table), package) SpreadsheetArchitect.to_axlsx_package(options_for(table, :xlsx), package)
end.to_stream.read end.to_stream.read
end end
def to_ods def to_ods
# We recursively build multi page spreadsheet # We recursively build multi page spreadsheet
@tables.reduce(nil) do |spreadsheet, table| @tables.reduce(nil) do |spreadsheet, table|
SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table), spreadsheet) SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table, :ods), spreadsheet)
end.bytes end.bytes
end end
@ -53,7 +53,7 @@ class ProcedureExportV2Service
[dossier.champs, dossier.champs_private] [dossier.champs, dossier.champs_private]
.flatten .flatten
.filter { |champ| champ.is_a?(Champs::RepetitionChamp) } .filter { |champ| champ.is_a?(Champs::RepetitionChamp) }
end.group_by(&:libelle) end.group_by(&:libelle_for_export)
end end
def champs_repetables_options def champs_repetables_options
@ -70,21 +70,16 @@ class ProcedureExportV2Service
row_style: { background_color: nil, color: "000000", font_size: 12 } row_style: { background_color: nil, color: "000000", font_size: 12 }
} }
def sanitize_sheet_name(name) def options_for(table, format)
ActiveStorage::Filename.new(name.to_s).sanitized.truncate(30)
end
def options_for(table)
case table case table
when :dossiers when :dossiers
{ instances: dossiers.to_a, sheet_name: 'Dossiers' }.merge(DEFAULT_STYLES) { instances: dossiers.to_a, sheet_name: 'Dossiers', spreadsheet_columns: :"spreadsheet_columns_#{format}" }.merge(DEFAULT_STYLES)
when :etablissements when :etablissements
{ instances: etablissements.to_a, sheet_name: 'Etablissements' }.merge(DEFAULT_STYLES) { instances: etablissements.to_a, sheet_name: 'Etablissements' }.merge(DEFAULT_STYLES)
when :avis when :avis
{ instances: avis.to_a, sheet_name: 'Avis' }.merge(DEFAULT_STYLES) { instances: avis.to_a, sheet_name: 'Avis' }.merge(DEFAULT_STYLES)
when Array when Array
# We have to truncate the label here as spreadsheets have a (30 char) limit on length. { instances: table.last, sheet_name: table.first }.merge(DEFAULT_STYLES)
{ instances: table.last, sheet_name: sanitize_sheet_name(table.first) }.merge(DEFAULT_STYLES)
end end
end end
end end

View file

@ -30,14 +30,14 @@
.procedure-list-element .procedure-list-element
Administrateurs Administrateurs
- if !feature_enabled?(:routage) - if !feature_enabled?(:administrateur_routage)
%a#onglet-instructeurs{ href: url_for(admin_procedure_assigns_path(@procedure)) } %a#onglet-instructeurs{ href: url_for(admin_procedure_assigns_path(@procedure)) }
.procedure-list-element{ class: ('active' if active == 'Instructeurs') } .procedure-list-element{ class: ('active' if active == 'Instructeurs') }
Instructeurs Instructeurs
- if @procedure.missing_steps.include?(:instructeurs) - if @procedure.missing_steps.include?(:instructeurs)
%p.missing-steps (à compléter) %p.missing-steps (à compléter)
- if feature_enabled?(:routage) - if feature_enabled?(:administrateur_routage)
%a#onglet-instructeurs{ href: url_for(procedure_groupe_instructeurs_path(@procedure)) } %a#onglet-instructeurs{ href: url_for(procedure_groupe_instructeurs_path(@procedure)) }
.procedure-list-element .procedure-list-element
Groupe d'instructeurs Groupe d'instructeurs

View file

@ -166,6 +166,39 @@ describe API::V2::GraphqlController do
expect(gql_data[:dossier][:champs][0][:id]).to eq(dossier.champs[0].type_de_champ.to_typed_id) expect(gql_data[:dossier][:champs][0][:id]).to eq(dossier.champs[0].type_de_champ.to_typed_id)
end end
end end
context "mutations" do
context 'createDirectUpload' do
let(:query) do
"mutation {
createDirectUpload(input: {
dossierId: \"#{dossier.to_typed_id}\",
filename: \"hello.png\",
byteSize: 1234,
checksum: \"qwerty1234\",
contentType: \"image/png\"
}) {
directUpload {
url
headers
blobId
signedBlobId
}
}
}"
end
it "should initiate a direct upload" do
expect(gql_errors).to eq(nil)
data = gql_data[:createDirectUpload][:directUpload]
expect(data[:url]).not_to be_nil
expect(data[:headers]).not_to be_nil
expect(data[:blobId]).not_to be_nil
expect(data[:signedBlobId]).not_to be_nil
end
end
end
end end
context "when not authenticated" do context "when not authenticated" do

View file

@ -7,7 +7,7 @@ feature 'The routing' do
let(:scientifique_user) { create(:user, password: password) } let(:scientifique_user) { create(:user, password: password) }
let(:litteraire_user) { create(:user, password: password) } let(:litteraire_user) { create(:user, password: password) }
before { Flipper.enable_actor(:routage, administrateur.user) } before { Flipper.enable_actor(:administrateur_routage, administrateur.user) }
scenario 'works' do scenario 'works' do
login_as administrateur.user, scope: :user login_as administrateur.user, scope: :user

View file

@ -1,4 +1,5 @@
require 'spec_helper' require 'spec_helper'
require 'csv'
describe ProcedureExportV2Service do describe ProcedureExportV2Service do
describe 'to_data' do describe 'to_data' do
@ -150,6 +151,91 @@ describe ProcedureExportV2Service do
] ]
end end
context 'as csv' do
subject do
Tempfile.create do |f|
f << ProcedureExportV2Service.new(procedure, procedure.dossiers).to_csv
f.rewind
CSV.read(f.path)
end
end
let(:nominal_headers) do
[
"ID",
"Email",
"Établissement SIRET",
"Établissement siège social",
"Établissement NAF",
"Établissement libellé NAF",
"Établissement Adresse",
"Établissement numero voie",
"Établissement type voie",
"Établissement nom voie",
"Établissement complément adresse",
"Établissement code postal",
"Établissement localité",
"Établissement code INSEE localité",
"Entreprise SIREN",
"Entreprise capital social",
"Entreprise numero TVA intracommunautaire",
"Entreprise forme juridique",
"Entreprise forme juridique code",
"Entreprise nom commercial",
"Entreprise raison sociale",
"Entreprise SIRET siège social",
"Entreprise code effectif entreprise",
"Entreprise date de création",
"Entreprise nom",
"Entreprise prénom",
"Association RNA",
"Association titre",
"Association objet",
"Association date de création",
"Association date de déclaration",
"Association date de publication",
"Archivé",
"État du dossier",
"Dernière mise à jour le",
"Déposé le",
"Passé en instruction le",
"Traité le",
"Motivation de la décision",
"Instructeurs",
"textarea",
"date",
"datetime",
"number",
"decimal_number",
"integer_number",
"checkbox",
"civilite",
"email",
"phone",
"address",
"yes_no",
"simple_drop_down_list",
"multiple_drop_down_list",
"linked_drop_down_list",
"pays",
"regions",
"departements",
"engagement",
"dossier_link",
"piece_justificative",
"siret",
"carte",
"text"
]
end
let(:dossiers_sheet_headers) { subject.first }
it 'should have headers' do
expect(dossiers_sheet_headers).to match(nominal_headers)
end
end
it 'should have headers' do it 'should have headers' do
expect(dossiers_sheet.headers).to match(nominal_headers) expect(dossiers_sheet.headers).to match(nominal_headers)
@ -225,7 +311,7 @@ describe ProcedureExportV2Service do
let(:champ_repetition) { dossiers.first.champs.find { |champ| champ.type_champ == 'repetition' } } let(:champ_repetition) { dossiers.first.champs.find { |champ| champ.type_champ == 'repetition' } }
it 'should have sheets' do it 'should have sheets' do
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle]) expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle_for_export])
end end
it 'should have headers' do it 'should have headers' do
@ -247,7 +333,18 @@ describe ProcedureExportV2Service do
end end
it 'should have valid sheet name' do it 'should have valid sheet name' do
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', "A - B - C"]) expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', "(#{champ_repetition.type_de_champ.stable_id}) A - B - C"])
end
end
context 'with non unique labels' do
let(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :for_individual, procedure: procedure) }
let(:champ_repetition) { dossier.champs.find { |champ| champ.type_champ == 'repetition' } }
let(:type_de_champ_repetition) { create(:type_de_champ_repetition, procedure: procedure, libelle: champ_repetition.libelle) }
let!(:another_champ_repetition) { create(:champ_repetition, type_de_champ: type_de_champ_repetition, dossier: dossier) }
it 'should have sheets' do
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle_for_export, another_champ_repetition.libelle_for_export])
end end
end end
end end