Merge branch 'dev' into 4482-echec-initilaisation-env-dev

This commit is contained in:
Alexandre Friquet 2019-11-14 17:07:28 +01:00 committed by GitHub
commit 8f5203cc2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
89 changed files with 1265 additions and 505 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
@ -93,6 +90,7 @@ group :test do
gem 'shoulda-matchers', require: false
gem 'timecop'
gem 'vcr'
gem 'webdrivers', '~> 4.0'
gem 'webmock'
end

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,11 +153,9 @@ 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.4)
crass (1.0.5)
css_parser (1.6.0)
addressable
curb (0.9.10)
@ -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
@ -356,7 +349,7 @@ GEM
railties (>= 4)
request_store (~> 1.0)
logstash-event (1.2.02)
loofah (2.2.3)
loofah (2.3.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lumberjack (1.0.13)
@ -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,8 +377,8 @@ GEM
mustermann (1.0.3)
nenv (0.3.0)
netrc (0.11.0)
nio4r (2.3.1)
nokogiri (1.10.4)
nio4r (2.5.2)
nokogiri (1.10.5)
mini_portile2 (~> 2.4.0)
notiffany (0.1.1)
nenv (~> 0.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)
@ -678,6 +671,10 @@ GEM
activemodel (>= 5.0)
bindex (>= 0.4.0)
railties (>= 5.0)
webdrivers (4.1.3)
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (>= 3.0, < 4.0)
webfinger (1.1.0)
activesupport
httpclient (>= 2.4)
@ -689,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)
@ -726,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
@ -812,6 +806,7 @@ DEPENDENCIES
vcr
warden
web-console
webdrivers (~> 4.0)
webmock
webpacker
xray-rails

View file

@ -43,8 +43,14 @@ Les informations nécessaire à l'initialisation de la base doivent être pré-c
> create user tps_test with password 'tps_test' superuser;
> \q
### Initialisation de l'environnement de développement
Sous Ubuntu, certains packages doivent être installés au préalable :
sudo apt-get install libcurl3 libcurl3-gnutls libcurl4-openssl-dev libcurl4-gnutls-dev zlib1g-dev
Afin d'initialiser l'environnement de développement, exécutez la commande suivante :
bin/setup

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

@ -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

@ -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

@ -11,7 +11,7 @@
}
}
h4.help-dropdown-title {
.help-dropdown-title {
font-size: 16px;
color: $blue;
}

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

@ -113,3 +113,19 @@ footer {
margin-bottom: 0;
}
}
.footer-site-links {
li {
display: inline;
&::before {
content: "-";
margin: $default-spacer;
}
&:first-child::before {
content: none;
}
}
}

View file

@ -0,0 +1,12 @@
@import "constants";
.huge-title {
text-align: center;
margin-bottom: 20px;
font-size: 35px;
font-weight: bold;
@media (max-width: $two-columns-breakpoint) {
font-size: 25px;
}
}

View file

@ -17,8 +17,9 @@ class Admin::AssignsController < AdminController
not_assign_scope = current_administrateur.instructeurs.where.not(id: assign_scope.ids)
if params[:filter]
not_assign_scope = not_assign_scope.where("email LIKE ?", "%#{params[:filter]}%")
if params[:filter].present?
filter = params[:filter].downcase.strip
not_assign_scope = not_assign_scope.where('users.email LIKE ?', "%#{filter}%")
end
@instructeurs_not_assign = smart_listing_create :instructeurs_not_assign,

View file

@ -2,6 +2,7 @@ class API::V1::DossiersController < APIController
before_action :fetch_procedure_and_check_token
DEFAULT_PAGE_SIZE = 100
MAX_PAGE_SIZE = 1000
ORDER_DIRECTIONS = { 'asc' => :asc, 'desc' => :desc }
def index
@ -33,7 +34,12 @@ class API::V1::DossiersController < APIController
end
def per_page # inherited value from will_paginate
[params[:resultats_par_page]&.to_i || DEFAULT_PAGE_SIZE, 1000].min
resultats_par_page = params[:resultats_par_page]&.to_i
if resultats_par_page && resultats_par_page > 0
[resultats_par_page, MAX_PAGE_SIZE].min
else
DEFAULT_PAGE_SIZE
end
end
def fetch_procedure_and_check_token
@ -47,7 +53,7 @@ class API::V1::DossiersController < APIController
end
order = ORDER_DIRECTIONS.fetch(params[:order], :asc)
@dossiers = @procedure.dossiers.state_not_brouillon.order_for_api(order)
@dossiers = @procedure.dossiers.state_not_brouillon.order_by_created_at(order)
rescue ActiveRecord::RecordNotFound
render json: {}, status: :not_found

View file

@ -250,7 +250,7 @@ class ApplicationController < ActionController::Base
payload: {
DS_SIGN_IN_COUNT: current_user&.sign_in_count,
DS_CREATED_AT: current_administrateur&.created_at,
DS_ACTIVE: current_administrateur&.active?,
DS_ACTIVE: current_user&.active?,
DS_ID: current_administrateur&.id,
DS_GESTIONNAIRE_ID: current_instructeur&.id,
DS_ROLES: current_user_roles

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)

View file

@ -47,7 +47,7 @@ module NewAdministrateur
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)

View file

@ -48,8 +48,11 @@ module Users
end
def attestation
if dossier.attestation.pdf.attached?
if dossier.attestation&.pdf&.attached?
redirect_to url_for(dossier.attestation.pdf)
else
flash.notice = "L'attestation n'est plus disponible sur ce dossier."
redirect_to dossier_path(dossier)
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

@ -1,4 +1,4 @@
module Mutations
class BaseMutation < GraphQL::Schema::Mutation
class BaseMutation < GraphQL::Schema::RelayClassicMutation
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

@ -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

@ -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!
}
@ -40,16 +79,78 @@ GeoJSON 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 {
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
}
@ -82,25 +183,35 @@ type Demarche {
"""
before: String
"""
Dossiers déposés depuis la date.
"""
createdSince: ISO8601DateTime
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Filtrer les dossiers par ID.
"""
ids: [ID!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Dossiers crées depuis la date.
L'ordre des dossiers.
"""
since: ISO8601DateTime
order: Order = ASC
"""
Dossiers avec statut.
"""
state: DossierState
"""
Dossiers mis à jour depuis la date.
"""
updatedSince: ISO8601DateTime
): DossierConnection!
groupeInstructeurs: [GroupeInstructeur!]!
id: ID!
@ -108,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!
@ -131,6 +246,31 @@ enum DemarcheState {
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
"""
@ -163,7 +303,7 @@ type Dossier {
"""
Le numero du dossier.
"""
number: ID!
number: Int!
"""
L'état du dossier.
@ -212,10 +352,45 @@ 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
}
@ -254,22 +429,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
}
@ -295,16 +465,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
}
@ -318,12 +504,41 @@ type Message {
type MultipleDropDownListChamp implements Champ {
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
values: [String!]!
}
type Mutation {
"""
File information required to prepare a direct upload
"""
createDirectUpload(input: CreateDirectUploadInput!): CreateDirectUploadPayload
"""
Envoyer un message à l'usager du dossier.
"""
dossierEnvoyerMessage(input: DossierEnvoyerMessageInput!): DossierEnvoyerMessagePayload
}
enum Order {
"""
Lordre ascendant.
"""
ASC
"""
Lordre descendant.
"""
DESC
}
"""
@ -383,7 +598,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
}
@ -410,7 +633,7 @@ type Query {
"""
Numéro de la démarche.
"""
number: ID!
number: Int!
): Demarche!
"""
@ -420,14 +643,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
}
@ -440,13 +671,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
}
@ -591,4 +838,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
@ -21,8 +21,10 @@ module Types
field :groupe_instructeurs, [Types::GroupeInstructeurType], null: false
field :dossiers, Types::DossierType.connection_type, "Liste de tous les dossiers d'une démarche.", null: false do
argument :ids, [ID], required: false, description: "Filtrer les dossiers par ID."
argument :since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers crées depuis la date."
argument :order, Types::Order, default_value: :asc, required: false, description: "L'ordre des dossiers."
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."
end
field :champ_descriptors, [Types::ChampDescriptorType], null: false, method: :types_de_champ
@ -36,15 +38,21 @@ module Types
Loaders::Association.for(object.class, groupe_instructeurs: { procedure: [:administrateurs] }).load(object)
end
def dossiers(ids: nil, since: nil)
dossiers = object.dossiers.for_api_v2
def dossiers(updated_since: nil, created_since: nil, state: nil, order:)
dossiers = object.dossiers.state_not_brouillon.for_api_v2
if ids.present?
dossiers = dossiers.where(id: ids)
if state.present?
dossiers = dossiers.where(state: state)
end
if since.present?
dossiers = dossiers.since(since)
if updated_since.present?
dossiers = dossiers.updated_since(updated_since).order_by_updated_at(order)
else
if created_since.present?
dossiers = dossiers.created_since(created_since)
end
dossiers = dossiers.order_by_created_at(order)
end
dossiers

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,4 +1,7 @@
module Types
class MutationType < Types::BaseObject
field :create_direct_upload, mutation: Mutations::CreateDirectUpload
field :dossier_envoyer_message, mutation: Mutations::DossierEnvoyerMessage
end
end

View file

@ -0,0 +1,6 @@
module Types
class Order < Types::BaseEnum
value('ASC', 'Lordre ascendant.', value: :asc)
value('DESC', 'Lordre descendant.', value: :desc)
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,6 @@
module StringToHtmlHelper
def string_to_html(str)
html_formatted = simple_format(str)
def string_to_html(str, wrapper_tag = 'p')
html_formatted = simple_format(str, {}, { wrapper_tag: wrapper_tag })
with_links = html_formatted.gsub(URI.regexp, '<a target="_blank" rel="noopener" href="\0">\0</a>')
sanitize(with_links, attributes: ['target', 'rel', 'href'])
end

View file

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

View file

@ -12,11 +12,14 @@ class ApiCarto::API
private
def self.call(url, geojson)
params = geojson.to_s
RestClient.post(url, params, content_type: 'application/json')
response = Typhoeus.post(url, body: geojson.to_s, headers: { 'content-type' => 'application/json' })
rescue RestClient::InternalServerError, RestClient::BadGateway, RestClient::GatewayTimeout, RestClient::ServiceUnavailable => e
Rails.logger.error "[ApiCarto] Error on #{url}: #{e}"
raise RestClient::ResourceNotFound
if response.success?
response.body
else
message = response.code == 0 ? response.return_message : response.code.to_s
Rails.logger.error "[ApiCarto] Error on #{url}: #{message}"
raise RestClient::ResourceNotFound
end
end
end

View file

@ -3,6 +3,11 @@ class ApplicationMailer < ActionMailer::Base
default from: "demarches-simplifiees.fr <#{CONTACT_EMAIL}>"
layout 'mailer'
# Dont retry to send a message if the server rejects the recipient address
rescue_from Net::SMTPSyntaxError do |_error|
message.perform_deliveries = false
end
# Attach the procedure logo to the email (if any).
# Returns the attachment url.
def attach_logo(procedure)

View file

@ -46,7 +46,7 @@ class Administrateur < ApplicationRecord
end
def registration_state
if active?
if user.active?
'Actif'
elsif user.reset_password_period_valid?
'En attente'
@ -56,17 +56,7 @@ class Administrateur < ApplicationRecord
end
def invitation_expired?
!active? && !user.reset_password_period_valid?
end
def self.reset_password(reset_password_token, password)
administrateur = self.reset_password_by_token({
password: password,
password_confirmation: password,
reset_password_token: reset_password_token
})
administrateur
!user.active? && !user.reset_password_period_valid?
end
def owns?(procedure)
@ -80,8 +70,4 @@ class Administrateur < ApplicationRecord
def can_be_deleted?
dossiers.state_instruction_commencee.none? && procedures.none?
end
def active?
user.last_sign_in_at.present?
end
end

View file

@ -33,6 +33,12 @@ class Champs::RepetitionChamp < Champ
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
property :index
property :dossier_id

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

@ -105,7 +105,9 @@ class Dossier < ApplicationRecord
scope :not_archived, -> { where(archived: false) }
scope :order_by_updated_at, -> (order = :desc) { order(updated_at: order) }
scope :order_for_api, -> (order = :asc) { order(en_construction_at: order, created_at: order, id: order) }
scope :order_by_created_at, -> (order = :asc) { order(en_construction_at: order, created_at: order, id: order) }
scope :updated_since, -> (since) { where('dossiers.updated_at >= ?', since) }
scope :created_since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
scope :all_state, -> { not_archived.state_not_brouillon }
scope :en_construction, -> { not_archived.state_en_construction }
@ -134,7 +136,6 @@ class Dossier < ApplicationRecord
scope :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) }
scope :with_champs, -> { includes(champs: :type_de_champ) }
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 :for_api, -> {
includes(commentaires: { piece_jointe_attachment: :blob },
champs: [
@ -472,7 +473,19 @@ class Dossier < ApplicationRecord
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
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 = [
['ID', id.to_s],
['Email', user.email]
@ -485,6 +498,39 @@ class Dossier < ApplicationRecord
['Prénom', individual&.prenom],
['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
columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
end

View file

@ -0,0 +1,16 @@
class DynamicSmtpSettingsInterceptor
def self.delivering_email(message)
if ENV['SENDINBLUE_BALANCING'] == 'enabled'
if rand(0..99) < ENV['SENDINBLUE_BALANCING_VALUE'].to_i
message.delivery_method.settings = {
user_name: ENV['SENDINBLUE_USER_NAME'],
password: ENV['SENDINBLUE_SMTP_KEY'],
address: 'smtp-relay.sendinblue.com',
domain: 'smtp-relay.sendinblue.com',
port: '587',
authentication: :cram_md5
}
end
end
end
end

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

@ -444,7 +444,7 @@ class Procedure < ApplicationRecord
version = options.delete(:version)
if version == 'v2'
options.delete(:tables)
ProcedureExportV2Service.new(self, dossiers, **options.to_h.symbolize_keys)
ProcedureExportV2Service.new(self, dossiers)
else
ProcedureExportService.new(self, dossiers, **options.to_h.symbolize_keys)
end
@ -595,14 +595,18 @@ class Procedure < ApplicationRecord
def move_type_de_champ_attributes(types_de_champ, type_de_champ, new_index)
old_index = types_de_champ.index(type_de_champ)
types_de_champ.insert(new_index, types_de_champ.delete_at(old_index))
.map.with_index do |type_de_champ, index|
{
id: type_de_champ.id,
libelle: type_de_champ.libelle,
order_place: index
}
end
if types_de_champ.delete_at(old_index)
types_de_champ.insert(new_index, type_de_champ)
.map.with_index do |type_de_champ, index|
{
id: type_de_champ.id,
libelle: type_de_champ.libelle,
order_place: index
}
end
else
[]
end
end
def before_publish

View file

@ -53,7 +53,7 @@ class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBas
def check_presence_of_primary_options
if !PRIMARY_PATTERN.match?(drop_down_list.options.second)
errors.add(libelle, "doit commencer par une entrée de menu primaire de la forme <code style='white-space: pre-wrap;'>--texte--</code>")
errors.add(libelle.presence || "La liste", "doit commencer par une entrée de menu primaire de la forme <code style='white-space: pre-wrap;'>--texte--</code>")
end
end

View file

@ -49,7 +49,7 @@ class User < ApplicationRecord
def invite_administrateur!(administration_id)
reset_password_token = nil
if !administrateur.active?
if !active?
reset_password_token = set_reset_password_token
end
@ -92,6 +92,10 @@ class User < ApplicationRecord
"User:#{id}"
end
def active?
last_sign_in_at.present?
end
private
def link_invites!

View file

@ -29,7 +29,7 @@ class AdministrateurUsageStatisticsService
result = {
ds_sign_in_count: administrateur.user.sign_in_count,
ds_created_at: administrateur.created_at,
ds_active: administrateur.active?,
ds_active: administrateur.user.active?,
ds_id: administrateur.id,
nb_services: nb_services_by_administrateur_id[administrateur.id],
nb_instructeurs: nb_instructeurs_by_administrateur_id[administrateur.id],

View file

@ -49,7 +49,7 @@ class ProcedureExportService
:prenom
]
def initialize(procedure, dossiers, tables: [], ids: nil, since: nil, limit: nil)
def initialize(procedure, dossiers, tables: [])
@procedure = procedure
@attributes = ATTRIBUTES.dup
@ -59,15 +59,6 @@ class ProcedureExportService
end
@dossiers = dossiers.downloadable_sorted
if ids
@dossiers = @dossiers.where(id: ids)
end
if since
@dossiers = @dossiers.since(since)
end
if limit
@dossiers = @dossiers.limit(limit)
end
@dossiers = @dossiers.to_a
@tables = tables.map(&:to_sym)
end

View file

@ -1,36 +1,27 @@
class ProcedureExportV2Service
attr_reader :dossiers
def initialize(procedure, dossiers, ids: nil, since: nil, limit: nil)
def initialize(procedure, dossiers)
@procedure = procedure
@dossiers = dossiers.downloadable_sorted
if ids
@dossiers = @dossiers.where(id: ids)
end
if since
@dossiers = @dossiers.since(since)
end
if limit
@dossiers = @dossiers.limit(limit)
end
@tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
end
def to_csv(table = :dossiers)
SpreadsheetArchitect.to_csv(options_for(table))
def to_csv
SpreadsheetArchitect.to_csv(options_for(:dossiers, :csv))
end
def to_xlsx
# We recursively build multi page spreadsheet
@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
def to_ods
# We recursively build multi page spreadsheet
@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
@ -53,7 +44,7 @@ class ProcedureExportV2Service
[dossier.champs, dossier.champs_private]
.flatten
.filter { |champ| champ.is_a?(Champs::RepetitionChamp) }
end.group_by(&:libelle)
end.group_by(&:libelle_for_export)
end
def champs_repetables_options
@ -70,21 +61,16 @@ class ProcedureExportV2Service
row_style: { background_color: nil, color: "000000", font_size: 12 }
}
def sanitize_sheet_name(name)
ActiveStorage::Filename.new(name.to_s).sanitized.truncate(30)
end
def options_for(table)
def options_for(table, format)
case table
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
{ instances: etablissements.to_a, sheet_name: 'Etablissements' }.merge(DEFAULT_STYLES)
when :avis
{ instances: avis.to_a, sheet_name: 'Avis' }.merge(DEFAULT_STYLES)
when Array
# We have to truncate the label here as spreadsheets have a (30 char) limit on length.
{ instances: table.last, sheet_name: sanitize_sheet_name(table.first) }.merge(DEFAULT_STYLES)
{ instances: table.last, sheet_name: table.first }.merge(DEFAULT_STYLES)
end
end
end

View file

@ -1,10 +1,11 @@
- champs = champ.rows.last
- index = (champ.rows.size - 1) * champs.size
%div{ class: "row row-#{champs.first.row}" }
- champs.each.with_index(index) do |champ, index|
= fields_for "#{attribute}[#{index}]", champ do |form|
= render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form }
= form.hidden_field :id
= form.hidden_field :_destroy, disabled: true
%button.button.danger.remove-row
Supprimer
- if champs.present?
- index = (champ.rows.size - 1) * champs.size
%div{ class: "row row-#{champs.first.row}" }
- champs.each.with_index(index) do |champ, index|
= fields_for "#{attribute}[#{index}]", champ do |form|
= render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form }
= form.hidden_field :id
= form.hidden_field :_destroy, disabled: true
%button.button.danger.remove-row
Supprimer

View file

@ -2,7 +2,7 @@
.commencer.form
- if !user_signed_in?
%h1 Commencer la démarche
%h2.huge-title Commencer la démarche
= link_to commencer_sign_up_path(path: @procedure.path), class: ['button large expand primary'] do
Créer un compte
%span.optional-on-small-screens
@ -20,7 +20,7 @@
- elsif drafts.count == 1 && not_drafts.count == 0
- dossier = drafts.first
%h1 Vous avez déjà commencé à remplir un dossier
%h2.huge-title Vous avez déjà commencé à remplir un dossier
%p
Il y a <strong>#{time_ago_in_words(dossier.created_at)}</strong>,
vous avez commencé à remplir un dossier sur la démarche « #{dossier.procedure.libelle} ».
@ -29,7 +29,7 @@
- elsif not_drafts.count == 1
- dossier = not_drafts.first
%h1 Vous avez déjà déposé un dossier
%h2.huge-title Vous avez déjà déposé un dossier
%p
Il y a <strong>#{time_ago_in_words(dossier.en_construction_at)}</strong>,
vous avez déposé un dossier sur la démarche « #{dossier.procedure.libelle} ».
@ -37,6 +37,6 @@
= link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand']
- else
%h1 Vous avez déjà des dossiers pour cette démarche
%h2.huge-title Vous avez déjà des dossiers pour cette démarche
= link_to 'Voir mes dossiers en cours', dossiers_path, class: ['button large expand primary']
= link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand']

View file

@ -3,7 +3,7 @@
- dossier = controller.try(:dossier_for_help)
- procedure = controller.try(:procedure_for_help)
.new-header{ class: current_page?(root_path) ? nil : "new-header-with-border" }
%header.new-header{ class: current_page?(root_path) ? nil : "new-header-with-border" }
.header-inner-content
.flex.align-center

View file

@ -33,8 +33,9 @@
Env Test
= render partial: "layouts/new_header"
= render partial: "layouts/flash_messages"
= content_for?(:content) ? yield(:content) : yield
%main
= render partial: "layouts/flash_messages"
= content_for?(:content) ? yield(:content) : yield
- if content_for?(:footer)
= content_for(:footer)

View file

@ -1,5 +1,5 @@
.no-procedure
= image_tag "landing/hero/dematerialiser.svg", class: "paperless-logo"
= image_tag "landing/hero/dematerialiser.svg", class: "paperless-logo", alt: "moins de papier"
.baseline.center
%h3 Un outil simple
%p

View file

@ -30,14 +30,14 @@
.procedure-list-element
Administrateurs
- if !feature_enabled?(:routage)
- if !feature_enabled?(:administrateur_routage)
%a#onglet-instructeurs{ href: url_for(admin_procedure_assigns_path(@procedure)) }
.procedure-list-element{ class: ('active' if active == 'Instructeurs') }
Instructeurs
- if @procedure.missing_steps.include?(:instructeurs)
%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)) }
.procedure-list-element
Groupe d'instructeurs

View file

@ -6,22 +6,22 @@
%ul.footer-logos
%li.footer-text
Un service fourni par la
= link_to "DINSIC", "http://www.modernisation.gouv.fr/"
= link_to "DINUM", "http://www.modernisation.gouv.fr/", title: "Direction Interministérielle au Numérique"
%br
et incubé par
= link_to "beta.gouv.fr", "https://beta.gouv.fr"
= link_to "beta.gouv.fr", "https://beta.gouv.fr", title: "le site de Beta.gouv.fr"
%li
= link_to "http://www.modernisation.gouv.fr/" do
= link_to "http://www.modernisation.gouv.fr/", title: "DINUM" do
%span.footer-logo.footer-logo-dinsic{ role: 'img', 'aria-label': 'DINSIC' }
= link_to "https://beta.gouv.fr" do
= link_to "https://beta.gouv.fr", title: "le site de Beta.gouv.fr" do
%span.footer-logo.footer-logo-beta-gouv-fr{ role: 'img', 'aria-label': 'beta.gouv.fr' }
%li.footer-column
%ul.footer-links
%li.footer-link
= link_to "Newsletter", "https://my.sendinblue.com/users/subscribe/js_id/3s2q1/id/1", :class => "footer-link", :target => "_blank", rel: "noopener"
= link_to "Newsletter", "https://my.sendinblue.com/users/subscribe/js_id/3s2q1/id/1", :title => "Notre newsletter", :class => "footer-link", :target => "_blank", rel: "noopener"
%li.footer-link
= link_to "Nouveautés", "https://github.com/betagouv/demarches-simplifiees.fr/releases", :class => "footer-link"
= link_to "Nouveautés", "https://github.com/betagouv/demarches-simplifiees.fr/releases", :class => "footer-link", :title => "Nos nouveautés"
%li.footer-link
= link_to "Statistiques", stats_path, :class => "footer-link", data: { turbolinks: false } # Turbolinks disabled for Chartkick. See Issue #350
%li.footer-link

View file

@ -14,13 +14,13 @@
%em.hero-tagline-em en ligne
.hero-illustration
%img{ :src => image_url("landing/hero/dematerialiser.svg"), alt: "" }
%img{ :src => image_url("landing/hero/dematerialiser.svg"), alt: "dématérialisez" }
.landing-panel.usagers-panel
.container
.role-panel-wrapper
.role-panel-30.role-usagers-image
%img.role-image{ :src => image_url("landing/roles/usagers.svg"), alt: "" }
%img.role-image{ :src => image_url("landing/roles/usagers.svg"), alt: "usager" }
.role-panel-70
%h1.role-panel-title Vous souhaitez effectuer une demande auprès d'une administration ?

View file

@ -1,8 +1,8 @@
.procedure-logos
= image_tag procedure.logo_url
= image_tag procedure.logo_url, alt: "logo #{procedure.libelle}"
- if procedure.euro_flag
= image_tag("flag_of_europe.svg", id: 'euro_flag', class: (!procedure.euro_flag ? "hidden" : ""))
%h2.procedure-title
%h1.procedure-title
= procedure.libelle
.procedure-description
.procedure-description-body.read-more-enabled.read-more-collapsed

View file

@ -2,5 +2,5 @@
= mail_to CONTACT_EMAIL do
%span.icon.mail
.dropdown-description
%h4.help-dropdown-title Contact technique
%span.help-dropdown-title Contact technique
%p Envoyez nous un message à #{CONTACT_EMAIL}.

View file

@ -2,5 +2,5 @@
= link_to FAQ_URL, target: "_blank", rel: "noopener" do
%span.icon.help
.dropdown-description
%h4.help-dropdown-title Un problème avec le site ?
%span.help-dropdown-title Un problème avec le site ?
%p Trouvez votre réponse dans laide en ligne.

View file

@ -2,5 +2,5 @@
= link_to messagerie_dossier_path(dossier) do
%span.icon.mail
.dropdown-description
%h4.help-dropdown-title= title
%span.help-dropdown-title= title
%p Envoyez directement un message à linstructeur.

View file

@ -1,7 +1,7 @@
%li.help-dropdown-service
%span.icon.person
.dropdown-description
%h4.help-dropdown-title= title
%span.help-dropdown-title= title
.help-dropdown-service-action
%p Contactez directement ladministration :
%p.help-dropdown-service-item

View file

@ -1,11 +1,7 @@
= link_to "Accessibilité", accessibilite_path, :class => "footer-link"
= link_to "CGU", CGU_URL, :class => "footer-link", :target => "_blank", rel: "noopener noreferrer"
= link_to "Mentions légales", MENTIONS_LEGALES_URL, :class => "footer-link", :target => "_blank", rel: "noopener noreferrer"
= link_to 'Documentation', DOC_URL
= contact_link "Contact technique", class: "footer-link", dossier_id: dossier&.id
= link_to 'Aide', FAQ_URL
%ul.footer-row.footer-bottom-line.footer-site-links
%li>= link_to "Accessibilité", accessibilite_path
%li>= link_to "CGU", CGU_URL, target: "_blank", rel: "noopener noreferrer"
%li>= link_to "Mentions légales", MENTIONS_LEGALES_URL, target: "_blank", rel: "noopener noreferrer"
%li>= link_to 'Documentation', DOC_URL
%li>= contact_link "Contact technique", dossier_id: dossier&.id
%li>= link_to 'Aide', FAQ_URL

View file

@ -2,19 +2,19 @@
.container
- service = procedure.service
- if service.present?
%ul.footer-row.footer-columns
%li.footer-column
%h3.footer-header Cette démarche est gérée par :
%p
.footer-row.footer-columns
%ul.footer-column
%p.footer-header Cette démarche est gérée par :
%li
= service.nom
%br
= service.organisme
%br
= string_to_html(service.adresse)
= string_to_html(service.adresse, wrapper_tag = 'span')
%li.footer-column
%h3.footer-header Poser une question sur votre dossier :
%p
%ul.footer-column
%p.footer-header Poser une question sur votre dossier :
%li
- if dossier.present? && dossier.messagerie_available?
Directement
= link_to "par la messagerie", messagerie_dossier_path(dossier)
@ -22,21 +22,21 @@
Par email :
= link_to service.email, "mailto:#{service.email}"
%p
%li
Par téléphone :
%a{ href: "tel:#{service.telephone}" }= service.telephone
%p
%li
- horaires = "Horaires : #{formatted_horaires(service.horaires)}"
= simple_format(horaires)
= simple_format(horaires, {}, wrapper_tag: 'span')
- politiques = politiques_conservation_de_donnees(procedure)
- if politiques.present?
%li.footer-column
%h3.footer-header Conservation des données :
%ul.footer-column
%p.footer-header Conservation des données :
- politiques.each do |politique|
%p= politique
%li= politique
= render partial: 'users/general_footer_row', locals: { dossier: dossier }
.footer-row.footer-bottom-line
= render partial: 'users/general_footer_row', locals: { dossier: dossier }

View file

@ -1,4 +1,3 @@
%footer.procedure-footer
.container
.footer-row.footer-bottom-line
= render partial: "users/general_footer_row", locals: { dossier: nil }
= render partial: "users/general_footer_row", locals: { dossier: nil }

View file

@ -3,7 +3,7 @@
.auth-form.sign-in-form
= form_for User.new, url: user_session_path, html: { class: "form" } do |f|
%h1 Connectez-vous
%h2.huge-title Connectez-vous
= f.label :email, "Email"
= f.text_field :email, autofocus: true

View file

@ -18,6 +18,8 @@ chdir APP_ROOT do
system('bundle check') || system!('bundle install')
system! 'bin/yarn install'
puts "\n== Updating webdrivers =="
system! 'RAILS_ENV=test bin/rails webdrivers:chromedriver:update'
puts "\n== Copying sample files =="
unless File.exist?('.env')

View file

@ -18,6 +18,9 @@ chdir APP_ROOT do
system('bundle check') || system!('bundle install')
system! 'bin/yarn install'
puts "\n== Updating webdrivers =="
system! 'RAILS_ENV=test bin/rails webdrivers:chromedriver:update'
puts "\n== Updating database =="
system! 'bin/rails db:migrate'

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=""
@ -46,8 +45,14 @@ SENTRY_DSN_JS=""
MATOMO_ENABLED="disabled"
MATOMO_ID="73"
SENDINBLUE_ENABLED="disabled"
SENDINBLUE_BALANCING=""
SENDINBLUE_BALANCING_VALUE=""
SENDINBLUE_ENABLED=""
SENDINBLUE_CLIENT_KEY=""
SENDINBLUE_SMTP_KEY=""
SENDINBLUE_USER_NAME=""
CRISP_ENABLED="disabled"
CRISP_CLIENT_KEY=""

View file

@ -45,14 +45,26 @@ Rails.application.configure do
config.assets.raise_runtime_errors = true
# Action Mailer settings
config.action_mailer.delivery_method = :letter_opener_web
# Configure default root URL for generating URLs to routes
config.action_mailer.default_url_options = {
host: 'localhost',
port: 3000
}
# Configure default root URL for email assets
config.action_mailer.asset_host = "http://" + ENV['APP_HOST']
if ENV['SENDINBLUE_ENABLED'] == 'enabled'
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
user_name: Rails.application.secrets.sendinblue[:username],
password: Rails.application.secrets.sendinblue[:smtp_key],
address: 'smtp-relay.sendinblue.com',
domain: 'smtp-relay.sendinblue.com',
port: '587',
authentication: :cram_md5
}
else
config.action_mailer.delivery_method = :letter_opener_web
config.action_mailer.default_url_options = {
host: 'localhost',
port: 3000
}
config.action_mailer.asset_host = "http://" + ENV['APP_HOST']
end
Rails.application.routes.default_url_options = {
host: 'localhost',

View file

@ -77,6 +77,16 @@ Rails.application.configure do
port: '2525',
authentication: :cram_md5
}
elsif ENV['SENDINBLUE_ENABLED'] == 'enabled'
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
user_name: Rails.application.secrets.sendinblue[:username],
password: Rails.application.secrets.sendinblue[:smtp_key],
address: 'smtp-relay.sendinblue.com',
domain: 'smtp-relay.sendinblue.com',
port: '587',
authentication: :cram_md5
}
else
config.action_mailer.delivery_method = :mailjet
end

View file

@ -1,9 +1,8 @@
Rails.application.config.content_security_policy do |policy|
# En cas de non respect d'une des règles, faire un POST sur cette URL
if Rails.env.production?
policy.report_uri "https://demarchessimplifieestest.report-uri.com/r/d/csp/reportOnly"
else
policy.report_uri "http://#{ENV['APP_HOST']}/csp/" # ne pas notifier report-uri en dev/test
if Rails.env.development?
# les CSP ne sont pas appliquées en dev: on notifie cependant une url quelconque de la violation
# pour détecter les erreurs lors de l'ajout d'une nouvelle brique externe durant le développement
policy.report_uri "http://#{ENV['APP_HOST']}/csp/"
end
# Whitelist image
policy.img_src :self, "*.openstreetmap.org", "static.demarches-simplifiees.fr", "*.cloud.ovh.net", "stats.data.gouv.fr", "*", :data

View file

@ -0,0 +1 @@
ActionMailer::Base.register_interceptor "DynamicSmtpSettingsInterceptor"

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

@ -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'] %>
@ -54,7 +52,9 @@ defaults: &defaults
webhook_secret: <%= ENV['HELPSCOUT_WEBHOOK_SECRET'] %>
sendinblue:
enabled: <%= ENV['SENDINBLUE_ENABLED'] == 'enabled' %>
username: <%= ENV['SENDINBLUE_USER_NAME'] %>
client_key: <%= ENV['SENDINBLUE_CLIENT_KEY'] %>
smtp_key: <%= ENV['SENDINBLUE_SMTP_KEY'] %>
api_v3_key: <%= ENV['SENDINBLUE_API_V3_KEY'] %>
matomo:
enabled: <%= ENV['MATOMO_ENABLED'] == 'enabled' %>
@ -82,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

@ -2,19 +2,53 @@ require 'spec_helper'
describe Admin::AssignsController, type: :controller do
let(:admin) { create(:administrateur) }
let(:procedure) { create :procedure, administrateur: admin }
let(:instructeur) { create :instructeur, administrateurs: [admin] }
before do
sign_in(admin.user)
end
describe 'GET #show' do
subject { get :show, params: { procedure_id: procedure.id } }
it { expect(subject.status).to eq(200) }
let(:procedure) { create :procedure, administrateur: admin, instructeurs: [instructeur_assigned_1, instructeur_assigned_2] }
let!(:instructeur_assigned_1) { create :instructeur, email: 'instructeur_1@ministere_a.gouv.fr', administrateurs: [admin] }
let!(:instructeur_assigned_2) { create :instructeur, email: 'instructeur_2@ministere_b.gouv.fr', administrateurs: [admin] }
let!(:instructeur_not_assigned_1) { create :instructeur, email: 'instructeur_3@ministere_a.gouv.fr', administrateurs: [admin] }
let!(:instructeur_not_assigned_2) { create :instructeur, email: 'instructeur_4@ministere_b.gouv.fr', administrateurs: [admin] }
let(:filter) { nil }
subject! { get :show, params: { procedure_id: procedure.id, filter: filter } }
it { expect(response.status).to eq(200) }
it 'sets the assigned and not assigned instructeurs' do
expect(assigns(:instructeurs_assign)).to match_array([instructeur_assigned_1, instructeur_assigned_2])
expect(assigns(:instructeurs_not_assign)).to match_array([instructeur_not_assigned_1, instructeur_not_assigned_2])
end
context 'with a search filter' do
let(:filter) { '@ministere_a.gouv.fr' }
it 'filters the unassigned instructeurs' do
expect(assigns(:instructeurs_not_assign)).to match_array([instructeur_not_assigned_1])
end
it 'does not filter the assigned instructeurs' do
expect(assigns(:instructeurs_assign)).to match_array([instructeur_assigned_1, instructeur_assigned_2])
end
context 'when the filter has spaces or a mixed case' do
let(:filter) { ' @ministere_A.gouv.fr ' }
it 'trims spaces and ignores the case' do
expect(assigns(:instructeurs_not_assign)).to match_array([instructeur_not_assigned_1])
end
end
end
end
describe 'PUT #update' do
let(:procedure) { create :procedure, administrateur: admin }
let(:instructeur) { create :instructeur, administrateurs: [admin] }
subject { put :update, params: { instructeur_id: instructeur.id, procedure_id: procedure.id, to: 'assign' } }
it { expect(subject).to redirect_to admin_procedure_assigns_path(procedure_id: procedure.id) }

View file

@ -12,6 +12,15 @@ describe API::V2::GraphqlController do
create(:commentaire, dossier: dossier, email: 'test@test.com')
dossier
end
let(:dossier1) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: 1.day.ago) }
let(:dossier2) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: 3.days.ago) }
let!(:dossier_brouillon) { create(:dossier, procedure: procedure) }
let(:dossiers) { [dossier2, dossier1, dossier] }
let(:instructeur) { create(:instructeur, followed_dossiers: dossiers) }
before do
instructeur.assign_to_procedure(procedure)
end
let(:query) do
"{
@ -62,31 +71,65 @@ describe API::V2::GraphqlController do
request.env['HTTP_AUTHORIZATION'] = authorization_header
end
it "should return demarche" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(demarche: {
id: procedure.to_typed_id,
number: procedure.id.to_s,
title: procedure.libelle,
description: procedure.description,
state: 'brouillon',
archivedAt: nil,
createdAt: procedure.created_at.iso8601,
updatedAt: procedure.updated_at.iso8601,
groupeInstructeurs: [{ instructeurs: [], label: "défaut" }],
champDescriptors: procedure.types_de_champ.map do |tdc|
{
id: tdc.to_typed_id,
label: tdc.libelle,
type: tdc.type_champ,
description: tdc.description,
required: tdc.mandatory?
context "demarche" do
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,
title: procedure.libelle,
description: procedure.description,
state: 'brouillon',
archivedAt: nil,
createdAt: procedure.created_at.iso8601,
updatedAt: procedure.updated_at.iso8601,
groupeInstructeurs: [
{
instructeurs: [{ email: instructeur.email }],
label: "défaut"
}
],
champDescriptors: procedure.types_de_champ.map do |tdc|
{
id: tdc.to_typed_id,
label: tdc.libelle,
type: tdc.type_champ,
description: tdc.description,
required: tdc.mandatory?
}
end,
dossiers: {
nodes: dossiers.map { |dossier| { id: dossier.to_typed_id } }
}
end,
dossiers: {
nodes: []
}
})
})
end
context "filter dossiers" do
let(:query) do
"{
demarche(number: #{procedure.id}) {
id
number
dossiers(createdSince: \"#{2.days.ago.iso8601}\") {
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: [{ id: dossier1.to_typed_id }, { id: dossier.to_typed_id }]
}
})
end
end
end
context "dossier" do
@ -130,11 +173,11 @@ describe API::V2::GraphqlController do
}"
end
it "should return dossier" do
it "should be returned" 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,
@ -146,7 +189,12 @@ describe API::V2::GraphqlController do
id: dossier.user.to_typed_id,
email: dossier.user.email
},
instructeurs: [],
instructeurs: [
{
id: instructeur.to_typed_id,
email: instructeur.email
}
],
messages: dossier.commentaires.map do |commentaire|
{
body: commentaire.body,
@ -166,6 +214,114 @@ describe API::V2::GraphqlController do
expect(gql_data[:dossier][:champs][0][:id]).to eq(dossier.champs[0].type_de_champ.to_typed_id)
end
end
context "mutations" 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
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
context "when not authenticated" do
@ -182,5 +338,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

@ -12,7 +12,7 @@ feature 'As an administrateur', js: true do
end
scenario 'I can register' do
expect(new_admin.reload.active?).to be(false)
expect(new_admin.reload.user.active?).to be(false)
confirmation_email = open_email(admin_email)
token_params = confirmation_email.body.match(/token=[^"]+/)
@ -24,6 +24,6 @@ feature 'As an administrateur', js: true do
expect(page).to have_content 'Mot de passe enregistré'
expect(new_admin.reload.active?).to be(true)
expect(new_admin.reload.user.active?).to be(true)
end
end

View file

@ -7,7 +7,7 @@ feature 'The routing' do
let(:scientifique_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
login_as administrateur.user, scope: :user

View file

@ -0,0 +1,20 @@
RSpec.describe ApplicationMailer, type: :mailer do
describe 'dealing with invalid emails' do
let(:dossier) { create(:dossier, procedure: build(:simple_procedure)) }
subject { DossierMailer.notify_new_draft(dossier) }
describe 'invalid emails are not sent' do
before do
allow_any_instance_of(DossierMailer)
.to receive(:notify_new_draft)
.and_raise(Net::SMTPSyntaxError)
end
it { expect(subject.message).to be_an_instance_of(ActionMailer::Base::NullMail) }
end
describe 'valid emails are sent' do
it { expect(subject.message).not_to be_an_instance_of(ActionMailer::Base::NullMail) }
end
end
end

View file

@ -50,22 +50,4 @@ describe Administrateur, type: :model do
# it { expect(subject).to eq([]) }
# end
# end
describe '#active?' do
let!(:administrateur) { create(:administrateur) }
subject { administrateur.active? }
context 'when the user has never signed in' do
before { administrateur.user.update(last_sign_in_at: nil) }
it { is_expected.to be false }
end
context 'when the user has already signed in' do
before { administrateur.user.update(last_sign_in_at: Time.zone.now) }
it { is_expected.to be true }
end
end
end

View file

@ -148,4 +148,22 @@ shared_examples 'type_de_champ_spec' do
expect(cloned_procedure.types_de_champ.first.types_de_champ).not_to be_empty
end
end
describe "linked_drop_down_list" do
let(:type_de_champ) { create(:type_de_champ_linked_drop_down_list) }
it 'should validate without label' do
type_de_champ.drop_down_list_value = 'toto'
expect(type_de_champ.validate).to be_falsey
messages = type_de_champ.errors.full_messages
expect(messages.size).to eq(1)
expect(messages.first.starts_with?("#{type_de_champ.libelle} doit commencer par")).to be_truthy
type_de_champ.libelle = ''
expect(type_de_champ.validate).to be_falsey
messages = type_de_champ.errors.full_messages
expect(messages.size).to eq(2)
expect(messages.last.starts_with?("La liste doit commencer par")).to be_truthy
end
end
end

View file

@ -191,4 +191,22 @@ describe User, type: :model do
it { expect(AdministrationMailer).to have_received(:invite_admin).with(user, nil, administration.id) }
end
end
describe '#active?' do
let!(:user) { create(:user) }
subject { user.active? }
context 'when the user has never signed in' do
before { user.update(last_sign_in_at: nil) }
it { is_expected.to be false }
end
context 'when the user has already signed in' do
before { user.update(last_sign_in_at: Time.zone.now) }
it { is_expected.to be true }
end
end
end

View file

@ -1,4 +1,5 @@
require 'spec_helper'
require 'csv'
describe ProcedureExportV2Service do
describe 'to_data' do
@ -150,6 +151,91 @@ describe ProcedureExportV2Service do
]
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
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' } }
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
it 'should have headers' do
@ -247,7 +333,18 @@ describe ProcedureExportV2Service do
end
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

View file

@ -83,7 +83,7 @@ VCR.configure do |c|
c.hook_into :webmock
c.cassette_library_dir = 'spec/fixtures/cassettes'
c.configure_rspec_metadata!
c.ignore_hosts 'test.host'
c.ignore_hosts 'test.host', 'chromedriver.storage.googleapis.com'
end
DatabaseCleaner.strategy = :transaction