diff --git a/.circleci/config.yml b/.circleci/config.yml
index 331c4b4b4..b28ae8fe2 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -13,7 +13,10 @@ defaults: &defaults
bundle_restore_cache: &bundle_restore_cache
restore_cache:
name: Restore Bundler Package Cache
- key: bundle-install-v9-{{ arch }}-{{ checksum "Gemfile.lock" }}
+ keys:
+ - bundle-install-v9-{{ arch }}-{{ checksum "Gemfile.lock" }}
+ - bundle-install-v9-{{ arch }}
+ - bundle-install-v9
bundle_save_cache: &bundle_save_cache
save_cache:
@@ -30,19 +33,45 @@ bundle_install: &bundle_install
yarn_restore_cache: &yarn_restore_cache
restore_cache:
name: Restore Yarn Package Cache
- key: yarn-install-v1-{{ arch }}-{{ checksum "yarn.lock" }}
+ keys:
+ - yarn-install-v3-{{ arch }}-{{ checksum "yarn.lock" }}
+ - yarn-install-v3-{{ arch }}
+ - yarn-install-v3
yarn_save_cache: &yarn_save_cache
save_cache:
name: Save Yarn Package Cache
- key: yarn-install-v1-{{ arch }}-{{ checksum "yarn.lock" }}
+ key: yarn-install-v3-{{ arch }}-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
yarn_install: &yarn_install
run:
name: Install JS Dependencies
- command: yarn install --non-interactive || yarn install --non-interactive
+ command: yarn install --frozen-lockfile --non-interactive || yarn install --frozen-lockfile --non-interactive
+
+webpacker_restore_cache: &webpacker_restore_cache
+ restore_cache:
+ name: Restore Webpacker Cache
+ keys:
+ - webpacker-v1-{{ .Branch }}-{{ .Revision }}
+ - webpacker-v1-{{ .Branch }}
+ - webpacker-v1
+
+webpacker_save_cache: &webpacker_save_cache
+ save_cache:
+ name: Save Webpacker Cache
+ key: webpacker-v1-{{ .Branch }}-{{ .Revision }}
+ paths:
+ - public/packs-test
+ - tmp/cache/webpacker
+
+webpacker_precompile: &webpacker_precompile
+ run:
+ environment:
+ RAILS_ENV: test
+ name: Precompile Webpack assets
+ command: bin/webpack
jobs:
build:
@@ -53,8 +82,8 @@ jobs:
- *bundle_install
- *bundle_save_cache
- *yarn_restore_cache
- - *yarn_save_cache
- *yarn_install
+ - *yarn_save_cache
test:
<<: *defaults
parallelism: 3
@@ -64,16 +93,15 @@ jobs:
- *bundle_install
- *yarn_restore_cache
- *yarn_install
+ - *webpacker_restore_cache
+ - *webpacker_precompile
+ - *webpacker_save_cache
- run:
environment:
DATABASE_URL: "postgres://tps_test@localhost:5432/tps_test"
- name: Create DB
+ name: Create Database
command: bundle exec rake db:create db:schema:load db:migrate RAILS_ENV=test
- - run:
- environment:
- RAILS_ENV: test
- name: Precompile Webpack assets
- command: bin/webpack
+
- run:
environment:
DATABASE_URL: "postgres://tps_test@localhost:5432/tps_test"
diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml
index c23336d4d..229855fc6 100644
--- a/.github/workflows/rebase.yml
+++ b/.github/workflows/rebase.yml
@@ -1,7 +1,7 @@
on:
issue_comment:
types: [created]
-name: Rebase automatique
+name: Automatic Rebase
jobs:
rebase:
name: Rebase
@@ -9,13 +9,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
+ with:
+ fetch-depth: 0
- name: Automatic Rebase
- uses: cirrus-actions/rebase@master
+ uses: cirrus-actions/rebase@1.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# https://github.community/t5/GitHub-Actions/Workflow-is-failing-if-no-job-can-be-ran-due-to-condition/m-p/38186#M3250
always_job:
- name: Aways run job
+ name: Always run job
runs-on: ubuntu-latest
steps:
- name: Always run
diff --git a/Gemfile.lock b/Gemfile.lock
index c3d75309d..e72a25cae 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -117,7 +117,7 @@ GEM
railties (>= 3.0)
brakeman (4.3.1)
browser (2.5.3)
- builder (3.2.3)
+ builder (3.2.4)
byebug (10.0.2)
capybara (3.29.0)
addressable
@@ -155,7 +155,7 @@ GEM
connection_pool (2.2.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
- crass (1.0.5)
+ crass (1.0.6)
css_parser (1.6.0)
addressable
curb (0.9.10)
@@ -242,7 +242,7 @@ GEM
graphiql-rails (1.7.0)
railties
sprockets-rails
- graphql (1.9.15)
+ graphql (1.9.16)
graphql-batch (0.4.1)
graphql (>= 1.3, < 2)
promise.rb (~> 0.7.2)
@@ -305,7 +305,7 @@ GEM
domain_name (~> 0.5)
http_parser.rb (0.6.0)
httpclient (2.8.3)
- i18n (1.7.0)
+ i18n (1.8.2)
concurrent-ruby (~> 1.0)
ipaddress (0.8.3)
jaro_winkler (1.5.2)
@@ -350,7 +350,7 @@ GEM
railties (>= 4)
request_store (~> 1.0)
logstash-event (1.2.02)
- loofah (2.3.1)
+ loofah (2.4.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lumberjack (1.0.13)
@@ -369,7 +369,7 @@ GEM
mimemagic (0.3.3)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
- minitest (5.13.0)
+ minitest (5.14.0)
momentjs-rails (2.20.1)
railties (>= 3.1)
multi_json (1.14.1)
@@ -379,7 +379,7 @@ GEM
nenv (0.3.0)
netrc (0.11.0)
nio4r (2.5.2)
- nokogiri (1.10.5)
+ nokogiri (1.10.7)
mini_portile2 (~> 2.4.0)
notiffany (0.1.1)
nenv (~> 0.1)
@@ -450,7 +450,7 @@ GEM
puma (3.12.2)
pundit (2.0.1)
activesupport (>= 3.0.0)
- rack (2.0.8)
+ rack (2.1.2)
rack-attack (6.0.0)
rack (>= 1.0, < 3)
rack-mini-profiler (1.0.1)
@@ -528,27 +528,27 @@ GEM
builder (>= 3.0)
rubyzip (>= 1.0)
rouge (3.9.0)
- rspec (3.8.0)
- rspec-core (~> 3.8.0)
- rspec-expectations (~> 3.8.0)
- rspec-mocks (~> 3.8.0)
- rspec-core (3.8.0)
- rspec-support (~> 3.8.0)
- rspec-expectations (3.8.2)
+ rspec (3.9.0)
+ rspec-core (~> 3.9.0)
+ rspec-expectations (~> 3.9.0)
+ rspec-mocks (~> 3.9.0)
+ rspec-core (3.9.1)
+ rspec-support (~> 3.9.1)
+ rspec-expectations (3.9.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.8.0)
- rspec-mocks (3.8.0)
+ rspec-support (~> 3.9.0)
+ rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.8.0)
- rspec-rails (3.8.1)
+ rspec-support (~> 3.9.0)
+ rspec-rails (3.9.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
- rspec-core (~> 3.8.0)
- rspec-expectations (~> 3.8.0)
- rspec-mocks (~> 3.8.0)
- rspec-support (~> 3.8.0)
- rspec-support (3.8.0)
+ rspec-core (~> 3.9.0)
+ rspec-expectations (~> 3.9.0)
+ rspec-mocks (~> 3.9.0)
+ rspec-support (~> 3.9.0)
+ rspec-support (3.9.2)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (0.62.0)
@@ -614,9 +614,9 @@ GEM
rack (~> 2.0)
rack-protection (= 2.0.5)
tilt (~> 2.0)
- skylight (3.1.2)
- skylight-core (= 3.1.2)
- skylight-core (3.1.2)
+ skylight (4.2.1)
+ skylight-core (= 4.2.1)
+ skylight-core (4.2.1)
activesupport (>= 4.2.0)
smart_listing (1.2.2)
coffee-rails
@@ -644,7 +644,7 @@ GEM
httpclient (>= 2.4)
sysexits (1.2.0)
temple (0.8.0)
- thor (0.20.3)
+ thor (1.0.1)
thread_safe (0.3.6)
tilt (2.0.9)
timecop (0.9.1)
@@ -654,7 +654,7 @@ GEM
turbolinks-source (5.2.0)
typhoeus (1.3.1)
ethon (>= 0.9.0)
- tzinfo (1.2.5)
+ tzinfo (1.2.6)
thread_safe (~> 0.1)
unf (0.1.4)
unf_ext
diff --git a/app/assets/stylesheets/new_design/dossier_edit.scss b/app/assets/stylesheets/new_design/dossier_edit.scss
index 05096311e..4e5c4ca75 100644
--- a/app/assets/stylesheets/new_design/dossier_edit.scss
+++ b/app/assets/stylesheets/new_design/dossier_edit.scss
@@ -1,6 +1,8 @@
@import "colors";
@import "constants";
+$dossier-actions-bar-border-width: 1px;
+
.dossier-header {
.container {
padding-bottom: $default-padding;
@@ -46,29 +48,40 @@
border-radius: 4px;
}
- .send-dossier-actions-bar {
- // scss-lint:disable VendorPrefix
+ .dossier-edit-sticky-footer {
+ // scss-lint:disable VendorPrefix DuplicateProperty
+ position: fixed; // Fallback for IE 11, and other browser that don't support sticky
position: -webkit-sticky; // This is needed on Safari (tested on 12.1)
- // scss-lint:enable VendorPrefix
position: sticky;
+ // scss-lint:enable VendorPrefix DuplicateProperty
+
+ // IE 11 uses `position:fixed` – and thus needs an explicit width, content-box for better layout, etc.
+ width: 100%;
+ max-width: $page-width + 2 * $default-padding;
+ box-sizing: content-box;
+
bottom: 0;
- display: flex;
- flex-direction: row;
- align-items: center;
margin-top: $default-padding;
margin-left: -$default-padding;
margin-right: -$default-padding;
margin-bottom: 0;
- padding-top: 0;
- padding-bottom: $default-spacer;
- padding-right: $default-padding;
- padding-left: $default-padding;
+
+ padding-right: $default-padding - $dossier-actions-bar-border-width;
+ padding-left: $default-padding - $dossier-actions-bar-border-width;
+
background: #FFFFFF;
- border: 1px solid #CCCCCC;
+
+ border: $dossier-actions-bar-border-width solid #CCCCCC;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom: none;
+ }
+
+ .send-dossier-actions-bar {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
.button:not(:small) {
min-height: 38px;
@@ -81,13 +94,13 @@
}
// Normal layout
- @media (min-width: 500px) {
+ @media (min-width: 620px) {
padding-top: $default-spacer * 2;
padding-bottom: $default-spacer * 2;
}
// Compact layout
- @media (max-width: 500px) {
+ @media (max-width: 620px) {
padding-top: $default-spacer;
padding-bottom: $default-spacer;
}
diff --git a/app/controllers/manager/users_controller.rb b/app/controllers/manager/users_controller.rb
index 9e1a9fc54..9bdbb0449 100644
--- a/app/controllers/manager/users_controller.rb
+++ b/app/controllers/manager/users_controller.rb
@@ -1,5 +1,18 @@
module Manager
class UsersController < Manager::ApplicationController
+ def update
+ user = User.find(params[:id])
+ new_email = params[:user][:email]
+ user.skip_reconfirmation!
+ user.update(email: new_email)
+ if (user.valid?)
+ flash[:notice] = "L'email a été modifié en « #{new_email} » sans notification ni validation par email."
+ else
+ flash[:error] = "« #{new_email} » n'est pas une adresse valide."
+ end
+ redirect_to edit_manager_user_path(user)
+ end
+
def resend_confirmation_instructions
user = User.find(params[:id])
user.resend_confirmation_instructions
diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb
index 2ad8886c8..2a3762f86 100644
--- a/app/dashboards/user_dashboard.rb
+++ b/app/dashboards/user_dashboard.rb
@@ -41,7 +41,9 @@ class UserDashboard < Administrate::BaseDashboard
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
- FORM_ATTRIBUTES = [].freeze
+ FORM_ATTRIBUTES = [
+ :email
+ ].freeze
# Overwrite this method to customize how users are displayed
# across all pages of the admin dashboard.
diff --git a/app/graphql/api/v2/schema.rb b/app/graphql/api/v2/schema.rb
index 26089ef11..10b1bfa13 100644
--- a/app/graphql/api/v2/schema.rb
+++ b/app/graphql/api/v2/schema.rb
@@ -59,19 +59,29 @@ class Api::V2::Schema < GraphQL::Schema
raise GraphQL::ExecutionError.new("An object of type #{error.type.graphql_name} was hidden due to permissions", extensions: { code: :unauthorized })
end
- middleware(GraphQL::Schema::TimeoutMiddleware.new(max_seconds: 5) do |_, query|
- Rails.logger.info("GraphQL Timeout: #{query.query_string}")
- end)
+ use GraphQL::Execution::Interpreter
+ use GraphQL::Analysis::AST
+ use GraphQL::Schema::Timeout, max_seconds: 5
+ use GraphQL::Batch
+ use GraphQL::Backtrace
if Rails.env.development?
- query_analyzer(GraphQL::Analysis::QueryComplexity.new do |_, complexity|
- Rails.logger.info("[GraphQL Query Complexity] #{complexity}")
- end)
- query_analyzer(GraphQL::Analysis::QueryDepth.new do |_, depth|
- Rails.logger.info("[GraphQL Query Depth] #{depth}")
- end)
- end
+ class LogQueryDepth < GraphQL::Analysis::AST::QueryDepth
+ def result
+ Rails.logger.info("[GraphQL Query Depth] #{super}")
+ end
+ end
- use GraphQL::Batch
- use GraphQL::Tracing::SkylightTracing
+ class LogQueryComplexity < GraphQL::Analysis::AST::QueryComplexity
+ def result
+ Rails.logger.info("[GraphQL Query Complexity] #{super}")
+ end
+ end
+
+ query_analyzer(LogQueryComplexity)
+ query_analyzer(LogQueryDepth)
+ else
+ query_analyzer(GraphQL::Analysis::AST::MaxQueryComplexity)
+ query_analyzer(GraphQL::Analysis::AST::MaxQueryDepth)
+ end
end
diff --git a/app/models/avis.rb b/app/models/avis.rb
index 2c1051a65..26b9bbf5a 100644
--- a/app/models/avis.rb
+++ b/app/models/avis.rb
@@ -43,7 +43,9 @@ class Avis < ApplicationRecord
['Question / Introduction', :introduction],
['Réponse', :answer],
['Créé le', :created_at],
- ['Répondu le', :updated_at]
+ ['Répondu le', :updated_at],
+ ['Instructeur', claimant&.email],
+ ['Expert', instructeur&.email]
]
end
diff --git a/app/models/champs/linked_drop_down_list_champ.rb b/app/models/champs/linked_drop_down_list_champ.rb
index 46011439c..53e02d880 100644
--- a/app/models/champs/linked_drop_down_list_champ.rb
+++ b/app/models/champs/linked_drop_down_list_champ.rb
@@ -45,8 +45,9 @@ class Champs::LinkedDropDownListChamp < Champ
value.present? ? { primary: primary_value, secondary: secondary_value } : nil
end
- def mandatory_and_blank?
- mandatory? && (primary_value.blank? || secondary_value.blank?)
+ def blank?
+ primary_value.blank? ||
+ (has_secondary_options_for_primary? && secondary_value.blank?)
end
def search_terms
@@ -58,4 +59,8 @@ class Champs::LinkedDropDownListChamp < Champ
def pack_value(primary, secondary)
self.value = JSON.generate([primary, secondary])
end
+
+ def has_secondary_options_for_primary?
+ primary_value.present? && secondary_options[primary_value]&.any?(&:present?)
+ end
end
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index a7c237bbc..bc03925bc 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -216,6 +216,7 @@ class Dossier < ApplicationRecord
validates :user, presence: true
validates :individual, presence: true, if: -> { procedure.for_individual? }
+ validates :groupe_instructeur, presence: true
def update_search_terms
self.search_terms = [
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index 8697e3d5d..a2f6126fe 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -16,6 +16,7 @@ class Procedure < ApplicationRecord
has_one :attestation_template, dependent: :destroy
belongs_to :parent_procedure, class_name: 'Procedure'
+ belongs_to :canonical_procedure, class_name: 'Procedure'
belongs_to :service
has_many :administrateurs_procedures
@@ -129,9 +130,10 @@ class Procedure < ApplicationRecord
other_procedure = other_procedure_with_path(path)
if other_procedure.present? && administrateur.owns?(other_procedure)
other_procedure.unpublish!
+ publish!(other_procedure.canonical_procedure || other_procedure)
+ else
+ publish!
end
-
- publish!
end
end
@@ -615,8 +617,8 @@ class Procedure < ApplicationRecord
update!(closed_at: nil, unpublished_at: nil)
end
- def after_publish
- update!(published_at: Time.zone.now)
+ def after_publish(canonical_procedure = nil)
+ update!(published_at: Time.zone.now, canonical_procedure: canonical_procedure)
end
def after_close
diff --git a/app/views/manager/users/show.html.erb b/app/views/manager/users/show.html.erb
index ab51789a6..bf49d4076 100644
--- a/app/views/manager/users/show.html.erb
+++ b/app/views/manager/users/show.html.erb
@@ -24,9 +24,11 @@ as well as a link to its edit page.
<%= content_for(:title) %>
+
+ <%= button_to "modifier", edit_manager_user_path(page.resource), method: :get, class: "button" %>
+
<%= button_to "supprimer", delete_manager_user_path(page.resource), method: :delete, disabled: !page.resource.can_be_deleted?, class: "button", data: { confirm: "Confirmez-vous la suppression de l'utilisateur ?" }, title: page.resource.can_be_deleted? ? "Supprimer" : "Cet utilisateur a des dossiers dont l'instruction a commencé et ne peut être supprimé" %>
-
<% if !user.confirmed? %>
diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml
index b0b353520..bda7f5c05 100644
--- a/app/views/shared/dossiers/_edit.html.haml
+++ b/app/views/shared/dossiers/_edit.html.haml
@@ -30,11 +30,22 @@
%hr
- if dossier.procedure.routee?
- = f.label :groupe_instructeur_id, dossier.procedure.routing_criteria_name
+ = f.label :groupe_instructeur_id do
+ = dossier.procedure.routing_criteria_name
+ %span.mandatory *
+ -# The routing dropdown has 'include_blank: false', because otherwise a blank
+ -# value may nullify the groupe_instructeur – and thus the link between the dossier
+ -# and its procedure.
+ -#
+ -# If, one day, we need to make clearer to the user that they must actually choose an
+ -# option, THINK TWICE before adding a blank option, and what would happen if the form is
+ -# saved when the blank option is selected.
+ -# Instead please consider other possibilities; like using CSS to gray out the default option,
+ -# or adding some "(please select an option)" wording aside the label of the default group.
+ -# CSS
= f.select :groupe_instructeur_id,
dossier.procedure.groupe_instructeurs.order(:label).map { |gi| [gi.label, gi.id] },
- {},
- required: true
+ { include_blank: false }
= f.fields_for :champs, dossier.champs do |champ_form|
- champ = champ_form.object
@@ -42,31 +53,32 @@
locals: { champ: champ, form: champ_form }
- if !apercu
- .send-dossier-actions-bar
- - if dossier.brouillon?
- - if autosave_available?(dossier)
- = render partial: 'users/dossiers/autosave'
+ .dossier-edit-sticky-footer
+ .send-dossier-actions-bar
+ - if dossier.brouillon?
+ - if autosave_available?(dossier)
+ = render partial: 'users/dossiers/autosave'
+ - else
+ = f.button 'Enregistrer le brouillon',
+ formnovalidate: true,
+ class: 'button send secondary',
+ data: { 'disable-with': "Envoi en cours…" }
+
+ - if dossier.can_transition_to_en_construction?
+ = f.button 'Déposer le dossier',
+ name: :submit_draft,
+ value: true,
+ class: 'button send primary',
+ disabled: !current_user.owns?(dossier),
+ data: { 'disable-with': "Envoi en cours…" }
+
- else
- = f.button 'Enregistrer le brouillon',
- formnovalidate: true,
- class: 'button send secondary',
- data: { 'disable-with': "Envoi en cours…" }
-
- - if dossier.can_transition_to_en_construction?
- = f.button 'Déposer le dossier',
- name: :submit_draft,
- value: true,
+ = f.button 'Enregistrer les modifications du dossier',
class: 'button send primary',
- disabled: !current_user.owns?(dossier),
data: { 'disable-with': "Envoi en cours…" }
- - else
- = f.button 'Enregistrer les modifications du dossier',
- class: 'button send primary',
- data: { 'disable-with': "Envoi en cours…" }
+ - if dossier.brouillon? && !current_user.owns?(dossier)
+ .send-notice.invite-cannot-submit
+ En tant qu’invité, vous pouvez remplir ce formulaire – mais le titulaire du dossier doit le déposer lui-même.
- - if dossier.brouillon? && !current_user.owns?(dossier)
- .send-notice.invite-cannot-submit
- En tant qu’invité, vous pouvez remplir ce formulaire – mais le titulaire du dossier doit le déposer lui-même.
-
- = render partial: "shared/dossiers/submit_is_over", locals: { dossier: dossier }
+ = render partial: "shared/dossiers/submit_is_over", locals: { dossier: dossier }
diff --git a/config/application.rb b/config/application.rb
index 65080acaf..be10d163e 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -49,5 +49,7 @@ module TPS
debounce_delay: 3000,
status_visible_duration: 6000
}
+
+ config.skylight.probes += [:graphql]
end
end
diff --git a/config/initializers/browser.rb b/config/initializers/browser.rb
index 798e342ed..f9d22d40b 100644
--- a/config/initializers/browser.rb
+++ b/config/initializers/browser.rb
@@ -2,7 +2,6 @@
Browser.modern_rules.clear
Browser.modern_rules << -> b { b.chrome? && b.version.to_i >= 50 && !b.platform.ios? }
Browser.modern_rules << -> b { b.edge? && b.version.to_i >= 14 && !b.compatibility_view? }
-Browser.modern_rules << -> b { b.ie? && b.version.to_i >= 11 && !b.compatibility_view? }
Browser.modern_rules << -> b { b.firefox? && b.version.to_i >= 50 && !b.platform.ios? }
Browser.modern_rules << -> b { b.opera? && b.version.to_i >= 40 }
Browser.modern_rules << -> b { b.safari? && b.version.to_i >= 8 }
diff --git a/config/routes.rb b/config/routes.rb
index 09f0d8fb8..7feeaadb4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -24,7 +24,7 @@ Rails.application.routes.draw do
delete 'delete', on: :member
end
- resources :users, only: [:index, :show] do
+ resources :users, only: [:index, :show, :edit, :update] do
delete 'delete', on: :member
post 'resend_confirmation_instructions', on: :member
put 'enable_feature', on: :member
diff --git a/db/migrate/20200114113700_add_canonical_procedure_id_to_procedures.rb b/db/migrate/20200114113700_add_canonical_procedure_id_to_procedures.rb
new file mode 100644
index 000000000..a2ae37565
--- /dev/null
+++ b/db/migrate/20200114113700_add_canonical_procedure_id_to_procedures.rb
@@ -0,0 +1,5 @@
+class AddCanonicalProcedureIdToProcedures < ActiveRecord::Migration[5.2]
+ def change
+ add_column :procedures, :canonical_procedure_id, :bigint
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e7ad727d2..e998f6be4 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_12_18_103727) do
+ActiveRecord::Schema.define(version: 2020_01_14_113700) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -502,6 +502,7 @@ ActiveRecord::Schema.define(version: 2019_12_18_103727) do
t.boolean "ods_export_queued"
t.datetime "closed_at"
t.datetime "unpublished_at"
+ t.bigint "canonical_procedure_id"
t.index ["declarative_with_state"], name: "index_procedures_on_declarative_with_state"
t.index ["hidden_at"], name: "index_procedures_on_hidden_at"
t.index ["parent_procedure_id"], name: "index_procedures_on_parent_procedure_id"
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index dffb9d849..990c55961 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -21,6 +21,7 @@ describe ApplicationController, type: :controller do
let(:payload) { {} }
before do
+ allow(@controller).to receive(:content_type).and_return('')
allow(@controller).to receive(:current_user).and_return(current_user)
expect(@controller).to receive(:current_instructeur).and_return(current_instructeur)
expect(@controller).to receive(:current_administrateur).and_return(current_administrateur)
@@ -42,6 +43,8 @@ describe ApplicationController, type: :controller do
payload.delete(key)
end
expect(payload).to eq({
+ sk_rendered_format: nil,
+ sk_variant: [],
user_agent: 'Rails Testing',
user_roles: 'Guest'
})
@@ -61,6 +64,8 @@ describe ApplicationController, type: :controller do
payload.delete(key)
end
expect(payload).to eq({
+ sk_rendered_format: nil,
+ sk_variant: [],
user_agent: 'Rails Testing',
user_id: current_user.id,
user_email: current_user.email,
@@ -85,6 +90,8 @@ describe ApplicationController, type: :controller do
payload.delete(key)
end
expect(payload).to eq({
+ sk_rendered_format: nil,
+ sk_variant: [],
user_agent: 'Rails Testing',
user_id: current_user.id,
user_email: current_user.email,
diff --git a/spec/controllers/manager/application_controller_spec.rb b/spec/controllers/manager/application_controller_spec.rb
index 3af6744c7..45d8d995f 100644
--- a/spec/controllers/manager/application_controller_spec.rb
+++ b/spec/controllers/manager/application_controller_spec.rb
@@ -4,6 +4,7 @@ describe Manager::ApplicationController, type: :controller do
let(:payload) { {} }
before do
+ allow(@controller).to receive(:content_type).and_return('')
allow(@controller).to receive(:current_user).and_return(current_user)
@controller.send(:append_info_to_payload, payload)
end
@@ -13,6 +14,8 @@ describe Manager::ApplicationController, type: :controller do
payload.delete(key)
end
expect(payload).to eq({
+ sk_rendered_format: nil,
+ sk_variant: [],
user_agent: 'Rails Testing',
user_id: current_user.id,
user_email: current_user.email
diff --git a/spec/controllers/manager/users_controller_spec.rb b/spec/controllers/manager/users_controller_spec.rb
index 9a703a82d..4aacf54a1 100644
--- a/spec/controllers/manager/users_controller_spec.rb
+++ b/spec/controllers/manager/users_controller_spec.rb
@@ -1,6 +1,36 @@
describe Manager::UsersController, type: :controller do
let(:administration) { create(:administration) }
+ describe '#update' do
+ let!(:user) { create(:user, email: 'ancien.email@domaine.fr') }
+
+ before {
+ sign_in administration
+ }
+ subject { patch :update, params: { id: user.id, user: { email: nouvel_email } } }
+
+ describe 'with a valid email' do
+ let(:nouvel_email) { 'nouvel.email@domaine.fr' }
+
+ it 'updates the user email' do
+ subject
+
+ expect(User.find_by(id: user.id).email).to eq(nouvel_email)
+ end
+ end
+
+ describe 'with an invalid email' do
+ let(:nouvel_email) { 'plop' }
+
+ it 'does not update the user email' do
+ subject
+
+ expect(User.find_by(id: user.id).email).not_to eq(nouvel_email)
+ expect(flash[:error]).to match("« #{nouvel_email} » n'est pas une adresse valide.")
+ end
+ end
+ end
+
describe '#delete' do
let!(:user) { create(:user) }
diff --git a/spec/features/outdated_browser_spec.rb b/spec/features/outdated_browser_spec.rb
index 57fa4815a..e6cb28efb 100644
--- a/spec/features/outdated_browser_spec.rb
+++ b/spec/features/outdated_browser_spec.rb
@@ -3,29 +3,29 @@ require 'spec_helper'
feature 'Outdated browsers support:' do
context 'when the user browser is outdated' do
before(:each) do
- ie_10_user_agent = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)'
- Capybara.page.driver.header('user-agent', ie_10_user_agent)
+ ie_11_user_agent = 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko'
+ Capybara.page.driver.header('user-agent', ie_11_user_agent)
end
scenario 'a banner is displayed' do
visit new_user_session_path
- expect(page).to have_content('Internet Explorer 10 est trop ancien')
+ expect(page).to have_content('Internet Explorer 11 est trop ancien')
end
scenario 'the banner can be dismissed' do
visit new_user_session_path
- expect(page).to have_content('Internet Explorer 10 est trop ancien')
+ expect(page).to have_content('Internet Explorer 11 est trop ancien')
# The banner is hidden immediately
within '#outdated-browser-banner' do
click_on 'Ignorer'
end
- expect(page).not_to have_content('Internet Explorer 10 est trop ancien')
+ expect(page).not_to have_content('Internet Explorer 11 est trop ancien')
expect(page).to have_current_path(new_user_session_path)
# The banner is hidden after a refresh
page.refresh
- expect(page).not_to have_content('Internet Explorer 10 est trop ancien')
+ expect(page).not_to have_content('Internet Explorer 11 est trop ancien')
end
end
end
diff --git a/spec/models/champs/linked_drop_down_list_champ_spec.rb b/spec/models/champs/linked_drop_down_list_champ_spec.rb
index 64ec6d092..204015030 100644
--- a/spec/models/champs/linked_drop_down_list_champ_spec.rb
+++ b/spec/models/champs/linked_drop_down_list_champ_spec.rb
@@ -91,7 +91,14 @@ describe Champs::LinkedDropDownListChamp do
end
context 'when there is a secondary value' do
- before { subject.secondary_value = 'Primary' }
+ before { subject.secondary_value = 'Secondary' }
+
+ it { is_expected.not_to be_mandatory_and_blank }
+ end
+
+ context 'when there is nothing to select for the secondary value' do
+ let(:drop_down_list) { build(:drop_down_list, value: "--A--\nAbbott\nAbelard\n--B--\n--C--\nCynthia") }
+ before { subject.primary_value = 'B' }
it { is_expected.not_to be_mandatory_and_blank }
end
diff --git a/spec/models/exercice_spec.rb b/spec/models/exercice_spec.rb
index cf64c6cfc..9a906ed7a 100644
--- a/spec/models/exercice_spec.rb
+++ b/spec/models/exercice_spec.rb
@@ -1,4 +1,7 @@
require 'spec_helper'
describe Exercice do
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:ca) }
+ end
end
diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb
index 099dd91a6..36d654b99 100644
--- a/spec/models/procedure_spec.rb
+++ b/spec/models/procedure_spec.rb
@@ -534,41 +534,101 @@ describe Procedure do
let(:procedure) { create(:procedure, path: 'example-path') }
let(:now) { Time.zone.now.beginning_of_minute }
- after { Timecop.return }
-
- context "without parent procedure" do
+ context 'when publishing a new procedure' do
before do
- Timecop.freeze(now)
- procedure.publish!
+ Timecop.freeze(now) do
+ procedure.publish!
+ end
end
- it do
+ it 'no reference to the canonical procedure on the published procedure' do
+ expect(procedure.canonical_procedure).to be_nil
+ end
+
+ it 'changes the procedure state to published' do
expect(procedure.closed_at).to be_nil
expect(procedure.published_at).to eq(now)
expect(Procedure.find_by(path: "example-path")).to eq(procedure)
expect(Procedure.find_by(path: "example-path").administrateurs).to eq(procedure.administrateurs)
end
end
+
+ context 'when publishing over a previous canonical procedure' do
+ let(:canonical_procedure) { create(:procedure, :published) }
+
+ before do
+ Timecop.freeze(now) do
+ procedure.publish!(canonical_procedure)
+ end
+ end
+
+ it 'references the canonical procedure on the published procedure' do
+ expect(procedure.canonical_procedure).to eq(canonical_procedure)
+ end
+
+ it 'changes the procedure state to published' do
+ expect(procedure.closed_at).to be_nil
+ expect(procedure.published_at).to eq(now)
+ end
+ end
end
describe "#publish_or_reopen!" do
- let(:published_procedure) { create(:procedure, :published) }
- let(:administrateur) { published_procedure.administrateurs.first }
+ let(:canonical_procedure) { create(:procedure, :published) }
+ let(:administrateur) { canonical_procedure.administrateurs.first }
let(:procedure) { create(:procedure, administrateurs: [administrateur]) }
let(:now) { Time.zone.now.beginning_of_minute }
- context "without parent procedure" do
+ context 'when publishing over a previous canonical procedure' do
before do
- Timecop.freeze(now)
- procedure.path = published_procedure.path
- procedure.publish_or_reopen!(administrateur)
+ procedure.path = canonical_procedure.path
+ Timecop.freeze(now) do
+ procedure.publish_or_reopen!(administrateur)
+ end
+ canonical_procedure.reload
end
- it do
+ it 'references the canonical procedure on the published procedure' do
+ expect(procedure.canonical_procedure).to eq(canonical_procedure)
+ end
+
+ it 'changes the procedure state to published' do
expect(procedure.closed_at).to be_nil
expect(procedure.published_at).to eq(now)
end
+
+ it 'unpublishes the canonical procedure' do
+ expect(canonical_procedure.unpublished_at).to eq(now)
+ end
+ end
+
+ context 'when publishing over a previous procedure with canonical procedure' do
+ let(:canonical_procedure) { create(:procedure, :closed) }
+ let(:parent_procedure) { create(:procedure, :published, administrateurs: [administrateur]) }
+
+ before do
+ parent_procedure.update!(path: canonical_procedure.path, canonical_procedure: canonical_procedure)
+ procedure.path = canonical_procedure.path
+ Timecop.freeze(now) do
+ procedure.publish_or_reopen!(administrateur)
+ end
+ parent_procedure.reload
+ end
+
+ it 'references the canonical procedure on the published procedure' do
+ expect(procedure.canonical_procedure).to eq(canonical_procedure)
+ end
+
+ it 'changes the procedure state to published' do
+ expect(procedure.canonical_procedure).to eq(canonical_procedure)
+ expect(procedure.closed_at).to be_nil
+ expect(procedure.published_at).to eq(now)
+ end
+
+ it 'unpublishes parent procedure' do
+ expect(parent_procedure.unpublished_at).to eq(now)
+ end
end
end
@@ -577,10 +637,10 @@ describe Procedure do
let(:now) { Time.zone.now.beginning_of_minute }
before do
- Timecop.freeze(now)
- procedure.unpublish!
+ Timecop.freeze(now) do
+ procedure.unpublish!
+ end
end
- after { Timecop.return }
it {
expect(procedure.closed_at).to eq(nil)
@@ -653,11 +713,11 @@ describe Procedure do
let(:procedure) { create(:procedure, :published) }
let(:now) { Time.zone.now.beginning_of_minute }
before do
- Timecop.freeze(now)
- procedure.close!
+ Timecop.freeze(now) do
+ procedure.close!
+ end
procedure.reload
end
- after { Timecop.return }
it { expect(procedure.close?).to be_truthy }
it { expect(procedure.closed_at).to eq(now) }
diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb
index 7d24508c8..a3ba1a43d 100644
--- a/spec/services/procedure_export_service_spec.rb
+++ b/spec/services/procedure_export_service_spec.rb
@@ -295,7 +295,9 @@ describe ProcedureExportService do
"Question / Introduction",
"Réponse",
"Créé le",
- "Répondu le"
+ "Répondu le",
+ "Instructeur",
+ "Expert"
])
end