Merge pull request #6991 from betagouv/main

2022-02-28-01
This commit is contained in:
mfo 2022-02-28 15:21:34 +01:00 committed by GitHub
commit 8aa8c4a9de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 1724 additions and 569 deletions

View file

@ -23,6 +23,7 @@ module.exports = {
rules: {
'prettier/prettier': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'react/prop-types': 'off'
},
settings: {
@ -51,7 +52,13 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier'
]
],
rules: {
'prettier/prettier': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'@typescript-eslint/no-explicit-any': 'error'
}
}
]
};

View file

@ -12,11 +12,12 @@ runs:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'yarn'
- name: Install Node modules
run: yarn install --frozen-lockfile
run: |
node --version
yarn install --frozen-lockfile
shell: bash
- name: Setup environment variables

1
.node-version Normal file
View file

@ -0,0 +1 @@
16.14.0

View file

@ -83,7 +83,7 @@ gem 'spreadsheet_architect'
gem 'typhoeus'
gem 'warden'
gem 'webpacker'
gem 'zipline', github: 'fringd/zipline', ref: 'd637bbff2' # Unreleased 1.3.0, with a fix for Ruby 3.0 kwargs
gem 'zipline'
gem 'zxcvbn-ruby', require: 'zxcvbn'
group :test do
@ -120,7 +120,7 @@ end
group :development, :test do
gem 'graphql-schema_comparator'
gem 'mina', git: 'https://github.com/mina-deploy/mina.git', require: false # Deploy
gem 'mina', require: false # Deploy
gem 'pry-byebug' # Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'rspec-rails'
gem 'simple_xlsx_reader'

View file

@ -1,60 +1,43 @@
GIT
remote: https://github.com/fringd/zipline.git
revision: d637bbff262f59718d23a65f50b50163b8ba749f
ref: d637bbff2
specs:
zipline (1.3.0)
actionpack (>= 3.2.1, < 7.0)
zip_tricks (>= 4.2.1, < 6.0)
GIT
remote: https://github.com/mina-deploy/mina.git
revision: 84fa84c7f7f94f9518ef9b7099396ab6676b5881
specs:
mina (1.2.3)
open4 (~> 1.3.4)
rake
GEM
remote: https://rubygems.org/
specs:
aasm (5.2.0)
concurrent-ruby (~> 1.0)
acsv (0.0.1)
actioncable (6.1.4.4)
actionpack (= 6.1.4.4)
activesupport (= 6.1.4.4)
actioncable (6.1.4.6)
actionpack (= 6.1.4.6)
activesupport (= 6.1.4.6)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.4.4)
actionpack (= 6.1.4.4)
activejob (= 6.1.4.4)
activerecord (= 6.1.4.4)
activestorage (= 6.1.4.4)
activesupport (= 6.1.4.4)
actionmailbox (6.1.4.6)
actionpack (= 6.1.4.6)
activejob (= 6.1.4.6)
activerecord (= 6.1.4.6)
activestorage (= 6.1.4.6)
activesupport (= 6.1.4.6)
mail (>= 2.7.1)
actionmailer (6.1.4.4)
actionpack (= 6.1.4.4)
actionview (= 6.1.4.4)
activejob (= 6.1.4.4)
activesupport (= 6.1.4.4)
actionmailer (6.1.4.6)
actionpack (= 6.1.4.6)
actionview (= 6.1.4.6)
activejob (= 6.1.4.6)
activesupport (= 6.1.4.6)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.4.4)
actionview (= 6.1.4.4)
activesupport (= 6.1.4.4)
actionpack (6.1.4.6)
actionview (= 6.1.4.6)
activesupport (= 6.1.4.6)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.4.4)
actionpack (= 6.1.4.4)
activerecord (= 6.1.4.4)
activestorage (= 6.1.4.4)
activesupport (= 6.1.4.4)
actiontext (6.1.4.6)
actionpack (= 6.1.4.6)
activerecord (= 6.1.4.6)
activestorage (= 6.1.4.6)
activesupport (= 6.1.4.6)
nokogiri (>= 1.8.5)
actionview (6.1.4.4)
activesupport (= 6.1.4.4)
actionview (6.1.4.6)
activesupport (= 6.1.4.6)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -72,26 +55,26 @@ GEM
activemodel (>= 5.2.0)
activestorage (>= 5.2.0)
activesupport (>= 5.2.0)
activejob (6.1.4.4)
activesupport (= 6.1.4.4)
activejob (6.1.4.6)
activesupport (= 6.1.4.6)
globalid (>= 0.3.6)
activemodel (6.1.4.4)
activesupport (= 6.1.4.4)
activerecord (6.1.4.4)
activemodel (= 6.1.4.4)
activesupport (= 6.1.4.4)
activestorage (6.1.4.4)
actionpack (= 6.1.4.4)
activejob (= 6.1.4.4)
activerecord (= 6.1.4.4)
activesupport (= 6.1.4.4)
activemodel (6.1.4.6)
activesupport (= 6.1.4.6)
activerecord (6.1.4.6)
activemodel (= 6.1.4.6)
activesupport (= 6.1.4.6)
activestorage (6.1.4.6)
actionpack (= 6.1.4.6)
activejob (= 6.1.4.6)
activerecord (= 6.1.4.6)
activesupport (= 6.1.4.6)
marcel (~> 1.0.0)
mini_mime (>= 1.1.0)
activestorage-openstack (1.5.1)
fog-openstack (~> 1.0)
marcel
rails (>= 5.2.2)
activesupport (6.1.4.4)
activesupport (6.1.4.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -183,6 +166,7 @@ GEM
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.1.9)
connection_pool (2.2.3)
content_disposition (1.0.0)
crack (0.4.5)
rexml
crass (1.0.6)
@ -351,7 +335,7 @@ GEM
domain_name (~> 0.5)
http_accept_language (2.1.1)
httpclient (2.8.3)
i18n (1.8.11)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.33)
activesupport (>= 4.0.2)
@ -417,7 +401,7 @@ GEM
railties (>= 4)
request_store (~> 1.0)
logstash-event (1.2.02)
loofah (2.13.0)
loofah (2.14.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -431,9 +415,12 @@ GEM
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0212)
mina (1.2.4)
open4 (~> 1.3.4)
rake
mini_magick (4.11.0)
mini_mime (1.1.2)
mini_portile2 (2.6.1)
mini_portile2 (2.7.1)
minitest (5.15.0)
momentjs-rails (2.20.1)
railties (>= 3.1)
@ -444,8 +431,8 @@ GEM
ruby2_keywords (~> 0.0.1)
netrc (0.11.0)
nio4r (2.5.8)
nokogiri (1.12.5)
mini_portile2 (~> 2.6.1)
nokogiri (1.13.1)
mini_portile2 (~> 2.7.0)
racc (~> 1.4)
open4 (1.3.4)
openid_connect (1.3.0)
@ -515,20 +502,20 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.1.4.4)
actioncable (= 6.1.4.4)
actionmailbox (= 6.1.4.4)
actionmailer (= 6.1.4.4)
actionpack (= 6.1.4.4)
actiontext (= 6.1.4.4)
actionview (= 6.1.4.4)
activejob (= 6.1.4.4)
activemodel (= 6.1.4.4)
activerecord (= 6.1.4.4)
activestorage (= 6.1.4.4)
activesupport (= 6.1.4.4)
rails (6.1.4.6)
actioncable (= 6.1.4.6)
actionmailbox (= 6.1.4.6)
actionmailer (= 6.1.4.6)
actionpack (= 6.1.4.6)
actiontext (= 6.1.4.6)
actionview (= 6.1.4.6)
activejob (= 6.1.4.6)
activemodel (= 6.1.4.6)
activerecord (= 6.1.4.6)
activestorage (= 6.1.4.6)
activesupport (= 6.1.4.6)
bundler (>= 1.15.0)
railties (= 6.1.4.4)
railties (= 6.1.4.6)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -547,9 +534,9 @@ GEM
rails-i18n (6.0.0)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
railties (6.1.4.4)
actionpack (= 6.1.4.4)
activesupport (= 6.1.4.4)
railties (6.1.4.6)
actionpack (= 6.1.4.6)
activesupport (= 6.1.4.6)
method_source
rake (>= 0.13)
thor (~> 1.0)
@ -768,8 +755,12 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.5.3)
zeitwerk (2.5.4)
zip_tricks (5.6.0)
zipline (1.4.1)
actionpack (>= 6.0, < 8.0)
content_disposition (~> 1.0)
zip_tricks (>= 4.2.1, < 6.0)
zxcvbn-ruby (1.2.0)
PLATFORMS
@ -842,7 +833,7 @@ DEPENDENCIES
lograge
logstash-event
mailjet
mina!
mina
openid_connect
pg
phonelib
@ -892,7 +883,7 @@ DEPENDENCIES
webdrivers (~> 4.0)
webmock
webpacker
zipline!
zipline
zxcvbn-ruby
BUNDLED WITH

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,27 @@
@import "colors";
@import "constants";
#agentconnect {
.agent {
color: $blue-france-500;
text-align: center;
font-size: 28px;
font-weight: bold;
}
.box {
background-color: #F2F2F9;
padding: $default-padding;
ul {
list-style: disc;
padding-left: inherit;
}
}
.citizen {
font-size: 16px;
color: $blue-france-500;
font-weight: bold;
}
}

View file

@ -3,17 +3,23 @@
@import "placeholders";
@import "mixins";
#auth {
#auth,
#agentconnect {
// On small screens, hide the procedure description text on auth pages.
// It avoids pushing the sign-in/sign-up form out of the viewport.
//
// The procedure description can still be read from the /commencer
// pages.
@media (max-width: $two-columns-breakpoint) {
.procedure-preview {
.procedure-preview,
.agent-intro {
display: none;
}
}
.column {
padding-top: 2 * $default-spacer;
}
}
.auth-form {
@ -53,6 +59,10 @@
}
.sign-in-form .form {
input[type="email"] {
margin-bottom: $default-padding;
}
input[type="password"] {
margin-bottom: $default-spacer;
}
@ -61,3 +71,10 @@
margin-bottom: 0;
}
}
#session-new {
.important-header {
font-weight: bold;
font-size: 18px;
}
}

View file

@ -15,8 +15,8 @@
.france-connect-login-button {
display: inline-block;
height: 52px;
width: 186px;
height: 60px;
width: 230px;
margin: auto;
margin-bottom: 8px;
background-image: image-url("login-with-fc.svg"), image-url("login-with-fc-hover.svg");

View file

@ -1,42 +0,0 @@
@import "constants";
.m-0 {
margin: 0px !important;
}
.mb-1 {
margin-bottom: $default-spacer;
}
.mr-1 {
margin-right: $default-spacer !important;
}
.mb-4 {
margin-bottom: 4 * $default-spacer !important;
}
.ml-1 {
margin-left: $default-spacer;
}
.pl-0 {
padding-left: 0px !important;
}
.p-0 {
padding: 0px !important;
}
.bold {
font-weight: bold;
}
.numbers-delimiter {
display: inline-block;
width: 5px;
}
.text-center {
text-align: center;
}

View file

@ -2,38 +2,8 @@
@import "common";
@import "constants";
.merci {
text-align: center;
margin-bottom: 60px;
.merci .monavis {
img {
margin-top: 4 * $default-padding;
}
h1 {
margin: (2 * $default-padding) 0;
}
b {
font-weight: bold;
}
.send {
margin-bottom: 2 * $default-padding;
font-size: 20px;
}
p {
margin: $default-padding;
}
a {
margin-top: 40px;
}
.monavis {
img {
margin-top: 2 * $default-padding;
}
margin-top: 2 * $default-padding;
}
}

View file

@ -22,6 +22,12 @@ $procedure-description-line-height: 22px;
font-weight: bold;
}
.small-simple {
font-size: 16px;
color: $blue-france-500;
font-weight: bold;
}
.close-procedure {
font-size: 12px;
}

View file

@ -1,6 +1,7 @@
@import "colors";
@import "constants";
// floats
.pull-left {
float: left;
}
@ -13,6 +14,9 @@
clear: both;
}
// text
.text-center,
.center {
text-align: center;
}
@ -21,12 +25,21 @@
text-align: right;
}
.hidden {
display: none;
.text-sm {
font-size: 14px;
}
.width-100 {
width: 100%;
.text-lg {
font-size: 18px;
}
.bold {
font-weight: bold;
}
.numbers-delimiter {
display: inline-block;
width: 5px;
}
.empty-text {
@ -45,95 +58,42 @@
}
}
// display
.hidden {
display: none;
}
// sizing
.width-100 {
width: 100%;
}
// who known
.highlighted {
background: $orange-bg;
color: $black;
}
.text-sm {
font-size: 14px;
}
// generate spacer utility like bootstrap my-2 -> margin-left/right: 2 * $default-spacer
// using $direction.key as css modifier, $direction.values to set css properties
// scale it using $steps
$directions: (
"t": ("margin-top"),
"r": ("margin-right"),
"b": ("margin-bottom"),
"l": ("margin-left"),
"x": ("margin-left", "margin-right"),
"y": ("margin-top", "margin-bottom"),
"": ("margin")
);
$steps: (0, 1, 2, 3, 4, 5, 6, 7, 8);
.text-lg {
font-size: 18px;
}
.mt-1 {
margin-top: $default-spacer;
}
.mt-2 {
margin-top: 2 * $default-spacer;
}
.mt-3 {
margin-top: 3 * $default-spacer;
}
.mt-4 {
margin-top: 4 * $default-spacer;
}
.mt-8 {
margin-top: 8 * $default-spacer;
}
.mb-1 {
margin-bottom: $default-spacer;
}
.mb-2 {
margin-bottom: 2 * $default-spacer;
}
.mb-3 {
margin-bottom: 3 * $default-spacer;
}
.mb-4 {
margin-bottom: 4 * $default-spacer;
}
.mb-8 {
margin-bottom: 8 * $default-spacer;
}
.pt-1 {
padding-top: $default-spacer;
}
.pt-2 {
padding-top: 2 * $default-spacer;
}
.pt-3 {
padding-top: 3 * $default-spacer;
}
.pt-4 {
padding-top: 4 * $default-spacer;
}
.pt-8 {
padding-top: 8 * $default-spacer;
}
.pb-1 {
padding-bottom: $default-spacer;
}
.pb-2 {
padding-bottom: 2 * $default-spacer;
}
.pb-3 {
padding-bottom: 3 * $default-spacer;
}
.pb-4 {
padding-bottom: 4 * $default-spacer;
}
.pb-8 {
padding-bottom: 8 * $default-spacer;
@each $modifier, $properties in $directions {
@each $step in $steps {
@each $property in $properties {
.m#{$modifier}-#{$step} {
#{$property}: $step * $default-spacer;
}
}
}
}

View file

@ -0,0 +1,45 @@
module Administrateurs
class DossierSubmittedMessagesController < AdministrateurController
before_action :retrieve_procedure
def edit
@dossier_submitted_message = build_dossier_submitted_message
end
def update
@dossier_submitted_message = build_dossier_submitted_message(dossier_submitted_message_params)
if @dossier_submitted_message.save
redirect_to admin_procedure_path(@procedure), flash: { notice: "Les informations de fin de dépot ont bien été sauvegardées." }
else
flash.alert = "Impossible de sauvegarder les informations de fin de dépot, veuillez ré-essayer."
render :edit, status: 400
end
end
def create
@dossier_submitted_message = build_dossier_submitted_message(dossier_submitted_message_params)
if @dossier_submitted_message.save
redirect_to admin_procedure_path(@procedure), flash: { notice: "Les informations de fin de dépot ont bien été sauvegardées." }
else
flash.alert = "Impossible de sauvegarder les informations de \"fin de dépot\", veuillez ré-essayer."
render :edit, status: 400
end
end
private
# for now, only works on active revision no matter the procedure_revision_policy
def build_dossier_submitted_message(attributes = {})
dossier_submitted_message = @procedure.active_revision.dossier_submitted_message || @procedure.active_revision.build_dossier_submitted_message
dossier_submitted_message.attributes = attributes unless attributes.empty?
dossier_submitted_message
end
def dossier_submitted_message_params
params.require(:dossier_submitted_message)
.permit(:message_on_submit_by_usager)
end
end
end

View file

@ -17,7 +17,7 @@ class Champs::SiretController < ApplicationController
begin
etablissement = find_etablissement_with_siret
rescue APIEntreprise::API::Error::RequestFailed, APIEntreprise::API::Error::ServiceUnavailable
rescue APIEntreprise::API::Error::RequestFailed, APIEntreprise::API::Error::BadGateway, APIEntreprise::API::Error::TimedOut, APIEntreprise::API::Error::ServiceUnavailable
# i18n-tasks-use t('errors.messages.siret_network_error')
return siret_error(:network_error)
end

View file

@ -17,7 +17,8 @@ module Experts
end
def procedure
@procedure = Procedure.find(params[:procedure_id])
@procedure = current_expert.procedures.find_by(id: params[:procedure_id])
redirect_to(expert_all_avis_path, flash: { alert: "Vous navez pas accès à cette démarche." }) and return unless @procedure
expert_avis = current_expert.avis.includes(:dossier).not_hidden_by_administration.where(dossiers: { groupe_instructeur: GroupeInstructeur.where(procedure: @procedure.id) })
@avis_a_donner = expert_avis.without_answer
@avis_donnes = expert_avis.with_answer
@ -156,7 +157,8 @@ module Experts
end
def set_avis_and_dossier
@avis = Avis.find(params[:id])
@avis = current_expert.avis.find_by(id: params[:id])
redirect_to(expert_all_avis_path, flash: { alert: "Vous navez pas accès à cet avis." }) and return unless @avis
@dossier = @avis.dossier
end

View file

@ -106,7 +106,7 @@ module Users
sanitized_siret = siret_model.siret
begin
etablissement = APIEntrepriseService.create_etablissement(@dossier, sanitized_siret, current_user.id)
rescue APIEntreprise::API::Error::RequestFailed, APIEntreprise::API::Error::BadGateway, APIEntreprise::API::Error::TimedOut
rescue APIEntreprise::API::Error::RequestFailed, APIEntreprise::API::Error::BadGateway, APIEntreprise::API::Error::TimedOut, APIEntreprise::API::Error::ServiceUnavailable
return render_siret_error(t('errors.messages.siret_network_error'))
end
if etablissement.nil?

View file

@ -26,6 +26,19 @@ import { useDeferredSubmit, useHiddenField } from './shared/hooks';
const Context = createContext();
const optionValueByLabel = (values, options, label) => {
const maybeOption = values.includes(label)
? [label, label]
: options.find(([optionLabel]) => optionLabel == label);
return maybeOption ? maybeOption[1] : undefined;
};
const optionLabelByValue = (values, options, value) => {
const maybeOption = values.includes(value)
? [value, value]
: options.find(([, optionValue]) => optionValue == value);
return maybeOption ? maybeOption[0] : undefined;
};
function ComboMultiple({
options,
id,
@ -40,9 +53,6 @@ function ComboMultiple({
invariant(id || label, 'ComboMultiple: `id` or a `label` are required');
invariant(group, 'ComboMultiple: `group` is required');
if (!Array.isArray(options[0])) {
options = options.filter((o) => o).map((o) => [o, o]);
}
const inputRef = useRef();
const [term, setTerm] = useState('');
const [selections, setSelections] = useState(selected);
@ -51,25 +61,22 @@ function ComboMultiple({
const removedLabelledby = `${inputId}-remove`;
const selectedLabelledby = `${inputId}-selected`;
const optionValueByLabel = (label) => {
const maybeOption = newValues.includes(label)
? [label, label]
: options.find(([optionLabel]) => optionLabel == label);
return maybeOption ? maybeOption[1] : undefined;
};
const optionLabelByValue = (value) => {
const maybeOption = newValues.includes(value)
? [value, value]
: options.find(([, optionValue]) => optionValue == value);
return maybeOption ? maybeOption[0] : undefined;
};
const optionsWithLabels = useMemo(
() =>
Array.isArray(options[0])
? options
: options.filter((o) => o).map((o) => [o, o]),
[options]
);
const extraOptions = useMemo(
() =>
acceptNewValues && term && term.length > 2 && !optionLabelByValue(term)
acceptNewValues &&
term &&
term.length > 2 &&
!optionLabelByValue(newValues, optionsWithLabels, term)
? [[term, term]]
: [],
[acceptNewValues, term, newValues.join(',')]
[acceptNewValues, term, optionsWithLabels, newValues]
);
const results = useMemo(
() =>
@ -77,12 +84,12 @@ function ComboMultiple({
...extraOptions,
...(term
? matchSorter(
options.filter(([label]) => !label.startsWith('--')),
optionsWithLabels.filter(([label]) => !label.startsWith('--')),
term
)
: options)
: optionsWithLabels)
].filter(([, value]) => !selections.includes(value)),
[term, selections.join(','), newValues.join(',')]
[term, selections, extraOptions, optionsWithLabels]
);
const [, setHiddenFieldValue, hiddenField] = useHiddenField(group, name);
const awaitFormSubmit = useDeferredSubmit(hiddenField);
@ -100,7 +107,7 @@ function ComboMultiple({
};
const onSelect = (value) => {
const maybeValue = [...extraOptions, ...options].find(
const maybeValue = [...extraOptions, ...optionsWithLabels].find(
([val]) => val == value
);
const selectedValue = maybeValue && maybeValue[1];
@ -128,7 +135,7 @@ function ComboMultiple({
};
const onRemove = (label) => {
const optionValue = optionValueByLabel(label);
const optionValue = optionValueByLabel(newValues, options, label);
if (optionValue) {
saveSelection((selections) =>
selections.filter((value) => value != optionValue)
@ -149,7 +156,9 @@ function ComboMultiple({
) {
if (
term &&
[...extraOptions, ...options].map(([label]) => label).includes(term)
[...extraOptions, ...optionsWithLabels]
.map(([label]) => label)
.includes(term)
) {
event.preventDefault();
onSelect(term);
@ -172,7 +181,9 @@ function ComboMultiple({
const onBlur = () => {
const shouldSelect =
term &&
[...extraOptions, ...options].map(([label]) => label).includes(term);
[...extraOptions, ...optionsWithLabels]
.map(([label]) => label)
.includes(term);
awaitFormSubmit(() => {
if (shouldSelect) {
@ -199,7 +210,7 @@ function ComboMultiple({
<ComboboxToken
key={selection}
describedby={removedLabelledby}
value={optionLabelByValue(selection)}
value={optionLabelByValue(newValues, options, selection)}
/>
))}
</ul>

View file

@ -1,4 +1,4 @@
import React, { useState, useCallback, useRef } from 'react';
import React, { useState, useRef, ChangeEventHandler } from 'react';
import { useDebounce } from 'use-debounce';
import { useQuery } from 'react-query';
import {
@ -68,7 +68,7 @@ function ComboSearch<Result>({
const [, value, label] = transformResult(result);
return label ?? value;
};
const setExternalValueAndId = useCallback((label: string) => {
const setExternalValueAndId = (label: string) => {
const { key, value, result } = resultsMap.current[label];
if (onChange) {
onChange(value, result);
@ -76,36 +76,35 @@ function ComboSearch<Result>({
setExternalId(key);
setExternalValue(value);
}
}, []);
};
const awaitFormSubmit = useDeferredSubmit(hiddenField);
const handleOnChange = useCallback(
({ target: { value } }) => {
setValue(value);
if (!value) {
if (onChange) {
onChange(null);
} else {
setExternalId('');
setExternalValue('');
}
} else if (value.length >= minimumInputLength) {
setSearchTerm(value.trim());
if (allowInputValues) {
setExternalId('');
setExternalValue(value);
}
const handleOnChange: ChangeEventHandler<HTMLInputElement> = ({
target: { value }
}) => {
setValue(value);
if (!value) {
if (onChange) {
onChange(null);
} else {
setExternalId('');
setExternalValue('');
}
},
[minimumInputLength]
);
} else if (value.length >= minimumInputLength) {
setSearchTerm(value.trim());
if (allowInputValues) {
setExternalId('');
setExternalValue(value);
}
}
};
const handleOnSelect = useCallback((value: string) => {
const handleOnSelect = (value: string) => {
setExternalValueAndId(value);
setValue(value);
setSearchTerm('');
awaitFormSubmit.done();
}, []);
};
const { isSuccess, data } = useQuery<void, void, unknown, QueryKey>(
[scope, debouncedSearchTerm, scopeExtra],
@ -117,14 +116,14 @@ function ComboSearch<Result>({
const results =
isSuccess && data ? transformResults(debouncedSearchTerm, data) : [];
const onBlur = useCallback(() => {
const onBlur = () => {
if (!allowInputValues && isSuccess && results[0]) {
const label = getLabel(results[0]);
awaitFormSubmit(() => {
handleOnSelect(label);
});
}
}, [data]);
};
return (
<Combobox onSelect={handleOnSelect}>

View file

@ -28,35 +28,41 @@ export function CadastreLayer({
const map = useMapLibre();
const selectedCadastresRef = useRef(new Set<string>());
const highlightFeature = useCallback((cid: string, highlight: boolean) => {
if (highlight) {
selectedCadastresRef.current.add(cid);
} else {
selectedCadastresRef.current.delete(cid);
}
if (selectedCadastresRef.current.size == 0) {
map.setFilter('parcelle-highlighted', ['in', 'id', '']);
} else {
map.setFilter('parcelle-highlighted', [
'in',
'id',
...selectedCadastresRef.current
]);
}
}, []);
const highlightFeature = useCallback(
(cid: string, highlight: boolean) => {
if (highlight) {
selectedCadastresRef.current.add(cid);
} else {
selectedCadastresRef.current.delete(cid);
}
if (selectedCadastresRef.current.size == 0) {
map.setFilter('parcelle-highlighted', ['in', 'id', '']);
} else {
map.setFilter('parcelle-highlighted', [
'in',
'id',
...selectedCadastresRef.current
]);
}
},
[map]
);
const hoverFeature = useCallback((feature: Feature, hover: boolean) => {
if (!selectedCadastresRef.current.has(feature.properties?.id)) {
map.setFeatureState(
{
source: 'cadastre',
sourceLayer: 'parcelles',
id: String(feature.id)
},
{ hover }
);
}
}, []);
const hoverFeature = useCallback(
(feature: Feature, hover: boolean) => {
if (!selectedCadastresRef.current.has(feature.properties?.id)) {
map.setFeatureState(
{
source: 'cadastre',
sourceLayer: 'parcelles',
id: String(feature.id)
},
{ hover }
);
}
},
[map]
);
useCadastres(featureCollection, {
hoverFeature,

View file

@ -48,6 +48,8 @@ export function DrawLayer({
trash: true
}
});
// We use mapbox-draw plugin with maplibre. They are compatible but types are not.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map.addControl(draw as any, 'top-left');
draw.set(
filterFeatureCollection(featureCollection, SOURCE_SELECTION_UTILISATEUR)
@ -64,11 +66,15 @@ export function DrawLayer({
return () => {
if (drawRef.current) {
// We use mapbox-draw plugin with maplibre. They are compatible but types are not.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map.removeControl(drawRef.current as any);
drawRef.current = null;
}
};
}, [enabled]);
// We only want to rerender draw layer on component mount or when the layer is toggled.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map, enabled]);
const onSetId = useCallback(({ detail }) => {
drawRef.current?.setFeatureProperty(detail.lid, 'id', detail.id);
@ -167,7 +173,9 @@ function useExternalEvents(
useEffect(() => {
fitBounds(featureCollection.bbox as LngLatBoundsLike);
}, []);
// We only want to zoom on bbox on component mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fitBounds]);
useEvent('map:feature:focus', onFeatureFocus);
useEvent('map:feature:create', onFeatureCreate);

View file

@ -44,13 +44,13 @@ export function GeoJSONLayer({
popup.remove();
}
},
[popup]
[map, popup]
);
const onMouseLeave = useCallback(() => {
map.getCanvas().style.cursor = '';
popup.remove();
}, [popup]);
}, [map, popup]);
useExternalEvents(featureCollection);
@ -99,17 +99,22 @@ export function GeoJSONLayer({
function useExternalEvents(featureCollection: FeatureCollection) {
const fitBounds = useFitBounds();
const onFeatureFocus = useCallback(({ detail }) => {
const { id } = detail;
const feature = findFeature(featureCollection, id);
if (feature) {
fitBounds(getBounds(feature.geometry));
}
}, []);
const onFeatureFocus = useCallback(
({ detail }) => {
const { id } = detail;
const feature = findFeature(featureCollection, id);
if (feature) {
fitBounds(getBounds(feature.geometry));
}
},
[featureCollection, fitBounds]
);
useEffect(() => {
fitBounds(featureCollection.bbox as LngLatBoundsLike);
}, []);
// We only want to zoom on bbox on component mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fitBounds]);
useEvent('map:feature:focus', onFeatureFocus);
}
@ -139,7 +144,7 @@ function LineStringLayer({
type: 'line',
paint: lineStringSelectionLine
});
}, []);
}, [map, layerId, sourceId, feature]);
useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId);
@ -172,7 +177,7 @@ function PointLayer({
type: 'circle',
paint: pointSelectionCircle
});
}, []);
}, [map, layerId, sourceId, feature]);
useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId);
@ -212,7 +217,7 @@ function PolygonLayer({
type: 'fill',
paint: polygonSelectionFill
});
}, []);
}, [map, layerId, lineLayerId, sourceId, feature]);
useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId);

View file

@ -1,5 +1,6 @@
import React from 'react';
import { createPortal } from 'react-dom';
import invariant from 'tiny-invariant';
export function FlashMessage({
message,
@ -12,11 +13,13 @@ export function FlashMessage({
sticky?: boolean;
fixed?: boolean;
}) {
const element = document.getElementById('flash_messages');
invariant(element, 'Flash messages root element not found');
return createPortal(
<div className="flash_message center">
<div className={flashClassName(level, sticky, fixed)}>{message}</div>
</div>,
document.getElementById('flash_messages')!
element
);
}

View file

@ -5,7 +5,8 @@ import React, {
useEffect,
useMemo,
ReactNode,
createContext
createContext,
useCallback
} from 'react';
import maplibre, { Map, Style, NavigationControl } from 'maplibre-gl';
@ -37,11 +38,14 @@ export function MapLibre({ children, header, footer, layers }: MapLibreProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [map, setMap] = useState<Map | null>();
const onStyleChange = (style: Style) => {
if (map) {
map.setStyle(style);
}
};
const onStyleChange = useCallback(
(style: Style) => {
if (map) {
map.setStyle(style);
}
},
[map]
);
const { style, ...mapStyleProps } = useStyle(layers, onStyleChange);
useEffect(() => {
@ -56,7 +60,7 @@ export function MapLibre({ children, header, footer, layers }: MapLibreProps) {
setMap(map);
});
}
}, []);
}, [map, style, isSupported]);
if (!isSupported) {
return (

View file

@ -12,16 +12,22 @@ import { useMapLibre } from './MapLibre';
export function useFitBounds() {
const map = useMapLibre();
return useCallback((bbox: LngLatBoundsLike) => {
map.fitBounds(bbox, { padding: 100 });
}, []);
return useCallback(
(bbox: LngLatBoundsLike) => {
map.fitBounds(bbox, { padding: 100 });
},
[map]
);
}
export function useFlyTo() {
const map = useMapLibre();
return useCallback((zoom: number, center: [number, number]) => {
map.flyTo({ zoom, center });
}, []);
return useCallback(
(zoom: number, center: [number, number]) => {
map.flyTo({ zoom, center });
},
[map]
);
}
export function useEvent(eventName: string, callback: EventListener) {
@ -44,12 +50,16 @@ export function useMapEvent(
const map = useMapLibre();
return useEffect(() => {
if (target) {
// event typing is hard
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map.on(eventName as keyof MapLayerEventType, target, callback as any);
} else {
map.on(eventName, callback);
}
return () => {
if (target) {
// event typing is hard
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map.off(eventName as keyof MapLayerEventType, target, callback as any);
} else {
map.off(eventName, callback);
@ -104,7 +114,7 @@ export function useStyle(
[styleId, enabledLayers]
);
useEffect(() => onStyleChange(style), [style]);
useEffect(() => onStyleChange(style), [onStyleChange, style]);
return { style, layers, setStyle, setLayerEnabled, setLayerOpacity };
}

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { AnyLayer } from 'maplibre-gl';
const layers: AnyLayer[] = [

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { AnyLayer } from 'maplibre-gl';
const layers: AnyLayer[] = [

View file

@ -2,6 +2,17 @@ import { QueryClient, QueryFunction } from 'react-query';
import { getJSON, isNumeric } from '@utils';
import { matchSorter } from 'match-sorter';
type Gon = {
gon: {
autocomplete?: {
api_geo_url?: string;
api_adresse_url?: string;
api_education_url?: string;
};
};
};
declare const window: Window & typeof globalThis & Gon;
const API_EDUCATION_QUERY_LIMIT = 5;
const API_GEO_QUERY_LIMIT = 5;
const API_ADRESSE_QUERY_LIMIT = 5;
@ -16,7 +27,7 @@ const API_ADRESSE_QUERY_LIMIT = 5;
const API_GEO_COMMUNES_QUERY_LIMIT = 60;
const { api_geo_url, api_adresse_url, api_education_url } =
(window as any).gon.autocomplete || {};
window.gon.autocomplete || {};
type QueryKey = readonly [
scope: string,
@ -70,8 +81,9 @@ const defaultQueryFn: QueryFunction<unknown, QueryKey> = async ({
}
throw new Error(`Error fetching from "${scope}" API`);
});
(promise as any).cancel = () => controller && controller.abort();
return promise;
return Object.assign(promise, {
cancel: () => controller && controller.abort()
});
};
let paysCache: { label: string }[];
@ -85,6 +97,8 @@ async function getPays(): Promise<{ label: string }[]> {
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// we don't really care about global queryFn type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
queryFn: defaultQueryFn as any
}
}

View file

@ -88,6 +88,15 @@ export function ajax(options: Rails.AjaxOptions) {
});
}
class ResponseError extends Error {
response: Response;
constructor(response: Response) {
super(String(response.statusText || response.status));
this.response = response;
}
}
export function getJSON(url: string, data: unknown, method = 'GET') {
const { query, ...options } = fetchOptions(data, method);
@ -98,9 +107,7 @@ export function getJSON(url: string, data: unknown, method = 'GET') {
}
return response.json();
}
const error = new Error(String(response.statusText || response.status));
(error as any).response = response;
throw error;
throw new ResponseError(response);
});
}
@ -125,8 +132,9 @@ export function on(
);
}
export function isNumeric(n: string) {
return !isNaN(parseFloat(n)) && isFinite(n as any as number);
export function isNumeric(s: string) {
const n = parseFloat(s);
return !isNaN(n) && isFinite(n);
}
function offset(element: HTMLElement) {

View file

@ -6,6 +6,7 @@
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# private :boolean default(FALSE), not null
# rebased_at :datetime
# row :integer
# type :string
# value :string

View file

@ -2,19 +2,57 @@ module DossierRebaseConcern
extend ActiveSupport::Concern
def rebase!
if brouillon? && revision != procedure.published_revision
if can_rebase?
transaction do
rebase
end
end
end
def can_rebase?
revision != procedure.published_revision &&
(brouillon? || accepted_en_construction_changes? || accepted_en_instruction_changes?)
end
def pending_changes
revision.compare(procedure.published_revision)
end
private
def accepted_en_construction_changes?
en_construction? && pending_changes.all? { |change| accepted_en_construction_change?(change) }
end
def accepted_en_instruction_changes?
en_instruction? && pending_changes.all? { |change| accepted_en_instruction_change?(change) }
end
def accepted_en_construction_change?(change)
if change[:model] == :attestation_template || change[:op] == :move || change[:op] == :remove
true
elsif change[:op] == :update
case change[:attribute]
when :carte_layers
true
when :mandatory
change[:from] && !change[:to]
else
false
end
else
false
end
end
def accepted_en_instruction_change?(change)
change[:model] == :attestation_template
end
def rebase
attachments_to_purge = []
geo_areas_to_delete = []
changes_by_type_de_champ = revision.compare(procedure.published_revision)
changes_by_type_de_champ = pending_changes
.filter { |change| change[:model] == :type_de_champ }
.group_by { |change| change[:stable_id] }
@ -51,7 +89,9 @@ module DossierRebaseConcern
when :drop_down_options
update[:value] = nil
when :carte_layers
geo_areas_to_delete += champ.geo_areas
if change[:from].include?(:cadastres) && !change[:to].include?(:cadastres)
geo_areas_to_delete += champ.cadastres
end
end
update[:rebased_at] = Time.zone.now
end

View file

@ -0,0 +1,12 @@
# == Schema Information
#
# Table name: dossier_submitted_messages
#
# id :bigint not null, primary key
# message_on_submit_by_usager :string
# created_at :datetime not null
# updated_at :datetime not null
#
class DossierSubmittedMessage < ApplicationRecord
has_many :revisions, class_name: 'ProcedureRevision', inverse_of: :dossier_submitted_message, dependent: :nullify
end

View file

@ -18,7 +18,6 @@
# description :string
# direction :string
# duree_conservation_dossiers_dans_ds :integer
# duree_conservation_dossiers_hors_ds :integer
# durees_conservation_required :boolean default(TRUE)
# encrypted_api_particulier_token :string
# euro_flag :boolean default(FALSE)
@ -80,6 +79,10 @@ class Procedure < ApplicationRecord
has_one :draft_attestation_template, through: :draft_revision, source: :attestation_template
has_one :published_attestation_template, through: :published_revision, source: :attestation_template
has_one :published_dossier_submitted_message, dependent: :destroy, through: :published_revision, source: :dossier_submitted_message
has_one :draft_dossier_submitted_message, dependent: :destroy, through: :draft_revision, source: :dossier_submitted_message
has_many :dossier_submitted_messages, through: :revisions, source: :dossier_submitted_message
has_many :experts_procedures, dependent: :destroy
has_many :experts, through: :experts_procedures
@ -92,6 +95,10 @@ class Procedure < ApplicationRecord
belongs_to :service, optional: true
belongs_to :zone, optional: true
def active_dossier_submitted_message
published_dossier_submitted_message || draft_dossier_submitted_message
end
def active_revision
brouillon? ? draft_revision : published_revision
end
@ -441,7 +448,8 @@ class Procedure < ApplicationRecord
revision_types_de_champ_private: {
type_de_champ: :types_de_champ
},
attestation_template: []
attestation_template: [],
dossier_submitted_message: []
}
}
include_list[:groupe_instructeurs] = :instructeurs if !is_different_admin
@ -738,9 +746,9 @@ class Procedure < ApplicationRecord
def publish_revision!
update!(draft_revision: create_new_revision, published_revision: draft_revision)
published_revision.touch(:published_at)
dossiers.state_brouillon.find_each do |dossier|
DossierRebaseJob.perform_later(dossier)
end
dossiers
.state_not_termine
.find_each { |dossier| DossierRebaseJob.perform_later(dossier) }
end
def cnaf_enabled?

View file

@ -7,12 +7,14 @@
# created_at :datetime not null
# updated_at :datetime not null
# attestation_template_id :bigint
# procedure_id :bigint not null
# dossier_submitted_message_id :bigint
# procedure_id :bigint not null
#
class ProcedureRevision < ApplicationRecord
self.implicit_order_column = :created_at
belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false
belongs_to :attestation_template, inverse_of: :revisions, optional: true, dependent: :destroy
belongs_to :dossier_submitted_message, inverse_of: :revisions, optional: true, dependent: :destroy
has_many :dossiers, inverse_of: :revision, foreign_key: :revision_id

View file

@ -3,7 +3,7 @@
# Table name: zones
#
# id :bigint not null, primary key
# acronym :string
# acronym :string not null
# label :string
# created_at :datetime not null
# updated_at :datetime not null

View file

@ -0,0 +1,3 @@
= f.label :message_on_submit_by_usager do
Message affiché après l'envoie du dossier
= f.text_area :message_on_submit_by_usager, placeholder: "Merci votre dossier sera traité dans les plus bref delais"

View file

@ -0,0 +1,34 @@
- content_for(:root_class, 'scroll-margins-for-sticky-footer')
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Fin de dépot'] }
.procedure-form
.procedure-form__columns.container
= form_for @dossier_submitted_message,
url: url_for({ controller: 'administrateurs/dossier_submitted_messages', action: :update, id: @procedure.id }),
html: { class: 'form procedure-form__column--form' } do |f|
%h1.page-title
Fin du dépot
%p.notice
L'utilisateur se vera afficher ce message une fois le dossier envoyé
= render partial: 'administrateurs/dossier_submitted_messages/informations', locals: { f: f }
.procedure-form__actions
.actions-left
= f.submit 'Enregistrer', class: 'button primary send'
.procedure-form__column--preview
%h3
.procedure-form__preview-title
Aperçu
.notice
Cet aperçu est mis à jour après chaque sauvegarde.
.procedure-preview
= render partial: 'users/dossiers/merci', locals: { procedure: @procedure, dossier: nil}

View file

@ -234,3 +234,18 @@
%p.card-admin-title MonAvis
%p.card-admin-subtitle Avis des usagers sur votre démarche
%p.button Modifier
= link_to edit_admin_procedure_dossier_submitted_message_path(@procedure), class: 'card-admin' do
- if @procedure.active_dossier_submitted_message.present?
%div
%span.icon.accept
%p.card-admin-status-accept Validé
- else
%div
%span.icon.clock
%p.card-admin-status-todo À configurer
%div
%p.card-admin-title Fin de dépot
%p.card-admin-subtitle Orienter l'usager suite à l'envoie de son dossier
%p.button Modifier

View file

@ -1,8 +1,45 @@
- content_for(:title, t('.cta'))
.container
%h1.mt-2.mb-2= t('.connect')
#agentconnect
.two-columns
.columns-container
.column.agent-intro
%h1.mt-2.mb-2.agent= t('.you_are_an_agent')
.box= t('.in_progress_html')
%p= t('.intro_html', app_name: APPLICATION_NAME)
.center.mt-2
%span.citizen= t('.you_are_a_citizen')
%br
%br
= link_to t('.citizen_page'), new_user_session_path, class: "button expend secondary"
= link_to t('.cta'), agent_connect_login_path, class: "france-connect-agent-login-button"
.column
= t('.connect_html')
= link_to t('.cta'), agent_connect_login_path, class: "france-connect-agent-login-button"
.france-connect-help-link
= link_to t('.whats_agentconnect'), 'https://agentconnect.gouv.fr/', class: 'link', target: '_blank', target: "_blank", rel: "noopener", class: "link"
.france-connect-login-separator
= t('views.shared.france_connect_login.separator')
#session-new.auth-form.sign-in-form
= form_for User.new, url: user_session_path, html: { class: "form" } do |f|
= f.label :email, t('.pro_email')
= f.text_field :email, type: :email, autocomplete: 'username', autofocus: true
= f.label :password, t('views.users.sessions.new.password', min_length: PASSWORD_MIN_LENGTH)
= f.password_field :password, autocomplete: 'current-password'
.auth-options
.flex-no-shrink
= f.check_box :remember_me
= f.label :remember_me, t('views.users.sessions.new.remember_me'), class: 'remember-me'
.text-right
= link_to t('views.users.sessions.new.reset_password'), new_user_password_path, class: "link"
= f.submit t('views.users.sessions.new.connection'), class: "button large primary expand"
- content_for :footer do
= render partial: 'users/dossiers/index_footer'

View file

@ -3,16 +3,19 @@
%p= t(:hello, scope: [:views, :shared, :greetings])
- if !@dossier.brouillon?
%p= t('.body_html', libelle_demarche: @dossier.procedure.libelle)
%p= t('.link')
= round_button(t('.access_message'), messagerie_dossier_url(@dossier), :primary)
- else
- if @dossier.brouillon?
%p= t('.body_draft_html', libelle_demarche: @dossier.procedure.libelle)
%p{ style: "padding: 8px; color: #333333; background-color: #EEEEEE; font-size: 14px;" }
= @body
%p= t('.contact')
- if @service&.email.present?
%p= t('.contact_html', email: @service.email)
- else
%p= t('.contact_no_email')
= round_button(t('.access_file'), dossier_url(@dossier), :primary)
- else
%p= t('.body_html', libelle_demarche: @dossier.procedure.libelle)
%p= t('.link')
= round_button(t('.access_message'), messagerie_dossier_url(@dossier), :primary)
= render 'layouts/mailers/signature', service: @service

View file

@ -62,7 +62,7 @@
%li
= render partial: 'layouts/account_dropdown', locals: { nav_bar_profile: nav_bar_profile }
- elsif request.path != new_user_session_path
- elsif (request.path != new_user_session_path && request.path != agent_connect_path)
- if request.path == new_user_registration_path
%li
= t('views.shared.account.already_user_question')

View file

@ -7,3 +7,8 @@
= t('.line2')
%br
= t('.line3')
%hr
%span.small-simple= t('.are_you_new', app_name: APPLICATION_NAME.gsub("-","&#8209;")).html_safe
%br
%br
= link_to t('views.users.sessions.new.find_procedure'), COMMENT_TROUVER_MA_DEMARCHE_URL, target: "_blank", class: "button expend secondary"

View file

@ -1,6 +1,6 @@
- if FranceConnectService.enabled?
.france-connect-login
%h2
%h2.important-header
= t('views.shared.france_connect_login.title')
%p
= t('views.shared.france_connect_login.description')

View file

@ -0,0 +1,26 @@
.merci.text-center.mb-7
.container
= image_tag('user/envoi-dossier.svg', alt: '', class: 'mt-8')
%h1.mt-4.mb-3.mx-0= t('views.users.dossiers.merci.thanks')
%p.send.m-2.text-lg
= t('views.users.dossiers.merci.dossier_send_l1')
%strong= procedure.libelle
= t('views.users.dossiers.merci.dossier_send_l2')
%p.m-2
= t('views.users.dossiers.merci.dossier_acces_l1')
%strong= t('views.users.dossiers.merci.dossier_acces_l2')
%p.m-2
= t('views.users.dossiers.merci.dossier_edit_l1')
- if !dossier&.read_only?
%strong= t('views.users.dossiers.merci.dossier_edit_l2')
= t('views.users.dossiers.merci.dossier_edit_l3')
%strong= t('views.users.dossiers.merci.dossier_edit_l4')
- if procedure.active_dossier_submitted_message
%p.m-2= procedure.active_dossier_submitted_message.message_on_submit_by_usager
.flex.column.align-center
= link_to t('views.users.dossiers.merci.acces_dossier'), dossier ? dossier_path(dossier) : "#dossier" , class: 'button large primary mt-4'
= link_to t('views.users.dossiers.merci.submit_dossier'), procedure_lien(procedure), class: 'mt-4'
.monavis
!= procedure.monavis_embed

View file

@ -3,26 +3,4 @@
- content_for :footer do
= render partial: "users/procedure_footer", locals: { procedure: @dossier.procedure, dossier: @dossier }
.merci
.container
= image_tag('user/envoi-dossier.svg', alt: '')
%h1= t('views.users.dossiers.merci.thanks')
%p.send
= t('views.users.dossiers.merci.dossier_send_l1')
%b= @dossier.procedure.libelle
= t('views.users.dossiers.merci.dossier_send_l2')
%p
= t('views.users.dossiers.merci.dossier_acces_l1')
%b= t('views.users.dossiers.merci.dossier_acces_l2')
%p
= t('views.users.dossiers.merci.dossier_edit_l1')
- if !@dossier.read_only?
%b= t('views.users.dossiers.merci.dossier_edit_l2')
= t('views.users.dossiers.merci.dossier_edit_l3')
%b= t('views.users.dossiers.merci.dossier_edit_l4')
.flex.column.align-center
= link_to t('views.users.dossiers.merci.acces_dossier'), dossier_path(@dossier), class: 'button large primary'
= link_to t('views.users.dossiers.merci.submit_dossier'), procedure_lien(@dossier.procedure)
.monavis
!= @dossier.procedure.monavis_embed
= render partial: 'users/dossiers/merci', locals: { dossier: @dossier, procedure: @dossier.procedure}

View file

@ -1,6 +1,6 @@
= content_for(:page_id, 'auth')
.auth-form.sign-in-form
#session-new.auth-form.sign-in-form
= form_for resource, url: user_session_path, html: { class: "form" } do |f|
%h1.huge-title= t('views.users.sessions.new.sign_in')
@ -23,15 +23,10 @@
= f.submit t('views.users.sessions.new.connection'), class: "button large primary expand"
.france-connect-login-separator
= t('views.shared.france_connect_login.separator')
- if AgentConnectService.enabled?
.france-connect-login-separator
= t('views.shared.france_connect_login.separator')
.center
%p.mb-2= t('views.users.sessions.new.instructor_or_admin')
= link_to t('views.users.sessions.new.connect_with_agent_connect'), agent_connect_path
%hr
%p.center
%span= t('views.users.sessions.new.are_you_new', app_name: APPLICATION_NAME.gsub("-","&#8209;")).html_safe
%br
%br
= link_to t('views.users.sessions.new.find_procedure'), COMMENT_TROUVER_MA_DEMARCHE_URL, target: "_blank", class: "button expend secondary"
%h2.important-header= t('views.users.sessions.new.state_civil_servant')
%br
= link_to t('views.users.sessions.new.connect_with_agent_connect'), agent_connect_path, class: "button expend secondary"

View file

@ -18,6 +18,7 @@ FileUtils.chdir APP_ROOT do
system('bundle check') || system!('bundle install')
# Install JavaScript dependencies
system! 'node --version'
system! 'bin/yarn install'
puts "\n== Updating webdrivers =="

View file

@ -15,7 +15,9 @@ FileUtils.chdir APP_ROOT do
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
system! 'node --version'
system! 'bin/yarn install'
system! 'bin/yarn clean'
puts "\n== Updating webdrivers =="
system! 'RAILS_ENV=test bin/rails webdrivers:chromedriver:update'

View file

@ -1,5 +1,46 @@
{
"ignored_warnings": [
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "1b805585567775589825c0eda58cb84c074fc760d0a7afb101c023a51427f2b5",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/users/dossiers/_merci.html.haml",
"line": 26,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "current_user.dossiers.includes(:procedure).find(params[:id]).procedure.monavis_embed",
"render_path": [
{
"type": "controller",
"class": "Users::DossiersController",
"method": "merci",
"line": 196,
"file": "app/controllers/users/dossiers_controller.rb",
"rendered": {
"name": "users/dossiers/merci",
"file": "app/views/users/dossiers/merci.html.haml"
}
},
{
"type": "template",
"name": "users/dossiers/merci",
"line": 6,
"file": "app/views/users/dossiers/merci.html.haml",
"rendered": {
"name": "users/dossiers/_merci",
"file": "app/views/users/dossiers/_merci.html.haml"
}
}
],
"location": {
"type": "template",
"template": "users/dossiers/_merci"
},
"user_input": "current_user.dossiers.includes(:procedure)",
"confidence": "Weak",
"note": ""
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
@ -38,7 +79,7 @@
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/traitement.rb",
"line": 51,
"line": 52,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "ActiveRecord::Base.connection.execute(\"select date_trunc('month', r1.processed_at::TIMESTAMPTZ AT TIME ZONE '#{Time.zone.formatted_offset}'::INTERVAL) as month, count(r1.processed_at)\\nfrom (#{Traitement.select(\"max(traitements.processed_at) as processed_at\").termine.where(:dossier => Dossier.state_termine.where(:groupe_instructeur => groupe_instructeurs)).group(:dossier_id).to_sql}) as r1\\ngroup by date_trunc('month', r1.processed_at::TIMESTAMPTZ AT TIME ZONE '#{Time.zone.formatted_offset}'::INTERVAL)\\norder by month desc\\n\")",
"render_path": null,
@ -51,37 +92,6 @@
"confidence": "Medium",
"note": ""
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "483ae8c038244eb3ed709e89846335e2c8ff6579260348ec31d3d03d1c94ad64",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/users/dossiers/merci.html.haml",
"line": 28,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "current_user.dossiers.includes(:procedure).find(params[:id]).procedure.monavis_embed",
"render_path": [
{
"type": "controller",
"class": "Users::DossiersController",
"method": "merci",
"line": 195,
"file": "app/controllers/users/dossiers_controller.rb",
"rendered": {
"name": "users/dossiers/merci",
"file": "app/views/users/dossiers/merci.html.haml"
}
}
],
"location": {
"type": "template",
"template": "users/dossiers/merci"
},
"user_input": "current_user.dossiers.includes(:procedure)",
"confidence": "Weak",
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
@ -102,26 +112,6 @@
"confidence": "Medium",
"note": "The table and column are escaped, which should make this safe"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "c0f93612a68c32da58f327e0b5fa33dd42fd8beb2984cf023338c5aadbbdacca",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/stat.rb",
"line": 83,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "association.where(date_attribute => ((3.months.ago.beginning_of_month..max_date))).group(\"DATE_TRUNC('month', #{date_attribute}::TIMESTAMPTZ AT TIME ZONE '#{Time.zone.formatted_offset}'::INTERVAL)\")",
"render_path": null,
"location": {
"type": "method",
"class": "Stat",
"method": "last_four_months_hash"
},
"user_input": "date_attribute",
"confidence": "Weak",
"note": ""
},
{
"warning_type": "Redirect",
"warning_code": 18,
@ -129,7 +119,7 @@
"check_name": "Redirect",
"message": "Possible unprotected redirect",
"file": "app/controllers/instructeurs/procedures_controller.rb",
"line": 195,
"line": 202,
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
"code": "redirect_to(Export.find_or_create_export(params[:export_format], (params[:time_span_type] or \"everything\"), current_instructeur.groupe_instructeurs.where(:procedure => procedure)).file.service_url)",
"render_path": null,
@ -141,28 +131,8 @@
"user_input": "Export.find_or_create_export(params[:export_format], (params[:time_span_type] or \"everything\"), current_instructeur.groupe_instructeurs.where(:procedure => procedure)).file.service_url",
"confidence": "High",
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "f2bb9bc6a56e44ab36ee18152c657395841cff354baed0a302b8d18650551529",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/stat.rb",
"line": 97,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "association.where(\"#{date_attribute} < ?\", max_date).group(\"DATE_TRUNC('month', #{date_attribute}::TIMESTAMPTZ AT TIME ZONE '#{Time.zone.formatted_offset}'::INTERVAL)\")",
"render_path": null,
"location": {
"type": "method",
"class": "Stat",
"method": "cumulative_hash"
},
"user_input": "date_attribute",
"confidence": "Weak",
"note": ""
}
],
"updated": "2021-12-01 17:39:08 -1000",
"updated": "2022-02-22 15:46:39 +0100",
"brakeman_version": "5.1.1"
}

View file

@ -66,8 +66,8 @@ Rails.application.configure do
protocol: :http
}
# Use Content-Security-Policy-Report-Only headers
config.content_security_policy_report_only = true
# Disallow all connections to external domains during tests
config.content_security_policy_report_only = false
config.active_job.queue_adapter = :test
config.active_storage.service = :test

View file

@ -5,22 +5,20 @@
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
Rails.application.config.content_security_policy do |policy|
# Whitelist image
images_whitelist = ["*.openstreetmap.org", "*.cloud.ovh.net", "*"]
images_whitelist << URI(DS_PROXY_URL).host if DS_PROXY_URL.present?
images_whitelist << URI(MATOMO_IFRAME_URL).host if MATOMO_IFRAME_URL.present?
policy.img_src(:self, :data, :blob, *images_whitelist)
# Whitelist JS: nous, sendinblue et matomo
# miniprofiler et nous avons quelques boutons inline :(
# Javascript: allow us, SendInBlue and Matomo.
# We need unsafe_inline because miniprofiler and us have some inline buttons :(
scripts_whitelist = ["*.sendinblue.com", "*.crisp.chat", "crisp.chat", "*.sibautomation.com", "sibautomation.com", "cdn.jsdelivr.net", "maxcdn.bootstrapcdn.com", "code.jquery.com"]
scripts_whitelist << URI(MATOMO_IFRAME_URL).host if MATOMO_IFRAME_URL.present?
policy.script_src(:self, :unsafe_eval, :unsafe_inline, :blob, *scripts_whitelist)
# Pour les CSS, on a beaucoup de style inline et quelques balises <style>
# c'est trop compliqué pour être rectifié immédiatement (et sans valeur ajoutée:
# c'est hardcodé dans les vues, donc pas injectable).
policy.style_src(:self, "*.crisp.chat", "crisp.chat", 'cdn.jsdelivr.net', 'maxcdn.bootstrapcdn.com', :unsafe_inline)
# CSS: We have a lot of inline style, and some <style> tags.
# It's too complicated to be fixed right now (and it wouldn't add value: this is hardcoded in views, so not subject to injections)
policy.style_src(:self, :unsafe_inline, "*.crisp.chat", "crisp.chat", 'cdn.jsdelivr.net', 'maxcdn.bootstrapcdn.com')
connect_whitelist = ["wss://*.crisp.chat", "*.crisp.chat", "in-automate.sendinblue.com", "app.franceconnect.gouv.fr", "sentry.io", "openmaptiles.geo.data.gouv.fr", "openmaptiles.github.io", "tiles.geo.api.gouv.fr", "wxs.ign.fr"]
connect_whitelist << ENV.fetch('APP_HOST')
@ -31,22 +29,34 @@ Rails.application.config.content_security_policy do |policy|
connect_whitelist << Rails.application.secrets.matomo[:host] if Rails.application.secrets.matomo[:enabled]
policy.connect_src(:self, *connect_whitelist)
# Frames: allow Matomo's iframe on the /suivi page
frame_whitelist = []
frame_whitelist << URI(MATOMO_IFRAME_URL).host if Rails.application.secrets.matomo[:enabled]
policy.frame_src(:self, *frame_whitelist)
# Pour tout le reste, par défaut on accepte uniquement ce qui vient de chez nous
# et dans la notification on inclue la source de l'erreur
# Everything else: allow us
# Add the error source in the violation notification
default_whitelist = ["fonts.gstatic.com", "in-automate.sendinblue.com", "player.vimeo.com", "app.franceconnect.gouv.fr", "sentry.io", "*.crisp.chat", "crisp.chat", "*.crisp.help", "*.sibautomation.com", "sibautomation.com", "data"]
default_whitelist << URI(DS_PROXY_URL).host if DS_PROXY_URL.present?
policy.default_src(:self, :data, :blob, :report_sample, *default_whitelist)
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.fetch('APP_HOST')}/csp/"
# En développement, quand bin/webpack-dev-server est utilisé, on autorise les requêtes faites par le live-reload
# Allow LiveReload requests
policy.connect_src(*policy.connect_src, "ws://localhost:3035", "http://localhost:3035")
# CSP are not enforced in development (see content_security_policy_report_only in development.rb)
# However we notify a random local URL, to see breakage in the DevTools when adding a new external resource.
policy.report_uri "http://#{ENV.fetch('APP_HOST')}/csp/"
elsif Rails.env.test?
# Disallow all connections to external domains during tests
policy.img_src(:self, :data, :blob)
policy.script_src(:self, :unsafe_eval, :unsafe_inline, :blob)
policy.style_src(:self)
policy.connect_src(:self)
policy.frame_src(:self)
policy.default_src(:self, :data, :blob)
else
policy.report_uri CSP_REPORT_URI if CSP_REPORT_URI.present?
end

View file

@ -56,6 +56,7 @@ en:
line1: A simple tool
line2: to manage dematerialized
line3: administrative forms.
are_you_new: First time on %{app_name}?
locale_dropdown:
languages: "Languages"
notifications:
@ -215,10 +216,9 @@ en:
remember_me: Remember me
reset_password: Forgot password?
connection: Sign in
are_you_new: First time on %{app_name}?
find_procedure: Find your procedure
instructor_or_admin: Instructor or Administrator ?
connect_with_agent_connect: Connect with AgentConnect
state_civil_servant: Are you a state civil servant?
connect_with_agent_connect: Visit our dedicated page
passwords:
reset_link_sent:
got_it: Got it!

View file

@ -47,6 +47,7 @@ fr:
line1: Un outil simple
line2: pour gérer les formulaires
line3: administratifs dématérialisés.
are_you_new: Vous êtes nouveau sur %{app_name} ?
locale_dropdown:
languages: "Langues"
notifications:
@ -212,10 +213,9 @@ fr:
remember_me: Se souvenir de moi
reset_password: Mot de passe oublié ?
connection: Se connecter
are_you_new: Vous êtes nouveau sur %{app_name} ?
find_procedure: Trouvez votre démarche
instructor_or_admin: Vous êtes instructeur ou administrateur ?
connect_with_agent_connect: Se connecter avec AgentConnect
state_civil_servant: Vous êtes agent de la fonction publique dÉtat ?
connect_with_agent_connect: Accédez à notre page dédiée
passwords:
reset_link_sent:
email_sent_html: "Nous vous avons envoyé un email à ladresse <strong>%{email}</strong>."

View file

@ -2,10 +2,21 @@ en:
agent_connect:
agent:
index:
connect: Connect with AgentConnect
intro_html: |
AgentConnect allows <b class='bold'>instructors et administrators</b> to use their usual login credentials to connect to %{app_name}.
<br />
<br />
Only agents of <b class='bold'>the Ministry of Ecological Transition</b> can currently benefit from it.
cta: Connect with AgentConnect
you_are_an_agent: Are you an employee of the state civil service or of a state operator?
in_progress_html: |
<p>
<b class="bold">AgentConnect is currently being deployed.</b>
<br>
The ministries and operators that can currently benefit from it are&nbsp;:
</p>
<ul>
<li>the Ministry of Ecological Transition</li>
</ul>
you_are_a_citizen: You are an individual ?
citizen_page: Go to our dedicated page
connect_html: |
<h1 class="mt-2 mb-2">Connect</h1>
<p><b class="bold">With AgentConnect</b></p>
whats_agentconnect: 'What is AgentConnect?'
pro_email: Professional email (nom@site.com)

View file

@ -2,10 +2,21 @@ fr:
agent_connect:
agent:
index:
connect: Connectez-vous avec AgentConnect
intro_html: |
AgentConnect permet aux <b class='bold'>instructeurs et administrateurs</b> dutiliser leurs identifiants habituels pour se connecter à %{app_name}.
<br />
<br />
Seul les agents du <b class='bold'>ministère de la Transition écologique</b> peuvent actuellement en bénéficier.
cta: Sidentifier avec AgentConnect
you_are_an_agent: Vous êtes agent de la fonction publique dʼÉtat ou dʼun opérateur de lʼÉtat ?
in_progress_html: |
<p>
<b class="bold">AgentConnect est en cours de déploiement.</b>
<br>
Les ministères et opérateurs qui peuvent l'utiliser à ce jour sont&nbsp;:
</p>
<ul>
<li>le ministère de la Transition écologique</li>
</ul>
you_are_a_citizen: Vous êtes un particulier ?
citizen_page: Accéder à notre page dédiée
connect_html: |
<h1 class="mt-2 mb-2">Connectez-vous</h1>
<p><b class="bold">Avec AgentConnect</b></p>
whats_agentconnect: 'Quʼest ce quʼAgentConnect ?'
pro_email: Email professionnel (nom@site.com)

View file

@ -8,6 +8,7 @@ en:
To read the message and answer it, select the following link:
body_draft_html: |
You received <b>a new message</b> from the service in charge of reviewing the file you started a draft for on the procedure « %{libelle_demarche} ».
contact: If you chose to contact the service, please use the email available below in the page.
contact_html: "If you chose to contact the service, please send an email directly to this address: <a href=\"mailto:%{email}\">%{email}</a>"
contact_no_email: If you chose to contact the service, please use the contact infos available below.
access_message: Read the message
access_file: Open file

View file

@ -3,11 +3,12 @@ fr:
notify_new_answer:
subject: Nouveau message pour votre dossier nº %{dossier_id} « %{libelle_demarche} »
body_html: |
Vous avez reçu un <b>nouveau message</b> de la part du service en charge de votre dossier sur la démarche « %{libelle_demarche} ».
Vous avez reçu un <b>nouveau message</b> de la part de ladministration en charge de votre dossier sur la démarche « %{libelle_demarche} ».
link: |
Pour consulter le message et y répondre, cliquez sur le bouton ci-dessous :
body_draft_html: |
Vous avez reçu un <b>nouveau message</b>du service pour lequel votre dossier est en brouillon pour la démarche « %{libelle_demarche} ».
contact: Si vous souhaitez contacter le service, merci de le faire directement à l'aide de l'email en bas de page.
Vous avez reçu un <b>nouveau message</b> de ladministration, au sujet de votre dossier en brouillon pour la démarche « %{libelle_demarche} ».
contact_html: "Si vous souhaitez répondre à ce message, contactez directement ladministration à ladresse suivante : <a href=\"mailto:%{email}\">%{email}</a>"
contact_no_email: Si vous souhaitez répondre à ce message, contactez directement ladministration à laide des coordonnées en bas de cet email.
access_message: Lire le message
access_file: Voir le dossier

View file

@ -466,6 +466,7 @@ Rails.application.routes.draw do
resource :attestation_template, only: [:edit, :update, :create] do
get 'preview', on: :member
end
resource :dossier_submitted_message, only: [:edit, :update, :create]
# ADDED TO ACCESS IT FROM THE IFRAME
get 'attestation_template/preview' => 'attestation_templates#preview'
end

View file

@ -0,0 +1,9 @@
class CreateDossierSubmittedMessages < ActiveRecord::Migration[6.1]
def change
create_table :dossier_submitted_messages do |t|
t.string :message_on_submit_by_usager
t.timestamps
end
add_reference :procedure_revisions, :dossier_submitted_message, foreign_key: { to_table: :dossier_submitted_messages }, null: true, index: true
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_02_04_093401) do
ActiveRecord::Schema.define(version: 2022_02_04_130722) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -275,6 +275,12 @@ ActiveRecord::Schema.define(version: 2022_02_04_093401) do
t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until"
end
create_table "dossier_submitted_messages", force: :cascade do |t|
t.string "message_on_submit_by_usager"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "dossier_transfer_logs", force: :cascade do |t|
t.string "from", null: false
t.string "to", null: false
@ -324,10 +330,10 @@ ActiveRecord::Schema.define(version: 2022_02_04_093401) do
t.datetime "identity_updated_at"
t.datetime "depose_at"
t.datetime "hidden_by_user_at"
t.datetime "hidden_by_administration_at"
t.string "hidden_by_reason"
t.index "to_tsvector('french'::regconfig, (search_terms || private_search_terms))", name: "index_dossiers_on_search_terms_private_search_terms", using: :gin
t.index "to_tsvector('french'::regconfig, search_terms)", name: "index_dossiers_on_search_terms", using: :gin
t.datetime "hidden_by_administration_at"
t.index ["archived"], name: "index_dossiers_on_archived"
t.index ["dossier_transfer_id"], name: "index_dossiers_on_dossier_transfer_id"
t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id"
@ -595,7 +601,9 @@ ActiveRecord::Schema.define(version: 2022_02_04_093401) do
t.datetime "updated_at", null: false
t.datetime "published_at"
t.bigint "attestation_template_id"
t.bigint "dossier_submitted_message_id"
t.index ["attestation_template_id"], name: "index_procedure_revisions_on_attestation_template_id"
t.index ["dossier_submitted_message_id"], name: "index_procedure_revisions_on_dossier_submitted_message_id"
t.index ["procedure_id"], name: "index_procedure_revisions_on_procedure_id"
end
@ -876,6 +884,7 @@ ActiveRecord::Schema.define(version: 2022_02_04_093401) do
add_foreign_key "procedure_revision_types_de_champ", "procedure_revisions", column: "revision_id"
add_foreign_key "procedure_revision_types_de_champ", "types_de_champ"
add_foreign_key "procedure_revisions", "attestation_templates"
add_foreign_key "procedure_revisions", "dossier_submitted_messages"
add_foreign_key "procedure_revisions", "procedures"
add_foreign_key "procedures", "procedure_revisions", column: "draft_revision_id"
add_foreign_key "procedures", "procedure_revisions", column: "published_revision_id"

View file

@ -0,0 +1,10 @@
namespace :after_party do
desc 'Deployment task: cleanup_deleted_dossiers'
task cleanup_deleted_dossiers: :environment do
puts "Running deploy task 'cleanup_deleted_dossiers'"
DeletedDossier.where(state: :brouillon).destroy_all
AfterParty::TaskRecord.create version: '20200326133630'
end
end

View file

@ -0,0 +1,27 @@
namespace :after_party do
desc 'Deployment task: process_expired_dossiers_en_construction'
task process_expired_dossiers_en_construction: :environment do
puts "Running deploy task 'process_expired_dossiers_en_construction'"
if ENV['APP_NAME'] == 'tps'
dossiers_close_to_expiration = Dossier
.en_construction_close_to_expiration
.without_en_construction_expiration_notice_sent
ExpiredDossiersDeletionService.send_expiration_notices(dossiers_close_to_expiration)
BATCH_SIZE = 1000
((dossiers_close_to_expiration.count / BATCH_SIZE).ceil + 1).times do |n|
dossiers_close_to_expiration
.offset(n * BATCH_SIZE)
.limit(BATCH_SIZE)
.update_all(en_construction_close_to_expiration_notice_sent_at: Time.zone.now + n.days)
end
end
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord.create version: '20200401123317'
end
end

View file

@ -0,0 +1,35 @@
namespace :after_party do
desc 'Deployment task: fix_champ_etablissement'
task fix_champ_etablissement: :environment do
puts "Running deploy task 'fix_champ_etablissement'"
etablissements = Etablissement.joins(:champ).where.not(dossier_id: nil).where('etablissements.created_at > ?', 1.month.ago)
dossiers_modif = []
etablissements.find_each do |e|
if e.dossier
user = e.dossier.user
dossier = e.dossier
if user.dossiers.count == 1 && user.siret == e.champ.value
e.update!(dossier_id: nil)
dossier.reload.etablissement = e.reload.dup
dossier.save!
dossiers_modif << dossier.id
fetch_api_entreprise_infos(dossier.etablissement.id, dossier.procedure.id, user.id)
end
end
end
puts "Nb dossiers modifiés: #{dossiers_modif.size}"
AfterParty::TaskRecord.create version: '20200527124112'
end
def fetch_api_entreprise_infos(etablissement_id, procedure_id, user_id)
[
APIEntreprise::EntrepriseJob, APIEntreprise::AssociationJob, APIEntreprise::ExercicesJob,
APIEntreprise::EffectifsJob, APIEntreprise::EffectifsAnnuelsJob, APIEntreprise::AttestationSocialeJob,
APIEntreprise::BilansBdfJob
].each do |job|
job.perform_later(etablissement_id, procedure_id)
end
APIEntreprise::AttestationFiscaleJob.perform_later(etablissement_id, procedure_id, user_id)
end
end

View file

@ -0,0 +1,20 @@
namespace :after_party do
desc 'Deployment task: fix_dossier_etablissement'
task fix_dossier_etablissement: :environment do
puts "Running deploy task 'fix_dossier_etablissement'"
etablissements = Etablissement.joins(:champ).where.not(dossier_id: nil).where('etablissements.created_at > ?', 1.month.ago)
dossiers_modif = []
etablissements.find_each do |e|
if e.dossier
dossier = e.dossier
e.update!(dossier_id: nil)
dossier.reload.etablissement = e.reload.dup
dossier.save!
dossiers_modif << dossier.id
end
end
puts "Nb dossiers modifiés: #{dossiers_modif.size}"
AfterParty::TaskRecord.create version: '20200528124044'
end
end

View file

@ -0,0 +1,25 @@
namespace :after_party do
desc 'Deployment task: drop_down_list_options_to_json'
task drop_down_list_options_to_json: :environment do
puts "Running deploy task 'drop_down_list_options_to_json'"
types_de_champ = TypeDeChamp.joins(:drop_down_list).where(type_champ: [
TypeDeChamp.type_champs.fetch(:drop_down_list),
TypeDeChamp.type_champs.fetch(:multiple_drop_down_list),
TypeDeChamp.type_champs.fetch(:linked_drop_down_list)
])
progress = ProgressReport.new(types_de_champ.count)
types_de_champ.find_each do |type_de_champ|
type_de_champ.drop_down_list_value = type_de_champ.drop_down_list_value
if type_de_champ.save
type_de_champ.drop_down_list.destroy
end
progress.inc
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord.create version: '20200618121241'
end
end

View file

@ -0,0 +1,22 @@
namespace :after_party do
desc 'Deployment task: migrate_revisions'
task migrate_revisions: :environment do
puts "Running deploy task 'migrate_revisions'"
procedures = Procedure.with_discarded.where(draft_revision_id: nil)
progress = ProgressReport.new(procedures.count)
puts "Processing procedures"
procedures.find_each do |procedure|
RevisionsMigration.add_revisions(procedure)
progress.inc
end
progress.finish
TmpDossiersMigrateRevisionsJob.perform_later([])
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord.create version: '20200625113026'
end
end

View file

@ -0,0 +1,18 @@
namespace :after_party do
desc 'Deployment task: add_traitements_from_dossiers'
task add_traitements_from_dossiers: :environment do
puts "Running deploy task 'add_traitements_from_dossiers'"
dossiers_termines = Dossier.state_termine
progress = ProgressReport.new(dossiers_termines.count)
dossiers_termines.find_each do |dossier|
dossier.traitements.create!(state: dossier.state, motivation: dossier.motivation, processed_at: dossier.processed_at)
progress.inc
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord.create version: '20200630154829'
end
end

View file

@ -0,0 +1,18 @@
namespace :after_party do
desc 'Deployment task: add_default_skip_validation_to_piece_justificative'
task add_default_skip_validation_to_piece_justificative: :environment do
puts "Running deploy task 'add_default_skip_validation_to_piece_justificative'"
tdcs = TypeDeChamp.where(type_champ: TypeDeChamp.type_champs.fetch(:piece_justificative))
progress = ProgressReport.new(tdcs.count)
tdcs.find_each do |tdc|
tdc.update(skip_pj_validation: true)
progress.inc
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord.create version: '20200708101123'
end
end

View file

@ -0,0 +1,25 @@
namespace :after_party do
desc 'Deployment task: fix_cloned_revisions'
task fix_cloned_revisions: :environment do
puts "Running deploy task 'fix_cloned_revisions'"
Procedure.with_discarded.where(aasm_state: :brouillon).where.not(published_revision_id: nil).update_all(published_revision_id: nil)
types_de_champ = TypeDeChamp.joins(:revision).where('types_de_champ.procedure_id != procedure_revisions.procedure_id')
progress = ProgressReport.new(types_de_champ.count)
types_de_champ.find_each do |type_de_champ|
procedure = type_de_champ.procedure ? type_de_champ.procedure : Procedure.with_discarded.find(type_de_champ.procedure_id)
revision_id = procedure.published_revision_id || procedure.draft_revision_id
type_de_champ.update_column(:revision_id, revision_id)
progress.inc
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -0,0 +1,71 @@
namespace :after_party do
desc 'Deployment task: fix_geo_areas_geometry'
task fix_geo_areas_geometry: :environment do
puts "Running deploy task 'fix_geo_areas_geometry'"
geometry_collections = GeoArea.where("geometry -> 'type' = '\"GeometryCollection\"'")
multi_polygons = GeoArea.where("geometry -> 'type' = '\"MultiPolygon\"'")
multi_line_strings = GeoArea.where("geometry -> 'type' = '\"MultiLineString\"'")
def valid_geometry?(geometry)
RGeo::GeoJSON.decode(geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory)
true
rescue
false
end
progress = ProgressReport.new(geometry_collections.count)
geometry_collections.find_each do |geometry_collection|
geometry_collection.geometry['geometries'].each do |geometry|
if valid_geometry?(geometry)
geometry_collection.champ.geo_areas.create!(geometry: geometry, source: 'selection_utilisateur')
end
end
geometry_collection.destroy
progress.inc
end
progress.finish
progress = ProgressReport.new(multi_line_strings.count)
multi_line_strings.find_each do |multi_line_string|
multi_line_string.geometry['coordinates'].each do |coordinates|
geometry = {
type: 'LineString',
coordinates: coordinates
}
if valid_geometry?(geometry)
multi_line_string.champ.geo_areas.create!(geometry: geometry, source: 'selection_utilisateur')
end
end
multi_line_string.destroy
progress.inc
end
progress.finish
progress = ProgressReport.new(multi_polygons.count)
multi_polygons.find_each do |multi_polygon|
multi_polygon.geometry['coordinates'].each do |coordinates|
geometry = {
type: 'Polygon',
coordinates: coordinates
}
if valid_geometry?(geometry)
multi_polygon.champ.geo_areas.create!(geometry: geometry, source: 'selection_utilisateur')
end
end
multi_polygon.destroy
progress.inc
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -0,0 +1,50 @@
namespace :after_party do
desc 'Deployment task: migrate_filters_to_use_stable_id'
task migrate_filters_to_use_stable_id: :environment do
puts "Running deploy task 'migrate_filters_to_use_stable_id'"
procedure_presentations = ProcedurePresentation.where("filters -> 'migrated' IS NULL")
progress = ProgressReport.new(procedure_presentations.count)
procedure_presentations.find_each do |procedure_presentation|
filters = procedure_presentation.filters
sort = procedure_presentation.sort
displayed_fields = procedure_presentation.displayed_fields
['tous', 'suivis', 'traites', 'a-suivre', 'archives'].each do |statut|
filters[statut] = filters[statut].map do |filter|
table, column = filter.values_at('table', 'column')
if table && (table == 'type_de_champ' || table == 'type_de_champ_private')
type_de_champ = TypeDeChamp.find_by(id: column)
filter['column'] = type_de_champ&.stable_id&.to_s
end
filter
end
end
table, column = sort.values_at('table', 'column')
if table && (table == 'type_de_champ' || table == 'type_de_champ_private')
type_de_champ = TypeDeChamp.find_by(id: column)
sort['column'] = type_de_champ&.stable_id&.to_s
end
displayed_fields = displayed_fields.map do |displayed_field|
table, column = displayed_field.values_at('table', 'column')
if table && (table == 'type_de_champ' || table == 'type_de_champ_private')
type_de_champ = TypeDeChamp.find_by(id: column)
displayed_field['column'] = type_de_champ&.stable_id&.to_s
end
displayed_field
end
filters['migrated'] = true
procedure_presentation.update_columns(filters: filters, sort: sort, displayed_fields: displayed_fields)
progress.inc
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -0,0 +1,11 @@
namespace :after_party do
desc 'Deployment task: setup_first_stats'
task setup_first_stats: :environment do
Stat.update_stats
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -0,0 +1,19 @@
namespace :after_party do
desc 'Deployment task: fix_types_de_champ_revisions'
task fix_types_de_champ_revisions: :environment do
puts "Running deploy task 'fix_types_de_champ_revisions'"
types_de_champ = TypeDeChamp.joins(:parent).where('types_de_champ.revision_id != parents_types_de_champ.revision_id')
progress = ProgressReport.new(types_de_champ.count)
types_de_champ.find_each do |type_de_champ|
type_de_champ.update_column(:revision_id, type_de_champ.parent.revision_id)
progress.inc
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -54,6 +54,7 @@
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"babel-eslint": "^10.1.0",
"del-cli": "^4.0.1",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
@ -66,6 +67,7 @@
"webpack-dev-server": "^4.6.0"
},
"scripts": {
"clean": "del tmp public/packs public/packs-test",
"lint:js": "eslint --ext .js,.jsx,.ts,.tsx ./app/javascript ./config/webpack",
"webpack:build": "NODE_ENV=production bin/webpack",
"lint:types": "tsc",

View file

@ -0,0 +1,81 @@
include ActionDispatch::TestProcess
describe Administrateurs::DossierSubmittedMessagesController, type: :controller do
let(:administrateur) { create(:administrateur) }
before { sign_in(administrateur.user) }
describe '#create' do
context 'when procedure is not published' do
let(:procedure) { create(:procedure, administrateur: administrateur) }
it 'creates a DossierSubmittedMessage on draft_revision' do
message_on_submit_by_usager = "hello"
expect {
post(:create, params: { procedure_id: procedure.id, dossier_submitted_message: { message_on_submit_by_usager: message_on_submit_by_usager } })
}.to change { DossierSubmittedMessage.count }.by(1)
expect(response).to redirect_to admin_procedure_path(procedure)
expect(procedure.reload.draft_revision.dossier_submitted_message).to eq(DossierSubmittedMessage.first)
end
end
context 'when procedure is published' do
let(:procedure) { create(:procedure, :published, administrateur: administrateur) }
it 'creates a DossierSubmittedMessage on published_revision' do
message_on_submit_by_usager = "hello"
expect {
post(:create, params: { procedure_id: procedure.id, dossier_submitted_message: { message_on_submit_by_usager: message_on_submit_by_usager } })
}.to change { DossierSubmittedMessage.count }.by(1)
expect(response).to redirect_to admin_procedure_path(procedure)
expect(procedure.reload.published_revision.dossier_submitted_message).to eq(DossierSubmittedMessage.first)
end
end
end
describe '#edit' do
context 'when procedure is draft and have a DossierSubmittedMessage' do
let(:procedure) { create(:procedure, :with_dossier_submitted_message, administrateur: administrateur) }
it 'assigns the existing DossierSubmittedMessage' do
get(:edit, params: { procedure_id: procedure.id })
expect(response).to have_http_status(200)
expect(assigns(:dossier_submitted_message)).to eq(procedure.active_dossier_submitted_message)
end
end
context 'when draft procedure does not have dossier_submitted_message' do
let(:procedure) { create(:procedure, administrateur: administrateur) }
it 'builds a new DossierSubmittedMessage' do
get(:edit, params: { procedure_id: procedure.id })
expect(response).to have_http_status(200)
expect(assigns(:dossier_submitted_message).persisted?).to eq(false)
expect(assigns(:dossier_submitted_message)).to be_an_instance_of(DossierSubmittedMessage)
end
end
end
describe '#update' do
context 'when procedure is draft' do
let(:procedure) { create(:procedure, :with_dossier_submitted_message, administrateur: administrateur) }
it 'updates the existing DossierSubmittedMessage on draft_revision' do
new_message_on_submit_by_usager = "hello"
patch(:update, params: { procedure_id: procedure.id, dossier_submitted_message: { message_on_submit_by_usager: new_message_on_submit_by_usager } })
expect(response).to redirect_to admin_procedure_path(procedure)
expect(procedure.draft_revision.dossier_submitted_message.message_on_submit_by_usager).to eq(new_message_on_submit_by_usager)
end
end
context 'when draft procedure is published' do
let(:procedure) { create(:procedure, :published, :with_dossier_submitted_message, administrateur: administrateur) }
it 'updates the existing DossierSubmittedMessage on published_revision' do
new_message_on_submit_by_usager = "hello"
patch(:update, params: { procedure_id: procedure.id, dossier_submitted_message: { message_on_submit_by_usager: new_message_on_submit_by_usager } })
expect(response).to redirect_to admin_procedure_path(procedure)
expect(procedure.published_revision.dossier_submitted_message.message_on_submit_by_usager).to eq(new_message_on_submit_by_usager)
end
end
end
end

View file

@ -25,18 +25,31 @@ describe Experts::AvisController, type: :controller do
end
describe '#procedure' do
before { get :procedure, params: { procedure_id: procedure.id } }
context 'without filter' do
before { get :procedure, params: { procedure_id: procedure.id } }
it { expect(response).to have_http_status(:success) }
it { expect(assigns(:avis_a_donner)).to match([avis_without_answer]) }
it { expect(assigns(:avis_donnes)).to match([avis_with_answer]) }
it { expect(assigns(:statut)).to eq('a-donner') }
it { expect(response).to have_http_status(:success) }
it { expect(assigns(:avis_a_donner)).to match([avis_without_answer]) }
it { expect(assigns(:avis_donnes)).to match([avis_with_answer]) }
it { expect(assigns(:statut)).to eq('a-donner') }
end
context 'with a statut equal to donnes' do
before { get :procedure, params: { statut: 'donnes', procedure_id: procedure.id } }
it { expect(assigns(:statut)).to eq('donnes') }
end
context 'with different procedure' do
subject { get :procedure, params: { statut: 'donnes', procedure_id: procedure.id } }
it 'fails' do
sign_in(create(:expert).user)
subject
expect(response).to redirect_to(expert_all_avis_path)
expect(flash.alert).to eq("Vous navez pas accès à cette démarche.")
end
end
end
describe '#bilans_bdf' do
@ -64,22 +77,52 @@ describe Experts::AvisController, type: :controller do
expect(response).to redirect_to(root_path)
end
end
context 'with an avis that does not belongs to current_expert' do
it "refuse l'accès au dossier" do
sign_in(create(:expert).user)
subject
expect(response).to redirect_to(expert_all_avis_path)
expect(flash.alert).to eq("Vous navez pas accès à cet avis.")
end
end
end
describe '#instruction' do
before { get :instruction, params: { id: avis_without_answer.id, procedure_id: procedure.id } }
it { expect(response).to have_http_status(:success) }
it { expect(assigns(:avis)).to eq(avis_without_answer) }
it { expect(assigns(:dossier)).to eq(dossier) }
subject { get :instruction, params: { id: avis_without_answer.id, procedure_id: procedure.id } }
context 'with valid avis' do
before { subject }
it { expect(response).to have_http_status(:success) }
it { expect(assigns(:avis)).to eq(avis_without_answer) }
it { expect(assigns(:dossier)).to eq(dossier) }
end
context 'with an avis that does not belongs to current_expert' do
it "refuse l'accès au dossier" do
sign_in(create(:expert).user)
subject
expect(response).to redirect_to(expert_all_avis_path)
expect(flash.alert).to eq("Vous navez pas accès à cet avis.")
end
end
end
describe '#messagerie' do
before { get :messagerie, params: { id: avis_without_answer.id, procedure_id: procedure.id } }
subject { get :messagerie, params: { id: avis_without_answer.id, procedure_id: procedure.id } }
context 'with valid avis' do
before { subject }
it { expect(response).to have_http_status(:success) }
it { expect(assigns(:avis)).to eq(avis_without_answer) }
it { expect(assigns(:dossier)).to eq(dossier) }
it { expect(response).to have_http_status(:success) }
it { expect(assigns(:avis)).to eq(avis_without_answer) }
it { expect(assigns(:dossier)).to eq(dossier) }
end
context 'with an avis that does not belongs to current_expert' do
it "refuse l'accès au dossier" do
sign_in(create(:expert).user)
subject
expect(response).to redirect_to(expert_all_avis_path)
expect(flash.alert).to eq("Vous navez pas accès à cet avis.")
end
end
end
describe '#update' do
@ -118,6 +161,14 @@ describe Experts::AvisController, type: :controller do
expect(flash.notice).to eq('Votre réponse est enregistrée.')
end
end
context 'with an avis that does not belongs to current_expert' do
it "refuse l'accès au dossier" do
sign_in(create(:expert).user)
patch :update, params: { id: avis_without_answer.id, procedure_id: procedure.id, avis: { answer: 'answer' } }
expect(response).to redirect_to(expert_all_avis_path)
expect(flash.alert).to eq("Vous navez pas accès à cet avis.")
end
end
end
describe '#create_commentaire' do

View file

@ -0,0 +1,5 @@
FactoryBot.define do
factory :dossier_submitted_message do
message_on_submit_by_usager { "BAM !" }
end
end

View file

@ -24,10 +24,11 @@ FactoryBot.define do
types_de_champ_private { [] }
updated_at { nil }
attestation_template { nil }
dossier_submitted_message { nil }
end
after(:build) do |procedure, evaluator|
initial_revision = build(:procedure_revision, procedure: procedure, attestation_template: evaluator.attestation_template)
initial_revision = build(:procedure_revision, procedure: procedure, attestation_template: evaluator.attestation_template, dossier_submitted_message: evaluator.dossier_submitted_message)
add_types_de_champs(evaluator.types_de_champ, to: initial_revision, scope: :public)
add_types_de_champs(evaluator.types_de_champ_private, to: initial_revision, scope: :private)
@ -319,6 +320,12 @@ FactoryBot.define do
end
end
end
trait :with_dossier_submitted_message do
after(:build) do |procedure, _evaluator|
build(:dossier_submitted_message, revisions: [procedure.active_revision])
end
end
end
end

View file

@ -0,0 +1,25 @@
describe '20200708101123_add_default_skip_validation_to_piece_justificative.rake' do
let(:rake_task) { Rake::Task['after_party:add_default_skip_validation_to_piece_justificative'] }
let!(:pj_type_de_champ) { create(:type_de_champ_piece_justificative) }
let!(:text_type_de_champ) { create(:type_de_champ_text) }
before do
rake_task.invoke
text_type_de_champ.reload
pj_type_de_champ.reload
end
after { rake_task.reenable }
context 'on a piece_justificative type de champ' do
it 'sets the skip_pj_validation option' do
expect(pj_type_de_champ.skip_pj_validation).to be_truthy
end
end
context 'on a non piece_justificative type de champ' do
it 'does not set the skip_pj_validation option' do
expect(text_type_de_champ.skip_pj_validation).to be_blank
end
end
end

View file

@ -0,0 +1,60 @@
describe '20201001161931_migrate_filters_to_use_stable_id' do
let(:rake_task) { Rake::Task['after_party:migrate_filters_to_use_stable_id'] }
let(:procedure) { create(:procedure, :with_instructeur, :with_type_de_champ) }
let(:type_de_champ) { procedure.types_de_champ.first }
let(:sort) do
{
"table" => "type_de_champ",
"column" => type_de_champ.id.to_s,
"order" => "asc"
}
end
let(:filters) do
{
'tous' => [
{
"label" => "test",
"table" => "type_de_champ",
"column" => type_de_champ.id.to_s,
"value" => "test"
}
],
'suivis' => [],
'traites' => [],
'a-suivre' => [],
'archives' => []
}
end
let(:displayed_fields) do
[
{
"label" => "test",
"table" => "type_de_champ",
"column" => type_de_champ.id.to_s
}
]
end
let!(:procedure_presentation) do
type_de_champ.update_column(:stable_id, 13)
procedure_presentation = create(:procedure_presentation, procedure: procedure, assign_to: procedure.groupe_instructeurs.first.assign_tos.first)
procedure_presentation.update_columns(sort: sort, filters: filters, displayed_fields: displayed_fields)
procedure_presentation
end
before do
rake_task.invoke
procedure_presentation.reload
end
after { rake_task.reenable }
context "should migrate procedure_presentation" do
it "columns are updated" do
expect(procedure_presentation.sort['column']).to eq(type_de_champ.stable_id.to_s)
expect(procedure_presentation.filters['tous'][0]['column']).to eq(type_de_champ.stable_id.to_s)
expect(procedure_presentation.displayed_fields[0]['column']).to eq(type_de_champ.stable_id.to_s)
expect(procedure_presentation.filters['migrated']).to eq(true)
end
end
end

View file

@ -27,15 +27,23 @@ RSpec.describe DossierMailer, type: :mailer do
end
describe '.notify_new_answer with dossier brouillon' do
let(:dossier) { create(:dossier, procedure: create(:simple_procedure)) }
let(:service) { build(:service) }
let(:procedure) { create(:simple_procedure, service: service) }
let(:dossier) { create(:dossier, procedure: procedure) }
let(:commentaire) { create(:commentaire, dossier: dossier) }
subject { described_class.with(commentaire: commentaire).notify_new_answer }
it { expect(subject.subject).to include("Nouveau message") }
it { expect(subject.subject).to include(dossier.id.to_s) }
it { expect(subject.body).to include(dossier.procedure.service.email) }
it { expect(subject.body).not_to include(messagerie_dossier_url(dossier)) }
it_behaves_like 'a dossier notification'
context 'when there is no associated service' do
let(:service) { nil }
it { expect { subject }.not_to raise_error }
end
end
describe '.notify_new_answer with dossier en construction' do

View file

@ -5,7 +5,7 @@ class DossierMailerPreview < ActionMailer::Preview
end
def notify_new_answer
DossierMailer.notify_new_answer(dossier)
DossierMailer.with(commentaire: commentaire(on: draft)).notify_new_answer
end
def notify_revert_to_instruction
@ -88,7 +88,7 @@ class DossierMailerPreview < ActionMailer::Preview
end
def draft
Dossier.new(id: 47882, procedure: procedure, user: user)
Dossier.new(id: 47882, state: :brouillon, procedure: procedure, user: user)
end
def dossier
@ -119,4 +119,9 @@ class DossierMailerPreview < ActionMailer::Preview
def transfer
DossierTransfer.new(email: usager_email, dossiers: [dossier, dossier_accepte])
end
def commentaire(on:)
dossier = on
Commentaire.new(id: 7726, body: "Bonjour, Vous avez commencé le dépôt dun dossier pour une subvention DETR /DSIL. Dans le cas où votre opération naurait pas connu un commencement dexécution, vous êtes encouragé(e) à redéposer un nouveau dossier sur le formulaire de cette année.\nLa DDT", dossier: dossier)
end
end

View file

@ -1573,6 +1573,158 @@ describe Dossier do
it { expect(dossier.spreadsheet_columns(types_de_champ: [])).to include(["État du dossier", "Brouillon"]) }
end
describe '#can_rebase?' do
let(:procedure) { create(:procedure, :with_type_de_champ_mandatory, :with_yes_no, attestation_template: build(:attestation_template)) }
let(:attestation_template) { procedure.draft_revision.attestation_template.find_or_revise! }
let(:type_de_champ) { procedure.types_de_champ.find { |tdc| !tdc.mandatory? } }
let(:mandatory_type_de_champ) { procedure.types_de_champ.find(&:mandatory?) }
before { Flipper.enable(:procedure_revisions, procedure) }
context 'en_construction' do
let(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
before do
procedure.publish!
procedure.reload
dossier
end
context 'with added type de champ' do
before do
procedure.draft_revision.add_type_de_champ({
type_champ: TypeDeChamp.type_champs.fetch(:text),
libelle: "Un champ text"
})
procedure.publish_revision!
dossier.reload
end
it 'should be false' do
expect(dossier.pending_changes).not_to be_empty
expect(dossier.can_rebase?).to be_falsey
end
end
context 'with type de champ made optional' do
before do
procedure.draft_revision.find_or_clone_type_de_champ(mandatory_type_de_champ.stable_id).update(mandatory: false)
procedure.publish_revision!
dossier.reload
end
it 'should be true' do
expect(dossier.pending_changes).not_to be_empty
expect(dossier.can_rebase?).to be_truthy
end
end
context 'with type de champ made mandatory' do
before do
procedure.draft_revision.find_or_clone_type_de_champ(type_de_champ.stable_id).update(mandatory: true)
procedure.publish_revision!
dossier.reload
end
it 'should be false' do
expect(dossier.pending_changes).not_to be_empty
expect(dossier.can_rebase?).to be_falsey
end
end
context 'with removed type de champ' do
before do
procedure.draft_revision.remove_type_de_champ(type_de_champ.stable_id)
procedure.publish_revision!
dossier.reload
end
it 'should be true' do
expect(dossier.pending_changes).not_to be_empty
expect(dossier.can_rebase?).to be_truthy
end
end
context 'with attestation template changes' do
before do
attestation_template.update(title: "Test")
procedure.publish_revision!
dossier.reload
end
it 'should be true' do
expect(dossier.pending_changes).not_to be_empty
expect(dossier.can_rebase?).to be_truthy
end
end
end
context 'en_instruction' do
let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) }
before do
procedure.publish!
procedure.reload
dossier
end
context 'with added type de champ' do
before do
procedure.draft_revision.add_type_de_champ({
type_champ: TypeDeChamp.type_champs.fetch(:text),
libelle: "Un champ text"
})
procedure.publish_revision!
dossier.reload
end
it 'should be false' do
expect(dossier.pending_changes).not_to be_empty
expect(dossier.can_rebase?).to be_falsey
end
end
context 'with removed type de champ' do
before do
procedure.draft_revision.remove_type_de_champ(type_de_champ.stable_id)
procedure.publish_revision!
dossier.reload
end
it 'should be false' do
expect(dossier.pending_changes).not_to be_empty
expect(dossier.can_rebase?).to be_falsey
end
end
context 'with attestation template changes' do
before do
attestation_template.update(title: "Test")
procedure.publish_revision!
dossier.reload
end
it 'should be true' do
expect(dossier.pending_changes).not_to be_empty
expect(dossier.can_rebase?).to be_truthy
end
end
context 'with type de champ made optional' do
before do
procedure.draft_revision.find_or_clone_type_de_champ(mandatory_type_de_champ.stable_id).update(mandatory: false)
procedure.publish_revision!
dossier.reload
end
it 'should be false' do
expect(dossier.pending_changes).not_to be_empty
expect(dossier.can_rebase?).to be_falsey
end
end
end
end
describe "#rebase" do
let(:procedure) { create(:procedure, :with_type_de_champ_mandatory, :with_yes_no, :with_repetition, :with_datetime) }
let(:dossier) { create(:dossier, procedure: procedure) }

View file

@ -825,13 +825,15 @@ describe Procedure do
context 'when the procedure has dossiers' do
let(:dossier_draft) { create(:dossier, :brouillon, procedure: procedure) }
let(:dossier_submitted) { create(:dossier, :en_construction, procedure: procedure) }
let(:dossier_termine) { create(:dossier, :accepte, procedure: procedure) }
before { [dossier_draft, dossier_submitted] }
before { [dossier_draft, dossier_submitted, dossier_termine] }
it 'enqueues rebase jobs for draft dossiers' do
subject
expect(DossierRebaseJob).to have_been_enqueued.with(dossier_draft)
expect(DossierRebaseJob).not_to have_been_enqueued.with(dossier_submitted)
expect(DossierRebaseJob).to have_been_enqueued.with(dossier_submitted)
expect(DossierRebaseJob).not_to have_been_enqueued.with(dossier_termine)
end
end
end

View file

@ -433,7 +433,7 @@ describe User, type: :model do
it 'transfers the dossier' do
subject
expect(targeted_user.dossiers.with_discarded).to match([dossier, hidden_dossier])
expect(targeted_user.dossiers.with_discarded).to contain_exactly(dossier, hidden_dossier)
expect(targeted_user.invites).to match([invite])
expect(targeted_user.merge_logs.first).to eq(merge_log)

176
yarn.lock
View file

@ -2442,6 +2442,11 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
"@types/minimist@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
"@types/node-fetch@^2.1.6":
version "2.5.12"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66"
@ -3224,6 +3229,11 @@ array.prototype.flatmap@^1.2.5:
define-properties "^1.1.3"
es-abstract "^1.19.0"
arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
arrify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
@ -3947,6 +3957,16 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
camelcase-keys@^7.0.0:
version "7.0.2"
resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-7.0.2.tgz#d048d8c69448745bb0de6fc4c1c52a30dfbe7252"
integrity sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==
dependencies:
camelcase "^6.3.0"
map-obj "^4.1.0"
quick-lru "^5.1.1"
type-fest "^1.2.1"
camelcase@^5.0.0, camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
@ -3957,6 +3977,11 @@ camelcase@^6.0.0, camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.1.tgz#250fd350cfd555d0d2160b1d51510eaf8326e86e"
integrity sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==
camelcase@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-api@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
@ -4943,11 +4968,24 @@ decache@^4.6.0:
dependencies:
callsite "^1.0.0"
decamelize@^1.2.0:
decamelize-keys@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
dependencies:
decamelize "^1.1.0"
map-obj "^1.0.0"
decamelize@^1.1.0, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
decamelize@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9"
integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@ -5105,6 +5143,14 @@ define-property@^2.0.2:
is-descriptor "^1.0.2"
isobject "^3.0.1"
del-cli@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/del-cli/-/del-cli-4.0.1.tgz#2303ccaa45708ee8c6211568344cf87336abf30a"
integrity sha512-KtR/6cBfZkGDAP2NA7z+bP4p1OMob3wjN9mq13+SWvExx6jT9gFWfLgXEeX8J2B47OKeNCq9yTONmtryQ+m+6g==
dependencies:
del "^6.0.0"
meow "^10.1.0"
del@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/del/-/del-5.1.0.tgz#d9487c94e367410e6eff2925ee58c0c84a75b3a7"
@ -6916,6 +6962,11 @@ har-validator@~5.1.3:
ajv "^6.12.3"
har-schema "^2.0.0"
hard-rejection@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
has-ansi@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e"
@ -7102,6 +7153,13 @@ hosted-git-info@^2.1.4:
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
hosted-git-info@^4.0.1:
version "4.1.0"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224"
integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==
dependencies:
lru-cache "^6.0.0"
hpack.js@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
@ -7340,6 +7398,11 @@ indent-string@^4.0.0:
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
indent-string@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5"
integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@ -7571,6 +7634,13 @@ is-core-module@^2.2.0:
dependencies:
has "^1.0.3"
is-core-module@^2.5.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
dependencies:
has "^1.0.3"
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@ -7752,7 +7822,7 @@ is-path-inside@^3.0.1, is-path-inside@^3.0.2:
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-plain-obj@^1.0.0:
is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
@ -8161,7 +8231,7 @@ kind-of@^5.0.0:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
kind-of@^6.0.0, kind-of@^6.0.2:
kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
@ -8553,6 +8623,11 @@ map-cache@^0.2.2:
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
map-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
map-obj@^4.0.0, map-obj@^4.1.0, map-obj@^4.2.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
@ -8681,6 +8756,24 @@ memory-fs@^0.5.0:
errno "^0.1.3"
readable-stream "^2.0.1"
meow@^10.1.0:
version "10.1.2"
resolved "https://registry.yarnpkg.com/meow/-/meow-10.1.2.tgz#62951cb69afa69594142c8250806bc30a3912e4d"
integrity sha512-zbuAlN+V/sXlbGchNS9WTWjUzeamwMt/BApKCJi7B0QyZstZaMx0n4Unll/fg0njGtMdC9UP5SAscvOCLYdM+Q==
dependencies:
"@types/minimist" "^1.2.2"
camelcase-keys "^7.0.0"
decamelize "^5.0.0"
decamelize-keys "^1.1.0"
hard-rejection "^2.1.0"
minimist-options "4.1.0"
normalize-package-data "^3.0.2"
read-pkg-up "^8.0.0"
redent "^4.0.0"
trim-newlines "^4.0.2"
type-fest "^1.2.2"
yargs-parser "^20.2.9"
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@ -8800,6 +8893,11 @@ mimic-response@^2.0.0, mimic-response@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
min-indent@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
mini-css-extract-plugin@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e"
@ -8827,6 +8925,15 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
minimist-options@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
dependencies:
arrify "^1.0.1"
is-plain-obj "^1.1.0"
kind-of "^6.0.3"
minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
@ -9361,6 +9468,16 @@ normalize-package-data@^2.5.0:
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
normalize-package-data@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e"
integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==
dependencies:
hosted-git-info "^4.0.1"
is-core-module "^2.5.0"
semver "^7.3.4"
validate-npm-package-license "^3.0.1"
normalize-path@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
@ -9957,7 +10074,7 @@ parse-json@^4.0.0:
error-ex "^1.3.1"
json-parse-better-errors "^1.0.1"
parse-json@^5.0.0:
parse-json@^5.0.0, parse-json@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
@ -11095,6 +11212,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
quick-lru@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
quickselect@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018"
@ -11241,6 +11363,15 @@ read-pkg-up@^7.0.1:
read-pkg "^5.2.0"
type-fest "^0.8.1"
read-pkg-up@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-8.0.0.tgz#72f595b65e66110f43b052dd9af4de6b10534670"
integrity sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==
dependencies:
find-up "^5.0.0"
read-pkg "^6.0.0"
type-fest "^1.0.1"
read-pkg@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
@ -11251,6 +11382,16 @@ read-pkg@^5.2.0:
parse-json "^5.0.0"
type-fest "^0.6.0"
read-pkg@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-6.0.0.tgz#a67a7d6a1c2b0c3cd6aa2ea521f40c458a4a504c"
integrity sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==
dependencies:
"@types/normalize-package-data" "^2.4.0"
normalize-package-data "^3.0.2"
parse-json "^5.2.0"
type-fest "^1.0.1"
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
@ -11296,6 +11437,14 @@ readdirp@^3.4.0, readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
redent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-4.0.0.tgz#0c0ba7caabb24257ab3bb7a4fd95dd1d5c5681f9"
integrity sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==
dependencies:
indent-string "^5.0.0"
strip-indent "^4.0.0"
regenerate-unicode-properties@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"
@ -12433,6 +12582,13 @@ strip-final-newline@^2.0.0:
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
strip-indent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-4.0.0.tgz#b41379433dd06f5eae805e21d631e07ee670d853"
integrity sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==
dependencies:
min-indent "^1.0.1"
strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
@ -12895,6 +13051,11 @@ traverse@~0.6.6:
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=
trim-newlines@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.0.2.tgz#d6aaaf6a0df1b4b536d183879a6b939489808c7c"
integrity sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew==
trim-repeated@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21"
@ -13018,6 +13179,11 @@ type-fest@^0.8.0, type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1"
integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==
type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@ -13772,7 +13938,7 @@ yargs-parser@^18.1.2:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^20.2.2:
yargs-parser@^20.2.2, yargs-parser@^20.2.9:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==