Merge branch 'dev' into 4482-echec-initilaisation-env-dev
This commit is contained in:
commit
8f5203cc2e
89 changed files with 1265 additions and 505 deletions
4
Gemfile
4
Gemfile
|
@ -13,12 +13,9 @@ gem 'bcrypt'
|
|||
gem 'bootstrap-sass', '>= 3.4.1'
|
||||
gem 'bootstrap-wysihtml5-rails', '~> 0.3.3.8'
|
||||
gem 'browser'
|
||||
gem 'carrierwave'
|
||||
gem 'carrierwave-i18n'
|
||||
gem 'chartkick'
|
||||
gem 'chunky_png'
|
||||
gem 'clamav-client', require: 'clamav/client'
|
||||
gem 'copy_carrierwave_file'
|
||||
gem 'daemons'
|
||||
gem 'deep_cloneable' # Enable deep clone of active record models
|
||||
gem 'delayed_cron_job' # Cron jobs
|
||||
|
@ -93,6 +90,7 @@ group :test do
|
|||
gem 'shoulda-matchers', require: false
|
||||
gem 'timecop'
|
||||
gem 'vcr'
|
||||
gem 'webdrivers', '~> 4.0'
|
||||
gem 'webmock'
|
||||
end
|
||||
|
||||
|
|
115
Gemfile.lock
115
Gemfile.lock
|
@ -20,25 +20,25 @@ GEM
|
|||
specs:
|
||||
aasm (5.0.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
actioncable (5.2.2.1)
|
||||
actionpack (= 5.2.2.1)
|
||||
actioncable (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailer (5.2.2.1)
|
||||
actionpack (= 5.2.2.1)
|
||||
actionview (= 5.2.2.1)
|
||||
activejob (= 5.2.2.1)
|
||||
actionmailer (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
activejob (= 5.2.3)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.2.2.1)
|
||||
actionview (= 5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
actionpack (5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
rack (~> 2.0)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
actionview (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
|
@ -51,25 +51,25 @@ GEM
|
|||
activemodel (>= 4.1, < 6)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
activejob (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
activerecord (5.2.2.1)
|
||||
activemodel (= 5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
activemodel (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
activerecord (5.2.3)
|
||||
activemodel (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
arel (>= 9.0)
|
||||
activestorage (5.2.2.1)
|
||||
actionpack (= 5.2.2.1)
|
||||
activerecord (= 5.2.2.1)
|
||||
activestorage (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
activerecord (= 5.2.3)
|
||||
marcel (~> 0.3.1)
|
||||
activestorage-openstack (1.0.0)
|
||||
activestorage-openstack (1.2.0)
|
||||
fog-openstack (~> 1.0)
|
||||
marcel
|
||||
mime-types
|
||||
rails (<= 6)
|
||||
activesupport (5.2.2.1)
|
||||
rails (>= 5.2.2)
|
||||
activesupport (5.2.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
|
@ -136,11 +136,6 @@ GEM
|
|||
capybara-selenium (0.0.6)
|
||||
capybara
|
||||
selenium-webdriver
|
||||
carrierwave (1.3.1)
|
||||
activemodel (>= 4.0.0)
|
||||
activesupport (>= 4.0.0)
|
||||
mime-types (>= 1.16)
|
||||
carrierwave-i18n (0.2.0)
|
||||
case_transform (0.2)
|
||||
activesupport
|
||||
chartkick (3.2.0)
|
||||
|
@ -158,11 +153,9 @@ GEM
|
|||
coffee-script-source (1.12.2)
|
||||
concurrent-ruby (1.1.5)
|
||||
connection_pool (2.2.2)
|
||||
copy_carrierwave_file (1.3.0)
|
||||
carrierwave (>= 0.9)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.4)
|
||||
crass (1.0.5)
|
||||
css_parser (1.6.0)
|
||||
addressable
|
||||
curb (0.9.10)
|
||||
|
@ -203,7 +196,7 @@ GEM
|
|||
em-websocket (0.5.1)
|
||||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
erubi (1.8.0)
|
||||
erubi (1.9.0)
|
||||
erubis (2.7.0)
|
||||
ethon (0.11.0)
|
||||
ffi (>= 1.3.0)
|
||||
|
@ -311,7 +304,7 @@ GEM
|
|||
domain_name (~> 0.5)
|
||||
http_parser.rb (0.6.0)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.6.0)
|
||||
i18n (1.7.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ipaddress (0.8.3)
|
||||
jaro_winkler (1.5.2)
|
||||
|
@ -320,7 +313,7 @@ GEM
|
|||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.2.0)
|
||||
json-jwt (1.10.0)
|
||||
json-jwt (1.11.0)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
bindata
|
||||
|
@ -356,7 +349,7 @@ GEM
|
|||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
logstash-event (1.2.02)
|
||||
loofah (2.2.3)
|
||||
loofah (2.3.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
lumberjack (1.0.13)
|
||||
|
@ -375,7 +368,7 @@ GEM
|
|||
mimemagic (0.3.3)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.11.3)
|
||||
minitest (5.13.0)
|
||||
momentjs-rails (2.20.1)
|
||||
railties (>= 3.1)
|
||||
multi_json (1.14.1)
|
||||
|
@ -384,8 +377,8 @@ GEM
|
|||
mustermann (1.0.3)
|
||||
nenv (0.3.0)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.3.1)
|
||||
nokogiri (1.10.4)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.10.5)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
notiffany (0.1.1)
|
||||
nenv (~> 0.1)
|
||||
|
@ -469,18 +462,18 @@ GEM
|
|||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (5.2.2.1)
|
||||
actioncable (= 5.2.2.1)
|
||||
actionmailer (= 5.2.2.1)
|
||||
actionpack (= 5.2.2.1)
|
||||
actionview (= 5.2.2.1)
|
||||
activejob (= 5.2.2.1)
|
||||
activemodel (= 5.2.2.1)
|
||||
activerecord (= 5.2.2.1)
|
||||
activestorage (= 5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
rails (5.2.3)
|
||||
actioncable (= 5.2.3)
|
||||
actionmailer (= 5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
activejob (= 5.2.3)
|
||||
activemodel (= 5.2.3)
|
||||
activerecord (= 5.2.3)
|
||||
activestorage (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 5.2.2.1)
|
||||
railties (= 5.2.3)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.4)
|
||||
actionpack (>= 5.0.1.x)
|
||||
|
@ -489,14 +482,14 @@ GEM
|
|||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.2.0)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
rails-i18n (5.1.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 5.0, < 6)
|
||||
railties (5.2.2.1)
|
||||
actionpack (= 5.2.2.1)
|
||||
activesupport (= 5.2.2.1)
|
||||
railties (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.19.0, < 2.0)
|
||||
|
@ -678,6 +671,10 @@ GEM
|
|||
activemodel (>= 5.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 5.0)
|
||||
webdrivers (4.1.3)
|
||||
nokogiri (~> 1.6)
|
||||
rubyzip (>= 1.3.0)
|
||||
selenium-webdriver (>= 3.0, < 4.0)
|
||||
webfinger (1.1.0)
|
||||
activesupport
|
||||
httpclient (>= 2.4)
|
||||
|
@ -689,9 +686,9 @@ GEM
|
|||
activesupport (>= 4.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 4.2)
|
||||
websocket-driver (0.7.0)
|
||||
websocket-driver (0.7.1)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.3)
|
||||
websocket-extensions (0.1.4)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
xray-rails (0.3.1)
|
||||
|
@ -726,12 +723,9 @@ DEPENDENCIES
|
|||
capybara-email
|
||||
capybara-screenshot
|
||||
capybara-selenium
|
||||
carrierwave
|
||||
carrierwave-i18n
|
||||
chartkick
|
||||
chunky_png
|
||||
clamav-client
|
||||
copy_carrierwave_file
|
||||
daemons
|
||||
database_cleaner
|
||||
deep_cloneable
|
||||
|
@ -812,6 +806,7 @@ DEPENDENCIES
|
|||
vcr
|
||||
warden
|
||||
web-console
|
||||
webdrivers (~> 4.0)
|
||||
webmock
|
||||
webpacker
|
||||
xray-rails
|
||||
|
|
|
@ -43,8 +43,14 @@ Les informations nécessaire à l'initialisation de la base doivent être pré-c
|
|||
> create user tps_test with password 'tps_test' superuser;
|
||||
> \q
|
||||
|
||||
|
||||
### Initialisation de l'environnement de développement
|
||||
|
||||
Sous Ubuntu, certains packages doivent être installés au préalable :
|
||||
|
||||
sudo apt-get install libcurl3 libcurl3-gnutls libcurl4-openssl-dev libcurl4-gnutls-dev zlib1g-dev
|
||||
|
||||
|
||||
Afin d'initialiser l'environnement de développement, exécutez la commande suivante :
|
||||
|
||||
bin/setup
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
|
||||
%horizontal-list {
|
||||
|
@ -17,3 +18,10 @@
|
|||
animation-fill-mode: forwards;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
%outline {
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: 3px solid $blue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
@import "placeholders";
|
||||
|
||||
.button {
|
||||
@extend %outline;
|
||||
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
border-radius: 30px;
|
||||
|
@ -20,11 +23,6 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
filter: saturate(50%);
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
@import "colors";
|
||||
@import "placeholders";
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
|
@ -14,5 +17,7 @@ html {
|
|||
}
|
||||
|
||||
a {
|
||||
@extend %outline;
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@import "constants";
|
||||
@import "colors";
|
||||
@import "placeholders";
|
||||
|
||||
.form {
|
||||
h1 {
|
||||
|
@ -177,6 +178,8 @@
|
|||
|
||||
input[type=checkbox],
|
||||
input[type=radio] {
|
||||
@extend %outline;
|
||||
|
||||
margin-left: 5px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 2 * $default-padding;
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
h4.help-dropdown-title {
|
||||
.help-dropdown-title {
|
||||
font-size: 16px;
|
||||
color: $blue;
|
||||
}
|
||||
|
|
|
@ -365,6 +365,11 @@ $cta-panel-button-border-size: 2px;
|
|||
color: #FFFFFF;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: 3px solid #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
.cta-panel-button-blue {
|
||||
|
|
|
@ -113,3 +113,19 @@ footer {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-site-links {
|
||||
li {
|
||||
display: inline;
|
||||
|
||||
|
||||
&::before {
|
||||
content: "-";
|
||||
margin: $default-spacer;
|
||||
}
|
||||
|
||||
&:first-child::before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
12
app/assets/stylesheets/new_design/title.scss
Normal file
12
app/assets/stylesheets/new_design/title.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -17,8 +17,9 @@ class Admin::AssignsController < AdminController
|
|||
|
||||
not_assign_scope = current_administrateur.instructeurs.where.not(id: assign_scope.ids)
|
||||
|
||||
if params[:filter]
|
||||
not_assign_scope = not_assign_scope.where("email LIKE ?", "%#{params[:filter]}%")
|
||||
if params[:filter].present?
|
||||
filter = params[:filter].downcase.strip
|
||||
not_assign_scope = not_assign_scope.where('users.email LIKE ?', "%#{filter}%")
|
||||
end
|
||||
|
||||
@instructeurs_not_assign = smart_listing_create :instructeurs_not_assign,
|
||||
|
|
|
@ -2,6 +2,7 @@ class API::V1::DossiersController < APIController
|
|||
before_action :fetch_procedure_and_check_token
|
||||
|
||||
DEFAULT_PAGE_SIZE = 100
|
||||
MAX_PAGE_SIZE = 1000
|
||||
ORDER_DIRECTIONS = { 'asc' => :asc, 'desc' => :desc }
|
||||
|
||||
def index
|
||||
|
@ -33,7 +34,12 @@ class API::V1::DossiersController < APIController
|
|||
end
|
||||
|
||||
def per_page # inherited value from will_paginate
|
||||
[params[:resultats_par_page]&.to_i || DEFAULT_PAGE_SIZE, 1000].min
|
||||
resultats_par_page = params[:resultats_par_page]&.to_i
|
||||
if resultats_par_page && resultats_par_page > 0
|
||||
[resultats_par_page, MAX_PAGE_SIZE].min
|
||||
else
|
||||
DEFAULT_PAGE_SIZE
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_procedure_and_check_token
|
||||
|
@ -47,7 +53,7 @@ class API::V1::DossiersController < APIController
|
|||
end
|
||||
|
||||
order = ORDER_DIRECTIONS.fetch(params[:order], :asc)
|
||||
@dossiers = @procedure.dossiers.state_not_brouillon.order_for_api(order)
|
||||
@dossiers = @procedure.dossiers.state_not_brouillon.order_by_created_at(order)
|
||||
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: {}, status: :not_found
|
||||
|
|
|
@ -250,7 +250,7 @@ class ApplicationController < ActionController::Base
|
|||
payload: {
|
||||
DS_SIGN_IN_COUNT: current_user&.sign_in_count,
|
||||
DS_CREATED_AT: current_administrateur&.created_at,
|
||||
DS_ACTIVE: current_administrateur&.active?,
|
||||
DS_ACTIVE: current_user&.active?,
|
||||
DS_ID: current_administrateur&.id,
|
||||
DS_GESTIONNAIRE_ID: current_instructeur&.id,
|
||||
DS_ROLES: current_user_roles
|
||||
|
|
|
@ -14,7 +14,7 @@ module Instructeurs
|
|||
end
|
||||
|
||||
def add_instructeur
|
||||
@instructeur = Instructeur.find_by(email: instructeur_email) ||
|
||||
@instructeur = Instructeur.by_email(instructeur_email) ||
|
||||
create_instructeur(instructeur_email)
|
||||
|
||||
if groupe_instructeur.instructeurs.include?(@instructeur)
|
||||
|
|
|
@ -47,7 +47,7 @@ module NewAdministrateur
|
|||
end
|
||||
|
||||
def add_instructeur
|
||||
@instructeur = Instructeur.find_by(email: instructeur_email) ||
|
||||
@instructeur = Instructeur.by_email(instructeur_email) ||
|
||||
create_instructeur(instructeur_email)
|
||||
|
||||
if groupe_instructeur.instructeurs.include?(@instructeur)
|
||||
|
|
|
@ -48,8 +48,11 @@ module Users
|
|||
end
|
||||
|
||||
def attestation
|
||||
if dossier.attestation.pdf.attached?
|
||||
if dossier.attestation&.pdf&.attached?
|
||||
redirect_to url_for(dossier.attestation.pdf)
|
||||
else
|
||||
flash.notice = "L'attestation n'est plus disponible sur ce dossier."
|
||||
redirect_to dossier_path(dossier)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -18,8 +18,7 @@ class ServiceDashboard < Administrate::BaseDashboard
|
|||
email: Field::String,
|
||||
telephone: Field::String,
|
||||
horaires: Field::String,
|
||||
adresse: Field::String,
|
||||
siret: Field::String
|
||||
adresse: Field::String
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
|
@ -45,8 +44,7 @@ class ServiceDashboard < Administrate::BaseDashboard
|
|||
:email,
|
||||
:telephone,
|
||||
:horaires,
|
||||
:adresse,
|
||||
:siret
|
||||
:adresse
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
module Mutations
|
||||
class BaseMutation < GraphQL::Schema::Mutation
|
||||
class BaseMutation < GraphQL::Schema::RelayClassicMutation
|
||||
end
|
||||
end
|
||||
|
|
41
app/graphql/mutations/create_direct_upload.rb
Normal file
41
app/graphql/mutations/create_direct_upload.rb
Normal 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
|
27
app/graphql/mutations/dossier_envoyer_message.rb
Normal file
27
app/graphql/mutations/dossier_envoyer_message.rb
Normal 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
|
|
@ -10,27 +10,66 @@ type Avis {
|
|||
type CarteChamp implements Champ {
|
||||
geoAreas: [GeoArea!]!
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
}
|
||||
|
||||
interface Champ {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
}
|
||||
|
||||
type ChampDescriptor {
|
||||
"""
|
||||
Description du champ.
|
||||
"""
|
||||
description: String
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
Est-ce que le champ est obligatoire ?
|
||||
"""
|
||||
required: Boolean!
|
||||
|
||||
"""
|
||||
Type de la valeur du champ.
|
||||
"""
|
||||
type: TypeDeChamp!
|
||||
}
|
||||
|
||||
type CheckboxChamp implements Champ {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
value: Boolean!
|
||||
}
|
||||
|
@ -40,16 +79,78 @@ GeoJSON coordinates
|
|||
"""
|
||||
scalar Coordinates
|
||||
|
||||
"""
|
||||
Autogenerated input type of CreateDirectUpload
|
||||
"""
|
||||
input CreateDirectUploadInput {
|
||||
"""
|
||||
File size (bytes)
|
||||
"""
|
||||
byteSize: Int!
|
||||
|
||||
"""
|
||||
MD5 file checksum as base64
|
||||
"""
|
||||
checksum: String!
|
||||
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
File content type
|
||||
"""
|
||||
contentType: String!
|
||||
|
||||
"""
|
||||
Dossier ID
|
||||
"""
|
||||
dossierId: ID!
|
||||
|
||||
"""
|
||||
Original file name
|
||||
"""
|
||||
filename: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated return type of CreateDirectUpload
|
||||
"""
|
||||
type CreateDirectUploadPayload {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
directUpload: DirectUpload!
|
||||
}
|
||||
|
||||
type DateChamp implements Champ {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
value: ISO8601DateTime
|
||||
}
|
||||
|
||||
type DecimalNumberChamp implements Champ {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
value: Float
|
||||
}
|
||||
|
@ -82,25 +183,35 @@ type Demarche {
|
|||
"""
|
||||
before: String
|
||||
|
||||
"""
|
||||
Dossiers déposés depuis la date.
|
||||
"""
|
||||
createdSince: ISO8601DateTime
|
||||
|
||||
"""
|
||||
Returns the first _n_ elements from the list.
|
||||
"""
|
||||
first: Int
|
||||
|
||||
"""
|
||||
Filtrer les dossiers par ID.
|
||||
"""
|
||||
ids: [ID!]
|
||||
|
||||
"""
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
|
||||
"""
|
||||
Dossiers crées depuis la date.
|
||||
L'ordre des dossiers.
|
||||
"""
|
||||
since: ISO8601DateTime
|
||||
order: Order = ASC
|
||||
|
||||
"""
|
||||
Dossiers avec statut.
|
||||
"""
|
||||
state: DossierState
|
||||
|
||||
"""
|
||||
Dossiers mis à jour depuis la date.
|
||||
"""
|
||||
updatedSince: ISO8601DateTime
|
||||
): DossierConnection!
|
||||
groupeInstructeurs: [GroupeInstructeur!]!
|
||||
id: ID!
|
||||
|
@ -108,7 +219,11 @@ type Demarche {
|
|||
"""
|
||||
Le numero de la démarche.
|
||||
"""
|
||||
number: ID!
|
||||
number: Int!
|
||||
|
||||
"""
|
||||
L'état de la démarche.
|
||||
"""
|
||||
state: DemarcheState!
|
||||
title: String!
|
||||
updatedAt: ISO8601DateTime!
|
||||
|
@ -131,6 +246,31 @@ enum DemarcheState {
|
|||
publiee
|
||||
}
|
||||
|
||||
"""
|
||||
Represents direct upload credentials
|
||||
"""
|
||||
type DirectUpload {
|
||||
"""
|
||||
Created blob record ID
|
||||
"""
|
||||
blobId: ID!
|
||||
|
||||
"""
|
||||
HTTP request headers (JSON-encoded)
|
||||
"""
|
||||
headers: String!
|
||||
|
||||
"""
|
||||
Created blob record signed ID
|
||||
"""
|
||||
signedBlobId: ID!
|
||||
|
||||
"""
|
||||
Upload URL
|
||||
"""
|
||||
url: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Un dossier
|
||||
"""
|
||||
|
@ -163,7 +303,7 @@ type Dossier {
|
|||
"""
|
||||
Le numero du dossier.
|
||||
"""
|
||||
number: ID!
|
||||
number: Int!
|
||||
|
||||
"""
|
||||
L'état du dossier.
|
||||
|
@ -212,10 +352,45 @@ type DossierEdge {
|
|||
node: Dossier
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated input type of DossierEnvoyerMessage
|
||||
"""
|
||||
input DossierEnvoyerMessageInput {
|
||||
attachment: ID
|
||||
body: String!
|
||||
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
dossierId: ID!
|
||||
instructeurId: ID!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated return type of DossierEnvoyerMessage
|
||||
"""
|
||||
type DossierEnvoyerMessagePayload {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
errors: [ValidationError!]
|
||||
message: Message
|
||||
}
|
||||
|
||||
type DossierLinkChamp implements Champ {
|
||||
dossier: Dossier
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
}
|
||||
|
||||
|
@ -254,22 +429,17 @@ interface GeoArea {
|
|||
|
||||
enum GeoAreaSource {
|
||||
"""
|
||||
translation missing: fr.activerecord.attributes.geo_area.source.cadastre
|
||||
Parcelle cadastrale
|
||||
"""
|
||||
cadastre
|
||||
|
||||
"""
|
||||
translation missing: fr.activerecord.attributes.geo_area.source.parcelle_agricole
|
||||
"""
|
||||
parcelle_agricole
|
||||
|
||||
"""
|
||||
translation missing: fr.activerecord.attributes.geo_area.source.quartier_prioritaire
|
||||
Quartier prioritaire
|
||||
"""
|
||||
quartier_prioritaire
|
||||
|
||||
"""
|
||||
translation missing: fr.activerecord.attributes.geo_area.source.selection_utilisateur
|
||||
Sélection utilisateur
|
||||
"""
|
||||
selection_utilisateur
|
||||
}
|
||||
|
@ -295,16 +465,32 @@ scalar ISO8601DateTime
|
|||
|
||||
type IntegerNumberChamp implements Champ {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
value: Int
|
||||
}
|
||||
|
||||
type LinkedDropDownListChamp implements Champ {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
primaryValue: String
|
||||
secondaryValue: String
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
}
|
||||
|
||||
|
@ -318,12 +504,41 @@ type Message {
|
|||
|
||||
type MultipleDropDownListChamp implements Champ {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
values: [String!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
"""
|
||||
File information required to prepare a direct upload
|
||||
"""
|
||||
createDirectUpload(input: CreateDirectUploadInput!): CreateDirectUploadPayload
|
||||
|
||||
"""
|
||||
Envoyer un message à l'usager du dossier.
|
||||
"""
|
||||
dossierEnvoyerMessage(input: DossierEnvoyerMessageInput!): DossierEnvoyerMessagePayload
|
||||
}
|
||||
|
||||
enum Order {
|
||||
"""
|
||||
L‘ordre ascendant.
|
||||
"""
|
||||
ASC
|
||||
|
||||
"""
|
||||
L‘ordre descendant.
|
||||
"""
|
||||
DESC
|
||||
}
|
||||
|
||||
"""
|
||||
|
@ -383,7 +598,15 @@ type PersonneMorale {
|
|||
|
||||
type PieceJustificativeChamp implements Champ {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
url: URL
|
||||
}
|
||||
|
@ -410,7 +633,7 @@ type Query {
|
|||
"""
|
||||
Numéro de la démarche.
|
||||
"""
|
||||
number: ID!
|
||||
number: Int!
|
||||
): Demarche!
|
||||
|
||||
"""
|
||||
|
@ -420,14 +643,22 @@ type Query {
|
|||
"""
|
||||
Numéro du dossier.
|
||||
"""
|
||||
number: ID!
|
||||
number: Int!
|
||||
): Dossier!
|
||||
}
|
||||
|
||||
type RepetitionChamp implements Champ {
|
||||
champs: [Champ!]!
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
}
|
||||
|
||||
|
@ -440,13 +671,29 @@ type SelectionUtilisateur implements GeoArea {
|
|||
type SiretChamp implements Champ {
|
||||
etablissement: PersonneMorale
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
}
|
||||
|
||||
type TextChamp implements Champ {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Libellé du champ.
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
La valeur du champ sous forme texte.
|
||||
"""
|
||||
stringValue: String
|
||||
value: String
|
||||
}
|
||||
|
@ -592,3 +839,13 @@ enum TypeDeChamp {
|
|||
A valid URL, transported as a string
|
||||
"""
|
||||
scalar URL
|
||||
|
||||
"""
|
||||
Éreur de validation
|
||||
"""
|
||||
type ValidationError {
|
||||
"""
|
||||
A description of the error
|
||||
"""
|
||||
message: String!
|
||||
}
|
|
@ -9,9 +9,9 @@ module Types
|
|||
end
|
||||
|
||||
global_id_field :id
|
||||
field :type, TypeDeChampType, null: false, method: :type_champ
|
||||
field :label, String, null: false, method: :libelle
|
||||
field :description, String, null: true
|
||||
field :required, Boolean, null: false, method: :mandatory?
|
||||
field :type, TypeDeChampType, "Type de la valeur du champ.", null: false, method: :type_champ
|
||||
field :label, String, "Libellé du champ.", null: false, method: :libelle
|
||||
field :description, String, "Description du champ.", null: true
|
||||
field :required, Boolean, "Est-ce que le champ est obligatoire ?", null: false, method: :mandatory?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,8 +3,8 @@ module Types
|
|||
include Types::BaseInterface
|
||||
|
||||
global_id_field :id
|
||||
field :label, String, null: false, method: :libelle
|
||||
field :string_value, String, null: true, method: :for_api_v2
|
||||
field :label, String, "Libellé du champ.", null: false, method: :libelle
|
||||
field :string_value, String, "La valeur du champ sous forme texte.", null: true, method: :for_api_v2
|
||||
|
||||
definition_methods do
|
||||
def resolve_type(object, context)
|
||||
|
|
|
@ -9,10 +9,10 @@ module Types
|
|||
description "Une demarche"
|
||||
|
||||
global_id_field :id
|
||||
field :number, ID, "Le numero de la démarche.", null: false, method: :id
|
||||
field :number, Int, "Le numero de la démarche.", null: false, method: :id
|
||||
field :title, String, null: false, method: :libelle
|
||||
field :description, String, "Déscription de la démarche.", null: false
|
||||
field :state, DemarcheState, null: false
|
||||
field :state, DemarcheState, "L'état de la démarche.", null: false
|
||||
|
||||
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||
|
@ -21,8 +21,10 @@ module Types
|
|||
field :groupe_instructeurs, [Types::GroupeInstructeurType], null: false
|
||||
|
||||
field :dossiers, Types::DossierType.connection_type, "Liste de tous les dossiers d'une démarche.", null: false do
|
||||
argument :ids, [ID], required: false, description: "Filtrer les dossiers par ID."
|
||||
argument :since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers crées depuis la date."
|
||||
argument :order, Types::Order, default_value: :asc, required: false, description: "L'ordre des dossiers."
|
||||
argument :created_since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers déposés depuis la date."
|
||||
argument :updated_since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers mis à jour depuis la date."
|
||||
argument :state, Types::DossierType::DossierState, required: false, description: "Dossiers avec statut."
|
||||
end
|
||||
|
||||
field :champ_descriptors, [Types::ChampDescriptorType], null: false, method: :types_de_champ
|
||||
|
@ -36,15 +38,21 @@ module Types
|
|||
Loaders::Association.for(object.class, groupe_instructeurs: { procedure: [:administrateurs] }).load(object)
|
||||
end
|
||||
|
||||
def dossiers(ids: nil, since: nil)
|
||||
dossiers = object.dossiers.for_api_v2
|
||||
def dossiers(updated_since: nil, created_since: nil, state: nil, order:)
|
||||
dossiers = object.dossiers.state_not_brouillon.for_api_v2
|
||||
|
||||
if ids.present?
|
||||
dossiers = dossiers.where(id: ids)
|
||||
if state.present?
|
||||
dossiers = dossiers.where(state: state)
|
||||
end
|
||||
|
||||
if since.present?
|
||||
dossiers = dossiers.since(since)
|
||||
if updated_since.present?
|
||||
dossiers = dossiers.updated_since(updated_since).order_by_updated_at(order)
|
||||
else
|
||||
if created_since.present?
|
||||
dossiers = dossiers.created_since(created_since)
|
||||
end
|
||||
|
||||
dossiers = dossiers.order_by_created_at(order)
|
||||
end
|
||||
|
||||
dossiers
|
||||
|
|
|
@ -9,7 +9,7 @@ module Types
|
|||
description "Un dossier"
|
||||
|
||||
global_id_field :id
|
||||
field :number, ID, "Le numero du dossier.", null: false, method: :id
|
||||
field :number, Int, "Le numero du dossier.", null: false, method: :id
|
||||
field :state, DossierState, "L'état du dossier.", null: false
|
||||
field :updated_at, GraphQL::Types::ISO8601DateTime, "Date de dernière mise à jour.", null: false
|
||||
|
||||
|
|
|
@ -4,11 +4,13 @@ module Types
|
|||
|
||||
class GeoAreaSource < Types::BaseEnum
|
||||
GeoArea.sources.each do |symbol_name, string_name|
|
||||
if string_name != "parcelle_agricole"
|
||||
value(string_name,
|
||||
I18n.t(symbol_name, scope: [:activerecord, :attributes, :geo_area, :source]),
|
||||
value: symbol_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
global_id_field :id
|
||||
field :source, GeoAreaSource, null: false
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
module Types
|
||||
class MutationType < Types::BaseObject
|
||||
field :create_direct_upload, mutation: Mutations::CreateDirectUpload
|
||||
|
||||
field :dossier_envoyer_message, mutation: Mutations::DossierEnvoyerMessage
|
||||
end
|
||||
end
|
||||
|
|
6
app/graphql/types/order.rb
Normal file
6
app/graphql/types/order.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
module Types
|
||||
class Order < Types::BaseEnum
|
||||
value('ASC', 'L‘ordre ascendant.', value: :asc)
|
||||
value('DESC', 'L‘ordre descendant.', value: :desc)
|
||||
end
|
||||
end
|
|
@ -1,11 +1,11 @@
|
|||
module Types
|
||||
class QueryType < Types::BaseObject
|
||||
field :demarche, DemarcheType, null: false, description: "Informations concernant une démarche." do
|
||||
argument :number, ID, "Numéro de la démarche.", required: true
|
||||
argument :number, Int, "Numéro de la démarche.", required: true
|
||||
end
|
||||
|
||||
field :dossier, DossierType, null: false, description: "Informations sur un dossier d'une démarche." do
|
||||
argument :number, ID, "Numéro du dossier.", required: true
|
||||
argument :number, Int, "Numéro du dossier.", required: true
|
||||
end
|
||||
|
||||
def demarche(number:)
|
||||
|
|
11
app/graphql/types/validation_error_type.rb
Normal file
11
app/graphql/types/validation_error_type.rb
Normal 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
|
|
@ -1,6 +1,6 @@
|
|||
module StringToHtmlHelper
|
||||
def string_to_html(str)
|
||||
html_formatted = simple_format(str)
|
||||
def string_to_html(str, wrapper_tag = 'p')
|
||||
html_formatted = simple_format(str, {}, { wrapper_tag: wrapper_tag })
|
||||
with_links = html_formatted.gsub(URI.regexp, '<a target="_blank" rel="noopener" href="\0">\0</a>')
|
||||
sanitize(with_links, attributes: ['target', 'rel', 'href'])
|
||||
end
|
||||
|
|
|
@ -56,7 +56,9 @@ function addTypeDeChamp(state, typeDeChamps, insertAfter, done) {
|
|||
state.flash.success();
|
||||
done();
|
||||
if (insertAfter) {
|
||||
scrollToComponent(insertAfter.target.nextElementSibling);
|
||||
scrollToComponent(insertAfter.target.nextElementSibling, {
|
||||
duration: 300
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(message => state.flash.error(message));
|
||||
|
@ -219,7 +221,7 @@ function getUpdateHandler(typeDeChamp, { queue, flash }) {
|
|||
}
|
||||
|
||||
function findItemToInsertAfter() {
|
||||
const target = getFirstTarget();
|
||||
const target = getLastVisibleTypeDeChamp();
|
||||
|
||||
return {
|
||||
target,
|
||||
|
@ -227,8 +229,10 @@ function findItemToInsertAfter() {
|
|||
};
|
||||
}
|
||||
|
||||
function getFirstTarget() {
|
||||
const [target] = document.querySelectorAll('[data-in-view]');
|
||||
function getLastVisibleTypeDeChamp() {
|
||||
const typeDeChamps = document.querySelectorAll('[data-in-view]');
|
||||
const target = typeDeChamps[typeDeChamps.length - 1];
|
||||
|
||||
if (target) {
|
||||
const parentTarget = target.closest('[data-repetition]');
|
||||
if (parentTarget) {
|
||||
|
|
|
@ -12,11 +12,14 @@ class ApiCarto::API
|
|||
private
|
||||
|
||||
def self.call(url, geojson)
|
||||
params = geojson.to_s
|
||||
RestClient.post(url, params, content_type: 'application/json')
|
||||
response = Typhoeus.post(url, body: geojson.to_s, headers: { 'content-type' => 'application/json' })
|
||||
|
||||
rescue RestClient::InternalServerError, RestClient::BadGateway, RestClient::GatewayTimeout, RestClient::ServiceUnavailable => e
|
||||
Rails.logger.error "[ApiCarto] Error on #{url}: #{e}"
|
||||
if response.success?
|
||||
response.body
|
||||
else
|
||||
message = response.code == 0 ? response.return_message : response.code.to_s
|
||||
Rails.logger.error "[ApiCarto] Error on #{url}: #{message}"
|
||||
raise RestClient::ResourceNotFound
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,11 @@ class ApplicationMailer < ActionMailer::Base
|
|||
default from: "demarches-simplifiees.fr <#{CONTACT_EMAIL}>"
|
||||
layout 'mailer'
|
||||
|
||||
# Don’t retry to send a message if the server rejects the recipient address
|
||||
rescue_from Net::SMTPSyntaxError do |_error|
|
||||
message.perform_deliveries = false
|
||||
end
|
||||
|
||||
# Attach the procedure logo to the email (if any).
|
||||
# Returns the attachment url.
|
||||
def attach_logo(procedure)
|
||||
|
|
|
@ -46,7 +46,7 @@ class Administrateur < ApplicationRecord
|
|||
end
|
||||
|
||||
def registration_state
|
||||
if active?
|
||||
if user.active?
|
||||
'Actif'
|
||||
elsif user.reset_password_period_valid?
|
||||
'En attente'
|
||||
|
@ -56,17 +56,7 @@ class Administrateur < ApplicationRecord
|
|||
end
|
||||
|
||||
def invitation_expired?
|
||||
!active? && !user.reset_password_period_valid?
|
||||
end
|
||||
|
||||
def self.reset_password(reset_password_token, password)
|
||||
administrateur = self.reset_password_by_token({
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
reset_password_token: reset_password_token
|
||||
})
|
||||
|
||||
administrateur
|
||||
!user.active? && !user.reset_password_period_valid?
|
||||
end
|
||||
|
||||
def owns?(procedure)
|
||||
|
@ -80,8 +70,4 @@ class Administrateur < ApplicationRecord
|
|||
def can_be_deleted?
|
||||
dossiers.state_instruction_commencee.none? && procedures.none?
|
||||
end
|
||||
|
||||
def active?
|
||||
user.last_sign_in_at.present?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,6 +33,12 @@ class Champs::RepetitionChamp < Champ
|
|||
end
|
||||
end
|
||||
|
||||
# We have to truncate the label here as spreadsheets have a (30 char) limit on length.
|
||||
def libelle_for_export
|
||||
str = "(#{type_de_champ.stable_id}) #{libelle}"
|
||||
ActiveStorage::Filename.new(str).sanitized.truncate(30)
|
||||
end
|
||||
|
||||
class Row < Hashie::Dash
|
||||
property :index
|
||||
property :dossier_id
|
||||
|
|
|
@ -10,7 +10,7 @@ class Commentaire < ApplicationRecord
|
|||
|
||||
has_one_attached :piece_jointe
|
||||
|
||||
validates :body, presence: { message: "Votre message ne peut être vide" }
|
||||
validates :body, presence: { message: "ne peut être vide" }
|
||||
|
||||
default_scope { order(created_at: :asc) }
|
||||
scope :updated_since?, -> (date) { where('commentaires.updated_at > ?', date) }
|
||||
|
|
|
@ -105,7 +105,9 @@ class Dossier < ApplicationRecord
|
|||
scope :not_archived, -> { where(archived: false) }
|
||||
|
||||
scope :order_by_updated_at, -> (order = :desc) { order(updated_at: order) }
|
||||
scope :order_for_api, -> (order = :asc) { order(en_construction_at: order, created_at: order, id: order) }
|
||||
scope :order_by_created_at, -> (order = :asc) { order(en_construction_at: order, created_at: order, id: order) }
|
||||
scope :updated_since, -> (since) { where('dossiers.updated_at >= ?', since) }
|
||||
scope :created_since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
|
||||
|
||||
scope :all_state, -> { not_archived.state_not_brouillon }
|
||||
scope :en_construction, -> { not_archived.state_en_construction }
|
||||
|
@ -134,7 +136,6 @@ class Dossier < ApplicationRecord
|
|||
scope :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) }
|
||||
scope :with_champs, -> { includes(champs: :type_de_champ) }
|
||||
scope :nearing_end_of_retention, -> (duration = '1 month') { joins(:procedure).where("en_instruction_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - now() < interval ?", duration) }
|
||||
scope :since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
|
||||
scope :for_api, -> {
|
||||
includes(commentaires: { piece_jointe_attachment: :blob },
|
||||
champs: [
|
||||
|
@ -472,7 +473,19 @@ class Dossier < ApplicationRecord
|
|||
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
|
||||
end
|
||||
|
||||
def spreadsheet_columns
|
||||
def spreadsheet_columns_csv
|
||||
spreadsheet_columns(with_etablissement: true)
|
||||
end
|
||||
|
||||
def spreadsheet_columns_xlsx
|
||||
spreadsheet_columns
|
||||
end
|
||||
|
||||
def spreadsheet_columns_ods
|
||||
spreadsheet_columns
|
||||
end
|
||||
|
||||
def spreadsheet_columns(with_etablissement: false)
|
||||
columns = [
|
||||
['ID', id.to_s],
|
||||
['Email', user.email]
|
||||
|
@ -485,6 +498,39 @@ class Dossier < ApplicationRecord
|
|||
['Prénom', individual&.prenom],
|
||||
['Date de naissance', individual&.birthdate]
|
||||
]
|
||||
elsif with_etablissement
|
||||
columns += [
|
||||
['Établissement SIRET', etablissement&.siret],
|
||||
['Établissement siège social', etablissement&.siege_social],
|
||||
['Établissement NAF', etablissement&.naf],
|
||||
['Établissement libellé NAF', etablissement&.libelle_naf],
|
||||
['Établissement Adresse', etablissement&.adresse],
|
||||
['Établissement numero voie', etablissement&.numero_voie],
|
||||
['Établissement type voie', etablissement&.type_voie],
|
||||
['Établissement nom voie', etablissement&.nom_voie],
|
||||
['Établissement complément adresse', etablissement&.complement_adresse],
|
||||
['Établissement code postal', etablissement&.code_postal],
|
||||
['Établissement localité', etablissement&.localite],
|
||||
['Établissement code INSEE localité', etablissement&.code_insee_localite],
|
||||
['Entreprise SIREN', etablissement&.entreprise_siren],
|
||||
['Entreprise capital social', etablissement&.entreprise_capital_social],
|
||||
['Entreprise numero TVA intracommunautaire', etablissement&.entreprise_numero_tva_intracommunautaire],
|
||||
['Entreprise forme juridique', etablissement&.entreprise_forme_juridique],
|
||||
['Entreprise forme juridique code', etablissement&.entreprise_forme_juridique_code],
|
||||
['Entreprise nom commercial', etablissement&.entreprise_nom_commercial],
|
||||
['Entreprise raison sociale', etablissement&.entreprise_raison_sociale],
|
||||
['Entreprise SIRET siège social', etablissement&.entreprise_siret_siege_social],
|
||||
['Entreprise code effectif entreprise', etablissement&.entreprise_code_effectif_entreprise],
|
||||
['Entreprise date de création', etablissement&.entreprise_date_creation],
|
||||
['Entreprise nom', etablissement&.entreprise_nom],
|
||||
['Entreprise prénom', etablissement&.entreprise_prenom],
|
||||
['Association RNA', etablissement&.association_rna],
|
||||
['Association titre', etablissement&.association_titre],
|
||||
['Association objet', etablissement&.association_objet],
|
||||
['Association date de création', etablissement&.association_date_creation],
|
||||
['Association date de déclaration', etablissement&.association_date_declaration],
|
||||
['Association date de publication', etablissement&.association_date_publication]
|
||||
]
|
||||
else
|
||||
columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
|
||||
end
|
||||
|
|
16
app/models/dynamic_smtp_settings_interceptor.rb
Normal file
16
app/models/dynamic_smtp_settings_interceptor.rb
Normal 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
|
|
@ -10,7 +10,7 @@ class Individual < ApplicationRecord
|
|||
GENDER_FEMALE = 'Mme'
|
||||
|
||||
def self.create_from_france_connect(fc_information)
|
||||
create(
|
||||
create!(
|
||||
nom: fc_information.family_name,
|
||||
prenom: fc_information.given_name,
|
||||
gender: fc_information.gender == 'female' ? GENDER_FEMALE : GENDER_MALE
|
||||
|
|
|
@ -444,7 +444,7 @@ class Procedure < ApplicationRecord
|
|||
version = options.delete(:version)
|
||||
if version == 'v2'
|
||||
options.delete(:tables)
|
||||
ProcedureExportV2Service.new(self, dossiers, **options.to_h.symbolize_keys)
|
||||
ProcedureExportV2Service.new(self, dossiers)
|
||||
else
|
||||
ProcedureExportService.new(self, dossiers, **options.to_h.symbolize_keys)
|
||||
end
|
||||
|
@ -595,7 +595,8 @@ class Procedure < ApplicationRecord
|
|||
|
||||
def move_type_de_champ_attributes(types_de_champ, type_de_champ, new_index)
|
||||
old_index = types_de_champ.index(type_de_champ)
|
||||
types_de_champ.insert(new_index, types_de_champ.delete_at(old_index))
|
||||
if types_de_champ.delete_at(old_index)
|
||||
types_de_champ.insert(new_index, type_de_champ)
|
||||
.map.with_index do |type_de_champ, index|
|
||||
{
|
||||
id: type_de_champ.id,
|
||||
|
@ -603,6 +604,9 @@ class Procedure < ApplicationRecord
|
|||
order_place: index
|
||||
}
|
||||
end
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def before_publish
|
||||
|
|
|
@ -53,7 +53,7 @@ class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBas
|
|||
|
||||
def check_presence_of_primary_options
|
||||
if !PRIMARY_PATTERN.match?(drop_down_list.options.second)
|
||||
errors.add(libelle, "doit commencer par une entrée de menu primaire de la forme <code style='white-space: pre-wrap;'>--texte--</code>")
|
||||
errors.add(libelle.presence || "La liste", "doit commencer par une entrée de menu primaire de la forme <code style='white-space: pre-wrap;'>--texte--</code>")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ class User < ApplicationRecord
|
|||
def invite_administrateur!(administration_id)
|
||||
reset_password_token = nil
|
||||
|
||||
if !administrateur.active?
|
||||
if !active?
|
||||
reset_password_token = set_reset_password_token
|
||||
end
|
||||
|
||||
|
@ -92,6 +92,10 @@ class User < ApplicationRecord
|
|||
"User:#{id}"
|
||||
end
|
||||
|
||||
def active?
|
||||
last_sign_in_at.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_invites!
|
||||
|
|
|
@ -29,7 +29,7 @@ class AdministrateurUsageStatisticsService
|
|||
result = {
|
||||
ds_sign_in_count: administrateur.user.sign_in_count,
|
||||
ds_created_at: administrateur.created_at,
|
||||
ds_active: administrateur.active?,
|
||||
ds_active: administrateur.user.active?,
|
||||
ds_id: administrateur.id,
|
||||
nb_services: nb_services_by_administrateur_id[administrateur.id],
|
||||
nb_instructeurs: nb_instructeurs_by_administrateur_id[administrateur.id],
|
||||
|
|
|
@ -49,7 +49,7 @@ class ProcedureExportService
|
|||
:prenom
|
||||
]
|
||||
|
||||
def initialize(procedure, dossiers, tables: [], ids: nil, since: nil, limit: nil)
|
||||
def initialize(procedure, dossiers, tables: [])
|
||||
@procedure = procedure
|
||||
|
||||
@attributes = ATTRIBUTES.dup
|
||||
|
@ -59,15 +59,6 @@ class ProcedureExportService
|
|||
end
|
||||
|
||||
@dossiers = dossiers.downloadable_sorted
|
||||
if ids
|
||||
@dossiers = @dossiers.where(id: ids)
|
||||
end
|
||||
if since
|
||||
@dossiers = @dossiers.since(since)
|
||||
end
|
||||
if limit
|
||||
@dossiers = @dossiers.limit(limit)
|
||||
end
|
||||
@dossiers = @dossiers.to_a
|
||||
@tables = tables.map(&:to_sym)
|
||||
end
|
||||
|
|
|
@ -1,36 +1,27 @@
|
|||
class ProcedureExportV2Service
|
||||
attr_reader :dossiers
|
||||
|
||||
def initialize(procedure, dossiers, ids: nil, since: nil, limit: nil)
|
||||
def initialize(procedure, dossiers)
|
||||
@procedure = procedure
|
||||
@dossiers = dossiers.downloadable_sorted
|
||||
if ids
|
||||
@dossiers = @dossiers.where(id: ids)
|
||||
end
|
||||
if since
|
||||
@dossiers = @dossiers.since(since)
|
||||
end
|
||||
if limit
|
||||
@dossiers = @dossiers.limit(limit)
|
||||
end
|
||||
@tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
|
||||
end
|
||||
|
||||
def to_csv(table = :dossiers)
|
||||
SpreadsheetArchitect.to_csv(options_for(table))
|
||||
def to_csv
|
||||
SpreadsheetArchitect.to_csv(options_for(:dossiers, :csv))
|
||||
end
|
||||
|
||||
def to_xlsx
|
||||
# We recursively build multi page spreadsheet
|
||||
@tables.reduce(nil) do |package, table|
|
||||
SpreadsheetArchitect.to_axlsx_package(options_for(table), package)
|
||||
SpreadsheetArchitect.to_axlsx_package(options_for(table, :xlsx), package)
|
||||
end.to_stream.read
|
||||
end
|
||||
|
||||
def to_ods
|
||||
# We recursively build multi page spreadsheet
|
||||
@tables.reduce(nil) do |spreadsheet, table|
|
||||
SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table), spreadsheet)
|
||||
SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table, :ods), spreadsheet)
|
||||
end.bytes
|
||||
end
|
||||
|
||||
|
@ -53,7 +44,7 @@ class ProcedureExportV2Service
|
|||
[dossier.champs, dossier.champs_private]
|
||||
.flatten
|
||||
.filter { |champ| champ.is_a?(Champs::RepetitionChamp) }
|
||||
end.group_by(&:libelle)
|
||||
end.group_by(&:libelle_for_export)
|
||||
end
|
||||
|
||||
def champs_repetables_options
|
||||
|
@ -70,21 +61,16 @@ class ProcedureExportV2Service
|
|||
row_style: { background_color: nil, color: "000000", font_size: 12 }
|
||||
}
|
||||
|
||||
def sanitize_sheet_name(name)
|
||||
ActiveStorage::Filename.new(name.to_s).sanitized.truncate(30)
|
||||
end
|
||||
|
||||
def options_for(table)
|
||||
def options_for(table, format)
|
||||
case table
|
||||
when :dossiers
|
||||
{ instances: dossiers.to_a, sheet_name: 'Dossiers' }.merge(DEFAULT_STYLES)
|
||||
{ instances: dossiers.to_a, sheet_name: 'Dossiers', spreadsheet_columns: :"spreadsheet_columns_#{format}" }.merge(DEFAULT_STYLES)
|
||||
when :etablissements
|
||||
{ instances: etablissements.to_a, sheet_name: 'Etablissements' }.merge(DEFAULT_STYLES)
|
||||
when :avis
|
||||
{ instances: avis.to_a, sheet_name: 'Avis' }.merge(DEFAULT_STYLES)
|
||||
when Array
|
||||
# We have to truncate the label here as spreadsheets have a (30 char) limit on length.
|
||||
{ instances: table.last, sheet_name: sanitize_sheet_name(table.first) }.merge(DEFAULT_STYLES)
|
||||
{ instances: table.last, sheet_name: table.first }.merge(DEFAULT_STYLES)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
- champs = champ.rows.last
|
||||
- index = (champ.rows.size - 1) * champs.size
|
||||
%div{ class: "row row-#{champs.first.row}" }
|
||||
- if champs.present?
|
||||
- index = (champ.rows.size - 1) * champs.size
|
||||
%div{ class: "row row-#{champs.first.row}" }
|
||||
- champs.each.with_index(index) do |champ, index|
|
||||
= fields_for "#{attribute}[#{index}]", champ do |form|
|
||||
= render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form }
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.commencer.form
|
||||
- if !user_signed_in?
|
||||
%h1 Commencer la démarche
|
||||
%h2.huge-title Commencer la démarche
|
||||
= link_to commencer_sign_up_path(path: @procedure.path), class: ['button large expand primary'] do
|
||||
Créer un compte
|
||||
%span.optional-on-small-screens
|
||||
|
@ -20,7 +20,7 @@
|
|||
|
||||
- elsif drafts.count == 1 && not_drafts.count == 0
|
||||
- dossier = drafts.first
|
||||
%h1 Vous avez déjà commencé à remplir un dossier
|
||||
%h2.huge-title Vous avez déjà commencé à remplir un dossier
|
||||
%p
|
||||
Il y a <strong>#{time_ago_in_words(dossier.created_at)}</strong>,
|
||||
vous avez commencé à remplir un dossier sur la démarche « #{dossier.procedure.libelle} ».
|
||||
|
@ -29,7 +29,7 @@
|
|||
|
||||
- elsif not_drafts.count == 1
|
||||
- dossier = not_drafts.first
|
||||
%h1 Vous avez déjà déposé un dossier
|
||||
%h2.huge-title Vous avez déjà déposé un dossier
|
||||
%p
|
||||
Il y a <strong>#{time_ago_in_words(dossier.en_construction_at)}</strong>,
|
||||
vous avez déposé un dossier sur la démarche « #{dossier.procedure.libelle} ».
|
||||
|
@ -37,6 +37,6 @@
|
|||
= link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand']
|
||||
|
||||
- else
|
||||
%h1 Vous avez déjà des dossiers pour cette démarche
|
||||
%h2.huge-title Vous avez déjà des dossiers pour cette démarche
|
||||
= link_to 'Voir mes dossiers en cours', dossiers_path, class: ['button large expand primary']
|
||||
= link_to 'Commencer un nouveau dossier', url_for_new_dossier(@procedure), class: ['button large expand']
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
- dossier = controller.try(:dossier_for_help)
|
||||
- procedure = controller.try(:procedure_for_help)
|
||||
|
||||
.new-header{ class: current_page?(root_path) ? nil : "new-header-with-border" }
|
||||
%header.new-header{ class: current_page?(root_path) ? nil : "new-header-with-border" }
|
||||
.header-inner-content
|
||||
|
||||
.flex.align-center
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
Env Test
|
||||
|
||||
= render partial: "layouts/new_header"
|
||||
%main
|
||||
= render partial: "layouts/flash_messages"
|
||||
= content_for?(:content) ? yield(:content) : yield
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.no-procedure
|
||||
= image_tag "landing/hero/dematerialiser.svg", class: "paperless-logo"
|
||||
= image_tag "landing/hero/dematerialiser.svg", class: "paperless-logo", alt: "moins de papier"
|
||||
.baseline.center
|
||||
%h3 Un outil simple
|
||||
%p
|
||||
|
|
|
@ -30,14 +30,14 @@
|
|||
.procedure-list-element
|
||||
Administrateurs
|
||||
|
||||
- if !feature_enabled?(:routage)
|
||||
- if !feature_enabled?(:administrateur_routage)
|
||||
%a#onglet-instructeurs{ href: url_for(admin_procedure_assigns_path(@procedure)) }
|
||||
.procedure-list-element{ class: ('active' if active == 'Instructeurs') }
|
||||
Instructeurs
|
||||
- if @procedure.missing_steps.include?(:instructeurs)
|
||||
%p.missing-steps (à compléter)
|
||||
|
||||
- if feature_enabled?(:routage)
|
||||
- if feature_enabled?(:administrateur_routage)
|
||||
%a#onglet-instructeurs{ href: url_for(procedure_groupe_instructeurs_path(@procedure)) }
|
||||
.procedure-list-element
|
||||
Groupe d'instructeurs
|
||||
|
|
|
@ -6,22 +6,22 @@
|
|||
%ul.footer-logos
|
||||
%li.footer-text
|
||||
Un service fourni par la
|
||||
= link_to "DINSIC", "http://www.modernisation.gouv.fr/"
|
||||
= link_to "DINUM", "http://www.modernisation.gouv.fr/", title: "Direction Interministérielle au Numérique"
|
||||
%br
|
||||
et incubé par
|
||||
= link_to "beta.gouv.fr", "https://beta.gouv.fr"
|
||||
= link_to "beta.gouv.fr", "https://beta.gouv.fr", title: "le site de Beta.gouv.fr"
|
||||
%li
|
||||
= link_to "http://www.modernisation.gouv.fr/" do
|
||||
= link_to "http://www.modernisation.gouv.fr/", title: "DINUM" do
|
||||
%span.footer-logo.footer-logo-dinsic{ role: 'img', 'aria-label': 'DINSIC' }
|
||||
= link_to "https://beta.gouv.fr" do
|
||||
= link_to "https://beta.gouv.fr", title: "le site de Beta.gouv.fr" do
|
||||
%span.footer-logo.footer-logo-beta-gouv-fr{ role: 'img', 'aria-label': 'beta.gouv.fr' }
|
||||
|
||||
%li.footer-column
|
||||
%ul.footer-links
|
||||
%li.footer-link
|
||||
= link_to "Newsletter", "https://my.sendinblue.com/users/subscribe/js_id/3s2q1/id/1", :class => "footer-link", :target => "_blank", rel: "noopener"
|
||||
= link_to "Newsletter", "https://my.sendinblue.com/users/subscribe/js_id/3s2q1/id/1", :title => "Notre newsletter", :class => "footer-link", :target => "_blank", rel: "noopener"
|
||||
%li.footer-link
|
||||
= link_to "Nouveautés", "https://github.com/betagouv/demarches-simplifiees.fr/releases", :class => "footer-link"
|
||||
= link_to "Nouveautés", "https://github.com/betagouv/demarches-simplifiees.fr/releases", :class => "footer-link", :title => "Nos nouveautés"
|
||||
%li.footer-link
|
||||
= link_to "Statistiques", stats_path, :class => "footer-link", data: { turbolinks: false } # Turbolinks disabled for Chartkick. See Issue #350
|
||||
%li.footer-link
|
||||
|
|
|
@ -14,13 +14,13 @@
|
|||
%em.hero-tagline-em en ligne
|
||||
|
||||
.hero-illustration
|
||||
%img{ :src => image_url("landing/hero/dematerialiser.svg"), alt: "" }
|
||||
%img{ :src => image_url("landing/hero/dematerialiser.svg"), alt: "dématérialisez" }
|
||||
|
||||
.landing-panel.usagers-panel
|
||||
.container
|
||||
.role-panel-wrapper
|
||||
.role-panel-30.role-usagers-image
|
||||
%img.role-image{ :src => image_url("landing/roles/usagers.svg"), alt: "" }
|
||||
%img.role-image{ :src => image_url("landing/roles/usagers.svg"), alt: "usager" }
|
||||
|
||||
.role-panel-70
|
||||
%h1.role-panel-title Vous souhaitez effectuer une demande auprès d'une administration ?
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
.procedure-logos
|
||||
= image_tag procedure.logo_url
|
||||
= image_tag procedure.logo_url, alt: "logo #{procedure.libelle}"
|
||||
- if procedure.euro_flag
|
||||
= image_tag("flag_of_europe.svg", id: 'euro_flag', class: (!procedure.euro_flag ? "hidden" : ""))
|
||||
%h2.procedure-title
|
||||
%h1.procedure-title
|
||||
= procedure.libelle
|
||||
.procedure-description
|
||||
.procedure-description-body.read-more-enabled.read-more-collapsed
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
= mail_to CONTACT_EMAIL do
|
||||
%span.icon.mail
|
||||
.dropdown-description
|
||||
%h4.help-dropdown-title Contact technique
|
||||
%span.help-dropdown-title Contact technique
|
||||
%p Envoyez nous un message à #{CONTACT_EMAIL}.
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
= link_to FAQ_URL, target: "_blank", rel: "noopener" do
|
||||
%span.icon.help
|
||||
.dropdown-description
|
||||
%h4.help-dropdown-title Un problème avec le site ?
|
||||
%span.help-dropdown-title Un problème avec le site ?
|
||||
%p Trouvez votre réponse dans l’aide en ligne.
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
= link_to messagerie_dossier_path(dossier) do
|
||||
%span.icon.mail
|
||||
.dropdown-description
|
||||
%h4.help-dropdown-title= title
|
||||
%span.help-dropdown-title= title
|
||||
%p Envoyez directement un message à l’instructeur.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%li.help-dropdown-service
|
||||
%span.icon.person
|
||||
.dropdown-description
|
||||
%h4.help-dropdown-title= title
|
||||
%span.help-dropdown-title= title
|
||||
.help-dropdown-service-action
|
||||
%p Contactez directement l’administration :
|
||||
%p.help-dropdown-service-item
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
= link_to "Accessibilité", accessibilite_path, :class => "footer-link"
|
||||
–
|
||||
= link_to "CGU", CGU_URL, :class => "footer-link", :target => "_blank", rel: "noopener noreferrer"
|
||||
–
|
||||
= link_to "Mentions légales", MENTIONS_LEGALES_URL, :class => "footer-link", :target => "_blank", rel: "noopener noreferrer"
|
||||
–
|
||||
= link_to 'Documentation', DOC_URL
|
||||
–
|
||||
= contact_link "Contact technique", class: "footer-link", dossier_id: dossier&.id
|
||||
–
|
||||
= link_to 'Aide', FAQ_URL
|
||||
%ul.footer-row.footer-bottom-line.footer-site-links
|
||||
%li>= link_to "Accessibilité", accessibilite_path
|
||||
%li>= link_to "CGU", CGU_URL, target: "_blank", rel: "noopener noreferrer"
|
||||
%li>= link_to "Mentions légales", MENTIONS_LEGALES_URL, target: "_blank", rel: "noopener noreferrer"
|
||||
%li>= link_to 'Documentation', DOC_URL
|
||||
%li>= contact_link "Contact technique", dossier_id: dossier&.id
|
||||
%li>= link_to 'Aide', FAQ_URL
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
.container
|
||||
- service = procedure.service
|
||||
- if service.present?
|
||||
%ul.footer-row.footer-columns
|
||||
%li.footer-column
|
||||
%h3.footer-header Cette démarche est gérée par :
|
||||
%p
|
||||
.footer-row.footer-columns
|
||||
%ul.footer-column
|
||||
%p.footer-header Cette démarche est gérée par :
|
||||
%li
|
||||
= service.nom
|
||||
%br
|
||||
= service.organisme
|
||||
%br
|
||||
= string_to_html(service.adresse)
|
||||
= string_to_html(service.adresse, wrapper_tag = 'span')
|
||||
|
||||
%li.footer-column
|
||||
%h3.footer-header Poser une question sur votre dossier :
|
||||
%p
|
||||
%ul.footer-column
|
||||
%p.footer-header Poser une question sur votre dossier :
|
||||
%li
|
||||
- if dossier.present? && dossier.messagerie_available?
|
||||
Directement
|
||||
= link_to "par la messagerie", messagerie_dossier_path(dossier)
|
||||
|
@ -22,21 +22,21 @@
|
|||
Par email :
|
||||
= link_to service.email, "mailto:#{service.email}"
|
||||
|
||||
%p
|
||||
%li
|
||||
Par téléphone :
|
||||
%a{ href: "tel:#{service.telephone}" }= service.telephone
|
||||
|
||||
%p
|
||||
%li
|
||||
- horaires = "Horaires : #{formatted_horaires(service.horaires)}"
|
||||
= simple_format(horaires)
|
||||
= simple_format(horaires, {}, wrapper_tag: 'span')
|
||||
|
||||
|
||||
- politiques = politiques_conservation_de_donnees(procedure)
|
||||
- if politiques.present?
|
||||
%li.footer-column
|
||||
%h3.footer-header Conservation des données :
|
||||
%ul.footer-column
|
||||
%p.footer-header Conservation des données :
|
||||
- politiques.each do |politique|
|
||||
%p= politique
|
||||
%li= politique
|
||||
|
||||
.footer-row.footer-bottom-line
|
||||
= render partial: 'users/general_footer_row', locals: { dossier: dossier }
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
%footer.procedure-footer
|
||||
.container
|
||||
.footer-row.footer-bottom-line
|
||||
= render partial: "users/general_footer_row", locals: { dossier: nil }
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.auth-form.sign-in-form
|
||||
|
||||
= form_for User.new, url: user_session_path, html: { class: "form" } do |f|
|
||||
%h1 Connectez-vous
|
||||
%h2.huge-title Connectez-vous
|
||||
|
||||
= f.label :email, "Email"
|
||||
= f.text_field :email, autofocus: true
|
||||
|
|
|
@ -18,6 +18,8 @@ chdir APP_ROOT do
|
|||
system('bundle check') || system!('bundle install')
|
||||
system! 'bin/yarn install'
|
||||
|
||||
puts "\n== Updating webdrivers =="
|
||||
system! 'RAILS_ENV=test bin/rails webdrivers:chromedriver:update'
|
||||
|
||||
puts "\n== Copying sample files =="
|
||||
unless File.exist?('.env')
|
||||
|
|
|
@ -18,6 +18,9 @@ chdir APP_ROOT do
|
|||
system('bundle check') || system!('bundle install')
|
||||
system! 'bin/yarn install'
|
||||
|
||||
puts "\n== Updating webdrivers =="
|
||||
system! 'RAILS_ENV=test bin/rails webdrivers:chromedriver:update'
|
||||
|
||||
puts "\n== Updating database =="
|
||||
system! 'bin/rails db:migrate'
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ FOG_OPENSTACK_IDENTITY_API_VERSION=""
|
|||
FOG_OPENSTACK_REGION=""
|
||||
FOG_DIRECTORY=""
|
||||
FOG_ENABLED=""
|
||||
CARRIERWAVE_CACHE_DIR="/tmp/tps-local-cache"
|
||||
DS_PROXY_URL=""
|
||||
|
||||
FC_PARTICULIER_ID=""
|
||||
|
@ -46,8 +45,14 @@ SENTRY_DSN_JS=""
|
|||
MATOMO_ENABLED="disabled"
|
||||
MATOMO_ID="73"
|
||||
|
||||
SENDINBLUE_ENABLED="disabled"
|
||||
SENDINBLUE_BALANCING=""
|
||||
SENDINBLUE_BALANCING_VALUE=""
|
||||
SENDINBLUE_ENABLED=""
|
||||
SENDINBLUE_CLIENT_KEY=""
|
||||
SENDINBLUE_SMTP_KEY=""
|
||||
SENDINBLUE_USER_NAME=""
|
||||
|
||||
|
||||
CRISP_ENABLED="disabled"
|
||||
CRISP_CLIENT_KEY=""
|
||||
|
||||
|
|
|
@ -45,14 +45,26 @@ Rails.application.configure do
|
|||
config.assets.raise_runtime_errors = true
|
||||
|
||||
# Action Mailer settings
|
||||
|
||||
if ENV['SENDINBLUE_ENABLED'] == 'enabled'
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
config.action_mailer.smtp_settings = {
|
||||
user_name: Rails.application.secrets.sendinblue[:username],
|
||||
password: Rails.application.secrets.sendinblue[:smtp_key],
|
||||
address: 'smtp-relay.sendinblue.com',
|
||||
domain: 'smtp-relay.sendinblue.com',
|
||||
port: '587',
|
||||
authentication: :cram_md5
|
||||
}
|
||||
else
|
||||
config.action_mailer.delivery_method = :letter_opener_web
|
||||
# Configure default root URL for generating URLs to routes
|
||||
config.action_mailer.default_url_options = {
|
||||
host: 'localhost',
|
||||
port: 3000
|
||||
}
|
||||
# Configure default root URL for email assets
|
||||
|
||||
config.action_mailer.asset_host = "http://" + ENV['APP_HOST']
|
||||
end
|
||||
|
||||
Rails.application.routes.default_url_options = {
|
||||
host: 'localhost',
|
||||
|
|
|
@ -77,6 +77,16 @@ Rails.application.configure do
|
|||
port: '2525',
|
||||
authentication: :cram_md5
|
||||
}
|
||||
elsif ENV['SENDINBLUE_ENABLED'] == 'enabled'
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
config.action_mailer.smtp_settings = {
|
||||
user_name: Rails.application.secrets.sendinblue[:username],
|
||||
password: Rails.application.secrets.sendinblue[:smtp_key],
|
||||
address: 'smtp-relay.sendinblue.com',
|
||||
domain: 'smtp-relay.sendinblue.com',
|
||||
port: '587',
|
||||
authentication: :cram_md5
|
||||
}
|
||||
else
|
||||
config.action_mailer.delivery_method = :mailjet
|
||||
end
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
Rails.application.config.content_security_policy do |policy|
|
||||
# En cas de non respect d'une des règles, faire un POST sur cette URL
|
||||
if Rails.env.production?
|
||||
policy.report_uri "https://demarchessimplifieestest.report-uri.com/r/d/csp/reportOnly"
|
||||
else
|
||||
policy.report_uri "http://#{ENV['APP_HOST']}/csp/" # ne pas notifier report-uri en dev/test
|
||||
if Rails.env.development?
|
||||
# les CSP ne sont pas appliquées en dev: on notifie cependant une url quelconque de la violation
|
||||
# pour détecter les erreurs lors de l'ajout d'une nouvelle brique externe durant le développement
|
||||
policy.report_uri "http://#{ENV['APP_HOST']}/csp/"
|
||||
end
|
||||
# Whitelist image
|
||||
policy.img_src :self, "*.openstreetmap.org", "static.demarches-simplifiees.fr", "*.cloud.ovh.net", "stats.data.gouv.fr", "*", :data
|
||||
|
|
1
config/initializers/dynamic_smtp_settings_interceptor.rb
Normal file
1
config/initializers/dynamic_smtp_settings_interceptor.rb
Normal file
|
@ -0,0 +1 @@
|
|||
ActionMailer::Base.register_interceptor "DynamicSmtpSettingsInterceptor"
|
79
config/initializers/graphiql.rb
Normal file
79
config/initializers/graphiql.rb
Normal 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'
|
|
@ -2,4 +2,5 @@ fr:
|
|||
activerecord:
|
||||
attributes:
|
||||
commentaire:
|
||||
body: 'Votre message'
|
||||
file: fichier
|
||||
|
|
8
config/locales/models/geo_area/fr.yml
Normal file
8
config/locales/models/geo_area/fr.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
fr:
|
||||
activerecord:
|
||||
attributes:
|
||||
geo_area:
|
||||
source:
|
||||
cadastre: Parcelle cadastrale
|
||||
quartier_prioritaire: Quartier prioritaire
|
||||
selection_utilisateur: Sélection utilisateur
|
|
@ -42,8 +42,6 @@ defaults: &defaults
|
|||
openstack_identity_api_version: "<%= ENV['FOG_OPENSTACK_IDENTITY_API_VERSION'] %>"
|
||||
openstack_region: <%= ENV['FOG_OPENSTACK_REGION'] %>
|
||||
directory: <%= ENV['FOG_DIRECTORY'] %>
|
||||
carrierwave:
|
||||
cache_dir: <%= ENV['CARRIERWAVE_CACHE_DIR'] %>
|
||||
mailtrap:
|
||||
username: <%= ENV['MAILTRAP_USERNAME'] %>
|
||||
password: <%= ENV['MAILTRAP_PASSWORD'] %>
|
||||
|
@ -54,7 +52,9 @@ defaults: &defaults
|
|||
webhook_secret: <%= ENV['HELPSCOUT_WEBHOOK_SECRET'] %>
|
||||
sendinblue:
|
||||
enabled: <%= ENV['SENDINBLUE_ENABLED'] == 'enabled' %>
|
||||
username: <%= ENV['SENDINBLUE_USER_NAME'] %>
|
||||
client_key: <%= ENV['SENDINBLUE_CLIENT_KEY'] %>
|
||||
smtp_key: <%= ENV['SENDINBLUE_SMTP_KEY'] %>
|
||||
api_v3_key: <%= ENV['SENDINBLUE_API_V3_KEY'] %>
|
||||
matomo:
|
||||
enabled: <%= ENV['MATOMO_ENABLED'] == 'enabled' %>
|
||||
|
@ -82,8 +82,6 @@ test:
|
|||
key: api_entreprise_test_key
|
||||
fog:
|
||||
directory: tps_dev
|
||||
carrierwave:
|
||||
cache_dir: /tmp/tps-test-cache
|
||||
pipedrive:
|
||||
key: pipedrive_test_key
|
||||
france_connect_particulier:
|
||||
|
|
5
db/migrate/20191113142816_instructeurs_remove_email.rb
Normal file
5
db/migrate/20191113142816_instructeurs_remove_email.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class InstructeursRemoveEmail < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
remove_column :instructeurs, :email
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2019_10_24_150452) do
|
||||
ActiveRecord::Schema.define(version: 2019_11_13_142816) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -414,12 +414,10 @@ ActiveRecord::Schema.define(version: 2019_10_24_150452) do
|
|||
end
|
||||
|
||||
create_table "instructeurs", id: :serial, force: :cascade do |t|
|
||||
t.string "email", default: "", null: false
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.text "encrypted_login_token"
|
||||
t.datetime "login_token_created_at"
|
||||
t.index ["email"], name: "index_instructeurs_on_email"
|
||||
end
|
||||
|
||||
create_table "invites", id: :serial, force: :cascade do |t|
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -2,19 +2,53 @@ require 'spec_helper'
|
|||
|
||||
describe Admin::AssignsController, type: :controller do
|
||||
let(:admin) { create(:administrateur) }
|
||||
let(:procedure) { create :procedure, administrateur: admin }
|
||||
let(:instructeur) { create :instructeur, administrateurs: [admin] }
|
||||
|
||||
before do
|
||||
sign_in(admin.user)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
subject { get :show, params: { procedure_id: procedure.id } }
|
||||
it { expect(subject.status).to eq(200) }
|
||||
let(:procedure) { create :procedure, administrateur: admin, instructeurs: [instructeur_assigned_1, instructeur_assigned_2] }
|
||||
let!(:instructeur_assigned_1) { create :instructeur, email: 'instructeur_1@ministere_a.gouv.fr', administrateurs: [admin] }
|
||||
let!(:instructeur_assigned_2) { create :instructeur, email: 'instructeur_2@ministere_b.gouv.fr', administrateurs: [admin] }
|
||||
let!(:instructeur_not_assigned_1) { create :instructeur, email: 'instructeur_3@ministere_a.gouv.fr', administrateurs: [admin] }
|
||||
let!(:instructeur_not_assigned_2) { create :instructeur, email: 'instructeur_4@ministere_b.gouv.fr', administrateurs: [admin] }
|
||||
let(:filter) { nil }
|
||||
|
||||
subject! { get :show, params: { procedure_id: procedure.id, filter: filter } }
|
||||
|
||||
it { expect(response.status).to eq(200) }
|
||||
|
||||
it 'sets the assigned and not assigned instructeurs' do
|
||||
expect(assigns(:instructeurs_assign)).to match_array([instructeur_assigned_1, instructeur_assigned_2])
|
||||
expect(assigns(:instructeurs_not_assign)).to match_array([instructeur_not_assigned_1, instructeur_not_assigned_2])
|
||||
end
|
||||
|
||||
context 'with a search filter' do
|
||||
let(:filter) { '@ministere_a.gouv.fr' }
|
||||
|
||||
it 'filters the unassigned instructeurs' do
|
||||
expect(assigns(:instructeurs_not_assign)).to match_array([instructeur_not_assigned_1])
|
||||
end
|
||||
|
||||
it 'does not filter the assigned instructeurs' do
|
||||
expect(assigns(:instructeurs_assign)).to match_array([instructeur_assigned_1, instructeur_assigned_2])
|
||||
end
|
||||
|
||||
context 'when the filter has spaces or a mixed case' do
|
||||
let(:filter) { ' @ministere_A.gouv.fr ' }
|
||||
|
||||
it 'trims spaces and ignores the case' do
|
||||
expect(assigns(:instructeurs_not_assign)).to match_array([instructeur_not_assigned_1])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
let(:procedure) { create :procedure, administrateur: admin }
|
||||
let(:instructeur) { create :instructeur, administrateurs: [admin] }
|
||||
|
||||
subject { put :update, params: { instructeur_id: instructeur.id, procedure_id: procedure.id, to: 'assign' } }
|
||||
|
||||
it { expect(subject).to redirect_to admin_procedure_assigns_path(procedure_id: procedure.id) }
|
||||
|
|
|
@ -12,6 +12,15 @@ describe API::V2::GraphqlController do
|
|||
create(:commentaire, dossier: dossier, email: 'test@test.com')
|
||||
dossier
|
||||
end
|
||||
let(:dossier1) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: 1.day.ago) }
|
||||
let(:dossier2) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: 3.days.ago) }
|
||||
let!(:dossier_brouillon) { create(:dossier, procedure: procedure) }
|
||||
let(:dossiers) { [dossier2, dossier1, dossier] }
|
||||
let(:instructeur) { create(:instructeur, followed_dossiers: dossiers) }
|
||||
|
||||
before do
|
||||
instructeur.assign_to_procedure(procedure)
|
||||
end
|
||||
|
||||
let(:query) do
|
||||
"{
|
||||
|
@ -62,18 +71,24 @@ describe API::V2::GraphqlController do
|
|||
request.env['HTTP_AUTHORIZATION'] = authorization_header
|
||||
end
|
||||
|
||||
it "should return demarche" do
|
||||
context "demarche" do
|
||||
it "should be returned" do
|
||||
expect(gql_errors).to eq(nil)
|
||||
expect(gql_data).to eq(demarche: {
|
||||
id: procedure.to_typed_id,
|
||||
number: procedure.id.to_s,
|
||||
number: procedure.id,
|
||||
title: procedure.libelle,
|
||||
description: procedure.description,
|
||||
state: 'brouillon',
|
||||
archivedAt: nil,
|
||||
createdAt: procedure.created_at.iso8601,
|
||||
updatedAt: procedure.updated_at.iso8601,
|
||||
groupeInstructeurs: [{ instructeurs: [], label: "défaut" }],
|
||||
groupeInstructeurs: [
|
||||
{
|
||||
instructeurs: [{ email: instructeur.email }],
|
||||
label: "défaut"
|
||||
}
|
||||
],
|
||||
champDescriptors: procedure.types_de_champ.map do |tdc|
|
||||
{
|
||||
id: tdc.to_typed_id,
|
||||
|
@ -84,11 +99,39 @@ describe API::V2::GraphqlController do
|
|||
}
|
||||
end,
|
||||
dossiers: {
|
||||
nodes: []
|
||||
nodes: dossiers.map { |dossier| { id: dossier.to_typed_id } }
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
context "filter dossiers" do
|
||||
let(:query) do
|
||||
"{
|
||||
demarche(number: #{procedure.id}) {
|
||||
id
|
||||
number
|
||||
dossiers(createdSince: \"#{2.days.ago.iso8601}\") {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}"
|
||||
end
|
||||
|
||||
it "should be returned" do
|
||||
expect(gql_errors).to eq(nil)
|
||||
expect(gql_data).to eq(demarche: {
|
||||
id: procedure.to_typed_id,
|
||||
number: procedure.id,
|
||||
dossiers: {
|
||||
nodes: [{ id: dossier1.to_typed_id }, { id: dossier.to_typed_id }]
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "dossier" do
|
||||
let(:query) do
|
||||
"{
|
||||
|
@ -130,11 +173,11 @@ describe API::V2::GraphqlController do
|
|||
}"
|
||||
end
|
||||
|
||||
it "should return dossier" do
|
||||
it "should be returned" do
|
||||
expect(gql_errors).to eq(nil)
|
||||
expect(gql_data).to eq(dossier: {
|
||||
id: dossier.to_typed_id,
|
||||
number: dossier.id.to_s,
|
||||
number: dossier.id,
|
||||
state: 'en_construction',
|
||||
updatedAt: dossier.updated_at.iso8601,
|
||||
datePassageEnConstruction: dossier.en_construction_at.iso8601,
|
||||
|
@ -146,7 +189,12 @@ describe API::V2::GraphqlController do
|
|||
id: dossier.user.to_typed_id,
|
||||
email: dossier.user.email
|
||||
},
|
||||
instructeurs: [],
|
||||
instructeurs: [
|
||||
{
|
||||
id: instructeur.to_typed_id,
|
||||
email: instructeur.email
|
||||
}
|
||||
],
|
||||
messages: dossier.commentaires.map do |commentaire|
|
||||
{
|
||||
body: commentaire.body,
|
||||
|
@ -166,6 +214,114 @@ describe API::V2::GraphqlController do
|
|||
expect(gql_data[:dossier][:champs][0][:id]).to eq(dossier.champs[0].type_de_champ.to_typed_id)
|
||||
end
|
||||
end
|
||||
|
||||
context "mutations" do
|
||||
describe 'dossierEnvoyerMessage' do
|
||||
context 'success' do
|
||||
let(:query) do
|
||||
"mutation {
|
||||
dossierEnvoyerMessage(input: {
|
||||
dossierId: \"#{dossier.to_typed_id}\",
|
||||
instructeurId: \"#{instructeur.to_typed_id}\",
|
||||
body: \"Bonjour\"
|
||||
}) {
|
||||
message {
|
||||
body
|
||||
}
|
||||
}
|
||||
}"
|
||||
end
|
||||
|
||||
it "should post a message" do
|
||||
expect(gql_errors).to eq(nil)
|
||||
|
||||
expect(gql_data).to eq(dossierEnvoyerMessage: {
|
||||
message: {
|
||||
body: "Bonjour"
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'schema error' do
|
||||
let(:query) do
|
||||
"mutation {
|
||||
dossierEnvoyerMessage(input: {
|
||||
dossierId: \"#{dossier.to_typed_id}\",
|
||||
instructeurId: \"#{instructeur.to_typed_id}\"
|
||||
}) {
|
||||
message {
|
||||
body
|
||||
}
|
||||
}
|
||||
}"
|
||||
end
|
||||
|
||||
it "should fail" do
|
||||
expect(gql_data).to eq(nil)
|
||||
expect(gql_errors).not_to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'validation error' do
|
||||
let(:query) do
|
||||
"mutation {
|
||||
dossierEnvoyerMessage(input: {
|
||||
dossierId: \"#{dossier.to_typed_id}\",
|
||||
instructeurId: \"#{instructeur.to_typed_id}\",
|
||||
body: \"\"
|
||||
}) {
|
||||
message {
|
||||
body
|
||||
}
|
||||
errors {
|
||||
message
|
||||
}
|
||||
}
|
||||
}"
|
||||
end
|
||||
|
||||
it "should fail" do
|
||||
expect(gql_errors).to eq(nil)
|
||||
expect(gql_data).to eq(dossierEnvoyerMessage: {
|
||||
errors: [{ message: "Votre message ne peut être vide" }],
|
||||
message: nil
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'createDirectUpload' do
|
||||
let(:query) do
|
||||
"mutation {
|
||||
createDirectUpload(input: {
|
||||
dossierId: \"#{dossier.to_typed_id}\",
|
||||
filename: \"hello.png\",
|
||||
byteSize: 1234,
|
||||
checksum: \"qwerty1234\",
|
||||
contentType: \"image/png\"
|
||||
}) {
|
||||
directUpload {
|
||||
url
|
||||
headers
|
||||
blobId
|
||||
signedBlobId
|
||||
}
|
||||
}
|
||||
}"
|
||||
end
|
||||
|
||||
it "should initiate a direct upload" do
|
||||
expect(gql_errors).to eq(nil)
|
||||
|
||||
data = gql_data[:createDirectUpload][:directUpload]
|
||||
expect(data[:url]).not_to be_nil
|
||||
expect(data[:headers]).not_to be_nil
|
||||
expect(data[:blobId]).not_to be_nil
|
||||
expect(data[:signedBlobId]).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when not authenticated" do
|
||||
|
@ -182,5 +338,26 @@ describe API::V2::GraphqlController do
|
|||
expect(gql_errors).not_to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "mutation" do
|
||||
let(:query) do
|
||||
"mutation {
|
||||
dossierEnvoyerMessage(input: {
|
||||
dossierId: \"#{dossier.to_typed_id}\",
|
||||
instructeurId: \"#{instructeur.to_typed_id}\",
|
||||
body: \"Bonjour\"
|
||||
}) {
|
||||
message {
|
||||
body
|
||||
}
|
||||
}
|
||||
}"
|
||||
end
|
||||
|
||||
it "should return error" do
|
||||
expect(gql_data[:dossierEnvoyerMessage]).to eq(nil)
|
||||
expect(gql_errors).not_to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@ feature 'As an administrateur', js: true do
|
|||
end
|
||||
|
||||
scenario 'I can register' do
|
||||
expect(new_admin.reload.active?).to be(false)
|
||||
expect(new_admin.reload.user.active?).to be(false)
|
||||
|
||||
confirmation_email = open_email(admin_email)
|
||||
token_params = confirmation_email.body.match(/token=[^"]+/)
|
||||
|
@ -24,6 +24,6 @@ feature 'As an administrateur', js: true do
|
|||
|
||||
expect(page).to have_content 'Mot de passe enregistré'
|
||||
|
||||
expect(new_admin.reload.active?).to be(true)
|
||||
expect(new_admin.reload.user.active?).to be(true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ feature 'The routing' do
|
|||
let(:scientifique_user) { create(:user, password: password) }
|
||||
let(:litteraire_user) { create(:user, password: password) }
|
||||
|
||||
before { Flipper.enable_actor(:routage, administrateur.user) }
|
||||
before { Flipper.enable_actor(:administrateur_routage, administrateur.user) }
|
||||
|
||||
scenario 'works' do
|
||||
login_as administrateur.user, scope: :user
|
||||
|
|
20
spec/mailers/application_mailer_spec.rb
Normal file
20
spec/mailers/application_mailer_spec.rb
Normal 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
|
|
@ -50,22 +50,4 @@ describe Administrateur, type: :model do
|
|||
# it { expect(subject).to eq([]) }
|
||||
# end
|
||||
# end
|
||||
|
||||
describe '#active?' do
|
||||
let!(:administrateur) { create(:administrateur) }
|
||||
|
||||
subject { administrateur.active? }
|
||||
|
||||
context 'when the user has never signed in' do
|
||||
before { administrateur.user.update(last_sign_in_at: nil) }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'when the user has already signed in' do
|
||||
before { administrateur.user.update(last_sign_in_at: Time.zone.now) }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -148,4 +148,22 @@ shared_examples 'type_de_champ_spec' do
|
|||
expect(cloned_procedure.types_de_champ.first.types_de_champ).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "linked_drop_down_list" do
|
||||
let(:type_de_champ) { create(:type_de_champ_linked_drop_down_list) }
|
||||
|
||||
it 'should validate without label' do
|
||||
type_de_champ.drop_down_list_value = 'toto'
|
||||
expect(type_de_champ.validate).to be_falsey
|
||||
messages = type_de_champ.errors.full_messages
|
||||
expect(messages.size).to eq(1)
|
||||
expect(messages.first.starts_with?("#{type_de_champ.libelle} doit commencer par")).to be_truthy
|
||||
|
||||
type_de_champ.libelle = ''
|
||||
expect(type_de_champ.validate).to be_falsey
|
||||
messages = type_de_champ.errors.full_messages
|
||||
expect(messages.size).to eq(2)
|
||||
expect(messages.last.starts_with?("La liste doit commencer par")).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -191,4 +191,22 @@ describe User, type: :model do
|
|||
it { expect(AdministrationMailer).to have_received(:invite_admin).with(user, nil, administration.id) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
subject { user.active? }
|
||||
|
||||
context 'when the user has never signed in' do
|
||||
before { user.update(last_sign_in_at: nil) }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'when the user has already signed in' do
|
||||
before { user.update(last_sign_in_at: Time.zone.now) }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require 'spec_helper'
|
||||
require 'csv'
|
||||
|
||||
describe ProcedureExportV2Service do
|
||||
describe 'to_data' do
|
||||
|
@ -150,6 +151,91 @@ describe ProcedureExportV2Service do
|
|||
]
|
||||
end
|
||||
|
||||
context 'as csv' do
|
||||
subject do
|
||||
Tempfile.create do |f|
|
||||
f << ProcedureExportV2Service.new(procedure, procedure.dossiers).to_csv
|
||||
f.rewind
|
||||
CSV.read(f.path)
|
||||
end
|
||||
end
|
||||
|
||||
let(:nominal_headers) do
|
||||
[
|
||||
"ID",
|
||||
"Email",
|
||||
"Établissement SIRET",
|
||||
"Établissement siège social",
|
||||
"Établissement NAF",
|
||||
"Établissement libellé NAF",
|
||||
"Établissement Adresse",
|
||||
"Établissement numero voie",
|
||||
"Établissement type voie",
|
||||
"Établissement nom voie",
|
||||
"Établissement complément adresse",
|
||||
"Établissement code postal",
|
||||
"Établissement localité",
|
||||
"Établissement code INSEE localité",
|
||||
"Entreprise SIREN",
|
||||
"Entreprise capital social",
|
||||
"Entreprise numero TVA intracommunautaire",
|
||||
"Entreprise forme juridique",
|
||||
"Entreprise forme juridique code",
|
||||
"Entreprise nom commercial",
|
||||
"Entreprise raison sociale",
|
||||
"Entreprise SIRET siège social",
|
||||
"Entreprise code effectif entreprise",
|
||||
"Entreprise date de création",
|
||||
"Entreprise nom",
|
||||
"Entreprise prénom",
|
||||
"Association RNA",
|
||||
"Association titre",
|
||||
"Association objet",
|
||||
"Association date de création",
|
||||
"Association date de déclaration",
|
||||
"Association date de publication",
|
||||
"Archivé",
|
||||
"État du dossier",
|
||||
"Dernière mise à jour le",
|
||||
"Déposé le",
|
||||
"Passé en instruction le",
|
||||
"Traité le",
|
||||
"Motivation de la décision",
|
||||
"Instructeurs",
|
||||
"textarea",
|
||||
"date",
|
||||
"datetime",
|
||||
"number",
|
||||
"decimal_number",
|
||||
"integer_number",
|
||||
"checkbox",
|
||||
"civilite",
|
||||
"email",
|
||||
"phone",
|
||||
"address",
|
||||
"yes_no",
|
||||
"simple_drop_down_list",
|
||||
"multiple_drop_down_list",
|
||||
"linked_drop_down_list",
|
||||
"pays",
|
||||
"regions",
|
||||
"departements",
|
||||
"engagement",
|
||||
"dossier_link",
|
||||
"piece_justificative",
|
||||
"siret",
|
||||
"carte",
|
||||
"text"
|
||||
]
|
||||
end
|
||||
|
||||
let(:dossiers_sheet_headers) { subject.first }
|
||||
|
||||
it 'should have headers' do
|
||||
expect(dossiers_sheet_headers).to match(nominal_headers)
|
||||
end
|
||||
end
|
||||
|
||||
it 'should have headers' do
|
||||
expect(dossiers_sheet.headers).to match(nominal_headers)
|
||||
|
||||
|
@ -225,7 +311,7 @@ describe ProcedureExportV2Service do
|
|||
let(:champ_repetition) { dossiers.first.champs.find { |champ| champ.type_champ == 'repetition' } }
|
||||
|
||||
it 'should have sheets' do
|
||||
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle])
|
||||
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle_for_export])
|
||||
end
|
||||
|
||||
it 'should have headers' do
|
||||
|
@ -247,7 +333,18 @@ describe ProcedureExportV2Service do
|
|||
end
|
||||
|
||||
it 'should have valid sheet name' do
|
||||
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', "A - B - C"])
|
||||
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', "(#{champ_repetition.type_de_champ.stable_id}) A - B - C"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non unique labels' do
|
||||
let(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :for_individual, procedure: procedure) }
|
||||
let(:champ_repetition) { dossier.champs.find { |champ| champ.type_champ == 'repetition' } }
|
||||
let(:type_de_champ_repetition) { create(:type_de_champ_repetition, procedure: procedure, libelle: champ_repetition.libelle) }
|
||||
let!(:another_champ_repetition) { create(:champ_repetition, type_de_champ: type_de_champ_repetition, dossier: dossier) }
|
||||
|
||||
it 'should have sheets' do
|
||||
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle_for_export, another_champ_repetition.libelle_for_export])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -83,7 +83,7 @@ VCR.configure do |c|
|
|||
c.hook_into :webmock
|
||||
c.cassette_library_dir = 'spec/fixtures/cassettes'
|
||||
c.configure_rspec_metadata!
|
||||
c.ignore_hosts 'test.host'
|
||||
c.ignore_hosts 'test.host', 'chromedriver.storage.googleapis.com'
|
||||
end
|
||||
|
||||
DatabaseCleaner.strategy = :transaction
|
||||
|
|
Loading…
Add table
Reference in a new issue