Merge pull request #4544 from betagouv/dev

2019-11-20-01
This commit is contained in:
Paul Chavard 2019-11-20 11:13:17 +01:00 committed by GitHub
commit 4faec089e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1255 additions and 332 deletions

View file

@ -13,12 +13,9 @@ gem 'bcrypt'
gem 'bootstrap-sass', '>= 3.4.1'
gem 'bootstrap-wysihtml5-rails', '~> 0.3.3.8'
gem 'browser'
gem 'carrierwave'
gem 'carrierwave-i18n'
gem 'chartkick'
gem 'chunky_png'
gem 'clamav-client', require: 'clamav/client'
gem 'copy_carrierwave_file'
gem 'daemons'
gem 'deep_cloneable' # Enable deep clone of active record models
gem 'delayed_cron_job' # Cron jobs

View file

@ -20,25 +20,25 @@ GEM
specs:
aasm (5.0.1)
concurrent-ruby (~> 1.0)
actioncable (5.2.2.1)
actionpack (= 5.2.2.1)
actioncable (5.2.3)
actionpack (= 5.2.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailer (5.2.2.1)
actionpack (= 5.2.2.1)
actionview (= 5.2.2.1)
activejob (= 5.2.2.1)
actionmailer (5.2.3)
actionpack (= 5.2.3)
actionview (= 5.2.3)
activejob (= 5.2.3)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.2.1)
actionview (= 5.2.2.1)
activesupport (= 5.2.2.1)
actionpack (5.2.3)
actionview (= 5.2.3)
activesupport (= 5.2.3)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.2.1)
activesupport (= 5.2.2.1)
actionview (5.2.3)
activesupport (= 5.2.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -51,25 +51,25 @@ GEM
activemodel (>= 4.1, < 6)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (5.2.2.1)
activesupport (= 5.2.2.1)
activejob (5.2.3)
activesupport (= 5.2.3)
globalid (>= 0.3.6)
activemodel (5.2.2.1)
activesupport (= 5.2.2.1)
activerecord (5.2.2.1)
activemodel (= 5.2.2.1)
activesupport (= 5.2.2.1)
activemodel (5.2.3)
activesupport (= 5.2.3)
activerecord (5.2.3)
activemodel (= 5.2.3)
activesupport (= 5.2.3)
arel (>= 9.0)
activestorage (5.2.2.1)
actionpack (= 5.2.2.1)
activerecord (= 5.2.2.1)
activestorage (5.2.3)
actionpack (= 5.2.3)
activerecord (= 5.2.3)
marcel (~> 0.3.1)
activestorage-openstack (1.0.0)
activestorage-openstack (1.2.0)
fog-openstack (~> 1.0)
marcel
mime-types
rails (<= 6)
activesupport (5.2.2.1)
rails (>= 5.2.2)
activesupport (5.2.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -136,11 +136,6 @@ GEM
capybara-selenium (0.0.6)
capybara
selenium-webdriver
carrierwave (1.3.1)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
carrierwave-i18n (0.2.0)
case_transform (0.2)
activesupport
chartkick (3.2.0)
@ -158,8 +153,6 @@ GEM
coffee-script-source (1.12.2)
concurrent-ruby (1.1.5)
connection_pool (2.2.2)
copy_carrierwave_file (1.3.0)
carrierwave (>= 0.9)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.5)
@ -203,7 +196,7 @@ GEM
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
erubi (1.8.0)
erubi (1.9.0)
erubis (2.7.0)
ethon (0.11.0)
ffi (>= 1.3.0)
@ -311,7 +304,7 @@ GEM
domain_name (~> 0.5)
http_parser.rb (0.6.0)
httpclient (2.8.3)
i18n (1.6.0)
i18n (1.7.0)
concurrent-ruby (~> 1.0)
ipaddress (0.8.3)
jaro_winkler (1.5.2)
@ -320,7 +313,7 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.2.0)
json-jwt (1.10.0)
json-jwt (1.11.0)
activesupport (>= 4.2)
aes_key_wrap
bindata
@ -375,7 +368,7 @@ GEM
mimemagic (0.3.3)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.11.3)
minitest (5.13.0)
momentjs-rails (2.20.1)
railties (>= 3.1)
multi_json (1.14.1)
@ -384,7 +377,7 @@ GEM
mustermann (1.0.3)
nenv (0.3.0)
netrc (0.11.0)
nio4r (2.3.1)
nio4r (2.5.2)
nokogiri (1.10.5)
mini_portile2 (~> 2.4.0)
notiffany (0.1.1)
@ -469,18 +462,18 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.2.1)
actioncable (= 5.2.2.1)
actionmailer (= 5.2.2.1)
actionpack (= 5.2.2.1)
actionview (= 5.2.2.1)
activejob (= 5.2.2.1)
activemodel (= 5.2.2.1)
activerecord (= 5.2.2.1)
activestorage (= 5.2.2.1)
activesupport (= 5.2.2.1)
rails (5.2.3)
actioncable (= 5.2.3)
actionmailer (= 5.2.3)
actionpack (= 5.2.3)
actionview (= 5.2.3)
activejob (= 5.2.3)
activemodel (= 5.2.3)
activerecord (= 5.2.3)
activestorage (= 5.2.3)
activesupport (= 5.2.3)
bundler (>= 1.3.0)
railties (= 5.2.2.1)
railties (= 5.2.3)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4)
actionpack (>= 5.0.1.x)
@ -489,14 +482,14 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.2.0)
loofah (~> 2.2, >= 2.2.2)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails-i18n (5.1.2)
i18n (>= 0.7, < 2)
railties (>= 5.0, < 6)
railties (5.2.2.1)
actionpack (= 5.2.2.1)
activesupport (= 5.2.2.1)
railties (5.2.3)
actionpack (= 5.2.3)
activesupport (= 5.2.3)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
@ -693,9 +686,9 @@ GEM
activesupport (>= 4.2)
rack-proxy (>= 0.6.1)
railties (>= 4.2)
websocket-driver (0.7.0)
websocket-driver (0.7.1)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
websocket-extensions (0.1.4)
xpath (3.2.0)
nokogiri (~> 1.8)
xray-rails (0.3.1)
@ -730,12 +723,9 @@ DEPENDENCIES
capybara-email
capybara-screenshot
capybara-selenium
carrierwave
carrierwave-i18n
chartkick
chunky_png
clamav-client
copy_carrierwave_file
daemons
database_cleaner
deep_cloneable

View file

@ -1,3 +1,4 @@
@import "colors";
@import "constants";
%horizontal-list {
@ -17,3 +18,10 @@
animation-fill-mode: forwards;
animation-duration: 0.3s;
}
%outline {
&:active,
&:focus {
outline: 3px solid $blue;
}
}

View file

@ -0,0 +1,38 @@
@import "constants";
@import "colors";
.instructeur-wrapper {
.select-instructeurs {
width: 100%;
}
.select2-container--default {
.select2-selection--multiple {
border: solid 1px $border-grey;
.select2-selection__choice, // scss-lint:disable SelectorFormat
.select2-search--inline {
padding: $default-spacer;
}
}
&.select2-container--focus {
.select2-selection--multiple {
border: 1px solid $blue;
box-shadow: 0px 0px 2px 1px $blue;
}
}
.select2-results__option { // scss-lint:disable SelectorFormat
padding: $default-spacer;
}
.custom-select2-option {
.icon {
margin-right: $default-spacer;
}
}
}
}

View file

@ -1,7 +1,10 @@
@import "colors";
@import "constants";
@import "placeholders";
.button {
@extend %outline;
display: inline-block;
padding: 8px 16px;
border-radius: 30px;
@ -20,11 +23,6 @@
text-decoration: none;
}
&:active,
&:focus {
outline: none;
}
&:disabled {
opacity: 0.5;
filter: saturate(50%);

View file

@ -40,6 +40,10 @@
.card-title {
margin-bottom: $default-spacer;
}
b {
font-weight: bold;
}
}
p:not(:last-of-type) {

View file

@ -1,3 +1,6 @@
@import "colors";
@import "placeholders";
html,
body {
height: 100%;
@ -14,5 +17,7 @@ html {
}
a {
@extend %outline;
text-decoration: none;
}

View file

@ -1,5 +1,6 @@
@import "constants";
@import "colors";
@import "placeholders";
.form {
h1 {
@ -177,6 +178,8 @@
input[type=checkbox],
input[type=radio] {
@extend %outline;
margin-left: 5px;
margin-right: 4px;
margin-bottom: 2 * $default-padding;

View file

@ -365,6 +365,11 @@ $cta-panel-button-border-size: 2px;
color: #FFFFFF;
text-decoration: none;
}
&:active,
&:focus {
outline: 3px solid #FFFFFF;
}
}
.cta-panel-button-blue {

View file

@ -141,6 +141,8 @@ class ApplicationController < ActionController::Base
Raven.user_context(sentry_user)
end
# private method called by rails fwk
# see https://github.com/roidrage/lograge
def append_info_to_payload(payload)
super

View file

@ -14,7 +14,7 @@ module Instructeurs
end
def add_instructeur
@instructeur = Instructeur.find_by(email: instructeur_email) ||
@instructeur = Instructeur.by_email(instructeur_email) ||
create_instructeur(instructeur_email)
if groupe_instructeur.instructeurs.include?(@instructeur)
@ -24,7 +24,7 @@ module Instructeurs
groupe_instructeur.instructeurs << @instructeur
flash[:notice] = "Linstructeur « #{instructeur_email} » a été affecté au groupe."
GroupeInstructeurMailer
.add_instructeur(groupe_instructeur, @instructeur, current_user.email)
.add_instructeurs(groupe_instructeur, [@instructeur], current_user.email)
.deliver_later
end

View file

@ -19,5 +19,29 @@ module Manager
redirect_to manager_sign_in_path
end
end
private
# private method called by rails fwk
# see https://github.com/roidrage/lograge
def append_info_to_payload(payload)
super
payload.merge!({
user_agent: request.user_agent,
user_id: current_user&.id,
user_email: current_user&.email
}.compact)
if browser.known?
payload.merge!({
browser: browser.name,
browser_version: browser.version.to_s,
platform: browser.platform.name
})
end
payload
end
end
end

View file

@ -12,6 +12,7 @@ module NewAdministrateur
@procedure = procedure
@groupe_instructeur = groupe_instructeur
@instructeurs = paginated_instructeurs
@available_instructeur_emails = available_instructeur_emails
end
def create
@ -40,6 +41,7 @@ module NewAdministrateur
else
@procedure = procedure
@instructeurs = paginated_instructeurs
@available_instructeur_emails = available_instructeur_emails
flash[:alert] = "le nom « #{label} » est déjà pris par un autre groupe."
render :show
@ -47,18 +49,35 @@ module NewAdministrateur
end
def add_instructeur
@instructeur = Instructeur.find_by(email: instructeur_email) ||
create_instructeur(instructeur_email)
emails = params['emails'].map(&:strip).map(&:downcase)
if groupe_instructeur.instructeurs.include?(@instructeur)
flash[:alert] = "Linstructeur « #{instructeur_email} » est déjà dans le groupe."
correct_emails, bad_emails = emails
.partition { |email| URI::MailTo::EMAIL_REGEXP.match?(email) }
if bad_emails.present?
flash[:alert] = t('.wrong_address',
count: bad_emails.count,
value: bad_emails.join(', '))
end
email_to_adds = correct_emails - groupe_instructeur.instructeurs.pluck(:email)
if email_to_adds.present?
instructeurs = email_to_adds.map do |instructeur_email|
Instructeur.by_email(instructeur_email) ||
create_instructeur(instructeur_email)
end
else
groupe_instructeur.instructeurs << @instructeur
flash[:notice] = "Linstructeur « #{instructeur_email} » a été affecté au groupe."
GroupeInstructeurMailer
.add_instructeur(groupe_instructeur, @instructeur, current_user.email)
.add_instructeurs(groupe_instructeur, instructeurs, current_user.email)
.deliver_later
groupe_instructeur.instructeurs << instructeurs
flash[:notice] = t('.assignment',
count: email_to_adds.count,
value: email_to_adds.join(', '),
groupe: groupe_instructeur.label)
end
redirect_to procedure_groupe_instructeur_path(procedure, groupe_instructeur)
@ -110,10 +129,6 @@ module NewAdministrateur
procedure.groupe_instructeurs.find(params[:id])
end
def instructeur_email
params[:instructeur][:email].strip.downcase
end
def instructeur_id
params[:instructeur][:id]
end
@ -141,5 +156,11 @@ module NewAdministrateur
def routing_criteria_name
params[:procedure][:routing_criteria_name]
end
def available_instructeur_emails
all = current_administrateur.instructeurs.pluck(:email)
assigned = groupe_instructeur.instructeurs.pluck(:email)
(all - assigned).sort
end
end
end

View file

@ -18,8 +18,7 @@ class ServiceDashboard < Administrate::BaseDashboard
email: Field::String,
telephone: Field::String,
horaires: Field::String,
adresse: Field::String,
siret: Field::String
adresse: Field::String
}.freeze
# COLLECTION_ATTRIBUTES
@ -45,8 +44,7 @@ class ServiceDashboard < Administrate::BaseDashboard
:email,
:telephone,
:horaires,
:adresse,
:siret
:adresse
].freeze
# FORM_ATTRIBUTES

View file

@ -0,0 +1,29 @@
module Mutations
class DossierAccepter < Mutations::BaseMutation
include DossierHelper
description "Accepter le dossier."
argument :dossier_id, ID, "Dossier ID", required: true, loads: Types::DossierType
argument :instructeur_id, ID, "Instructeur qui prend la décision sur le dossier.", required: true, loads: Types::ProfileType
argument :motivation, String, required: false
argument :justificatif, ID, required: false
field :dossier, Types::DossierType, null: true
field :errors, [Types::ValidationErrorType], null: true
def resolve(dossier:, instructeur:, motivation: nil, justificatif: nil)
if dossier.en_instruction?
dossier.accepter!(instructeur, motivation, justificatif)
{ dossier: dossier }
else
{ errors: ["Le dossier est déjà #{dossier_display_state(dossier, lower: true)}"] }
end
end
def authorized?(dossier:, instructeur:, motivation: nil)
instructeur.is_a?(Instructeur) && instructeur.dossiers.exists?(id: dossier.id)
end
end
end

View file

@ -0,0 +1,29 @@
module Mutations
class DossierClasserSansSuite < Mutations::BaseMutation
include DossierHelper
description "Classer le dossier sans suite."
argument :dossier_id, ID, "Dossier ID", required: true, loads: Types::DossierType
argument :instructeur_id, ID, "Instructeur qui prend la décision sur le dossier.", required: true, loads: Types::ProfileType
argument :motivation, String, required: true
argument :justificatif, ID, required: false
field :dossier, Types::DossierType, null: true
field :errors, [Types::ValidationErrorType], null: true
def resolve(dossier:, instructeur:, motivation:, justificatif: nil)
if dossier.en_instruction?
dossier.classer_sans_suite!(instructeur, motivation, justificatif)
{ dossier: dossier }
else
{ errors: ["Le dossier est déjà #{dossier_display_state(dossier, lower: true)}"] }
end
end
def authorized?(dossier:, instructeur:, motivation:)
instructeur.is_a?(Instructeur) && instructeur.dossiers.exists?(id: dossier.id)
end
end
end

View file

@ -0,0 +1,27 @@
module Mutations
class DossierEnvoyerMessage < Mutations::BaseMutation
description "Envoyer un message à l'usager du dossier."
argument :dossier_id, ID, required: true, loads: Types::DossierType
argument :instructeur_id, ID, required: true, loads: Types::ProfileType
argument :body, String, required: true
argument :attachment, ID, required: false
field :message, Types::MessageType, null: true
field :errors, [Types::ValidationErrorType], null: true
def resolve(dossier:, instructeur:, body:, attachment: nil)
message = CommentaireService.build(instructeur, dossier, body: body, piece_jointe: attachment)
if message.save
{ message: message }
else
{ errors: message.errors.full_messages }
end
end
def authorized?(dossier:, instructeur:, body:)
instructeur.is_a?(Instructeur) && instructeur.dossiers.exists?(id: dossier.id)
end
end
end

View file

@ -0,0 +1,27 @@
module Mutations
class DossierPasserEnInstruction < Mutations::BaseMutation
include DossierHelper
description "Passer le dossier en instruction."
argument :dossier_id, ID, "Dossier ID", required: true, loads: Types::DossierType
argument :instructeur_id, ID, "Instructeur qui prend la décision sur le dossier.", required: true, loads: Types::ProfileType
field :dossier, Types::DossierType, null: true
field :errors, [Types::ValidationErrorType], null: true
def resolve(dossier:, instructeur:)
if dossier.en_construction?
dossier.passer_en_instruction!(instructeur)
{ dossier: dossier }
else
{ errors: ["Le dossier est déjà #{dossier_display_state(dossier, lower: true)}"] }
end
end
def authorized?(dossier:, instructeur:)
instructeur.is_a?(Instructeur) && instructeur.dossiers.exists?(id: dossier.id)
end
end
end

View file

@ -0,0 +1,29 @@
module Mutations
class DossierRefuser < Mutations::BaseMutation
include DossierHelper
description "Refuser le dossier."
argument :dossier_id, ID, "Dossier ID", required: true, loads: Types::DossierType
argument :instructeur_id, ID, "Instructeur qui prend la décision sur le dossier.", required: true, loads: Types::ProfileType
argument :motivation, String, required: true
argument :justificatif, ID, required: false
field :dossier, Types::DossierType, null: true
field :errors, [Types::ValidationErrorType], null: true
def resolve(dossier:, instructeur:, motivation:, justificatif: nil)
if dossier.en_instruction?
dossier.refuser!(instructeur, motivation, justificatif)
{ dossier: dossier }
else
{ errors: ["Le dossier est déjà #{dossier_display_state(dossier, lower: true)}"] }
end
end
def authorized?(dossier:, instructeur:, motivation:)
instructeur.is_a?(Instructeur) && instructeur.dossiers.exists?(id: dossier.id)
end
end
end

View file

@ -10,27 +10,66 @@ type Avis {
type CarteChamp implements Champ {
geoAreas: [GeoArea!]!
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
}
interface Champ {
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
}
type ChampDescriptor {
"""
Description du champ.
"""
description: String
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
Est-ce que le champ est obligatoire ?
"""
required: Boolean!
"""
Type de la valeur du champ.
"""
type: TypeDeChamp!
}
type CheckboxChamp implements Champ {
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
value: Boolean!
}
@ -88,14 +127,30 @@ type CreateDirectUploadPayload {
type DateChamp implements Champ {
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
value: ISO8601DateTime
}
type DecimalNumberChamp implements Champ {
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
value: Float
}
@ -164,7 +219,11 @@ type Demarche {
"""
Le numero de la démarche.
"""
number: ID!
number: Int!
"""
L'état de la démarche.
"""
state: DemarcheState!
title: String!
updatedAt: ISO8601DateTime!
@ -244,7 +303,7 @@ type Dossier {
"""
Le numero du dossier.
"""
number: ID!
number: Int!
"""
L'état du dossier.
@ -258,6 +317,74 @@ type Dossier {
usager: Profile!
}
"""
Autogenerated input type of DossierAccepter
"""
input DossierAccepterInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Dossier ID
"""
dossierId: ID!
"""
Instructeur qui prend la décision sur le dossier.
"""
instructeurId: ID!
justificatif: ID
motivation: String
}
"""
Autogenerated return type of DossierAccepter
"""
type DossierAccepterPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
dossier: Dossier
errors: [ValidationError!]
}
"""
Autogenerated input type of DossierClasserSansSuite
"""
input DossierClasserSansSuiteInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Dossier ID
"""
dossierId: ID!
"""
Instructeur qui prend la décision sur le dossier.
"""
instructeurId: ID!
justificatif: ID
motivation: String!
}
"""
Autogenerated return type of DossierClasserSansSuite
"""
type DossierClasserSansSuitePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
dossier: Dossier
errors: [ValidationError!]
}
"""
The connection type for Dossier.
"""
@ -293,13 +420,114 @@ type DossierEdge {
node: Dossier
}
"""
Autogenerated input type of DossierEnvoyerMessage
"""
input DossierEnvoyerMessageInput {
attachment: ID
body: String!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
dossierId: ID!
instructeurId: ID!
}
"""
Autogenerated return type of DossierEnvoyerMessage
"""
type DossierEnvoyerMessagePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
errors: [ValidationError!]
message: Message
}
type DossierLinkChamp implements Champ {
dossier: Dossier
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
}
"""
Autogenerated input type of DossierPasserEnInstruction
"""
input DossierPasserEnInstructionInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Dossier ID
"""
dossierId: ID!
"""
Instructeur qui prend la décision sur le dossier.
"""
instructeurId: ID!
}
"""
Autogenerated return type of DossierPasserEnInstruction
"""
type DossierPasserEnInstructionPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
dossier: Dossier
errors: [ValidationError!]
}
"""
Autogenerated input type of DossierRefuser
"""
input DossierRefuserInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Dossier ID
"""
dossierId: ID!
"""
Instructeur qui prend la décision sur le dossier.
"""
instructeurId: ID!
justificatif: ID
motivation: String!
}
"""
Autogenerated return type of DossierRefuser
"""
type DossierRefuserPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
dossier: Dossier
errors: [ValidationError!]
}
enum DossierState {
"""
Accepté
@ -335,22 +563,17 @@ interface GeoArea {
enum GeoAreaSource {
"""
translation missing: fr.activerecord.attributes.geo_area.source.cadastre
Parcelle cadastrale
"""
cadastre
"""
translation missing: fr.activerecord.attributes.geo_area.source.parcelle_agricole
"""
parcelle_agricole
"""
translation missing: fr.activerecord.attributes.geo_area.source.quartier_prioritaire
Quartier prioritaire
"""
quartier_prioritaire
"""
translation missing: fr.activerecord.attributes.geo_area.source.selection_utilisateur
Sélection utilisateur
"""
selection_utilisateur
}
@ -376,16 +599,32 @@ scalar ISO8601DateTime
type IntegerNumberChamp implements Champ {
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
value: Int
}
type LinkedDropDownListChamp implements Champ {
id: ID!
"""
Libellé du champ.
"""
label: String!
primaryValue: String
secondaryValue: String
"""
La valeur du champ sous forme texte.
"""
stringValue: String
}
@ -399,7 +638,15 @@ type Message {
type MultipleDropDownListChamp implements Champ {
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
values: [String!]!
}
@ -409,6 +656,31 @@ type Mutation {
File information required to prepare a direct upload
"""
createDirectUpload(input: CreateDirectUploadInput!): CreateDirectUploadPayload
"""
Accepter le dossier.
"""
dossierAccepter(input: DossierAccepterInput!): DossierAccepterPayload
"""
Classer le dossier sans suite.
"""
dossierClasserSansSuite(input: DossierClasserSansSuiteInput!): DossierClasserSansSuitePayload
"""
Envoyer un message à l'usager du dossier.
"""
dossierEnvoyerMessage(input: DossierEnvoyerMessageInput!): DossierEnvoyerMessagePayload
"""
Passer le dossier en instruction.
"""
dossierPasserEnInstruction(input: DossierPasserEnInstructionInput!): DossierPasserEnInstructionPayload
"""
Refuser le dossier.
"""
dossierRefuser(input: DossierRefuserInput!): DossierRefuserPayload
}
enum Order {
@ -480,7 +752,15 @@ type PersonneMorale {
type PieceJustificativeChamp implements Champ {
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
url: URL
}
@ -507,7 +787,7 @@ type Query {
"""
Numéro de la démarche.
"""
number: ID!
number: Int!
): Demarche!
"""
@ -517,14 +797,22 @@ type Query {
"""
Numéro du dossier.
"""
number: ID!
number: Int!
): Dossier!
}
type RepetitionChamp implements Champ {
champs: [Champ!]!
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
}
@ -537,13 +825,29 @@ type SelectionUtilisateur implements GeoArea {
type SiretChamp implements Champ {
etablissement: PersonneMorale
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
}
type TextChamp implements Champ {
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
value: String
}
@ -688,4 +992,14 @@ enum TypeDeChamp {
"""
A valid URL, transported as a string
"""
scalar URL
scalar URL
"""
Éreur de validation
"""
type ValidationError {
"""
A description of the error
"""
message: String!
}

View file

@ -9,9 +9,9 @@ module Types
end
global_id_field :id
field :type, TypeDeChampType, null: false, method: :type_champ
field :label, String, null: false, method: :libelle
field :description, String, null: true
field :required, Boolean, null: false, method: :mandatory?
field :type, TypeDeChampType, "Type de la valeur du champ.", null: false, method: :type_champ
field :label, String, "Libellé du champ.", null: false, method: :libelle
field :description, String, "Description du champ.", null: true
field :required, Boolean, "Est-ce que le champ est obligatoire ?", null: false, method: :mandatory?
end
end

View file

@ -3,8 +3,8 @@ module Types
include Types::BaseInterface
global_id_field :id
field :label, String, null: false, method: :libelle
field :string_value, String, null: true, method: :for_api_v2
field :label, String, "Libellé du champ.", null: false, method: :libelle
field :string_value, String, "La valeur du champ sous forme texte.", null: true, method: :for_api_v2
definition_methods do
def resolve_type(object, context)

View file

@ -9,10 +9,10 @@ module Types
description "Une demarche"
global_id_field :id
field :number, ID, "Le numero de la démarche.", null: false, method: :id
field :number, Int, "Le numero de la démarche.", null: false, method: :id
field :title, String, null: false, method: :libelle
field :description, String, "Déscription de la démarche.", null: false
field :state, DemarcheState, null: false
field :state, DemarcheState, "L'état de la démarche.", null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false

View file

@ -9,7 +9,7 @@ module Types
description "Un dossier"
global_id_field :id
field :number, ID, "Le numero du dossier.", null: false, method: :id
field :number, Int, "Le numero du dossier.", null: false, method: :id
field :state, DossierState, "L'état du dossier.", null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, "Date de dernière mise à jour.", null: false

View file

@ -4,9 +4,11 @@ module Types
class GeoAreaSource < Types::BaseEnum
GeoArea.sources.each do |symbol_name, string_name|
value(string_name,
I18n.t(symbol_name, scope: [:activerecord, :attributes, :geo_area, :source]),
value: symbol_name)
if string_name != "parcelle_agricole"
value(string_name,
I18n.t(symbol_name, scope: [:activerecord, :attributes, :geo_area, :source]),
value: symbol_name)
end
end
end

View file

@ -1,5 +1,11 @@
module Types
class MutationType < Types::BaseObject
field :create_direct_upload, mutation: Mutations::CreateDirectUpload
field :dossier_envoyer_message, mutation: Mutations::DossierEnvoyerMessage
field :dossier_passer_en_instruction, mutation: Mutations::DossierPasserEnInstruction
field :dossier_classer_sans_suite, mutation: Mutations::DossierClasserSansSuite
field :dossier_refuser, mutation: Mutations::DossierRefuser
field :dossier_accepter, mutation: Mutations::DossierAccepter
end
end

View file

@ -1,11 +1,11 @@
module Types
class QueryType < Types::BaseObject
field :demarche, DemarcheType, null: false, description: "Informations concernant une démarche." do
argument :number, ID, "Numéro de la démarche.", required: true
argument :number, Int, "Numéro de la démarche.", required: true
end
field :dossier, DossierType, null: false, description: "Informations sur un dossier d'une démarche." do
argument :number, ID, "Numéro du dossier.", required: true
argument :number, Int, "Numéro du dossier.", required: true
end
def demarche(number:)

View file

@ -0,0 +1,11 @@
module Types
class ValidationErrorType < Types::BaseObject
description "Éreur de validation"
field :message, String, "A description of the error", null: false
def message
object
end
end
end

View file

@ -1,6 +1,13 @@
import $ from 'jquery';
import 'select2';
const optionTemplate = email =>
$(
'<span class="custom-select2-option"><span class="icon person"></span>' +
email.text +
'</span>'
);
addEventListener('ds:page:update', () => {
$('select.select2').select2({
language: 'fr',
@ -20,4 +27,20 @@ addEventListener('ds:page:update', () => {
maximumSelectionLength: '30',
width: '300px'
});
$('select.select2-limited.select-instructeurs').select2({
language: 'fr',
dropdownParent: $('.instructeur-wrapper'),
placeholder: 'Saisir ladresse email de linstructeur',
tags: true,
tokenSeparators: [',', ' '],
templateResult: optionTemplate,
templateSelection: function(email) {
return $(
'<span class="custom-select2-option"><span class="icon person"></span>' +
email.text +
'</span>'
);
}
});
});

View file

@ -1,4 +0,0 @@
module ActiveStorage
# activestorage-openstack uses ActiveStorage::FileNotFoundError which only exists in rails 6
class FileNotFoundError < StandardError; end
end

View file

@ -1,9 +1,9 @@
class GroupeInstructeurMailer < ApplicationMailer
layout 'mailers/layout'
def add_instructeur(group, instructeur, current_instructeur_email)
@email = instructeur.email
@group = group
def add_instructeurs(group, instructeurs, current_instructeur_email)
@new_instructeur_emails = instructeurs.map(&:email)
@group = Procedure.last.defaut_groupe_instructeur
@current_instructeur_email = current_instructeur_email
subject = "Ajout dun instructeur dans le groupe \"#{group.label}\""

View file

@ -10,7 +10,7 @@ class Commentaire < ApplicationRecord
has_one_attached :piece_jointe
validates :body, presence: { message: "Votre message ne peut être vide" }
validates :body, presence: { message: "ne peut être vide" }
default_scope { order(created_at: :asc) }
scope :updated_since?, -> (date) { where('commentaires.updated_at > ?', date) }

View file

@ -10,7 +10,7 @@ class Individual < ApplicationRecord
GENDER_FEMALE = 'Mme'
def self.create_from_france_connect(fc_information)
create(
create!(
nom: fc_information.family_name,
prenom: fc_information.given_name,
gender: fc_information.gender == 'female' ? GENDER_FEMALE : GENDER_MALE

View file

@ -1,11 +0,0 @@
%p
Bonjour,
%p
Linstructeur « #{@email} » a été affecté au groupe « #{@group.label} » par « #{@current_instructeur_email} », en charge de la démarche « #{@group.procedure.libelle} ».
%p
Cliquez sur le lien ci-dessous pour voir la liste des instructeurs de ce groupe :
= link_to(@group.label, procedure_groupe_instructeur_url(@group.procedure, @group))
= render partial: "layouts/mailers/signature"

View file

@ -0,0 +1,11 @@
%p
Bonjour,
%p
#{t('new_administrateur.groupe_instructeurs.add_instructeur.assignment', count: @new_instructeur_emails.count, value: @new_instructeur_emails.join(', '), groupe: @group.label).chomp('.')} par « #{@current_instructeur_email} », en charge de la démarche « #{@group.procedure.libelle} ».
%p
Cliquez sur le lien ci-dessous pour voir la liste des instructeurs de ce groupe :
= link_to(@group.label, procedure_groupe_instructeur_url(@group.procedure, @group))
= render partial: "layouts/mailers/signature"

View file

@ -12,7 +12,7 @@
= f.label :routing_criteria_name do
Libellé du routage
%span.notice Ce texte apparaitra sur le formulaire usager comme le libellé d'une liste
= f.text_field :routing_criteria_name, placeholder: 'Votre ville', required: true
= f.text_field :routing_criteria_name, placeholder: 'ex. Votre ville', required: true
= f.submit 'Renommer', class: 'button primary send'
.card
@ -22,13 +22,13 @@
= f.label :label do
Ajouter un groupe
%span.notice Ce groupe sera un choix de la liste « #{@procedure.routing_criteria_name} » .
= f.text_field :label, placeholder: 'Ville de Bordeaux', required: true
= f.text_field :label, placeholder: 'ex. Ville de Bordeaux', required: true
= f.submit 'Ajouter le groupe', class: 'button primary send'
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 } Liste des groupes
%th{ colspan: 2 }= t(".existing_groupe", count: @groupes_instructeurs.count)
%tbody
- @groupes_instructeurs.each do |group|
%tr

View file

@ -17,24 +17,29 @@
= f.submit 'Renommer', class: 'button primary send'
.card
.card-title Gestion des instructeurs
.card-title Affectation des instructeurs
= form_for :instructeur,
url: { action: :add_instructeur },
html: { class: 'form' } do |f|
= f.label :email do
Affecter un nouvel instructeur
= f.email_field :email, placeholder: 'marie.dupont@exemple.fr', required: true
.instructeur-wrapper
= select_tag :emails,
options_for_select(@available_instructeur_emails),
multiple: true,
class: 'select-instructeurs select2-limited'
= f.submit 'Affecter', class: 'button primary send'
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 } Instructeurs affectés
%th{ colspan: 2 }= t('.assigned_instructeur', count: @instructeurs.count)
%tbody
- @instructeurs.each do |instructeur|
%tr
%td= instructeur.email
%td
%span.icon.person
#{instructeur.email}
%td.actions= button_to 'retirer',
{ action: :remove_instructeur },
{ method: :delete,

View file

@ -7,4 +7,4 @@
- if dossier.messagerie_available?
= render partial: "shared/dossiers/messages/form", locals: { commentaire: new_commentaire, form_url: form_url }
- else
= render partial: "shared/dossiers/messages/messagerie_disabled", locals: { service: dossier.procedure.service }
= render partial: "shared/dossiers/messages/messagerie_disabled", locals: { service: dossier.procedure.service, dossier: dossier }

View file

@ -12,4 +12,11 @@
- horaires = "Horaires : #{formatted_horaires(service.horaires)}"
= simple_format(horaires)
%p
= link_to service.email, "mailto:#{service.email}"
= mail_to service.email,
service.email,
subject: "[demarches-simplifiees.fr] Question sur le dossier Nº #{dossier.id} de la démarche Nº #{dossier.procedure.id}",
rel: "noopener noreferrer",
target: '_blank'
%p
Penser bien à préciser que votre demande concerne le <b>dossier Nº #{dossier.id}</b>.

View file

@ -23,7 +23,6 @@ FOG_OPENSTACK_IDENTITY_API_VERSION=""
FOG_OPENSTACK_REGION=""
FOG_DIRECTORY=""
FOG_ENABLED=""
CARRIERWAVE_CACHE_DIR="/tmp/tps-local-cache"
DS_PROXY_URL=""
FC_PARTICULIER_ID=""

View file

@ -40,7 +40,7 @@ features = [
def database_exists?
ActiveRecord::Base.connection
true
rescue ActiveRecord::NoDatabaseError
rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
false
end

View file

@ -0,0 +1,79 @@
DEFAULT_QUERY = "# La documentation officielle de la spécification (Anglais) : https://graphql.org/
# Une introduction aux concepts et raisons d'être de GraphQL (Français) : https://blog.octo.com/graphql-et-pourquoi-faire/
# Le schema GraphQL de demarches-simplifiees.fr : https://demarches-simplifiees-graphql.netlify.com
# Le endpoint GraphQL de demarches-simplifiees.fr : https://demarches-simplifiees.fr/api/v2/graphql
query getDemarche($demarcheNumber: Int!) {
demarche(number: $demarcheNumber) {
id
number
title
champDescriptors {
id
type
label
}
dossiers(first: 3) {
nodes {
id
number
datePassageEnConstruction
datePassageEnInstruction
dateTraitement
usager {
email
}
champs {
id
label
... on TextChamp {
value
}
... on DecimalNumberChamp {
value
}
... on IntegerNumberChamp {
value
}
... on CheckboxChamp {
value
}
... on DateChamp {
value
}
... on DossierLinkChamp {
dossier {
id
}
}
... on MultipleDropDownListChamp {
values
}
... on LinkedDropDownListChamp {
primaryValue
secondaryValue
}
... on PieceJustificativeChamp {
url
}
... on CarteChamp {
geoAreas {
source
geometry {
type
coordinates
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}"
GraphiQL::Rails.config.initial_query = DEFAULT_QUERY
GraphiQL::Rails.config.title = 'demarches-simplifiees.fr'

View file

@ -2,4 +2,5 @@ fr:
activerecord:
attributes:
commentaire:
body: 'Votre message'
file: fichier

View file

@ -0,0 +1,8 @@
fr:
activerecord:
attributes:
geo_area:
source:
cadastre: Parcelle cadastrale
quartier_prioritaire: Quartier prioritaire
selection_utilisateur: Sélection utilisateur

View file

@ -0,0 +1,18 @@
fr:
new_administrateur:
groupe_instructeurs:
index:
existing_groupe:
one: "%{count} groupe existe"
other: "%{count} groupes existent"
show:
assigned_instructeur:
one: "%{count} instructeur est affecté"
other: "%{count} instructeurs sont affectés"
add_instructeur:
wrong_address:
one: "%{value} n'est pas une adresse email valide"
other: "%{value} ne sont pas des adresses emails valides"
assignment:
one: "Linstructeur %{value} a été affecté au groupe « %{groupe} »."
other: "Les instructeurs %{value} ont été affectés au groupe « %{groupe} »."

View file

@ -42,8 +42,6 @@ defaults: &defaults
openstack_identity_api_version: "<%= ENV['FOG_OPENSTACK_IDENTITY_API_VERSION'] %>"
openstack_region: <%= ENV['FOG_OPENSTACK_REGION'] %>
directory: <%= ENV['FOG_DIRECTORY'] %>
carrierwave:
cache_dir: <%= ENV['CARRIERWAVE_CACHE_DIR'] %>
mailtrap:
username: <%= ENV['MAILTRAP_USERNAME'] %>
password: <%= ENV['MAILTRAP_PASSWORD'] %>
@ -84,8 +82,6 @@ test:
key: api_entreprise_test_key
fog:
directory: tps_dev
carrierwave:
cache_dir: /tmp/tps-test-cache
pipedrive:
key: pipedrive_test_key
france_connect_particulier:

View file

@ -0,0 +1,5 @@
class InstructeursRemoveEmail < ActiveRecord::Migration[5.2]
def change
remove_column :instructeurs, :email
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_10_24_150452) do
ActiveRecord::Schema.define(version: 2019_11_13_142816) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -414,12 +414,10 @@ ActiveRecord::Schema.define(version: 2019_10_24_150452) do
end
create_table "instructeurs", id: :serial, force: :cascade do |t|
t.string "email", default: "", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.text "encrypted_login_token"
t.datetime "login_token_created_at"
t.index ["email"], name: "index_instructeurs_on_email"
end
create_table "invites", id: :serial, force: :cascade do |t|

View file

@ -1,51 +0,0 @@
require Rails.root.join("lib", "tasks", "task_helper")
namespace :'2017_10_30_copy_commentaire_piece_justificative_to_file' do
task set: :environment do
commentaires_to_process = Commentaire.where(file: nil).where.not(piece_justificative_id: nil).reorder(id: :desc)
rake_puts "#{commentaires_to_process.count} commentaires to process..."
commentaires_to_process.each do |c|
process_commentaire(c)
end
end
task fix: :environment do
commentaires_to_fix = Commentaire.where.not(file: nil).where.not(piece_justificative_id: nil).reorder(id: :desc)
rake_puts "#{commentaires_to_fix.count} commentaires to fix..."
commentaires_to_fix.each do |c|
process_commentaire(c)
end
end
def sanitize_name(name) # from https://github.com/carrierwaveuploader/carrierwave/blob/master/lib/carrierwave/sanitized_file.rb#L323
name = name.gsub(/[^[:word:]\.\-\+]/, "_")
name = "_#{name}" if name.match?(/\A\.+\z/)
name = "unnamed" if name.empty?
return name.mb_chars.to_s
end
def process_commentaire(commentaire)
rake_puts "Processing commentaire #{commentaire.id}"
if commentaire.piece_justificative.present?
# https://github.com/carrierwaveuploader/carrierwave#uploading-files-from-a-remote-location
commentaire.remote_file_url = commentaire.piece_justificative.content_url
if commentaire.piece_justificative.original_filename.present?
commentaire.file.define_singleton_method(:filename) { sanitize_name(commentaire.piece_justificative.original_filename) }
end
if commentaire.body.blank?
commentaire.body = commentaire.piece_justificative.original_filename || "."
end
commentaire.save
if commentaire.file.blank?
rake_puts "Failed to save file for commentaire #{commentaire.id}"
end
end
end
end

View file

@ -1,113 +0,0 @@
require Rails.root.join("lib", "tasks", "task_helper")
namespace :cloudstorage do
task init: :environment do
os_config = (YAML.load_file(Fog.credentials_path))['default']
@os = OpenStack::Connection.create(
{
username: os_config['openstack_username'],
api_key: os_config['openstack_api_key'],
auth_method: "password",
auth_url: "https://auth.cloud.ovh.net/v2.0/",
authtenant_name: os_config['openstack_tenant'],
service_type: "object-store",
region: os_config['openstack_region']
}
)
@cont = @os.container(CarrierWave::Uploader::Base.fog_directory)
end
desc 'Move local attestations on cloud storage'
task migrate: :environment do
puts 'Starting migration'
Rake::Task['cloudstorage:init'].invoke
error_count = 0
[Cerfa, PieceJustificative, Procedure].each do |c|
c.all.each do |entry|
content = (c == Procedure) ? entry.logo : entry.content
if !(content.current_path.nil? || File.exist?(File.dirname(content.current_path) + '/uploaded'))
secure_token = SecureRandom.uuid
filename = "#{entry.class.to_s.underscore}-#{secure_token}#{File.extname(content.current_path)}"
rake_puts "Uploading #{content.current_path}"
begin
@cont.create_object(filename, {}, File.open(content.current_path))
File.open(File.dirname(content.current_path) + '/uploaded', "w+") { |f| f.write(File.basename(content.current_path)) }
File.open(File.dirname(content.current_path) + '/filename_cloudstorage', "w+") { |f| f.write(filename) }
File.open(File.dirname(content.current_path) + '/secure_token_cloudstorage', "w+") { |f| f.write(secure_token) }
entry.update_column(c == Procedure ? :logo : :content, filename)
entry.update_column(c == Procedure ? :logo_secure_token : :content_secure_token, secure_token)
rescue Errno::ENOENT
rake_puts "ERROR: #{content.current_path} does not exist!"
File.open('upload_errors.report', "a+") { |f| f.write(content.current_path) }
error_count += 1
end
else
if content.current_path.present? && File.exist?(File.dirname(content.current_path) + '/uploaded')
filename = File.open(File.dirname(content.current_path) + '/filename_cloudstorage', "r").read
secure_token = File.open(File.dirname(content.current_path) + '/secure_token_cloudstorage', "r").read
entry.update_column(c == Procedure ? :logo : :content, filename)
entry.update_column(c == Procedure ? :logo_secure_token : :content_secure_token, secure_token)
rake_puts "RESTORE IN DATABASE: #{filename} "
elsif content.current_path.present?
rake_puts "Skipping #{content.current_path}"
end
end
end
end
rake_puts "There were #{error_count} errors while uploading files. See upload_errors.report file for details."
puts 'Enf of migration'
end
desc 'Clear documents in tenant and revert file entries in database'
task :revert do
Rake::Task['cloudstorage:init'].invoke
[Cerfa, PieceJustificative, Procedure].each do |c|
c.all.each do |entry|
content = (c == Procedure) ? entry.logo : entry.content
if content.current_path.present?
if File.exist?(File.dirname(content.current_path) + '/uploaded')
previous_filename = File.read(File.dirname(content.current_path) + '/uploaded')
entry.update_column(c == Procedure ? :logo : :content, previous_filename)
entry.update_column(c == Procedure ? :logo_secure_token : :content_secure_token, nil)
rake_puts "restoring #{content.current_path} db data to #{previous_filename}"
@cont.delete_object(File.open(File.dirname(content.current_path) + '/filename_cloudstorage', "r").read)
FileUtils.rm(File.dirname(content.current_path) + '/uploaded')
FileUtils.rm(File.dirname(content.current_path) + '/filename_cloudstorage')
FileUtils.rm(File.dirname(content.current_path) + '/secure_token_cloudstorage')
end
end
end
end
end
desc 'Clear old documents in tenant'
task :clear do
Rake::Task['cloudstorage:init'].invoke
@cont.objects.each do |object|
rake_puts "Removing #{object}"
@cont.delete_object(object)
end
end
task :clear_old_objects do
Rake::Task['cloudstorage:init'].invoke
@cont.objects_detail.each do |object, details|
last_modified = Time.zone.parse(details[:last_modified])
@cont.delete_object(object) if last_modified.utc <= (Time.zone.now - 2.years).utc
end
end
end

View file

@ -76,7 +76,7 @@ describe API::V2::GraphqlController do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(demarche: {
id: procedure.to_typed_id,
number: procedure.id.to_s,
number: procedure.id,
title: procedure.libelle,
description: procedure.description,
state: 'brouillon',
@ -123,7 +123,7 @@ describe API::V2::GraphqlController do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(demarche: {
id: procedure.to_typed_id,
number: procedure.id.to_s,
number: procedure.id,
dossiers: {
nodes: [{ id: dossier1.to_typed_id }, { id: dossier.to_typed_id }]
}
@ -177,7 +177,7 @@ describe API::V2::GraphqlController do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossier: {
id: dossier.to_typed_id,
number: dossier.id.to_s,
number: dossier.id,
state: 'en_construction',
updatedAt: dossier.updated_at.iso8601,
datePassageEnConstruction: dossier.en_construction_at.iso8601,
@ -216,7 +216,310 @@ describe API::V2::GraphqlController do
end
context "mutations" do
context 'createDirectUpload' do
describe 'dossierEnvoyerMessage' do
context 'success' do
let(:query) do
"mutation {
dossierEnvoyerMessage(input: {
dossierId: \"#{dossier.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
body: \"Bonjour\"
}) {
message {
body
}
}
}"
end
it "should post a message" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierEnvoyerMessage: {
message: {
body: "Bonjour"
}
})
end
end
context 'schema error' do
let(:query) do
"mutation {
dossierEnvoyerMessage(input: {
dossierId: \"#{dossier.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\"
}) {
message {
body
}
}
}"
end
it "should fail" do
expect(gql_data).to eq(nil)
expect(gql_errors).not_to eq(nil)
end
end
context 'validation error' do
let(:query) do
"mutation {
dossierEnvoyerMessage(input: {
dossierId: \"#{dossier.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
body: \"\"
}) {
message {
body
}
errors {
message
}
}
}"
end
it "should fail" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierEnvoyerMessage: {
errors: [{ message: "Votre message ne peut être vide" }],
message: nil
})
end
end
end
describe 'dossierPasserEnInstruction' do
let(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
let(:query) do
"mutation {
dossierPasserEnInstruction(input: {
dossierId: \"#{dossier.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\"
}) {
dossier {
id
state
motivation
}
errors {
message
}
}
}"
end
context 'success' do
it "should passer en instruction dossier" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierPasserEnInstruction: {
dossier: {
id: dossier.to_typed_id,
state: "en_instruction",
motivation: nil
},
errors: nil
})
end
end
context 'validation error' do
let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) }
it "should fail" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierPasserEnInstruction: {
errors: [{ message: "Le dossier est déjà en instruction" }],
dossier: nil
})
end
end
end
describe 'dossierClasserSansSuite' do
let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) }
let(:query) do
"mutation {
dossierClasserSansSuite(input: {
dossierId: \"#{dossier.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
motivation: \"Parce que\"
}) {
dossier {
id
state
motivation
}
errors {
message
}
}
}"
end
context 'success' do
it "should classer sans suite dossier" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierClasserSansSuite: {
dossier: {
id: dossier.to_typed_id,
state: "sans_suite",
motivation: "Parce que"
},
errors: nil
})
end
end
context 'validation error' do
let(:dossier) { create(:dossier, :accepte, procedure: procedure) }
it "should fail" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierClasserSansSuite: {
errors: [{ message: "Le dossier est déjà accepté" }],
dossier: nil
})
end
end
end
describe 'dossierRefuser' do
let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) }
let(:query) do
"mutation {
dossierRefuser(input: {
dossierId: \"#{dossier.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
motivation: \"Parce que\"
}) {
dossier {
id
state
motivation
}
errors {
message
}
}
}"
end
context 'success' do
it "should refuser dossier" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierRefuser: {
dossier: {
id: dossier.to_typed_id,
state: "refuse",
motivation: "Parce que"
},
errors: nil
})
end
end
context 'validation error' do
let(:dossier) { create(:dossier, :sans_suite, procedure: procedure) }
it "should fail" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierRefuser: {
errors: [{ message: "Le dossier est déjà sans suite" }],
dossier: nil
})
end
end
end
describe 'dossierAccepter' do
let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) }
let(:query) do
"mutation {
dossierAccepter(input: {
dossierId: \"#{dossier.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
motivation: \"Parce que\"
}) {
dossier {
id
state
motivation
}
errors {
message
}
}
}"
end
context 'success' do
it "should accepter dossier" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierAccepter: {
dossier: {
id: dossier.to_typed_id,
state: "accepte",
motivation: "Parce que"
},
errors: nil
})
end
end
context 'success without motivation' do
let(:query) do
"mutation {
dossierAccepter(input: {
dossierId: \"#{dossier.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\"
}) {
dossier {
id
state
motivation
}
errors {
message
}
}
}"
end
it "should accepter dossier" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierAccepter: {
dossier: {
id: dossier.to_typed_id,
state: "accepte",
motivation: nil
},
errors: nil
})
end
end
context 'validation error' do
let(:dossier) { create(:dossier, :refuse, procedure: procedure) }
it "should fail" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierAccepter: {
errors: [{ message: "Le dossier est déjà refusé" }],
dossier: nil
})
end
end
end
describe 'createDirectUpload' do
let(:query) do
"mutation {
createDirectUpload(input: {
@ -263,5 +566,26 @@ describe API::V2::GraphqlController do
expect(gql_errors).not_to eq(nil)
end
end
context "mutation" do
let(:query) do
"mutation {
dossierEnvoyerMessage(input: {
dossierId: \"#{dossier.to_typed_id}\",
instructeurId: \"#{instructeur.to_typed_id}\",
body: \"Bonjour\"
}) {
message {
body
}
}
}"
end
it "should return error" do
expect(gql_data[:dossierEnvoyerMessage]).to eq(nil)
expect(gql_errors).not_to eq(nil)
end
end
end
end

View file

@ -0,0 +1,22 @@
describe Manager::ApplicationController, type: :controller do
describe 'append_info_to_payload' do
let(:current_user) { create(:administration) }
let(:payload) { {} }
before do
allow(@controller).to receive(:current_user).and_return(current_user)
@controller.send(:append_info_to_payload, payload)
end
it do
[:db_runtime, :view_runtime, :variant, :rendered_format].each do |key|
payload.delete(key)
end
expect(payload).to eq({
user_agent: 'Rails Testing',
user_id: current_user.id,
user_email: current_user.email
})
end
end
end

View file

@ -93,20 +93,26 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
params: {
procedure_id: procedure.id,
id: gi_1_1.id,
instructeur: { email: new_instructeur_email }
emails: new_instructeur_emails
}
end
context 'of a new instructeur' do
let(:new_instructeur_email) { 'new_instructeur@mail.com' }
context 'of a news instructeurs' do
let(:new_instructeur_emails) { ['new_i1@mail.com', 'new_i2@mail.com'] }
it { expect(gi_1_1.instructeurs.map(&:email)).to include(new_instructeur_email) }
it { expect(gi_1_1.instructeurs.pluck(:email)).to include(*new_instructeur_emails) }
it { expect(flash.notice).to be_present }
it { expect(response).to redirect_to(procedure_groupe_instructeur_path(procedure, gi_1_1)) }
end
context 'of an instructeur already in the group' do
let(:new_instructeur_email) { instructeur.email }
let(:new_instructeur_emails) { [instructeur.email] }
it { expect(response).to redirect_to(procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur)) }
end
context 'of badly formed email' do
let(:new_instructeur_emails) { ['badly_formed_email'] }
it { expect(flash.alert).to be_present }
it { expect(response).to redirect_to(procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur)) }

View file

@ -1,13 +1,16 @@
require 'spec_helper'
feature 'The routing' do
feature 'The routing', js: true do
let(:password) { 'a very complicated password' }
let(:procedure) { create(:procedure, :with_type_de_champ, :with_service, :for_individual) }
let(:administrateur) { create(:administrateur, procedures: [procedure]) }
let(:scientifique_user) { create(:user, password: password) }
let(:litteraire_user) { create(:user, password: password) }
before { Flipper.enable_actor(:administrateur_routage, administrateur.user) }
before do
procedure.defaut_groupe_instructeur.instructeurs << administrateur.instructeur
Flipper.enable_actor(:administrateur_routage, administrateur.user)
end
scenario 'works' do
login_as administrateur.user, scope: :user
@ -22,11 +25,15 @@ feature 'The routing' do
# rename defaut groupe to littéraire
click_on 'voir'
fill_in 'groupe_instructeur_label', with: 'littéraire'
expect(page).to have_css('#groupe_instructeur_label')
2.times { find(:css, "#groupe_instructeur_label").set("littéraire") }
click_on 'Renommer'
expect(procedure.defaut_groupe_instructeur.reload.label).to eq('littéraire')
# add victor to littéraire groupe
fill_in 'instructeur_email', with: 'victor@inst.com'
try_twice { find('input.select2-search__field').send_keys('victor@inst.com', :enter) }
perform_enqueued_jobs { click_on 'Affecter' }
victor = User.find_by(email: 'victor@inst.com').instructeur
@ -37,13 +44,13 @@ feature 'The routing' do
click_on 'Ajouter le groupe'
# add marie to scientifique groupe
fill_in 'instructeur_email', with: 'marie@inst.com'
try_twice { find('input.select2-search__field').send_keys('marie@inst.com', :enter) }
perform_enqueued_jobs { click_on 'Affecter' }
marie = User.find_by(email: 'marie@inst.com').instructeur
# publish
publish_procedure(procedure)
log_out
log_out(old_layout: true)
# 2 users fill a dossier in each group
user_send_dossier(scientifique_user, 'scientifique')
@ -165,7 +172,25 @@ feature 'The routing' do
expect(page).to have_content 'Mot de passe enregistré'
end
def log_out
click_on 'Se déconnecter'
def log_out(old_layout: false)
if old_layout
expect(page).to have_content('Se déconnecter')
click_on 'Se déconnecter'
else
try_twice do
expect(page).to have_css('[title="Mon compte"]')
find('[title="Mon compte"]').click
expect(page).to have_content('Se déconnecter')
click_on 'Se déconnecter'
end
end
end
def try_twice
begin
yield
rescue Selenium::WebDriver::Error::ElementNotInteractableError, Capybara::ElementNotFound
yield
end
end
end

View file

@ -0,0 +1,8 @@
class GroupeInstructeurMailerPreview < ActionMailer::Preview
def add_instructeurs
groupe = GroupeInstructeur.new(label: 'Val-De-Marne')
current_instructeur_email = 'admin@dgfip.com'
instructeurs = Instructeur.limit(2)
GroupeInstructeurMailer.add_instructeurs(groupe, instructeurs, current_instructeur_email)
end
end

View file

@ -149,7 +149,7 @@ describe User, type: :model do
it 'keeps the existing instructeurs and adds administrateur' do
user = subject
expect(user.instructeur).to eq(instructeur)
expect(user.instructeur.administrateurs).to eq(old_admins + admins)
expect(user.instructeur.administrateurs).to match_array(old_admins + admins)
end
end
end