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-sass', '>= 3.4.1'
gem 'bootstrap-wysihtml5-rails', '~> 0.3.3.8' gem 'bootstrap-wysihtml5-rails', '~> 0.3.3.8'
gem 'browser' gem 'browser'
gem 'carrierwave'
gem 'carrierwave-i18n'
gem 'chartkick' gem 'chartkick'
gem 'chunky_png' gem 'chunky_png'
gem 'clamav-client', require: 'clamav/client' gem 'clamav-client', require: 'clamav/client'
gem 'copy_carrierwave_file'
gem 'daemons' gem 'daemons'
gem 'deep_cloneable' # Enable deep clone of active record models gem 'deep_cloneable' # Enable deep clone of active record models
gem 'delayed_cron_job' # Cron jobs gem 'delayed_cron_job' # Cron jobs
@ -93,6 +90,7 @@ group :test do
gem 'shoulda-matchers', require: false gem 'shoulda-matchers', require: false
gem 'timecop' gem 'timecop'
gem 'vcr' gem 'vcr'
gem 'webdrivers', '~> 4.0'
gem 'webmock' gem 'webmock'
end end

View file

@ -20,25 +20,25 @@ GEM
specs: specs:
aasm (5.0.1) aasm (5.0.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
actioncable (5.2.2.1) actioncable (5.2.3)
actionpack (= 5.2.2.1) actionpack (= 5.2.3)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailer (5.2.2.1) actionmailer (5.2.3)
actionpack (= 5.2.2.1) actionpack (= 5.2.3)
actionview (= 5.2.2.1) actionview (= 5.2.3)
activejob (= 5.2.2.1) activejob (= 5.2.3)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.2.2.1) actionpack (5.2.3)
actionview (= 5.2.2.1) actionview (= 5.2.3)
activesupport (= 5.2.2.1) activesupport (= 5.2.3)
rack (~> 2.0) rack (~> 2.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.2.1) actionview (5.2.3)
activesupport (= 5.2.2.1) activesupport (= 5.2.3)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -51,25 +51,25 @@ GEM
activemodel (>= 4.1, < 6) activemodel (>= 4.1, < 6)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (5.2.2.1) activejob (5.2.3)
activesupport (= 5.2.2.1) activesupport (= 5.2.3)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.2.2.1) activemodel (5.2.3)
activesupport (= 5.2.2.1) activesupport (= 5.2.3)
activerecord (5.2.2.1) activerecord (5.2.3)
activemodel (= 5.2.2.1) activemodel (= 5.2.3)
activesupport (= 5.2.2.1) activesupport (= 5.2.3)
arel (>= 9.0) arel (>= 9.0)
activestorage (5.2.2.1) activestorage (5.2.3)
actionpack (= 5.2.2.1) actionpack (= 5.2.3)
activerecord (= 5.2.2.1) activerecord (= 5.2.3)
marcel (~> 0.3.1) marcel (~> 0.3.1)
activestorage-openstack (1.0.0) activestorage-openstack (1.2.0)
fog-openstack (~> 1.0) fog-openstack (~> 1.0)
marcel marcel
mime-types mime-types
rails (<= 6) rails (>= 5.2.2)
activesupport (5.2.2.1) activesupport (5.2.3)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
@ -136,11 +136,6 @@ GEM
capybara-selenium (0.0.6) capybara-selenium (0.0.6)
capybara capybara
selenium-webdriver 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) case_transform (0.2)
activesupport activesupport
chartkick (3.2.0) chartkick (3.2.0)
@ -158,11 +153,9 @@ GEM
coffee-script-source (1.12.2) coffee-script-source (1.12.2)
concurrent-ruby (1.1.5) concurrent-ruby (1.1.5)
connection_pool (2.2.2) connection_pool (2.2.2)
copy_carrierwave_file (1.3.0)
carrierwave (>= 0.9)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.4) crass (1.0.5)
css_parser (1.6.0) css_parser (1.6.0)
addressable addressable
curb (0.9.10) curb (0.9.10)
@ -203,7 +196,7 @@ GEM
em-websocket (0.5.1) em-websocket (0.5.1)
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0.6.0)
erubi (1.8.0) erubi (1.9.0)
erubis (2.7.0) erubis (2.7.0)
ethon (0.11.0) ethon (0.11.0)
ffi (>= 1.3.0) ffi (>= 1.3.0)
@ -311,7 +304,7 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
http_parser.rb (0.6.0) http_parser.rb (0.6.0)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.6.0) i18n (1.7.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
ipaddress (0.8.3) ipaddress (0.8.3)
jaro_winkler (1.5.2) jaro_winkler (1.5.2)
@ -320,7 +313,7 @@ GEM
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (2.2.0) json (2.2.0)
json-jwt (1.10.0) json-jwt (1.11.0)
activesupport (>= 4.2) activesupport (>= 4.2)
aes_key_wrap aes_key_wrap
bindata bindata
@ -356,7 +349,7 @@ GEM
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
logstash-event (1.2.02) logstash-event (1.2.02)
loofah (2.2.3) loofah (2.3.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
lumberjack (1.0.13) lumberjack (1.0.13)
@ -375,7 +368,7 @@ GEM
mimemagic (0.3.3) mimemagic (0.3.3)
mini_mime (1.0.2) mini_mime (1.0.2)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
minitest (5.11.3) minitest (5.13.0)
momentjs-rails (2.20.1) momentjs-rails (2.20.1)
railties (>= 3.1) railties (>= 3.1)
multi_json (1.14.1) multi_json (1.14.1)
@ -384,8 +377,8 @@ GEM
mustermann (1.0.3) mustermann (1.0.3)
nenv (0.3.0) nenv (0.3.0)
netrc (0.11.0) netrc (0.11.0)
nio4r (2.3.1) nio4r (2.5.2)
nokogiri (1.10.4) nokogiri (1.10.5)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
notiffany (0.1.1) notiffany (0.1.1)
nenv (~> 0.1) nenv (~> 0.1)
@ -469,18 +462,18 @@ GEM
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (5.2.2.1) rails (5.2.3)
actioncable (= 5.2.2.1) actioncable (= 5.2.3)
actionmailer (= 5.2.2.1) actionmailer (= 5.2.3)
actionpack (= 5.2.2.1) actionpack (= 5.2.3)
actionview (= 5.2.2.1) actionview (= 5.2.3)
activejob (= 5.2.2.1) activejob (= 5.2.3)
activemodel (= 5.2.2.1) activemodel (= 5.2.3)
activerecord (= 5.2.2.1) activerecord (= 5.2.3)
activestorage (= 5.2.2.1) activestorage (= 5.2.3)
activesupport (= 5.2.2.1) activesupport (= 5.2.3)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.2.2.1) railties (= 5.2.3)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4) rails-controller-testing (1.0.4)
actionpack (>= 5.0.1.x) actionpack (>= 5.0.1.x)
@ -489,14 +482,14 @@ GEM
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.2.0) rails-html-sanitizer (1.3.0)
loofah (~> 2.2, >= 2.2.2) loofah (~> 2.3)
rails-i18n (5.1.2) rails-i18n (5.1.2)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 5.0, < 6) railties (>= 5.0, < 6)
railties (5.2.2.1) railties (5.2.3)
actionpack (= 5.2.2.1) actionpack (= 5.2.3)
activesupport (= 5.2.2.1) activesupport (= 5.2.3)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0) thor (>= 0.19.0, < 2.0)
@ -678,6 +671,10 @@ GEM
activemodel (>= 5.0) activemodel (>= 5.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 5.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) webfinger (1.1.0)
activesupport activesupport
httpclient (>= 2.4) httpclient (>= 2.4)
@ -689,9 +686,9 @@ GEM
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
websocket-driver (0.7.0) websocket-driver (0.7.1)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3) websocket-extensions (0.1.4)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
xray-rails (0.3.1) xray-rails (0.3.1)
@ -726,12 +723,9 @@ DEPENDENCIES
capybara-email capybara-email
capybara-screenshot capybara-screenshot
capybara-selenium capybara-selenium
carrierwave
carrierwave-i18n
chartkick chartkick
chunky_png chunky_png
clamav-client clamav-client
copy_carrierwave_file
daemons daemons
database_cleaner database_cleaner
deep_cloneable deep_cloneable
@ -812,6 +806,7 @@ DEPENDENCIES
vcr vcr
warden warden
web-console web-console
webdrivers (~> 4.0)
webmock webmock
webpacker webpacker
xray-rails 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; > create user tps_test with password 'tps_test' superuser;
> \q > \q
### Initialisation de l'environnement de développement ### 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 : Afin d'initialiser l'environnement de développement, exécutez la commande suivante :
bin/setup bin/setup

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -113,3 +113,19 @@ footer {
margin-bottom: 0; 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) not_assign_scope = current_administrateur.instructeurs.where.not(id: assign_scope.ids)
if params[:filter] if params[:filter].present?
not_assign_scope = not_assign_scope.where("email LIKE ?", "%#{params[:filter]}%") filter = params[:filter].downcase.strip
not_assign_scope = not_assign_scope.where('users.email LIKE ?', "%#{filter}%")
end end
@instructeurs_not_assign = smart_listing_create :instructeurs_not_assign, @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 before_action :fetch_procedure_and_check_token
DEFAULT_PAGE_SIZE = 100 DEFAULT_PAGE_SIZE = 100
MAX_PAGE_SIZE = 1000
ORDER_DIRECTIONS = { 'asc' => :asc, 'desc' => :desc } ORDER_DIRECTIONS = { 'asc' => :asc, 'desc' => :desc }
def index def index
@ -33,7 +34,12 @@ class API::V1::DossiersController < APIController
end end
def per_page # inherited value from will_paginate 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 end
def fetch_procedure_and_check_token def fetch_procedure_and_check_token
@ -47,7 +53,7 @@ class API::V1::DossiersController < APIController
end end
order = ORDER_DIRECTIONS.fetch(params[:order], :asc) 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 rescue ActiveRecord::RecordNotFound
render json: {}, status: :not_found render json: {}, status: :not_found

View file

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

View file

@ -14,7 +14,7 @@ module Instructeurs
end end
def add_instructeur def add_instructeur
@instructeur = Instructeur.find_by(email: instructeur_email) || @instructeur = Instructeur.by_email(instructeur_email) ||
create_instructeur(instructeur_email) create_instructeur(instructeur_email)
if groupe_instructeur.instructeurs.include?(@instructeur) if groupe_instructeur.instructeurs.include?(@instructeur)

View file

@ -47,7 +47,7 @@ module NewAdministrateur
end end
def add_instructeur def add_instructeur
@instructeur = Instructeur.find_by(email: instructeur_email) || @instructeur = Instructeur.by_email(instructeur_email) ||
create_instructeur(instructeur_email) create_instructeur(instructeur_email)
if groupe_instructeur.instructeurs.include?(@instructeur) if groupe_instructeur.instructeurs.include?(@instructeur)

View file

@ -48,8 +48,11 @@ module Users
end end
def attestation def attestation
if dossier.attestation.pdf.attached? if dossier.attestation&.pdf&.attached?
redirect_to url_for(dossier.attestation.pdf) 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
end end

View file

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

View file

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

View file

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

View file

@ -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 { type CarteChamp implements Champ {
geoAreas: [GeoArea!]! geoAreas: [GeoArea!]!
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
} }
interface Champ { interface Champ {
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
} }
type ChampDescriptor { type ChampDescriptor {
"""
Description du champ.
"""
description: String description: String
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
Est-ce que le champ est obligatoire ?
"""
required: Boolean! required: Boolean!
"""
Type de la valeur du champ.
"""
type: TypeDeChamp! type: TypeDeChamp!
} }
type CheckboxChamp implements Champ { type CheckboxChamp implements Champ {
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
value: Boolean! value: Boolean!
} }
@ -40,16 +79,78 @@ GeoJSON coordinates
""" """
scalar Coordinates scalar Coordinates
"""
Autogenerated input type of CreateDirectUpload
"""
input CreateDirectUploadInput {
"""
File size (bytes)
"""
byteSize: Int!
"""
MD5 file checksum as base64
"""
checksum: String!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
File content type
"""
contentType: String!
"""
Dossier ID
"""
dossierId: ID!
"""
Original file name
"""
filename: String!
}
"""
Autogenerated return type of CreateDirectUpload
"""
type CreateDirectUploadPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
directUpload: DirectUpload!
}
type DateChamp implements Champ { type DateChamp implements Champ {
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
value: ISO8601DateTime value: ISO8601DateTime
} }
type DecimalNumberChamp implements Champ { type DecimalNumberChamp implements Champ {
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
value: Float value: Float
} }
@ -82,25 +183,35 @@ type Demarche {
""" """
before: String before: String
"""
Dossiers déposés depuis la date.
"""
createdSince: ISO8601DateTime
""" """
Returns the first _n_ elements from the list. Returns the first _n_ elements from the list.
""" """
first: Int first: Int
"""
Filtrer les dossiers par ID.
"""
ids: [ID!]
""" """
Returns the last _n_ elements from the list. Returns the last _n_ elements from the list.
""" """
last: Int 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! ): DossierConnection!
groupeInstructeurs: [GroupeInstructeur!]! groupeInstructeurs: [GroupeInstructeur!]!
id: ID! id: ID!
@ -108,7 +219,11 @@ type Demarche {
""" """
Le numero de la démarche. Le numero de la démarche.
""" """
number: ID! number: Int!
"""
L'état de la démarche.
"""
state: DemarcheState! state: DemarcheState!
title: String! title: String!
updatedAt: ISO8601DateTime! updatedAt: ISO8601DateTime!
@ -131,6 +246,31 @@ enum DemarcheState {
publiee publiee
} }
"""
Represents direct upload credentials
"""
type DirectUpload {
"""
Created blob record ID
"""
blobId: ID!
"""
HTTP request headers (JSON-encoded)
"""
headers: String!
"""
Created blob record signed ID
"""
signedBlobId: ID!
"""
Upload URL
"""
url: String!
}
""" """
Un dossier Un dossier
""" """
@ -163,7 +303,7 @@ type Dossier {
""" """
Le numero du dossier. Le numero du dossier.
""" """
number: ID! number: Int!
""" """
L'état du dossier. L'état du dossier.
@ -212,10 +352,45 @@ type DossierEdge {
node: Dossier 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 { type DossierLinkChamp implements Champ {
dossier: Dossier dossier: Dossier
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
} }
@ -254,22 +429,17 @@ interface GeoArea {
enum GeoAreaSource { enum GeoAreaSource {
""" """
translation missing: fr.activerecord.attributes.geo_area.source.cadastre Parcelle cadastrale
""" """
cadastre cadastre
""" """
translation missing: fr.activerecord.attributes.geo_area.source.parcelle_agricole Quartier prioritaire
"""
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 selection_utilisateur
} }
@ -295,16 +465,32 @@ scalar ISO8601DateTime
type IntegerNumberChamp implements Champ { type IntegerNumberChamp implements Champ {
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
value: Int value: Int
} }
type LinkedDropDownListChamp implements Champ { type LinkedDropDownListChamp implements Champ {
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
primaryValue: String primaryValue: String
secondaryValue: String secondaryValue: String
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
} }
@ -318,12 +504,41 @@ type Message {
type MultipleDropDownListChamp implements Champ { type MultipleDropDownListChamp implements Champ {
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
values: [String!]! values: [String!]!
} }
type Mutation { 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 { type PieceJustificativeChamp implements Champ {
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
url: URL url: URL
} }
@ -410,7 +633,7 @@ type Query {
""" """
Numéro de la démarche. Numéro de la démarche.
""" """
number: ID! number: Int!
): Demarche! ): Demarche!
""" """
@ -420,14 +643,22 @@ type Query {
""" """
Numéro du dossier. Numéro du dossier.
""" """
number: ID! number: Int!
): Dossier! ): Dossier!
} }
type RepetitionChamp implements Champ { type RepetitionChamp implements Champ {
champs: [Champ!]! champs: [Champ!]!
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
} }
@ -440,13 +671,29 @@ type SelectionUtilisateur implements GeoArea {
type SiretChamp implements Champ { type SiretChamp implements Champ {
etablissement: PersonneMorale etablissement: PersonneMorale
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
} }
type TextChamp implements Champ { type TextChamp implements Champ {
id: ID! id: ID!
"""
Libellé du champ.
"""
label: String! label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String stringValue: String
value: String value: String
} }
@ -591,4 +838,14 @@ enum TypeDeChamp {
""" """
A valid URL, transported as a string 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 end
global_id_field :id global_id_field :id
field :type, TypeDeChampType, null: false, method: :type_champ field :type, TypeDeChampType, "Type de la valeur du champ.", null: false, method: :type_champ
field :label, String, null: false, method: :libelle field :label, String, "Libellé du champ.", null: false, method: :libelle
field :description, String, null: true field :description, String, "Description du champ.", null: true
field :required, Boolean, null: false, method: :mandatory? field :required, Boolean, "Est-ce que le champ est obligatoire ?", null: false, method: :mandatory?
end end
end end

View file

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

View file

@ -9,10 +9,10 @@ module Types
description "Une demarche" description "Une demarche"
global_id_field :id 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 :title, String, null: false, method: :libelle
field :description, String, "Déscription de la démarche.", null: false 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 :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_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 :groupe_instructeurs, [Types::GroupeInstructeurType], null: false
field :dossiers, Types::DossierType.connection_type, "Liste de tous les dossiers d'une démarche.", null: false do 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 :order, Types::Order, default_value: :asc, required: false, description: "L'ordre des dossiers."
argument :since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers crées depuis la date." 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 end
field :champ_descriptors, [Types::ChampDescriptorType], null: false, method: :types_de_champ 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) Loaders::Association.for(object.class, groupe_instructeurs: { procedure: [:administrateurs] }).load(object)
end end
def dossiers(ids: nil, since: nil) def dossiers(updated_since: nil, created_since: nil, state: nil, order:)
dossiers = object.dossiers.for_api_v2 dossiers = object.dossiers.state_not_brouillon.for_api_v2
if ids.present? if state.present?
dossiers = dossiers.where(id: ids) dossiers = dossiers.where(state: state)
end end
if since.present? if updated_since.present?
dossiers = dossiers.since(since) 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 end
dossiers dossiers

View file

@ -9,7 +9,7 @@ module Types
description "Un dossier" description "Un dossier"
global_id_field :id 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 :state, DossierState, "L'état du dossier.", null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, "Date de dernière mise à jour.", 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 class GeoAreaSource < Types::BaseEnum
GeoArea.sources.each do |symbol_name, string_name| GeoArea.sources.each do |symbol_name, string_name|
value(string_name, if string_name != "parcelle_agricole"
I18n.t(symbol_name, scope: [:activerecord, :attributes, :geo_area, :source]), value(string_name,
value: symbol_name) I18n.t(symbol_name, scope: [:activerecord, :attributes, :geo_area, :source]),
value: symbol_name)
end
end end
end end

View file

@ -1,4 +1,7 @@
module Types module Types
class MutationType < Types::BaseObject class MutationType < Types::BaseObject
field :create_direct_upload, mutation: Mutations::CreateDirectUpload
field :dossier_envoyer_message, mutation: Mutations::DossierEnvoyerMessage
end end
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 module Types
class QueryType < Types::BaseObject class QueryType < Types::BaseObject
field :demarche, DemarcheType, null: false, description: "Informations concernant une démarche." do 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 end
field :dossier, DossierType, null: false, description: "Informations sur un dossier d'une démarche." do 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 end
def demarche(number:) 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 module StringToHtmlHelper
def string_to_html(str) def string_to_html(str, wrapper_tag = 'p')
html_formatted = simple_format(str) 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>') with_links = html_formatted.gsub(URI.regexp, '<a target="_blank" rel="noopener" href="\0">\0</a>')
sanitize(with_links, attributes: ['target', 'rel', 'href']) sanitize(with_links, attributes: ['target', 'rel', 'href'])
end end

View file

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

View file

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

View file

@ -3,6 +3,11 @@ class ApplicationMailer < ActionMailer::Base
default from: "demarches-simplifiees.fr <#{CONTACT_EMAIL}>" default from: "demarches-simplifiees.fr <#{CONTACT_EMAIL}>"
layout 'mailer' 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). # Attach the procedure logo to the email (if any).
# Returns the attachment url. # Returns the attachment url.
def attach_logo(procedure) def attach_logo(procedure)

View file

@ -46,7 +46,7 @@ class Administrateur < ApplicationRecord
end end
def registration_state def registration_state
if active? if user.active?
'Actif' 'Actif'
elsif user.reset_password_period_valid? elsif user.reset_password_period_valid?
'En attente' 'En attente'
@ -56,17 +56,7 @@ class Administrateur < ApplicationRecord
end end
def invitation_expired? def invitation_expired?
!active? && !user.reset_password_period_valid? !user.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
end end
def owns?(procedure) def owns?(procedure)
@ -80,8 +70,4 @@ class Administrateur < ApplicationRecord
def can_be_deleted? def can_be_deleted?
dossiers.state_instruction_commencee.none? && procedures.none? dossiers.state_instruction_commencee.none? && procedures.none?
end end
def active?
user.last_sign_in_at.present?
end
end end

View file

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

View file

@ -10,7 +10,7 @@ class Commentaire < ApplicationRecord
has_one_attached :piece_jointe 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) } default_scope { order(created_at: :asc) }
scope :updated_since?, -> (date) { where('commentaires.updated_at > ?', date) } 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 :not_archived, -> { where(archived: false) }
scope :order_by_updated_at, -> (order = :desc) { order(updated_at: order) } 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 :all_state, -> { not_archived.state_not_brouillon }
scope :en_construction, -> { not_archived.state_en_construction } 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 :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) }
scope :with_champs, -> { includes(champs: :type_de_champ) } 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 :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, -> { scope :for_api, -> {
includes(commentaires: { piece_jointe_attachment: :blob }, includes(commentaires: { piece_jointe_attachment: :blob },
champs: [ champs: [
@ -472,7 +473,19 @@ class Dossier < ApplicationRecord
log_dossier_operation(avis.claimant, :demander_un_avis, avis) log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end end
def spreadsheet_columns def spreadsheet_columns_csv
spreadsheet_columns(with_etablissement: true)
end
def spreadsheet_columns_xlsx
spreadsheet_columns
end
def spreadsheet_columns_ods
spreadsheet_columns
end
def spreadsheet_columns(with_etablissement: false)
columns = [ columns = [
['ID', id.to_s], ['ID', id.to_s],
['Email', user.email] ['Email', user.email]
@ -485,6 +498,39 @@ class Dossier < ApplicationRecord
['Prénom', individual&.prenom], ['Prénom', individual&.prenom],
['Date de naissance', individual&.birthdate] ['Date de naissance', individual&.birthdate]
] ]
elsif with_etablissement
columns += [
['Établissement SIRET', etablissement&.siret],
['Établissement siège social', etablissement&.siege_social],
['Établissement NAF', etablissement&.naf],
['Établissement libellé NAF', etablissement&.libelle_naf],
['Établissement Adresse', etablissement&.adresse],
['Établissement numero voie', etablissement&.numero_voie],
['Établissement type voie', etablissement&.type_voie],
['Établissement nom voie', etablissement&.nom_voie],
['Établissement complément adresse', etablissement&.complement_adresse],
['Établissement code postal', etablissement&.code_postal],
['Établissement localité', etablissement&.localite],
['Établissement code INSEE localité', etablissement&.code_insee_localite],
['Entreprise SIREN', etablissement&.entreprise_siren],
['Entreprise capital social', etablissement&.entreprise_capital_social],
['Entreprise numero TVA intracommunautaire', etablissement&.entreprise_numero_tva_intracommunautaire],
['Entreprise forme juridique', etablissement&.entreprise_forme_juridique],
['Entreprise forme juridique code', etablissement&.entreprise_forme_juridique_code],
['Entreprise nom commercial', etablissement&.entreprise_nom_commercial],
['Entreprise raison sociale', etablissement&.entreprise_raison_sociale],
['Entreprise SIRET siège social', etablissement&.entreprise_siret_siege_social],
['Entreprise code effectif entreprise', etablissement&.entreprise_code_effectif_entreprise],
['Entreprise date de création', etablissement&.entreprise_date_creation],
['Entreprise nom', etablissement&.entreprise_nom],
['Entreprise prénom', etablissement&.entreprise_prenom],
['Association RNA', etablissement&.association_rna],
['Association titre', etablissement&.association_titre],
['Association objet', etablissement&.association_objet],
['Association date de création', etablissement&.association_date_creation],
['Association date de déclaration', etablissement&.association_date_declaration],
['Association date de publication', etablissement&.association_date_publication]
]
else else
columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale] columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
end end

View file

@ -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' GENDER_FEMALE = 'Mme'
def self.create_from_france_connect(fc_information) def self.create_from_france_connect(fc_information)
create( create!(
nom: fc_information.family_name, nom: fc_information.family_name,
prenom: fc_information.given_name, prenom: fc_information.given_name,
gender: fc_information.gender == 'female' ? GENDER_FEMALE : GENDER_MALE gender: fc_information.gender == 'female' ? GENDER_FEMALE : GENDER_MALE

View file

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

View file

@ -53,7 +53,7 @@ class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBas
def check_presence_of_primary_options def check_presence_of_primary_options
if !PRIMARY_PATTERN.match?(drop_down_list.options.second) 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
end end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
.commencer.form .commencer.form
- if !user_signed_in? - 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 = link_to commencer_sign_up_path(path: @procedure.path), class: ['button large expand primary'] do
Créer un compte Créer un compte
%span.optional-on-small-screens %span.optional-on-small-screens
@ -20,7 +20,7 @@
- elsif drafts.count == 1 && not_drafts.count == 0 - elsif drafts.count == 1 && not_drafts.count == 0
- dossier = drafts.first - dossier = drafts.first
%h1 Vous avez déjà commencé à remplir un dossier %h2.huge-title Vous avez déjà commencé à remplir un dossier
%p %p
Il y a <strong>#{time_ago_in_words(dossier.created_at)}</strong>, 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} ». vous avez commencé à remplir un dossier sur la démarche « #{dossier.procedure.libelle} ».
@ -29,7 +29,7 @@
- elsif not_drafts.count == 1 - elsif not_drafts.count == 1
- dossier = not_drafts.first - 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 %p
Il y a <strong>#{time_ago_in_words(dossier.en_construction_at)}</strong>, 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} ». 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'] = link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand']
- else - 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 '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'] = 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) - dossier = controller.try(:dossier_for_help)
- procedure = controller.try(:procedure_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 .header-inner-content
.flex.align-center .flex.align-center

View file

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

View file

@ -1,5 +1,5 @@
.no-procedure .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 .baseline.center
%h3 Un outil simple %h3 Un outil simple
%p %p

View file

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

View file

@ -6,22 +6,22 @@
%ul.footer-logos %ul.footer-logos
%li.footer-text %li.footer-text
Un service fourni par la 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 %br
et incubé par 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 %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' } %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' } %span.footer-logo.footer-logo-beta-gouv-fr{ role: 'img', 'aria-label': 'beta.gouv.fr' }
%li.footer-column %li.footer-column
%ul.footer-links %ul.footer-links
%li.footer-link %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 %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 %li.footer-link
= link_to "Statistiques", stats_path, :class => "footer-link", data: { turbolinks: false } # Turbolinks disabled for Chartkick. See Issue #350 = link_to "Statistiques", stats_path, :class => "footer-link", data: { turbolinks: false } # Turbolinks disabled for Chartkick. See Issue #350
%li.footer-link %li.footer-link

View file

@ -14,13 +14,13 @@
%em.hero-tagline-em en ligne %em.hero-tagline-em en ligne
.hero-illustration .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 .landing-panel.usagers-panel
.container .container
.role-panel-wrapper .role-panel-wrapper
.role-panel-30.role-usagers-image .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 .role-panel-70
%h1.role-panel-title Vous souhaitez effectuer une demande auprès d'une administration ? %h1.role-panel-title Vous souhaitez effectuer une demande auprès d'une administration ?

View file

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

View file

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

View file

@ -2,5 +2,5 @@
= link_to FAQ_URL, target: "_blank", rel: "noopener" do = link_to FAQ_URL, target: "_blank", rel: "noopener" do
%span.icon.help %span.icon.help
.dropdown-description .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. %p Trouvez votre réponse dans laide en ligne.

View file

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

View file

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

View file

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

View file

@ -2,19 +2,19 @@
.container .container
- service = procedure.service - service = procedure.service
- if service.present? - if service.present?
%ul.footer-row.footer-columns .footer-row.footer-columns
%li.footer-column %ul.footer-column
%h3.footer-header Cette démarche est gérée par : %p.footer-header Cette démarche est gérée par :
%p %li
= service.nom = service.nom
%br %br
= service.organisme = service.organisme
%br %br
= string_to_html(service.adresse) = string_to_html(service.adresse, wrapper_tag = 'span')
%li.footer-column %ul.footer-column
%h3.footer-header Poser une question sur votre dossier : %p.footer-header Poser une question sur votre dossier :
%p %li
- if dossier.present? && dossier.messagerie_available? - if dossier.present? && dossier.messagerie_available?
Directement Directement
= link_to "par la messagerie", messagerie_dossier_path(dossier) = link_to "par la messagerie", messagerie_dossier_path(dossier)
@ -22,21 +22,21 @@
Par email : Par email :
= link_to service.email, "mailto:#{service.email}" = link_to service.email, "mailto:#{service.email}"
%p %li
Par téléphone : Par téléphone :
%a{ href: "tel:#{service.telephone}" }= service.telephone %a{ href: "tel:#{service.telephone}" }= service.telephone
%p %li
- horaires = "Horaires : #{formatted_horaires(service.horaires)}" - horaires = "Horaires : #{formatted_horaires(service.horaires)}"
= simple_format(horaires) = simple_format(horaires, {}, wrapper_tag: 'span')
- politiques = politiques_conservation_de_donnees(procedure) - politiques = politiques_conservation_de_donnees(procedure)
- if politiques.present? - if politiques.present?
%li.footer-column %ul.footer-column
%h3.footer-header Conservation des données : %p.footer-header Conservation des données :
- politiques.each do |politique| - 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 %footer.procedure-footer
.container .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 .auth-form.sign-in-form
= form_for User.new, url: user_session_path, html: { class: "form" } do |f| = 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.label :email, "Email"
= f.text_field :email, autofocus: true = f.text_field :email, autofocus: true

View file

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

View file

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

View file

@ -23,7 +23,6 @@ FOG_OPENSTACK_IDENTITY_API_VERSION=""
FOG_OPENSTACK_REGION="" FOG_OPENSTACK_REGION=""
FOG_DIRECTORY="" FOG_DIRECTORY=""
FOG_ENABLED="" FOG_ENABLED=""
CARRIERWAVE_CACHE_DIR="/tmp/tps-local-cache"
DS_PROXY_URL="" DS_PROXY_URL=""
FC_PARTICULIER_ID="" FC_PARTICULIER_ID=""
@ -46,8 +45,14 @@ SENTRY_DSN_JS=""
MATOMO_ENABLED="disabled" MATOMO_ENABLED="disabled"
MATOMO_ID="73" MATOMO_ID="73"
SENDINBLUE_ENABLED="disabled" SENDINBLUE_BALANCING=""
SENDINBLUE_BALANCING_VALUE=""
SENDINBLUE_ENABLED=""
SENDINBLUE_CLIENT_KEY="" SENDINBLUE_CLIENT_KEY=""
SENDINBLUE_SMTP_KEY=""
SENDINBLUE_USER_NAME=""
CRISP_ENABLED="disabled" CRISP_ENABLED="disabled"
CRISP_CLIENT_KEY="" CRISP_CLIENT_KEY=""

View file

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

View file

@ -77,6 +77,16 @@ Rails.application.configure do
port: '2525', port: '2525',
authentication: :cram_md5 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 else
config.action_mailer.delivery_method = :mailjet config.action_mailer.delivery_method = :mailjet
end end

View file

@ -1,9 +1,8 @@
Rails.application.config.content_security_policy do |policy| 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.development?
if Rails.env.production? # les CSP ne sont pas appliquées en dev: on notifie cependant une url quelconque de la violation
policy.report_uri "https://demarchessimplifieestest.report-uri.com/r/d/csp/reportOnly" # pour détecter les erreurs lors de l'ajout d'une nouvelle brique externe durant le développement
else policy.report_uri "http://#{ENV['APP_HOST']}/csp/"
policy.report_uri "http://#{ENV['APP_HOST']}/csp/" # ne pas notifier report-uri en dev/test
end end
# Whitelist image # Whitelist image
policy.img_src :self, "*.openstreetmap.org", "static.demarches-simplifiees.fr", "*.cloud.ovh.net", "stats.data.gouv.fr", "*", :data 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: activerecord:
attributes: attributes:
commentaire: commentaire:
body: 'Votre message'
file: fichier 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_identity_api_version: "<%= ENV['FOG_OPENSTACK_IDENTITY_API_VERSION'] %>"
openstack_region: <%= ENV['FOG_OPENSTACK_REGION'] %> openstack_region: <%= ENV['FOG_OPENSTACK_REGION'] %>
directory: <%= ENV['FOG_DIRECTORY'] %> directory: <%= ENV['FOG_DIRECTORY'] %>
carrierwave:
cache_dir: <%= ENV['CARRIERWAVE_CACHE_DIR'] %>
mailtrap: mailtrap:
username: <%= ENV['MAILTRAP_USERNAME'] %> username: <%= ENV['MAILTRAP_USERNAME'] %>
password: <%= ENV['MAILTRAP_PASSWORD'] %> password: <%= ENV['MAILTRAP_PASSWORD'] %>
@ -54,7 +52,9 @@ defaults: &defaults
webhook_secret: <%= ENV['HELPSCOUT_WEBHOOK_SECRET'] %> webhook_secret: <%= ENV['HELPSCOUT_WEBHOOK_SECRET'] %>
sendinblue: sendinblue:
enabled: <%= ENV['SENDINBLUE_ENABLED'] == 'enabled' %> enabled: <%= ENV['SENDINBLUE_ENABLED'] == 'enabled' %>
username: <%= ENV['SENDINBLUE_USER_NAME'] %>
client_key: <%= ENV['SENDINBLUE_CLIENT_KEY'] %> client_key: <%= ENV['SENDINBLUE_CLIENT_KEY'] %>
smtp_key: <%= ENV['SENDINBLUE_SMTP_KEY'] %>
api_v3_key: <%= ENV['SENDINBLUE_API_V3_KEY'] %> api_v3_key: <%= ENV['SENDINBLUE_API_V3_KEY'] %>
matomo: matomo:
enabled: <%= ENV['MATOMO_ENABLED'] == 'enabled' %> enabled: <%= ENV['MATOMO_ENABLED'] == 'enabled' %>
@ -82,8 +82,6 @@ test:
key: api_entreprise_test_key key: api_entreprise_test_key
fog: fog:
directory: tps_dev directory: tps_dev
carrierwave:
cache_dir: /tmp/tps-test-cache
pipedrive: pipedrive:
key: pipedrive_test_key key: pipedrive_test_key
france_connect_particulier: 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. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -414,12 +414,10 @@ ActiveRecord::Schema.define(version: 2019_10_24_150452) do
end end
create_table "instructeurs", id: :serial, force: :cascade do |t| create_table "instructeurs", id: :serial, force: :cascade do |t|
t.string "email", default: "", null: false
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.text "encrypted_login_token" t.text "encrypted_login_token"
t.datetime "login_token_created_at" t.datetime "login_token_created_at"
t.index ["email"], name: "index_instructeurs_on_email"
end end
create_table "invites", id: :serial, force: :cascade do |t| 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 describe Admin::AssignsController, type: :controller do
let(:admin) { create(:administrateur) } let(:admin) { create(:administrateur) }
let(:procedure) { create :procedure, administrateur: admin }
let(:instructeur) { create :instructeur, administrateurs: [admin] }
before do before do
sign_in(admin.user) sign_in(admin.user)
end end
describe 'GET #show' do describe 'GET #show' do
subject { get :show, params: { procedure_id: procedure.id } } let(:procedure) { create :procedure, administrateur: admin, instructeurs: [instructeur_assigned_1, instructeur_assigned_2] }
it { expect(subject.status).to eq(200) } 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 end
describe 'PUT #update' do 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' } } 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) } 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') create(:commentaire, dossier: dossier, email: 'test@test.com')
dossier dossier
end 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 let(:query) do
"{ "{
@ -62,31 +71,65 @@ describe API::V2::GraphqlController do
request.env['HTTP_AUTHORIZATION'] = authorization_header request.env['HTTP_AUTHORIZATION'] = authorization_header
end end
it "should return demarche" do context "demarche" do
expect(gql_errors).to eq(nil) it "should be returned" do
expect(gql_data).to eq(demarche: { expect(gql_errors).to eq(nil)
id: procedure.to_typed_id, expect(gql_data).to eq(demarche: {
number: procedure.id.to_s, id: procedure.to_typed_id,
title: procedure.libelle, number: procedure.id,
description: procedure.description, title: procedure.libelle,
state: 'brouillon', description: procedure.description,
archivedAt: nil, state: 'brouillon',
createdAt: procedure.created_at.iso8601, archivedAt: nil,
updatedAt: procedure.updated_at.iso8601, createdAt: procedure.created_at.iso8601,
groupeInstructeurs: [{ instructeurs: [], label: "défaut" }], updatedAt: procedure.updated_at.iso8601,
champDescriptors: procedure.types_de_champ.map do |tdc| groupeInstructeurs: [
{ {
id: tdc.to_typed_id, instructeurs: [{ email: instructeur.email }],
label: tdc.libelle, label: "défaut"
type: tdc.type_champ, }
description: tdc.description, ],
required: tdc.mandatory? 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: { end
nodes: []
} 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 end
context "dossier" do context "dossier" do
@ -130,11 +173,11 @@ describe API::V2::GraphqlController do
}" }"
end end
it "should return dossier" do it "should be returned" do
expect(gql_errors).to eq(nil) expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossier: { expect(gql_data).to eq(dossier: {
id: dossier.to_typed_id, id: dossier.to_typed_id,
number: dossier.id.to_s, number: dossier.id,
state: 'en_construction', state: 'en_construction',
updatedAt: dossier.updated_at.iso8601, updatedAt: dossier.updated_at.iso8601,
datePassageEnConstruction: dossier.en_construction_at.iso8601, datePassageEnConstruction: dossier.en_construction_at.iso8601,
@ -146,7 +189,12 @@ describe API::V2::GraphqlController do
id: dossier.user.to_typed_id, id: dossier.user.to_typed_id,
email: dossier.user.email email: dossier.user.email
}, },
instructeurs: [], instructeurs: [
{
id: instructeur.to_typed_id,
email: instructeur.email
}
],
messages: dossier.commentaires.map do |commentaire| messages: dossier.commentaires.map do |commentaire|
{ {
body: commentaire.body, 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) expect(gql_data[:dossier][:champs][0][:id]).to eq(dossier.champs[0].type_de_champ.to_typed_id)
end end
end end
context "mutations" do
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 end
context "when not authenticated" do context "when not authenticated" do
@ -182,5 +338,26 @@ describe API::V2::GraphqlController do
expect(gql_errors).not_to eq(nil) expect(gql_errors).not_to eq(nil)
end end
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
end end

View file

@ -12,7 +12,7 @@ feature 'As an administrateur', js: true do
end end
scenario 'I can register' do 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) confirmation_email = open_email(admin_email)
token_params = confirmation_email.body.match(/token=[^"]+/) 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(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
end end

View file

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

View file

@ -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([]) } # it { expect(subject).to eq([]) }
# end # end
# 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 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 expect(cloned_procedure.types_de_champ.first.types_de_champ).not_to be_empty
end end
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 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) } it { expect(AdministrationMailer).to have_received(:invite_admin).with(user, nil, administration.id) }
end end
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 end

View file

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

View file

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