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