From b7b7a4643a77b5f3510c5e714cfe329b671f2c02 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 27 Feb 2024 15:54:33 +0000 Subject: [PATCH 01/95] Draft feedback question model field migration --- cms/migrate/01-create-page.js | 10 ----- cms/migrate/02-create-question.js | 57 ++++++++++++++++++++---- cms/migrate/03-create-video.js | 10 ----- cms/migrate/04-create-training-module.js | 7 +-- lib/tasks/cms.rake | 3 +- 5 files changed, 54 insertions(+), 33 deletions(-) diff --git a/cms/migrate/01-create-page.js b/cms/migrate/01-create-page.js index 64c4eda73..c73c24a6e 100644 --- a/cms/migrate/01-create-page.js +++ b/cms/migrate/01-create-page.js @@ -82,16 +82,6 @@ module.exports = function(migration) { helpText: 'All page content including sub-headings, bullet points and images.', }) - /* number */ - - page.changeFieldControl('submodule', 'builtin', 'numberEditor', { - helpText: 'Select the sub-module number the page belongs to, the second number of the page name.' - }) - - page.changeFieldControl('topic', 'builtin', 'numberEditor', { - helpText: 'Select the topic number the page belongs to, the third number in the page name.' - }) - /* toggle */ page.changeFieldControl('notes', 'builtin', 'boolean', { diff --git a/cms/migrate/02-create-question.js b/cms/migrate/02-create-question.js index bec12dac8..d46301012 100644 --- a/cms/migrate/02-create-question.js +++ b/cms/migrate/02-create-question.js @@ -1,9 +1,9 @@ module.exports = function(migration) { - const question = migration.createContentType('question', { - name: 'Question', + const question = migration.createContentType('question_test', { + name: 'Question test', displayField: 'name', - description: 'Formative, Summative or Confidence' + description: 'Formative, Summative, Confidence or Feedback' }) /* Fields ----------------------------------------------------------------- */ @@ -34,6 +34,7 @@ module.exports = function(migration) { 'formative', 'summative', 'confidence', + 'feedback', ] } ] @@ -51,6 +52,7 @@ module.exports = function(migration) { formative: customise both summative: customise failure only (success unused) confidence: both use default + feedback: both use default */ question.createField('success_message', { @@ -76,6 +78,44 @@ module.exports = function(migration) { type: 'Object', }) + /* Feedback Only ---------------------------------------------------------- */ + + question.createField('other', { + name: 'Other', + type: 'Text', + }) + + question.createField('or', { + name: 'Or', + type: 'Text', + }) + + question.createField('hint', { + name: 'Hint', + type: 'Text', + }) + + /* + overrides default + ====================== + formative and summative are dynamic based off number of correct options + confidence are hard-coded + feedback are more nuanced + */ + question.createField('response_type', { + name: 'Feedback response type', + type: 'Symbol', + }) + + question.createField('skippable', { + name: 'One-shot question', + type: 'Boolean', + required: true, + defaultValue: { + 'en-US': false + } + }) + /* Interface -------------------------------------------------------------- */ /* JSON */ @@ -98,14 +138,13 @@ module.exports = function(migration) { helpText: 'Displayed after "That’s not quite right" if the user selects the wrong answer.' }) - /* number */ + /* toggle */ - question.changeFieldControl('submodule', 'builtin', 'numberEditor', { - helpText: 'Select the sub-module number the page belongs to, the second number of the page name.' + question.changeFieldControl('skippable', 'builtin', 'boolean', { + helpText: 'Hide once answered?', + trueLabel: 'yes', + falseLabel: 'no' }) - question.changeFieldControl('topic', 'builtin', 'numberEditor', { - helpText: 'Select the topic number the page belongs to, the third number in the page name.' - }) } diff --git a/cms/migrate/03-create-video.js b/cms/migrate/03-create-video.js index 82b6ee1c7..53eac84dd 100644 --- a/cms/migrate/03-create-video.js +++ b/cms/migrate/03-create-video.js @@ -76,16 +76,6 @@ module.exports = function(migration) { helpText: 'Title of video.', }) - /* number */ - - video.changeFieldControl('submodule', 'builtin', 'numberEditor', { - helpText: 'Select the sub-module number the page belongs to, the second number of the page name.' - }) - - video.changeFieldControl('topic', 'builtin', 'numberEditor', { - helpText: 'Select the topic number the page belongs to, the third number in the page name.' - }) - /* markdown */ video.changeFieldControl('body', 'builtin', 'markdown', { diff --git a/cms/migrate/04-create-training-module.js b/cms/migrate/04-create-training-module.js index edc843ce4..57a6077e5 100644 --- a/cms/migrate/04-create-training-module.js +++ b/cms/migrate/04-create-training-module.js @@ -38,6 +38,7 @@ module.exports = function(migration) { required: true }) + // markdown not permitted trainingModule.createField('upcoming', { name: 'Upcoming', type: 'Text', @@ -50,7 +51,7 @@ module.exports = function(migration) { }) // markdown not permitted - trainingModule.createField('description', { // list moved to outcomes + trainingModule.createField('description', { name: 'Description', type: 'Text', required: true, @@ -61,7 +62,7 @@ module.exports = function(migration) { ] }) - trainingModule.createField('outcomes', { // list moved out of description + trainingModule.createField('outcomes', { name: 'Skills', type: 'Text', required: true @@ -74,7 +75,7 @@ module.exports = function(migration) { }) // markdown not permitted - trainingModule.createField('about', { // formerly objective + trainingModule.createField('about', { name: 'About', type: 'Text', required: true, diff --git a/lib/tasks/cms.rake b/lib/tasks/cms.rake index c50696f8e..ec4088b6a 100644 --- a/lib/tasks/cms.rake +++ b/lib/tasks/cms.rake @@ -18,7 +18,8 @@ namespace :eyfs do desc 'Define Contentful entry models' task migrate: :environment do - Dir[Rails.root.join('cms/migrate/*')].each do |file| + # Dir[Rails.root.join('cms/migrate/*')].each do |file| + Dir[Rails.root.join('cms/migrate/*question*')].each do |file| system <<~CMD contentful space migration \ --management-token #{ContentfulRails.configuration.management_token} \ From 67e04d31a9a29f07737e8cf2680d3809e535f35c Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Wed, 28 Feb 2024 09:55:35 +0000 Subject: [PATCH 02/95] Defend against fat controller anti-pattern The inbound site-wide feedback form attempt fails to reuse existing code and this will help the refactor once it is merged in. The Pagination module functionality relies on the "parent" having "pages". Course.config now functions like a Training::Module. --- app/models/course.rb | 13 ++++++++++++ app/models/training/module.rb | 2 ++ spec/models/course_spec.rb | 30 +++++++++++++++++++++++++--- spec/models/{ahoy => }/event_spec.rb | 0 spec/models/{ahoy => }/visit_spec.rb | 0 5 files changed, 42 insertions(+), 3 deletions(-) rename spec/models/{ahoy => }/event_spec.rb (100%) rename spec/models/{ahoy => }/visit_spec.rb (100%) diff --git a/app/models/course.rb b/app/models/course.rb index 66b9f81cc..fa101049a 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -22,4 +22,17 @@ def self.config def feedback super.to_a end + + # @return [Array] with parent + def pages + feedback.map do |question| + question.define_singleton_method(:parent) { Course.config } + question + end + end + + # @return [Training::Question] + def page_by_id(id) + pages.find { |page| page.id.eql?(id) } + end end diff --git a/app/models/training/module.rb b/app/models/training/module.rb index 955f6e0fb..06bb9f55d 100644 --- a/app/models/training/module.rb +++ b/app/models/training/module.rb @@ -114,6 +114,7 @@ def submodule_count # Selects from ordered array # + # @param id [String] # @return [Training::Page, Training::Video, Training::Question] def page_by_id(id) pages.find { |page| page.id.eql?(id) } @@ -121,6 +122,7 @@ def page_by_id(id) # Selects from ordered array # + # @param name [String] # @return [Training::Page, Training::Video, Training::Question] def page_by_name(name) pages.find { |page| page.name.eql?(name) } diff --git a/spec/models/course_spec.rb b/spec/models/course_spec.rb index 114624d05..127f287d5 100644 --- a/spec/models/course_spec.rb +++ b/spec/models/course_spec.rb @@ -17,9 +17,33 @@ end it 'feedback' do - expect(course.feedback).to be_empty - # - only one question type - # - number of questions + expect(course.feedback).not_to be_empty + expect(course.feedback.count).to be 5 + # expect(course.feedback.map(&:question_type)).to be 'feedback' + end + end + + # PoC to ensure exisiting parent#pages logic is reusable + # + describe 'site-wide feedback form navigation/pagination' do + let(:parent) { described_class.config } + let(:pages) { described_class.config.pages } + + it 'parent has pages' do + expect(parent.pages.first).to be_a Training::Question + expect(pages.first.parent.pages.last).to be_a Training::Question + end + + it 'pages have a parent' do + expect(pages.first.parent).to be_a described_class + expect(pages.first.parent).to eq pages.last.parent + end + + it 'page order uing previous_item/next_item' do + expect(pages.first.name).to eq 'main-feedback-1' + expect(pages.first.next_item.name).to eq 'main-feedback-2' + expect(pages.first.next_item.next_item.name).to eq 'main-feedback-3' + expect(pages.first.next_item.previous_item.name).to eq 'main-feedback-1' end end end diff --git a/spec/models/ahoy/event_spec.rb b/spec/models/event_spec.rb similarity index 100% rename from spec/models/ahoy/event_spec.rb rename to spec/models/event_spec.rb diff --git a/spec/models/ahoy/visit_spec.rb b/spec/models/visit_spec.rb similarity index 100% rename from spec/models/ahoy/visit_spec.rb rename to spec/models/visit_spec.rb From bc32ff0e1c46711162f9c3d23d094259afa020de Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:47:06 +0000 Subject: [PATCH 03/95] End of module feedback form and main feedback form (#1089) * wip * wip: Update to add opinion intro and opinion * wip: Update to include other text field * wip: Update for new question response type * wip: add text area feedback question type * wip: Update for end of module feedback journey and feedback intro buttons * wip: Update end of module feedback journey * wip: Update to hide Additional feedback in module overview * wip: Progress bar update to match designs * wip: Update to ensure error handling is shown when no option is selected * wip: Update to include optional show of hint text for feedback questions * wip: update to ensure feedback questions can be used across modules * wip: create feedback forms controller * ER-892 Assessment Results Banner styling update (#1011) * Update margin bottom for assessment result banner * Update to ensure only assessment banner styling is updated * Update margin bottom for assessment results banner following PR comment * Update to where styling is applied for assessment results banner * GovOne authentication integration changes (#1020) * WIP * tweak logout path logic * Existing session logic * Opt in WCAG logic * add check for session id token in logout link * QA debug * Avoid duplicate hits on about page * Sanity check * Fix memory issue for QA locally and in pipeline? * Trigger full WCAG check on demand if already deployed and includes changes * Factory and count * Test feature in pipeline * Integrate GovOne to replace existing sessions and skip deprecated specs * Update docs and fix specs * Increase coverage * Fix pa11y and plan to retire old devise functionality * Tidy * Delete old GPaaS debugging var --------- Co-authored-by: jack.coggin * Bump rubocop-govuk from 4.12.0 to 4.13.0 (#1025) * Bump rubocop-govuk from 4.12.0 to 4.13.0 Bumps [rubocop-govuk](https://github.com/alphagov/rubocop-govuk) from 4.12.0 to 4.13.0. - [Changelog](https://github.com/alphagov/rubocop-govuk/blob/main/CHANGELOG.md) - [Commits](https://github.com/alphagov/rubocop-govuk/compare/v4.12.0...v4.13.0) --- updated-dependencies: - dependency-name: rubocop-govuk dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Conform to new style guide --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Peter David Hamilton * ER-872 GovOne Banner (#1038) * Add banner regarding change to Gov One authentication * Update link to external content * Quick CMS resource debugger (#1040) * Quick CMS resource debugger * Improve coverage * LINTING!!! * Correct case * ER-899 Local Authority User active user update (#1041) * Update local authority user to not count closed accounts * Update closed trait to inherit from registered * Remove validation skipping for GOV.UK One Login user creation (#1031) * remove validation skipping for one login user creation * rubocop * move random password logic to own method * update t&c logic for one login enabled * rubocop * skip existing sign up spec if gov one login enabled * add one login enabled to check to specs * skip one login user creation in specs if gov one login not enabled * tweak user spec * update sessions controller spec * update user spec * add spec for terms and conditions form * add spec for role type form * update user spec * Exceed coverage threshold * fix locales typo * Improve course engagement test coverage and fix misnamed method --------- Co-authored-by: Peter David Hamilton * update gov.uk one login integration credentials (#1037) * update gov one integration credentials * Fix domain running in production locally --------- Co-authored-by: Peter David Hamilton * Always recreate container for background workers (#1045) * CMS powered site-wide config (#1044) * PoC for top-level config object defined in CMS * Course configuration * Spec and CMS migration * Fix invalid JS migration * Correct double escape in validation regexp * Force confirmation when destroying containers. (#1052) Use --yes. * Bump puma from 6.4.0 to 6.4.2 (#1039) Bumps [puma](https://github.com/puma/puma) from 6.4.0 to 6.4.2. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v6.4.0...v6.4.2) --- updated-dependencies: - dependency-name: puma dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump follow-redirects from 1.15.3 to 1.15.4 (#1043) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Er 832 Early years experience (#1048) * remove validation skipping for one login user creation * rubocop * move random password logic to own method * update t&c logic for one login enabled * rubocop * skip existing sign up spec if gov one login enabled * add one login enabled to check to specs * skip one login user creation in specs if gov one login not enabled * tweak user spec * update sessions controller spec * update user spec * add spec for terms and conditions form * add spec for role type form * update user spec * Exceed coverage threshold * fix locales typo * add early years experience registration step * update back links for registration steps * add early years experience to account page * add new registration journey events and update early years experience registration form * update yard annotation in ey experience form * update seeds snippets spec locales count * add tracking to all registration controllers * fix key typo * fix event tracking in registration controllers --------- Co-authored-by: Peter David Hamilton * Update Local Authority User to count using specific criteria and add new counts to user overview (#1053) * ER-893 Certificate design (#1051) - Adjust the design of the certificate page to include a thumbnail sample. - Adjust the typography of the certificate PDF with conditional styling. - Install additional fonts for use by Puppeteer and Chromium (further work required). * ER-898 CMS model evolution (#1018) * wip * Make assessment threshold value a dev concern and validate new field * Refactor module #card_title to #heading with coverage * Update effected spec * Update release docs and QA password * Post catch-up renaming * Migration correct names * Migration correct names more * Delete stubbed field used during transition * wip: navigate between main feedback form questions * wip * wip: add answer validation * wip: add validation * rubocop * add support for nil user responses * wip: allow user to update previous answers * add introduction and thank you pages * add additional text input for further details * wip * wip: Update to add opinion intro and opinion * wip: Update to include other text field * wip: Update for new question response type * wip: add text area feedback question type * wip: Update for end of module feedback journey and feedback intro buttons * wip: Update end of module feedback journey * wip: Update to hide Additional feedback in module overview * wip: Progress bar update to match designs * wip: Update to ensure error handling is shown when no option is selected * wip: Update to include optional show of hint text for feedback questions * wip: update to ensure feedback questions can be used across modules * wip: Update for responses table and saving text input questions * wip: Update one-off question logic and page count logic in module * wip: Copy across opinion radio button * update migrations * create guest struct * Update logic for link helper and update specs * Update specs and text input logic update in response model * Ensure CMS webhooks continue to work even whilst in maintenance mode (#1103) * fix course spec * Update ci workflow with correct feature flags and update specs --------- Signed-off-by: dependabot[bot] Co-authored-by: jack.coggin Co-authored-by: Peter Hamilton Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: jack-coggin <119428483+jack-coggin@users.noreply.github.com> --- .github/workflows/ci.yml | 16 +- app/controllers/application_controller.rb | 4 +- app/controllers/concerns/learning.rb | 2 +- app/controllers/feedback_controller.rb | 156 ++++++++++++++++++ app/controllers/training/pages_controller.rb | 4 +- .../training/questions_controller.rb | 9 +- .../training/responses_controller.rb | 17 +- app/decorators/module_overview_decorator.rb | 1 + app/decorators/next_page_decorator.rb | 11 +- app/decorators/pagination_decorator.rb | 18 +- app/forms/form_builder.rb | 10 ++ app/helpers/link_helper.rb | 34 +++- app/models/concerns/content_types.rb | 25 +++ app/models/concerns/pagination.rb | 4 +- app/models/guest.rb | 11 ++ app/models/response.rb | 16 +- app/models/training/content.rb | 5 + app/models/training/module.rb | 10 ++ app/models/training/question.rb | 32 +++- app/models/user.rb | 29 +++- app/views/feedback/index.html.slim | 17 ++ app/views/feedback/show.html.slim | 16 ++ app/views/feedback/thank_you.html.slim | 8 + app/views/training/modules/_section.html.slim | 5 +- app/views/training/modules/show.html.slim | 3 +- app/views/training/pages/_content.html.slim | 1 + .../training/pages/section_intro.html.slim | 1 + .../_main_feedback_check_boxes.html.slim | 15 ++ .../_main_feedback_radio_buttons.html.slim | 16 ++ .../_opinion_radio_buttons.html.slim | 13 ++ config/application.rb | 10 +- config/credentials/production.yml.enc | 2 +- config/locales/en.yml | 29 ++++ config/routes.rb | 6 + ...344_change_training_module_in_responses.rb | 5 + ...40207105459_add_text_input_to_responses.rb | 5 + db/schema.rb | 5 +- lib/content_test_schema.rb | 6 +- spec/config_spec.rb | 13 ++ spec/controllers/feedback_controller_spec.rb | 58 +++++++ .../training/responses_controller_spec.rb | 22 ++- spec/decorators/pagination_decorator_spec.rb | 20 +++ spec/helpers/link_helper_spec.rb | 33 ++++ spec/lib/seed_snippets_spec.rb | 2 +- spec/models/concerns/content_types_spec.rb | 14 ++ spec/models/course_spec.rb | 5 +- .../confidence_check_scores_spec.rb | 14 +- .../data_analysis/high_fail_questions_spec.rb | 28 ++-- spec/models/response_spec.rb | 1 + spec/models/training/module_spec.rb | 10 +- spec/models/training/question_spec.rb | 22 +++ spec/models/training/response_spec.rb | 12 ++ spec/support/ast/alpha-pass.yml | 5 + spec/support/shared/with_content.rb | 2 + spec/support/shared/with_progress.rb | 10 ++ spec/system/common_page_spec.rb | 8 + spec/system/event_log_spec.rb | 2 +- spec/system/module_overview_content_spec.rb | 4 + spec/system/module_overview_progress_spec.rb | 4 +- spec/system/opinion_spec.rb | 38 +++++ 60 files changed, 820 insertions(+), 84 deletions(-) create mode 100644 app/controllers/feedback_controller.rb create mode 100644 app/models/guest.rb create mode 100644 app/views/feedback/index.html.slim create mode 100644 app/views/feedback/show.html.slim create mode 100644 app/views/feedback/thank_you.html.slim create mode 100644 app/views/training/questions/_main_feedback_check_boxes.html.slim create mode 100644 app/views/training/questions/_main_feedback_radio_buttons.html.slim create mode 100644 app/views/training/questions/_opinion_radio_buttons.html.slim create mode 100644 db/migrate/20240131135344_change_training_module_in_responses.rb create mode 100644 db/migrate/20240207105459_add_text_input_to_responses.rb create mode 100644 spec/controllers/feedback_controller_spec.rb create mode 100644 spec/system/opinion_spec.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 615e9b8ed..b1fefb540 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,7 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/test DOMAIN: recovery.app BOT_TOKEN: bot_token + CONTENTFUL_PREVIEW: true # TODO: Reminder to delete this line services: postgres: @@ -77,21 +78,12 @@ jobs: - name: Compile assets run: bundle exec rails assets:precompile - - - name: Run test suite - run: bundle exec rspec - - - name: Run rubocop - run: bundle exec rubocop - # Answer migration feature test - name: Run test suite run: bundle exec rspec env: DISABLE_USER_ANSWER: true - # Gov One feature test - - - name: Run test suite - run: bundle exec rspec - env: GOV_ONE_LOGIN: true + - + name: Run rubocop + run: bundle exec rubocop diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 409d1be72..e6ca56aad 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -72,9 +72,9 @@ def timeout_timer private - # @return [Boolean] health check and landing page requests are exempt + # @return [Boolean] def maintenance? - return false if %w[/maintenance /health].include?(request.path) + return false if Rails.configuration.protected_endpoints.include?(request.path) Rails.application.maintenance? end diff --git a/app/controllers/concerns/learning.rb b/app/controllers/concerns/learning.rb index 77a623c4d..4df9a4f26 100644 --- a/app/controllers/concerns/learning.rb +++ b/app/controllers/concerns/learning.rb @@ -67,6 +67,6 @@ def section_bar # @note memoization ensures validation errors work # @return [UserAnswer, Response] def current_user_response - @current_user_response ||= current_user.response_for(content) + @current_user_response ||= content.opinion_question? ? current_user.response_for_shared(content, mod) : current_user.response_for(content) end end diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb new file mode 100644 index 000000000..54cf4478b --- /dev/null +++ b/app/controllers/feedback_controller.rb @@ -0,0 +1,156 @@ +class FeedbackController < ApplicationController + helper_method :previous_path, :next_path, :content, :is_checkbox?, :is_free_text?, :feedback_exists? + + # @return [nil] + def show + redirect_to next_path if skip_question? + end + + # @return [nil] + def index; end + + # @return [nil] + def update + return if invalid_answer? || other_blank? + + response_exists? ? update_response : create_response + redirect_to next_path + end + + # @return [String] + def is_checkbox? + content.response_type + end + + # @return [Boolean] + def is_free_text? + content.answers.empty? + end + + # @return [Boolean] + def feedback_exists? + return false if current_user.nil? + + Response.where(user_id: current_user.id).exists? + end + + # @return [String] path to next feedback step + def next_path + return my_modules_path if action_name == 'thank_you' + return feedback_path(1) if params[:id].nil? + return thank_you_path if params[:id].to_i == questions.count + + feedback_path(params[:id].to_i + 1) + end + + # @return [String] path to previous feedback step + def previous_path + return my_modules_path if params[:id].nil? + return feedback_path(1) if params[:id] == '1' + + feedback_path(params[:id].to_i - 1) + end + +private + + # @return [Boolean] + def invalid_answer? + if answer.blank? || answer.all?(&:blank?) + flash[:error] = 'Please answer the question' + redirect_to current_feedback_path and return true + else + false + end + end + + # @return [Boolean] + def other_blank? + if answer_content.include?('Other') && text_input.blank? + flash[:error] = 'Please specify' + redirect_to current_feedback_path and return true + else + false + end + end + + # @return [Response] + def create_response + Response.create!( + user_id: current_user ? current_user.id : nil, + answers: answer_content, + question_name: content.name, + text_input: text_input, + ) + end + + # @return [Response] + def update_response + Response.where(user_id: current_user.id, question_name: content.name).update( + answers: answer_content, + text_input: text_input, + ) + end + + # @return [Boolean] + def response_exists? + return false if current_user.nil? + + Response.where(user_id: current_user.id, question_name: content.name).exists? + end + + # @return [Boolean] + def skip_question? + return false unless skipped_questions.include?(content.name) + + true if current_user.nil? + end + + # @return [Array] + def skipped_questions + %w[main-feedback-6] + end + + # @param answer [String] + # @return [String] + def answer_wording(answer) + content.answers[answer.to_i - 1].first + end + + # @return [Array] + def questions + Course.config.feedback + end + + # @return [String] + def current_feedback_path + feedback_path(params[:id]) + end + + # @return [Hash] + def content + @content ||= questions[params[:id].to_i - 1] + end + + # @return [Array] + def answer + @answer ||= if is_free_text? + params[:answers] + else + Array.wrap(params[:answers]) + end + end + + # @return [Array] + def answer_content + @answer_content ||= begin + return [] if answer.blank? + return answer if is_free_text? + + answer.reject(&:blank?).map { |a| answer_wording(a) }.flatten + end + end + + def text_input + @text_input ||= params[:answers_custom] + end +end diff --git a/app/controllers/training/pages_controller.rb b/app/controllers/training/pages_controller.rb index a01dbab70..5a4b1b5ea 100644 --- a/app/controllers/training/pages_controller.rb +++ b/app/controllers/training/pages_controller.rb @@ -19,7 +19,7 @@ def index end def show - if content.is_question? + if content.is_question? || content.opinion_question? redirect_to training_module_question_path(mod.name, content.name) elsif content.assessment_results? redirect_to training_module_assessment_path(mod.name, content.name) @@ -38,7 +38,7 @@ def note end def render_page - if content.section? && !content.certificate? + if content.section? && !content.certificate? && !content.opinion_intro? render 'section_intro' else render content.page_type diff --git a/app/controllers/training/questions_controller.rb b/app/controllers/training/questions_controller.rb index c03641773..7dd0f8161 100644 --- a/app/controllers/training/questions_controller.rb +++ b/app/controllers/training/questions_controller.rb @@ -30,7 +30,9 @@ def show; end # @see Tracking # @return [Event] Show action def track_events - if track_confidence_start? + if track_feedback_start? + track('feedback_start') + elsif track_confidence_start? track('confidence_check_start') elsif track_assessment_start? track('summative_assessment_start') @@ -50,6 +52,11 @@ def track_confidence_start? content.first_confidence? && confidence_start_untracked? end + # @return [Boolean] + def track_feedback_start? + content.opinion_question? || content.opinion_intro? + end + # @return [Boolean] def track_assessment_start? content.first_assessment? && summative_start_untracked? diff --git a/app/controllers/training/responses_controller.rb b/app/controllers/training/responses_controller.rb index c24a6c88b..aba982842 100644 --- a/app/controllers/training/responses_controller.rb +++ b/app/controllers/training/responses_controller.rb @@ -17,10 +17,14 @@ class ResponsesController < ApplicationController layout 'hero' def update - if save_response! + if save_response! || (content.opinion_question? && content.options.blank?) track_question_answer redirect else + if content.opinion_question? && user_answer_text.blank? && content.options.present? + current_user_response.errors.clear + current_user_response.errors.add :answers, :invalid + end render 'training/questions/show', status: :unprocessable_entity end end @@ -41,10 +45,10 @@ def response_params # @note migrate from user_answer to response # @return [Boolean] def save_response! - correct_answers = content.confidence_question? ? true : content.correct_answers.eql?(user_answers) + correct_answers = content.confidence_question? || content.opinion_question? ? true : content.correct_answers.eql?(user_answers) if Rails.application.migrated_answers? - current_user_response.update(answers: user_answers, correct: correct_answers) + current_user_response.update(answers: user_answers, correct: correct_answers, text_input: user_answer_text) else current_user_response.update(answer: user_answers, correct: correct_answers) end @@ -55,6 +59,10 @@ def user_answers Array(response_params[:answers]).compact_blank.map(&:to_i) end + def user_answer_text + response_params[:text_input] + end + def redirect assessment.grade! if content.last_assessment? @@ -73,7 +81,8 @@ def track_question_answer mod_uid: mod.id, type: content.question_type, success: current_user_response.correct?, - answers: current_user_response.answers) + answers: current_user_response.answers, + text_input: user_answer_text) else track('questionnaire_answer', uid: content.id, diff --git a/app/decorators/module_overview_decorator.rb b/app/decorators/module_overview_decorator.rb index f196c0375..a60682bd5 100644 --- a/app/decorators/module_overview_decorator.rb +++ b/app/decorators/module_overview_decorator.rb @@ -42,6 +42,7 @@ def sections display_line: position != mod.submodule_count, icon: status(content_items), subsections: subsections(submodule: submodule, items: content_items), + hide: content_items.first.opinion_intro?, } end end diff --git a/app/decorators/next_page_decorator.rb b/app/decorators/next_page_decorator.rb index 3076bb1e8..edeaf4b9c 100644 --- a/app/decorators/next_page_decorator.rb +++ b/app/decorators/next_page_decorator.rb @@ -34,7 +34,8 @@ def text case when next? then label[:next] when missing? then label[:missing] - when content.section? then label[:section] + when content_section? then label[:section] + when opinion_intro? then label[:give_feedback] # Make confidence outro when test_start? then label[:start_test] when test_finish? then label[:finish_test] when finish? then label[:finish] @@ -99,6 +100,14 @@ def missing? content.next_item.eql?(content) && wip? end + def opinion_intro? + content.opinion_intro? + end + + def content_section? + content.section? && !content.opinion_intro? + end + # @return [Boolean] def wip? Rails.application.preview? || Rails.env.test? diff --git a/app/decorators/pagination_decorator.rb b/app/decorators/pagination_decorator.rb index 1960fac0f..c386cbce6 100644 --- a/app/decorators/pagination_decorator.rb +++ b/app/decorators/pagination_decorator.rb @@ -15,7 +15,11 @@ def heading # @return [String] def section_numbers - I18n.t(:section, scope: :pagination, current: content.submodule, total: section_total) + if content.opinion_intro? || content.opinion_question? + I18n.t(:section, scope: :pagination, current: content.submodule - 1, total: section_total - 1) + else + I18n.t(:section, scope: :pagination, current: content.submodule, total: section_total - 1) + end end # @return [String] @@ -37,7 +41,17 @@ def current_page # @return [Integer] def page_total - content.section_content.size + size = content.section_content.size + if content.section_content.any?(&:skippable?) # && response_for_shared.responded? + # don't count skipped page + content.section_content.each do |section_content| + if section_content.opinion_question? && section_content.always_show_question.eql?(false) + size -= 1 + end + end + end + + size end # @return [Integer] diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index 45cd9f10e..6df81b0b3 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -31,6 +31,16 @@ def question_radio_button(option) checked: option.checked? end + # @param option [Training::Answer::Option] + def opinion_radio_button(option) + govuk_radio_button :answers, + option.id, + label: { text: option.label }, + link_errors: true, + disabled: option.disabled?, + checked: option.checked? + end + # @param option [Training::Answer::Option] def question_check_box(option) govuk_check_box :answers, diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 127fc1903..11b2a7bae 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -32,6 +32,11 @@ def link_to_action [text, path] end + # @return [Boolean] + def one_off_question? + always_show_question.eql?(false) + end + # @return [String] next page (ends on certificate) def link_to_next govuk_button_link_to next_page.text, training_module_page_path(mod.name, next_page.name), @@ -41,18 +46,30 @@ def link_to_next # @return [String] previous page or module overview def link_to_previous + previous_content = content.previous_item path = if content.interruption_page? training_module_path(mod.name) else - training_module_page_path(mod.name, content.previous_item.name) + training_module_page_path(mod.name, previous_content.name) end - style = content.section? ? 'section-intro-previous-button' : 'govuk-button--secondary' + if !content.interruption_page? && content.previous_item.skippable? && current_user.response_for_shared(content.previous_item, mod).responded? + path = training_module_page_path(mod.name, previous_content.previous_item.name) + end - govuk_button_link_to 'Previous', path, - class: style, - aria: { label: t('pagination.previous') } + style = content.section? && !content.opinion_intro? ? 'section-intro-previous-button' : 'govuk-button--secondary' + + # Check if feedback questions have been skipped + if content.thankyou? && !current_user.response_for_shared(content.previous_item, mod).responded? + govuk_button_link_to 'Previous', training_module_page_path(mod.name, mod.opinion_intro_page.name), + class: style, + aria: { label: t('pagination.previous') } + else + govuk_button_link_to 'Previous', path, + class: style, + aria: { label: t('pagination.previous') } + end end # Bottom of my-modules card component @@ -73,6 +90,13 @@ def link_to_retake_or_results(mod) end end + # @return [String, nil] thank you page (skips feedback questions) + def link_to_skip + return unless content.opinion_intro? + + govuk_link_to 'Skip feedback', training_module_page_path(mod.name, mod.thankyou_page.name) + end + # @return [NextPageDecorator] def next_page NextPageDecorator.new( diff --git a/app/models/concerns/content_types.rb b/app/models/concerns/content_types.rb index b028ee1bd..e8d3c482d 100644 --- a/app/models/concerns/content_types.rb +++ b/app/models/concerns/content_types.rb @@ -6,6 +6,11 @@ def interruption_page? page_type.eql?('interruption_page') end + # @return [Boolean] + def feedback_page? + page_type.eql?('feedback') + end + # ============================================================================ # TRAINING SECTIONS # ============================================================================ @@ -25,6 +30,11 @@ def is_question? page_type.match?(/question/) end + # @return [Boolean] + def is_opinion? + page_type.match?(/opinion/) + end + # @return [Boolean] def text_page? page_type.eql?('text_page') @@ -79,6 +89,21 @@ def confidence_question? page_type.eql?('confidence_questionnaire') end + # @return [Boolean] + def opinion_intro? + page_type.eql?('opinion_intro') + end + + # @return [Boolean] + def one_off_question? + always_show_question.eql?(false) + end + + # @return [Boolean] + def opinion_question? + page_type.eql?('opinion') + end + # @return [Boolean] def thankyou? page_type.eql?('thankyou') diff --git a/app/models/concerns/pagination.rb b/app/models/concerns/pagination.rb index 794ca82f8..3387d236c 100644 --- a/app/models/concerns/pagination.rb +++ b/app/models/concerns/pagination.rb @@ -3,7 +3,7 @@ module Pagination # @return [Boolean] def section? - submodule_intro? || summary_intro? || certificate? + submodule_intro? || summary_intro? || opinion_intro? || certificate? end # @return [Boolean] @@ -34,7 +34,7 @@ def next_item # @return [String] def previous_item_id - parent.pages[content_index - 1].id + content_index.zero? ? parent.pages[content_index].id : parent.pages[content_index - 1].id end # @return [String, nil] diff --git a/app/models/guest.rb b/app/models/guest.rb new file mode 100644 index 000000000..4efaf6c1a --- /dev/null +++ b/app/models/guest.rb @@ -0,0 +1,11 @@ +class Guest < Dry::Struct + # @return [Boolean] + def guest? + true + end + + # @return [String] + def id + current_visit.visitor_id + end +end diff --git a/app/models/response.rb b/app/models/response.rb index f902d7990..ec24dc9fe 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -4,10 +4,11 @@ class Response < ApplicationRecord include ToCsv - belongs_to :user + belongs_to :user, optional: true belongs_to :assessment, optional: true - validates :answers, presence: true + validates :answers, presence: true, unless: -> { free_text_answer? } + validates :text_input, presence: true, if: -> { free_text_answer? } scope :incorrect, -> { where(correct: false) } scope :correct, -> { where(correct: true) } @@ -34,7 +35,9 @@ def question # @return [Array] def options - if question.formative_question? || assessment&.graded? + if question.confidence_question? || question.opinion_question? + question.options(checked: answers) + elsif question.formative_question? || assessment&.graded? question.options(checked: answers, disabled: responded?) else question.options(checked: answers) @@ -48,7 +51,7 @@ def archived? # @return [Boolean] def responded? - answers.any? + answers.any? || text_input.present? end # @return [Boolean] @@ -61,6 +64,11 @@ def revised? correct && !correct? end + # @return [Boolean] + def free_text_answer? + question.free_text? && text_input.present? unless training_module.nil? + end + ######################## # Decorators # ######################## diff --git a/app/models/training/content.rb b/app/models/training/content.rb index bf6a2701d..9b7db1f63 100644 --- a/app/models/training/content.rb +++ b/app/models/training/content.rb @@ -16,6 +16,11 @@ def parent @parent ||= Training::Module.by_content_id(id) end + # @return [Boolean] + def skippable? + false + end + # @return [String] def debug_summary <<~SUMMARY diff --git a/app/models/training/module.rb b/app/models/training/module.rb index 06bb9f55d..3f118927f 100644 --- a/app/models/training/module.rb +++ b/app/models/training/module.rb @@ -198,6 +198,11 @@ def confidence_intro_page content.find(&:confidence_intro?) end + # @return [Training::Page] + def opinion_intro_page + content.find(&:opinion_intro?) + end + # @return [Training::Page] def thankyou_page content.find(&:thankyou?) @@ -240,6 +245,11 @@ def confidence_questions content.select(&:confidence_question?) end + # @return [Array] + def opinion_questions + content.select(&:opinion_question?) + end + # @param text [String] # @return [Array] def answers_with(text) diff --git a/app/models/training/question.rb b/app/models/training/question.rb index bf7e0173d..b01449da6 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -14,8 +14,15 @@ def answer @answer ||= Answer.new(json: json) end + # @return [Boolean] + def skippable? + !always_show_question + end + # @return [String] powered by JSON not type def to_partial_path + return 'training/questions/opinion_radio_buttons' if opinion_question? + partial = multi_select? ? 'check_boxes' : 'radio_buttons' partial = "learning_#{partial}" if formative_question? "training/questions/#{partial}" @@ -23,7 +30,16 @@ def to_partial_path # @return [Boolean] def multi_select? - confidence_question? ? false : answer.multi_select? + confidence_question? || opinion_question? ? false : answer.multi_select? + end + + def opinion_question? + page_type == 'opinion' + end + + # @return [Boolean] feedback free text + def free_text? + opinion_question? && options.empty? end # @return [Boolean] event tracking @@ -41,6 +57,16 @@ def last_assessment? parent.summative_questions.last.eql?(self) end + # @return [Boolean] event tracking + def first_feedback? + parent.opinion_questions.first.eql?(self) + end + + # @return [Boolean] event tracking + def last_feedback? + parent.opinion_questions.last.eql?(self) + end + # @return [Boolean] def true_false? return false if multi_select? @@ -55,6 +81,7 @@ def assessments_type formative_questionnaire: 'formative_assessment', summative_questionnaire: 'summative_assessment', confidence_questionnaire: 'confidence_check', + opinion: 'opinion', }.fetch(page_type.to_sym) end @@ -67,6 +94,7 @@ def question_type summative_questionnaire: 'summative', summative: 'summative', confidence_questionnaire: 'confidence', + opinion: 'opinion', confidence: 'confidence', }.fetch(page_type.to_sym) end @@ -97,6 +125,8 @@ def legend #{body} LEGEND + elsif opinion_question? + body.to_s else "#{body} (Select one answer)" end diff --git a/app/models/user.rb b/app/models/user.rb index e3ce9df3b..a371835fc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -191,6 +191,16 @@ def update_with_password(params) super end + # @see ResponsesController#response_params + # @param content [Training::Question] + # @return [UserAnswer, Response] + def response_for_shared(content, mod) + responses.find_or_initialize_by( + question_name: content.name, + training_module: mod.name, + ) + end + # @see ResponsesController#response_params # @param content [Training::Question] # @return [UserAnswer, Response] @@ -204,12 +214,19 @@ def response_for(content) assessments.create(training_module: content.parent.name, started_at: Time.zone.now) end - responses.find_or_initialize_by( - assessment_id: assessment&.id, - training_module: content.parent.name, - question_name: content.name, - question_type: content.question_type, # TODO: RENAME options for Question#page_type removing "questionnaire" suffix - ) + if content.opinion_question? + responses.find_or_initialize_by( + question_name: content.name, + training_module: module_name, + ) + else + responses.find_or_initialize_by( + assessment_id: assessment&.id, + training_module: content.parent.name, + question_name: content.name, + question_type: content.question_type, # TODO: RENAME options for Question#page_type removing "questionnaire" suffix + ) + end else user_answers.find_or_initialize_by( assessments_type: content.assessments_type, diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim new file mode 100644 index 000000000..d90b0d76a --- /dev/null +++ b/app/views/feedback/index.html.slim @@ -0,0 +1,17 @@ +.govuk-grid-row + .govuk-grid-column-full + - if feedback_exists? + h1 t('feedback.feedback_exists.heading') + .govuk-button-group + .govuk-button-group + = govuk_button_link_to t('feedback.feedback_exists.back_button'), previous_path, class: 'govuk-button--secondary' + = govuk_button_link_to t('feedback.feedback_exists.next_button'), next_path + - else + h1 = t('feedback.heading') + p = m('feedback.body') + h2 = t('feedback.technical_support.heading') + p.text-secondary = t('feedback.technical_support.body') + + hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' + + = govuk_button_link_to t('feedback.next_button'), next_path \ No newline at end of file diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim new file mode 100644 index 000000000..a6ea892dc --- /dev/null +++ b/app/views/feedback/show.html.slim @@ -0,0 +1,16 @@ += form_with url: feedback_path, method: :patch do |f| + .govuk-grid-row + .govuk-grid-column-two-thirds + + hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' + + h1.govuk-heading-xl class='govuk-!-margin-bottom-4' + + - if !is_checkbox? + = render 'training/questions/main_feedback_radio_buttons', f: f, response: content, required: true + - else + = render 'training/questions/main_feedback_check_boxes', f: f, response: content, required: true + + .govuk-button-group + = govuk_button_link_to 'Previous', previous_path, class: 'govuk-button--secondary' + = f.submit 'Next', class: 'govuk-button' \ No newline at end of file diff --git a/app/views/feedback/thank_you.html.slim b/app/views/feedback/thank_you.html.slim new file mode 100644 index 000000000..d247b8a58 --- /dev/null +++ b/app/views/feedback/thank_you.html.slim @@ -0,0 +1,8 @@ +.govuk-grid-row + .govuk-grid-column-full + h1 = t('feedback.thank_you.heading') + p = m('feedback.thank_you.body') + + hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' + + = govuk_button_link_to t('feedback.thank_you.next_button'), next_path \ No newline at end of file diff --git a/app/views/training/modules/_section.html.slim b/app/views/training/modules/_section.html.slim index 7585100fa..6205c017f 100644 --- a/app/views/training/modules/_section.html.slim +++ b/app/views/training/modules/_section.html.slim @@ -1,7 +1,10 @@ section.module-overview--section .progress-bar span.icon class=icon - span.number = position + - if position == 5 + span.number = 4 + - else + span.number = position - if display_line .line diff --git a/app/views/training/modules/show.html.slim b/app/views/training/modules/show.html.slim index a197c4c47..628921f1e 100644 --- a/app/views/training/modules/show.html.slim +++ b/app/views/training/modules/show.html.slim @@ -31,7 +31,8 @@ hr.govuk-section-break.govuk-section-break--m.govuk-section-break--visible.module-overview-section-break - module_progress.sections.each do |section| - = render 'section', **section + - unless section[:hide] + = render 'section', **section = govuk_button_link_to link_to_action[1], class: 'govuk-button--start', id: 'module-call-to-action' do | #{link_to_action[0]} diff --git a/app/views/training/pages/_content.html.slim b/app/views/training/pages/_content.html.slim index 8b4ee79a0..c6b1b26f6 100644 --- a/app/views/training/pages/_content.html.slim +++ b/app/views/training/pages/_content.html.slim @@ -11,3 +11,4 @@ .govuk-button-group = link_to_previous = link_to_next + = link_to_skip diff --git a/app/views/training/pages/section_intro.html.slim b/app/views/training/pages/section_intro.html.slim index e890eabb1..3487fcadd 100644 --- a/app/views/training/pages/section_intro.html.slim +++ b/app/views/training/pages/section_intro.html.slim @@ -18,3 +18,4 @@ .govuk-button-group = link_to_previous = link_to_next + = link_to_skip diff --git a/app/views/training/questions/_main_feedback_check_boxes.html.slim b/app/views/training/questions/_main_feedback_check_boxes.html.slim new file mode 100644 index 000000000..4345f6404 --- /dev/null +++ b/app/views/training/questions/_main_feedback_check_boxes.html.slim @@ -0,0 +1,15 @@ += f.govuk_check_boxes_fieldset :answers, multiple: true, legend: { text: response.legend } do + = m(content.hint) unless content.hint.nil? + + = f.hidden_field :answers, multiple: true + - if response.options.any? + - response.options.each.with_index(1) do |option, index| + - if response.options.count.eql?(index) && content.other.present? + = f.govuk_check_box :answers, option.id, label: { text: 'Other' }, link_errors: true do + = f.govuk_text_field :answers_custom, label: { text: content.other } + - elsif response.options.count.eql?(index) && content.or.present? + .govuk-checkboxes__divider + = 'Or' + = f.govuk_check_box :answers, 'Or', label: { text: content.or }, link_errors: true + - else + = f.question_check_box(option) \ No newline at end of file diff --git a/app/views/training/questions/_main_feedback_radio_buttons.html.slim b/app/views/training/questions/_main_feedback_radio_buttons.html.slim new file mode 100644 index 000000000..e5f3309c2 --- /dev/null +++ b/app/views/training/questions/_main_feedback_radio_buttons.html.slim @@ -0,0 +1,16 @@ += f.govuk_radio_buttons_fieldset :answers, multiple: true, legend: { text: response.legend } do + + = f.hidden_field :answers, multiple: true + - if response.options.any? + - response.options.each.with_index(1) do |option, index| + - if response.options.count.eql?(index) && content.other.present? + = f.govuk_radio_button :answers, option.id, label: { text: 'Other' }, link_errors: true do + = f.govuk_text_field :answers_custom, label: { text: content.other } + - else + = f.opinion_radio_button(option) + - if !content.hint.nil? + = f.govuk_text_area :answers_custom, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } + - else + = m(content.hint) unless content.hint.nil? + = f.govuk_text_area :answers, label: nil, multiple: true + \ No newline at end of file diff --git a/app/views/training/questions/_opinion_radio_buttons.html.slim b/app/views/training/questions/_opinion_radio_buttons.html.slim new file mode 100644 index 000000000..e436b3337 --- /dev/null +++ b/app/views/training/questions/_opinion_radio_buttons.html.slim @@ -0,0 +1,13 @@ += f.govuk_radio_buttons_fieldset :answers, legend: { text: response.legend } do + = m(content.hint) unless content.hint.nil? + + = f.hidden_field :answers, multiple: true + - if response.options.any? + - response.options.each.with_index(1) do |option, index| + - if response.options.count.eql?(index) && content.other.present? + = f.govuk_radio_button :answers, option.id, label: { text: 'No' }, link_errors: true, checked: option.checked? do + = f.govuk_text_area :text_input, label: { text: content.other } + - else + = f.opinion_radio_button(option) + - else + = f.govuk_text_area :text_input, label: nil, rows: 9 diff --git a/config/application.rb b/config/application.rb index 29a814306..1876cc817 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,7 +2,6 @@ require 'rails/all' Bundler.require(*Rails.groups) -# require 'grover' module EarlyYearsFoundationRecovery class Application < Rails::Application @@ -18,6 +17,15 @@ class Application < Rails::Application # config.time_zone = ENV.fetch('TZ', 'Europe/London') config.service_url = (Rails.env.production? ? 'https://' : 'http://') + ENV.fetch('DOMAIN', 'child-development-training') + # @see #maintenance? + # These endpoints are exempt from maintenance page redirection + config.protected_endpoints = %w[ + /maintenance + /health + /change + /release + ] + config.middleware.use Grover::Middleware config.active_record.yaml_column_permitted_classes = [Symbol] config.action_view.sanitized_allowed_tags = %w[p ul li div ol strong].freeze diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index 3f9bfddd0..bdf960eec 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -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--vx+cSpRyNJ59Lb/V--JELolhAJlC4w1e30/BNCSg== \ No newline at end of file +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--vx+cSpRyNJ59Lb/V--JELolhAJlC4w1e30/BNCSg== diff --git a/config/locales/en.yml b/config/locales/en.yml index 8a554bb7f..f71526842 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -50,6 +50,7 @@ en: attributes: answers: blank: Please select an answer. + invalid: Please select an option. user_answer: attributes: answer: @@ -120,6 +121,7 @@ en: save_continue: Save and continue finish_test: Finish test finish: Finish + give_feedback: Give feedback links: save: Save @@ -607,6 +609,33 @@ en: This module covers: %{criteria} + + # /feedback + feedback: + heading: Give feedback + body: | + The purpose of this feedback form is to gather your opinon on the child development training course that the Department for Education has created for early years practitioners. + + For more information on how you data will be used, please view our privacy notice. ADD LINK + + Completing this form is voluntary and you can withdraw your feedback at any time. + + By completing this form you have understood the above and consent to take part. + feedback_exists: + heading: You have already submitted feedback + body: Thank you for helping to improve this training + back_button: Previous + next_button: Update my feedback + technical_support: + heading: Technical support queries + body: If you have any questions about how to sue this website or are experiencing any technical issues, please use our contact form ADD LINK so that our team can follow up with your enquiry. + next_button: Next + + # /feedback/thank-you + thank_you: + heading: Thank you + body: Thank you for helping to improve this training + next_button: Go to my modules # /gov-one/info gov_one_info: diff --git a/config/routes.rb b/config/routes.rb index e50c5c706..b304da8ae 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -87,6 +87,12 @@ end end + resources :feedback, only: %i[index show update] do + collection do + get 'thank-you', to: 'feedback#thank_you' + end + end + post 'change', to: 'hook#change' post 'release', to: 'hook#release' diff --git a/db/migrate/20240131135344_change_training_module_in_responses.rb b/db/migrate/20240131135344_change_training_module_in_responses.rb new file mode 100644 index 000000000..e682aaa2c --- /dev/null +++ b/db/migrate/20240131135344_change_training_module_in_responses.rb @@ -0,0 +1,5 @@ +class ChangeTrainingModuleInResponses < ActiveRecord::Migration[7.0] + def change + change_column_null :responses, :training_module, true + end +end diff --git a/db/migrate/20240207105459_add_text_input_to_responses.rb b/db/migrate/20240207105459_add_text_input_to_responses.rb new file mode 100644 index 000000000..f8f3b79c6 --- /dev/null +++ b/db/migrate/20240207105459_add_text_input_to_responses.rb @@ -0,0 +1,5 @@ +class AddTextInputToResponses < ActiveRecord::Migration[7.0] + def change + add_column :responses, :text_input, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index ce18569a4..68822cd93 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -129,8 +129,8 @@ end create_table "responses", force: :cascade do |t| - t.bigint "user_id", null: false - t.string "training_module", null: false + t.bigint "user_id" + t.string "training_module" t.string "question_name", null: false t.jsonb "answers", default: [] t.boolean "correct" @@ -138,6 +138,7 @@ t.datetime "updated_at", null: false t.string "question_type" t.bigint "assessment_id" + t.text "text_input" t.index ["assessment_id"], name: "index_responses_on_assessment_id" t.index ["user_id", "training_module", "question_name"], name: "user_question" t.index ["user_id"], name: "index_responses_on_user_id" diff --git a/lib/content_test_schema.rb b/lib/content_test_schema.rb index e3e724eb2..cf4850ff6 100644 --- a/lib/content_test_schema.rb +++ b/lib/content_test_schema.rb @@ -57,9 +57,13 @@ def inputs [ [:click_on, results_button], ] + elsif type.match?(/opinion_intro/) + [ + [:click_on, 'Skip feedback'], + ] elsif type.match?(/thankyou/) [ - [:click_on, 'Finish'], + [:click_on, 'View certificate'], ] elsif type.match?(/certificate/) [] diff --git a/spec/config_spec.rb b/spec/config_spec.rb index 6379459d4..76f6877ae 100644 --- a/spec/config_spec.rb +++ b/spec/config_spec.rb @@ -8,7 +8,9 @@ expect(config.contentful_environment).to eq 'test' end + # Uncomment this when feedback forms have been published it 'tests against published content' do + skip 'feedback wip' expect(Rails.application).not_to be_preview end @@ -48,4 +50,15 @@ expect(config.user_timeout_modal_visible).to eq 5 end end + + describe 'pages accessible even when in maintenance mode' do + specify do + expect(config.protected_endpoints).to eq %w[ + /maintenance + /health + /change + /release + ] + end + end end diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb new file mode 100644 index 000000000..b4ffa2f3a --- /dev/null +++ b/spec/controllers/feedback_controller_spec.rb @@ -0,0 +1,58 @@ +require 'rails_helper' + +RSpec.describe FeedbackController, type: :controller do + context 'when user is signed in' do + let(:user) { create :user, :registered } + let(:valid_attributes) do + { id: 1, answers: %w[Yes], answers_custom: 'Custom answer', training_module: nil, question_name: 'main-feedback-1' } + end + + before { sign_in user } + + describe 'GET #show' do + it 'returns a success response' do + get :show, params: { id: 1 } + expect(response).to be_successful + end + end + + describe 'GET #index' do + it 'returns a success response' do + get :index + expect(response).to be_successful + end + end + + describe 'POST #update' do + context 'with valid params' do + it 'creates a new Response' do + expect { + post :update, params: valid_attributes + }.to change(Response, :count).by(1) + end + + it 'redirects to the next feedback path' do + post :update, params: valid_attributes + expect(response).to redirect_to(feedback_path(2)) + end + end + + context 'with invalid params' do + let(:invalid_attributes) do + { id: 1, answers: [''] } + end + + it 'does not create a new Response' do + expect { + post :update, params: invalid_attributes + }.not_to change(Response, :count) + end + + it 'redirects to the current feedback path' do + post :update, params: invalid_attributes + expect(response).to redirect_to(feedback_path(1)) + end + end + end + end +end diff --git a/spec/controllers/training/responses_controller_spec.rb b/spec/controllers/training/responses_controller_spec.rb index d7b9ab17d..5c397a2ff 100644 --- a/spec/controllers/training/responses_controller_spec.rb +++ b/spec/controllers/training/responses_controller_spec.rb @@ -8,7 +8,7 @@ patch :update, params: { training_module_id: 'alpha', id: question_name, - response: { answers: answers }, + response: { answers: answers, text_input: 'Text input' }, } else patch :update, params: { @@ -78,6 +78,26 @@ specify { expect(records).to be 0 } end end + + context 'when text input (text area)' do + let(:question_name) { 'end-of-module-feedback-4' } + + context 'with text input' do + let(:answers) { [] } + let(:text_input) { 'Text input for feedback question' } + + specify { expect(response).to have_http_status(:redirect) } + specify { expect(records).to be 1 } + end + + context 'and no text input' do + let(:answers) { [] } + let(:text_input) { nil } + + specify { expect(response).to have_http_status(:redirect) } + specify { expect(records).to be 1 } + end + end end end end diff --git a/spec/decorators/pagination_decorator_spec.rb b/spec/decorators/pagination_decorator_spec.rb index 115ec7afe..60953e88e 100644 --- a/spec/decorators/pagination_decorator_spec.rb +++ b/spec/decorators/pagination_decorator_spec.rb @@ -23,4 +23,24 @@ it '#percentage' do expect(decorator.percentage).to eq '29%' end + + describe('skippable questions') do + let(:content) { mod.page_by_name('end-of-module-feedback-4') } + + context('when answered') do + before do + create(:response, question_name: content.name, text_input: 'text input') + end + + it '#page_numbers' do + expect(decorator.page_numbers).to eq 'Page 4 of 5' + end + end + + context('when unanswered') do + it '#page_numbers' do + expect(decorator.page_numbers).to eq 'Page 4 of 5' + end + end + end end diff --git a/spec/helpers/link_helper_spec.rb b/spec/helpers/link_helper_spec.rb index 4fb088420..37e81eed0 100644 --- a/spec/helpers/link_helper_spec.rb +++ b/spec/helpers/link_helper_spec.rb @@ -48,6 +48,20 @@ expect(link).to include 'Next page has not been created' end end + + context 'when page is feedback intro' do + let(:skip_link) { helper.link_to_skip } + + let(:content) { mod.pages_by_type('opinion_intro').first } + + it 'targets start of feedback questions' do + expect(link).to include 'Give feedback' + end + + it 'offers button to skip feedback questions' do + expect(skip_link).to include 'Skip feedback' + end + end end describe '#link_to_previous' do @@ -184,4 +198,23 @@ end end end + + describe '#link_to_skip' do + subject(:link) { helper.link_to_skip } + + before do + without_partial_double_verification do + allow(view).to receive(:content).and_return(content) + allow(view).to receive(:mod).and_return(mod) + end + end + + context 'when page is feedback intro' do + let(:content) { mod.pages_by_type('opinion_intro').first } + + it 'targets thank you page' do + expect(link).to include 'Skip feedback' + end + end + end end diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 06efce431..28e5ee7bb 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 216 + expect(locales.count).to be 230 end it 'dot separated key -> Page::Resource#name' do diff --git a/spec/models/concerns/content_types_spec.rb b/spec/models/concerns/content_types_spec.rb index 049fd596d..509120164 100644 --- a/spec/models/concerns/content_types_spec.rb +++ b/spec/models/concerns/content_types_spec.rb @@ -88,6 +88,18 @@ specify { expect(content).to be_confidence_question } end + describe '#opinion_intro?' do + before { content.page_type = 'opinion_intro' } + + specify { expect(content).to be_opinion_intro } + end + + describe '#opinion_question?' do + before { content.page_type = 'opinion' } + + specify { expect(content).to be_opinion_question } + end + describe '#thankyou?' do before { content.page_type = 'thankyou' } @@ -120,6 +132,8 @@ assessment_results confidence_intro confidence_questionnaire + opinion_intro + opinion thankyou certificate ]) diff --git a/spec/models/course_spec.rb b/spec/models/course_spec.rb index 127f287d5..fd96137c5 100644 --- a/spec/models/course_spec.rb +++ b/spec/models/course_spec.rb @@ -17,9 +17,8 @@ end it 'feedback' do - expect(course.feedback).not_to be_empty - expect(course.feedback.count).to be 5 - # expect(course.feedback.map(&:question_type)).to be 'feedback' + expect(course.feedback.count).to eq 5 + expect(course.feedback.first.page_type).to eq 'opinion' end end diff --git a/spec/models/data_analysis/confidence_check_scores_spec.rb b/spec/models/data_analysis/confidence_check_scores_spec.rb index add96392d..4e8d852cb 100644 --- a/spec/models/data_analysis/confidence_check_scores_spec.rb +++ b/spec/models/data_analysis/confidence_check_scores_spec.rb @@ -4,9 +4,9 @@ before do skip unless Rails.application.migrated_answers? - create(:response, question_type: 'confidence', training_module: 'module_1', question_name: 'q1', answers: [1]) - create(:response, question_type: 'confidence', training_module: 'module_1', question_name: 'q2', answers: [2]) - create(:response, question_type: 'confidence', training_module: 'module_1', question_name: 'q2', answers: [2]) + create(:response, question_type: 'confidence', training_module: 'alpha', question_name: '1-3-3-1', answers: [1]) + create(:response, question_type: 'confidence', training_module: 'alpha', question_name: '1-3-3-2', answers: [2]) + create(:response, question_type: 'confidence', training_module: 'alpha', question_name: '1-3-3-2', answers: [2]) end let(:headers) do @@ -21,14 +21,14 @@ let(:rows) do [ { - module_name: 'module_1', - question_name: 'q1', + module_name: 'alpha', + question_name: '1-3-3-1', answers: [1], count: 1, }, { - module_name: 'module_1', - question_name: 'q2', + module_name: 'alpha', + question_name: '1-3-3-2', answers: [2], count: 2, }, diff --git a/spec/models/data_analysis/high_fail_questions_spec.rb b/spec/models/data_analysis/high_fail_questions_spec.rb index 39506515d..abc353491 100644 --- a/spec/models/data_analysis/high_fail_questions_spec.rb +++ b/spec/models/data_analysis/high_fail_questions_spec.rb @@ -17,8 +17,8 @@ fail_rate_percentage: 0.5, }, { - module_name: 'module_1', - question_name: 'q2', + module_name: 'alpha', + question_name: '1-3-2-2', fail_rate_percentage: 1.0, }, ] @@ -28,37 +28,37 @@ if Rails.application.migrated_answers? create :response, question_type: 'summative', - training_module: 'module_1', - question_name: 'q1', + training_module: 'alpha', + question_name: '1-3-2-1', answers: [1], correct: true create :response, question_type: 'summative', - training_module: 'module_2', - question_name: 'q1', + training_module: 'bravo', + question_name: '1-3-2-1', answers: [1], correct: true create :response, question_type: 'summative', - training_module: 'module_1', - question_name: 'q2', + training_module: 'alpha', + question_name: '1-3-2-2', answers: [2], correct: false create :response, question_type: 'summative', - training_module: 'module_1', - question_name: 'q2', + training_module: 'alpha', + question_name: '1-3-2-2', answers: [2], correct: false else - create(:user_answer, :correct, :questionnaire, :summative, module: 'module_1', name: 'q1') - create(:user_answer, :correct, :questionnaire, :summative, module: 'module_2', name: 'q1') - create(:user_answer, :incorrect, :questionnaire, :summative, module: 'module_1', name: 'q2') - create(:user_answer, :incorrect, :questionnaire, :summative, module: 'module_1', name: 'q2') + create(:user_answer, :correct, :questionnaire, :summative, module: 'alpha', name: '1-3-2-1') + create(:user_answer, :correct, :questionnaire, :summative, module: 'bravo', name: '1-3-2-1') + create(:user_answer, :incorrect, :questionnaire, :summative, module: 'alpha', name: '1-3-2-2') + create(:user_answer, :incorrect, :questionnaire, :summative, module: 'alpha', name: '1-3-2-2') end end diff --git a/spec/models/response_spec.rb b/spec/models/response_spec.rb index 629c61da0..b08788996 100644 --- a/spec/models/response_spec.rb +++ b/spec/models/response_spec.rb @@ -22,6 +22,7 @@ updated_at question_type assessment_id + text_input ] end diff --git a/spec/models/training/module_spec.rb b/spec/models/training/module_spec.rb index c39dcb329..4c4db0d82 100644 --- a/spec/models/training/module_spec.rb +++ b/spec/models/training/module_spec.rb @@ -41,8 +41,8 @@ it 'returns sections' do expect(sections).to be_a Hash - expect(sections.keys).to eq [1, 2, 3, 4] - expect(sections.values.map(&:count)).to eq [7, 5, 20, 1] + expect(sections.keys).to eq [1, 2, 3, 4, 5] + expect(sections.values.map(&:count)).to eq [7, 5, 19, 6, 1] end end @@ -51,14 +51,14 @@ it 'returns subsections' do expect(subsections).to be_a Hash - expect(subsections.keys).to eq [[1, 0], [1, 1], [1, 2], [1, 3], [1, 4], [2, 0], [2, 1], [3, 0], [3, 1], [3, 2], [3, 3], [4, 0]] - expect(subsections.values.map(&:count)).to eq [1, 1, 1, 2, 2, 1, 4, 1, 1, 12, 6, 1] + expect(subsections.keys).to eq [[1, 0], [1, 1], [1, 2], [1, 3], [1, 4], [2, 0], [2, 1], [3, 0], [3, 1], [3, 2], [3, 3], [4, 0], [5, 0]] + expect(subsections.values.map(&:count)).to eq [1, 1, 1, 2, 2, 1, 4, 1, 1, 12, 5, 6, 1] end end describe '#submodule_count' do it 'returns the number of sections' do - expect(mod.submodule_count).to eq 4 + expect(mod.submodule_count).to eq 5 end end diff --git a/spec/models/training/question_spec.rb b/spec/models/training/question_spec.rb index 9a837f13c..dd8aa9006 100644 --- a/spec/models/training/question_spec.rb +++ b/spec/models/training/question_spec.rb @@ -96,6 +96,28 @@ expect(question.legend).to start_with 'True or false?' end end + + context 'when the question is a feedback question' do + subject(:question) do + Training::Module.by_name('alpha').page_by_name('end-of-module-feedback-1') + end + + let(:first_option) { question.options.first } + + specify do + expect(first_option.label).to eq 'Strongly agree' + end + end + + context 'when the question is a feedback free text question' do + subject(:question) do + Training::Module.by_name('alpha').page_by_name('end-of-module-feedback-4') + end + + specify do + expect(question.answers).to eq [] + end + end end describe '#debug_summary' do diff --git a/spec/models/training/response_spec.rb b/spec/models/training/response_spec.rb index ca935ba5d..aced29c50 100644 --- a/spec/models/training/response_spec.rb +++ b/spec/models/training/response_spec.rb @@ -100,4 +100,16 @@ end end end + + context 'with radio buttons for opinion question' do + let(:question_name) { 'end-of-module-feedback-1' } + + describe 'and no answer' do + let(:answers) { nil } + + it 'is invalid' do + expect(response).to be_invalid + end + end + end end diff --git a/spec/support/ast/alpha-pass.yml b/spec/support/ast/alpha-pass.yml index 3dfe96bda..aaaade9c3 100644 --- a/spec/support/ast/alpha-pass.yml +++ b/spec/support/ast/alpha-pass.yml @@ -215,6 +215,11 @@ - user-answer-answers-5-field - - :click_on - Next +- :path: /modules/alpha/content-pages/feedback-intro + :text: Additional Feedback + :inputs: + - - :click_on + - Give feedback - :path: /modules/alpha/content-pages/1-3-3-5 :text: Thank you :inputs: diff --git a/spec/support/shared/with_content.rb b/spec/support/shared/with_content.rb index 8b2e88614..e404558e3 100644 --- a/spec/support/shared/with_content.rb +++ b/spec/support/shared/with_content.rb @@ -70,6 +70,8 @@ assessment_results confidence_intro confidence_questionnaire + opinion_intro + opinion thankyou certificate ] diff --git a/spec/support/shared/with_progress.rb b/spec/support/shared/with_progress.rb index fa8170392..1549cd3d2 100644 --- a/spec/support/shared/with_progress.rb +++ b/spec/support/shared/with_progress.rb @@ -36,6 +36,16 @@ def start_confidence_check(mod) view_pages_upto(mod, 'confidence_questionnaire') end + # @param mod [Training::Module] + def start_end_of_module_feedback_intro(mod) + view_pages_upto(mod, 'opinion_intro') + end + + # @param mod [Training::Module] + def start_end_of_module_feedback_form(mod) + view_pages_upto(mod, 'opinion') + end + # @param mod [Training::Module] def start_summative_assessment(mod) view_pages_upto(mod, 'summative_questionnaire') diff --git a/spec/system/common_page_spec.rb b/spec/system/common_page_spec.rb index 5234be1a4..9bd9f26fd 100644 --- a/spec/system/common_page_spec.rb +++ b/spec/system/common_page_spec.rb @@ -26,6 +26,14 @@ end end + describe 'feedback intro' do + subject(:common_page) { '/modules/alpha/content-pages/feedback-intro' } + + it 'uses generic content' do + expect(page).to have_content 'Additional feedback' + end + end + describe 'thank you' do subject(:common_page) { '/modules/alpha/content-pages/1-3-3-5' } diff --git a/spec/system/event_log_spec.rb b/spec/system/event_log_spec.rb index c3e9961ed..30a6c6fac 100644 --- a/spec/system/event_log_spec.rb +++ b/spec/system/event_log_spec.rb @@ -133,7 +133,7 @@ it 'tracks start and completion' do expect(events.where(name: 'module_start').size).to be 1 - expect(events.where(name: 'module_content_page').size).to be 33 + expect(events.where(name: 'module_content_page').size).to be 38 expect(events.where(name: 'module_complete').size).to eq 1 end end diff --git a/spec/system/module_overview_content_spec.rb b/spec/system/module_overview_content_spec.rb index f66dd51ef..5dec0d0b7 100644 --- a/spec/system/module_overview_content_spec.rb +++ b/spec/system/module_overview_content_spec.rb @@ -30,6 +30,10 @@ .and have_content('Summary and next steps') end + it 'hides feedback section' do + expect(page).not_to have_content('Additional feedback') + end + it 'has the topic names' do expect(page).to have_content('1-1-1') .and have_content('1-1-2') diff --git a/spec/system/module_overview_progress_spec.rb b/spec/system/module_overview_progress_spec.rb index 5bb114158..16b154fe7 100644 --- a/spec/system/module_overview_progress_spec.rb +++ b/spec/system/module_overview_progress_spec.rb @@ -27,7 +27,7 @@ expect(page).not_to have_link 'Reflect on your learning' end - within '#section-content-4' do + within '#section-content-5' do expect(page).to have_content 'not started', count: 1 expect(page).not_to have_link 'Download your certificate' end @@ -203,7 +203,7 @@ expect(page).to have_content 'complete', count: 3 end - within '#section-content-4' do + within '#section-content-5' do expect(page).to have_content 'complete', count: 1 end end diff --git a/spec/system/opinion_spec.rb b/spec/system/opinion_spec.rb new file mode 100644 index 000000000..92127ef7c --- /dev/null +++ b/spec/system/opinion_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe 'End of module feedback form' do + include_context 'with progress' + include_context 'with user' + + let(:first_question_path) { '/modules/alpha/questionnaires/end-of-module-feedback-1' } + let(:third_question_path) { '/modules/alpha/questionnaires/end-of-module-feedback-3' } + + it 'shows feedback question' do + visit '/modules/alpha/content-pages/feedback-intro' + expect(page).to have_content('Additional feedback') + click_on 'Give feedback' + expect(page).to have_content('Regarding the training module you have just completed: the content was easy to understand') + expect(page).to have_content('Strongly agree') + end + + it do + visit third_question_path + expect(page).to have_content('Did the module meet your expectations') + expect(page).to have_content('Yes') + end + + context 'when no answer is submitted' do + it 'displays an error message' do + visit first_question_path + click_on 'Next' + expect(page).to have_content 'Please select an option' + end + end + + it 'does not link to additional feedback from the module overview page' do + visit '/modules/alpha' + + expect(page).to have_content 'Reflect on your learning' + expect(page).not_to have_link 'Additional feedback' + end +end From 85fb273bfc4521260c75a97315989117228f1d72 Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Fri, 1 Mar 2024 10:32:12 +0000 Subject: [PATCH 04/95] Update based on PR comments --- app/forms/form_builder.rb | 10 ---------- app/models/concerns/content_types.rb | 10 ---------- app/models/concerns/pagination.rb | 2 +- app/models/response.rb | 4 +--- app/views/training/modules/_section.html.slim | 5 +---- app/views/training/modules/show.html.slim | 1 + .../questions/_main_feedback_radio_buttons.html.slim | 2 +- .../questions/_opinion_radio_buttons.html.slim | 2 +- config/locales/en.yml | 2 +- spec/controllers/training/responses_controller_spec.rb | 7 +++---- spec/decorators/pagination_decorator_spec.rb | 6 +++--- 11 files changed, 13 insertions(+), 38 deletions(-) diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index 6df81b0b3..45cd9f10e 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -31,16 +31,6 @@ def question_radio_button(option) checked: option.checked? end - # @param option [Training::Answer::Option] - def opinion_radio_button(option) - govuk_radio_button :answers, - option.id, - label: { text: option.label }, - link_errors: true, - disabled: option.disabled?, - checked: option.checked? - end - # @param option [Training::Answer::Option] def question_check_box(option) govuk_check_box :answers, diff --git a/app/models/concerns/content_types.rb b/app/models/concerns/content_types.rb index e8d3c482d..8f0dd2129 100644 --- a/app/models/concerns/content_types.rb +++ b/app/models/concerns/content_types.rb @@ -6,11 +6,6 @@ def interruption_page? page_type.eql?('interruption_page') end - # @return [Boolean] - def feedback_page? - page_type.eql?('feedback') - end - # ============================================================================ # TRAINING SECTIONS # ============================================================================ @@ -30,11 +25,6 @@ def is_question? page_type.match?(/question/) end - # @return [Boolean] - def is_opinion? - page_type.match?(/opinion/) - end - # @return [Boolean] def text_page? page_type.eql?('text_page') diff --git a/app/models/concerns/pagination.rb b/app/models/concerns/pagination.rb index 3387d236c..5d897a5b4 100644 --- a/app/models/concerns/pagination.rb +++ b/app/models/concerns/pagination.rb @@ -34,7 +34,7 @@ def next_item # @return [String] def previous_item_id - content_index.zero? ? parent.pages[content_index].id : parent.pages[content_index - 1].id + parent.pages[content_index - 1].id end # @return [String, nil] diff --git a/app/models/response.rb b/app/models/response.rb index ec24dc9fe..42be7f02c 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -35,9 +35,7 @@ def question # @return [Array] def options - if question.confidence_question? || question.opinion_question? - question.options(checked: answers) - elsif question.formative_question? || assessment&.graded? + if question.formative_question? || assessment&.graded? question.options(checked: answers, disabled: responded?) else question.options(checked: answers) diff --git a/app/views/training/modules/_section.html.slim b/app/views/training/modules/_section.html.slim index 6205c017f..7585100fa 100644 --- a/app/views/training/modules/_section.html.slim +++ b/app/views/training/modules/_section.html.slim @@ -1,10 +1,7 @@ section.module-overview--section .progress-bar span.icon class=icon - - if position == 5 - span.number = 4 - - else - span.number = position + span.number = position - if display_line .line diff --git a/app/views/training/modules/show.html.slim b/app/views/training/modules/show.html.slim index 628921f1e..f5e88fc95 100644 --- a/app/views/training/modules/show.html.slim +++ b/app/views/training/modules/show.html.slim @@ -33,6 +33,7 @@ - module_progress.sections.each do |section| - unless section[:hide] = render 'section', **section + /= the feedback section is not visible in the module overview = govuk_button_link_to link_to_action[1], class: 'govuk-button--start', id: 'module-call-to-action' do | #{link_to_action[0]} diff --git a/app/views/training/questions/_main_feedback_radio_buttons.html.slim b/app/views/training/questions/_main_feedback_radio_buttons.html.slim index e5f3309c2..b10cf3254 100644 --- a/app/views/training/questions/_main_feedback_radio_buttons.html.slim +++ b/app/views/training/questions/_main_feedback_radio_buttons.html.slim @@ -7,7 +7,7 @@ = f.govuk_radio_button :answers, option.id, label: { text: 'Other' }, link_errors: true do = f.govuk_text_field :answers_custom, label: { text: content.other } - else - = f.opinion_radio_button(option) + = f.question_radio_button(option) - if !content.hint.nil? = f.govuk_text_area :answers_custom, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } - else diff --git a/app/views/training/questions/_opinion_radio_buttons.html.slim b/app/views/training/questions/_opinion_radio_buttons.html.slim index e436b3337..141527d3c 100644 --- a/app/views/training/questions/_opinion_radio_buttons.html.slim +++ b/app/views/training/questions/_opinion_radio_buttons.html.slim @@ -8,6 +8,6 @@ = f.govuk_radio_button :answers, option.id, label: { text: 'No' }, link_errors: true, checked: option.checked? do = f.govuk_text_area :text_input, label: { text: content.other } - else - = f.opinion_radio_button(option) + = f.question_radio_button(option) - else = f.govuk_text_area :text_input, label: nil, rows: 9 diff --git a/config/locales/en.yml b/config/locales/en.yml index f71526842..d7975739e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -120,7 +120,7 @@ en: start_test: Start test save_continue: Save and continue finish_test: Finish test - finish: Finish + finish: View certificate give_feedback: Give feedback links: diff --git a/spec/controllers/training/responses_controller_spec.rb b/spec/controllers/training/responses_controller_spec.rb index 5c397a2ff..3a85966fc 100644 --- a/spec/controllers/training/responses_controller_spec.rb +++ b/spec/controllers/training/responses_controller_spec.rb @@ -79,19 +79,18 @@ end end - context 'when text input (text area)' do + context 'when the question expects text and is answered' do let(:question_name) { 'end-of-module-feedback-4' } + let(:answers) { [] } context 'with text input' do - let(:answers) { [] } let(:text_input) { 'Text input for feedback question' } specify { expect(response).to have_http_status(:redirect) } specify { expect(records).to be 1 } end - context 'and no text input' do - let(:answers) { [] } + context 'with no text input' do let(:text_input) { nil } specify { expect(response).to have_http_status(:redirect) } diff --git a/spec/decorators/pagination_decorator_spec.rb b/spec/decorators/pagination_decorator_spec.rb index 60953e88e..f6b034916 100644 --- a/spec/decorators/pagination_decorator_spec.rb +++ b/spec/decorators/pagination_decorator_spec.rb @@ -24,10 +24,10 @@ expect(decorator.percentage).to eq '29%' end - describe('skippable questions') do + describe 'skippable questions' do let(:content) { mod.page_by_name('end-of-module-feedback-4') } - context('when answered') do + context 'when answered' do before do create(:response, question_name: content.name, text_input: 'text input') end @@ -37,7 +37,7 @@ end end - context('when unanswered') do + context 'when unanswered' do it '#page_numbers' do expect(decorator.page_numbers).to eq 'Page 4 of 5' end From 5504a0381c2d4312828d39355746474f09dac2cf Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:54:17 +0000 Subject: [PATCH 05/95] Rename opinion type to feedback and rename free text question used in specs --- app/controllers/concerns/learning.rb | 2 +- app/controllers/training/pages_controller.rb | 2 +- app/controllers/training/questions_controller.rb | 2 +- app/controllers/training/responses_controller.rb | 6 +++--- app/decorators/pagination_decorator.rb | 4 ++-- app/models/concerns/content_types.rb | 4 ++-- app/models/training/module.rb | 2 +- app/models/training/question.rb | 16 ++++++++-------- app/models/user.rb | 2 +- .../training/responses_controller_spec.rb | 2 +- spec/decorators/next_page_decorator_spec.rb | 2 +- spec/decorators/pagination_decorator_spec.rb | 2 +- spec/models/concerns/content_types_spec.rb | 8 ++++---- spec/models/course_spec.rb | 2 +- spec/models/training/question_spec.rb | 2 +- spec/models/training/response_spec.rb | 2 +- spec/support/shared/with_content.rb | 2 +- spec/support/shared/with_progress.rb | 2 +- 18 files changed, 32 insertions(+), 32 deletions(-) diff --git a/app/controllers/concerns/learning.rb b/app/controllers/concerns/learning.rb index 4df9a4f26..34a0b2cd4 100644 --- a/app/controllers/concerns/learning.rb +++ b/app/controllers/concerns/learning.rb @@ -67,6 +67,6 @@ def section_bar # @note memoization ensures validation errors work # @return [UserAnswer, Response] def current_user_response - @current_user_response ||= content.opinion_question? ? current_user.response_for_shared(content, mod) : current_user.response_for(content) + @current_user_response ||= content.feedback_question? ? current_user.response_for_shared(content, mod) : current_user.response_for(content) end end diff --git a/app/controllers/training/pages_controller.rb b/app/controllers/training/pages_controller.rb index 5a4b1b5ea..ee90f78ee 100644 --- a/app/controllers/training/pages_controller.rb +++ b/app/controllers/training/pages_controller.rb @@ -19,7 +19,7 @@ def index end def show - if content.is_question? || content.opinion_question? + if content.is_question? || content.feedback_question? redirect_to training_module_question_path(mod.name, content.name) elsif content.assessment_results? redirect_to training_module_assessment_path(mod.name, content.name) diff --git a/app/controllers/training/questions_controller.rb b/app/controllers/training/questions_controller.rb index 7dd0f8161..88b3be0e8 100644 --- a/app/controllers/training/questions_controller.rb +++ b/app/controllers/training/questions_controller.rb @@ -54,7 +54,7 @@ def track_confidence_start? # @return [Boolean] def track_feedback_start? - content.opinion_question? || content.opinion_intro? + content.feedback_question? || content.opinion_intro? end # @return [Boolean] diff --git a/app/controllers/training/responses_controller.rb b/app/controllers/training/responses_controller.rb index aba982842..2af040af3 100644 --- a/app/controllers/training/responses_controller.rb +++ b/app/controllers/training/responses_controller.rb @@ -17,11 +17,11 @@ class ResponsesController < ApplicationController layout 'hero' def update - if save_response! || (content.opinion_question? && content.options.blank?) + if save_response! || (content.feedback_question? && content.options.blank?) track_question_answer redirect else - if content.opinion_question? && user_answer_text.blank? && content.options.present? + if content.feedback_question? && user_answer_text.blank? && content.options.present? current_user_response.errors.clear current_user_response.errors.add :answers, :invalid end @@ -45,7 +45,7 @@ def response_params # @note migrate from user_answer to response # @return [Boolean] def save_response! - correct_answers = content.confidence_question? || content.opinion_question? ? true : content.correct_answers.eql?(user_answers) + correct_answers = content.confidence_question? || content.feedback_question? ? true : content.correct_answers.eql?(user_answers) if Rails.application.migrated_answers? current_user_response.update(answers: user_answers, correct: correct_answers, text_input: user_answer_text) diff --git a/app/decorators/pagination_decorator.rb b/app/decorators/pagination_decorator.rb index c386cbce6..5da6930cb 100644 --- a/app/decorators/pagination_decorator.rb +++ b/app/decorators/pagination_decorator.rb @@ -15,7 +15,7 @@ def heading # @return [String] def section_numbers - if content.opinion_intro? || content.opinion_question? + if content.opinion_intro? || content.feedback_question? I18n.t(:section, scope: :pagination, current: content.submodule - 1, total: section_total - 1) else I18n.t(:section, scope: :pagination, current: content.submodule, total: section_total - 1) @@ -45,7 +45,7 @@ def page_total if content.section_content.any?(&:skippable?) # && response_for_shared.responded? # don't count skipped page content.section_content.each do |section_content| - if section_content.opinion_question? && section_content.always_show_question.eql?(false) + if section_content.feedback_question? && section_content.always_show_question.eql?(false) size -= 1 end end diff --git a/app/models/concerns/content_types.rb b/app/models/concerns/content_types.rb index 8f0dd2129..d42fbfdea 100644 --- a/app/models/concerns/content_types.rb +++ b/app/models/concerns/content_types.rb @@ -90,8 +90,8 @@ def one_off_question? end # @return [Boolean] - def opinion_question? - page_type.eql?('opinion') + def feedback_question? + page_type.eql?('feedback') end # @return [Boolean] diff --git a/app/models/training/module.rb b/app/models/training/module.rb index 3f118927f..1d3907584 100644 --- a/app/models/training/module.rb +++ b/app/models/training/module.rb @@ -247,7 +247,7 @@ def confidence_questions # @return [Array] def opinion_questions - content.select(&:opinion_question?) + content.select(&:feedback_question?) end # @param text [String] diff --git a/app/models/training/question.rb b/app/models/training/question.rb index b01449da6..1e9ee97b6 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -21,7 +21,7 @@ def skippable? # @return [String] powered by JSON not type def to_partial_path - return 'training/questions/opinion_radio_buttons' if opinion_question? + return 'training/questions/opinion_radio_buttons' if feedback_question? partial = multi_select? ? 'check_boxes' : 'radio_buttons' partial = "learning_#{partial}" if formative_question? @@ -30,16 +30,16 @@ def to_partial_path # @return [Boolean] def multi_select? - confidence_question? || opinion_question? ? false : answer.multi_select? + confidence_question? || feedback_question? ? false : answer.multi_select? end - def opinion_question? - page_type == 'opinion' + def feedback_question? + page_type == 'feedback' end # @return [Boolean] feedback free text def free_text? - opinion_question? && options.empty? + feedback_question? && options.empty? end # @return [Boolean] event tracking @@ -81,7 +81,7 @@ def assessments_type formative_questionnaire: 'formative_assessment', summative_questionnaire: 'summative_assessment', confidence_questionnaire: 'confidence_check', - opinion: 'opinion', + feedback: 'feedback', }.fetch(page_type.to_sym) end @@ -94,7 +94,7 @@ def question_type summative_questionnaire: 'summative', summative: 'summative', confidence_questionnaire: 'confidence', - opinion: 'opinion', + feedback: 'feedback', confidence: 'confidence', }.fetch(page_type.to_sym) end @@ -125,7 +125,7 @@ def legend #{body} LEGEND - elsif opinion_question? + elsif feedback_question? body.to_s else "#{body} (Select one answer)" diff --git a/app/models/user.rb b/app/models/user.rb index a371835fc..41667ac12 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -214,7 +214,7 @@ def response_for(content) assessments.create(training_module: content.parent.name, started_at: Time.zone.now) end - if content.opinion_question? + if content.feedback_question? responses.find_or_initialize_by( question_name: content.name, training_module: module_name, diff --git a/spec/controllers/training/responses_controller_spec.rb b/spec/controllers/training/responses_controller_spec.rb index 3a85966fc..bd865c23b 100644 --- a/spec/controllers/training/responses_controller_spec.rb +++ b/spec/controllers/training/responses_controller_spec.rb @@ -80,7 +80,7 @@ end context 'when the question expects text and is answered' do - let(:question_name) { 'end-of-module-feedback-4' } + let(:question_name) { 'feedback-freetext' } let(:answers) { [] } context 'with text input' do diff --git a/spec/decorators/next_page_decorator_spec.rb b/spec/decorators/next_page_decorator_spec.rb index 5ea783757..38c9da1cd 100644 --- a/spec/decorators/next_page_decorator_spec.rb +++ b/spec/decorators/next_page_decorator_spec.rb @@ -100,7 +100,7 @@ let(:content) { mod.page_by_name('1-3-3-5') } it '#text' do - expect(decorator.text).to eq 'Finish' + expect(decorator.text).to eq 'View certificate' end end end diff --git a/spec/decorators/pagination_decorator_spec.rb b/spec/decorators/pagination_decorator_spec.rb index f6b034916..55845e7a1 100644 --- a/spec/decorators/pagination_decorator_spec.rb +++ b/spec/decorators/pagination_decorator_spec.rb @@ -25,7 +25,7 @@ end describe 'skippable questions' do - let(:content) { mod.page_by_name('end-of-module-feedback-4') } + let(:content) { mod.page_by_name('feedback-freetext') } context 'when answered' do before do diff --git a/spec/models/concerns/content_types_spec.rb b/spec/models/concerns/content_types_spec.rb index 509120164..7b79310f0 100644 --- a/spec/models/concerns/content_types_spec.rb +++ b/spec/models/concerns/content_types_spec.rb @@ -94,10 +94,10 @@ specify { expect(content).to be_opinion_intro } end - describe '#opinion_question?' do - before { content.page_type = 'opinion' } + describe '#feedback_question?' do + before { content.page_type = 'feedback' } - specify { expect(content).to be_opinion_question } + specify { expect(content).to be_feedback_question } end describe '#thankyou?' do @@ -133,7 +133,7 @@ confidence_intro confidence_questionnaire opinion_intro - opinion + feedback thankyou certificate ]) diff --git a/spec/models/course_spec.rb b/spec/models/course_spec.rb index fd96137c5..f97cc28f9 100644 --- a/spec/models/course_spec.rb +++ b/spec/models/course_spec.rb @@ -18,7 +18,7 @@ it 'feedback' do expect(course.feedback.count).to eq 5 - expect(course.feedback.first.page_type).to eq 'opinion' + expect(course.feedback.first.page_type).to eq 'feedback' end end diff --git a/spec/models/training/question_spec.rb b/spec/models/training/question_spec.rb index dd8aa9006..d30a53483 100644 --- a/spec/models/training/question_spec.rb +++ b/spec/models/training/question_spec.rb @@ -111,7 +111,7 @@ context 'when the question is a feedback free text question' do subject(:question) do - Training::Module.by_name('alpha').page_by_name('end-of-module-feedback-4') + Training::Module.by_name('alpha').page_by_name('feedback-freetext') end specify do diff --git a/spec/models/training/response_spec.rb b/spec/models/training/response_spec.rb index aced29c50..d734b2419 100644 --- a/spec/models/training/response_spec.rb +++ b/spec/models/training/response_spec.rb @@ -101,7 +101,7 @@ end end - context 'with radio buttons for opinion question' do + context 'with radio buttons for feedback question' do let(:question_name) { 'end-of-module-feedback-1' } describe 'and no answer' do diff --git a/spec/support/shared/with_content.rb b/spec/support/shared/with_content.rb index e404558e3..6190380f2 100644 --- a/spec/support/shared/with_content.rb +++ b/spec/support/shared/with_content.rb @@ -71,7 +71,7 @@ confidence_intro confidence_questionnaire opinion_intro - opinion + feedback thankyou certificate ] diff --git a/spec/support/shared/with_progress.rb b/spec/support/shared/with_progress.rb index 1549cd3d2..9626c34d1 100644 --- a/spec/support/shared/with_progress.rb +++ b/spec/support/shared/with_progress.rb @@ -43,7 +43,7 @@ def start_end_of_module_feedback_intro(mod) # @param mod [Training::Module] def start_end_of_module_feedback_form(mod) - view_pages_upto(mod, 'opinion') + view_pages_upto(mod, 'feedback') end # @param mod [Training::Module] From d00b00f038d10f1e69f62e8a3d67bcf1cfc1b167 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Fri, 1 Mar 2024 15:33:08 +0000 Subject: [PATCH 06/95] use response validation for other text answer --- app/controllers/feedback_controller.rb | 51 +++++++++++++------------- app/models/response.rb | 9 ++++- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 54cf4478b..3a322de85 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -3,7 +3,7 @@ class FeedbackController < ApplicationController # @return [nil] def show - redirect_to next_path if skip_question? + redirect_to next_path unless always_show_question? end # @return [nil] @@ -11,10 +11,16 @@ def index; end # @return [nil] def update - return if invalid_answer? || other_blank? + return if invalid_answer? - response_exists? ? update_response : create_response + res = response_exists? ? update_response : create_response + + unless res.save and res.errors[:text_input].empty? + flash[:error] = res.errors.full_messages.to_sentence + redirect_to current_feedback_path + else redirect_to next_path + end end # @return [String] @@ -63,51 +69,44 @@ def invalid_answer? end end - # @return [Boolean] - def other_blank? - if answer_content.include?('Other') && text_input.blank? - flash[:error] = 'Please specify' - redirect_to current_feedback_path and return true - else - false - end - end - # @return [Response] def create_response - Response.create!( - user_id: current_user ? current_user.id : nil, + current_id = current_user ? current_user.id : current_visit.visitor_token + response = Response.new( + user_id: current_id, answers: answer_content, question_name: content.name, text_input: text_input, ) + + response end # @return [Response] def update_response - Response.where(user_id: current_user.id, question_name: content.name).update( + current_id = current_user ? current_user.id : current_visit.visitor_token + Response.where(user_id: current_id, question_name: content.name).update( answers: answer_content, text_input: text_input, - ) + ).first end # @return [Boolean] def response_exists? - return false if current_user.nil? + # TODO - replace with visitor in the case of nil user + current_id = current_user ? current_user.id : current_visit.visitor_token - Response.where(user_id: current_user.id, question_name: content.name).exists? + Response.where(user_id: current_id, question_name: content.name).exists? end # @return [Boolean] - def skip_question? - return false unless skipped_questions.include?(content.name) - - true if current_user.nil? + def show_question? + always_show_question? && (!current_user.nil? || !response_exists?) end - # @return [Array] - def skipped_questions - %w[main-feedback-6] + # @return [Boolean] + def always_show_question? + !content.always_show_question.eql?(false) end # @param answer [String] diff --git a/app/models/response.rb b/app/models/response.rb index 42be7f02c..4688a944a 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -4,11 +4,11 @@ class Response < ApplicationRecord include ToCsv - belongs_to :user, optional: true + belongs_to :user, optional: true belongs_to :assessment, optional: true validates :answers, presence: true, unless: -> { free_text_answer? } - validates :text_input, presence: true, if: -> { free_text_answer? } + validates :text_input, presence: true, if: -> { free_text_answer? || other_selected?} scope :incorrect, -> { where(correct: false) } scope :correct, -> { where(correct: true) } @@ -42,6 +42,11 @@ def options end end + # @return [Boolean] + def other_selected? + answers.include?('Other') + end + # @return [Boolean] def archived? archived From f47ee1204bcbc57b503f26db6c6460853cfa1119 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Fri, 1 Mar 2024 17:11:31 +0000 Subject: [PATCH 07/95] add update responses for guest users --- app/controllers/feedback_controller.rb | 41 +++++++++++-------- app/models/guest.rb | 11 ----- app/models/response.rb | 4 +- ...0301154905_add_guest_visit_to_responses.rb | 5 +++ ...nge_user_id_to_be_nullable_in_responses.rb | 5 +++ db/schema.rb | 3 +- 6 files changed, 39 insertions(+), 30 deletions(-) delete mode 100644 app/models/guest.rb create mode 100644 db/migrate/20240301154905_add_guest_visit_to_responses.rb create mode 100644 db/migrate/20240301164142_change_user_id_to_be_nullable_in_responses.rb diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 3a322de85..6e6b90242 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -14,12 +14,12 @@ def update return if invalid_answer? res = response_exists? ? update_response : create_response - - unless res.save and res.errors[:text_input].empty? - flash[:error] = res.errors.full_messages.to_sentence - redirect_to current_feedback_path + + if res.save and res.errors[:text_input].empty? + redirect_to next_path else - redirect_to next_path + flash[:error] = res.errors.full_messages.to_sentence + redirect_to current_feedback_path end end @@ -59,6 +59,11 @@ def previous_path private + # @return [Boolean] + def guest? + current_user.nil? + end + # @return [Boolean] def invalid_answer? if answer.blank? || answer.all?(&:blank?) @@ -71,32 +76,36 @@ def invalid_answer? # @return [Response] def create_response - current_id = current_user ? current_user.id : current_visit.visitor_token - response = Response.new( - user_id: current_id, + Response.new( + user_id: current_user ? current_user.id : nil, answers: answer_content, question_name: content.name, text_input: text_input, + guest_visit: guest? ? current_visit.visitor_token : nil, ) - - response end # @return [Response] def update_response - current_id = current_user ? current_user.id : current_visit.visitor_token - Response.where(user_id: current_id, question_name: content.name).update( + existing_response.update( answers: answer_content, text_input: text_input, - ).first + ) + existing_response end # @return [Boolean] def response_exists? - # TODO - replace with visitor in the case of nil user - current_id = current_user ? current_user.id : current_visit.visitor_token + existing_response.present? + end - Response.where(user_id: current_id, question_name: content.name).exists? + # @return [Response] + def existing_response + Response.find_by( + guest_visit: guest? ? current_visit.visitor_token : nil, + user_id: current_user&.id, + question_name: content.name, + ) end # @return [Boolean] diff --git a/app/models/guest.rb b/app/models/guest.rb deleted file mode 100644 index 4efaf6c1a..000000000 --- a/app/models/guest.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Guest < Dry::Struct - # @return [Boolean] - def guest? - true - end - - # @return [String] - def id - current_visit.visitor_id - end -end diff --git a/app/models/response.rb b/app/models/response.rb index 4688a944a..9068d5fcf 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -4,11 +4,11 @@ class Response < ApplicationRecord include ToCsv - belongs_to :user, optional: true + belongs_to :user, optional: true belongs_to :assessment, optional: true validates :answers, presence: true, unless: -> { free_text_answer? } - validates :text_input, presence: true, if: -> { free_text_answer? || other_selected?} + validates :text_input, presence: true, if: -> { free_text_answer? || other_selected? } scope :incorrect, -> { where(correct: false) } scope :correct, -> { where(correct: true) } diff --git a/db/migrate/20240301154905_add_guest_visit_to_responses.rb b/db/migrate/20240301154905_add_guest_visit_to_responses.rb new file mode 100644 index 000000000..dd3c724f7 --- /dev/null +++ b/db/migrate/20240301154905_add_guest_visit_to_responses.rb @@ -0,0 +1,5 @@ +class AddGuestVisitToResponses < ActiveRecord::Migration[7.1] + def change + add_column :responses, :guest_visit, :string + end +end diff --git a/db/migrate/20240301164142_change_user_id_to_be_nullable_in_responses.rb b/db/migrate/20240301164142_change_user_id_to_be_nullable_in_responses.rb new file mode 100644 index 000000000..fde546a04 --- /dev/null +++ b/db/migrate/20240301164142_change_user_id_to_be_nullable_in_responses.rb @@ -0,0 +1,5 @@ +class ChangeUserIdToBeNullableInResponses < ActiveRecord::Migration[7.1] + def change + change_column_null :responses, :user_id, true + end +end diff --git a/db/schema.rb b/db/schema.rb index 68822cd93..56fd7587a 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[7.1].define(version: 2024_02_14_175923) do +ActiveRecord::Schema[7.1].define(version: 2024_03_01_164142) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -139,6 +139,7 @@ t.string "question_type" t.bigint "assessment_id" t.text "text_input" + t.string "guest_visit" t.index ["assessment_id"], name: "index_responses_on_assessment_id" t.index ["user_id", "training_module", "question_name"], name: "user_question" t.index ["user_id"], name: "index_responses_on_user_id" From 452d72017e127474a76f7b3c564d3b394e02f8c2 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Mon, 4 Mar 2024 11:59:59 +0000 Subject: [PATCH 08/95] refactor: use form_builder to create main feedback forms for radio buttons and check boxes --- app/controllers/feedback_controller.rb | 6 +-- app/forms/form_builder.rb | 53 +++++++++++++++++++ app/models/training/question.rb | 16 ++++++ .../_main_feedback_check_boxes.html.slim | 12 +---- .../_main_feedback_radio_buttons.html.slim | 22 +++----- 5 files changed, 81 insertions(+), 28 deletions(-) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 6e6b90242..049f467df 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -15,7 +15,7 @@ def update res = response_exists? ? update_response : create_response - if res.save and res.errors[:text_input].empty? + if res.save && res.errors[:text_input].empty? redirect_to next_path else flash[:error] = res.errors.full_messages.to_sentence @@ -87,7 +87,7 @@ def create_response # @return [Response] def update_response - existing_response.update( + existing_response.update!( answers: answer_content, text_input: text_input, ) @@ -159,6 +159,6 @@ def answer_content end def text_input - @text_input ||= params[:answers_custom] + @text_input ||= params[:text_input] end end diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index 45cd9f10e..280700100 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -11,6 +11,59 @@ def govuk_password_field(attribute_name, options = {}) super(attribute_name, **options.reverse_merge(width: 'two-thirds')) end + # @param content [Object] + # @return [String] + def feedback_question_radio_buttons(content) + @template.capture do + content.options.each.with_index(1) do |option, index| + if content.is_last_option?(index) && content.has_other? + @template.concat feedback_other_radio_button(content, option) + else + @template.concat question_radio_button(option) + end + end + + if content.has_hint? + @template.concat govuk_text_area :text_input, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } + end + end + end + + # @param content [Object] + # @return [String] + def feedback_question_check_boxes(content) + @template.capture do + content.options.each.with_index(1) do |option, index| + if content.is_last_option?(index) && content.other.present? + @template.concat feedback_other_check_box(content, option) + elsif content.is_last_option?(index) && content.or.present? + @template.concat @template.content_tag(:div, 'Or', class: 'govuk-checkboxes__divider') + @template.concat govuk_check_box :answers, 'Or', label: { text: content.or }, link_errors: true + else + @template.concat question_check_box(option) + end + end + end + end + + # @param content [Object] + # @param option [Object] The content for the 'Other' checkbox option + # @return [String] + def feedback_other_check_box(content, option) + govuk_check_box :answers, option.id, label: { text: 'Other' }, link_errors: true do + govuk_text_field :text_input, label: { text: content.other } + end + end + + # @param content [Object] + # @param option [Object] The content for the 'Other' radio button option + # @return [String] + def feedback_other_radio_button(content, option) + govuk_radio_button :answers, option.id, label: { text: 'Other' }, link_errors: true do + govuk_text_field :text_input, label: { text: content.other } + end + end + def terms_and_conditions_check_box govuk_check_box :terms_and_conditions_agreed_at, Time.zone.now, diff --git a/app/models/training/question.rb b/app/models/training/question.rb index 1e9ee97b6..628f14f89 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -42,6 +42,22 @@ def free_text? feedback_question? && options.empty? end + # @return [Boolean] + def has_hint? + hint.present? + end + + # @return [Boolean] + def has_other? + other.present? + end + + # @param index [Integer] + # @return [Boolean] + def is_last_option?(index) + options.count == index + end + # @return [Boolean] event tracking def first_confidence? parent.confidence_questions.first.eql?(self) diff --git a/app/views/training/questions/_main_feedback_check_boxes.html.slim b/app/views/training/questions/_main_feedback_check_boxes.html.slim index 4345f6404..183e6a44a 100644 --- a/app/views/training/questions/_main_feedback_check_boxes.html.slim +++ b/app/views/training/questions/_main_feedback_check_boxes.html.slim @@ -2,14 +2,4 @@ = m(content.hint) unless content.hint.nil? = f.hidden_field :answers, multiple: true - - if response.options.any? - - response.options.each.with_index(1) do |option, index| - - if response.options.count.eql?(index) && content.other.present? - = f.govuk_check_box :answers, option.id, label: { text: 'Other' }, link_errors: true do - = f.govuk_text_field :answers_custom, label: { text: content.other } - - elsif response.options.count.eql?(index) && content.or.present? - .govuk-checkboxes__divider - = 'Or' - = f.govuk_check_box :answers, 'Or', label: { text: content.or }, link_errors: true - - else - = f.question_check_box(option) \ No newline at end of file + = f.feedback_question_check_boxes(content) \ No newline at end of file diff --git a/app/views/training/questions/_main_feedback_radio_buttons.html.slim b/app/views/training/questions/_main_feedback_radio_buttons.html.slim index b10cf3254..36f1a4be1 100644 --- a/app/views/training/questions/_main_feedback_radio_buttons.html.slim +++ b/app/views/training/questions/_main_feedback_radio_buttons.html.slim @@ -1,16 +1,10 @@ -= f.govuk_radio_buttons_fieldset :answers, multiple: true, legend: { text: response.legend } do +- if !content.free_text? - = f.hidden_field :answers, multiple: true - - if response.options.any? - - response.options.each.with_index(1) do |option, index| - - if response.options.count.eql?(index) && content.other.present? - = f.govuk_radio_button :answers, option.id, label: { text: 'Other' }, link_errors: true do - = f.govuk_text_field :answers_custom, label: { text: content.other } - - else - = f.question_radio_button(option) - - if !content.hint.nil? - = f.govuk_text_area :answers_custom, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } - - else - = m(content.hint) unless content.hint.nil? - = f.govuk_text_area :answers, label: nil, multiple: true + = f.govuk_radio_buttons_fieldset :answers, multiple: true, legend: { text: content.legend } do + = f.hidden_field :answers, multiple: true + = f.feedback_question_radio_buttons(content) + +- else + = m(content.hint) if content.has_hint? + = f.govuk_text_area :answers, label: nil, multiple: true \ No newline at end of file From dffbd1a0ee94494d3545839ac1ffc3123172bdcc Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Mon, 4 Mar 2024 12:01:36 +0000 Subject: [PATCH 09/95] revert prod creds changes --- config/credentials/production.yml.enc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index bdf960eec..3f9bfddd0 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -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--vx+cSpRyNJ59Lb/V--JELolhAJlC4w1e30/BNCSg== +FIXaVEfD+aGbxh/IR4rLub6jgFlC08Y71bPc2Z7VYyXBmox2hXOi1e1dSo12wkhj8/BkCaebdwL8u/LFBbWKHVHPC3cpzWuBH+uRdY/UUb/GBRF2m/8AMCSoEvsIzVveLNvoKTOnDHK71kSY1DVgb2WN+Auwj3g9W792lj57HTrCv92GtmmVaMFhTAKhFluIYjdAU28vzppVMLVitMUM36DM8+Iso5Zmoip5opJWYXwmZnvTTAzl47II+piDAHckXt+qaAxaJglRkqIWXZI0X9UNwdWv1FqAVtZTzeXjiJclTpBeXrWafvkAbiMcNG8os/PJye98LUcM8eoWhW7TmA7s63dpmdpvUcQhLAhnJ8CIjGdwJl7XsR4UJfnZEaAv+u2cgfDy+OQDuXy8ggGy4FTlaBrS8RTr0DARVhZirE4iQ6+N7GdvMCARUWejTmutYxdux+XBPOzbeaBl0C6FcWX49bm6fa3iUw/O89KcOc8kD2RGAIQzZKEAkjBjEz4K68AkJrk3a5cnQjnBIqlCbNxGkmbdeJqjhFeRg5lSPTDVudKwYsXFv4SoB0yjvd7fd9HTuIbuIQSG31HctJMop71C1KpbYYD06aOxa+VdnX69PQMWjrhytxw99gWNxVA5RodF5f7QB7X8RUR1zv9SVNckZurG6oH8ZVo0TkoquTtvIdG7IkUmkjBCkTyhP0EEGnV0kgln8i/cql4ovLNl5Eqfv+58WIZlaFORGV9OKtUkp/EOtgxI0VnJVk+Yz86Vly9iyFS/iuwe1uxDOBtVXDfbzve1nDbqJ9s2KE/hK/YTroHGCPuiJ15axZmb4QDRY++0QM3liGxCUd2fjxsUV4EmRb7GDJCVpnmRXfemhM3N9MoxgMvUfYfBD2ZeXLMd9S4TSCizQr4LIMaaKm1m6hd0lF2aOGewkWeRMvE9fPkpS7BybHK8VPnu1i+EXdfkR/avyHI2MMuz5RQXS6e4p7gNtZZQpCEwu0dEyeXz/D6BD+MD9kU7bmKu3dxpBfaieCcct+Ww3XPedZnvcgrEeCJQCX/Azd+7CRFkUtxHUa5B3nUjF37+FFXKuXVb+jeGrA277uO1Fdq6PCcImPyudK55LQnk0sTR1kyKk+1TuSK0RTsFVSc7U3hlZQVI1U4021ql4nPlZxdktnoufzbp4WPrWJetGJMPrB/KJfJ2NsCbS6aCkfDs6R8fn7VX0eJL4YYntzpfl8zzNhK94+FppeZSxQeXqw4dMBFynJQk23oY5v/EVdEGzc40SFUZctCto4JvvISL6iq8iGLv/NfN1MZ4AqLiTjmA5NRb/3LG/1ZwAiD8j2kI2soij2lfscPMkrT3NE+7x+fY1umIudeQpUFRMlrPSfeVhOrANl/Zpw/ejRmn9YOWZru9lR+9+45EXeEWzsBK/aRCj0dubxaAG4GkOCBqVwv1p5iE66sWnQRE+oKKJbN/M14JlTyB+fGV0FZBON/sW5+Kl2TzDvMzW9AQergtYWjVK9yHMMEPtB9DIx4rFsqvxFtlHgdQjyF0Ju/Ytj5bHyuIwqfoouvsEDqbo76qva6gGBJmDOyeEP7/RsVGRI6pUSeSTDJMIo/96Z4OHy23JFTIoYjK2thLIfniT4XQ09SiC8ycMGfN87q6SgrMw3jBpbvYYUhDfajC7154972KniFsfKvJwiECmNk9tPEujDtqqMrD9vZ7yrhBJRhMrfikoRwHocX2PAy47vv/tsglhDFTTT7Ofbgfl/DcDIAhI+FhmHsX1MSQud3bx2+FzgGtGHj52PWBshmPRP6+D2uC8z04/b4sT4FgE8jqUO6rPTrJDydmevZafRPDWyoLvNnKQIcu3Jqat1M968IKKgR5Ecyq0EumkABedxt2UTt+J8FZBGXUhWzzhc/jrMiahMQolgchYe5Vx83efgpIOErT5ZAjMJhWnglSuytkB+ZJybZpFRMZn+JQCZvOuJVT5JSqJkBChJS6BQ/eEk5bSe4Mf3A1hyMjXPl8kloKm6oug60xBfOh8TFDRCxczBJ2v/7ELDtos8TC4mvq+ODj5QkHCG+8SPExjvMfuQnqu5IQajyUwvYLIurFpHITN4dHStNpBWlCQsTPYa6mPCwJMumDORpyB+BFRyypc+luoLKnEKRMhTErD3MHSclDadeo+Pf3j1AwE73TPw8WAp0Bh9XNsjfIffBVjRWU5gL6yk/bhshPsPkLTMgBHvd4Ok7/38Ct/TlhyHrnRGiGnunXDR7pLIJttAZZZWky45NiD3MAwo8CScJOC7y7Sp4p+n+5r8GWWAri86oTXQKCYfeHW6Ap1OHeZ/dwyvzaFlw4YJdSgOaJYuuoK+PLwoqnvqVIvrwLx6NPeP8+jYgPhRkgBTiEjuSe4QpGVToI84s+2Ksa+z/8Ia+hl2e/HASYuxDlaaXgcnVkIXSaFW4RwggssPIpAN6ienULU9e/VWWa91bQFHNtWCsWQKo98teHeNvr7S6jGOsfEQLal7hUbzN9EipiUngtmGEJXd8/wbyhjojOxD+ZpA30m+I9KqRR+C/IiDXiLFAxPSlQKOP/DE/6NtJk75eYxKNBjqGYrKZ2vDeyq1TqhO7z1avGVUPzH7soPhWAw+Bj32q/viUze3zxxP4fpzrpLzOXGMWeiMGqIdEvAf5JvhUBD5oCcYOKUk9D1uKbXJ/SYOwRJBFRVzWf3DlmVU4MxlLsvxlfEOUm1zckDkTr/6JCGcemPmKypxqP3R7xF9ZmIgADYgkZtkcIPMRJQ4VM2WAEn0gHC2mk11mBgw+AikwoHlBaaONwmjHlUnyaXn/Emp6+JB9TKSLaLrbC/sp+ChycGj/NbvD4Oea0owruF2CXxbfZ+RAaEJlGJwxrAvQTj9/Rqkwj1/Ux+W9vaoNJbCP7IKnPyltrpXn9MGG3fbHbj5WalHYvyiZcO+0sulN1kNPjMCFO951iDeskRpe0XxiJy10/IYAsmpY+gN35OgL2Yw6hg1NxAYCGpN/mRtZprsQUItejYHGneQVMMPrGfBQLVURxZ/mTr9NWdToU/O7RuWu0OfNL+92qMcwGF2nYqOIgjyymtu7mjmp+kR6NQlvona1s8jkARtx1eA5x3jG2YrLaaUbwOq3pa4gdeqAGWR1BAzHQvnkYrP37+MNao+rGC6TPtjQXD38dPXU/rNKw8eo5KxJGcy6Wpjf9MqAIFt7T9tD78X0k8r/oVPaRS3TuPR/vPH49GaNIxTdCptx6NTDYoLaOL+D5aaLmaRDry8ZIDjYbL/z36ySB8QObamhpfFYezc6OWjgjfw1MvzcXn6Z4JH6v1qhocywO5TZ+9r2iQOq3Q/Um4raA4dY8WYZDtK8kL/UdzwBxFeHh2rhdbqwKmuO5m0DjCo2AIODn0hBVWLAa0hU94zfb4EyrP1lCAC1GxinQztkGVhmX4ep/JCNnTsi3XN4hxiHlaPkgIEilB3GKDH7Bad8I/leMN39ThG7AtNSOUh/7SzDiA6hOKxkPgyYS7nqMjpOAr/hEN3s2N/1/BQpS3EMVWJGatK57djjXefhg5mMd+yk2PYFSJU1Y2LvO5Bp9FRG9zKd5hdjlPWmaaZQkiLQps0w+i1VmJNLVjxQZ0Nc5z72+w00qN9DZ0mHVdwo+o8f8ebA3xC1nIne64MJiIKiBAB1u9dwEtT0uU2yDif7jDYEzk2d/3O29QrV2Rg59l+8btLpAMk1M2HhInfvRGq3wfgqdnMaOCmYJdMHPVk8qcNPkp2zo1MF7OfhxgPK+2GJ+Nj+EftPLY/biyCR9RfUj4s2mNkHscx596rHWL6oziOQSfatDc9t6+S48akSKqQ8Yu4c0Ghz+tmefLQXRPuIq9V4klZHhtNMo70slMU9dOGfRdx3pDxxmg0eMizAVhjoTFrn6pGUBAZavI/kOh2NKEe7umbzcc9yIi50Ozsd45bkYn1h7WaVVHf3H+sDlnyBzBX14nf910iUoy6gbVthjoshbqetG9dCFYc6MA/VxvNfkIWe4OM0dSYp0BkNBFfiwCoKABR4Q9FspeJDQKQ4KoV65MudcS+7+YKpsOTdQwVrsVFQ3aBnyJpw7yttAaFvK0bvzZz8wGo0va3g5zhHAUOo0E8/3FJ0p0MbcAVf/uIxzMv8kSVr1K7SX2bJST9dhPryPOWmNViZKsCqZKXos1LdgTGFJNBWdWHtE1a+0r2gmlRQRSXTwon8rwexS17LZV6UDvKKYmV8o6+9/nCHdgUycZRciPd8cXLF22/cPKWOtt5nBlgz9Rz8J5ZtOQ3K2Q4WNQEtyTJbyoizhBPBfqnUxKdKXZrNHxttOZVrfEtpoDxA5oqh9K5N/7hks9hJFUH3BTZ2Xj0GqsBkNshdY5stG9ihdmYxsROx3B/Wipcyp+iMra+SOTanNKYze3VJPUpoGF16+fDACjd/4wSOZnrni5UG9ZBLnHoWrpz+pf+MYpvT8/AoEn4gp5kNulzOVBbcfcmTPw195OfOyyrZrzVK9W6U4o26Wj/KYUHZvtM77dvVnqmxrS5ASVHDv6la3B8asKGoT/nKUZuaYdt7F9wQ62XirL+kpV3e0vO27kf+n2COwFELQiWdDpET3+ZY6fG3x7BAv5wUudOYnzJ4GtvTq3p3He5dE7XLKt8swzuimi1JHeTzCQvNjKNGjs4rIdXGB8BhRY4lmRGBbh3ac5TbQom0RjRXqdsxov9HeQb4KgCnkSoEbotM1XsBH2t2y08F6UvBWWFDC5DIjGS7cDINXmOo4/tR7IaSqBoPd6KZP4sykpGwjlxsOX8qKhg4AG8HYb5PBP7HSDd7u9lPOhsFWBszX8Au8EM/WAhwbpjQzLJ0pX88CqBDYa1hXF+rc0EEZxRHaOAl3NCfVRk5cvT8bR1Fxg1z9nWYq7iKo5+ZKdCJVufBuaUtgeFPlGupVrntg7OLBWesV1nHaz3q5KafALYi5iKoh/C/IBPowgYAWG2e5quj1hYR6rX5R8lHYmGXsSgQIDEyCrjFmk9K8HAHFlaxNIklpM2PSIKGKK+4dWVQGR6MPDYVhh3vLA7mjW5F03hNTsX0HSEGPAwbPp+68XoEN2h5Ryo20BfOBrAGGsoHNa6sAmLxAa/+sl/MJUdpjVMvsZbaZUs7HwEOsWBE4PwVoV9BRnXOpLn4bxHI4YMs+eYPIAv69RcKFd6HReWfJ3trAcLUWkE1q7owEowEnEgekGG8oHHhShZpMKqfjl/zbcuyBH+o3M0FuGL9QlXQ0lZIY1jJ2Obe+78MZ+S0RXwiD5IJi+kw0HO9Bpso1RiTEwEYKUmd189cl97eOk72Msv++lZ/yNmLMe0J+KUmV5eAsCp47DOkGGF4n9sViFY0IyDtpshGbmpf2lwbEDyWarGny+N/TzpfNz1VoZKt/eV3T/2E4vQdt+Mhlfu38gmqomXg4Qg75LgqR07/x10AN5m0EhQPzXHh7mk9z6nR3PonK/mq5iegwwhftY6/M0flmNXOUbmsG6xNxKIRWhBN5azf+94QYm+tAU3qGCFVOthLobLEyoEJB8jN3Ope1vI+Lx3ALTx8hFotFhtz38kL9i0x5iPJSW3XcDvHEhWKwTPd0TWqTSFGPDgIiphdJbd8XU9Q4FuNBbNmWUxQA6qNqtCvjFxBIf3glXNSVIPNb/TvZYtK0AAYDVz3pMGBO8SKjzXuJ5ydOXVx4LWLcHoYBvAA8fi1VnphVC0U4B2i+1bf/QKZxxGlbVguXnm3brqXzgZqTyS/qMN5vLu+rarii1FvvM3fgmLDxzAS24jWA56V4S6And7y+XiFc8bIeybIsoEizSGWdRNAEyvBu8WOykQaze6t7OPfrufzgOUtYJjj2V9DJ072As4PahYMFsXMV70kE3GeTRR7Q/LB9OQPasQQ8YDd/njqO3+IeeZNmfsTIn3POxWvoSvyI4L+g2aTrryA0DpxHIEujzsT3VC/Qcwti2iuk59T1nB9Cc6mzBXxXKilf6Q9NPA9lvv2k7N+kfhaGexUCB5QOADknNrNUtnpuICFDqYSbZu87nv6MAUvZtWhMabIOVO8IR1ZfPpq8WnO02QR21fYWAhZN8ZMpt6iMW/t6yTHcIKHrRnM1sdWKoxJFMdCJb/RDyMISQDtT1oCPWvRk1GhJiwjizQ4IouD4Voq1pug1y1qaZebATAaKX5FlUBKORrseH+z5b28iglSqcBr1myAJ/Et0nKVrQGqyubdJPHuBDN4qE4QpWfyIAjJr9MEu8zxXhOmAe/IA0WvFmdv5w4BHbMkcpSxw/SoFNoh21D/Jng8S8UbXyLMiguxqrk1OIN86d/hwFogrv2FWeJz9vSOJX2TOiqziREvghFyank5B4oZVcglNFFJiWBAYBLTIA25PEBQXNN6RCA8XHVXD/p3hvvSehRoSbndC9HeWTzy4LT7gzNVecj6eUQJWXlb2RzLVJRDEZLIq0KzAuoOxWdNnNLmT9xrHrljSEbeUaHCB6380HFI26ab+US8w+NJmDRki4k66GbLAnvhmMRQJDq5h2/apW3kbnha4O7efatga3B5SPAyLklSlyt2RBzUcjn1N60puH63dOrd9sil//Z4bY7rG/Vde0kSOYk/AXmXRajNMxSGsNWdniYBij7RaZ8PwR8bDP6x9Ravbh6fBMzPMiEaJeR8a3syJprmHWl4jYe6V2dgcNgX2RtTRO1lxHQJLhvwlgJF49wxvnRgLerTV4oMla/0xAz0nPr5E0KtlNN6xqgfBW3UwGoEzUgclurhAk3rylReGU3KDLFb6hR83pGGvx9GNjVMseeKE0L1szJHSWlEYaH7/7HbURniMmO5LLEgvF6fZFb6EplGpgL33IA60/CBxuZqKszA9ejuZhhsM+NfsKLZxAyqJUIqtDoG4isjLNLtVsnvelF3uuaLTZwOxxmZLuT1uWhjfH4kZDPaAX8uSTo6jf7a5WGKqbeVMRl15eFMrR7ic++EI3RuPe7kPdVG90hTnCw0mrwKjdmC4h5L4encbfPljV189MARYzWMrIp0bHUNPi78YpWV4Wa8OWHbdFOlJxUKA0W3d94gE9qHkRL3pLmywiKu49kfMXyfjcdm96Lal7zfVcVrfW7MvixSqAmRBuzENE18Z9BAcmbfNihy5mwmL07zllgl/ji5pNs0PVfqk0ueIU75nhDY7GJRgxXcl50mgQ4YpXLeLcIkddPLQNq8eIjih7/Q5Z4w+f0SKG6qpv/r97tBQbaLPZwONPniB7Ly5y+lmOAN/19Ejg8VBOADBDPi2oBgbAoZjFXcmSNRs7z8zyK+mpdYC6Oq8xv5U/p4UpPuSyscQ7G0g04IJEFyGt/F+cAap1g+XMialVgWmy1vLUYf2sY3miZrUR2ntfZ/+WKTEImOocF5I8fv7sLVeQatnFcJEogsUlbbnczGec7uZMiO1rSWrjcvffDXraHOobmS0AsWtYG1N18cZvqyibvfGJbWXJnr0T3akCXvZWUyKzC6xLqsmYPMzpr/XJKyRD8axJ8yFIw7ZDdQShySnrAqsTqsGYS9nQLCnY1kB09CHkt2a6AIrVjghRASMaqmm/rWJOviRl7y8M0rEqVL4D5/5xESTY5sGdr5jZdjUoTiSsPOnXGybpk00keNPyVITIW2d5nBwTHnNWx3XP8GILB5DVlSkRlCHsc7Ozm7IbeAMnyNFGG9HnIGyGYTtfTqHi6fd9JbR1UN+rs+XVcY+p/ZJjPovsQ9TKw7oaV8m2ouVB0LhmQOz4s0oowOK1rymjXvq6oz+IzW+DamcvY8olZkxtMWSxj1+nPA9pFBx9KbUqWYsasK9UPRbwjZsN0Bcnwff3ctWBmyZzvlaVb7m1yxj9GnSQ6sGF86Z1kmo8tw3WAdkTAEMhgd/K3QTex9vQc8isMea8wayEo7Vi0YHTbVdAq1Yp7+A4E2nd//WI2J2/Oq/Pm6T1+B7D0W1su0uSmIHgFJ8bCe8oiPwvObYDvEscd9nmx423F0ki8SDhSr4QMUiodAUpwwUfV/0enkwVXpwRhC8Q+mHQYBL2Itx+FZRu+bk1pHbEzOHvucQbfAmcP3p39J7QNTjz+4NF9uvGBUP/YrFQkiyWPjfv6/3YBPt8IQeGdZoGzhto2Y/9tVM5oUlnMFVjXqWDEm+9eCN8/ZA4wDR5ZTClIWFZ+Hk8tTYFb3VZ4g3ly0pulmDsc6on0tMCRfCBP/m4+/pP2URMHIbQo42mxPP0LIw5f6hH9Jjdybq2u9sEFPNPwnLhW4h4MgU4fOMN4yR4reQU3q1QGw68xle1HATkATMvmAAfKe45xfU9DwfS1LAY2RADEoPa0K8aVFychLX1DDygG5WUkiNNSq/kqRqajnmglHxy0g7X7qn+5f7NfpTb4K4jSEti6dIStteWTPy2bFoblTX7ic12zbDHHGYqnoFqtplvJyuwlWZgDYCXZSXUHH4ZYLAkBgO+qeG3LoPDBONJXL72T98Z5gSgPlLh7A4q5PLw8X3NbDqiZ3zjp/ZMPj25WuRnWWqjb6D6C29CmUHgyMm9onqW5UdEp24ho2WFV56DnfTsnUsM18wVQQRNGOJnT++j9ZRQ8I1QFwmYA6zmBwXnExU+6bAm6+zSWBUiYryjd/jKKrC0fmV1pGs0BaZIyXjJV9ACGIP72ikOvi1dbTTESx74UWRdDhdJ73RvMacuB1nw/MBnq7y61pNoFzPPS2sDaiCTFsmAuSAhhjVnR8LHLGvC4B9DUBR/Bv8/KnLFpVSOPrEs92T6WmvjdESmd8qjdYW30qPOQd8zA7LqzaOTgpqbeczgSyYGDg4qTQwTvbfebHpM/klY1+ZvbaexppOHxnaR7+ZHt3Xk5DxnAe4yBEM8ja0A1xwcTbiOkSelgpGihQCUjqiHTXyiJcTdtrjCvEY/tHsRuwcO4md3W+jO3lrEWhx0sMwY5AtemeaMtZRhIzMdbpy10EQnp4g6WhKNrMqD0s/pTZxiX2lfLkti4w6vu6W9uTm6XveGGlRotU1s5EqBZPFan0l3x8tnhVs6zd1a0je+LcfQOyaeh0xGff3GKnpJTnAmqBOaHjmi3SSbH8GXmuPaihhfisPmHzWpIXlPvrc4YUrN12X+VixdgA2fn//KaAdKW8tlddEZ1Tkx84ntWJ2RPGVWhucGO4o0sdj7s3RXIzFz9Itb0XglgXS24cJu+Q/GxXA++02BATd0BFGMvk6Iztc0ERUHXObSA8aRj8DrRathVBETZkIm7TpsoLBkggwe/FWhHUnXhkqNCtsjx7co2a3Mn8RjSMdJ4IdeVk2pIlhsZq8bWukGKfHOECcUmZpNvZCiomU0lkkc9w+WSQ+JgLYXZUL+3vwRWoiTy2hsCFxXiNcz9zQKk4sYkwTtKi3v2+g8+ymMemcy3CfNzug7hAZAVo9m8kfMgg7sSWOgasNm6ERR0mRKYUnonbn6vDjIOlwCubHPdwXylOuDOW2aq7rtUMi1kL6bYJ1DPV3N0PbRoXjuMpnBhWFbzeIJ+Is9yM+5gR5M/KedRozq1jFnPbU20bO/a/qI7Jf2ou3as9188FSGj6mfBlcXP/xKJKVA4GPBJw/ZDKU+l8vEGo1kCYlwmSOWF0e5Yf4Ca0eHqvoz1waR99RJXdk0jXyOdIh6XYbzRA3R9t+0XeTtMIR0zfKgFQxOoMtvELRjxp3PgHtVwP1VUAZpBUQoSXr3dnevqqJbIbSSTml5zgFZQ6ei917Ex0uwd3QWDstH70FynQO602LjrJYgYuxKXTvHm3XZ5C04/ICxIYCnx0HNb8La/XmDDnQxvjw/vD1fJa8f62b+fQ+8hX0Q4vpYAMYyJs4NyWaA6EbIO8tduQDoGS63s7Th0MfwTjU4v5dwfSpd9M1zaAbtLkI/xa3wroM234SIpXARW0yyfmCL8HJ5cRTjD5p62N6tiNmwXaYZC9UkhesFr9234U2P62SMnhDQRvoQtJvUxa8C1/YXf7bDDRCSttv/d43QHZ0ZzRJrcVIYK4eupWdpM26aRbSIfCJDKIZH2yf0HNCl9Qq36FUBj8K1ie88/4lSs83kV/pX9oi2wN93gtKeBArZ+q893uBHhb3KRloQZg58Dzp52EO6nG/Y4zs/V5HhzxnHbZDFY1VZF0Z/Vk6PQQCvN9aT1LPYmG8V78U+GEIoUFtl1ZrkLrqQlgHaZ7QYI8KXWfmfG9CssR4OoiKCgJkHDLMpqucXJQkTmoibeDVmUL95EVKZ2YNdDdnEk4HgnOMHnDRba27iaBUE4mWEyCvrci8QwnCdMWG0yihk8tdqecicdUMp4j5iLkvkpUVCfSa8HOaS7rXaO0a/F/2tNiikR/BShF1X1GnBqDtsLmNZFfM9VTsz85TIB+QT10z5goraXv29xcNNGhT8JfOXvwwkaRwbMxfzIDgxSkcgMb9LxKBNh4u/apFgpw2bUFpcGSQE1ZYoREfjQRewS3prCXELF5gywI+2DUnZphaS65OQp57F6M+Bokd4c5XQeAW/ohE7QWslhemWuv0G14gTFtuK3HaYBvAxo8FO2iVntE7+OjUj+L81k3KFkFzOhsMgG/WxD1GnEUcXYglUdg/2CvX2Jk5SAzEXJEy2soLGPIpP3VNF1pkIfMRIAawx5LGARJwW2kOjcw/xkYVDBO9PDbcU9ZiAAXJXRYdHWPRfLCBWB2ZrBJAN+oDFsZqoGC2mjo8cVFt71VYisjx/9w6jkFuaqZ0+EXtFnU1vgLvTzZwmSNbNjO6hW9PoLnosaCdcqnYSGKjUqEE6GWpGTcEq1mt5hJYduk55VB2ek82tSZZb2S3rxLxxbtJlpYI7s2no8xGCdPNSryU4X2QP4YW9Ek/qXWPl4g0GkDYMw9LmHrF0vryE0OIvVdjRKo9N6+hAl3AKSPeN8bte9t3p12H6+G67Y/Vcf1MD3nLA8H67pZhCTNhBKUkBPQGngvhSliPKgUZo+jGbJS+tOy6Kyfy38vv1YY16yt++FUTsJHVKPqLyhzEMKU0vRlw3krcumvjfDov/FctOKnRPZ0jW5eluTOJK+DibYOonk/ks6i6TfO+PVqKC+6eN4Lt1YkbJgUjTvNRzmUKJjjvKbUzRMCDw26A1MWCzEg3XsSsOr9L8xm4nJI8t2XZWVFZOPjt5yPthREBq1yyQ8UjLD3C4IAbyNtwjwZ9TZWES4tCZCNtV7jZlfRkULbKYwgrtbG/PAy5v1KrCGTCL0C+WjWbR03MPvC1BxGuHYoOkByVqOuaLGN8d4GId42xwISa6Yz1m38x5/Yyd8vYFUb2ejJJ/rLajBwAwSbq5ViRbJIj3pk+3Wybgf5TO4FNj8v+zH/nR0DjlOphl8rYSBgLjONme3bAyMgwmUr/AorfpaUHaRz8DV9w26iZcfa0rK/924zwep6hB812WxaZZ8v7OPbrEEPUi3tXQhCb3UX8gsigk6oBzWyvFtjQYe3vObgNnl/Se2+tojajXGm3O7NfqaivaLOvutDgEFeJkNoWw0Hl1YQC0JzU17unC59FU9qUJ6pLZ5hvgdKDLoA1tSg1ESfUs679Pp6hmOCzwgST2ZREzQYyT1b960q6CRI6ksI53+PFydLC5RpBZh/JbNE1Vv9MLbxIl4EvlyVbNo1TgyWtlePzaSxT9x5ERiwP0+SWltpBlSHtjZr3jH4+WZ3b5UDfWsCm27j+BW9EQi2YgMysPEdxRwRDn3PtuFYwSnduTaPFH3OX+VPY9NpiSpnE1nWSKEyokoJxCGagPiIH6XctzBQjkBtAkJ4fCiquIqz6ft2Uo+7fhZ+iP96K0ivghWGCBP3mUtNGUU3NW/QL8aocD7+0WVfaAQ6ulOCcTVKdg/XGOFPxNSImKIqPZp/kdYkUOTHpKq674a2OVlrpuOmx2QwitlXEVQ86gKiMxyicUb4yzXX5crQBjmSv6H9oNyuFTgPhDoTU9Kwn3DdJPK+64wr7QDXnErRMYI5F3imQR1znl56VKNOCMsBx2NTqJ5IZbZfio0eBAZOhUufF5F5UYGz7vT9tNHMifhTvy0HCDI8aqtM57YKyNGiaPaqbduqgXumiQFSjE302Zb621utgSWiIXTdBLuWKSXMOdVOqFBRGc9gzyny030nKVzNEr+pwwWHryXTKDM0Hdh5xRWuuHSJFRIhoVQ0z72ub74cHNc2iWs9Ap1f6n6sj/2CUuNQ5HbC8MadO+6eO3JHfAzeGixkUUBBNLxbcnHLAPcGFLBfTwzrEbdIADPy/OyroRxnSVAqU1drP942jciWToSXZFd9vX/h/DtxOp92tgSRzZ2thQEHRljWh4H3JmzrSoXzcq6Yhs73Qm3OKwwhs4J2sEeWew5NImdiKdSYNNDYYW7mQ5d0tRN451yRZg/0zZz2e87zh40Pj5QqVE5Z2K1JZXylhtRlRZ32JbN5Ie1tlWprXM79GBJ18UJUsIRJEh/LjlTBl9mxUsoY6bQVr4OchfzOpr7t+LCKRLBUNluMBOvUjg3hmq+QhBRvBu+YqZLkrKwXXgjOn6GU88o/e5KrdlALa3W3WPQcin3lH0vZ8Q0y5UhP10dfqj5dEzNiKAIpi2eg8JFoKaUYKEMyrZJPqNB8y8D3zM/h9WMiVe0Dn15p4RHNZidwHGv5PL79FVIeBXGjul9W2MK0gzGW/FGFxEDbrzxSExsVu+hmonhjam8JJdMP/2bkb+5RegTgTWN8AdKK/KpJSrpnhV9k/tsBlnQH36fY7ZDr4VaFkxQ57KXYW+P2s5Q1EaOa/EvcqjmN2h9WZWhCIVxiw3JJJTaEg2NWWDEXHrzlAHzKD4NsQmpUpgIFZA83QRqArWPa3GxO1nEIDDdY3RX9jb6F1iqti/OJ4TK++RGhRXSHAL1bx7Cmp8oIXAoKodLjvOq5zeIEi89iCCgzpOIg+8JA1sbN8g9hiNrOf5JWRRVTXFUkxfaVucKz1D879e4TfcY/36l8vqeUs0TtV19XGUqwBxgEBSpnVIgF/CFD+5IDe2HxQhklt1VFWYN8w12QDUP6omY6eTyUiBzctuNG1rVX5tC6zHoiIvFn7LK2ikOR8OE3ByjzmOr4Ihxc1x2qDouWseEXQRpWJOiOQi4IEOBR5KUW3wlkomx1keniFcd84w2gWzAdLXwgOE8WPTxU9A9sfA5oCK74WEJcISEWHrWMdOHfqRcsknVbFINx0THGlERt1E6lTVNowhsU/TXcmeMChI8Q03M/j/RRSA7ltYBvGJut8GZcVGlLvEstw9KbmAUw46lSuaW+Aw/moH1baa9SZoQrM6jyv1GN87RkK14OiGwyhkdWDu5yaL95xOiroOsIUwhU1E24niF0hD1MPnFZfi0Yhmu12VdyBAOqagcGx99t45DSQ58ZVTgVt42XfvRpy4QMCx7uXuV0TjTxxm3PHcngxfFGfmzcRWjTmnOWk3r8FayptlGnLa3YGrjJx85Uq+8URLOpGShLwhHO5hqSUqxhtKQd35ESstSPDGO/Jrur3AePkclD9nwebsIusb7qdfRDfx+UxrrRi+4MgoVJO0F142gYPRqwMN9TooZ1eSUbCw4vHBHQVTIAmNY8+9xRuecyMoi2+CKeJGv5EVsXwzo9bpKkx7LgOL6Q9ZnM3N6iOStrUmCbYvvmsId9OImXcd/2KC72b15iZcNUVeZ/ANCf2GID+fRklEy3tkJtwzpuBz2Ku1j3E8ynbCcyPKqHgJz/wPGZqh9d3C6oQL4svY8ooIbeMNIkjA8cg2aPh5JBQLf5X/eccMZbq2F+I5u03RAjrKhXF7fzJ71d46HddDgtbO5pnE2eh/oU6AzeIN3xAMvxtqCA6fgP2+9F7yQW3wDSjs2IPA1KKeLsKHWVFo17ZPK72OTlJ1wg4liB1MQuGxAf0EFmhY2PC8Lv/p8mh+GVYSp8ReDMjnLrRMA8PW0WCGlUbfAwfpMJPeCewIKfAKussV3X8HrLCuIBjyG7Gfgb49KDD4Z7IZpDUGl4DvCRlqsuNwFCUAM0DSojgQIGKdFe69u22xGviQJpjjerRHQ0c+yR9w9944nrqmExdMDWNgKxlFOKzFuHvNNoobZ7ctBia+p84WM6OHT6qdu5yLb2rwRV7hYhsaN1lePMij5yQJgocEW8Ymhn4q44R+dfddS2keFBlYY+Lx/oQOxzqgBQ22QhorG91l8AD2ZCNhU7RfXFkgPee28Jrpyt5E/T6UF1EnZZ1kiZCwriJlh1jLAnNV73UBm9kUXJB+s+712ni664cX+GLr08xbGh6RH8Kfrdl5z4e9X/pxTH+zR9ibt1E16LdUrD96EvZoFe8kqm7MrW5JGOcJLMj3nZ5SHSzZvBXfw3Cigfb16U6tWlVgGO07chFOQ2x9q42izTLpsPFpLGR/90M4/KAyWcqT1O0uRJbNIjCrRYZ2Oo9l/7zlHxXBSBJZDjEnSu21dpndR3LgLd+55XgNOjN7ZgnNNbWy2/1yFfHyVMb3ICzhdsleA2/sfl8b8hhEDmBxf3OwmHhGYNricMNbIwwwXsA8AxwUR6zUVXG5OgPyuiIG5rx5mppYiRFOZZgV6s5kdwFsLz6OlaZa2wPyM//ask4ZSM5haiua9HOLeaMJWUEfjnD8+z3KhFATtJpiWLcN/3zSsnaKJ21IAgEXO4EM+wf2svN9lBkzI3p1oohxZDONftiQiR6P0DOa+JCmtdfUVKqbZ/u6/exHvkPCNRwOn8jc0okVZ0eItVQbPxmD3ep0dhav2noq/t0oAchIHpeE5pcqYEeAM5UfN1SjE7383xZsPYwoYbA1vCRZHzOr3z2jdGgb1UBqIe77JZuJOihrlmm1DjBtcZIhyUiJaJjf9zICBvaylu+8LugipTfHAKbI1pAN53t4s8+g/Vt0WsAbDC7POEvOsGiswWI7pDJRKP/XTDlyBcVoomj40QvkJISVQp50hLzwB3oOeNArdnd/OtW5zvPEgcmh0jNbNtmsPB25RrSsxLUcSLXGSbB1wrRQf7vJTj0ACn1eQRTvMsciw/hcc4j1JMatZsmTHD0rVw8GtZKHq6wGC++IJwQPQ9V4/TPZMVmCkoPIvk2/lzh0s2jYlN42+rBrNatbJodPOA27OhSQ3xJBs3330koc4FmGcKna2qLUXTH1xYGUKEMprtVBlZVv+4/E2QNAIj0DD3JLO8zzeebY75EdhlR+1n2hYRlahCuT3aFdQnYLY8iBMoo8cIHUseNaDBZQFcdgIZ9gz3oe7fxSMKT4zvK2TUW2L2FhxNfyY7WkIjwwBXrWcv+wVi/6WUsRYt7s7qvhDjR6FlH+JZdCH6ivAdEMQSaoBSxn0oi7I8POw6+46XduTncb86CCImXxsAbKt5rM4Eq/tqXWrztGRDOLrTqemuXGCganQmlIMlb3DyDzI9IOLEiVpQ=--vx+cSpRyNJ59Lb/V--JELolhAJlC4w1e30/BNCSg== \ No newline at end of file From ded557b1c20d559a0e984cb0c5065d5555c247e9 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Mon, 4 Mar 2024 14:25:25 +0000 Subject: [PATCH 10/95] add partial for free text feedback questions --- app/controllers/feedback_controller.rb | 16 +++------------- app/forms/form_builder.rb | 6 +++--- app/models/training/question.rb | 7 ++++++- app/views/feedback/index.html.slim | 2 +- app/views/feedback/show.html.slim | 8 +++++--- .../questions/_main_feedback_free_text.html.slim | 3 +++ 6 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 app/views/training/questions/_main_feedback_free_text.html.slim diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 049f467df..c571f0c94 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -1,5 +1,5 @@ class FeedbackController < ApplicationController - helper_method :previous_path, :next_path, :content, :is_checkbox?, :is_free_text?, :feedback_exists? + helper_method :previous_path, :next_path, :content, :feedback_exists? # @return [nil] def show @@ -23,16 +23,6 @@ def update end end - # @return [String] - def is_checkbox? - content.response_type - end - - # @return [Boolean] - def is_free_text? - content.answers.empty? - end - # @return [Boolean] def feedback_exists? return false if current_user.nil? @@ -141,7 +131,7 @@ def content # @return [Array] def answer - @answer ||= if is_free_text? + @answer ||= if content.free_text? params[:answers] else Array.wrap(params[:answers]) @@ -152,7 +142,7 @@ def answer def answer_content @answer_content ||= begin return [] if answer.blank? - return answer if is_free_text? + return answer if content.free_text? answer.reject(&:blank?).map { |a| answer_wording(a) }.flatten end diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index 280700100..beba57969 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -16,7 +16,7 @@ def govuk_password_field(attribute_name, options = {}) def feedback_question_radio_buttons(content) @template.capture do content.options.each.with_index(1) do |option, index| - if content.is_last_option?(index) && content.has_other? + if content.last_option?(index) && content.has_other? @template.concat feedback_other_radio_button(content, option) else @template.concat question_radio_button(option) @@ -34,9 +34,9 @@ def feedback_question_radio_buttons(content) def feedback_question_check_boxes(content) @template.capture do content.options.each.with_index(1) do |option, index| - if content.is_last_option?(index) && content.other.present? + if content.last_option?(index) && content.other.present? @template.concat feedback_other_check_box(content, option) - elsif content.is_last_option?(index) && content.or.present? + elsif content.last_option?(index) && content.or.present? @template.concat @template.content_tag(:div, 'Or', class: 'govuk-checkboxes__divider') @template.concat govuk_check_box :answers, 'Or', label: { text: content.or }, link_errors: true else diff --git a/app/models/training/question.rb b/app/models/training/question.rb index 628f14f89..97a853255 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -54,10 +54,15 @@ def has_other? # @param index [Integer] # @return [Boolean] - def is_last_option?(index) + def last_option?(index) options.count == index end + # @return [Boolean] + def checkbox? + response_type + end + # @return [Boolean] event tracking def first_confidence? parent.confidence_questions.first.eql?(self) diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index d90b0d76a..6110bcb98 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -1,7 +1,7 @@ .govuk-grid-row .govuk-grid-column-full - if feedback_exists? - h1 t('feedback.feedback_exists.heading') + h1 = t('feedback.feedback_exists.heading') .govuk-button-group .govuk-button-group = govuk_button_link_to t('feedback.feedback_exists.back_button'), previous_path, class: 'govuk-button--secondary' diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index a6ea892dc..8342a485f 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -6,10 +6,12 @@ h1.govuk-heading-xl class='govuk-!-margin-bottom-4' - - if !is_checkbox? - = render 'training/questions/main_feedback_radio_buttons', f: f, response: content, required: true - - else + - if content.free_text? + = render 'training/questions/main_feedback_free_text', f: f, response: content, required: true + - elsif content.checkbox? = render 'training/questions/main_feedback_check_boxes', f: f, response: content, required: true + - else + = render 'training/questions/main_feedback_radio_buttons', f: f, response: content, required: true .govuk-button-group = govuk_button_link_to 'Previous', previous_path, class: 'govuk-button--secondary' diff --git a/app/views/training/questions/_main_feedback_free_text.html.slim b/app/views/training/questions/_main_feedback_free_text.html.slim new file mode 100644 index 000000000..9a29c3145 --- /dev/null +++ b/app/views/training/questions/_main_feedback_free_text.html.slim @@ -0,0 +1,3 @@ += f.govuk_fieldset legend: { text: m(content.legend) } do + = m(content.hint) if content.has_hint? + = f.govuk_text_area :answers, label: nil, multiple: true \ No newline at end of file From 1d466ec4661387bb48cca5abc35d1293b3b26b9d Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Mon, 4 Mar 2024 14:41:05 +0000 Subject: [PATCH 11/95] update feedback thank you path --- app/controllers/feedback_controller.rb | 2 +- config/routes.rb | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index c571f0c94..2166c9644 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -34,7 +34,7 @@ def feedback_exists? def next_path return my_modules_path if action_name == 'thank_you' return feedback_path(1) if params[:id].nil? - return thank_you_path if params[:id].to_i == questions.count + return feedback_thank_you_path if params[:id].to_i == questions.count feedback_path(params[:id].to_i + 1) end diff --git a/config/routes.rb b/config/routes.rb index b304da8ae..7bc0fb272 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -87,11 +87,8 @@ end end - resources :feedback, only: %i[index show update] do - collection do - get 'thank-you', to: 'feedback#thank_you' - end - end + get 'feedback/thank-you', to: 'feedback#thank_you', as: :feedback_thank_you + resources :feedback, only: %i[index show update] post 'change', to: 'hook#change' post 'release', to: 'hook#release' From fc0beb8f34aac784adc9e507d3de4ee506a5278d Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:48:02 +0000 Subject: [PATCH 12/95] Refactor: use form_builder to create end of module feedback forms --- app/forms/form_builder.rb | 18 ++++++++++++++++- .../_main_feedback_free_text.html.slim | 2 +- .../_opinion_radio_buttons.html.slim | 20 +++++++++---------- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index beba57969..2b1cde504 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -29,6 +29,22 @@ def feedback_question_radio_buttons(content) end end + def end_of_module_feedback_question_radio_buttons(content, response) + @template.capture do + response.options.each.with_index(1) do |option, index| + if content.last_option?(index) && content.has_other? + @template.concat feedback_other_radio_button(content, option) + else + @template.concat question_radio_button(option) + end + end + + if content.has_hint? && content.has_other? + @template.concat govuk_text_area :text_input, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } + end + end + end + # @param content [Object] # @return [String] def feedback_question_check_boxes(content) @@ -59,7 +75,7 @@ def feedback_other_check_box(content, option) # @param option [Object] The content for the 'Other' radio button option # @return [String] def feedback_other_radio_button(content, option) - govuk_radio_button :answers, option.id, label: { text: 'Other' }, link_errors: true do + govuk_radio_button :answers, option.id, label: { text: option.label }, link_errors: true, checked: option.checked? do govuk_text_field :text_input, label: { text: content.other } end end diff --git a/app/views/training/questions/_main_feedback_free_text.html.slim b/app/views/training/questions/_main_feedback_free_text.html.slim index 9a29c3145..ecec4fb4b 100644 --- a/app/views/training/questions/_main_feedback_free_text.html.slim +++ b/app/views/training/questions/_main_feedback_free_text.html.slim @@ -1,3 +1,3 @@ = f.govuk_fieldset legend: { text: m(content.legend) } do = m(content.hint) if content.has_hint? - = f.govuk_text_area :answers, label: nil, multiple: true \ No newline at end of file + = f.govuk_text_area :text_input, label: nil, multiple: true \ No newline at end of file diff --git a/app/views/training/questions/_opinion_radio_buttons.html.slim b/app/views/training/questions/_opinion_radio_buttons.html.slim index 141527d3c..32639e90a 100644 --- a/app/views/training/questions/_opinion_radio_buttons.html.slim +++ b/app/views/training/questions/_opinion_radio_buttons.html.slim @@ -1,13 +1,11 @@ -= f.govuk_radio_buttons_fieldset :answers, legend: { text: response.legend } do - = m(content.hint) unless content.hint.nil? +- if !content.free_text? - = f.hidden_field :answers, multiple: true - - if response.options.any? - - response.options.each.with_index(1) do |option, index| - - if response.options.count.eql?(index) && content.other.present? - = f.govuk_radio_button :answers, option.id, label: { text: 'No' }, link_errors: true, checked: option.checked? do - = f.govuk_text_area :text_input, label: { text: content.other } - - else - = f.question_radio_button(option) - - else + = f.govuk_radio_buttons_fieldset :answers, multiple: true, legend: { text: content.legend } do + = f.hidden_field :answers, multiple: true + = m(content.hint) unless content.hint.nil? + = f.end_of_module_feedback_question_radio_buttons(content, response) + +- else + = f.govuk_fieldset legend: { text: m(content.legend) } do + = m(content.hint) if content.has_hint? = f.govuk_text_area :text_input, label: nil, rows: 9 From 59979e1bc35347c34533e9d022a513a861fa4711 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Mon, 4 Mar 2024 15:51:22 +0000 Subject: [PATCH 13/95] change response name to content in feedback show --- app/views/feedback/show.html.slim | 6 +++--- .../training/questions/_main_feedback_check_boxes.html.slim | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index 8342a485f..f38a2a1ec 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -7,11 +7,11 @@ h1.govuk-heading-xl class='govuk-!-margin-bottom-4' - if content.free_text? - = render 'training/questions/main_feedback_free_text', f: f, response: content, required: true + = render 'training/questions/main_feedback_free_text', f: f, content: content, required: true - elsif content.checkbox? - = render 'training/questions/main_feedback_check_boxes', f: f, response: content, required: true + = render 'training/questions/main_feedback_check_boxes', f: f, content: content, required: true - else - = render 'training/questions/main_feedback_radio_buttons', f: f, response: content, required: true + = render 'training/questions/main_feedback_radio_buttons', f: f, content: content, required: true .govuk-button-group = govuk_button_link_to 'Previous', previous_path, class: 'govuk-button--secondary' diff --git a/app/views/training/questions/_main_feedback_check_boxes.html.slim b/app/views/training/questions/_main_feedback_check_boxes.html.slim index 183e6a44a..c3465f5c1 100644 --- a/app/views/training/questions/_main_feedback_check_boxes.html.slim +++ b/app/views/training/questions/_main_feedback_check_boxes.html.slim @@ -1,4 +1,4 @@ -= f.govuk_check_boxes_fieldset :answers, multiple: true, legend: { text: response.legend } do += f.govuk_check_boxes_fieldset :answers, multiple: true, legend: { text: content.legend } do = m(content.hint) unless content.hint.nil? = f.hidden_field :answers, multiple: true From 3da14381dabec0c30d4a5b4d77af1abc4e42f595 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Mon, 4 Mar 2024 16:00:52 +0000 Subject: [PATCH 14/95] add feedback answer check for free text input --- app/controllers/feedback_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 2166c9644..458332aa8 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -56,7 +56,7 @@ def guest? # @return [Boolean] def invalid_answer? - if answer.blank? || answer.all?(&:blank?) + if answer.blank? || answer.all?(&:blank?) || (content.free_text? && text_input.blank?) flash[:error] = 'Please answer the question' redirect_to current_feedback_path and return true else From 9eb9dd6508880090082e498d2791bb2c7716802a Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Mon, 4 Mar 2024 16:07:34 +0000 Subject: [PATCH 15/95] add form builder link for yard docs --- app/forms/form_builder.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index 2b1cde504..ab967d8e4 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -1,3 +1,4 @@ +# @see https://govuk-form-builder.netlify.app/ class FormBuilder < GOVUKDesignSystemFormBuilder::FormBuilder def govuk_text_field(attribute_name, options = {}) super(attribute_name, **options.reverse_merge(width: 'two-thirds')) From f5ddcbb570836890c60e575d54a7003cccf58768 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 4 Mar 2024 17:13:25 +0000 Subject: [PATCH 16/95] Refactor feedback using idiomatic ruby and existing patterns --- .github/workflows/ci.yml | 3 +- app/controllers/feedback_controller.rb | 153 ++++-------------- app/controllers/training/pages_controller.rb | 2 +- .../training/responses_controller.rb | 2 +- app/models/concerns/content_types.rb | 12 +- app/models/concerns/pagination.rb | 12 +- app/models/course.rb | 24 +++ app/models/response.rb | 11 +- app/models/training/question.rb | 17 +- app/models/user.rb | 5 + app/views/feedback/_check_boxes.html.slim | 5 + app/views/feedback/_radio_buttons.html.slim | 6 + app/views/feedback/index.html.slim | 36 +++-- app/views/feedback/show.html.slim | 23 +-- app/views/feedback/thank_you.html.slim | 8 +- app/views/training/assessments/show.html.slim | 2 +- .../_main_feedback_radio_buttons.html.slim | 10 -- config/locales/en.yml | 15 +- config/routes.rb | 2 +- spec/support/shared/with_progress.rb | 10 -- 20 files changed, 162 insertions(+), 196 deletions(-) create mode 100644 app/views/feedback/_check_boxes.html.slim create mode 100644 app/views/feedback/_radio_buttons.html.slim delete mode 100644 app/views/training/questions/_main_feedback_radio_buttons.html.slim diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1fefb540..1bf1134c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,6 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/test DOMAIN: recovery.app BOT_TOKEN: bot_token - CONTENTFUL_PREVIEW: true # TODO: Reminder to delete this line services: postgres: @@ -82,6 +81,8 @@ jobs: name: Run test suite run: bundle exec rspec env: + # TODO: Check envs before merging into main + CONTENTFUL_PREVIEW: true DISABLE_USER_ANSWER: true GOV_ONE_LOGIN: true - diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 458332aa8..b4b06a2bc 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -1,154 +1,59 @@ class FeedbackController < ApplicationController - helper_method :previous_path, :next_path, :content, :feedback_exists? + helper_method :content, + :mod, + :current_user_response - # @return [nil] - def show - redirect_to next_path unless always_show_question? - end + def show; end - # @return [nil] def index; end - # @return [nil] def update - return if invalid_answer? - - res = response_exists? ? update_response : create_response - - if res.save && res.errors[:text_input].empty? - redirect_to next_path + if save_response! + redirect_to feedback_path(content.next_item.name) else - flash[:error] = res.errors.full_messages.to_sentence - redirect_to current_feedback_path + render 'feedback/show', status: :unprocessable_entity end end - # @return [Boolean] - def feedback_exists? - return false if current_user.nil? - - Response.where(user_id: current_user.id).exists? - end - - # @return [String] path to next feedback step - def next_path - return my_modules_path if action_name == 'thank_you' - return feedback_path(1) if params[:id].nil? - return feedback_thank_you_path if params[:id].to_i == questions.count - - feedback_path(params[:id].to_i + 1) - end - - # @return [String] path to previous feedback step - def previous_path - return my_modules_path if params[:id].nil? - return feedback_path(1) if params[:id] == '1' - - feedback_path(params[:id].to_i - 1) - end - private # @return [Boolean] - def guest? - current_user.nil? - end - - # @return [Boolean] - def invalid_answer? - if answer.blank? || answer.all?(&:blank?) || (content.free_text? && text_input.blank?) - flash[:error] = 'Please answer the question' - redirect_to current_feedback_path and return true - else - false - end - end - - # @return [Response] - def create_response - Response.new( - user_id: current_user ? current_user.id : nil, - answers: answer_content, - question_name: content.name, - text_input: text_input, - guest_visit: guest? ? current_visit.visitor_token : nil, + def save_response! + current_user_response.update( + question_type: 'feedback', + answers: user_answers, + correct: true, + text_input: response_params[:text_input], ) end - # @return [Response] - def update_response - existing_response.update!( - answers: answer_content, - text_input: text_input, - ) - existing_response + # @return [Course] + def mod + Course.config end - # @return [Boolean] - def response_exists? - existing_response.present? + # @return [Training::Question] + def content + mod.page_by_name(question_name) end # @return [Response] - def existing_response - Response.find_by( - guest_visit: guest? ? current_visit.visitor_token : nil, - user_id: current_user&.id, - question_name: content.name, - ) - end - - # @return [Boolean] - def show_question? - always_show_question? && (!current_user.nil? || !response_exists?) - end - - # @return [Boolean] - def always_show_question? - !content.always_show_question.eql?(false) - end - - # @param answer [String] - # @return [String] - def answer_wording(answer) - content.answers[answer.to_i - 1].first - end - - # @return [Array] - def questions - Course.config.feedback + def current_user_response + @current_user_response ||= current_user.response_for_shared(content, mod) end # @return [String] - def current_feedback_path - feedback_path(params[:id]) + def question_name + params[:id] end - # @return [Hash] - def content - @content ||= questions[params[:id].to_i - 1] - end - - # @return [Array] - def answer - @answer ||= if content.free_text? - params[:answers] - else - Array.wrap(params[:answers]) - end - end - - # @return [Array] - def answer_content - @answer_content ||= begin - return [] if answer.blank? - return answer if content.free_text? - - answer.reject(&:blank?).map { |a| answer_wording(a) }.flatten - end + # OPTIMIZE: duplicated from ResponsesController + def response_params + params.require(:response).permit! end - def text_input - @text_input ||= params[:text_input] + # OPTIMIZE: duplicated from ResponsesController + def user_answers + Array(response_params[:answers]).compact_blank.map(&:to_i) end end diff --git a/app/controllers/training/pages_controller.rb b/app/controllers/training/pages_controller.rb index ee90f78ee..182059788 100644 --- a/app/controllers/training/pages_controller.rb +++ b/app/controllers/training/pages_controller.rb @@ -19,7 +19,7 @@ def index end def show - if content.is_question? || content.feedback_question? + if content.is_question? redirect_to training_module_question_path(mod.name, content.name) elsif content.assessment_results? redirect_to training_module_assessment_path(mod.name, content.name) diff --git a/app/controllers/training/responses_controller.rb b/app/controllers/training/responses_controller.rb index 2af040af3..9ffc8dfa2 100644 --- a/app/controllers/training/responses_controller.rb +++ b/app/controllers/training/responses_controller.rb @@ -45,7 +45,7 @@ def response_params # @note migrate from user_answer to response # @return [Boolean] def save_response! - correct_answers = content.confidence_question? || content.feedback_question? ? true : content.correct_answers.eql?(user_answers) + correct_answers = content.opinion_question? ? true : content.correct_answers.eql?(user_answers) if Rails.application.migrated_answers? current_user_response.update(answers: user_answers, correct: correct_answers, text_input: user_answer_text) diff --git a/app/models/concerns/content_types.rb b/app/models/concerns/content_types.rb index d42fbfdea..27e4b6296 100644 --- a/app/models/concerns/content_types.rb +++ b/app/models/concerns/content_types.rb @@ -22,7 +22,17 @@ def topic_intro? # @return [Boolean] def is_question? - page_type.match?(/question/) + page_type.match?(/formative|summative|confidence|feedback/) + end + + # @return [Boolean] + def opinion_question? + page_type.match?(/confidence|feedback/) + end + + # @return [Boolean] + def factual_question? + page_type.match?(/formative|summative/) end # @return [Boolean] diff --git a/app/models/concerns/pagination.rb b/app/models/concerns/pagination.rb index 5d897a5b4..44d22bb96 100644 --- a/app/models/concerns/pagination.rb +++ b/app/models/concerns/pagination.rb @@ -44,14 +44,22 @@ def next_item_id # @return [Array] def section_content - parent.content_sections.fetch(submodule) + if parent.is_a? Training::Module # OPTIMIZE: introduce parent check predicates? + parent.content_sections.fetch(submodule) + else + Course.config.pages + end end # TODO: duplicated in overview decorator #fetch_submodule_topic # # @return [Array] def subsection_content - parent.content_subsections.fetch([submodule, topic]) + if parent.is_a? Training::Module + parent.content_subsections.fetch([submodule, topic]) + else + Course.config.pages + end end # @return [nil, Integer] diff --git a/app/models/course.rb b/app/models/course.rb index fa101049a..a1b2a28c1 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -18,6 +18,25 @@ def self.config fetch_or_store('course') { first } end + # + # Quacks like training module + # + + # @return [nil] mod.name + def name + nil + end + + # @return [Array] mod.content_sections + def content_sections + Types::EMPTY_ARRAY + end + + # @return [Array] mod.content_subsections + def content_subsections + Types::EMPTY_ARRAY + end + # @return [Array] def feedback super.to_a @@ -35,4 +54,9 @@ def pages def page_by_id(id) pages.find { |page| page.id.eql?(id) } end + + # @return [Training::Question] + def page_by_name(name) + pages.find { |page| page.name.eql?(name) } + end end diff --git a/app/models/response.rb b/app/models/response.rb index 9068d5fcf..bec112a36 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -20,12 +20,17 @@ class Response < ApplicationRecord scope :summative, -> { where(question_type: 'summative') } scope :confidence, -> { where(question_type: 'confidence') } scope :feedback, -> { where(question_type: 'feedback') } + scope :main_feedback, -> { where(question_type: 'feedback', training_module: nil) } delegate :to_partial_path, :legend, to: :question - # @return [Training::Module] + # @return [Training::Module, Course] def mod - Training::Module.by_name(training_module) + if training_module + Training::Module.by_name(training_module) + else + Course.config + end end # @return [Training::Question] @@ -59,7 +64,7 @@ def responded? # @return [Boolean] def correct? - question.confidence_question? || question.correct_answers.eql?(answers) + question.opinon_question? || question.correct_answers.eql?(answers) end # @return [Boolean] diff --git a/app/models/training/question.rb b/app/models/training/question.rb index 97a853255..ab2b04053 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -21,20 +21,23 @@ def skippable? # @return [String] powered by JSON not type def to_partial_path - return 'training/questions/opinion_radio_buttons' if feedback_question? - partial = multi_select? ? 'check_boxes' : 'radio_buttons' + + return "feedback/#{partial}" if feedback_question? + partial = "learning_#{partial}" if formative_question? "training/questions/#{partial}" end # @return [Boolean] def multi_select? - confidence_question? || feedback_question? ? false : answer.multi_select? - end - - def feedback_question? - page_type == 'feedback' + if feedback_question? + response_type # FIXME: this field smells + elsif confidence_question? + false + else + answer.multi_select? + end end # @return [Boolean] feedback free text diff --git a/app/models/user.rb b/app/models/user.rb index 41667ac12..2a4adefd8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -477,6 +477,11 @@ def content_changes @content_changes ||= ContentChanges.new(user: self) end + # @return [Boolean] + def completed_main_feedback? + responses.main_feedback.any? + end + private # @return [Hash] diff --git a/app/views/feedback/_check_boxes.html.slim b/app/views/feedback/_check_boxes.html.slim new file mode 100644 index 000000000..eb8d902f2 --- /dev/null +++ b/app/views/feedback/_check_boxes.html.slim @@ -0,0 +1,5 @@ +h1 Feedback Partial + += f.govuk_check_boxes_fieldset :answers, legend: { text: response.legend } do + - response.options.each do |option| + = f.question_check_box(option) diff --git a/app/views/feedback/_radio_buttons.html.slim b/app/views/feedback/_radio_buttons.html.slim new file mode 100644 index 000000000..32148c039 --- /dev/null +++ b/app/views/feedback/_radio_buttons.html.slim @@ -0,0 +1,6 @@ +h1 Feedback Partial + += f.govuk_radio_buttons_fieldset :answers, legend: { text: response.legend } do + = f.hidden_field :answers + - response.options.each do |option| + = f.question_radio_button(option) diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index 6110bcb98..097dddcb6 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -1,17 +1,23 @@ .govuk-grid-row .govuk-grid-column-full - - if feedback_exists? - h1 = t('feedback.feedback_exists.heading') - .govuk-button-group - .govuk-button-group - = govuk_button_link_to t('feedback.feedback_exists.back_button'), previous_path, class: 'govuk-button--secondary' - = govuk_button_link_to t('feedback.feedback_exists.next_button'), next_path - - else - h1 = t('feedback.heading') - p = m('feedback.body') - h2 = t('feedback.technical_support.heading') - p.text-secondary = t('feedback.technical_support.body') - - hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' - - = govuk_button_link_to t('feedback.next_button'), next_path \ No newline at end of file + - if current_user.completed_main_feedback? + + h1= t('feedback.feedback_exists.heading') + + .govuk-button-group + .govuk-button-group + = govuk_button_link_to t('previous_page.previous'), my_modules_path, secondary: true + = govuk_button_link_to t('links.update_feedback'), feedback_path(mod.pages.first.name) + + - else + h1= t('feedback.heading') + + p= m('feedback.body') + + h2= t('feedback.technical_support.heading') + + p.text-secondary= t('feedback.technical_support.body') + + hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' + + = govuk_button_link_to t('feedback.next_button'), feedback_path(mod.pages.first.name) diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index f38a2a1ec..e68b841e8 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -1,18 +1,21 @@ -= form_with url: feedback_path, method: :patch do |f| += render 'training/questions/debug' + += form_with model: current_user_response, url: feedback_path, method: :patch do |f| .govuk-grid-row .govuk-grid-column-two-thirds hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' + = f.govuk_error_summary + h1.govuk-heading-xl class='govuk-!-margin-bottom-4' - - - if content.free_text? - = render 'training/questions/main_feedback_free_text', f: f, content: content, required: true - - elsif content.checkbox? - = render 'training/questions/main_feedback_check_boxes', f: f, content: content, required: true - - else - = render 'training/questions/main_feedback_radio_buttons', f: f, content: content, required: true + + = render partial: content.to_partial_path, locals: { f: f }, object: current_user_response, as: :response .govuk-button-group - = govuk_button_link_to 'Previous', previous_path, class: 'govuk-button--secondary' - = f.submit 'Next', class: 'govuk-button' \ No newline at end of file + - if mod.pages.first.eql?(content) + = govuk_button_link_to t('previous_page.previous'), feedback_index_path, secondary: true + - else + = govuk_button_link_to t('previous_page.previous'), feedback_path(content.previous_item.name), secondary: true + + = f.govuk_submit t('next_page.next') diff --git a/app/views/feedback/thank_you.html.slim b/app/views/feedback/thank_you.html.slim index d247b8a58..13c6e17a7 100644 --- a/app/views/feedback/thank_you.html.slim +++ b/app/views/feedback/thank_you.html.slim @@ -1,8 +1,8 @@ .govuk-grid-row .govuk-grid-column-full - h1 = t('feedback.thank_you.heading') - p = m('feedback.thank_you.body') + h1= t('feedback.thank_you.heading') + p= m('feedback.thank_you.body') - hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' + hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' - = govuk_button_link_to t('feedback.thank_you.next_button'), next_path \ No newline at end of file + = govuk_button_link_to t('links.my_modules'), my_modules_path diff --git a/app/views/training/assessments/show.html.slim b/app/views/training/assessments/show.html.slim index 16350d541..716f85e08 100644 --- a/app/views/training/assessments/show.html.slim +++ b/app/views/training/assessments/show.html.slim @@ -37,6 +37,6 @@ hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class = link_to_next - else - = govuk_button_link_to 'Go to My modules', my_modules_path, class: 'govuk-button--secondary' + = govuk_button_link_to 'Go to My modules', my_modules_path, secondary: true = govuk_button_link_to 'Retake test', new_training_module_assessment_path(mod.name) diff --git a/app/views/training/questions/_main_feedback_radio_buttons.html.slim b/app/views/training/questions/_main_feedback_radio_buttons.html.slim deleted file mode 100644 index 36f1a4be1..000000000 --- a/app/views/training/questions/_main_feedback_radio_buttons.html.slim +++ /dev/null @@ -1,10 +0,0 @@ -- if !content.free_text? - - = f.govuk_radio_buttons_fieldset :answers, multiple: true, legend: { text: content.legend } do - = f.hidden_field :answers, multiple: true - = f.feedback_question_radio_buttons(content) - -- else - = m(content.hint) if content.has_hint? - = f.govuk_text_area :answers, label: nil, multiple: true - \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index d7975739e..f75d0d0a9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -123,7 +123,12 @@ en: finish: View certificate give_feedback: Give feedback + previous_page: + previous: Previous + links: + my_modules: Go to my modules + update_feedback: Update my feedback save: Save continue: Continue cancel: Cancel @@ -621,21 +626,21 @@ en: Completing this form is voluntary and you can withdraw your feedback at any time. By completing this form you have understood the above and consent to take part. + feedback_exists: heading: You have already submitted feedback body: Thank you for helping to improve this training - back_button: Previous - next_button: Update my feedback + technical_support: heading: Technical support queries - body: If you have any questions about how to sue this website or are experiencing any technical issues, please use our contact form ADD LINK so that our team can follow up with your enquiry. - next_button: Next + body: | + If you have any questions about how to sue this website or are experiencing any + technical issues, please use our contact form ADD LINK so that our team can follow up with your enquiry. # /feedback/thank-you thank_you: heading: Thank you body: Thank you for helping to improve this training - next_button: Go to my modules # /gov-one/info gov_one_info: diff --git a/config/routes.rb b/config/routes.rb index 7bc0fb272..5284044e5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -87,7 +87,7 @@ end end - get 'feedback/thank-you', to: 'feedback#thank_you', as: :feedback_thank_you + get 'feedback/thank-you', to: 'feedback#thank_you' resources :feedback, only: %i[index show update] post 'change', to: 'hook#change' diff --git a/spec/support/shared/with_progress.rb b/spec/support/shared/with_progress.rb index 9626c34d1..fa8170392 100644 --- a/spec/support/shared/with_progress.rb +++ b/spec/support/shared/with_progress.rb @@ -36,16 +36,6 @@ def start_confidence_check(mod) view_pages_upto(mod, 'confidence_questionnaire') end - # @param mod [Training::Module] - def start_end_of_module_feedback_intro(mod) - view_pages_upto(mod, 'opinion_intro') - end - - # @param mod [Training::Module] - def start_end_of_module_feedback_form(mod) - view_pages_upto(mod, 'feedback') - end - # @param mod [Training::Module] def start_summative_assessment(mod) view_pages_upto(mod, 'summative_questionnaire') From 64cb042bba8a89fbad261cd76435e6e4bcd18591 Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Tue, 5 Mar 2024 08:54:30 +0000 Subject: [PATCH 17/95] Renaming opinion to feedback and update feedback logic (based on PR comments) --- app/controllers/training/questions_controller.rb | 7 ++++++- app/helpers/link_helper.rb | 5 ----- app/models/response.rb | 2 +- app/models/training/module.rb | 2 +- app/models/training/question.rb | 4 ++-- config/locales/en.yml | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/controllers/training/questions_controller.rb b/app/controllers/training/questions_controller.rb index 88b3be0e8..b99c9b642 100644 --- a/app/controllers/training/questions_controller.rb +++ b/app/controllers/training/questions_controller.rb @@ -54,7 +54,7 @@ def track_confidence_start? # @return [Boolean] def track_feedback_start? - content.feedback_question? || content.opinion_intro? + content.first_feedback? && feedback_start_untracked? end # @return [Boolean] @@ -73,5 +73,10 @@ def summative_start_untracked? def confidence_start_untracked? untracked?('confidence_check_start', training_module_id: mod.name) end + + # @return [Boolean] + def feedback_start_untracked? + untracked?('feedback_start', training_module_id: mod.name) + end end end diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 11b2a7bae..370a58938 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -32,11 +32,6 @@ def link_to_action [text, path] end - # @return [Boolean] - def one_off_question? - always_show_question.eql?(false) - end - # @return [String] next page (ends on certificate) def link_to_next govuk_button_link_to next_page.text, training_module_page_path(mod.name, next_page.name), diff --git a/app/models/response.rb b/app/models/response.rb index bec112a36..b51d4d563 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -64,7 +64,7 @@ def responded? # @return [Boolean] def correct? - question.opinon_question? || question.correct_answers.eql?(answers) + question.opinion_question? || question.correct_answers.eql?(answers) end # @return [Boolean] diff --git a/app/models/training/module.rb b/app/models/training/module.rb index 1d3907584..ebf74f41b 100644 --- a/app/models/training/module.rb +++ b/app/models/training/module.rb @@ -246,7 +246,7 @@ def confidence_questions end # @return [Array] - def opinion_questions + def feedback_questions content.select(&:feedback_question?) end diff --git a/app/models/training/question.rb b/app/models/training/question.rb index ab2b04053..164da2d7d 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -83,12 +83,12 @@ def last_assessment? # @return [Boolean] event tracking def first_feedback? - parent.opinion_questions.first.eql?(self) + parent.feedback_questions.first.eql?(self) end # @return [Boolean] event tracking def last_feedback? - parent.opinion_questions.last.eql?(self) + parent.feedback_questions.last.eql?(self) end # @return [Boolean] diff --git a/config/locales/en.yml b/config/locales/en.yml index f75d0d0a9..1e74f3141 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -619,7 +619,7 @@ en: feedback: heading: Give feedback body: | - The purpose of this feedback form is to gather your opinon on the child development training course that the Department for Education has created for early years practitioners. + The purpose of this feedback form is to gather your opinion on the child development training course that the Department for Education has created for early years practitioners. For more information on how you data will be used, please view our privacy notice. ADD LINK From 5d9a7535095edb29846793d49d063f235c37047e Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 5 Mar 2024 09:33:03 +0000 Subject: [PATCH 18/95] Refine first/next logic and flag potential content changes --- app/helpers/link_helper.rb | 4 ++-- app/models/course.rb | 5 +---- app/models/response.rb | 2 +- app/models/training/question.rb | 7 +------ app/views/feedback/index.html.slim | 7 ++++++- app/views/feedback/show.html.slim | 4 +++- app/views/feedback/thank_you.html.slim | 4 ++-- .../questions/_main_feedback_check_boxes.html.slim | 8 +++++--- .../questions/_main_feedback_free_text.html.slim | 7 +++++-- .../questions/_opinion_radio_buttons.html.slim | 11 ----------- config/locales/en.yml | 8 ++------ 11 files changed, 28 insertions(+), 39 deletions(-) delete mode 100644 app/views/training/questions/_opinion_radio_buttons.html.slim diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 370a58938..6d40cf049 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -57,11 +57,11 @@ def link_to_previous # Check if feedback questions have been skipped if content.thankyou? && !current_user.response_for_shared(content.previous_item, mod).responded? - govuk_button_link_to 'Previous', training_module_page_path(mod.name, mod.opinion_intro_page.name), + govuk_button_link_to t('previous_page.previous'), training_module_page_path(mod.name, mod.opinion_intro_page.name), class: style, aria: { label: t('pagination.previous') } else - govuk_button_link_to 'Previous', path, + govuk_button_link_to t('previous_page.previous'), path, class: style, aria: { label: t('pagination.previous') } end diff --git a/app/models/course.rb b/app/models/course.rb index a1b2a28c1..d76a90b1f 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -50,10 +50,7 @@ def pages end end - # @return [Training::Question] - def page_by_id(id) - pages.find { |page| page.id.eql?(id) } - end + alias_method :feedback_questions, :pages # @return [Training::Question] def page_by_name(name) diff --git a/app/models/response.rb b/app/models/response.rb index b51d4d563..0a4954513 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -72,7 +72,7 @@ def revised? correct && !correct? end - # @return [Boolean] + # @return [Boolean] LIES!! def free_text_answer? question.free_text? && text_input.present? unless training_module.nil? end diff --git a/app/models/training/question.rb b/app/models/training/question.rb index 164da2d7d..4b117b08f 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -81,16 +81,11 @@ def last_assessment? parent.summative_questions.last.eql?(self) end - # @return [Boolean] event tracking + # @return [Boolean] def first_feedback? parent.feedback_questions.first.eql?(self) end - # @return [Boolean] event tracking - def last_feedback? - parent.feedback_questions.last.eql?(self) - end - # @return [Boolean] def true_false? return false if multi_select? diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index 097dddcb6..5ebd43ea6 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -2,6 +2,8 @@ .govuk-grid-column-full - if current_user.completed_main_feedback? + -# why not one single block of markdown? + h1= t('feedback.feedback_exists.heading') .govuk-button-group @@ -10,9 +12,12 @@ = govuk_button_link_to t('links.update_feedback'), feedback_path(mod.pages.first.name) - else + + -# why not one single block of markdown? + h1= t('feedback.heading') - p= m('feedback.body') + = m('feedback.body') h2= t('feedback.technical_support.heading') diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index e68b841e8..4ed5f6a90 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -8,12 +8,14 @@ = f.govuk_error_summary + -# a form inside a heading element is not accessible + h1.govuk-heading-xl class='govuk-!-margin-bottom-4' = render partial: content.to_partial_path, locals: { f: f }, object: current_user_response, as: :response .govuk-button-group - - if mod.pages.first.eql?(content) + - if content.first_feedback? = govuk_button_link_to t('previous_page.previous'), feedback_index_path, secondary: true - else = govuk_button_link_to t('previous_page.previous'), feedback_path(content.previous_item.name), secondary: true diff --git a/app/views/feedback/thank_you.html.slim b/app/views/feedback/thank_you.html.slim index 13c6e17a7..3bae33dbb 100644 --- a/app/views/feedback/thank_you.html.slim +++ b/app/views/feedback/thank_you.html.slim @@ -1,7 +1,7 @@ .govuk-grid-row .govuk-grid-column-full - h1= t('feedback.thank_you.heading') - p= m('feedback.thank_you.body') + h1= t('.heading') + p.govuk-body-m= t('.body') hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' diff --git a/app/views/training/questions/_main_feedback_check_boxes.html.slim b/app/views/training/questions/_main_feedback_check_boxes.html.slim index c3465f5c1..599d900c8 100644 --- a/app/views/training/questions/_main_feedback_check_boxes.html.slim +++ b/app/views/training/questions/_main_feedback_check_boxes.html.slim @@ -1,5 +1,7 @@ = f.govuk_check_boxes_fieldset :answers, multiple: true, legend: { text: content.legend } do - = m(content.hint) unless content.hint.nil? - = f.hidden_field :answers, multiple: true - = f.feedback_question_check_boxes(content) \ No newline at end of file + + - if content.has_hint? + = m(content.hint) + + = f.feedback_question_check_boxes(content) diff --git a/app/views/training/questions/_main_feedback_free_text.html.slim b/app/views/training/questions/_main_feedback_free_text.html.slim index ecec4fb4b..a3e38da72 100644 --- a/app/views/training/questions/_main_feedback_free_text.html.slim +++ b/app/views/training/questions/_main_feedback_free_text.html.slim @@ -1,3 +1,6 @@ = f.govuk_fieldset legend: { text: m(content.legend) } do - = m(content.hint) if content.has_hint? - = f.govuk_text_area :text_input, label: nil, multiple: true \ No newline at end of file + + - if content.has_hint? + = m(content.hint) + + = f.govuk_text_area :text_input, label: nil, multiple: true diff --git a/app/views/training/questions/_opinion_radio_buttons.html.slim b/app/views/training/questions/_opinion_radio_buttons.html.slim deleted file mode 100644 index 32639e90a..000000000 --- a/app/views/training/questions/_opinion_radio_buttons.html.slim +++ /dev/null @@ -1,11 +0,0 @@ -- if !content.free_text? - - = f.govuk_radio_buttons_fieldset :answers, multiple: true, legend: { text: content.legend } do - = f.hidden_field :answers, multiple: true - = m(content.hint) unless content.hint.nil? - = f.end_of_module_feedback_question_radio_buttons(content, response) - -- else - = f.govuk_fieldset legend: { text: m(content.legend) } do - = m(content.hint) if content.has_hint? - = f.govuk_text_area :text_input, label: nil, rows: 9 diff --git a/config/locales/en.yml b/config/locales/en.yml index 1e74f3141..68355fc03 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -222,11 +222,6 @@ en: Please rate to what extent you agree with the statements on the following pages. - thankyou: - heading: Thank you - body: | - You can also {external}[give feedback about the training](%{feedback_url}){/external} - certificate: heading: Download your certificate @@ -619,7 +614,8 @@ en: feedback: heading: Give feedback body: | - The purpose of this feedback form is to gather your opinion on the child development training course that the Department for Education has created for early years practitioners. + The purpose of this feedback form is to gather your opinon on the child development + training course that the Department for Education has created for early years practitioners. For more information on how you data will be used, please view our privacy notice. ADD LINK From 4ff9def82bbca7e27856500e88be855920755199 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 5 Mar 2024 10:20:08 +0000 Subject: [PATCH 19/95] Setup unit testing for custom form builder fields --- app/forms/form_builder.rb | 4 +++- spec/forms/form_builder_spec.rb | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 spec/forms/form_builder_spec.rb diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index ab967d8e4..5e91496f5 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -1,4 +1,6 @@ -# @see https://govuk-form-builder.netlify.app/ +# @see https://govuk-form-builder.netlify.app +# +# class FormBuilder < GOVUKDesignSystemFormBuilder::FormBuilder def govuk_text_field(attribute_name, options = {}) super(attribute_name, **options.reverse_merge(width: 'two-thirds')) diff --git a/spec/forms/form_builder_spec.rb b/spec/forms/form_builder_spec.rb new file mode 100644 index 000000000..5488aef11 --- /dev/null +++ b/spec/forms/form_builder_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +RSpec.describe FormBuilder do + let(:assigns) { {} } + let(:controller) { ActionController::Base.new } + let(:lookup_context) { ActionView::LookupContext.new(nil) } + let(:helper) { ActionView::Base.new(lookup_context, assigns, controller) } + let(:object) { create :user } + let(:object_name) { :user } + let(:builder) { described_class.new(object_name, object, helper, {}) } + + describe '#select_trainee_setting' do + subject(:output) { builder.select_trainee_setting } + + it 'element' do + expect(output).to include '
Date: Tue, 5 Mar 2024 11:00:33 +0000 Subject: [PATCH 20/95] Update Previous/Next logic so it uses Previous/Next Decorators --- app/decorators/next_page_decorator.rb | 2 + app/decorators/previous_page_decorator.rb | 123 ++++++++++++++++++ app/helpers/link_helper.rb | 38 ++---- .../previous_page_decorator_spec.rb | 106 +++++++++++++++ 4 files changed, 245 insertions(+), 24 deletions(-) create mode 100644 app/decorators/previous_page_decorator.rb create mode 100644 spec/decorators/previous_page_decorator_spec.rb diff --git a/app/decorators/next_page_decorator.rb b/app/decorators/next_page_decorator.rb index edeaf4b9c..a5e3e94b1 100644 --- a/app/decorators/next_page_decorator.rb +++ b/app/decorators/next_page_decorator.rb @@ -100,10 +100,12 @@ def missing? content.next_item.eql?(content) && wip? end + # @return [Boolean] def opinion_intro? content.opinion_intro? end + # @return [Boolean] def content_section? content.section? && !content.opinion_intro? end diff --git a/app/decorators/previous_page_decorator.rb b/app/decorators/previous_page_decorator.rb new file mode 100644 index 000000000..aadd25f7c --- /dev/null +++ b/app/decorators/previous_page_decorator.rb @@ -0,0 +1,123 @@ +# TODO: assessments controller #new can be removed +# +# Button or link labels to the previous page +# @see [LinkHelper#link_to_previous] +# +class PreviousPageDecorator + extend Dry::Initializer + + # @!attribute [r] user + # @return [User] + option :user, Types.Instance(User), required: true + # @!attribute [r] mod + # @return [Training::Module] + option :mod, Types::TrainingModule, required: true + # @!attribute [r] content + # @return [Training::Page, Training::Question, Training::Video] + option :content, Types::TrainingContent, required: true + # @!attribute [r] assessment + # @return [AssessmentProgress] + option :assessment, required: true + + # @return [String] + def name + if content.interruption_page? + mod.content_start.name + else + content.previous_item.name + end + end + + # @return [String] + def previous_path + previous_content = content.previous_item + + if content.interruption_page? + Rails.application.routes.url_helpers.training_module_path(mod.name) + elsif skippable_page? + Rails.application.routes.url_helpers.training_module_page_path(mod.name, previous_content.previous_item.name) + elsif skip_back_to_feedback_intro? + Rails.application.routes.url_helpers.training_module_page_path(mod.name, mod.opinion_intro_page.name) + else + Rails.application.routes.url_helpers.training_module_page_path(mod.name, content.previous_item.name) + end + end + + # @return [String] + def style + if content.section? && !content.opinion_intro? + 'section-intro-previous-button' + else + 'govuk-button--secondary' + end + end + + # @see [Pagination] + # @return [String] + def text + label[:previous] + end + + # @return [Boolean] + def disable_question_submission? + if content.formative_question? + answered? + elsif content.summative_question? + answered? && (Rails.application.migrated_answers? ? assessment.graded? : assessment.score.present?) + else + false + end + end + +private + + # @return [Hash=>Symbol] + def label + I18n.t(:previous_page) + end + + # @return [Boolean] + def previous? + content.interruption_page? || disable_question_submission? + end + + # @return [Boolean] + def save? + content.notes? || content.summative_question? + end + + # @return [Boolean] + def answered? + user.response_for(content).responded? + end + + # @return [Boolean] + def missing? + content.previous_item.eql?(content) && wip? + end + + # @return [Boolean] + def opinion_intro? + content.opinion_intro? + end + + # @return [Boolean] + def content_section? + content.section? && !content.opinion_intro? + end + + # @return [Boolean] + def skippable_page? + !content.interruption_page? && content.previous_item.skippable? && user.response_for_shared(content.previous_item, mod).responded? + end + + # @return [Boolean] + def skip_back_to_feedback_intro? + content.thankyou? && !user.response_for_shared(content.previous_item, mod).responded? + end + + # @return [Boolean] + def wip? + Rails.application.preview? || Rails.env.test? + end +end diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 6d40cf049..3ed541674 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -41,30 +41,10 @@ def link_to_next # @return [String] previous page or module overview def link_to_previous - previous_content = content.previous_item - path = - if content.interruption_page? - training_module_path(mod.name) - else - training_module_page_path(mod.name, previous_content.name) - end - - if !content.interruption_page? && content.previous_item.skippable? && current_user.response_for_shared(content.previous_item, mod).responded? - path = training_module_page_path(mod.name, previous_content.previous_item.name) - end - - style = content.section? && !content.opinion_intro? ? 'section-intro-previous-button' : 'govuk-button--secondary' - - # Check if feedback questions have been skipped - if content.thankyou? && !current_user.response_for_shared(content.previous_item, mod).responded? - govuk_button_link_to t('previous_page.previous'), training_module_page_path(mod.name, mod.opinion_intro_page.name), - class: style, - aria: { label: t('pagination.previous') } - else - govuk_button_link_to t('previous_page.previous'), path, - class: style, - aria: { label: t('pagination.previous') } - end + govuk_button_link_to previous_page.text, previous_page.previous_path, + id: 'previous-action', + class: previous_page.style, + aria: { label: t('pagination.previous') } end # Bottom of my-modules card component @@ -101,4 +81,14 @@ def next_page assessment: assessment_progress_service(mod), ) end + + # @return [PreviousPageDecorator] + def previous_page + PreviousPageDecorator.new( + user: current_user, + mod: mod, + content: content, + assessment: assessment_progress_service(mod), + ) + end end diff --git a/spec/decorators/previous_page_decorator_spec.rb b/spec/decorators/previous_page_decorator_spec.rb new file mode 100644 index 000000000..bf8d04452 --- /dev/null +++ b/spec/decorators/previous_page_decorator_spec.rb @@ -0,0 +1,106 @@ +require 'rails_helper' + +RSpec.describe PreviousPageDecorator do + subject(:decorator) do + described_class.new(user: user, mod: mod, content: content, assessment: assessment) + end + + let(:user) { create :user, :registered } + let(:mod) { Training::Module.by_name(:alpha) } + let(:content) { mod.page_by_name('1-1-2') } + let(:assessment) { double } + + it '#name' do + expect(decorator.name).to eq '1-1-1' + end + + it '#text' do + expect(decorator.text).to eq 'Previous' + end + + it '#disable_question_submission?' do + expect(decorator.disable_question_submission?).to be false + end + + context 'when adding to the learning log' do + let(:content) { mod.page_by_name('1-1-3-1') } + + it '#text' do + expect(decorator.text).to eq 'Previous' + end + end + + context 'when starting a section' do + let(:content) { mod.page_by_name('1-2') } + + it '#text' do + expect(decorator.text).to eq 'Previous' + end + end + + context 'when starting an assessment' do + let(:content) { mod.page_by_name('1-3-2') } + + it '#text' do + expect(decorator.text).to eq 'Previous' + end + end + + context 'when answering an assessment question' do + let(:content) { mod.page_by_name('1-3-2-1') } + + it '#text' do + expect(decorator.text).to eq 'Previous' + end + end + + context 'when finishing an assessment' do + let(:content) { mod.page_by_name('1-3-2-10') } + + it '#text' do + expect(decorator.text).to eq 'Previous' + end + end + + context 'when reviewing a completed assessment' do + let(:content) { mod.page_by_name('1-3-2-1') } + let(:assessment) { instance_double(AssessmentProgress, graded?: true, score: 100) } + + before do + if Rails.application.migrated_answers? + create :response, + user: user, + question_name: '1-3-2-1', + question_type: 'summative', + training_module: 'alpha', + answers: [1], + assessment: create(:assessment, user: user) + else + create :user_answer, + user: user, + name: '1-3-2-1', + module: 'alpha', + assessments_type: 'summative_assessment', + question: 'N/A for CMS only questions', + questionnaire_id: 0, + answer: [1] + end + end + + it '#text' do + expect(decorator.text).to eq 'Previous' + end + + it '#disable_question_submission?' do + expect(decorator.disable_question_submission?).to be true + end + end + + context 'when finishing a module' do + let(:content) { mod.page_by_name('1-3-3-5') } + + it '#text' do + expect(decorator.text).to eq 'Previous' + end + end +end From 710e6afdfb067ee8ea7fd782d4fd40694cb10a04 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Tue, 5 Mar 2024 11:23:12 +0000 Subject: [PATCH 21/95] wip: - extract feedback intro partials for complete and incomplete feedback - create guest struct --- app/controllers/feedback_controller.rb | 10 +++++-- app/models/guest.rb | 21 +++++++++++++++ app/models/user.rb | 5 ++++ ...leted_main_feedback_introduction.html.slim | 6 +++++ .../_main_feedback_introduction.html.slim | 11 ++++++++ app/views/feedback/index.html.slim | 27 +++---------------- 6 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 app/models/guest.rb create mode 100644 app/views/feedback/_completed_main_feedback_introduction.html.slim create mode 100644 app/views/feedback/_main_feedback_introduction.html.slim diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index b4b06a2bc..ddc1a545e 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -1,7 +1,8 @@ class FeedbackController < ApplicationController helper_method :content, :mod, - :current_user_response + :current_user_response, + :respondant def show; end @@ -15,6 +16,11 @@ def update end end + # @return [User | Guest] + def respondant + @respondant ||= current_user || Guest.new(visit_id: current_visit.id) + end + private # @return [Boolean] @@ -39,7 +45,7 @@ def content # @return [Response] def current_user_response - @current_user_response ||= current_user.response_for_shared(content, mod) + @current_user_response ||= respondant.response_for_shared(content, mod) end # @return [String] diff --git a/app/models/guest.rb b/app/models/guest.rb new file mode 100644 index 000000000..fa32e9580 --- /dev/null +++ b/app/models/guest.rb @@ -0,0 +1,21 @@ +class Guest < Dry::Struct + attribute :visit_id, Types::Strict::Integer + + def guest? + true + end + + # @param content [Training::Question] + # @return [Response] + def response_for_shared(content, _) + Response.find_or_initialize_by( + question_name: content.name, + guest_visit: visit_id, + ) + end + + # @return [Boolean] + def completed_main_feedback? + Response.where(guest_visit: visit_id).main_feedback.any? + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 2a4adefd8..e5066a8e3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -179,6 +179,11 @@ def gov_one? !gov_one_id.nil? end + # @return [Boolean] + def guest? + false + end + # @see Devise database_authenticatable # @param params [Hash] # @return [Boolean] diff --git a/app/views/feedback/_completed_main_feedback_introduction.html.slim b/app/views/feedback/_completed_main_feedback_introduction.html.slim new file mode 100644 index 000000000..bba3aa896 --- /dev/null +++ b/app/views/feedback/_completed_main_feedback_introduction.html.slim @@ -0,0 +1,6 @@ +h1= t('feedback.feedback_exists.heading') + +.govuk-button-group +.govuk-button-group + = govuk_button_link_to t('previous_page.previous'), my_modules_path, secondary: true + = govuk_button_link_to t('links.update_feedback'), feedback_path(mod.pages.first.name) \ No newline at end of file diff --git a/app/views/feedback/_main_feedback_introduction.html.slim b/app/views/feedback/_main_feedback_introduction.html.slim new file mode 100644 index 000000000..c75951fd2 --- /dev/null +++ b/app/views/feedback/_main_feedback_introduction.html.slim @@ -0,0 +1,11 @@ +h1= t('feedback.heading') + += m('feedback.body') + +h2= t('feedback.technical_support.heading') + +p.text-secondary= t('feedback.technical_support.body') + +hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' + += govuk_button_link_to t('feedback.next_button'), feedback_path(mod.pages.first.name) \ No newline at end of file diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index 5ebd43ea6..a6df4e128 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -1,28 +1,7 @@ .govuk-grid-row .govuk-grid-column-full - - if current_user.completed_main_feedback? - - -# why not one single block of markdown? - - h1= t('feedback.feedback_exists.heading') - - .govuk-button-group - .govuk-button-group - = govuk_button_link_to t('previous_page.previous'), my_modules_path, secondary: true - = govuk_button_link_to t('links.update_feedback'), feedback_path(mod.pages.first.name) + - if respondant.completed_main_feedback? + = render 'completed_main_feedback_introduction', mod: mod - else - - -# why not one single block of markdown? - - h1= t('feedback.heading') - - = m('feedback.body') - - h2= t('feedback.technical_support.heading') - - p.text-secondary= t('feedback.technical_support.body') - - hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' - - = govuk_button_link_to t('feedback.next_button'), feedback_path(mod.pages.first.name) + = render 'main_feedback_introduction', mod: mod From b612248984938f85f034be804f65dbe366eed4bd Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Tue, 5 Mar 2024 11:57:14 +0000 Subject: [PATCH 22/95] Update logic in previous decorator and remove unused methods --- app/decorators/previous_page_decorator.rb | 34 +++-------------------- app/helpers/link_helper.rb | 15 +++++++--- 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/app/decorators/previous_page_decorator.rb b/app/decorators/previous_page_decorator.rb index aadd25f7c..3f6f98a87 100644 --- a/app/decorators/previous_page_decorator.rb +++ b/app/decorators/previous_page_decorator.rb @@ -21,25 +21,14 @@ class PreviousPageDecorator # @return [String] def name - if content.interruption_page? - mod.content_start.name - else - content.previous_item.name - end - end - - # @return [String] - def previous_path previous_content = content.previous_item - if content.interruption_page? - Rails.application.routes.url_helpers.training_module_path(mod.name) - elsif skippable_page? - Rails.application.routes.url_helpers.training_module_page_path(mod.name, previous_content.previous_item.name) + if skippable_page? + previous_content.previous_item.name elsif skip_back_to_feedback_intro? - Rails.application.routes.url_helpers.training_module_page_path(mod.name, mod.opinion_intro_page.name) + mod.opinion_intro_page.name else - Rails.application.routes.url_helpers.training_module_page_path(mod.name, content.previous_item.name) + content.previous_item.name end end @@ -81,21 +70,11 @@ def previous? content.interruption_page? || disable_question_submission? end - # @return [Boolean] - def save? - content.notes? || content.summative_question? - end - # @return [Boolean] def answered? user.response_for(content).responded? end - # @return [Boolean] - def missing? - content.previous_item.eql?(content) && wip? - end - # @return [Boolean] def opinion_intro? content.opinion_intro? @@ -115,9 +94,4 @@ def skippable_page? def skip_back_to_feedback_intro? content.thankyou? && !user.response_for_shared(content.previous_item, mod).responded? end - - # @return [Boolean] - def wip? - Rails.application.preview? || Rails.env.test? - end end diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 3ed541674..f7ba97f66 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -41,10 +41,17 @@ def link_to_next # @return [String] previous page or module overview def link_to_previous - govuk_button_link_to previous_page.text, previous_page.previous_path, - id: 'previous-action', - class: previous_page.style, - aria: { label: t('pagination.previous') } + if content.interruption_page? + govuk_button_link_to previous_page.text, training_module_path(mod.name), + id: 'previous-action', + class: previous_page.style, + aria: { label: t('pagination.previous') } + else + govuk_button_link_to previous_page.text, training_module_page_path(mod.name, previous_page.name), + id: 'previous-action', + class: previous_page.style, + aria: { label: t('pagination.previous') } + end end # Bottom of my-modules card component From 27ffb4be853f2fe2b855549df99d20a5e9c89725 Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:30:02 +0000 Subject: [PATCH 23/95] Update logic for previous button, previous decorator spec and skippable question logic for next page decorator --- app/decorators/next_page_decorator.rb | 7 ++ app/helpers/link_helper.rb | 17 ++--- .../previous_page_decorator_spec.rb | 74 +------------------ 3 files changed, 17 insertions(+), 81 deletions(-) diff --git a/app/decorators/next_page_decorator.rb b/app/decorators/next_page_decorator.rb index a5e3e94b1..aa2f92a2d 100644 --- a/app/decorators/next_page_decorator.rb +++ b/app/decorators/next_page_decorator.rb @@ -23,6 +23,8 @@ class NextPageDecorator def name if content.interruption_page? mod.content_start.name + elsif skippable_page? + content.next_item.next_item.name else content.next_item.name end @@ -114,4 +116,9 @@ def content_section? def wip? Rails.application.preview? || Rails.env.test? end + + # @return [Boolean] + def skippable_page? + !content.interruption_page? && content.next_item.skippable? && user.response_for_shared(content.next_item, mod).responded? + end end diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index f7ba97f66..105b9efe1 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -41,17 +41,12 @@ def link_to_next # @return [String] previous page or module overview def link_to_previous - if content.interruption_page? - govuk_button_link_to previous_page.text, training_module_path(mod.name), - id: 'previous-action', - class: previous_page.style, - aria: { label: t('pagination.previous') } - else - govuk_button_link_to previous_page.text, training_module_page_path(mod.name, previous_page.name), - id: 'previous-action', - class: previous_page.style, - aria: { label: t('pagination.previous') } - end + path = content.interruption_page? ? training_module_path(mod.name) : training_module_page_path(mod.name, previous_page.name) + + govuk_button_link_to previous_page.text, path, + id: 'previous-action', + class: previous_page.style, + aria: { label: t('pagination.previous') } end # Bottom of my-modules card component diff --git a/spec/decorators/previous_page_decorator_spec.rb b/spec/decorators/previous_page_decorator_spec.rb index bf8d04452..8098c5303 100644 --- a/spec/decorators/previous_page_decorator_spec.rb +++ b/spec/decorators/previous_page_decorator_spec.rb @@ -18,18 +18,6 @@ expect(decorator.text).to eq 'Previous' end - it '#disable_question_submission?' do - expect(decorator.disable_question_submission?).to be false - end - - context 'when adding to the learning log' do - let(:content) { mod.page_by_name('1-1-3-1') } - - it '#text' do - expect(decorator.text).to eq 'Previous' - end - end - context 'when starting a section' do let(:content) { mod.page_by_name('1-2') } @@ -38,69 +26,15 @@ end end - context 'when starting an assessment' do - let(:content) { mod.page_by_name('1-3-2') } - - it '#text' do - expect(decorator.text).to eq 'Previous' - end - end - - context 'when answering an assessment question' do - let(:content) { mod.page_by_name('1-3-2-1') } - - it '#text' do - expect(decorator.text).to eq 'Previous' - end - end - - context 'when finishing an assessment' do - let(:content) { mod.page_by_name('1-3-2-10') } - - it '#text' do - expect(decorator.text).to eq 'Previous' - end - end - - context 'when reviewing a completed assessment' do - let(:content) { mod.page_by_name('1-3-2-1') } - let(:assessment) { instance_double(AssessmentProgress, graded?: true, score: 100) } - - before do - if Rails.application.migrated_answers? - create :response, - user: user, - question_name: '1-3-2-1', - question_type: 'summative', - training_module: 'alpha', - answers: [1], - assessment: create(:assessment, user: user) - else - create :user_answer, - user: user, - name: '1-3-2-1', - module: 'alpha', - assessments_type: 'summative_assessment', - question: 'N/A for CMS only questions', - questionnaire_id: 0, - answer: [1] - end - end + context 'when finishing a module and skipping feedback form' do + let(:content) { mod.page_by_name('1-3-3-5') } it '#text' do expect(decorator.text).to eq 'Previous' end - it '#disable_question_submission?' do - expect(decorator.disable_question_submission?).to be true - end - end - - context 'when finishing a module' do - let(:content) { mod.page_by_name('1-3-3-5') } - - it '#text' do - expect(decorator.text).to eq 'Previous' + it '#name' do + expect(decorator.name).to eq 'feedback-intro' end end end From 92eb28a4966ea19e193281684c4a4d537fd3a3c2 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Tue, 5 Mar 2024 15:43:41 +0000 Subject: [PATCH 24/95] use markdown in feedback intro --- ...leted_main_feedback_introduction.html.slim | 6 ----- .../_main_feedback_introduction.html.slim | 11 --------- app/views/feedback/index.html.slim | 14 +++++++++-- config/locales/en.yml | 24 +++++++++++-------- spec/forms/form_builder_spec.rb | 2 +- 5 files changed, 27 insertions(+), 30 deletions(-) delete mode 100644 app/views/feedback/_completed_main_feedback_introduction.html.slim delete mode 100644 app/views/feedback/_main_feedback_introduction.html.slim diff --git a/app/views/feedback/_completed_main_feedback_introduction.html.slim b/app/views/feedback/_completed_main_feedback_introduction.html.slim deleted file mode 100644 index bba3aa896..000000000 --- a/app/views/feedback/_completed_main_feedback_introduction.html.slim +++ /dev/null @@ -1,6 +0,0 @@ -h1= t('feedback.feedback_exists.heading') - -.govuk-button-group -.govuk-button-group - = govuk_button_link_to t('previous_page.previous'), my_modules_path, secondary: true - = govuk_button_link_to t('links.update_feedback'), feedback_path(mod.pages.first.name) \ No newline at end of file diff --git a/app/views/feedback/_main_feedback_introduction.html.slim b/app/views/feedback/_main_feedback_introduction.html.slim deleted file mode 100644 index c75951fd2..000000000 --- a/app/views/feedback/_main_feedback_introduction.html.slim +++ /dev/null @@ -1,11 +0,0 @@ -h1= t('feedback.heading') - -= m('feedback.body') - -h2= t('feedback.technical_support.heading') - -p.text-secondary= t('feedback.technical_support.body') - -hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' - -= govuk_button_link_to t('feedback.next_button'), feedback_path(mod.pages.first.name) \ No newline at end of file diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index a6df4e128..aa9776791 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -1,7 +1,17 @@ .govuk-grid-row .govuk-grid-column-full - if respondant.completed_main_feedback? - = render 'completed_main_feedback_introduction', mod: mod + = m('feedback.feedback_exists') + + .govuk-button-group + = govuk_button_link_to t('previous_page.previous'), my_modules_path, secondary: true + = govuk_button_link_to t('links.update_feedback'), feedback_path(mod.pages.first.name) - else - = render 'main_feedback_introduction', mod: mod + = m('feedback.intro') + + .govuk-button-group + = govuk_button_link_to t('next_page.give_feedback'), feedback_path(mod.pages.first.name) + + + diff --git a/config/locales/en.yml b/config/locales/en.yml index 68355fc03..1dfae1876 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -612,8 +612,9 @@ en: # /feedback feedback: - heading: Give feedback - body: | + intro: | + # Give feedback + The purpose of this feedback form is to gather your opinon on the child development training course that the Department for Education has created for early years practitioners. @@ -623,15 +624,18 @@ en: By completing this form you have understood the above and consent to take part. - feedback_exists: - heading: You have already submitted feedback - body: Thank you for helping to improve this training + feedback_exists: | + # You have already submitted feedback + + Thank you for helping to improve this training + + ## Technical support queries + + If you have any questions about how to sue this website or are experiencing any + technical issues, please use our contact form ADD LINK so that our team can follow up with your enquiry. - technical_support: - heading: Technical support queries - body: | - If you have any questions about how to sue this website or are experiencing any - technical issues, please use our contact form ADD LINK so that our team can follow up with your enquiry. + --- + # /feedback/thank-you thank_you: diff --git a/spec/forms/form_builder_spec.rb b/spec/forms/form_builder_spec.rb index 5488aef11..81fcd96e2 100644 --- a/spec/forms/form_builder_spec.rb +++ b/spec/forms/form_builder_spec.rb @@ -31,7 +31,7 @@ it 'options' do options = YAML.load_file Rails.root.join('data/setting-type.yml') - expect(output).to include *options.map { |s| s['name'] } + expect(output).to include(*options.map { |s| s['name'] }) end end end From 4ddc09471a3ff7255925d31bcb7f890babef9554 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 5 Mar 2024 15:59:48 +0000 Subject: [PATCH 25/95] Unit test for previous page --- .../previous_page_decorator_spec.rb | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/spec/decorators/previous_page_decorator_spec.rb b/spec/decorators/previous_page_decorator_spec.rb index 8098c5303..2a7600568 100644 --- a/spec/decorators/previous_page_decorator_spec.rb +++ b/spec/decorators/previous_page_decorator_spec.rb @@ -1,3 +1,13 @@ +# Helps to make notes: +# +# feedback-intro (opinion_intro) +# end-of-module-feedback-1 (feedback) +# end-of-module-feedback-3 (feedback) +# feedback-freetext (feedback) +# end-of-module-feedback-5 (feedback) <-- SKIPPABLE +# 1-3-3-5 (thankyou) +# +# require 'rails_helper' RSpec.describe PreviousPageDecorator do @@ -10,31 +20,40 @@ let(:content) { mod.page_by_name('1-1-2') } let(:assessment) { double } - it '#name' do - expect(decorator.name).to eq '1-1-1' - end - - it '#text' do - expect(decorator.text).to eq 'Previous' - end - - context 'when starting a section' do - let(:content) { mod.page_by_name('1-2') } - - it '#text' do + describe '#text' do + it do expect(decorator.text).to eq 'Previous' end end - context 'when finishing a module and skipping feedback form' do - let(:content) { mod.page_by_name('1-3-3-5') } - - it '#text' do - expect(decorator.text).to eq 'Previous' + describe '#name' do + it 'is previous page name' do + expect(decorator.name).to eq '1-1-1' end - it '#name' do - expect(decorator.name).to eq 'feedback-intro' + context 'when previous page is skippable' do + let(:content) { mod.page_by_name('1-3-3-5') } + + context 'and unanswered' do + it 'is one step back' do + expect(decorator.name).to eq 'end-of-module-feedback-5' + end + end + + context 'and answered' do + before do + create :response, + question_name: 'end-of-module-feedback-5', + training_module: 'alpha', + answers: [1], + correct: true, + user: create(:user) + end + + it 'is two steps back' do + expect(decorator.name).to eq 'feedback-freetext' + end + end end end end From 6469325cf2a5f2af91c453c2cc316e81bab4ffd6 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Tue, 5 Mar 2024 17:12:05 +0000 Subject: [PATCH 26/95] add last_option? method to option class --- app/forms/form_builder.rb | 14 +++++++------- app/models/training/answer.rb | 10 ++++++++++ app/models/training/question.rb | 6 ------ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index 5e91496f5..a3fc56148 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -18,8 +18,8 @@ def govuk_password_field(attribute_name, options = {}) # @return [String] def feedback_question_radio_buttons(content) @template.capture do - content.options.each.with_index(1) do |option, index| - if content.last_option?(index) && content.has_other? + content.options.each.with_index(1) do |option, _index| + if option.last_option? && content.has_other? @template.concat feedback_other_radio_button(content, option) else @template.concat question_radio_button(option) @@ -34,8 +34,8 @@ def feedback_question_radio_buttons(content) def end_of_module_feedback_question_radio_buttons(content, response) @template.capture do - response.options.each.with_index(1) do |option, index| - if content.last_option?(index) && content.has_other? + response.options.each.with_index(1) do |option, _index| + if option.last_option? && content.has_other? @template.concat feedback_other_radio_button(content, option) else @template.concat question_radio_button(option) @@ -52,10 +52,10 @@ def end_of_module_feedback_question_radio_buttons(content, response) # @return [String] def feedback_question_check_boxes(content) @template.capture do - content.options.each.with_index(1) do |option, index| - if content.last_option?(index) && content.other.present? + content.options.each.with_index(1) do |option, _index| + if option.last_option? && content.other.present? @template.concat feedback_other_check_box(content, option) - elsif content.last_option?(index) && content.or.present? + elsif option.last_option? && content.or.present? @template.concat @template.content_tag(:div, 'Or', class: 'govuk-checkboxes__divider') @template.concat govuk_check_box :answers, 'Or', label: { text: content.or }, link_errors: true else diff --git a/app/models/training/answer.rb b/app/models/training/answer.rb index 5b70ff72f..c67c9ed0c 100644 --- a/app/models/training/answer.rb +++ b/app/models/training/answer.rb @@ -13,6 +13,15 @@ class Answer < Dry::Struct # attribute :json, Types::Array.of(Types::Array).default(Types::EMPTY_ARRAY) + class Option < Dry::Struct + attribute :answer, Types.Instance(Answer) + + # @return [Boolean] + def last_option? + answer.options.last.eql?(self) + end + end + # @return [Boolean] def valid? options.all? && gteq?(options, 2) && gteq?(correct_answers, 1) @@ -58,6 +67,7 @@ def options(disabled: false, checked: []) correct: value, disabled: disabled, checked: checked.include?(order), + answer: self, ) end end diff --git a/app/models/training/question.rb b/app/models/training/question.rb index 4b117b08f..5e1752d8a 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -55,12 +55,6 @@ def has_other? other.present? end - # @param index [Integer] - # @return [Boolean] - def last_option?(index) - options.count == index - end - # @return [Boolean] def checkbox? response_type From a97e4bc013a4497c0a89d39c07f8a7d5d1db5f5d Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Wed, 6 Mar 2024 12:03:51 +0000 Subject: [PATCH 27/95] As discussed --- app/decorators/previous_page_decorator.rb | 59 ++++++------------- app/helpers/link_helper.rb | 1 - app/models/concerns/content_types.rb | 2 + .../previous_page_decorator_spec.rb | 30 ++++++++++ 4 files changed, 50 insertions(+), 42 deletions(-) diff --git a/app/decorators/previous_page_decorator.rb b/app/decorators/previous_page_decorator.rb index 3f6f98a87..2f885ef12 100644 --- a/app/decorators/previous_page_decorator.rb +++ b/app/decorators/previous_page_decorator.rb @@ -1,4 +1,3 @@ -# TODO: assessments controller #new can be removed # # Button or link labels to the previous page # @see [LinkHelper#link_to_previous] @@ -15,18 +14,13 @@ class PreviousPageDecorator # @!attribute [r] content # @return [Training::Page, Training::Question, Training::Video] option :content, Types::TrainingContent, required: true - # @!attribute [r] assessment - # @return [AssessmentProgress] - option :assessment, required: true # @return [String] def name - previous_content = content.previous_item - - if skippable_page? - previous_content.previous_item.name - elsif skip_back_to_feedback_intro? - mod.opinion_intro_page.name + if skip_previous_question? + content.previous_item.previous_item.name + elsif feedback_not_started? + mod.feedback_questions.first.previous_item.name # OPTIMIZE: doubtful a specific type is even necessary else content.previous_item.name end @@ -34,11 +28,7 @@ def name # @return [String] def style - if content.section? && !content.opinion_intro? - 'section-intro-previous-button' - else - 'govuk-button--secondary' - end + content_section? ? 'section-intro-previous-button' : 'govuk-button--secondary' end # @see [Pagination] @@ -47,17 +37,6 @@ def text label[:previous] end - # @return [Boolean] - def disable_question_submission? - if content.formative_question? - answered? - elsif content.summative_question? - answered? && (Rails.application.migrated_answers? ? assessment.graded? : assessment.score.present?) - else - false - end - end - private # @return [Hash=>Symbol] @@ -66,18 +45,10 @@ def label end # @return [Boolean] - def previous? - content.interruption_page? || disable_question_submission? - end - - # @return [Boolean] - def answered? - user.response_for(content).responded? - end + def answered?(question) + return false unless question.feedback_question? - # @return [Boolean] - def opinion_intro? - content.opinion_intro? + user.response_for_shared(question, mod).responded? end # @return [Boolean] @@ -86,12 +57,18 @@ def content_section? end # @return [Boolean] - def skippable_page? - !content.interruption_page? && content.previous_item.skippable? && user.response_for_shared(content.previous_item, mod).responded? + def skip_previous_question? + content.previous_item.skippable? && answered?(content.previous_item) end + # on the post-feedback page with the last feedback question unanswered + # + # - once a feedback question is answered the feedback form is started + # - a response for the last question is therefore sufficient to determine this + # - because you can't get here without answering the last question + # # @return [Boolean] - def skip_back_to_feedback_intro? - content.thankyou? && !user.response_for_shared(content.previous_item, mod).responded? + def feedback_not_started? + content.thankyou? && !answered?(content.previous_item) end end diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 105b9efe1..388b5fe65 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -90,7 +90,6 @@ def previous_page user: current_user, mod: mod, content: content, - assessment: assessment_progress_service(mod), ) end end diff --git a/app/models/concerns/content_types.rb b/app/models/concerns/content_types.rb index 27e4b6296..94bd36337 100644 --- a/app/models/concerns/content_types.rb +++ b/app/models/concerns/content_types.rb @@ -92,6 +92,8 @@ def confidence_question? # @return [Boolean] def opinion_intro? page_type.eql?('opinion_intro') + # def confidence_outro? + # page_type.eql?('confidence_outro') end # @return [Boolean] diff --git a/spec/decorators/previous_page_decorator_spec.rb b/spec/decorators/previous_page_decorator_spec.rb index 2a7600568..c2d8e2346 100644 --- a/spec/decorators/previous_page_decorator_spec.rb +++ b/spec/decorators/previous_page_decorator_spec.rb @@ -20,6 +20,20 @@ let(:content) { mod.page_by_name('1-1-2') } let(:assessment) { double } + describe '#style' do + it do + expect(decorator.style).to eq 'govuk-button--secondary' + end + + context 'when page introduces a section' do + let(:content) { mod.page_by_name('1-2') } + + it do + expect(decorator.style).to eq 'section-intro-previous-button' + end + end + end + describe '#text' do it do expect(decorator.text).to eq 'Previous' @@ -31,7 +45,23 @@ expect(decorator.name).to eq '1-1-1' end + context 'when feedback was skipped' do + let(:content) { mod.page_by_name('1-3-3-5') } + + it 'skips back to start of feedback form' do + expect(decorator.name).to eq 'feedback-intro' + end + end + context 'when previous page is skippable' do + # This context is insufficiently prepared + # + # The assertion here is that a special kind of feedback question is asked + # in every form but once answered is never asked again. + # + # Therefore, as we transition through 'alpha' in this spec, we need a scenario + # where the question was answered in the main feedack form or another module. + # let(:content) { mod.page_by_name('1-3-3-5') } context 'and unanswered' do From b24c5a84cd7574bd22ccde43ebd1032b340ac3a2 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Wed, 6 Mar 2024 12:47:55 +0000 Subject: [PATCH 28/95] Split logic between partial / builder / option for feedback form --- app/forms/form_builder.rb | 84 ++++++------------- app/models/concerns/content_types.rb | 4 +- app/models/training/answer.rb | 2 +- app/models/training/question.rb | 5 ++ app/views/feedback/_check_boxes.html.slim | 17 +++- app/views/feedback/_radio_buttons.html.slim | 14 +++- .../previous_page_decorator_spec.rb | 10 +-- 7 files changed, 62 insertions(+), 74 deletions(-) diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index a3fc56148..48f39dec8 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -14,75 +14,32 @@ def govuk_password_field(attribute_name, options = {}) super(attribute_name, **options.reverse_merge(width: 'two-thirds')) end - # @param content [Object] - # @return [String] - def feedback_question_radio_buttons(content) - @template.capture do - content.options.each.with_index(1) do |option, _index| - if option.last_option? && content.has_other? - @template.concat feedback_other_radio_button(content, option) - else - @template.concat question_radio_button(option) - end - end - - if content.has_hint? - @template.concat govuk_text_area :text_input, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } - end - end - end - - def end_of_module_feedback_question_radio_buttons(content, response) - @template.capture do - response.options.each.with_index(1) do |option, _index| - if option.last_option? && content.has_other? - @template.concat feedback_other_radio_button(content, option) - else - @template.concat question_radio_button(option) - end - end - - if content.has_hint? && content.has_other? - @template.concat govuk_text_area :text_input, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } - end - end - end - - # @param content [Object] + # @param option [Training::Answer::Option] + # @param text [String] # @return [String] - def feedback_question_check_boxes(content) - @template.capture do - content.options.each.with_index(1) do |option, _index| - if option.last_option? && content.other.present? - @template.concat feedback_other_check_box(content, option) - elsif option.last_option? && content.or.present? - @template.concat @template.content_tag(:div, 'Or', class: 'govuk-checkboxes__divider') - @template.concat govuk_check_box :answers, 'Or', label: { text: content.or }, link_errors: true - else - @template.concat question_check_box(option) - end - end + def other_check_box(option, text) + govuk_check_box :answers, + option.id, + label: { text: 'Other' }, # TODO: use locale + link_errors: true do + govuk_text_field :text_input, label: { text: text } end end - # @param content [Object] - # @param option [Object] The content for the 'Other' checkbox option + # @param option [Training::Answer::Option] + # @param text [String] # @return [String] - def feedback_other_check_box(content, option) - govuk_check_box :answers, option.id, label: { text: 'Other' }, link_errors: true do - govuk_text_field :text_input, label: { text: content.other } + def other_radio_button(option, text) + govuk_radio_button :answers, + option.id, + label: { text: option.label }, + link_errors: true, + checked: option.checked? do + govuk_text_field :text_input, label: { text: text } end end - # @param content [Object] - # @param option [Object] The content for the 'Other' radio button option # @return [String] - def feedback_other_radio_button(content, option) - govuk_radio_button :answers, option.id, label: { text: option.label }, link_errors: true, checked: option.checked? do - govuk_text_field :text_input, label: { text: content.other } - end - end - def terms_and_conditions_check_box govuk_check_box :terms_and_conditions_agreed_at, Time.zone.now, @@ -94,6 +51,7 @@ def terms_and_conditions_check_box end # @param option [Training::Answer::Option] + # @return [String] def question_radio_button(option) govuk_radio_button :answers, option.id, @@ -104,6 +62,7 @@ def question_radio_button(option) end # @param option [Training::Answer::Option] + # @return [String] def question_check_box(option) govuk_check_box :answers, option.id, @@ -113,6 +72,7 @@ def question_check_box(option) checked: option.checked? end + # @return [String] def select_trainee_setting govuk_collection_select :setting_type_id, Trainee::Setting.all, :name, :title, @@ -124,6 +84,7 @@ def select_trainee_setting form_group: { classes: %w[data-hj-suppress] } end + # @return [String] def select_trainee_authority govuk_collection_select :local_authority, Trainee::Authority.all, :name, :name, @@ -135,6 +96,7 @@ def select_trainee_authority form_group: { classes: %w[data-hj-suppress] } end + # @return [String] def select_trainee_experience govuk_collection_select :early_years_experience, Trainee::Experience.all, :name, :name, @@ -143,6 +105,8 @@ def select_trainee_experience form_group: { classes: %w[data-hj-suppress] } end + # @param field [Symbol] + # @return [String] def opt_in_out(field) govuk_collection_radio_buttons field, FormOption.build(field), :id, :name, diff --git a/app/models/concerns/content_types.rb b/app/models/concerns/content_types.rb index 94bd36337..4a055bee3 100644 --- a/app/models/concerns/content_types.rb +++ b/app/models/concerns/content_types.rb @@ -92,8 +92,8 @@ def confidence_question? # @return [Boolean] def opinion_intro? page_type.eql?('opinion_intro') - # def confidence_outro? - # page_type.eql?('confidence_outro') + # def confidence_outro? + # page_type.eql?('confidence_outro') end # @return [Boolean] diff --git a/app/models/training/answer.rb b/app/models/training/answer.rb index c67c9ed0c..7cabdfe7c 100644 --- a/app/models/training/answer.rb +++ b/app/models/training/answer.rb @@ -17,7 +17,7 @@ class Option < Dry::Struct attribute :answer, Types.Instance(Answer) # @return [Boolean] - def last_option? + def last? answer.options.last.eql?(self) end end diff --git a/app/models/training/question.rb b/app/models/training/question.rb index 5e1752d8a..758c96182 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -55,6 +55,11 @@ def has_other? other.present? end + # @return [Boolean] + def has_or? + self.or.present? + end + # @return [Boolean] def checkbox? response_type diff --git a/app/views/feedback/_check_boxes.html.slim b/app/views/feedback/_check_boxes.html.slim index eb8d902f2..a8c5b6b12 100644 --- a/app/views/feedback/_check_boxes.html.slim +++ b/app/views/feedback/_check_boxes.html.slim @@ -1,5 +1,16 @@ -h1 Feedback Partial - = f.govuk_check_boxes_fieldset :answers, legend: { text: response.legend } do + = f.hidden_field :answers + - response.options.each do |option| - = f.question_check_box(option) + + - if option.last? + - if content.has_other? + = f.other_check_box(option, content.other) + + - elsif content.has_or? + / TODO: use locale + = f.govuk_check_box_divider 'Or' + = f.govuk_check_box :answers, 'Or', label: { text: content.or }, link_errors: true + + - else + = f.question_check_box(option) diff --git a/app/views/feedback/_radio_buttons.html.slim b/app/views/feedback/_radio_buttons.html.slim index 32148c039..2d0ff588a 100644 --- a/app/views/feedback/_radio_buttons.html.slim +++ b/app/views/feedback/_radio_buttons.html.slim @@ -1,6 +1,14 @@ -h1 Feedback Partial - = f.govuk_radio_buttons_fieldset :answers, legend: { text: response.legend } do = f.hidden_field :answers + - response.options.each do |option| - = f.question_radio_button(option) + + - if option.last? + - if content.has_other? + = f.other_radio_button(content, option) + + - else + = f.question_radio_button(option) + + - if content.has_hint? + = f.govuk_text_area :text_input, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } diff --git a/spec/decorators/previous_page_decorator_spec.rb b/spec/decorators/previous_page_decorator_spec.rb index c2d8e2346..b45390ec7 100644 --- a/spec/decorators/previous_page_decorator_spec.rb +++ b/spec/decorators/previous_page_decorator_spec.rb @@ -73,11 +73,11 @@ context 'and answered' do before do create :response, - question_name: 'end-of-module-feedback-5', - training_module: 'alpha', - answers: [1], - correct: true, - user: create(:user) + question_name: 'end-of-module-feedback-5', + training_module: 'alpha', + answers: [1], + correct: true, + user: create(:user) end it 'is two steps back' do From 68258ffaeeef3d98f754ce623ed59a843cc24e23 Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:51:06 +0000 Subject: [PATCH 29/95] Update logic for pagination, create feedback page decorator and update skippable questions logic --- app/controllers/concerns/learning.rb | 6 +++- app/controllers/training/pages_controller.rb | 2 +- .../feedback_pagination_decorator.rb | 33 +++++++++++++++++++ app/decorators/module_overview_decorator.rb | 2 +- app/decorators/next_page_decorator.rb | 22 ++++++------- app/decorators/pagination_decorator.rb | 18 ++-------- app/decorators/previous_page_decorator.rb | 8 +++-- app/helpers/link_helper.rb | 2 -- app/models/concerns/content_types.rb | 10 ++---- app/models/concerns/pagination.rb | 12 ++++++- app/models/training/module.rb | 5 --- app/models/training/question.rb | 2 +- app/views/feedback/_free_text.html.slim | 6 ++++ app/views/training/pages/_content.html.slim | 3 +- .../training/pages/section_intro.html.slim | 1 - spec/models/concerns/content_types_spec.rb | 6 ---- 16 files changed, 81 insertions(+), 57 deletions(-) create mode 100644 app/decorators/feedback_pagination_decorator.rb create mode 100644 app/views/feedback/_free_text.html.slim diff --git a/app/controllers/concerns/learning.rb b/app/controllers/concerns/learning.rb index 34a0b2cd4..6226020d5 100644 --- a/app/controllers/concerns/learning.rb +++ b/app/controllers/concerns/learning.rb @@ -57,7 +57,11 @@ def module_table # @return [PaginationDecorator] def section_bar - PaginationDecorator.new(content) + if content.feedback_question? + FeedbackPaginationDecorator.new(content) + else + PaginationDecorator.new(content) + end end # ---------------------------------------------------------------------------- diff --git a/app/controllers/training/pages_controller.rb b/app/controllers/training/pages_controller.rb index 182059788..c28e2e581 100644 --- a/app/controllers/training/pages_controller.rb +++ b/app/controllers/training/pages_controller.rb @@ -38,7 +38,7 @@ def note end def render_page - if content.section? && !content.certificate? && !content.opinion_intro? + if content.section? && !content.certificate? && !content.feedback_question? render 'section_intro' else render content.page_type diff --git a/app/decorators/feedback_pagination_decorator.rb b/app/decorators/feedback_pagination_decorator.rb new file mode 100644 index 000000000..2f5ca70d5 --- /dev/null +++ b/app/decorators/feedback_pagination_decorator.rb @@ -0,0 +1,33 @@ +class FeedbackPaginationDecorator < PaginationDecorator + # TODO: Add type check for feedback question type + # @!attribute [r] content + # @return [Training::Question] + # param :content, Types::TrainingContent, required: true + + # @return [String] + def heading + 'Additional feedback' + end + + # @return [String] + # def section_numbers + # I18n.t(:section, scope: :pagination, current: content.submodule - 1, total: section_total - 1) + # end + + # private + + # @return [Integer] + # def page_total + # size = content.section_content.size + # if content.section_content.any?(&:skippable?) # && response_for_shared.responded? + # # don't count skipped page + # content.section_content.each do |section_content| + # if section_content.feedback_question? && section_content.skippable_question.eql?(false) + # size -= 1 + # end + # end + # end + + # size + # end +end diff --git a/app/decorators/module_overview_decorator.rb b/app/decorators/module_overview_decorator.rb index a60682bd5..83ae1d0c2 100644 --- a/app/decorators/module_overview_decorator.rb +++ b/app/decorators/module_overview_decorator.rb @@ -42,7 +42,7 @@ def sections display_line: position != mod.submodule_count, icon: status(content_items), subsections: subsections(submodule: submodule, items: content_items), - hide: content_items.first.opinion_intro?, + hide: content_items.first.feedback_form?, } end end diff --git a/app/decorators/next_page_decorator.rb b/app/decorators/next_page_decorator.rb index aa2f92a2d..f4914031c 100644 --- a/app/decorators/next_page_decorator.rb +++ b/app/decorators/next_page_decorator.rb @@ -34,14 +34,14 @@ def name # @return [String] def text case - when next? then label[:next] - when missing? then label[:missing] - when content_section? then label[:section] - when opinion_intro? then label[:give_feedback] # Make confidence outro - when test_start? then label[:start_test] - when test_finish? then label[:finish_test] - when finish? then label[:finish] - when save? then label[:save_continue] + when next? then label[:next] + when missing? then label[:missing] + when content_section? then label[:section] + when confidence_outro? then label[:give_feedback] + when test_start? then label[:start_test] + when test_finish? then label[:finish_test] + when finish? then label[:finish] + when save? then label[:save_continue] else label[:next] end @@ -103,13 +103,13 @@ def missing? end # @return [Boolean] - def opinion_intro? - content.opinion_intro? + def confidence_outro? + mod.feedback_questions.first.previous_item.eql?(content) end # @return [Boolean] def content_section? - content.section? && !content.opinion_intro? + content.section? && !content.first_feedback? end # @return [Boolean] diff --git a/app/decorators/pagination_decorator.rb b/app/decorators/pagination_decorator.rb index 5da6930cb..1960fac0f 100644 --- a/app/decorators/pagination_decorator.rb +++ b/app/decorators/pagination_decorator.rb @@ -15,11 +15,7 @@ def heading # @return [String] def section_numbers - if content.opinion_intro? || content.feedback_question? - I18n.t(:section, scope: :pagination, current: content.submodule - 1, total: section_total - 1) - else - I18n.t(:section, scope: :pagination, current: content.submodule, total: section_total - 1) - end + I18n.t(:section, scope: :pagination, current: content.submodule, total: section_total) end # @return [String] @@ -41,17 +37,7 @@ def current_page # @return [Integer] def page_total - size = content.section_content.size - if content.section_content.any?(&:skippable?) # && response_for_shared.responded? - # don't count skipped page - content.section_content.each do |section_content| - if section_content.feedback_question? && section_content.always_show_question.eql?(false) - size -= 1 - end - end - end - - size + content.section_content.size end # @return [Integer] diff --git a/app/decorators/previous_page_decorator.rb b/app/decorators/previous_page_decorator.rb index 2f885ef12..d60fdab89 100644 --- a/app/decorators/previous_page_decorator.rb +++ b/app/decorators/previous_page_decorator.rb @@ -28,7 +28,11 @@ def name # @return [String] def style - content_section? ? 'section-intro-previous-button' : 'govuk-button--secondary' + if content.section? && !content.first_feedback? + 'section-intro-previous-button' + else + 'govuk-button--secondary' + end end # @see [Pagination] @@ -53,7 +57,7 @@ def answered?(question) # @return [Boolean] def content_section? - content.section? && !content.opinion_intro? + content.section? && !content.first_feedback? end # @return [Boolean] diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 388b5fe65..1bc068cc9 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -69,8 +69,6 @@ def link_to_retake_or_results(mod) # @return [String, nil] thank you page (skips feedback questions) def link_to_skip - return unless content.opinion_intro? - govuk_link_to 'Skip feedback', training_module_page_path(mod.name, mod.thankyou_page.name) end diff --git a/app/models/concerns/content_types.rb b/app/models/concerns/content_types.rb index 4a055bee3..40c0490cf 100644 --- a/app/models/concerns/content_types.rb +++ b/app/models/concerns/content_types.rb @@ -89,16 +89,10 @@ def confidence_question? page_type.eql?('confidence_questionnaire') end - # @return [Boolean] - def opinion_intro? - page_type.eql?('opinion_intro') - # def confidence_outro? - # page_type.eql?('confidence_outro') - end - # @return [Boolean] def one_off_question? - always_show_question.eql?(false) + # is this in the right place? + skippable_question.eql?(true) end # @return [Boolean] diff --git a/app/models/concerns/pagination.rb b/app/models/concerns/pagination.rb index 44d22bb96..29870e0e0 100644 --- a/app/models/concerns/pagination.rb +++ b/app/models/concerns/pagination.rb @@ -3,7 +3,7 @@ module Pagination # @return [Boolean] def section? - submodule_intro? || summary_intro? || opinion_intro? || certificate? + submodule_intro? || summary_intro? || feedback_form? || certificate? end # @return [Boolean] @@ -97,6 +97,16 @@ def position_within(collection) (collection.index(self) + 1).ordinalize end + # @return [Boolean] + def feedback_form? + parent.feedback_questions.first.eql?(self) + end + + # @return [Boolean] + def feedback_form_heading + parent.page_by_id('feedback-intro') + end + private # @return [Integer] diff --git a/app/models/training/module.rb b/app/models/training/module.rb index ebf74f41b..9147407ef 100644 --- a/app/models/training/module.rb +++ b/app/models/training/module.rb @@ -198,11 +198,6 @@ def confidence_intro_page content.find(&:confidence_intro?) end - # @return [Training::Page] - def opinion_intro_page - content.find(&:opinion_intro?) - end - # @return [Training::Page] def thankyou_page content.find(&:thankyou?) diff --git a/app/models/training/question.rb b/app/models/training/question.rb index 758c96182..f722842d6 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -16,7 +16,7 @@ def answer # @return [Boolean] def skippable? - !always_show_question + skippable_question end # @return [String] powered by JSON not type diff --git a/app/views/feedback/_free_text.html.slim b/app/views/feedback/_free_text.html.slim new file mode 100644 index 000000000..c387248fc --- /dev/null +++ b/app/views/feedback/_free_text.html.slim @@ -0,0 +1,6 @@ += f.govuk_fieldset legend: { text: m(content.legend) } do + - if content.has_hint? + = m(content.hint) + + = f.govuk_text_area :text_input, label: nil, rows: 9 + \ No newline at end of file diff --git a/app/views/training/pages/_content.html.slim b/app/views/training/pages/_content.html.slim index c6b1b26f6..0f2cb9cc5 100644 --- a/app/views/training/pages/_content.html.slim +++ b/app/views/training/pages/_content.html.slim @@ -11,4 +11,5 @@ .govuk-button-group = link_to_previous = link_to_next - = link_to_skip + - if content.next_item.feedback_question? + = link_to_skip diff --git a/app/views/training/pages/section_intro.html.slim b/app/views/training/pages/section_intro.html.slim index 3487fcadd..e890eabb1 100644 --- a/app/views/training/pages/section_intro.html.slim +++ b/app/views/training/pages/section_intro.html.slim @@ -18,4 +18,3 @@ .govuk-button-group = link_to_previous = link_to_next - = link_to_skip diff --git a/spec/models/concerns/content_types_spec.rb b/spec/models/concerns/content_types_spec.rb index 7b79310f0..63ea05365 100644 --- a/spec/models/concerns/content_types_spec.rb +++ b/spec/models/concerns/content_types_spec.rb @@ -88,12 +88,6 @@ specify { expect(content).to be_confidence_question } end - describe '#opinion_intro?' do - before { content.page_type = 'opinion_intro' } - - specify { expect(content).to be_opinion_intro } - end - describe '#feedback_question?' do before { content.page_type = 'feedback' } From a027dea3ec617ac76651772287ecb10ccf688417 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Wed, 6 Mar 2024 16:09:51 +0000 Subject: [PATCH 30/95] Form partials were being hard-coded to narrow designs when they should be more flexible --- app/forms/form_builder.rb | 53 ++++++++++--------- app/models/concerns/content_types.rb | 6 +-- app/views/feedback/_check_boxes.html.slim | 8 +-- app/views/feedback/_radio_buttons.html.slim | 7 ++- .../_main_feedback_check_boxes.html.slim | 7 --- .../_main_feedback_free_text.html.slim | 6 --- 6 files changed, 42 insertions(+), 45 deletions(-) delete mode 100644 app/views/training/questions/_main_feedback_check_boxes.html.slim delete mode 100644 app/views/training/questions/_main_feedback_free_text.html.slim diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index 48f39dec8..2a9491976 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -14,31 +14,6 @@ def govuk_password_field(attribute_name, options = {}) super(attribute_name, **options.reverse_merge(width: 'two-thirds')) end - # @param option [Training::Answer::Option] - # @param text [String] - # @return [String] - def other_check_box(option, text) - govuk_check_box :answers, - option.id, - label: { text: 'Other' }, # TODO: use locale - link_errors: true do - govuk_text_field :text_input, label: { text: text } - end - end - - # @param option [Training::Answer::Option] - # @param text [String] - # @return [String] - def other_radio_button(option, text) - govuk_radio_button :answers, - option.id, - label: { text: option.label }, - link_errors: true, - checked: option.checked? do - govuk_text_field :text_input, label: { text: text } - end - end - # @return [String] def terms_and_conditions_check_box govuk_check_box :terms_and_conditions_agreed_at, @@ -61,6 +36,20 @@ def question_radio_button(option) checked: option.checked? end + # @param option [Training::Answer::Option] + # @option text [String] + # @return [String] + def other_radio_button(option, text:) + govuk_radio_button :answers, + option.id, + label: { text: option.label }, + link_errors: true, + disabled: option.disabled?, + checked: option.checked? do + govuk_text_field :text_input, label: { text: text } + end + end + # @param option [Training::Answer::Option] # @return [String] def question_check_box(option) @@ -72,6 +61,20 @@ def question_check_box(option) checked: option.checked? end + # @param option [Training::Answer::Option] + # @option text [String] + # @return [String] + def other_check_box(option, text:) + govuk_check_box :answers, + option.id, + label: { text: option.label }, + link_errors: true, + disabled: option.disabled?, + checked: option.checked? do + govuk_text_field :text_input, label: { text: text } + end + end + # @return [String] def select_trainee_setting govuk_collection_select :setting_type_id, diff --git a/app/models/concerns/content_types.rb b/app/models/concerns/content_types.rb index 40c0490cf..d7a057e02 100644 --- a/app/models/concerns/content_types.rb +++ b/app/models/concerns/content_types.rb @@ -22,17 +22,17 @@ def topic_intro? # @return [Boolean] def is_question? - page_type.match?(/formative|summative|confidence|feedback/) + factual_question? || opinion_question? end # @return [Boolean] def opinion_question? - page_type.match?(/confidence|feedback/) + confidence_question? || feedback_question? end # @return [Boolean] def factual_question? - page_type.match?(/formative|summative/) + formative_question? || summative_question? end # @return [Boolean] diff --git a/app/views/feedback/_check_boxes.html.slim b/app/views/feedback/_check_boxes.html.slim index a8c5b6b12..6d76823de 100644 --- a/app/views/feedback/_check_boxes.html.slim +++ b/app/views/feedback/_check_boxes.html.slim @@ -1,16 +1,18 @@ = f.govuk_check_boxes_fieldset :answers, legend: { text: response.legend } do - = f.hidden_field :answers - response.options.each do |option| - if option.last? - if content.has_other? - = f.other_check_box(option, content.other) + = f.other_check_box(option, text: content.other) - - elsif content.has_or? + - if content.has_or? / TODO: use locale = f.govuk_check_box_divider 'Or' = f.govuk_check_box :answers, 'Or', label: { text: content.or }, link_errors: true - else = f.question_check_box(option) + + - if content.has_hint? + = f.govuk_text_area :text_input, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } diff --git a/app/views/feedback/_radio_buttons.html.slim b/app/views/feedback/_radio_buttons.html.slim index 2d0ff588a..36ed5273b 100644 --- a/app/views/feedback/_radio_buttons.html.slim +++ b/app/views/feedback/_radio_buttons.html.slim @@ -5,7 +5,12 @@ - if option.last? - if content.has_other? - = f.other_radio_button(content, option) + = f.other_radio_button(option, text: content.other) + + - if content.has_or? + / TODO: use locale + = f.govuk_radio_divider 'Or' + = f.govuk_radio_button :answers, 'Or', label: { text: content.or }, link_errors: true - else = f.question_radio_button(option) diff --git a/app/views/training/questions/_main_feedback_check_boxes.html.slim b/app/views/training/questions/_main_feedback_check_boxes.html.slim deleted file mode 100644 index 599d900c8..000000000 --- a/app/views/training/questions/_main_feedback_check_boxes.html.slim +++ /dev/null @@ -1,7 +0,0 @@ -= f.govuk_check_boxes_fieldset :answers, multiple: true, legend: { text: content.legend } do - = f.hidden_field :answers, multiple: true - - - if content.has_hint? - = m(content.hint) - - = f.feedback_question_check_boxes(content) diff --git a/app/views/training/questions/_main_feedback_free_text.html.slim b/app/views/training/questions/_main_feedback_free_text.html.slim deleted file mode 100644 index a3e38da72..000000000 --- a/app/views/training/questions/_main_feedback_free_text.html.slim +++ /dev/null @@ -1,6 +0,0 @@ -= f.govuk_fieldset legend: { text: m(content.legend) } do - - - if content.has_hint? - = m(content.hint) - - = f.govuk_text_area :text_input, label: nil, multiple: true From b4fab8411e45ad92353316d735f38c884cdaf8e6 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Thu, 7 Mar 2024 14:09:18 +0000 Subject: [PATCH 31/95] Apply consistent patterns and conventions - work on unauthenticated visitor journey - model validations - form logic and field building - debugging --- app/components/header_component.rb | 9 +++- app/controllers/application_controller.rb | 8 +++- app/controllers/feedback_controller.rb | 16 +++---- app/controllers/training/pages_controller.rb | 2 +- .../training/responses_controller.rb | 6 +-- app/decorators/module_overview_decorator.rb | 2 +- app/decorators/previous_page_decorator.rb | 10 ++--- app/forms/form_builder.rb | 21 ++++++--- app/models/concerns/content_types.rb | 6 --- app/models/concerns/pagination.rb | 13 ++---- app/models/course.rb | 8 +++- app/models/guest.rb | 29 ++++++++++--- app/models/response.rb | 42 ++++++++++++++---- app/models/training/answer.rb | 13 ++---- app/models/training/content.rb | 10 ++--- app/models/training/question.rb | 43 ++++++++++++------- app/models/user.rb | 20 +++------ app/services/content_integrity.rb | 3 +- app/views/feedback/_check_boxes.html.slim | 11 +---- app/views/feedback/_radio_buttons.html.slim | 13 +++--- ...ee_text.html.slim => _text_area.html.slim} | 0 app/views/feedback/index.html.slim | 2 +- app/views/learning/_debug.html.slim | 4 +- app/views/training/modules/_debug.html.slim | 2 + app/views/training/questions/_debug.html.slim | 18 ++++++++ spec/models/guest_spec.rb | 34 +++++++++++++++ spec/models/user_spec.rb | 6 +++ 27 files changed, 225 insertions(+), 126 deletions(-) rename app/views/feedback/{_free_text.html.slim => _text_area.html.slim} (100%) create mode 100644 spec/models/guest_spec.rb diff --git a/app/components/header_component.rb b/app/components/header_component.rb index 8017b942f..2cae6bee5 100644 --- a/app/components/header_component.rb +++ b/app/components/header_component.rb @@ -14,7 +14,12 @@ def default_attributes def navigation_html_attributes nc = %w[dfe-header__navigation] << custom_navigation_classes.compact - { id: 'header-navigation', class: nc, data: { 'reveal-target': 'item' }, aria: { label: navigation_label, labelledby: 'label-navigation' } } + { + id: 'header-navigation', + class: nc, + data: { 'reveal-target': 'item' }, + aria: { label: navigation_label, labelledby: 'label-navigation' }, + } end def container_html_attributes @@ -31,7 +36,7 @@ def crown_fallback_image_attributes class ActionLinkItem < GovukComponent::HeaderComponent::NavigationItem def active_class - ['dfe-header__action-item--current'] if active? + %w[dfe-header__action-item--current] if active? end def call diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e6ca56aad..d9d4ac986 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -88,16 +88,22 @@ def set_time_zone(&block) end # @see Auditing - # @return [User] + # @return [User, nil] def current_user return bot if bot? super end + # @return [Guest, nil] + def guest + Guest.new(visit: current_visit) if current_visit.present? + end + # @see Auditing # @return [Boolean] def user_signed_in? + return false if current_user&.guest? return true if bot? super diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index ddc1a545e..2901f6cea 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -1,8 +1,7 @@ class FeedbackController < ApplicationController helper_method :content, :mod, - :current_user_response, - :respondant + :current_user_response def show; end @@ -16,17 +15,11 @@ def update end end - # @return [User | Guest] - def respondant - @respondant ||= current_user || Guest.new(visit_id: current_visit.id) - end - private # @return [Boolean] def save_response! current_user_response.update( - question_type: 'feedback', answers: user_answers, correct: true, text_input: response_params[:text_input], @@ -43,9 +36,14 @@ def content mod.page_by_name(question_name) end + # @return [User, Guest, nil] + def current_user + super || guest + end + # @return [Response] def current_user_response - @current_user_response ||= respondant.response_for_shared(content, mod) + @current_user_response ||= current_user.response_for_shared(content, mod) end # @return [String] diff --git a/app/controllers/training/pages_controller.rb b/app/controllers/training/pages_controller.rb index c28e2e581..a01dbab70 100644 --- a/app/controllers/training/pages_controller.rb +++ b/app/controllers/training/pages_controller.rb @@ -38,7 +38,7 @@ def note end def render_page - if content.section? && !content.certificate? && !content.feedback_question? + if content.section? && !content.certificate? render 'section_intro' else render content.page_type diff --git a/app/controllers/training/responses_controller.rb b/app/controllers/training/responses_controller.rb index 9ffc8dfa2..a0990287d 100644 --- a/app/controllers/training/responses_controller.rb +++ b/app/controllers/training/responses_controller.rb @@ -17,14 +17,10 @@ class ResponsesController < ApplicationController layout 'hero' def update - if save_response! || (content.feedback_question? && content.options.blank?) + if save_response! track_question_answer redirect else - if content.feedback_question? && user_answer_text.blank? && content.options.present? - current_user_response.errors.clear - current_user_response.errors.add :answers, :invalid - end render 'training/questions/show', status: :unprocessable_entity end end diff --git a/app/decorators/module_overview_decorator.rb b/app/decorators/module_overview_decorator.rb index 83ae1d0c2..09220055f 100644 --- a/app/decorators/module_overview_decorator.rb +++ b/app/decorators/module_overview_decorator.rb @@ -42,7 +42,7 @@ def sections display_line: position != mod.submodule_count, icon: status(content_items), subsections: subsections(submodule: submodule, items: content_items), - hide: content_items.first.feedback_form?, + hide: content_items.first.feedback_question?, } end end diff --git a/app/decorators/previous_page_decorator.rb b/app/decorators/previous_page_decorator.rb index d60fdab89..78e5d572c 100644 --- a/app/decorators/previous_page_decorator.rb +++ b/app/decorators/previous_page_decorator.rb @@ -20,7 +20,7 @@ def name if skip_previous_question? content.previous_item.previous_item.name elsif feedback_not_started? - mod.feedback_questions.first.previous_item.name # OPTIMIZE: doubtful a specific type is even necessary + mod.feedback_questions.first.previous_item.name else content.previous_item.name end @@ -28,11 +28,7 @@ def name # @return [String] def style - if content.section? && !content.first_feedback? - 'section-intro-previous-button' - else - 'govuk-button--secondary' - end + content_section? ? 'section-intro-previous-button' : 'govuk-button--secondary' end # @see [Pagination] @@ -57,7 +53,7 @@ def answered?(question) # @return [Boolean] def content_section? - content.section? && !content.first_feedback? + content.section? && !content.feedback_question? end # @return [Boolean] diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index 2a9491976..b1b207db4 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -41,15 +41,26 @@ def question_radio_button(option) # @return [String] def other_radio_button(option, text:) govuk_radio_button :answers, - option.id, - label: { text: option.label }, - link_errors: true, - disabled: option.disabled?, - checked: option.checked? do + option.id, + label: { text: option.label }, + link_errors: true, + disabled: option.disabled?, + checked: option.checked? do govuk_text_field :text_input, label: { text: text } end end + # @param option [Training::Answer::Option] + # @option text [String] + # @return [String] + def or_radio_button(text:, checked:) + govuk_radio_button :answers, + 0, + label: { text: text }, + link_errors: true, + checked: checked + end + # @param option [Training::Answer::Option] # @return [String] def question_check_box(option) diff --git a/app/models/concerns/content_types.rb b/app/models/concerns/content_types.rb index d7a057e02..3e2dbb937 100644 --- a/app/models/concerns/content_types.rb +++ b/app/models/concerns/content_types.rb @@ -89,12 +89,6 @@ def confidence_question? page_type.eql?('confidence_questionnaire') end - # @return [Boolean] - def one_off_question? - # is this in the right place? - skippable_question.eql?(true) - end - # @return [Boolean] def feedback_question? page_type.eql?('feedback') diff --git a/app/models/concerns/pagination.rb b/app/models/concerns/pagination.rb index 29870e0e0..741cf36b6 100644 --- a/app/models/concerns/pagination.rb +++ b/app/models/concerns/pagination.rb @@ -3,7 +3,7 @@ module Pagination # @return [Boolean] def section? - submodule_intro? || summary_intro? || feedback_form? || certificate? + submodule_intro? || summary_intro? || feedback_intro? || certificate? end # @return [Boolean] @@ -97,18 +97,13 @@ def position_within(collection) (collection.index(self) + 1).ordinalize end - # @return [Boolean] - def feedback_form? - parent.feedback_questions.first.eql?(self) - end +private # @return [Boolean] - def feedback_form_heading - parent.page_by_id('feedback-intro') + def feedback_intro? + feedback_question? && first_feedback? end -private - # @return [Integer] def content_index parent.pages.rindex(self) diff --git a/app/models/course.rb b/app/models/course.rb index d76a90b1f..52e4a8c32 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -24,6 +24,7 @@ def self.config # @return [nil] mod.name def name + # OPTIMIZE: this could return a string nil end @@ -49,9 +50,14 @@ def pages question end end - alias_method :feedback_questions, :pages + # @see Pagination + # @return [Training::Question] + def page_by_id(id) + pages.find { |page| page.id.eql?(id) } + end + # @return [Training::Question] def page_by_name(name) pages.find { |page| page.name.eql?(name) } diff --git a/app/models/guest.rb b/app/models/guest.rb index fa32e9580..bca20cf81 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -1,21 +1,38 @@ class Guest < Dry::Struct - attribute :visit_id, Types::Strict::Integer + # @!attribute [r] visit + # @return [Visit] + attribute :visit, Types.Instance(Visit) + # @return [Boolean] def guest? true end - # @param content [Training::Question] + # @param content [Training::Question] feedback questions # @return [Response] - def response_for_shared(content, _) - Response.find_or_initialize_by( + def response_for_shared(content, _mod) + responses.find_or_initialize_by( + question_type: content.page_type, question_name: content.name, - guest_visit: visit_id, + # training_module: nil, + guest_visit: visit.id, ) end + # @return [Boolean] + # def started_main_feedback? + # responses.any? + # end + # @return [Boolean] def completed_main_feedback? - Response.where(guest_visit: visit_id).main_feedback.any? + Course.config.pages.count.eql? responses.count + end + +private + + # @return [Response::ActiveRecord_Relation] + def responses + Response.main_feedback.where(guest_visit: visit.id) end end diff --git a/app/models/response.rb b/app/models/response.rb index 0a4954513..33948da5d 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -7,8 +7,11 @@ class Response < ApplicationRecord belongs_to :user, optional: true belongs_to :assessment, optional: true - validates :answers, presence: true, unless: -> { free_text_answer? } - validates :text_input, presence: true, if: -> { free_text_answer? || other_selected? } + # validates :training_module, presence: true + validates :question_type, inclusion: { in: %w(formative summative confidence feedback) } + + validates :answers, presence: true, unless: -> { text_input_only? } + validates :text_input, presence: true, if: -> { text_input_extra? } scope :incorrect, -> { where(correct: false) } scope :correct, -> { where(correct: true) } @@ -20,6 +23,8 @@ class Response < ApplicationRecord scope :summative, -> { where(question_type: 'summative') } scope :confidence, -> { where(question_type: 'confidence') } scope :feedback, -> { where(question_type: 'feedback') } + + # OPTIMIZE: module name needn't be nil now scope :main_feedback, -> { where(question_type: 'feedback', training_module: nil) } delegate :to_partial_path, :legend, to: :question @@ -42,14 +47,38 @@ def question def options if question.formative_question? || assessment&.graded? question.options(checked: answers, disabled: responded?) + + # the numeric value for "Or" could be all options plus one but "zero" is consistent + elsif question.feedback_question? && !question.has_other? && question.has_or? && text_input.present? + question.options(checked: [0]) + else question.options(checked: answers) end end # @return [Boolean] - def other_selected? - answers.include?('Other') + def checked_other? + question.has_other? && answers.include?(question.options.last.id) + # question.has_other? && question.options.last.checked? + end + + # @see #options + # Additional "Or" option is given index zero + # @return [Boolean] + def checked_or? + answers.include?(0) + # answers.pop.zero? + end + + # @return [Boolean] + def text_input_only? + question.only_text? + end + + # @return [Boolean] + def text_input_extra? + question.and_text? && checked_other? end # @return [Boolean] @@ -72,11 +101,6 @@ def revised? correct && !correct? end - # @return [Boolean] LIES!! - def free_text_answer? - question.free_text? && text_input.present? unless training_module.nil? - end - ######################## # Decorators # ######################## diff --git a/app/models/training/answer.rb b/app/models/training/answer.rb index 7cabdfe7c..6c695f52a 100644 --- a/app/models/training/answer.rb +++ b/app/models/training/answer.rb @@ -13,15 +13,6 @@ class Answer < Dry::Struct # attribute :json, Types::Array.of(Types::Array).default(Types::EMPTY_ARRAY) - class Option < Dry::Struct - attribute :answer, Types.Instance(Answer) - - # @return [Boolean] - def last? - answer.options.last.eql?(self) - end - end - # @return [Boolean] def valid? options.all? && gteq?(options, 2) && gteq?(correct_answers, 1) @@ -67,7 +58,7 @@ def options(disabled: false, checked: []) correct: value, disabled: disabled, checked: checked.include?(order), - answer: self, + last: json.size.eql?(order), ) end end @@ -97,10 +88,12 @@ class Option < Dry::Struct attribute :correct, Types::Params::Bool.fallback(false) attribute :disabled, Types::Params::Bool.default(false) attribute :checked, Types::Params::Bool.default(false) + attribute :last, Types::Params::Bool.default(false) alias_method :correct?, :correct alias_method :checked?, :checked alias_method :disabled?, :disabled + alias_method :last?, :last # @return [Hash{Symbol => nil, String}] def schema diff --git a/app/models/training/content.rb b/app/models/training/content.rb index 9b7db1f63..64580fd2b 100644 --- a/app/models/training/content.rb +++ b/app/models/training/content.rb @@ -16,11 +16,6 @@ def parent @parent ||= Training::Module.by_content_id(id) end - # @return [Boolean] - def skippable? - false - end - # @return [String] def debug_summary <<~SUMMARY @@ -62,5 +57,10 @@ def title def notes? (topic_intro? || text_page?) && notes end + + # @return [Boolean] + def skippable? + false + end end end diff --git a/app/models/training/question.rb b/app/models/training/question.rb index f722842d6..5e96dd325 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -14,19 +14,17 @@ def answer @answer ||= Answer.new(json: json) end - # @return [Boolean] - def skippable? - skippable_question - end - # @return [String] powered by JSON not type def to_partial_path partial = multi_select? ? 'check_boxes' : 'radio_buttons' - return "feedback/#{partial}" if feedback_question? - - partial = "learning_#{partial}" if formative_question? - "training/questions/#{partial}" + if feedback_question? + partial = 'text_area' if only_text? + "feedback/#{partial}" + else + partial = "learning_#{partial}" if formative_question? + "training/questions/#{partial}" + end end # @return [Boolean] @@ -40,29 +38,44 @@ def multi_select? end end - # @return [Boolean] feedback free text - def free_text? + # @return [Boolean] + def skippable? + feedback_question? && skippable_question + end + + # @return [Boolean] + def no_options? feedback_question? && options.empty? end + # @return [Boolean] + def only_text? + no_options? && !has_hint? + end + + # @return [Boolean] + def and_text? + !no_options? && (has_hint? || has_or?) + end + # @return [Boolean] def has_hint? - hint.present? + feedback_question? && hint.present? end # @return [Boolean] def has_other? - other.present? + feedback_question? && other.present? end # @return [Boolean] def has_or? - self.or.present? + feedback_question? && self.or.present? end # @return [Boolean] def checkbox? - response_type + feedback_question? && response_type end # @return [Boolean] event tracking diff --git a/app/models/user.rb b/app/models/user.rb index e5066a8e3..d30b8e89d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -201,6 +201,7 @@ def update_with_password(params) # @return [UserAnswer, Response] def response_for_shared(content, mod) responses.find_or_initialize_by( + question_type: content.page_type, question_name: content.name, training_module: mod.name, ) @@ -219,19 +220,12 @@ def response_for(content) assessments.create(training_module: content.parent.name, started_at: Time.zone.now) end - if content.feedback_question? - responses.find_or_initialize_by( - question_name: content.name, - training_module: module_name, - ) - else - responses.find_or_initialize_by( - assessment_id: assessment&.id, - training_module: content.parent.name, - question_name: content.name, - question_type: content.question_type, # TODO: RENAME options for Question#page_type removing "questionnaire" suffix - ) - end + responses.find_or_initialize_by( + assessment_id: assessment&.id, + training_module: content.parent.name, + question_name: content.name, + question_type: content.question_type, # TODO: RENAME options for Question#page_type removing "questionnaire" suffix + ) else user_answers.find_or_initialize_by( assessments_type: content.assessments_type, diff --git a/app/services/content_integrity.rb b/app/services/content_integrity.rb index 726ca4d33..baee624c2 100644 --- a/app/services/content_integrity.rb +++ b/app/services/content_integrity.rb @@ -45,7 +45,8 @@ class ContentIntegrity summative: 'Insufficient summative questions', confidence: 'Insufficient confidence checks', - question_answers: 'Question answers are incorrectly formatted', # TODO: which question? + # NB: disabled until new validity of feedback questions can be asserted + # question_answers: 'Question answers are incorrectly formatted', # TODO: which question? }.freeze # @return [nil] diff --git a/app/views/feedback/_check_boxes.html.slim b/app/views/feedback/_check_boxes.html.slim index 6d76823de..1bd226b4d 100644 --- a/app/views/feedback/_check_boxes.html.slim +++ b/app/views/feedback/_check_boxes.html.slim @@ -2,15 +2,8 @@ - response.options.each do |option| - - if option.last? - - if content.has_other? - = f.other_check_box(option, text: content.other) - - - if content.has_or? - / TODO: use locale - = f.govuk_check_box_divider 'Or' - = f.govuk_check_box :answers, 'Or', label: { text: content.or }, link_errors: true - + - if content.has_other? && option.last? + = f.other_check_box(option, text: content.other) - else = f.question_check_box(option) diff --git a/app/views/feedback/_radio_buttons.html.slim b/app/views/feedback/_radio_buttons.html.slim index 36ed5273b..88844b188 100644 --- a/app/views/feedback/_radio_buttons.html.slim +++ b/app/views/feedback/_radio_buttons.html.slim @@ -3,14 +3,13 @@ - response.options.each do |option| - - if option.last? - - if content.has_other? - = f.other_radio_button(option, text: content.other) + - if content.has_other? && option.last? + = f.other_radio_button(option, text: content.other) - - if content.has_or? - / TODO: use locale - = f.govuk_radio_divider 'Or' - = f.govuk_radio_button :answers, 'Or', label: { text: content.or }, link_errors: true + - elsif content.has_or? && option.last? + = f.question_radio_button(option) + = f.govuk_radio_divider 'Or' + = f.or_radio_button(text: content.or, checked: response.checked_or?) - else = f.question_radio_button(option) diff --git a/app/views/feedback/_free_text.html.slim b/app/views/feedback/_text_area.html.slim similarity index 100% rename from app/views/feedback/_free_text.html.slim rename to app/views/feedback/_text_area.html.slim diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index aa9776791..d5b5914fd 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -1,6 +1,6 @@ .govuk-grid-row .govuk-grid-column-full - - if respondant.completed_main_feedback? + - if current_user.completed_main_feedback? = m('feedback.feedback_exists') .govuk-button-group diff --git a/app/views/learning/_debug.html.slim b/app/views/learning/_debug.html.slim index ff5cf4f67..b0bb2fa2e 100644 --- a/app/views/learning/_debug.html.slim +++ b/app/views/learning/_debug.html.slim @@ -1,5 +1,3 @@ - if debug? pre.debug_dump - - current_user.course.debug_summary.each do |summary| - = summary - hr + = current_user.course.debug_summary diff --git a/app/views/training/modules/_debug.html.slim b/app/views/training/modules/_debug.html.slim index 551f04b71..2dde998ed 100644 --- a/app/views/training/modules/_debug.html.slim +++ b/app/views/training/modules/_debug.html.slim @@ -1,5 +1,7 @@ - if debug? pre.debug_dump + = govuk_link_to 'Structure', training_module_structure_path(mod.name) + hr h4 Module = mod.debug_summary diff --git a/app/views/training/questions/_debug.html.slim b/app/views/training/questions/_debug.html.slim index ac7358407..7f377ebeb 100644 --- a/app/views/training/questions/_debug.html.slim +++ b/app/views/training/questions/_debug.html.slim @@ -12,8 +12,26 @@ hr | Multiple choice: #{content.multi_select?} hr + | FEEDBACK + br + | Hint: #{content.hint} + br + | Or: #{content.or} + br + | Other: #{content.other} + br + | Only text input: #{content.only_text?} + br + | Additional text input: #{content.and_text?} + hr | Responded: #{current_user_response.responded?} hr + | Answered: + br + | Chosen option(s): #{current_user_response.answers} + br + | Text input: #{current_user_response.text_input} + hr | Response Options: - current_user_response.options.map do |option| br diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb new file mode 100644 index 000000000..043e41544 --- /dev/null +++ b/spec/models/guest_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +RSpec.describe Guest, type: :model do + subject(:guest) { described_class.new(visit: visit) } + + let(:visit) { create :visit } + + it 'needs a visit' do + expect { described_class.new }.to raise_error Dry::Struct::Error + expect { guest }.not_to raise_error + end + + describe '#guest?' do + specify do + expect(guest).to be_guest + end + end + + describe '#response_for_shared' do + before do + create :response, question_name: content.name # , training_module: nil + end + + let(:content) { Course.config.pages.first } + let(:response) { guest.response_for_shared(content, nil) } + + specify do + expect(response).to be_a Response + end + end + + # describe 'completed_main_feedback?' do + # end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1a7b32c56..589ca7815 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -5,6 +5,12 @@ expect(build(:user, :registered)).to be_valid end + describe '#guest?' do + specify do + expect(build(:user, :registered)).not_to be_guest + end + end + describe '#first_name' do it 'must be present' do expect(build(:user, :registered, first_name: nil)).not_to be_valid From 8274aa03c3ad985fb4cb5efe649250226c3487d7 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 8 Mar 2024 08:16:06 +0000 Subject: [PATCH 32/95] Improve skipping feedback link --- app/decorators/next_page_decorator.rb | 2 +- app/helpers/link_helper.rb | 4 ++-- app/views/training/pages/_content.html.slim | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/decorators/next_page_decorator.rb b/app/decorators/next_page_decorator.rb index f4914031c..cc679c989 100644 --- a/app/decorators/next_page_decorator.rb +++ b/app/decorators/next_page_decorator.rb @@ -109,7 +109,7 @@ def confidence_outro? # @return [Boolean] def content_section? - content.section? && !content.first_feedback? + content.section? && !content.feedback_question? end # @return [Boolean] diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 1bc068cc9..f159c9bce 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -67,8 +67,8 @@ def link_to_retake_or_results(mod) end end - # @return [String, nil] thank you page (skips feedback questions) - def link_to_skip + # @return [String] + def link_to_skip_feedback govuk_link_to 'Skip feedback', training_module_page_path(mod.name, mod.thankyou_page.name) end diff --git a/app/views/training/pages/_content.html.slim b/app/views/training/pages/_content.html.slim index 0f2cb9cc5..c65d75efd 100644 --- a/app/views/training/pages/_content.html.slim +++ b/app/views/training/pages/_content.html.slim @@ -11,5 +11,6 @@ .govuk-button-group = link_to_previous = link_to_next - - if content.next_item.feedback_question? - = link_to_skip + + - if !content.interruption_page? && content.next_item.feedback_question? + = link_to_skip_feedback From 24196623917da37375e907da054f249bf5791012 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 8 Mar 2024 08:17:59 +0000 Subject: [PATCH 33/95] Remove opinion intro type --- lib/content_test_schema.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/content_test_schema.rb b/lib/content_test_schema.rb index cf4850ff6..3a59ac66e 100644 --- a/lib/content_test_schema.rb +++ b/lib/content_test_schema.rb @@ -57,10 +57,6 @@ def inputs [ [:click_on, results_button], ] - elsif type.match?(/opinion_intro/) - [ - [:click_on, 'Skip feedback'], - ] elsif type.match?(/thankyou/) [ [:click_on, 'View certificate'], From e8a8b50e4927a5302a6e773b539c193d2074d0ac Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 8 Mar 2024 15:02:11 +0000 Subject: [PATCH 34/95] Fix database migrations, validations, null values and foreign associations --- app/controllers/feedback_controller.rb | 14 +++++++++++++- app/models/course.rb | 9 ++------- app/models/guest.rb | 9 +++++---- app/models/response.rb | 13 ++++++------- ...31135344_change_training_module_in_responses.rb | 5 ----- .../20240301154905_add_guest_visit_to_responses.rb | 5 ----- ...2_change_user_id_to_be_nullable_in_responses.rb | 5 ----- ...rb => 20240308140000_add_freetext_responses.rb} | 2 +- db/migrate/20240308142000_add_guest_responses.rb | 9 +++++++++ db/schema.rb | 8 +++++--- ...back_form_spec.rb => feedback_external_spec.rb} | 2 +- spec/system/feedback_internal_spec.rb | 14 ++++++++++++++ 12 files changed, 56 insertions(+), 39 deletions(-) delete mode 100644 db/migrate/20240131135344_change_training_module_in_responses.rb delete mode 100644 db/migrate/20240301154905_add_guest_visit_to_responses.rb delete mode 100644 db/migrate/20240301164142_change_user_id_to_be_nullable_in_responses.rb rename db/migrate/{20240207105459_add_text_input_to_responses.rb => 20240308140000_add_freetext_responses.rb} (53%) create mode 100644 db/migrate/20240308142000_add_guest_responses.rb rename spec/system/{feedback_form_spec.rb => feedback_external_spec.rb} (93%) create mode 100644 spec/system/feedback_internal_spec.rb diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 2901f6cea..5dff19603 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -9,7 +9,7 @@ def index; end def update if save_response! - redirect_to feedback_path(content.next_item.name) + redirect else render 'feedback/show', status: :unprocessable_entity end @@ -17,6 +17,18 @@ def update private + def redirect + if content.eql?(mod.pages.last) + if current_user.guest? + redirect_to root_path + else + redirect_to feedback_thank_you_path + end + else + redirect_to feedback_path(content.next_item.name) + end + end + # @return [Boolean] def save_response! current_user_response.update( diff --git a/app/models/course.rb b/app/models/course.rb index 52e4a8c32..7773cd582 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -18,14 +18,9 @@ def self.config fetch_or_store('course') { first } end - # - # Quacks like training module - # - - # @return [nil] mod.name + # @return [String] mod.name def name - # OPTIMIZE: this could return a string - nil + 'course' end # @return [Array] mod.content_sections diff --git a/app/models/guest.rb b/app/models/guest.rb index bca20cf81..c1be5bbbd 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -9,13 +9,14 @@ def guest? end # @param content [Training::Question] feedback questions + # @param mod [Course] # @return [Response] - def response_for_shared(content, _mod) + def response_for_shared(content, mod) responses.find_or_initialize_by( question_type: content.page_type, question_name: content.name, - # training_module: nil, - guest_visit: visit.id, + training_module: mod.name, + visit_id: visit.id, ) end @@ -33,6 +34,6 @@ def completed_main_feedback? # @return [Response::ActiveRecord_Relation] def responses - Response.main_feedback.where(guest_visit: visit.id) + Response.course_feedback.where(visit_id: visit.id) end end diff --git a/app/models/response.rb b/app/models/response.rb index 33948da5d..84bef1f37 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -6,8 +6,9 @@ class Response < ApplicationRecord belongs_to :user, optional: true belongs_to :assessment, optional: true + belongs_to :visit, optional: true - # validates :training_module, presence: true + validates :training_module, presence: true validates :question_type, inclusion: { in: %w(formative summative confidence feedback) } validates :answers, presence: true, unless: -> { text_input_only? } @@ -23,18 +24,16 @@ class Response < ApplicationRecord scope :summative, -> { where(question_type: 'summative') } scope :confidence, -> { where(question_type: 'confidence') } scope :feedback, -> { where(question_type: 'feedback') } - - # OPTIMIZE: module name needn't be nil now - scope :main_feedback, -> { where(question_type: 'feedback', training_module: nil) } + scope :course_feedback, -> { feedback.where(training_module: 'course') } delegate :to_partial_path, :legend, to: :question # @return [Training::Module, Course] def mod - if training_module - Training::Module.by_name(training_module) - else + if training_module.eql?('course') Course.config + else + Training::Module.by_name(training_module) end end diff --git a/db/migrate/20240131135344_change_training_module_in_responses.rb b/db/migrate/20240131135344_change_training_module_in_responses.rb deleted file mode 100644 index e682aaa2c..000000000 --- a/db/migrate/20240131135344_change_training_module_in_responses.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ChangeTrainingModuleInResponses < ActiveRecord::Migration[7.0] - def change - change_column_null :responses, :training_module, true - end -end diff --git a/db/migrate/20240301154905_add_guest_visit_to_responses.rb b/db/migrate/20240301154905_add_guest_visit_to_responses.rb deleted file mode 100644 index dd3c724f7..000000000 --- a/db/migrate/20240301154905_add_guest_visit_to_responses.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddGuestVisitToResponses < ActiveRecord::Migration[7.1] - def change - add_column :responses, :guest_visit, :string - end -end diff --git a/db/migrate/20240301164142_change_user_id_to_be_nullable_in_responses.rb b/db/migrate/20240301164142_change_user_id_to_be_nullable_in_responses.rb deleted file mode 100644 index fde546a04..000000000 --- a/db/migrate/20240301164142_change_user_id_to_be_nullable_in_responses.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ChangeUserIdToBeNullableInResponses < ActiveRecord::Migration[7.1] - def change - change_column_null :responses, :user_id, true - end -end diff --git a/db/migrate/20240207105459_add_text_input_to_responses.rb b/db/migrate/20240308140000_add_freetext_responses.rb similarity index 53% rename from db/migrate/20240207105459_add_text_input_to_responses.rb rename to db/migrate/20240308140000_add_freetext_responses.rb index f8f3b79c6..20bef43ee 100644 --- a/db/migrate/20240207105459_add_text_input_to_responses.rb +++ b/db/migrate/20240308140000_add_freetext_responses.rb @@ -1,4 +1,4 @@ -class AddTextInputToResponses < ActiveRecord::Migration[7.0] +class AddFreetextResponses < ActiveRecord::Migration[7.1] def change add_column :responses, :text_input, :text end diff --git a/db/migrate/20240308142000_add_guest_responses.rb b/db/migrate/20240308142000_add_guest_responses.rb new file mode 100644 index 000000000..2289faff9 --- /dev/null +++ b/db/migrate/20240308142000_add_guest_responses.rb @@ -0,0 +1,9 @@ +class AddGuestResponses < ActiveRecord::Migration[7.1] + def change + change_column_null :responses, :user_id, true + + change_table :responses, bulk: true do |t| + t.references :visit, null: true, foreign_key: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 56fd7587a..82bf11bf8 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[7.1].define(version: 2024_03_01_164142) do +ActiveRecord::Schema[7.1].define(version: 2024_03_08_142000) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -130,7 +130,7 @@ create_table "responses", force: :cascade do |t| t.bigint "user_id" - t.string "training_module" + t.string "training_module", null: false t.string "question_name", null: false t.jsonb "answers", default: [] t.boolean "correct" @@ -139,10 +139,11 @@ t.string "question_type" t.bigint "assessment_id" t.text "text_input" - t.string "guest_visit" + t.bigint "visit_id" t.index ["assessment_id"], name: "index_responses_on_assessment_id" t.index ["user_id", "training_module", "question_name"], name: "user_question" t.index ["user_id"], name: "index_responses_on_user_id" + t.index ["visit_id"], name: "index_responses_on_visit_id" end create_table "user_answers", force: :cascade do |t| @@ -256,6 +257,7 @@ add_foreign_key "que_scheduler_audit_enqueued", "que_scheduler_audit", column: "scheduler_job_id", primary_key: "scheduler_job_id", name: "que_scheduler_audit_enqueued_scheduler_job_id_fkey" add_foreign_key "responses", "assessments" add_foreign_key "responses", "users" + add_foreign_key "responses", "visits" add_foreign_key "user_answers", "user_assessments" add_foreign_key "user_answers", "users" add_foreign_key "user_assessments", "users" diff --git a/spec/system/feedback_form_spec.rb b/spec/system/feedback_external_spec.rb similarity index 93% rename from spec/system/feedback_form_spec.rb rename to spec/system/feedback_external_spec.rb index 2208ec168..a77d52bbe 100644 --- a/spec/system/feedback_form_spec.rb +++ b/spec/system/feedback_external_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'Feedback form' do +RSpec.describe 'Feedback external' do before do visit '/' end diff --git a/spec/system/feedback_internal_spec.rb b/spec/system/feedback_internal_spec.rb new file mode 100644 index 000000000..67f417661 --- /dev/null +++ b/spec/system/feedback_internal_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe 'Feedback internal' do + before do + visit '/feedback' + end + + describe 'foo' do + it 'bar' do + expect(page).to have_content 'The purpose of this feedback form is to gather your opinon on the child development training course' + end + end + +end From 4762e205c64329e5f42f1cf3cbb3b60603fe37a3 Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Tue, 12 Mar 2024 12:53:32 +0000 Subject: [PATCH 35/95] Update specs that are currently failing --- app/models/guest.rb | 2 +- app/models/response.rb | 2 +- spec/controllers/feedback_controller_spec.rb | 2 +- spec/decorators/module_overview_decorator_spec.rb | 2 +- spec/helpers/link_helper_spec.rb | 12 +++--------- spec/lib/seed_snippets_spec.rb | 2 +- spec/models/course_spec.rb | 10 +++++----- spec/models/response_spec.rb | 2 ++ spec/models/training/module_spec.rb | 6 +++--- spec/models/training/question_spec.rb | 2 +- spec/system/feedback_internal_spec.rb | 1 - spec/system/opinion_spec.rb | 10 +++++----- 12 files changed, 24 insertions(+), 29 deletions(-) diff --git a/app/models/guest.rb b/app/models/guest.rb index c1be5bbbd..ab9e9a919 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -15,7 +15,7 @@ def response_for_shared(content, mod) responses.find_or_initialize_by( question_type: content.page_type, question_name: content.name, - training_module: mod.name, + training_module: mod.nil? ? nil : mod.name, visit_id: visit.id, ) end diff --git a/app/models/response.rb b/app/models/response.rb index 84bef1f37..f759448c7 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -9,7 +9,7 @@ class Response < ApplicationRecord belongs_to :visit, optional: true validates :training_module, presence: true - validates :question_type, inclusion: { in: %w(formative summative confidence feedback) } + validates :question_type, inclusion: { in: %w[formative summative confidence feedback] } validates :answers, presence: true, unless: -> { text_input_only? } validates :text_input, presence: true, if: -> { text_input_extra? } diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index b4ffa2f3a..5d606b8a1 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -4,7 +4,7 @@ context 'when user is signed in' do let(:user) { create :user, :registered } let(:valid_attributes) do - { id: 1, answers: %w[Yes], answers_custom: 'Custom answer', training_module: nil, question_name: 'main-feedback-1' } + { id: 1, answers: %w[Yes], answers_custom: 'Custom answer', training_module: nil, question_name: 'feedback-radiobutton' } end before { sign_in user } diff --git a/spec/decorators/module_overview_decorator_spec.rb b/spec/decorators/module_overview_decorator_spec.rb index 4716256a8..66b3ac273 100644 --- a/spec/decorators/module_overview_decorator_spec.rb +++ b/spec/decorators/module_overview_decorator_spec.rb @@ -55,7 +55,7 @@ end it 'goes to the certificate' do - expect(user.events.count).to be 32 + expect(user.events.count).to be 41 expect(state).to be :completed expect(page_name).to eq '1-3-4' end diff --git a/spec/helpers/link_helper_spec.rb b/spec/helpers/link_helper_spec.rb index 37e81eed0..90e6777cb 100644 --- a/spec/helpers/link_helper_spec.rb +++ b/spec/helpers/link_helper_spec.rb @@ -50,17 +50,11 @@ end context 'when page is feedback intro' do - let(:skip_link) { helper.link_to_skip } - - let(:content) { mod.pages_by_type('opinion_intro').first } + let(:content) { mod.page_by_name('feedback-intro') } it 'targets start of feedback questions' do expect(link).to include 'Give feedback' end - - it 'offers button to skip feedback questions' do - expect(skip_link).to include 'Skip feedback' - end end end @@ -200,7 +194,7 @@ end describe '#link_to_skip' do - subject(:link) { helper.link_to_skip } + subject(:link) { helper.link_to_skip_feedback } before do without_partial_double_verification do @@ -210,7 +204,7 @@ end context 'when page is feedback intro' do - let(:content) { mod.pages_by_type('opinion_intro').first } + let(:content) { mod.page_by_name('feedback-intro') } it 'targets thank you page' do expect(link).to include 'Skip feedback' diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 28e5ee7bb..547414bfd 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 230 + expect(locales.count).to be 223 end it 'dot separated key -> Page::Resource#name' do diff --git a/spec/models/course_spec.rb b/spec/models/course_spec.rb index f97cc28f9..f5187a6d1 100644 --- a/spec/models/course_spec.rb +++ b/spec/models/course_spec.rb @@ -17,7 +17,7 @@ end it 'feedback' do - expect(course.feedback.count).to eq 5 + expect(course.feedback.count).to eq 8 expect(course.feedback.first.page_type).to eq 'feedback' end end @@ -39,10 +39,10 @@ end it 'page order uing previous_item/next_item' do - expect(pages.first.name).to eq 'main-feedback-1' - expect(pages.first.next_item.name).to eq 'main-feedback-2' - expect(pages.first.next_item.next_item.name).to eq 'main-feedback-3' - expect(pages.first.next_item.previous_item.name).to eq 'main-feedback-1' + expect(pages.first.name).to eq 'feedback-radiobutton' + expect(pages.first.next_item.name).to eq 'feedback-yesnoandtext' + expect(pages.first.next_item.next_item.name).to eq 'feedback-freetext' + expect(pages.first.next_item.previous_item.name).to eq 'feedback-radiobutton' end end end diff --git a/spec/models/response_spec.rb b/spec/models/response_spec.rb index b08788996..cd30cee43 100644 --- a/spec/models/response_spec.rb +++ b/spec/models/response_spec.rb @@ -22,7 +22,9 @@ updated_at question_type assessment_id + guest_visit text_input + visit_id ] end diff --git a/spec/models/training/module_spec.rb b/spec/models/training/module_spec.rb index 4c4db0d82..e2e91f2cb 100644 --- a/spec/models/training/module_spec.rb +++ b/spec/models/training/module_spec.rb @@ -32,7 +32,7 @@ expect(mod.answers_with('foo')).to eq [] # no match # expect(mod.answers_with('')).to eq [] # formative expect(mod.answers_with('Wrong\s.+ 3')).to eq %w[1-3-2-4 1-3-2-5 1-3-2-6 1-3-2-7 1-3-2-8 1-3-2-9 1-3-2-10] # summative - expect(mod.answers_with('NOR')).to eq %w[1-3-3-1 1-3-3-2 1-3-3-3 1-3-3-4] # confidence + expect(mod.answers_with('NOR')).to eq %w[1-3-3-1 1-3-3-2 1-3-3-3 1-3-3-4 feedback-radiobutton] # confidence end end @@ -42,7 +42,7 @@ it 'returns sections' do expect(sections).to be_a Hash expect(sections.keys).to eq [1, 2, 3, 4, 5] - expect(sections.values.map(&:count)).to eq [7, 5, 19, 6, 1] + expect(sections.values.map(&:count)).to eq [7, 5, 20, 9, 1] end end @@ -52,7 +52,7 @@ it 'returns subsections' do expect(subsections).to be_a Hash expect(subsections.keys).to eq [[1, 0], [1, 1], [1, 2], [1, 3], [1, 4], [2, 0], [2, 1], [3, 0], [3, 1], [3, 2], [3, 3], [4, 0], [5, 0]] - expect(subsections.values.map(&:count)).to eq [1, 1, 1, 2, 2, 1, 4, 1, 1, 12, 5, 6, 1] + expect(subsections.values.map(&:count)).to eq [1, 1, 1, 2, 2, 1, 4, 1, 1, 12, 6, 9, 1] end end diff --git a/spec/models/training/question_spec.rb b/spec/models/training/question_spec.rb index d30a53483..fa5b8196e 100644 --- a/spec/models/training/question_spec.rb +++ b/spec/models/training/question_spec.rb @@ -99,7 +99,7 @@ context 'when the question is a feedback question' do subject(:question) do - Training::Module.by_name('alpha').page_by_name('end-of-module-feedback-1') + Training::Module.by_name('alpha').page_by_name('feedback-radiobutton') end let(:first_option) { question.options.first } diff --git a/spec/system/feedback_internal_spec.rb b/spec/system/feedback_internal_spec.rb index 67f417661..131fd3086 100644 --- a/spec/system/feedback_internal_spec.rb +++ b/spec/system/feedback_internal_spec.rb @@ -10,5 +10,4 @@ expect(page).to have_content 'The purpose of this feedback form is to gather your opinon on the child development training course' end end - end diff --git a/spec/system/opinion_spec.rb b/spec/system/opinion_spec.rb index 92127ef7c..aa2123f42 100644 --- a/spec/system/opinion_spec.rb +++ b/spec/system/opinion_spec.rb @@ -4,19 +4,19 @@ include_context 'with progress' include_context 'with user' - let(:first_question_path) { '/modules/alpha/questionnaires/end-of-module-feedback-1' } - let(:third_question_path) { '/modules/alpha/questionnaires/end-of-module-feedback-3' } + let(:first_question_path) { '/modules/alpha/questionnaires/feedback-radiobutton' } + let(:second_question_path) { '/modules/alpha/questionnaires/feedback-yesnoandtext' } it 'shows feedback question' do visit '/modules/alpha/content-pages/feedback-intro' expect(page).to have_content('Additional feedback') click_on 'Give feedback' - expect(page).to have_content('Regarding the training module you have just completed: the content was easy to understand') + expect(page).to have_content('Regarding the training module') expect(page).to have_content('Strongly agree') end it do - visit third_question_path + visit second_question_path expect(page).to have_content('Did the module meet your expectations') expect(page).to have_content('Yes') end @@ -25,7 +25,7 @@ it 'displays an error message' do visit first_question_path click_on 'Next' - expect(page).to have_content 'Please select an option' + expect(page).to have_content 'Please select an answer' end end From 6b3b47b38506fdc04f1c39ed6d87ed47dd4a1c75 Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:47:38 +0000 Subject: [PATCH 36/95] Update failing specs --- spec/decorators/pagination_decorator_spec.rb | 6 +++--- spec/decorators/previous_page_decorator_spec.rb | 4 ++-- spec/models/concerns/content_types_spec.rb | 1 - spec/support/shared/with_content.rb | 1 - 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/spec/decorators/pagination_decorator_spec.rb b/spec/decorators/pagination_decorator_spec.rb index 55845e7a1..9b9306d4e 100644 --- a/spec/decorators/pagination_decorator_spec.rb +++ b/spec/decorators/pagination_decorator_spec.rb @@ -13,7 +13,7 @@ end it '#section_numbers' do - expect(decorator.section_numbers).to eq 'Section 1 of 4' + expect(decorator.section_numbers).to eq 'Section 1 of 5' end it '#page_numbers' do @@ -33,13 +33,13 @@ end it '#page_numbers' do - expect(decorator.page_numbers).to eq 'Page 4 of 5' + expect(decorator.page_numbers).to eq 'Page 3 of 9' end end context 'when unanswered' do it '#page_numbers' do - expect(decorator.page_numbers).to eq 'Page 4 of 5' + expect(decorator.page_numbers).to eq 'Page 3 of 9' end end end diff --git a/spec/decorators/previous_page_decorator_spec.rb b/spec/decorators/previous_page_decorator_spec.rb index b45390ec7..783917312 100644 --- a/spec/decorators/previous_page_decorator_spec.rb +++ b/spec/decorators/previous_page_decorator_spec.rb @@ -66,14 +66,14 @@ context 'and unanswered' do it 'is one step back' do - expect(decorator.name).to eq 'end-of-module-feedback-5' + expect(decorator.name).to eq 'feedback-checkbox-otherandtext' end end context 'and answered' do before do create :response, - question_name: 'end-of-module-feedback-5', + question_name: 'feedback-checkbox-otherandtext', training_module: 'alpha', answers: [1], correct: true, diff --git a/spec/models/concerns/content_types_spec.rb b/spec/models/concerns/content_types_spec.rb index 63ea05365..eb99c05c2 100644 --- a/spec/models/concerns/content_types_spec.rb +++ b/spec/models/concerns/content_types_spec.rb @@ -126,7 +126,6 @@ assessment_results confidence_intro confidence_questionnaire - opinion_intro feedback thankyou certificate diff --git a/spec/support/shared/with_content.rb b/spec/support/shared/with_content.rb index 6190380f2..821490c3f 100644 --- a/spec/support/shared/with_content.rb +++ b/spec/support/shared/with_content.rb @@ -70,7 +70,6 @@ assessment_results confidence_intro confidence_questionnaire - opinion_intro feedback thankyou certificate From 0649d0394356ad195c87dda3f3a9b1cd2451248c Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:24:24 +0000 Subject: [PATCH 37/95] Update specs to include response yml --- app/decorators/pagination_decorator.rb | 3 +++ spec/models/training/response_spec.rb | 2 +- spec/support/ast/alpha-pass-response.yml | 7 ++++++- spec/support/ast/alpha-pass.yml | 5 ----- spec/system/event_log_spec.rb | 5 +++-- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/decorators/pagination_decorator.rb b/app/decorators/pagination_decorator.rb index 1960fac0f..deee5795f 100644 --- a/app/decorators/pagination_decorator.rb +++ b/app/decorators/pagination_decorator.rb @@ -10,6 +10,9 @@ class PaginationDecorator # @return [String] def heading + # TODO Look at improving this so it isn't hardcoded + return 'Summary and next steps' if content.thankyou? + content.section_content.first.heading end diff --git a/spec/models/training/response_spec.rb b/spec/models/training/response_spec.rb index d734b2419..958174f3a 100644 --- a/spec/models/training/response_spec.rb +++ b/spec/models/training/response_spec.rb @@ -102,7 +102,7 @@ end context 'with radio buttons for feedback question' do - let(:question_name) { 'end-of-module-feedback-1' } + let(:question_name) { 'feedback-radiobutton' } describe 'and no answer' do let(:answers) { nil } diff --git a/spec/support/ast/alpha-pass-response.yml b/spec/support/ast/alpha-pass-response.yml index d59146ebc..29806bafd 100644 --- a/spec/support/ast/alpha-pass-response.yml +++ b/spec/support/ast/alpha-pass-response.yml @@ -215,11 +215,16 @@ - response-answers-5-field - - :click_on - Next +- :path: /modules/alpha/content-pages/feedback-intro + :text: Additional feedback + :inputs: + - - :click_on + - Skip feedback - :path: /modules/alpha/content-pages/1-3-3-5 :text: Thank you :inputs: - - :click_on - - Finish + - View certificate - :path: /modules/alpha/content-pages/1-3-4 :text: Congratulations! :inputs: [] diff --git a/spec/support/ast/alpha-pass.yml b/spec/support/ast/alpha-pass.yml index aaaade9c3..3dfe96bda 100644 --- a/spec/support/ast/alpha-pass.yml +++ b/spec/support/ast/alpha-pass.yml @@ -215,11 +215,6 @@ - user-answer-answers-5-field - - :click_on - Next -- :path: /modules/alpha/content-pages/feedback-intro - :text: Additional Feedback - :inputs: - - - :click_on - - Give feedback - :path: /modules/alpha/content-pages/1-3-3-5 :text: Thank you :inputs: diff --git a/spec/system/event_log_spec.rb b/spec/system/event_log_spec.rb index 1632e1de7..e1007f903 100644 --- a/spec/system/event_log_spec.rb +++ b/spec/system/event_log_spec.rb @@ -4,6 +4,7 @@ include_context 'with events' include_context 'with user' include_context 'with automated path' + let(:fixture) { 'spec/support/ast/alpha-pass-response.yml' } describe 'confidence check' do context 'when viewing the first question' do @@ -49,7 +50,7 @@ end context 'when all questions are answered incorrectly' do - # let(:fixture) { 'spec/support/ast/alpha-fail-response.yml' } + let(:fixture) { 'spec/support/ast/alpha-fail-response.yml' } let(:happy) { false } it 'tracks answers and failed attempt' do @@ -77,7 +78,7 @@ describe 'visiting every page' do it 'tracks start and completion' do expect(events.where(name: 'module_start').size).to be 1 - expect(events.where(name: 'module_content_page').size).to be 34 + expect(events.where(name: 'module_content_page').size).to be 35 expect(events.where(name: 'module_complete').size).to eq 1 end end From 1c3d39f30df005129b93e694255255f43e3f31be Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Thu, 14 Mar 2024 15:46:23 +0000 Subject: [PATCH 38/95] add guest feedback cookie and update tracking --- app/controllers/feedback_controller.rb | 42 ++++++++++++++++++- .../training/questions_controller.rb | 12 ++++++ app/decorators/pagination_decorator.rb | 2 +- app/models/guest.rb | 13 +++--- app/models/training/question.rb | 5 +++ app/views/feedback/index.html.slim | 2 +- 6 files changed, 65 insertions(+), 11 deletions(-) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 5dff19603..a7722a854 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -1,7 +1,10 @@ class FeedbackController < ApplicationController + before_action :track_feedback_start, only: :show + after_action :track_feedback_complete, only: :update helper_method :content, :mod, - :current_user_response + :current_user_response, + :feedback_exists? def show; end @@ -15,11 +18,21 @@ def update end end + # @return [Boolean] + def feedback_exists? + if current_user.guest? + cookies[:feedback_complete].present? + else + current_user.completed_main_feedback? + end + end + private def redirect if content.eql?(mod.pages.last) if current_user.guest? + feedback_complete_cookie redirect_to root_path else redirect_to feedback_thank_you_path @@ -58,6 +71,13 @@ def current_user_response @current_user_response ||= current_user.response_for_shared(content, mod) end + # @return [Hash] + def feedback_complete_cookie + cookies[:feedback_complete] = { + value: current_user.visit.visit_token, + } + end + # @return [String] def question_name params[:id] @@ -72,4 +92,24 @@ def response_params def user_answers Array(response_params[:answers]).compact_blank.map(&:to_i) end + + def track_feedback_start + if content.first_feedback? && feedback_start_untracked? + track('feedback_start') + end + end + + def track_feedback_complete + if content.last_feedback? && feedback_complete_untracked? + track('feedback_complete') + end + end + + def feedback_start_untracked? + untracked?('feedback_start', training_module_id: mod.name) + end + + def feedback_complete_untracked? + untracked?('feedback_complete', training_module_id: mod.name) + end end diff --git a/app/controllers/training/questions_controller.rb b/app/controllers/training/questions_controller.rb index b99c9b642..fb9ff056f 100644 --- a/app/controllers/training/questions_controller.rb +++ b/app/controllers/training/questions_controller.rb @@ -32,6 +32,8 @@ def show; end def track_events if track_feedback_start? track('feedback_start') + elsif track_feedback_complete? + track('feedback_complete') elsif track_confidence_start? track('confidence_check_start') elsif track_assessment_start? @@ -57,6 +59,11 @@ def track_feedback_start? content.first_feedback? && feedback_start_untracked? end + # @return [Boolean] + def track_feedback_complete? + content.last_feedback? && feedback_complete_untracked? + end + # @return [Boolean] def track_assessment_start? content.first_assessment? && summative_start_untracked? @@ -78,5 +85,10 @@ def confidence_start_untracked? def feedback_start_untracked? untracked?('feedback_start', training_module_id: mod.name) end + + # @return [Boolean] + def feedback_complete_untracked? + untracked?('feedback_complete', training_module_id: mod.name) + end end end diff --git a/app/decorators/pagination_decorator.rb b/app/decorators/pagination_decorator.rb index deee5795f..20154b973 100644 --- a/app/decorators/pagination_decorator.rb +++ b/app/decorators/pagination_decorator.rb @@ -10,7 +10,7 @@ class PaginationDecorator # @return [String] def heading - # TODO Look at improving this so it isn't hardcoded + # TODO: Look at improving this so it isn't hardcoded return 'Summary and next steps' if content.thankyou? content.section_content.first.heading diff --git a/app/models/guest.rb b/app/models/guest.rb index ab9e9a919..993e2fda2 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -20,14 +20,11 @@ def response_for_shared(content, mod) ) end - # @return [Boolean] - # def started_main_feedback? - # responses.any? - # end - - # @return [Boolean] - def completed_main_feedback? - Course.config.pages.count.eql? responses.count + # @return [Hash] + def feedback_complete_cookie + cookies[:feedback_complete] = { + value: visit.visit_token, + } end private diff --git a/app/models/training/question.rb b/app/models/training/question.rb index 5e96dd325..483a09ace 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -98,6 +98,11 @@ def first_feedback? parent.feedback_questions.first.eql?(self) end + # @return [Boolean] + def last_feedback? + parent.feedback_questions.last.eql?(self) + end + # @return [Boolean] def true_false? return false if multi_select? diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index d5b5914fd..d0c7db00f 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -1,6 +1,6 @@ .govuk-grid-row .govuk-grid-column-full - - if current_user.completed_main_feedback? + - if feedback_exists? = m('feedback.feedback_exists') .govuk-button-group From 33f687f0c0b806c9715cbf2b8b02fba556e95395 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Thu, 14 Mar 2024 16:06:24 +0000 Subject: [PATCH 39/95] update feedback controller spec --- spec/controllers/feedback_controller_spec.rb | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index 5d606b8a1..2d23dae12 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -55,4 +55,32 @@ end end end + + describe 'feedback_exists?' do + let(:user) { create :user, :registered } + + context 'when feedback exists' do + before do + allow(controller).to receive(:current_user).and_return(user) + allow(user).to receive(:completed_main_feedback?).and_return(true) + end + + it 'is true' do + get :index + expect(controller.feedback_exists?).to be true + end + end + + context 'when feedback does not exist' do + before do + allow(controller).to receive(:current_user).and_return(user) + allow(user).to receive(:completed_main_feedback?).and_return(false) + end + + it 'is false' do + get :index + expect(controller.feedback_exists?).to be false + end + end + end end From b4b02b38b89d94581bc1a1762a87769821ed8494 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Thu, 14 Mar 2024 16:12:31 +0000 Subject: [PATCH 40/95] add guest feedback exists test to user spec --- spec/controllers/feedback_controller_spec.rb | 29 ++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index 2d23dae12..ce4971ae4 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -58,8 +58,9 @@ describe 'feedback_exists?' do let(:user) { create :user, :registered } + let(:visit) { create :visit } - context 'when feedback exists' do + context 'when user feedback exists' do before do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:completed_main_feedback?).and_return(true) @@ -71,7 +72,7 @@ end end - context 'when feedback does not exist' do + context 'when user feedback does not exist' do before do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:completed_main_feedback?).and_return(false) @@ -82,5 +83,29 @@ expect(controller.feedback_exists?).to be false end end + + context 'when guest feedback exists' do + before do + allow(controller).to receive(:current_user).and_return(Guest.new(visit: visit)) + allow(controller).to receive(:cookies).and_return({ feedback_complete: 'some-token' }) + end + + it 'is true' do + get :index + expect(controller.feedback_exists?).to be true + end + end + + context 'when guest feedback does not exist' do + before do + allow(controller).to receive(:current_user).and_return(Guest.new(visit: visit)) + allow(controller).to receive(:cookies).and_return({}) + end + + it 'is false' do + get :index + expect(controller.feedback_exists?).to be false + end + end end end From ef84df2c07d013da210b479d61da714d69370129 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Thu, 14 Mar 2024 16:25:38 +0000 Subject: [PATCH 41/95] tidy guest struct --- app/models/guest.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/models/guest.rb b/app/models/guest.rb index 993e2fda2..e0c296feb 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -20,13 +20,6 @@ def response_for_shared(content, mod) ) end - # @return [Hash] - def feedback_complete_cookie - cookies[:feedback_complete] = { - value: visit.visit_token, - } - end - private # @return [Response::ActiveRecord_Relation] From 39b36e9db628ccf4c2c808056614462c393b3459 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Thu, 14 Mar 2024 17:10:14 +0000 Subject: [PATCH 42/95] fix feedback controller spec params and add tests for tracking --- spec/controllers/feedback_controller_spec.rb | 21 ++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index ce4971ae4..393904ea1 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -4,14 +4,26 @@ context 'when user is signed in' do let(:user) { create :user, :registered } let(:valid_attributes) do - { id: 1, answers: %w[Yes], answers_custom: 'Custom answer', training_module: nil, question_name: 'feedback-radiobutton' } + { + id: 'feedback-radiobutton', + training_module: nil, + response: { + answers: %w[Yes], + answers_custom: 'Custom answer', + }, + } end before { sign_in user } describe 'GET #show' do + it 'tracks feedback start' do + expect(controller).to receive(:track_feedback_start) + get :show, params: valid_attributes + end + it 'returns a success response' do - get :show, params: { id: 1 } + get :show, params: valid_attributes expect(response).to be_successful end end @@ -35,6 +47,11 @@ post :update, params: valid_attributes expect(response).to redirect_to(feedback_path(2)) end + + it 'tracks feedback complete' do + expect(controller).to receive(:track_feedback_complete) + post :update, params: valid_attributes + end end context 'with invalid params' do From 59a6c2b2b5bac82a9cf77a079b1a9dc3bba8ff8a Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Fri, 15 Mar 2024 12:21:15 +0000 Subject: [PATCH 43/95] add data analysis exports for feedback forms and update debug panel --- app/models/data_analysis/feedback_forms.rb | 44 ++++++++++++++ app/models/user.rb | 2 +- app/services/dashboard.rb | 1 + app/views/training/questions/_debug.html.slim | 2 + spec/factories/event.rb | 4 ++ .../data_analysis/feedback_forms_spec.rb | 58 +++++++++++++++++++ 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 app/models/data_analysis/feedback_forms.rb create mode 100644 spec/models/data_analysis/feedback_forms_spec.rb diff --git a/app/models/data_analysis/feedback_forms.rb b/app/models/data_analysis/feedback_forms.rb new file mode 100644 index 000000000..73aacdaa0 --- /dev/null +++ b/app/models/data_analysis/feedback_forms.rb @@ -0,0 +1,44 @@ +module DataAnalysis + class FeedbackForms + include ToCsv + + class << self + # @return [Array] + def column_names + [ + 'Module', + 'Total Responses', + 'Signed in Users', + 'Guest Users', + ] + end + + # @return [Array] + def dashboard + rows = Training::Module.ordered.map { |mod| feedback_for(mod.name, "properties ->> 'training_module_id' = ?") } + rows << feedback_for('feedback', "properties ->> 'controller' = ?") + rows + end + + private + + # @param mod_name [String] the name of the module or "feedback" for site wide. + # @param condition [String] the condition to filter the events. + # @return [Hash] + def feedback_for(mod_name, condition) + events = complete_events.where(condition, mod_name.to_s) + { + mod: mod_name == 'feedback' ? 'site wide' : mod_name, + total: events.count, + signed_in: events.where.not(user_id: nil).count, + guest: events.where(user_id: nil).count, + } + end + + # @return [ActiveRecord::Relation] + def complete_events + Event.where(name: 'feedback_complete') + end + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index d30b8e89d..8c8c89d81 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -478,7 +478,7 @@ def content_changes # @return [Boolean] def completed_main_feedback? - responses.main_feedback.any? + responses.course_feedback.any? end private diff --git a/app/services/dashboard.rb b/app/services/dashboard.rb index 4e4c69828..e1831369f 100644 --- a/app/services/dashboard.rb +++ b/app/services/dashboard.rb @@ -33,6 +33,7 @@ class Dashboard { model: 'DataAnalysis::UserModuleCompletion', folder: 'nonlinear', file: 'user_module_completion' }, { model: 'DataAnalysis::UserModuleCompletionCount', folder: 'nonlinear', file: 'user_module_completions_count' }, { model: 'DataAnalysis::ReturningUsers', folder: 'nonlinear', file: 'returning_users' }, + { model: 'DataAnalysis::FeedbackForms', folder: 'feedback', file: 'feedback_forms' }, ].freeze # @return [String] 30-06-2022-09-30 diff --git a/app/views/training/questions/_debug.html.slim b/app/views/training/questions/_debug.html.slim index 7f377ebeb..a9d647a4c 100644 --- a/app/views/training/questions/_debug.html.slim +++ b/app/views/training/questions/_debug.html.slim @@ -14,6 +14,8 @@ hr | FEEDBACK br + | User type: #{current_user.guest? ? 'guest' : 'authenticated user'} + br | Hint: #{content.hint} br | Or: #{content.or} diff --git a/spec/factories/event.rb b/spec/factories/event.rb index bcb508ac3..d317d8c29 100644 --- a/spec/factories/event.rb +++ b/spec/factories/event.rb @@ -5,6 +5,10 @@ properties { {} } time { Time.zone.now } + trait :feedback_complete do + name { 'feedback_complete' } + end + # FIXME: event.user != visit.user association :visit, factory: :visit end diff --git a/spec/models/data_analysis/feedback_forms_spec.rb b/spec/models/data_analysis/feedback_forms_spec.rb new file mode 100644 index 000000000..1031c7dfa --- /dev/null +++ b/spec/models/data_analysis/feedback_forms_spec.rb @@ -0,0 +1,58 @@ +require 'rails_helper' + +RSpec.describe DataAnalysis::FeedbackForms do + let(:headers) do + [ + 'Module', + 'Total Responses', + 'Signed in Users', + 'Guest Users', + ] + end + + let(:rows) do + [ + { + mod: 'alpha', + total: 2, + signed_in: 2, + guest: 0, + }, + { + mod: 'bravo', + total: 0, + signed_in: 0, + guest: 0, + }, + { + mod: 'charlie', + total: 0, + signed_in: 0, + guest: 0, + }, + { + mod: 'delta', + total: 0, + signed_in: 0, + guest: 0, + }, + { + mod: 'site wide', + total: 2, + signed_in: 1, + guest: 1, + }, + ] + end + + let(:user) { create(:user, :registered) } + + before do + create :event, :feedback_complete, user_id: user.id, properties: { 'training_module_id' => 'alpha' } + create :event, :feedback_complete, user_id: user.id, properties: { 'training_module_id' => 'alpha' } + create :event, :feedback_complete, user_id: user.id, properties: { 'controller' => 'feedback' } + create :event, :feedback_complete, user_id: nil, properties: { 'controller' => 'feedback' } + end + + it_behaves_like 'a data export model' +end From bf2280afcedf8be4c2a91ae36e7fd713421cbd71 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Fri, 15 Mar 2024 13:41:40 +0000 Subject: [PATCH 44/95] update feedback controller spec --- spec/controllers/feedback_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index 393904ea1..596d3adc5 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -45,7 +45,7 @@ it 'redirects to the next feedback path' do post :update, params: valid_attributes - expect(response).to redirect_to(feedback_path(2)) + expect(response).to redirect_to(feedback_path('feedback-yesnoandtext')) end it 'tracks feedback complete' do From 1b846628344f8dfa6a02740c755cf94c9a108a50 Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:45:18 +0000 Subject: [PATCH 45/95] Update styling for feedback questions and update failing specs to use yml file --- .../feedback_pagination_decorator.rb | 32 +-- app/forms/form_builder.rb | 11 + app/services/content_integrity.rb | 4 +- app/views/feedback/_check_boxes.html.slim | 4 + app/views/feedback/_radio_buttons.html.slim | 2 +- app/views/feedback/index.html.slim | 2 +- app/views/feedback/show.html.slim | 5 +- cms/migrate/02-create-question.js | 2 +- .../feedback_pagination_decorator_spec.rb | 46 +++ spec/lib/content_test_schema_spec.rb | 4 +- spec/models/response_spec.rb | 1 - .../ast/alpha-fail-feedback-form-response.yml | 222 ++++++++++++++ .../ast/alpha-pass-feedback-form-response.yml | 270 ++++++++++++++++++ spec/system/summative_assessment_spec.rb | 1 + 14 files changed, 580 insertions(+), 26 deletions(-) create mode 100644 spec/decorators/feedback_pagination_decorator_spec.rb create mode 100644 spec/support/ast/alpha-fail-feedback-form-response.yml create mode 100644 spec/support/ast/alpha-pass-feedback-form-response.yml diff --git a/app/decorators/feedback_pagination_decorator.rb b/app/decorators/feedback_pagination_decorator.rb index 2f5ca70d5..f1ef32c22 100644 --- a/app/decorators/feedback_pagination_decorator.rb +++ b/app/decorators/feedback_pagination_decorator.rb @@ -10,24 +10,24 @@ def heading end # @return [String] - # def section_numbers - # I18n.t(:section, scope: :pagination, current: content.submodule - 1, total: section_total - 1) - # end + def section_numbers + I18n.t(:section, scope: :pagination, current: content.submodule - 1, total: section_total - 1) + end - # private +private # @return [Integer] - # def page_total - # size = content.section_content.size - # if content.section_content.any?(&:skippable?) # && response_for_shared.responded? - # # don't count skipped page - # content.section_content.each do |section_content| - # if section_content.feedback_question? && section_content.skippable_question.eql?(false) - # size -= 1 - # end - # end - # end + def page_total + size = content.section_content.size + if content.section_content.any?(&:skippable?) + # don't count skipped page + content.section_content.each do |section_content| + if section_content.feedback_question? && section_content.skippable_question.eql?(true) # && response_for_shared.responded? + size -= 1 + end + end + end - # size - # end + size + end end diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index b1b207db4..99c3b9434 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -61,6 +61,17 @@ def or_radio_button(text:, checked:) checked: checked end + # @param option [Training::Answer::Option] + # @option text [String] + # @return [String] + def or_checkbox_button(text:, checked:) + govuk_check_box :answers, + 0, + label: { text: text }, + link_errors: true, + checked: checked + end + # @param option [Training::Answer::Option] # @return [String] def question_check_box(option) diff --git a/app/services/content_integrity.rb b/app/services/content_integrity.rb index baee624c2..abcf09fe1 100644 --- a/app/services/content_integrity.rb +++ b/app/services/content_integrity.rb @@ -46,7 +46,7 @@ class ContentIntegrity confidence: 'Insufficient confidence checks', # NB: disabled until new validity of feedback questions can be asserted - # question_answers: 'Question answers are incorrectly formatted', # TODO: which question? + question_answers: 'Question answers are incorrectly formatted', # TODO: which question? }.freeze # @return [nil] @@ -164,7 +164,7 @@ def video? # @return [Boolean] def question_answers? - mod.questions.all? { |question| question.answer.valid? } + mod.questions.all? { |question| question.answer.valid? && question.feedback_question? } end # @return [Boolean] diff --git a/app/views/feedback/_check_boxes.html.slim b/app/views/feedback/_check_boxes.html.slim index 1bd226b4d..98361acdb 100644 --- a/app/views/feedback/_check_boxes.html.slim +++ b/app/views/feedback/_check_boxes.html.slim @@ -7,5 +7,9 @@ - else = f.question_check_box(option) + - if content.has_or? && option.last? + = f.govuk_radio_divider 'Or' + = f.or_checkbox_button(text: content.or, checked: response.checked_or?) + - if content.has_hint? = f.govuk_text_area :text_input, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } diff --git a/app/views/feedback/_radio_buttons.html.slim b/app/views/feedback/_radio_buttons.html.slim index 88844b188..ca015d16d 100644 --- a/app/views/feedback/_radio_buttons.html.slim +++ b/app/views/feedback/_radio_buttons.html.slim @@ -14,5 +14,5 @@ - else = f.question_radio_button(option) - - if content.has_hint? + - if content.has_hint? && !content.skippable? = f.govuk_text_area :text_input, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index d0c7db00f..dbb56cde3 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -11,7 +11,7 @@ = m('feedback.intro') .govuk-button-group - = govuk_button_link_to t('next_page.give_feedback'), feedback_path(mod.pages.first.name) + = govuk_button_link_to t('next_page.next'), feedback_path(mod.pages.first.name) diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index 4ed5f6a90..c6e700d25 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -3,8 +3,7 @@ = form_with model: current_user_response, url: feedback_path, method: :patch do |f| .govuk-grid-row .govuk-grid-column-two-thirds - - hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' + = govuk_back_link href: my_modules_path = f.govuk_error_summary @@ -14,6 +13,8 @@ = render partial: content.to_partial_path, locals: { f: f }, object: current_user_response, as: :response + hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' + .govuk-button-group - if content.first_feedback? = govuk_button_link_to t('previous_page.previous'), feedback_index_path, secondary: true diff --git a/cms/migrate/02-create-question.js b/cms/migrate/02-create-question.js index d46301012..d2113449f 100644 --- a/cms/migrate/02-create-question.js +++ b/cms/migrate/02-create-question.js @@ -108,7 +108,7 @@ module.exports = function(migration) { }) question.createField('skippable', { - name: 'One-shot question', + name: 'One-off question', type: 'Boolean', required: true, defaultValue: { diff --git a/spec/decorators/feedback_pagination_decorator_spec.rb b/spec/decorators/feedback_pagination_decorator_spec.rb new file mode 100644 index 000000000..77499719f --- /dev/null +++ b/spec/decorators/feedback_pagination_decorator_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +RSpec.describe FeedbackPaginationDecorator do + subject(:decorator) do + described_class.new(content) + end + + let(:mod) { Training::Module.by_name(:alpha) } + let(:content) { mod.page_by_name('feedback-radiobutton') } + + it '#heading' do + expect(decorator.heading).to eq 'Additional feedback' + end + + it '#section_numbers' do + expect(decorator.section_numbers).to eq 'Section 3 of 4' + end + + it '#page_numbers (should skip the one off question)' do + expect(decorator.page_numbers).to eq 'Page 1 of 8' + end + + it '#percentage' do + expect(decorator.percentage).to eq '13%' + end + + describe 'skippable questions' do + let(:content) { mod.page_by_name('feedback-freetext') } + + context 'when answered' do + before do + create(:response, question_name: content.name, text_input: 'text input') + end + + it '#page_numbers' do + expect(decorator.page_numbers).to eq 'Page 3 of 8' + end + end + + context 'when unanswered' do + it '#page_numbers' do + expect(decorator.page_numbers).to eq 'Page 3 of 8' + end + end + end +end diff --git a/spec/lib/content_test_schema_spec.rb b/spec/lib/content_test_schema_spec.rb index c77f5064d..7677bf52d 100644 --- a/spec/lib/content_test_schema_spec.rb +++ b/spec/lib/content_test_schema_spec.rb @@ -14,7 +14,7 @@ end context 'when pass is true' do - let(:fixture) { 'alpha-pass' } + let(:fixture) { 'alpha-pass-feedback-form' } specify do expect(schema.call).to eq ast @@ -22,7 +22,7 @@ end context 'when pass is false' do - let(:fixture) { 'alpha-fail' } + let(:fixture) { 'alpha-fail-feedback-form' } specify do expect(schema.call(pass: false).compact).to eq ast diff --git a/spec/models/response_spec.rb b/spec/models/response_spec.rb index cd30cee43..1552ae425 100644 --- a/spec/models/response_spec.rb +++ b/spec/models/response_spec.rb @@ -22,7 +22,6 @@ updated_at question_type assessment_id - guest_visit text_input visit_id ] diff --git a/spec/support/ast/alpha-fail-feedback-form-response.yml b/spec/support/ast/alpha-fail-feedback-form-response.yml new file mode 100644 index 000000000..23bdee867 --- /dev/null +++ b/spec/support/ast/alpha-fail-feedback-form-response.yml @@ -0,0 +1,222 @@ +# +# Feedback form for end of module +# +--- +- :path: /modules/alpha/content-pages/what-to-expect + :text: What to expect during the training + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-1 + :text: The first submodule + :inputs: + - - :click_on + - Start section +- :path: /modules/alpha/content-pages/1-1-1 + :text: 1-1-1 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-1-2 + :text: 1-1-2 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-1-3 + :text: 1-1-3 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-1-3-1 + :text: 1-1-3-1 + :inputs: + - - :make_note + - note-body-field + - hello world + - - :click_on + - Save and continue +- :path: /modules/alpha/content-pages/1-1-4 + :text: 1-1-4 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/questionnaires/1-1-4-1 + :text: Question One - Select from following + :inputs: + - - :choose + - response-answers-2-field + - - :click_on + - Next + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-2 + :text: The second submodule + :inputs: + - - :click_on + - Start section +- :path: /modules/alpha/content-pages/1-2-1 + :text: 1-2-1 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/questionnaires/1-2-1-1 + :text: Question Two - Select from following + :inputs: + - - :check + - response-answers-2-field + - - :click_on + - Next + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-2-1-2 + :text: 1-2-1-2 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/questionnaires/1-2-1-3 + :text: Question Three - Select from following + :inputs: + - - :check + - response-answers-2-field + - - :check + - response-answers-4-field + - - :click_on + - Next + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-3 + :text: Summary and next steps + :inputs: + - - :click_on + - Start section +- :path: /modules/alpha/content-pages/1-3-1 + :text: Recap + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-3-2 + :text: End of module test + :inputs: + - - :click_on + - Start test +- :path: /modules/alpha/questionnaires/1-3-2-1 + :text: Question One - Select from following + :inputs: + - - :check + - response-answers-2-field + - - :check + - response-answers-4-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-2 + :text: Question Two - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :check + - response-answers-4-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-3 + :text: Question Three - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :check + - response-answers-2-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-4 + :text: Question Four - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-5 + :text: Question Five - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-6 + :text: Question Six - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-7 + :text: Question Seven - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-8 + :text: Question Eight - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-9 + :text: Question Nine - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-10 + :text: Question Ten - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Finish test +- :path: /modules/alpha/content-pages/feedback-intro + :text: Additional feedback + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-radiobutton + :text: 'Feedback question 1 - Select from following' + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-yesnoandtext + :text: Feedback question 2 - Select from following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-freetext + :text: Feedback question 3 - Complete the following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-radio-otherandtext + :text: Feedback question 4 - Select from following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-radio-and-freetext + :text: Feedback question 5 - Select from following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-checkbox-othertextandor + :text: Feedback question 6 - Select from following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-checkbox-otherandtext + :text: Feedback question 7 - Select from following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-oneoffquestion + :text: Feedback question 8 - Select from following + :inputs: + - - :click_on + - Next diff --git a/spec/support/ast/alpha-pass-feedback-form-response.yml b/spec/support/ast/alpha-pass-feedback-form-response.yml new file mode 100644 index 000000000..97998040a --- /dev/null +++ b/spec/support/ast/alpha-pass-feedback-form-response.yml @@ -0,0 +1,270 @@ +# +# Feedback form for end of module +# +--- +- :path: /modules/alpha/content-pages/what-to-expect + :text: What to expect during the training + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-1 + :text: The first submodule + :inputs: + - - :click_on + - Start section +- :path: /modules/alpha/content-pages/1-1-1 + :text: 1-1-1 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-1-2 + :text: 1-1-2 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-1-3 + :text: 1-1-3 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-1-3-1 + :text: 1-1-3-1 + :inputs: + - - :make_note + - note-body-field + - hello world + - - :click_on + - Save and continue +- :path: /modules/alpha/content-pages/1-1-4 + :text: 1-1-4 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/questionnaires/1-1-4-1 + :text: Question One - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-2 + :text: The second submodule + :inputs: + - - :click_on + - Start section +- :path: /modules/alpha/content-pages/1-2-1 + :text: 1-2-1 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/questionnaires/1-2-1-1 + :text: Question Two - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :check + - response-answers-3-field + - - :click_on + - Next + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-2-1-2 + :text: 1-2-1-2 + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/questionnaires/1-2-1-3 + :text: Question Three - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :check + - response-answers-3-field + - - :click_on + - Next + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-3 + :text: Summary and next steps + :inputs: + - - :click_on + - Start section +- :path: /modules/alpha/content-pages/1-3-1 + :text: Recap + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-3-2 + :text: End of module test + :inputs: + - - :click_on + - Start test +- :path: /modules/alpha/questionnaires/1-3-2-1 + :text: Question One - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :check + - response-answers-3-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-2 + :text: Question Two - Select from following + :inputs: + - - :check + - response-answers-2-field + - - :check + - response-answers-3-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-3 + :text: Question Three - Select from following + :inputs: + - - :check + - response-answers-3-field + - - :check + - response-answers-4-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-4 + :text: Question Four - Select from following + :inputs: + - - :choose + - response-answers-3-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-5 + :text: Question Five - Select from following + :inputs: + - - :choose + - response-answers-3-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-6 + :text: Question Six - Select from following + :inputs: + - - :choose + - response-answers-3-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-7 + :text: Question Seven - Select from following + :inputs: + - - :choose + - response-answers-3-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-8 + :text: Question Eight - Select from following + :inputs: + - - :choose + - response-answers-3-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-9 + :text: Question Nine - Select from following + :inputs: + - - :choose + - response-answers-3-field + - - :click_on + - Save and continue +- :path: /modules/alpha/questionnaires/1-3-2-10 + :text: Question Ten - Select from following + :inputs: + - - :choose + - response-answers-3-field + - - :click_on + - Finish test +- :path: /modules/alpha/assessment-result/1-3-2-11 + :text: Assessment results + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-3-3 + :text: Reflect on your learning + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/questionnaires/1-3-3-1 + :text: Question One - Select from 1 to 5 + :inputs: + - - :choose + - response-answers-5-field + - - :click_on + - Next +- :path: /modules/alpha/questionnaires/1-3-3-2 + :text: Question Two - Select from 1 to 5 + :inputs: + - - :choose + - response-answers-5-field + - - :click_on + - Next +- :path: /modules/alpha/questionnaires/1-3-3-3 + :text: Question Three - Select from 1 to 5 + :inputs: + - - :choose + - response-answers-5-field + - - :click_on + - Next +- :path: /modules/alpha/questionnaires/1-3-3-4 + :text: Question Four - Select from 1 to 5 + :inputs: + - - :choose + - response-answers-5-field + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-intro + :text: Additional feedback + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-radiobutton + :text: 'Feedback question 1 - Select from following' + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-yesnoandtext + :text: Feedback question 2 - Select from following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-freetext + :text: Feedback question 3 - Complete the following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-radio-otherandtext + :text: Feedback question 4 - Select from following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-radio-and-freetext + :text: Feedback question 5 - Select from following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-checkbox-othertextandor + :text: Feedback question 6 - Select from following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-checkbox-otherandtext + :text: Feedback question 7 - Select from following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/feedback-oneoffquestion + :text: Feedback question 8 - Select from following + :inputs: + - - :click_on + - Next +- :path: /modules/alpha/content-pages/1-3-3-5 + :text: Thank you + :inputs: + - - :click_on + - View certificate +- :path: /modules/alpha/content-pages/1-3-4 + :text: Congratulations! + :inputs: [] \ No newline at end of file diff --git a/spec/system/summative_assessment_spec.rb b/spec/system/summative_assessment_spec.rb index 555ee9a4c..ed165d59a 100644 --- a/spec/system/summative_assessment_spec.rb +++ b/spec/system/summative_assessment_spec.rb @@ -4,6 +4,7 @@ include_context 'with progress' include_context 'with user' + let(:fixture) { 'spec/support/ast/alpha-pass-response.yml' } let(:first_question_path) { '/modules/alpha/questionnaires/1-3-2-1' } before do From 128bce22b7949b98a9315f20c922d70c043fecba Mon Sep 17 00:00:00 2001 From: Katherine Martin <78093815+martikat@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:57:22 +0000 Subject: [PATCH 46/95] Update spelling and small update for content integrity check --- app/services/content_integrity.rb | 5 +++++ app/views/feedback/index.html.slim | 2 +- config/locales/en.yml | 2 +- spec/system/feedback_internal_spec.rb | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/services/content_integrity.rb b/app/services/content_integrity.rb index abcf09fe1..b924d42ab 100644 --- a/app/services/content_integrity.rb +++ b/app/services/content_integrity.rb @@ -203,6 +203,11 @@ def confidence_intro? page_by_type_position(type: 'confidence_intro') end + # @return [Boolean] + def feedback? + page_by_type_position(type: 'feedback') + end + # @return [Boolean] def results? page_by_type_position(type: 'assessment_results') diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index dbb56cde3..a0c01777e 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -4,7 +4,7 @@ = m('feedback.feedback_exists') .govuk-button-group - = govuk_button_link_to t('previous_page.previous'), my_modules_path, secondary: true + / = govuk_button_link_to t('previous_page.previous'), my_modules_path, secondary: true = govuk_button_link_to t('links.update_feedback'), feedback_path(mod.pages.first.name) - else diff --git a/config/locales/en.yml b/config/locales/en.yml index 1dfae1876..c72cb400e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -615,7 +615,7 @@ en: intro: | # Give feedback - The purpose of this feedback form is to gather your opinon on the child development + The purpose of this feedback form is to gather your opinion on the child development training course that the Department for Education has created for early years practitioners. For more information on how you data will be used, please view our privacy notice. ADD LINK diff --git a/spec/system/feedback_internal_spec.rb b/spec/system/feedback_internal_spec.rb index 131fd3086..8548ad79a 100644 --- a/spec/system/feedback_internal_spec.rb +++ b/spec/system/feedback_internal_spec.rb @@ -7,7 +7,7 @@ describe 'foo' do it 'bar' do - expect(page).to have_content 'The purpose of this feedback form is to gather your opinon on the child development training course' + expect(page).to have_content 'The purpose of this feedback form is to gather your opinion on the child development training course' end end end From ca42b28787b09e0b4db99ea8bfb2d432c679b6c0 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Tue, 2 Apr 2024 17:41:39 +0100 Subject: [PATCH 47/95] update branch, spec updates, content integrity tweak --- Gemfile | 2 - Gemfile.lock | 8 -- README.md | 3 + app/controllers/application_controller.rb | 8 +- app/controllers/close_accounts_controller.rb | 9 +- app/controllers/confirmations_controller.rb | 27 ----- app/controllers/home_controller.rb | 2 - app/controllers/passwords_controller.rb | 7 -- app/controllers/registrations_controller.rb | 46 -------- app/controllers/users/sessions_controller.rb | 18 +-- app/helpers/content_helper.rb | 6 - app/helpers/link_helper.rb | 3 +- app/mailers/notify_mailer.rb | 75 ------------- app/models/user.rb | 63 ++--------- app/services/content_integrity.rb | 2 +- app/views/about/_enrol.html.slim | 8 +- app/views/close_accounts/new.html.slim | 20 ---- app/views/devise/confirmations/new.html.slim | 19 ---- app/views/devise/passwords/edit.html.slim | 16 --- app/views/devise/passwords/new.html.slim | 19 ---- app/views/devise/registrations/edit.html.slim | 16 --- app/views/devise/registrations/new.html.slim | 25 ----- app/views/devise/sessions/new.html.slim | 38 ------- app/views/home/_hero.html.slim | 22 +--- app/views/home/index.html.slim | 44 ++------ app/views/user/edit_email.html.slim | 16 --- app/views/user/edit_password.html.slim | 19 ---- app/views/user/show.html.slim | 17 +-- config/application.rb | 13 --- config/initializers/devise.rb | 4 +- config/initializers/devise_security.rb | 52 --------- config/locales/devise.en.yml | 1 - config/locales/en.yml | 26 ----- config/routes.rb | 7 +- config/sitemap.rb | 7 -- spec/controllers/user_controller_spec.rb | 106 ------------------ .../omniauth_callbacks_controller_spec.rb | 3 +- .../users/sessions_controller_spec.rb | 4 - spec/factories/users.rb | 7 +- spec/lib/seed_snippets_spec.rb | 6 +- spec/mailers/notify_mailer_spec.rb | 84 -------------- .../data_analysis/user_overview_spec.rb | 4 +- spec/models/training/response_spec.rb | 2 + spec/models/user_spec.rb | 25 ++++- spec/rails_helper.rb | 2 +- spec/requests/authentication_spec.rb | 28 +---- spec/requests/user_spec.rb | 14 --- spec/support/shared/with_user.rb | 20 +--- spec/system/account_page_spec.rb | 10 +- .../completing_registration_spec.rb | 16 ++- spec/system/forgotten_password_spec.rb | 97 ---------------- spec/system/front_page_spec.rb | 42 ++----- spec/system/gov_one_spec.rb | 4 - spec/system/locked_account_spec.rb | 40 ------- spec/system/page_title_spec.rb | 45 +------- .../registered_user/changing_password_spec.rb | 54 --------- .../registered_user/closing_account_spec.rb | 27 +---- spec/system/registration_journey_spec.rb | 67 ----------- spec/system/sign_in_spec.rb | 74 ------------ spec/system/sign_up_spec.rb | 74 ------------ spec/system/whats_new_page_spec.rb | 12 +- 61 files changed, 116 insertions(+), 1419 deletions(-) delete mode 100644 app/controllers/confirmations_controller.rb delete mode 100644 app/controllers/passwords_controller.rb delete mode 100644 app/controllers/registrations_controller.rb delete mode 100644 app/views/close_accounts/new.html.slim delete mode 100644 app/views/devise/confirmations/new.html.slim delete mode 100644 app/views/devise/passwords/edit.html.slim delete mode 100644 app/views/devise/passwords/new.html.slim delete mode 100644 app/views/devise/registrations/edit.html.slim delete mode 100644 app/views/devise/registrations/new.html.slim delete mode 100644 app/views/devise/sessions/new.html.slim delete mode 100644 app/views/user/edit_email.html.slim delete mode 100644 app/views/user/edit_password.html.slim delete mode 100644 config/initializers/devise_security.rb delete mode 100644 spec/controllers/user_controller_spec.rb delete mode 100644 spec/system/forgotten_password_spec.rb delete mode 100644 spec/system/locked_account_spec.rb delete mode 100644 spec/system/registered_user/changing_password_spec.rb delete mode 100644 spec/system/registration_journey_spec.rb delete mode 100644 spec/system/sign_in_spec.rb delete mode 100644 spec/system/sign_up_spec.rb diff --git a/Gemfile b/Gemfile index 4773565d0..f146fab03 100644 --- a/Gemfile +++ b/Gemfile @@ -30,8 +30,6 @@ gem 'bootsnap', require: false # Users gem 'devise' -gem 'devise-pwned_password' -gem 'devise-security' gem 'dibber' gem 'jwt' gem 'omniauth_openid_connect' diff --git a/Gemfile.lock b/Gemfile.lock index cfa8a4f92..27eb35b28 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,11 +150,6 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-pwned_password (0.1.10) - devise (~> 4) - pwned (~> 2.0.0) - devise-security (0.18.0) - devise (>= 4.3.0) dibber (0.7.0) diff-lcs (1.5.1) digest-crc (0.6.5) @@ -396,7 +391,6 @@ GEM public_suffix (5.0.4) puma (6.4.2) nio4r (~> 2.0) - pwned (2.0.2) que (2.3.0) que-scheduler (4.4.0) activesupport (>= 5.0) @@ -657,8 +651,6 @@ DEPENDENCIES cssbundling-rails debug devise - devise-pwned_password - devise-security dibber dotenv-rails dry-core diff --git a/README.md b/README.md index a4329f396..2a6acc222 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,9 @@ or in the UK Government digital slack workspace in the `#govuk-notify` channel. # GOV.UK One Login +GOV.UK One Login admin tool exists for managing the integration environment client config. +It can be accessed at (currently only 1 email can access the client config). + ### Account Registration Register an account on the integration OIDC used in development . diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d9d4ac986..1574e811b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -20,17 +20,11 @@ def authenticate_registered_user! authenticate_user! unless user_signed_in? return true if current_user.registration_complete? - if Rails.application.gov_one_login? - redirect_to edit_registration_terms_and_conditions_path, notice: 'Please complete registration' - else - redirect_to edit_registration_name_path, notice: 'Please complete registration' - end + redirect_to edit_registration_terms_and_conditions_path, notice: 'Please complete registration' end def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:terms_and_conditions_agreed_at]) - update_attrs = %i[password password_confirmation current_password] - devise_parameter_sanitizer.permit :account_update, keys: update_attrs end # @return [nil] diff --git a/app/controllers/close_accounts_controller.rb b/app/controllers/close_accounts_controller.rb index 6a28ee5e1..a5b3a4114 100644 --- a/app/controllers/close_accounts_controller.rb +++ b/app/controllers/close_accounts_controller.rb @@ -3,15 +3,8 @@ class CloseAccountsController < ApplicationController def show; end - def new; end - def update - if current_user.valid_password?(user_password_params[:current_password]) - redirect_to edit_reason_user_close_account_path - else - current_user.errors.add(:current_password, :confirmation_invalid, message: 'Enter a valid password') - render :new, status: :unprocessable_entity - end + redirect_to edit_reason_user_close_account_path end def reset_password diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb deleted file mode 100644 index fb7d93376..000000000 --- a/app/controllers/confirmations_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -class ConfirmationsController < Devise::ConfirmationsController - def show - self.resource = resource_class.confirm_by_token(params[:confirmation_token]) - yield resource if block_given? - - if resource.errors.empty? - set_flash_message!(:notice, (resource.registration_complete ? :confirmed : :activated)) - respond_with_navigational(resource) { redirect_to after_confirmation_path_for(resource_name, resource) } - else - respond_with_navigational(resource.errors, status: :unprocessable_entity) { render :new } - end - end - -protected - - def after_confirmation_path_for(resource_name, _resource) - if signed_in?(resource_name) - user_path - else - new_session_path(resource_name) - end - end - - def after_resending_confirmation_instructions_path_for(_resource_name) - check_email_confirmation_user_path(ref: resource.confirmation_token) - end -end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 49c4e3cfa..8d57bbadf 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -5,8 +5,6 @@ class HomeController < ApplicationController def index track('home_page') - - flash.now[:important] = t('banners.gov_one') unless Rails.application.gov_one_login? end def show diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb deleted file mode 100644 index c9d862fab..000000000 --- a/app/controllers/passwords_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -class PasswordsController < Devise::PasswordsController -protected - - def after_sending_reset_password_instructions_path_for(_resource_name) - check_email_password_reset_user_path - end -end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb deleted file mode 100644 index 1d2210c00..000000000 --- a/app/controllers/registrations_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -class RegistrationsController < Devise::RegistrationsController - # Patch to prevent user enumeration exploit by silencing the 'taken' error - def create - build_resource(sign_up_params) - - # "save!" would raise validation that email has already been taken - resource.save - yield resource if block_given? - - if resource.persisted? - if resource.active_for_authentication? - set_flash_message! :notice, :signed_up - sign_up(resource_name, resource) - respond_with resource, location: after_sign_up_path_for(resource) - else - set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}" - expire_data_after_sign_in! - respond_with resource, location: after_inactive_sign_up_path_for(resource) - end - - # imitate success - elsif resource.errors.one? && resource.errors.first.type.eql?(:taken) - resource.errors.delete :email - - @user = resource - render 'user/check_email_confirmation', status: :unprocessable_entity - - resource.send_email_taken_notification - - track('email_address_taken', email: resource.email, ip: request.ip, user_agent: request.user_agent) - else - # always hide taken message - resource.errors.delete(:email) if resource.errors.first.type.eql?(:taken) - - clean_up_passwords resource - set_minimum_password_length - respond_with resource - end - end - -protected - - def after_inactive_sign_up_path_for(resource) - check_email_confirmation_user_path(ref: resource.confirmation_token) - end -end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index b079bb626..653f68b9a 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,13 +1,17 @@ # frozen_string_literal: true class Users::SessionsController < Devise::SessionsController - layout 'hero' if Rails.application.gov_one_login? + layout 'hero' def new - if Rails.application.gov_one_login? - render 'gov_one' - else - super + render 'gov_one' + end + + # @return [nil] + def sign_in_test_user + unless Rails.application.live? + test_user = User.test_user + sign_in_and_redirect test_user if test_user end end @@ -26,10 +30,8 @@ def after_sign_in_path_for(resource) end elsif resource.private_beta_registration_complete? static_path('new-registration') - elsif Rails.application.gov_one_login? - edit_registration_terms_and_conditions_path else - edit_registration_name_path + edit_registration_terms_and_conditions_path end end end diff --git a/app/helpers/content_helper.rb b/app/helpers/content_helper.rb index 3868f9164..f33e2d2c4 100644 --- a/app/helpers/content_helper.rb +++ b/app/helpers/content_helper.rb @@ -73,10 +73,4 @@ def service_name def privacy_policy_url Course.config.privacy_policy_url end - - # TODO: remove this method and coresponding locale string - # @yield [String] - def password_complexity - t('password_complexity', length: User.password_length.first) - end end diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index f159c9bce..63eff4584 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -1,8 +1,7 @@ module LinkHelper - # @note Handle active sessions for feature flag Rails.application.gov_one_login? # @return [String] def destroy_user_session_path - session[:id_token].present? ? logout_uri.to_s : super + session[:id_token].present? && !current_user.test_user? ? logout_uri.to_s : super end # OPTIMIZE: use this helper for all back link logic and consistent location diff --git a/app/mailers/notify_mailer.rb b/app/mailers/notify_mailer.rb index 499c00772..00200eb06 100644 --- a/app/mailers/notify_mailer.rb +++ b/app/mailers/notify_mailer.rb @@ -2,11 +2,6 @@ class NotifyMailer < GovukNotifyRails::Mailer ACCOUNT_CLOSED_TEMPLATE_ID = '0a4754ee-6175-444c-98a1-ebef0b14e7f7'.freeze ACCOUNT_CLOSED_INTERNAL_TEMPLATE_ID = 'a2dba0ef-84f1-4b4d-b50a-ce953050798e'.freeze ACTIVATION_TEMPLATE_ID = 'd6ab2e3b-923e-429e-abd2-cfe7be0e9193'.freeze - EMAIL_CHANGED_TEMPLATE_ID = 'c1228884-6621-4a1e-9606-b219bedb677f'.freeze - EMAIL_CONFIRMATION_TEMPLATE_ID = 'a2412831-e253-4df4-a8f1-19332eed4cef'.freeze - EMAIL_TAKEN_TEMPLATE_ID = '857dc6d0-7179-48bf-8079-916fedb43528'.freeze - PASSWORD_CHANGED_TEMPLATE_ID = 'f77e1eba-3fa8-45ae-9cec-a4cc54633395'.freeze - RESET_PASSWORD_TEMPLATE_ID = 'ad77aab8-d903-4f77-b074-a16c2658ca79'.freeze UNLOCK_TEMPLATE_ID = 'e18e8419-cfcc-4fcb-abdb-84f932f3cf55'.freeze COMPLETE_REGISTRATION_TEMPLATE_ID = 'b960eb6a-d183-484b-ac3b-93ae01b3cee1'.freeze START_TRAINING_TEMPLATE_ID = 'b3c2e4ff-da06-4672-8941-b2f50d37eadc'.freeze @@ -41,76 +36,6 @@ def account_closed_internal(record, user_email_address) mail(to: record.email) end - def activation_instructions(record, token, _opts = {}) - set_template(ACTIVATION_TEMPLATE_ID) - - set_personalisation( - confirmation_url: confirmation_url(record, confirmation_token: token), - ) - mail(to: record.unconfirmed_email? ? record.unconfirmed_email : record.email) - end - - def email_changed(record, _opts = {}) - set_template(EMAIL_CHANGED_TEMPLATE_ID) - - set_personalisation( - is_unconfirmed_email: record.unconfirmed_email? ? 'Yes' : 'No', - is_not_unconfirmed_email: record.unconfirmed_email? ? 'No' : 'Yes', - email: record.unconfirmed_email? ? record.unconfirmed_email : record.email, - ) - mail(to: record.email) - end - - def email_confirmation_instructions(record, token, _opts = {}) - set_template(EMAIL_CONFIRMATION_TEMPLATE_ID) - - set_personalisation( - confirmation_url: confirmation_url(record, confirmation_token: token), - ) - mail(to: record.unconfirmed_email? ? record.unconfirmed_email : record.email) - end - - def email_taken(record) - set_template(EMAIL_TAKEN_TEMPLATE_ID) - - set_personalisation( - name: record.name, - email: record.email, - reset_password_url: new_user_password_url, - ) - mail(to: record.email) - end - - def password_change(record, _opts = {}) - set_template(PASSWORD_CHANGED_TEMPLATE_ID) - - set_personalisation( - email_subject: 'Password changed', - name: record.name, - ) - mail(to: record.email) - end - - def reset_password_instructions(record, token, _opts = {}) - set_template(RESET_PASSWORD_TEMPLATE_ID) - - set_personalisation( - name: record.name, - edit_password_url: edit_password_url(record, reset_password_token: token), - ) - mail(to: record.email) - end - - def unlock_instructions(record, token, _opts = {}) - set_template(UNLOCK_TEMPLATE_ID) - - set_personalisation( - name: record.name, - unlock_url: unlock_url(record, unlock_token: token), - ) - mail(to: record.email) - end - # @param [User] record def complete_registration(record) set_template(COMPLETE_REGISTRATION_TEMPLATE_ID) diff --git a/app/models/user.rb b/app/models/user.rb index 8c8c89d81..50a5fa1ff 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -47,23 +47,20 @@ def self.find_or_create_from_gov_one(email:, gov_one_id:) user end - attr_accessor :context + # @return [User] + def self.test_user + find_by(email: 'completed@example.com') + end - devise :database_authenticatable, :registerable, :recoverable, - :validatable, :rememberable, :confirmable, :lockable, :timeoutable, - :secure_validatable, :omniauthable, omniauth_providers: [:openid_connect] + # @return [Boolean] + def test_user? + email == 'completed@example.com' + end - # FIXME: retire old devise functionality - # if Rails.application.gov_one_login? - # devise :database_authenticatable, :rememberable, :lockable, :timeoutable, - # :omniauthable, omniauth_providers: [:openid_connect] - # else - # devise :database_authenticatable, :registerable, :recoverable, - # :validatable, :rememberable, :confirmable, :lockable, :timeoutable, - # :secure_validatable - # end + attr_accessor :context - devise :pwned_password unless Rails.env.test? + devise :database_authenticatable, :rememberable, :lockable, :timeoutable, + :omniauthable, omniauth_providers: [:openid_connect] has_many :responses has_many :user_answers @@ -163,11 +160,7 @@ def self.find_or_create_from_gov_one(email:, gov_one_id:) inclusion: { in: Trainee::Setting.valid_types }, if: proc { |u| u.registration_complete? } - if Rails.application.gov_one_login? - validates :terms_and_conditions_agreed_at, presence: true, allow_nil: false, on: :update, if: proc { |u| u.registration_complete? } - else - validates :terms_and_conditions_agreed_at, presence: true, allow_nil: false, on: :create - end + validates :terms_and_conditions_agreed_at, presence: true, allow_nil: false, on: :update, if: proc { |u| u.registration_complete? } # @return [Boolean] def notes? @@ -184,18 +177,6 @@ def guest? false end - # @see Devise database_authenticatable - # @param params [Hash] - # @return [Boolean] - def update_with_password(params) - if params[:password].blank? - errors.add :password, :blank - return false - end - - super - end - # @see ResponsesController#response_params # @param content [Training::Question] # @return [UserAnswer, Response] @@ -245,23 +226,6 @@ def active_modules end end - # @see Devise::Confirmable - # send_confirmation_instructions - def send_confirmation_instructions - unless @raw_confirmation_token - generate_confirmation_token! - end - - opts = pending_reconfirmation? ? { to: unconfirmed_email } : {} - mailer = registration_complete? ? :email_confirmation_instructions : :activation_instructions - send_devise_notification(mailer, @raw_confirmation_token, opts) - end - - # send email to registered user if attempt is made to create account with registered email - def send_email_taken_notification - send_devise_notification(:email_taken) - end - def send_account_closed_notification send_devise_notification(:account_closed) end @@ -440,9 +404,6 @@ def module_ttc end def redact! - skip_reconfirmation! - skip_email_changed_notification! - skip_password_change_notification! update!(first_name: 'Redacted', last_name: 'User', email: "redacted_user#{id}@example.com", diff --git a/app/services/content_integrity.rb b/app/services/content_integrity.rb index b924d42ab..e461a1555 100644 --- a/app/services/content_integrity.rb +++ b/app/services/content_integrity.rb @@ -164,7 +164,7 @@ def video? # @return [Boolean] def question_answers? - mod.questions.all? { |question| question.answer.valid? && question.feedback_question? } + mod.questions.all? { |question| question.answer.valid? || question.feedback_question? } end # @return [Boolean] diff --git a/app/views/about/_enrol.html.slim b/app/views/about/_enrol.html.slim index d1d078cc7..9b2e28db8 100644 --- a/app/views/about/_enrol.html.slim +++ b/app/views/about/_enrol.html.slim @@ -3,11 +3,5 @@ = m('about.enrol') .govuk-button-group - - if Rails.application.gov_one_login? - = govuk_link_to 'Create an account or sign in', new_user_session_path - - - else - = govuk_button_link_to 'Create an account', new_user_registration_path - .white-space-pre-wrap= ' or ' - = govuk_link_to 'sign in', new_user_session_path + = govuk_link_to 'Create an account or sign in', new_user_session_path diff --git a/app/views/close_accounts/new.html.slim b/app/views/close_accounts/new.html.slim deleted file mode 100644 index 6343c9184..000000000 --- a/app/views/close_accounts/new.html.slim +++ /dev/null @@ -1,20 +0,0 @@ -- content_for :page_title do - = html_title 'For security, enter your password.' - -.govuk-grid-row - .govuk-grid-column-two-thirds-from-desktop - = govuk_back_link(href: user_path) - - = form_with model: current_user, url: user_close_account_path do |f| - = f.govuk_error_summary - = f.govuk_fieldset legend: { text: 'Enter your password' } do - = f.govuk_password_field :current_password, - autofocus: true, - aria: { required: true }, - label: { text: 'For security, enter your password.' }, - hint: { text: 'Your password' } do - p= govuk_details(summary_text: 'Problems with your password?') do - p.govuk-heading-s= 'I have forgotten my password' - = "If you have forgotten your password you can " - = govuk_link_to 'reset it', reset_password_user_close_account_path - = f.govuk_submit t('links.continue') diff --git a/app/views/devise/confirmations/new.html.slim b/app/views/devise/confirmations/new.html.slim deleted file mode 100644 index 53b0277ee..000000000 --- a/app/views/devise/confirmations/new.html.slim +++ /dev/null @@ -1,19 +0,0 @@ -- content_for :page_title do - = html_title t(:title, scope: :account_confirm) - -- if url_for(:back).include?(check_email_confirmation_user_path) - a.govuk-back-link href=url_for(:back) Back - -- content_for :devise_form do - = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| - = f.govuk_error_summary - = f.govuk_fieldset legend: { text: 'Resend your confirmation'} do - p.govuk-body - | If you haven't received an email from us containing a link to validate your email, please check your spam folder. - = f.govuk_email_field :email, autofocus: true, autocomplete: 'email', value: resource.email_to_confirm, label: { text: t('helpers.label.user.email') }, aria: { required: true } - = f.govuk_submit 'Send email' - -= render 'devise/shared/form_wrap' - -- if url_for(:back).include?(check_email_confirmation_user_path) - a.govuk-link href=url_for(:back) Go back to check your email diff --git a/app/views/devise/passwords/edit.html.slim b/app/views/devise/passwords/edit.html.slim deleted file mode 100644 index d2ef2765e..000000000 --- a/app/views/devise/passwords/edit.html.slim +++ /dev/null @@ -1,16 +0,0 @@ -- content_for :page_title do - = html_title t(:title, scope: :account_password_change) - -- content_for :devise_form do - = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| - = f.govuk_error_summary - = f.hidden_field :reset_password_token - = f.govuk_fieldset legend: { text: 'Choose a new password' } do - - p.govuk-hint= password_complexity - - = f.govuk_password_field :password, label: { text: 'New password', size: 's' }, aria: { required: true } - = f.govuk_password_field :password_confirmation, label: { text: 'Confirm password', size: 's' }, aria: { required: true } - = f.govuk_submit 'Reset password' - -= render "devise/shared/form_wrap" diff --git a/app/views/devise/passwords/new.html.slim b/app/views/devise/passwords/new.html.slim deleted file mode 100644 index 33789840e..000000000 --- a/app/views/devise/passwords/new.html.slim +++ /dev/null @@ -1,19 +0,0 @@ -- content_for :page_title do - = html_title t(:title, scope: :account_reset) - -= govuk_link_to 'Back', url_for(:back), class: 'govuk-back-link' - -- content_for :devise_form do - = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| - = f.govuk_error_summary - = f.govuk_fieldset legend: { text: 'I have forgotten my password' } do - p.govuk-body - | You will need to reset your password. - p.govuk-body - | To reset your password we need to send you an email with a validation link. - p.govuk-body - | If you can't see it in your inbox within a few minutes, please check your spam folder. - = f.govuk_email_field :email, autofocus: true, autocomplete: 'email', aria: { required: true } - = f.govuk_submit 'Send email' - -= render 'devise/shared/form_wrap' diff --git a/app/views/devise/registrations/edit.html.slim b/app/views/devise/registrations/edit.html.slim deleted file mode 100644 index c8cdfdad5..000000000 --- a/app/views/devise/registrations/edit.html.slim +++ /dev/null @@ -1,16 +0,0 @@ -- content_for :page_title do - = html_title t(:title, scope: :account_password) - -- content_for :devise_form do - = form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| - = f.govuk_error_summary - = f.govuk_fieldset legend: { text: 'Change your password' } do - - p.govuk-hint= password_complexity - - = f.govuk_password_field :current_password - = f.govuk_password_field :password, label: { text: 'Create a new password' } - = f.govuk_password_field :password_confirmation, label: { text: 'Confirm password' } - = f.govuk_submit 'Save' - -= render 'devise/shared/form_wrap' diff --git a/app/views/devise/registrations/new.html.slim b/app/views/devise/registrations/new.html.slim deleted file mode 100644 index 42b19342b..000000000 --- a/app/views/devise/registrations/new.html.slim +++ /dev/null @@ -1,25 +0,0 @@ -- content_for :page_title do - = html_title t(:title, scope: :account_register) - -= govuk_link_to 'Back', root_path, class: 'govuk-back-link' - -- content_for :devise_form do - = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| - = f.govuk_error_summary - = f.govuk_fieldset legend: { text: content_tag(:span, "Create an #{service_name} account", class: 'govuk-heading-l') } do - p.govuk-body - | Create an account to access the training course. - - = f.govuk_email_field :email, autofocus: true, autocomplete: 'email', label: { text: t('helpers.label.user.email') }, hint: { text: "We'll send you an email with an account activation link." } - = f.govuk_password_field :password, label: { text: 'Create password' }, hint: { text: password_complexity }, aria: { required: true } - = f.govuk_password_field :password_confirmation, aria: { required: true } - - = f.govuk_check_boxes_fieldset :terms_and_conditions_agreed_at, - legend: { class: 'govuk-visually-hidden', text: 'Terms and conditions'}, classes: 'light-grey-box' do - p.govuk-body - | To use this service, you must accept the #{govuk_link_to 'terms and conditions', static_path('terms-and-conditions')} and #{govuk_link_to 'privacy policy', static_path('privacy-policy')}. - = f.terms_and_conditions_check_box - - = f.govuk_submit t('links.continue') - -= render 'devise/shared/form_wrap' diff --git a/app/views/devise/sessions/new.html.slim b/app/views/devise/sessions/new.html.slim deleted file mode 100644 index e9f707c7c..000000000 --- a/app/views/devise/sessions/new.html.slim +++ /dev/null @@ -1,38 +0,0 @@ -- content_for :page_title do - = html_title t(:title, scope: :account_login) - -- content_for :devise_sidebar do - h2.govuk-heading-m - | Don't have an account yet? - - if devise_mapping.registerable? && controller_name != 'registrations' - = link_to "Create an #{service_name} account", new_registration_path(resource_name), class: 'govuk-link' - | to access training material. - -- content_for :devise_form do - = form_for(resource, as: resource_name, url: session_path(resource_name), data: { turbo: false } ) do |f| - = f.govuk_error_summary do - p.govuk-body - | You will need to reset your password if you enter the wrong details five times. - - = f.govuk_fieldset legend: { text: content_tag(:span, "Sign in to #{service_name}", class: 'govuk-heading-l') } do - p.govuk-body - | Enter your details to sign in to your personal #{service_name} account. These details are different to any other GOV.UK accounts you may use. - = f.govuk_email_field :email, autofocus: true, autocomplete: 'email', label: { text: t('helpers.label.user.email') }, aria: { required: true } - = f.govuk_password_field :password, label: { text: 'Password' }, aria: { required: true } - - = govuk_details(summary_text: 'Problems signing in') do - -# OPTIMIZE: refactor to render with locals - - if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' - = govuk_link_to 'I have forgotten my password', new_password_path(resource_name) - br - - if devise_mapping.confirmable? && controller_name != 'confirmations' - = govuk_link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) - br - = govuk_link_to 'Other problems signing in', static_path('other-problems-signing-in') - = f.govuk_submit 'Sign in' - / TODO: confirm whether the remember me functionality should be added back in - / = f.govuk_check_box :remember_me, 1, 0, multiple: false, link_errors: true, label: { text: t('password.remember_me') } - br/ - - -= render 'devise/shared/form_wrap' diff --git a/app/views/home/_hero.html.slim b/app/views/home/_hero.html.slim index c4c919757..4a1b857f9 100644 --- a/app/views/home/_hero.html.slim +++ b/app/views/home/_hero.html.slim @@ -7,22 +7,12 @@ p.govuk-body-l = t('home.hero') - - if Rails.application.gov_one_login? - = link_to course_overview_path, class: 'govuk-!-margin-bottom-4 govuk-link--no-visited-state govuk-!-font-weight-bold govuk-body' do - - if current_user - | Learn more - - else - | Learn more about this training - p.govuk-visually-hidden on the course - - else - = govuk_button_link_to course_overview_path, class: 'govuk-button--start govuk-!-margin-bottom-4' do - - if current_user - | Learn more - - else - | Learn more and enrol - p.govuk-visually-hidden on the course - - = render 'chevron' + = link_to course_overview_path, class: 'govuk-!-margin-bottom-4 govuk-link--no-visited-state govuk-!-font-weight-bold govuk-body' do + - if current_user + | Learn more + - else + | Learn more about this training + p.govuk-visually-hidden on the course .govuk-grid-column-one-third class='govuk-!-text-align-right' = m('home.thumb') diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index 3a811e45f..678d133e4 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -7,35 +7,15 @@ = render 'learning/cms_debug' = render 'debug' -- if Rails.application.gov_one_login? # ---------------------------------------- - - .govuk-grid-row - .govuk-grid-column-one-half - = m('home.about', headings_start_with: 'xl') - - = render 'prompt' - - - unless current_user - .govuk-grid-row class='govuk-!-margin-top-9' - .govuk-grid-column-full - = govuk_button_link_to new_user_session_path, class: 'govuk-button--start' do - = t('home.gov_one_button') - = render 'chevron' - -- else # ----------------------------------------------------------------------- - - .govuk-grid-row - .govuk-grid-column-one-half - = m('home.about', headings_start_with: 'xl') - - - unless current_user - .govuk-grid-column-one-half - .light-grey-box.enrol-box - = m('home.login', headings_start_with: 'xl') - - .govuk-button-group - = govuk_button_link_to 'Sign in', new_user_session_path - .white-space-pre-wrap= ' or ' - = govuk_link_to 'create an account', new_user_registration_path - - = render 'prompt' +.govuk-grid-row + .govuk-grid-column-one-half + = m('home.about', headings_start_with: 'xl') + += render 'prompt' + +- unless current_user + .govuk-grid-row class='govuk-!-margin-top-9' + .govuk-grid-column-full + = govuk_button_link_to new_user_session_path, class: 'govuk-button--start' do + = t('home.gov_one_button') + = render 'chevron' diff --git a/app/views/user/edit_email.html.slim b/app/views/user/edit_email.html.slim deleted file mode 100644 index cb395f1dd..000000000 --- a/app/views/user/edit_email.html.slim +++ /dev/null @@ -1,16 +0,0 @@ -- content_for :page_title do - = html_title t(:title, scope: :account_email) - -.govuk-grid-row - .govuk-grid-column-two-thirds-from-desktop - = form_for current_user, url: { action: 'update_email' } do |f| - = f.govuk_error_summary - = f.govuk_fieldset legend: { text: 'Change email address' } do - = f.govuk_email_field :email, autofocus: true, autocomplete: 'email', aria: { required: true } - - - if current_user.pending_reconfirmation? - p Currently waiting confirmation for: #{current_user.unconfirmed_email} - - .govuk-button-group - = f.govuk_submit t('links.save') - = govuk_link_to t('links.cancel'), user_path diff --git a/app/views/user/edit_password.html.slim b/app/views/user/edit_password.html.slim deleted file mode 100644 index bd429d409..000000000 --- a/app/views/user/edit_password.html.slim +++ /dev/null @@ -1,19 +0,0 @@ -- content_for :page_title do - = html_title t(:title, scope: :account_password) - -.govuk-grid-row - .govuk-grid-column-two-thirds-from-desktop - = form_for current_user, url: { action: 'update_password' } do |f| - = f.govuk_error_summary - = f.govuk_fieldset legend: { text: 'Change password' } do - - p.govuk-hint= password_complexity - - = f.govuk_password_field :current_password, autofocus: true, aria: { required: true } - = f.govuk_password_field :password, aria: { required: true } - = f.govuk_password_field :password_confirmation, aria: { required: true } - - .govuk-button-group - = f.govuk_submit t('links.save') - = govuk_link_to t('links.cancel'), user_path - diff --git a/app/views/user/show.html.slim b/app/views/user/show.html.slim index 66c2ba389..5229d042f 100644 --- a/app/views/user/show.html.slim +++ b/app/views/user/show.html.slim @@ -14,20 +14,9 @@ - row.with_key { 'Name' } - row.with_value(text: current_user.name, classes: %w[data-hj-suppress]) - row.with_action(text: 'Change name', href: edit_registration_name_path, html_attributes: { id: :edit_name_registration }) - - - unless Rails.application.gov_one_login? - - your_details.with_row do |row| - - row.with_key { 'Email' } - - row.with_value(text: current_user.email, classes: %w[data-hj-suppress]) - - row.with_action(text: 'Change email', href: edit_email_user_path, html_attributes: { id: :edit_email_user }) - - your_details.with_row do |row| - - row.with_key { 'Password' } - - row.with_value { t('my_account.password_changed', date: current_user.password_last_changed) } - - row.with_action(text: 'Change password', href: edit_password_user_path, html_attributes: { id: :edit_password_user }) - - if Rails.application.gov_one_login? - p.text-secondary - = t('my_account.name_information') + p.text-secondary + = t('my_account.name_information') = govuk_summary_list do |other_details| @@ -58,4 +47,4 @@ - row.with_action(text: 'Change email preferences', href: edit_training_emails_user_path, html_attributes: { id: :edit_training_emails_user }) = m('my_account.closing.information') - = govuk_button_link_to t('my_account.closing.button'), new_user_close_account_path + = govuk_button_link_to t('my_account.closing.button'), edit_reason_user_close_account_path diff --git a/config/application.rb b/config/application.rb index 1876cc817..52ddb1eba 100644 --- a/config/application.rb +++ b/config/application.rb @@ -95,23 +95,10 @@ def maintenance? Types::Params::Bool[ENV.fetch('MAINTENANCE', false)] end - # - # Feature flags - # - - # @return [Boolean] - def gov_one_login? - Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN', false)] - end - def migrated_answers? Types::Params::Bool[ENV.fetch('DISABLE_USER_ANSWER', false)] end - # - # Significant dates - # - # @return [ActiveSupport::TimeWithZone] def public_beta_launch_date Time.zone.local(2023, 2, 9, 15, 0, 0) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 6ff37f9ac..47183497e 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -169,10 +169,10 @@ def i18n_message(default = nil) # config.pepper = '4593c4ce86055f3b5b98e71644cc0ba3739484cff6a35c539fc21e522bf03ece501f01a65728854d1e7000853c3f0e2b57c3540c39ec8207c454f3d6d77ac4b6' # Send a notification to the original email when the user's email is changed. - config.send_email_changed_notification = true + # config.send_email_changed_notification = true # Send a notification email when the user's password is changed. - config.send_password_change_notification = true + # config.send_password_change_notification = true # ==> Configuration for :confirmable # A period that the user is allowed to access the website even without diff --git a/config/initializers/devise_security.rb b/config/initializers/devise_security.rb deleted file mode 100644 index 2895799dd..000000000 --- a/config/initializers/devise_security.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -Devise.setup do |config| - # ==> Security Extension - # Configure security extension for devise - - # Should the password expire (e.g 3.months) - # config.expire_password_after = false - - # Need 1 char each of: A-Z, a-z, 0-9, and a punctuation mark or symbol - # You may use "digits" in place of "digit" and "symbols" in place of - # "symbol" based on your preference - config.password_complexity = { digit: 2, lower: 2, symbol: 2, upper: 2 } - - # How many passwords to keep in archive - # config.password_archiving_count = 5 - - # Deny old passwords (true, false, number_of_old_passwords_to_check) - # Examples: - # config.deny_old_passwords = false # allow old passwords - # config.deny_old_passwords = true # will deny all the old passwords - # config.deny_old_passwords = 3 # will deny new passwords that matches with the last 3 passwords - # config.deny_old_passwords = true - - # enable email validation for :secure_validatable. (true, false, validation_options) - # dependency: see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation - config.email_validation = false - - # captcha integration for recover form - # config.captcha_for_recover = true - - # captcha integration for sign up form - # config.captcha_for_sign_up = true - - # captcha integration for sign in form - # config.captcha_for_sign_in = true - - # captcha integration for unlock form - # config.captcha_for_unlock = true - - # captcha integration for confirmation form - # config.captcha_for_confirmation = true - - # Time period for account expiry from last_activity_at - # config.expire_after = 90.days - - # Allow password to equal the email - # config.allow_passwords_equal_to_email = false - - # paranoid_verification will regenerate verification code after failed attempt - # config.paranoid_code_regenerate_after_attempt = 10 -end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 71b680100..a86d1e025 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -63,4 +63,3 @@ en: not_saved: one: "1 error prohibited this %{resource} from being saved:" other: "%{count} errors prohibited this %{resource} from being saved:" - pwned_password: "Password has previously appeared in a data breach and should never be used. Please choose a different password." diff --git a/config/locales/en.yml b/config/locales/en.yml index c72cb400e..b115c868c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -6,21 +6,10 @@ en: models: user: attributes: - current_password: - blank: Enter your current password. - invalid: Current password is invalid. first_name: blank: Enter a first name. last_name: blank: Enter a surname. - email: - blank: Enter an email address. - invalid: Enter a valid email address. - password: - blank: Enter a password. - too_short: Password must be at least 10 characters. # devise override - password_confirmation: - confirmation: Sorry, your passwords don't match. terms_and_conditions_agreed_at: blank: You must accept the terms and conditions and privacy policy to create an account. setting_type_id: @@ -67,17 +56,6 @@ en: email: blank: Enter an email address. invalid: Enter a valid email address. - password: - blank: Enter a password. - too_short: Password must be at least 10 characters. # devise override - password_complexity: - digit: Password must contain at least 2 digits - lower: Password must contain at least 2 lowercase characters - upper: Password must contain at least 2 uppercase characters - symbol: Password must contain at least 2 special characters or non-alphanumeric characters - - password_confirmation: - confirmation: Sorry, your passwords don't match. terms_and_conditions_agreed_at: blank: You must accept the terms and conditions and privacy policy to create an account. @@ -101,10 +79,6 @@ en: # Common Elements ------------------------------------------------------------ phase_banner: This is a new service, your %{link} will help us improve it. - - password_complexity: | - Passwords should be a minimum of %{length} characters long, contain at least two uppercase letters, two lowercase letters, two numbers, and two special characters or non-alphanumeric characters. - pagination: section: Section %{current} of %{total} page: Page %{current} of %{total} diff --git a/config/routes.rb b/config/routes.rb index 5284044e5..9031ffb0f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,15 +13,11 @@ devise_for :users, controllers: { sessions: 'users/sessions', - confirmations: 'confirmations', # unless Rails.application.gov_one_login? - passwords: 'passwords', # unless Rails.application.gov_one_login? - registrations: 'registrations', # unless Rails.application.gov_one_login? omniauth_callbacks: 'users/omniauth_callbacks', }, path_names: { sign_in: 'sign-in', sign_out: 'sign-out', - sign_up: 'sign-up', # unless Rails.application.gov_one_login? } # @see TimeoutWarning js component @@ -31,6 +27,7 @@ get 'extend_session', to: 'timeout#extend' get 'users/timeout', to: 'timeout#timeout_user' get '/users/sign_out', to: 'users/sessions#destroy' + get 'users/review', to: 'users/sessions#sign_in_test_user' unless Rails.application.live? end namespace :registration do @@ -57,7 +54,7 @@ get 'edit-training-emails' patch 'update-training-emails' - resource :close_account, only: %i[new update show], path: 'close' do + resource :close_account, only: %i[update show], path: 'close' do get 'reset-password' get 'edit-reason' patch 'update-reason' diff --git a/config/sitemap.rb b/config/sitemap.rb index 11daeee62..cb901416b 100644 --- a/config/sitemap.rb +++ b/config/sitemap.rb @@ -49,11 +49,6 @@ end # devise - # unless Rails.application.gov_one_login? - add new_user_unlock_path - add new_user_confirmation_path - add new_user_registration_path - # end add check_email_confirmation_user_path add check_email_password_reset_user_path add new_user_session_path @@ -65,7 +60,6 @@ add user_path # edit registration/account - add edit_email_user_path add edit_password_user_path add edit_registration_terms_and_conditions_path add edit_registration_name_path # unless Rails.application.gov_one_login? @@ -81,7 +75,6 @@ # close account add edit_reason_user_close_account_path add confirm_user_close_account_path - add new_user_close_account_path add user_close_account_path # learning diff --git a/spec/controllers/user_controller_spec.rb b/spec/controllers/user_controller_spec.rb deleted file mode 100644 index e5e08691a..000000000 --- a/spec/controllers/user_controller_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -require 'rails_helper' - -RSpec.describe UserController, type: :controller do - let(:user) { create :user, :registered } - let(:params) { {} } - - describe '#update_email' do - before do - sign_in user - post :update_email, params: { user: params } - end - - context 'when successful' do - let(:params) do - { - email: 'user@example.com', - } - end - - it 'redirects' do - expect(response).to have_http_status(:redirect) - end - - it 'renders a flash notice' do - expect(flash[:notice]).to match(/We have sent an email to your new email address with a link to click to confirm the change.\n\nIf you have not received the email after a few minutes, please check your spam folder.\n/) - end - end - - context 'when email is wrong' do - let(:params) do - { - email: 'invalidemail', - } - end - - it 'renders edit' do - expect(response).to have_http_status(:unprocessable_entity) - end - - it 'does not render a flash notice' do - expect(flash[:notice]).to be_nil - end - end - end - - describe '#update_password' do - before do - sign_in user - post :update_password, params: { user: params } - end - - context 'when successful' do - let(:params) do - { - password: 'NewPassword12!@', - confirm_password: 'NewPassword12!@', - current_password: 'Str0ngPa$$w0rd12', - } - end - - it 'redirects' do - expect(response).to have_http_status(:redirect) - end - - it 'renders a flash notice' do - expect(flash[:notice]).to match(/Your new password has been saved./) - end - end - - context 'when current password is wrong' do - let(:params) do - { - password: 'NewPassword12!@', - confirm_password: 'NewPassword12!@', - current_password: 'wrongpassword', - } - end - - it 'renders edit' do - expect(response).to have_http_status(:unprocessable_entity) - end - - it 'does not render a flash notice' do - expect(flash[:notice]).to be_nil - end - end - - context 'when new password is blank' do - let(:params) do - { - password: '', - confirm_password: '', - current_password: 'Str0ngPa$$w0rd12', - } - end - - it 'renders edit' do - expect(response).to have_http_status(:unprocessable_entity) - end - - it 'does not render a flash notice' do - expect(flash[:notice]).to be_nil - end - end - end -end diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index 45ffc2d5d..25e28613b 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -24,7 +24,6 @@ context 'with a new user' do before do - skip unless Rails.application.gov_one_login? get :openid_connect, params: params end @@ -41,7 +40,7 @@ context 'with an existing non-gov-one user' do before do - create :user, :registered, email: email + create :user, :registered, email: email, gov_one_id: nil get :openid_connect, params: params end diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index 4b46646b0..8440e998c 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -47,10 +47,6 @@ context 'when gov one login is enabled' do let(:user) { create :user, :confirmed } - before do - skip unless Rails.application.gov_one_login? - end - it 'redirects to terms and conditions page' do expect(response).to redirect_to edit_registration_terms_and_conditions_path end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 386dcd158..b2e813bbb 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -3,6 +3,7 @@ email { Faker::Internet.email } password { Rails.configuration.user_password } terms_and_conditions_agreed_at { Date.new(2000, 0o1, 0o1) } + gov_one_id { "urn:fdc:gov.uk:2022:23-#{Faker::Alphanumeric.alphanumeric(number: 10)}" } trait :confirmed do confirmed_at { 1.minute.ago } @@ -34,12 +35,6 @@ closed_reason_custom { 'I did not find the training useful' } end - factory :gov_one_user do - registered - gov_one_id { 'urn:fdc:gov.uk:2022:23-random-alpha-numeric' } - # password { nil } - end - # Personas ----------------------------------------------------------------- trait :agency_childminder do diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 547414bfd..e8037597f 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,14 +5,14 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 223 + expect(locales.count).to be 208 end it 'dot separated key -> Page::Resource#name' do - expect(locales.first[:name]).to eq 'activemodel.errors.models.user.attributes.current_password.blank' + expect(locales.first[:name]).to eq 'activemodel.errors.models.user.attributes.first_name.blank' end it 'value -> Page::Resource#body' do - expect(locales.first[:body]).to eq 'Enter your current password.' + expect(locales.first[:body]).to eq 'Enter a first name.' end end diff --git a/spec/mailers/notify_mailer_spec.rb b/spec/mailers/notify_mailer_spec.rb index baa5ed040..7204c0f1c 100644 --- a/spec/mailers/notify_mailer_spec.rb +++ b/spec/mailers/notify_mailer_spec.rb @@ -4,90 +4,6 @@ let(:user) { create(:user) } let(:mailbox) { User.new(email: 'child-development.training@education.gov.uk') } - describe 'email confirmation / account activation' do - context 'when signing up' do - it 'send activation email to correct user' do - response = user.send_confirmation_instructions - expect(response.recipients).to contain_exactly(user.email) - expect(response.subject).to eq 'Activation instructions' - end - end - - context 'when already signed up' do - it 'send confirmation email to correct user' do - user.registration_complete = true - response = user.send_confirmation_instructions - expect(response.recipients).to contain_exactly(user.email) - expect(response.subject).to eq 'Email confirmation instructions' - end - end - end - - describe 'reset password instructions' do - context 'when resetting password' do - it 'send instructions to correct user' do - mail = described_class.reset_password_instructions(user, :anything) - expect(mail.to).to contain_exactly(user.email) - expect(mail.subject).to eq 'Reset password instructions' - end - end - end - - describe 'password change' do - context 'when changing password' do - it 'send confirmation to correct user' do - response = user.send_password_change_notification - expect(response.recipients).to contain_exactly(user.email) - expect(response.subject).to eq 'Password change' - end - end - end - - describe 'email change' do - context 'when changing email' do - it 'send confirmation to correct user' do - response = user.send_email_changed_notification - expect(response.recipients).to contain_exactly(user.email) - expect(response.subject).to eq 'Email changed' - end - end - end - - describe 'unlock email' do - context 'when account is locked' do - it 'send unlock email to correct user' do - mail = described_class.unlock_instructions(user, :anything) - expect(mail.to).to contain_exactly(user.email) - expect(mail.subject).to eq 'Unlock instructions' - end - end - end - - describe 'email address taken' do - context 'when email is taken' do - it 'send email to user to tell them how to access their account' do - mail = described_class.email_taken(user) - expect(mail.to).to contain_exactly(user.email) - expect(mail.subject).to eq 'Email taken' - end - end - end - - describe 'account closed' do - context 'when account has been closed' do - it 'send email to user to confirm account has been closed' do - mail = described_class.account_closed(user) - expect(mail.to).to contain_exactly(user.email) - expect(mail.subject).to eq 'Account closed' - end - - it 'send email to internal mailbox' do - mail = described_class.account_closed_internal(mailbox, user) - expect(mail.to).to contain_exactly(mailbox.email) - end - end - end - describe 'users without email preferences' do context 'when user not completed registration and not opted out of emails' do it 'sends email to user to remind them to complete registration' do diff --git a/spec/models/data_analysis/user_overview_spec.rb b/spec/models/data_analysis/user_overview_spec.rb index 4283870f1..b3351d047 100644 --- a/spec/models/data_analysis/user_overview_spec.rb +++ b/spec/models/data_analysis/user_overview_spec.rb @@ -50,7 +50,7 @@ locked_out: 0, confirmed: 6, unconfirmed: 0, - gov_one: 1, + gov_one: 6, user_defined_roles: 2, started_learning: 3, not_started_learning: 3, @@ -103,7 +103,7 @@ # user#5 start training notification create :user, :registered, confirmed_at: 4.weeks.ago # user#6 - create :gov_one_user + create :user, :registered end it_behaves_like 'a data export model' diff --git a/spec/models/training/response_spec.rb b/spec/models/training/response_spec.rb index 958174f3a..35b75bc8e 100644 --- a/spec/models/training/response_spec.rb +++ b/spec/models/training/response_spec.rb @@ -6,6 +6,7 @@ training_module: 'alpha', question_name: question_name, answers: answers, + question_type: 'formative', ) end @@ -27,6 +28,7 @@ let(:answers) { [1] } it '#correct?' do + expect(response.errors.full_messages).to be_empty expect(response).to be_correct end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 589ca7815..1ec68e438 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -174,10 +174,10 @@ it 'exports formatted attributes as CSV' do expect(described_class.to_csv(batch_size: 2)).to eq <<~CSV id,local_authority,setting_type,setting_type_other,role_type,role_type_other,registration_complete,private_beta_registration_complete,registration_complete_any?,registered_at,terms_and_conditions_agreed_at,gov_one?,module_1_time,module_2_time,module_3_time - 1,Watford Borough Council,,DfE,other,Developer,true,true,true,,2000-01-01 00:00:00,false,4,2,0 - 2,Leeds City Council,,DfE,Trainer or lecturer,,true,false,true,,2000-01-01 00:00:00,false,1,0, - 3,City of London,,DfE,other,Developer,true,false,true,2023-01-12 10:15:59,2000-01-01 00:00:00,false,3,, - 4,,,,,,false,false,false,,2000-01-01 00:00:00,false,,, + 1,Watford Borough Council,,DfE,other,Developer,true,true,true,,2000-01-01 00:00:00,true,4,2,0 + 2,Leeds City Council,,DfE,Trainer or lecturer,,true,false,true,,2000-01-01 00:00:00,true,1,0, + 3,City of London,,DfE,other,Developer,true,false,true,2023-01-12 10:15:59,2000-01-01 00:00:00,true,3,, + 4,,,,,,false,false,false,,2000-01-01 00:00:00,true,,, CSV end end @@ -250,7 +250,6 @@ context 'without an existing account' do before do - skip unless Rails.application.gov_one_login? described_class.find_or_create_from_gov_one(**params) end @@ -272,7 +271,7 @@ context 'and using GovOne for the first time' do let(:user) do - create :user, :registered, email: email + create :user, :registered, email: email, gov_one_id: nil end let(:params) do @@ -309,4 +308,18 @@ expect(described_class.random_password.scan(/[^A-Za-z0-9]/).count).to be >= 2 end end + + describe '.test_user' do + let!(:user) { create :user, email: 'completed@example.com' } + + it 'returns the completed seeded user' do + expect(described_class.test_user).to eq user + end + end + + describe '.test_user?' do + let!(:user) { create :user, email: 'completed@example.com' } + + specify { expect(user.test_user?).to eq true } + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 4b379e5e5..71faef9dd 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -71,5 +71,5 @@ config.include ShowMeTheCookies, type: :system # enable OIDC session - config.include Devise::Test::IntegrationHelpers, type: :system if Rails.application.gov_one_login? + config.include Devise::Test::IntegrationHelpers, type: :system end diff --git a/spec/requests/authentication_spec.rb b/spec/requests/authentication_spec.rb index 8d44687c1..c333bdef1 100644 --- a/spec/requests/authentication_spec.rb +++ b/spec/requests/authentication_spec.rb @@ -21,17 +21,6 @@ get edit_registration_terms_and_conditions_path end - context 'when unconfirmed' do - let(:user) { create :user } - - it { expect(response).to redirect_to new_user_session_path } - - it 'email address must be confirmed' do - follow_redirect! - expect(response.body).to include 'You have to confirm your email address' - end - end - context 'when partially registered' do let(:user) { create :user, :confirmed } @@ -66,26 +55,11 @@ get my_modules_path end - context 'when unconfirmed' do - let(:user) { create :user } - - it { expect(response).to redirect_to new_user_session_path } - - it 'email address must be confirmed' do - follow_redirect! - expect(response.body).to include 'You have to confirm your email address' - end - end - context 'when partially registered' do let(:user) { create :user, :confirmed } it do - if Rails.application.gov_one_login? - expect(response).to redirect_to edit_registration_terms_and_conditions_path - else - expect(response).to redirect_to edit_registration_name_path - end + expect(response).to redirect_to edit_registration_terms_and_conditions_path end it 'registration must be completed' do diff --git a/spec/requests/user_spec.rb b/spec/requests/user_spec.rb index 91cef0b3d..82cee9ed9 100644 --- a/spec/requests/user_spec.rb +++ b/spec/requests/user_spec.rb @@ -36,18 +36,4 @@ end end end - - describe 'Unconfirmed user, not signed in' do - let(:unconfirmed_user) { create :user } - - describe 'GET /user/check-email-confirmation?ref=' do - it 'renders check email page' do - get check_email_confirmation_user_path(ref: unconfirmed_user.confirmation_token) - - expect(response).to have_http_status(:success) - expect(response.body).to include('Check your email') - expect(response.body).to include(unconfirmed_user.email) - end - end - end end diff --git a/spec/support/shared/with_user.rb b/spec/support/shared/with_user.rb index de931312f..1fcfea440 100644 --- a/spec/support/shared/with_user.rb +++ b/spec/support/shared/with_user.rb @@ -1,20 +1,8 @@ RSpec.shared_context 'with user' do - if Rails.application.gov_one_login? - let(:user) { create(:gov_one_user) } + let(:user) { create(:user, :registered) } - before do - sign_in user - visit '/users/sign-in' - end - - else - let(:user) { create(:user, :registered) } - - before do - visit '/users/sign-in' - fill_in 'Email address', with: user.email - fill_in 'Password', with: user.password - click_button 'Sign in' - end + before do + sign_in user + visit '/users/sign-in' end end diff --git a/spec/system/account_page_spec.rb b/spec/system/account_page_spec.rb index c0741be2d..644f945c6 100644 --- a/spec/system/account_page_spec.rb +++ b/spec/system/account_page_spec.rb @@ -11,13 +11,9 @@ expect(page).to have_text 'Manage your account' expect(page).to have_link 'Change name' - if Rails.application.gov_one_login? - expect(page).not_to have_link 'Change password' - expect(page).to have_content 'This is the name that will appear on your end of module certificate' - expect(page).to have_content 'Changing your name on this account will not affect your GOV.UK One Login' - else - expect(page).to have_link 'Change password' - end + expect(page).not_to have_link 'Change password' + expect(page).to have_content 'This is the name that will appear on your end of module certificate' + expect(page).to have_content 'Changing your name on this account will not affect your GOV.UK One Login' expect(page).to have_link 'Change setting details' expect(page).to have_link 'Change email preferences' diff --git a/spec/system/confirmed_user/completing_registration_spec.rb b/spec/system/confirmed_user/completing_registration_spec.rb index 62fb7cd1b..7e4ba95e6 100644 --- a/spec/system/confirmed_user/completing_registration_spec.rb +++ b/spec/system/confirmed_user/completing_registration_spec.rb @@ -6,17 +6,15 @@ let(:user) { create :user, :confirmed } it 'complete registration' do - if Rails.application.gov_one_login? - expect(page).to have_text('Terms and conditions') - click_button 'Continue' - expect(page).to have_text('There is a problem') - .and have_text('You must accept the terms and conditions and privacy policy to create an account.') + expect(page).to have_text('Terms and conditions') + click_button 'Continue' + expect(page).to have_text('There is a problem') + .and have_text('You must accept the terms and conditions and privacy policy to create an account.') - expect(page).to have_text('Agree to our terms and conditions') + expect(page).to have_text('Agree to our terms and conditions') - check 'I confirm that I accept the terms and conditions and privacy policy.' - click_button 'Continue' - end + check 'I confirm that I accept the terms and conditions and privacy policy.' + click_button 'Continue' expect(page).to have_text('About you') click_button 'Continue' diff --git a/spec/system/forgotten_password_spec.rb b/spec/system/forgotten_password_spec.rb deleted file mode 100644 index eca71c72c..000000000 --- a/spec/system/forgotten_password_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'User following forgotten password process' do - let(:user) { create :user, :confirmed } - let(:token) { user.send_reset_password_instructions } - let(:password) { 'ABCDE123xyh!@' } - - before do - skip if Rails.application.gov_one_login? - end - - context 'when choosing a new password' do - before do - visit edit_user_password_path + "?reset_password_token=#{token}" - end - - # Happy path scenario - context 'and new password meets criteria' do - let(:password) { 'NewPassword12!@' } - - it 'flash message displays correctly' do - fill_in 'New password', with: password - fill_in 'Confirm password', with: password - click_on 'Reset password' - - expect(page).to have_current_path(new_user_session_path) - .and have_text('Success') - .and have_text('Your new password has been saved.') - end - end - - # Unhappy path scenarios - context 'and password is less than 10 characters' do - let(:password) { 'Password' } - - it 'displays error message' do - fill_in 'New password', with: password - fill_in 'Confirm password', with: password - click_on 'Reset password' - - expect(page).to have_text('Password must be at least 10 characters.') - end - end - - context "and password and confirm password don't match" do - let(:password) { 'NewPassword12!@' } - let(:confirm_password) { 'NewPassword45!@' } - - it 'displays error message' do - fill_in 'New password', with: password - fill_in 'Confirm password', with: confirm_password - click_on 'Reset password' - - expect(page).to have_text("Sorry, your passwords don't match.") - end - end - end - - context 'when entering valid email address' do - it 'shows "Check email" page' do - visit new_user_session_path - click_link 'I have forgotten my password', visible: false - - expect(page).to have_text('I have forgotten my password') - - fill_in 'Email', with: user.email - click_button 'Send email' - - expect(page).to have_text('Check your email') - end - end - - # context 'when navigating to reset password page' do - # before do - # visit check_email_password_reset_user_path - # end - - # it 'provides link to resend the email' do - # click_link 'Send me another email', visible: false - - # expect(page).to have_current_path(new_user_password_path) - # end - - # it 'provides link text contact us' do - # expect(page).to have_text('contact us') - # end - # end - - context 'when navigating to the "Check email" page' do - it 'provides back button to sign in page' do - visit check_email_password_reset_user_path - click_link 'Back' - - expect(page).to have_current_path(new_user_session_path) - end - end -end diff --git a/spec/system/front_page_spec.rb b/spec/system/front_page_spec.rb index 526075e18..95a6886a5 100644 --- a/spec/system/front_page_spec.rb +++ b/spec/system/front_page_spec.rb @@ -9,21 +9,12 @@ expect(page).to have_text 'Learn more' - if Rails.application.gov_one_login? - expect(page).not_to have_text 'Learn more about this training' - expect(page).not_to have_text 'Start your training now' - - # banner - expect(page).not_to have_text 'Access to this website is changing' - expect(page).not_to have_link href: 'https://www.gov.uk/using-your-gov-uk-one-login' - else - expect(page).not_to have_text 'Learn more and enrol' - expect(page).not_to have_text 'Sign in to continue learning' - - # banner - expect(page).to have_text 'Access to this website is changing' - expect(page).to have_link href: 'https://www.gov.uk/using-your-gov-uk-one-login' - end + expect(page).not_to have_text 'Learn more about this training' + expect(page).not_to have_text 'Start your training now' + + # banner + expect(page).not_to have_text 'Access to this website is changing' + expect(page).not_to have_link href: 'https://www.gov.uk/using-your-gov-uk-one-login' end end @@ -31,21 +22,12 @@ it 'log in content' do visit '/' - if Rails.application.gov_one_login? - expect(page).to have_text 'Learn more about this training' - expect(page).to have_text 'Start your training now' - - # banner - expect(page).not_to have_text 'Access to this website is changing' - expect(page).not_to have_link href: 'https://www.gov.uk/using-your-gov-uk-one-login' - else - expect(page).to have_text 'Learn more and enrol' - expect(page).to have_text 'Sign in to continue learning' - - # banner - expect(page).to have_text 'Access to this website is changing' - expect(page).to have_link href: 'https://www.gov.uk/using-your-gov-uk-one-login' - end + expect(page).to have_text 'Learn more about this training' + expect(page).to have_text 'Start your training now' + + # banner + expect(page).not_to have_text 'Access to this website is changing' + expect(page).not_to have_link href: 'https://www.gov.uk/using-your-gov-uk-one-login' end end end diff --git a/spec/system/gov_one_spec.rb b/spec/system/gov_one_spec.rb index 79c8fb74e..8716cead6 100644 --- a/spec/system/gov_one_spec.rb +++ b/spec/system/gov_one_spec.rb @@ -1,10 +1,6 @@ require 'rails_helper' RSpec.describe 'Gov One' do - before do - skip unless Rails.application.gov_one_login? - end - context 'with an unauthenticated visitor' do before do visit '/users/sign-in' diff --git a/spec/system/locked_account_spec.rb b/spec/system/locked_account_spec.rb deleted file mode 100644 index e8372fd12..000000000 --- a/spec/system/locked_account_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'User has locked account' do - let(:user) { create :user, :confirmed } - let!(:token) { user.lock_access! } - - context 'when the link in the email is clicked' do - it 'then it unlocks the account and shows the sign in page' do - visit user_unlock_path + "?unlock_token=#{token}" - - expect(page).to have_current_path(new_user_session_path) - .and have_content('Your account has been unlocked successfully. Please sign in to continue') - end - end - - context 'when the link in the email is clicked a second time' do - it 'then an error message is displayed' do - visit user_unlock_path + "?unlock_token=#{token}" - visit user_unlock_path + "?unlock_token=#{token}" - - expect(page).to have_content('The link you followed has expired') - end - end - - context 'when a user waits for their account to be unlocked' do - context 'and then locks their account again by entering the wrong password' do - context 'and then clicks on the old unlock link in their inbox' do - it 'then an error message is displayed' do - user.unlock_access! - - user.lock_access! - - visit user_unlock_path + "?unlock_token=#{token}" - - expect(page).to have_content('The link you followed has expired') - end - end - end - end -end diff --git a/spec/system/page_title_spec.rb b/spec/system/page_title_spec.rb index 3043c453e..f2bcd2285 100644 --- a/spec/system/page_title_spec.rb +++ b/spec/system/page_title_spec.rb @@ -9,9 +9,6 @@ it { expect(about_path('alpha')).to have_page_title 'First Training Module' } it { expect(new_user_session_path).to have_page_title 'Sign in' } - it { expect(new_user_password_path).to have_page_title 'Reset password' } - it { expect(cancel_user_registration_path).to have_page_title 'Create an Early years child development training account' } - it { expect(new_user_registration_path).to have_page_title 'Create an Early years child development training account' } it { expect(setting_path('cookie-policy')).to have_page_title 'Cookie policy' } @@ -23,37 +20,6 @@ it { expect(static_path('sitemap')).to have_page_title 'Sitemap' } it { expect(static_path('terms-and-conditions')).to have_page_title 'Terms and conditions' } it { expect(static_path('wifi-and-data')).to have_page_title 'Free internet, wifi and data resources' } - - context 'and is confirmed' do - let(:user) { create(:user, :confirmed) } - - it 'requesting password reset' do - token = user.send_reset_password_instructions - expect(edit_user_password_path(reset_password_token: token)).to have_page_title 'Choose a new password' - end - end - - context 'and is unconfirmed' do - let(:user) { create(:user) } - - it 'is a valid confirmation token' do - expect(user_confirmation_path(confirmation_token: user.confirmation_token)).to have_page_title 'Sign in' - expect(page).to have_current_path new_user_session_path - end - - it { expect(new_user_unlock_path).to have_page_title 'Resend unlock instructions' } - - it 'is a valid unlock token' do - token = user.lock_access! - expect(user_unlock_path(unlock_token: token)).to have_page_title 'Sign in' - expect(page).to have_current_path new_user_session_path - end - - it 'is an expired/invalid unlock token' do - user.lock_access! - expect(user_unlock_path(unlock_token: 'invalid_token')).to have_page_title 'Resend unlock instructions' - end - end end context 'when user is authenticated' do @@ -71,21 +37,14 @@ it { expect(users_timeout_path).to have_page_title('User timeout') } it { expect(setting_path('cookie-policy')).to have_page_title('Cookie policy') } - it { expect(edit_user_registration_path).to have_page_title('Change your password') } - it { expect(new_user_confirmation_path).to have_page_title('Resend your confirmation') } - + it { expect(edit_registration_terms_and_conditions_path).to have_page_title('Terms and Conditions') } it { expect(edit_registration_name_path).to have_page_title 'About you' } it { expect(edit_registration_setting_type_path).to have_page_title('What setting type do you work in?') } it { expect(edit_registration_setting_type_other_path).to have_page_title('Where do you work?') } it { expect(edit_registration_local_authority_path).to have_page_title('What local authority area do you work in?') } it { expect(edit_registration_role_type_path).to have_page_title('Which of the following best describes your role?') } it { expect(edit_registration_role_type_other_path).to have_page_title('What is your role?') } - - it { expect(edit_email_user_path).to have_page_title('Change email address') } - it { expect(edit_password_user_path).to have_page_title('Change your password') } - - it { expect(check_email_confirmation_user_path).to have_page_title('Check email confirmation') } - it { expect(check_email_password_reset_user_path).to have_page_title('Check email password reset') } + it { expect(edit_registration_early_years_emails_path).to have_page_title('Do you want to get early years email updates from the Department for Education?') } it { expect(static_path('whats-new')).to have_page_title "What's new in the training" } diff --git a/spec/system/registered_user/changing_password_spec.rb b/spec/system/registered_user/changing_password_spec.rb deleted file mode 100644 index 3e0c48d63..000000000 --- a/spec/system/registered_user/changing_password_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Registered user changing password', type: :system do - subject(:user) { create :user, :registered, created_at: 1.month.ago } - - let(:password) { 'Str0ngPa$$w0rd13' } - - include_context 'with user' - - before do - visit '/my-account/edit-password' - fill_in 'Enter your current password', with: 'Str0ngPa$$w0rd12' - fill_in 'Create a new password', with: password - fill_in 'Confirm password', with: password - end - - context 'when cancelled' do - it 'returns to account page' do - click_link 'Cancel' - expect(page).to have_current_path '/my-account' - expect(page).not_to have_text 'Your new password has been saved.' - end - end - - context 'when successful' do - let(:password) { '12!@NewPassword' } - let(:today) { Time.zone.today.to_formatted_s(:rfc822) } # 18 May 2022 - - it 'updates password' do - click_button 'Save' - expect(page).to have_current_path '/my-account' - expect(page).to have_text('Manage your account') # page heading - .and have_text('Your new password has been saved.') - end - end - - context 'when too short' do - let(:password) { 'short' } - - it 'renders an error message' do - click_button 'Save' - expect(page).to have_text 'Password must be at least 10 characters.' - end - end - - context 'when blank' do - let(:password) { '' } - - it 'renders an error message' do - click_button 'Save' - expect(page).to have_text 'Enter a password.' - end - end -end diff --git a/spec/system/registered_user/closing_account_spec.rb b/spec/system/registered_user/closing_account_spec.rb index 44e18d2e1..1b90c228d 100644 --- a/spec/system/registered_user/closing_account_spec.rb +++ b/spec/system/registered_user/closing_account_spec.rb @@ -6,32 +6,7 @@ context 'when on my account page' do it 'has button to close account' do visit '/my-account' - expect(page).to have_link 'Request to close account', href: '/my-account/close/new' - end - end - - context 'when on password confirmation page' do - before do - visit '/my-account/close/new' - fill_in 'For security, enter your password', with: password - click_on 'Continue' - end - - context 'and correct password is entered' do - let(:password) { user.password } - - it 'can progress to next page' do - expect(page).to have_current_path '/my-account/close/edit-reason' - end - end - - context 'and incorrect password is entered' do - let(:password) { 'IncorrectPassword' } - - it 'cannot progress to next page' do - expect(page).to have_content('There is a problem') - .and have_content('Enter a valid password') - end + expect(page).to have_link 'Request to close account', href: '/my-account/close/edit-reason' end end diff --git a/spec/system/registration_journey_spec.rb b/spec/system/registration_journey_spec.rb deleted file mode 100644 index 7e40eae00..000000000 --- a/spec/system/registration_journey_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Following registration journey' do - let(:user) { create :user } - - context 'when on the check your email page' do - before do - visit "/my-account/check-email-confirmation?ref=#{user.confirmation_token}" - end - - context 'and the user is already confirmed' do - let(:user) { create :user, :confirmed } - - it 'does not show the email address' do - expect(page).not_to have_text 'We sent the email to' - expect(page).not_to have_text user.email - end - end - - context 'and can click on "I haven\'t received the email" link' do - it 'taken to "resend your confirmation" page' do - click_on 'Send me another email', visible: :hidden - - expect(page).to have_current_path(new_user_confirmation_path, ignore_query: true) - expect(page).to have_text('Resend your confirmation') - end - end - - context 'and can click on the "I don\'t have access to the email" link' do - it 'I am taken to the ‘create your account’ page to re-register' do - click_on 'create a new account', visible: :hidden - - expect(page).to have_current_path(new_user_registration_path, ignore_query: true) - expect(page).to have_text('Create an Early years child development training account') - end - end - - context 'and can click on the ‘other problems’ link' do - it 'taken to the ‘MS form to contact us’' do - expect(page).to have_text('contact us') - end - end - - context 'and navigate to "resend your confirmation"' do - it 'can click on a back button to be taken to the "check your email" page' do - click_on 'Send me another email', visible: :hidden - click_on 'Go back to check your email' - expect(page).to have_text('Check your email') - end - end - end - - context 'when on the ‘resend your confirmation’ page' do - before do - visit new_user_confirmation_path - end - - context 'and can enter email address and click send' do - it 'taken to the confirmation email sent page' do - fill_in 'Email address', with: user.email - click_on 'Send email' - - expect(page).to have_text('Check your email') - end - end - end -end diff --git a/spec/system/sign_in_spec.rb b/spec/system/sign_in_spec.rb deleted file mode 100644 index 3e0039c74..000000000 --- a/spec/system/sign_in_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Sign in' do - let(:email_address) { user.email } - let(:password) { Rails.configuration.user_password } - - before do - skip if Rails.application.gov_one_login? - - visit '/users/sign-in' - fill_in 'Email address', with: email_address - fill_in 'Password', with: password - click_button 'Sign in' - end - - context 'when user is registered' do - let(:user) { create :user, :registered } - - context 'and enters valid credentials' do - it 'signs in successfully' do - expect(page).to have_text('My modules') - expect(page).not_to have_text('Signed in successfully') - end - end - - context 'and enters incorrect email' do - let(:email_address) { 'user@incorrect.com' } - - it 'displays a warning' do - expect(page).to have_text('Warning') - .and have_text('Enter a valid email address and password. Your account will be locked after 5 unsuccessful attempts. We will email you instructions to unlock your account.') - end - end - - context 'and enters incorrect password' do - let(:password) { 'IncorrectPassword' } - - it 'displays a warning' do - expect(page).to have_text('Warning') - .and have_text('Enter a valid email address and password. Your account will be locked after 5 unsuccessful attempts. We will email you instructions to unlock your account.') - end - end - end - - context 'when user is confirmed' do - let(:user) { create :user, :confirmed } - - context 'and enters valid credentials' do - it 'signs in successfully' do - expect(page).to have_text 'About you' # extra registration - end - end - - context 'and makes 5 incorrect password attempts' do - let(:password) { 'IncorrectPassword' } - - 4.times do # 4 additional times, 5 in total - before do - fill_in 'Email address', with: email_address - fill_in 'Password', with: password - click_button 'Sign in' - end - end - - it 'locks account' do - user.reload - expect(user.failed_attempts).to eq 5 - expect(user.access_locked?).to be true - expect(page).to have_text('Warning') - .and have_text('Enter a valid email address and password. Your account will be locked after 5 unsuccessful attempts. We will email you instructions to unlock your account.') - end - end - end -end diff --git a/spec/system/sign_up_spec.rb b/spec/system/sign_up_spec.rb deleted file mode 100644 index 40c0aa75d..000000000 --- a/spec/system/sign_up_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Sign up' do - before do - skip if Rails.application.gov_one_login? - visit '/users/sign-up' - end - - context 'when user does not exist' do - let(:user) { create(:user) } - - it 'must accept terms and conditions' do - fill_in 'Email address', with: user.email - fill_in 'Create password', with: user.password - fill_in 'Confirm password', with: user.password - click_button 'Continue' - - expect(page).to have_text('There is a problem') - .and have_text('You must accept the terms and conditions and privacy policy to create an account.') - end - - context 'when entering email address' do - before do - fill_in 'Email address', with: email - fill_in 'Create password', with: user.password - fill_in 'Confirm password', with: user.password - check 'I confirm that I accept the terms and conditions and privacy policy.' - click_on 'Continue' - end - - describe 'with one dot in domain' do - let(:email) { 'hello@example.com' } - - it 'is valid' do - expect(page).to have_content 'Check your email' - end - end - - describe 'with two dots in domain' do - let(:email) { 'hello@example.co.uk' } - - it 'is valid' do - expect(page).to have_content 'Check your email' - end - end - - describe 'with comma in domain' do - let(:email) { 'hello@example,com' } - - it 'is invalid' do - expect(page).to have_content 'There is a problem' - end - end - end - end - - context 'when user already exists' do - let(:user) { create(:user, :registered) } - - # re-registration / enumeration exploit - it 'does not expose email accounts' do - fill_in 'Email address', with: user.email - fill_in 'Create password', with: user.password - fill_in 'Confirm password', with: user.password - check 'I confirm that I accept the terms and conditions and privacy policy.' - click_button 'Continue' - - expect(page).to have_text('We sent the email to').and have_text(user.email) - - expect(page).not_to have_text('There is a problem') - expect(page).not_to have_text('has already been taken') - end - end -end diff --git a/spec/system/whats_new_page_spec.rb b/spec/system/whats_new_page_spec.rb index 4c9ecdc1d..79c675b85 100644 --- a/spec/system/whats_new_page_spec.rb +++ b/spec/system/whats_new_page_spec.rb @@ -22,16 +22,8 @@ describe 'with subsequent logins' do before do click_on 'sign-out-desktop' - - if Rails.application.gov_one_login? - sign_in user - visit '/users/sign-in' - else - visit '/users/sign-in' - fill_in 'Email address', with: user.email - fill_in 'Password', with: user.password - click_button 'Sign in' - end + sign_in user + visit '/users/sign-in' end it 'does not appear' do From 2cc61a8103423cdf1b8589c9b68717e6c2109a42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:54:45 +0000 Subject: [PATCH 48/95] Bump rack from 3.0.9 to 3.0.9.1 (#1105) Bumps [rack](https://github.com/rack/rack) from 3.0.9 to 3.0.9.1. - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](https://github.com/rack/rack/compare/v3.0.9...v3.0.9.1) --- updated-dependencies: - dependency-name: rack dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 27eb35b28..73c1ed5ff 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -399,7 +399,7 @@ GEM que (>= 0.14, < 3.0.0) raabro (1.4.0) racc (1.7.3) - rack (3.0.9) + rack (3.0.9.1) rack-oauth2 (2.2.1) activesupport attr_required From 56323a6354e4fbe3e059cf27a6dbc4562abe0ba4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:10:56 +0000 Subject: [PATCH 49/95] Bump json-jwt from 1.16.5 to 1.16.6 (#1109) Bumps [json-jwt](https://github.com/nov/json-jwt) from 1.16.5 to 1.16.6. - [Release notes](https://github.com/nov/json-jwt/releases) - [Changelog](https://github.com/nov/json-jwt/blob/main/CHANGELOG.md) - [Commits](https://github.com/nov/json-jwt/compare/v1.16.5...v1.16.6) --- updated-dependencies: - dependency-name: json-jwt dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 73c1ed5ff..b83cdfc56 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,7 +92,7 @@ GEM rack (>= 0.9.0) rouge (>= 1.0.0) bigdecimal (3.1.6) - bindata (2.4.15) + bindata (2.5.0) bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) @@ -160,8 +160,7 @@ GEM dotenv-rails (3.0.2) dotenv (= 3.0.2) railties (>= 6.1) - drb (2.2.0) - ruby2_keywords + drb (2.2.1) dry-core (1.0.1) concurrent-ruby (~> 1.0) zeitwerk (~> 2.6) @@ -292,7 +291,7 @@ GEM jsbundling-rails (1.3.0) railties (>= 6.0.0) json (2.7.1) - json-jwt (1.16.5) + json-jwt (1.16.6) activesupport (>= 4.2) aes_key_wrap base64 @@ -527,7 +526,6 @@ GEM rexml ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) - ruby2_keywords (0.0.5) rubyzip (2.3.2) safely_block (0.4.0) selenium-webdriver (4.17.0) From 7fba373e5ec38c1ff06ad1ccb80f40fdda694d1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 10:07:31 +0000 Subject: [PATCH 50/95] Bump rdoc from 6.6.2 to 6.6.3.1 (#1123) Bumps [rdoc](https://github.com/ruby/rdoc) from 6.6.2 to 6.6.3.1. - [Release notes](https://github.com/ruby/rdoc/releases) - [Changelog](https://github.com/ruby/rdoc/blob/master/History.rdoc) - [Commits](https://github.com/ruby/rdoc/compare/v6.6.2...v6.6.3.1) --- updated-dependencies: - dependency-name: rdoc dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index b83cdfc56..faee74dd7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -452,7 +452,7 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.1.0) - rdoc (6.6.2) + rdoc (6.6.3.1) psych (>= 4.0.0) redcarpet (3.6.0) regexp_parser (2.9.0) From 4883b12b6c55fb0ec215e8fab8d6a5dea39d4601 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Wed, 3 Apr 2024 14:53:13 +0100 Subject: [PATCH 51/95] update test schema and prev page decorator spec --- lib/content_test_schema.rb | 2 +- .../previous_page_decorator_spec.rb | 17 ++----- .../ast/alpha-fail-feedback-form-response.yml | 46 ++----------------- 3 files changed, 9 insertions(+), 56 deletions(-) diff --git a/lib/content_test_schema.rb b/lib/content_test_schema.rb index 3a59ac66e..f66f4cc7b 100644 --- a/lib/content_test_schema.rb +++ b/lib/content_test_schema.rb @@ -149,6 +149,6 @@ def next_schema # @return [Boolean] def skip? - !pass && type.match?(/results|confidence|thank|certificate/) + !pass && (type.match?(/confidence|thank|certificate/) || slug.match?(/feedback/)) end end diff --git a/spec/decorators/previous_page_decorator_spec.rb b/spec/decorators/previous_page_decorator_spec.rb index 783917312..e18497b71 100644 --- a/spec/decorators/previous_page_decorator_spec.rb +++ b/spec/decorators/previous_page_decorator_spec.rb @@ -64,25 +64,18 @@ # let(:content) { mod.page_by_name('1-3-3-5') } - context 'and unanswered' do - it 'is one step back' do - expect(decorator.name).to eq 'feedback-checkbox-otherandtext' - end - end - context 'and answered' do before do create :response, - question_name: 'feedback-checkbox-otherandtext', - training_module: 'alpha', + question_name: 'feedback-oneoffquestion', + training_module: mod.name, answers: [1], correct: true, - user: create(:user) + user: user, + question_type: 'feedback' end - it 'is two steps back' do - expect(decorator.name).to eq 'feedback-freetext' - end + specify { expect(decorator.name).to eq 'feedback-checkbox-otherandtext' } end end end diff --git a/spec/support/ast/alpha-fail-feedback-form-response.yml b/spec/support/ast/alpha-fail-feedback-form-response.yml index 23bdee867..65004507b 100644 --- a/spec/support/ast/alpha-fail-feedback-form-response.yml +++ b/spec/support/ast/alpha-fail-feedback-form-response.yml @@ -175,48 +175,8 @@ - response-answers-1-field - - :click_on - Finish test -- :path: /modules/alpha/content-pages/feedback-intro - :text: Additional feedback +- :path: /modules/alpha/assessment-result/1-3-2-11 + :text: Assessment results :inputs: - - :click_on - - Next -- :path: /modules/alpha/content-pages/feedback-radiobutton - :text: 'Feedback question 1 - Select from following' - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/feedback-yesnoandtext - :text: Feedback question 2 - Select from following - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/feedback-freetext - :text: Feedback question 3 - Complete the following - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/feedback-radio-otherandtext - :text: Feedback question 4 - Select from following - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/feedback-radio-and-freetext - :text: Feedback question 5 - Select from following - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/feedback-checkbox-othertextandor - :text: Feedback question 6 - Select from following - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/feedback-checkbox-otherandtext - :text: Feedback question 7 - Select from following - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/feedback-oneoffquestion - :text: Feedback question 8 - Select from following - :inputs: - - - :click_on - - Next + - Retake test \ No newline at end of file From 5c2bf8699baee47dd4e4ae7172f6946320c08616 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Wed, 3 Apr 2024 17:25:25 +0100 Subject: [PATCH 52/95] update opinion spec, add tests for skippable questions pagination --- app/decorators/pagination_decorator.rb | 1 + spec/system/opinion_spec.rb | 33 +++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/decorators/pagination_decorator.rb b/app/decorators/pagination_decorator.rb index 20154b973..af47be52b 100644 --- a/app/decorators/pagination_decorator.rb +++ b/app/decorators/pagination_decorator.rb @@ -38,6 +38,7 @@ def current_page content.section_content.index(content) + 1 end + # TODO: - update for skipped questions # @return [Integer] def page_total content.section_content.size diff --git a/spec/system/opinion_spec.rb b/spec/system/opinion_spec.rb index aa2123f42..9efe58bdb 100644 --- a/spec/system/opinion_spec.rb +++ b/spec/system/opinion_spec.rb @@ -6,18 +6,19 @@ let(:first_question_path) { '/modules/alpha/questionnaires/feedback-radiobutton' } let(:second_question_path) { '/modules/alpha/questionnaires/feedback-yesnoandtext' } + let(:intro_path) { '/modules/alpha/content-pages/feedback-intro' } it 'shows feedback question' do - visit '/modules/alpha/content-pages/feedback-intro' + visit intro_path expect(page).to have_content('Additional feedback') click_on 'Give feedback' - expect(page).to have_content('Regarding the training module') + expect(page).to have_content('Feedback question 1 - Select from following') expect(page).to have_content('Strongly agree') end it do visit second_question_path - expect(page).to have_content('Did the module meet your expectations') + expect(page).to have_content('Feedback question 2 - Select from following') expect(page).to have_content('Yes') end @@ -35,4 +36,30 @@ expect(page).to have_content 'Reflect on your learning' expect(page).not_to have_link 'Additional feedback' end + + describe 'skippable questions' do + context 'when not skipped' do + it 'pagination shows all pages' do + visit first_question_path + expect(page).to have_content 'Page 1 of 8' + end + end + + context 'when skipped' do + before do + create :response, + question_name: 'feedback-oneoffquestion', + training_module: 'alpha', + answers: [1], + correct: true, + user: user, + question_type: 'feedback' + end + + it 'pagination does not show skipped pages' do + visit first_question_path + expect(page).to have_content 'Page 1 of 7' + end + end + end end From 5ab28a9a6995112e7c230bf34dcd56b5798c2a88 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 11 Mar 2024 17:19:35 +0000 Subject: [PATCH 53/95] Update banner and footer links and refactor some specs --- .rubocop.yml | 2 +- Gemfile | 2 +- app/controllers/errors_controller.rb | 7 +- app/models/user.rb | 1 + app/services/content_integrity.rb | 9 +- .../errors/service_unavailable.html.slim | 4 + .../errors/unprocessable_entity.html.slim | 4 +- app/views/layouts/_banner.html.slim | 2 +- app/views/layouts/_footer.html.slim | 3 +- config/application.rb | 1 - config/routes.rb | 5 +- docker-compose.test.yml | 1 - spec/controllers/feedback_controller_spec.rb | 121 +++++++++--------- spec/helpers/link_helper_spec.rb | 8 +- spec/system/feedback_internal_spec.rb | 13 -- ...back_external_spec.rb => feedback_spec.rb} | 8 +- 16 files changed, 88 insertions(+), 103 deletions(-) create mode 100644 app/views/errors/service_unavailable.html.slim delete mode 100644 spec/system/feedback_internal_spec.rb rename spec/system/{feedback_external_spec.rb => feedback_spec.rb} (61%) diff --git a/.rubocop.yml b/.rubocop.yml index 279cf276a..96e32f92a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,7 +14,7 @@ inherit_mode: require: rubocop-performance AllCops: - TargetRubyVersion: 3.1.3 + TargetRubyVersion: 3.2.2 Style/StringLiterals: EnforcedStyle: single_quotes diff --git a/Gemfile b/Gemfile index f146fab03..846530873 100644 --- a/Gemfile +++ b/Gemfile @@ -50,7 +50,7 @@ gem 'govuk_markdown' gem 'govuk_notify_rails' -# Sentry -Monitor errors +# Monitor errors gem 'sentry-rails' gem 'sentry-ruby' diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb index 66d66dd00..d21b70ae5 100644 --- a/app/controllers/errors_controller.rb +++ b/app/controllers/errors_controller.rb @@ -1,21 +1,22 @@ class ErrorsController < ApplicationController before_action :log_error - # 404 error def not_found render status: :not_found end - # 422 error def unprocessable_entity render status: :unprocessable_entity end - # 500 error def internal_server_error render status: :internal_server_error end + def service_unavailable + render status: :service_unavailable + end + private def log_error diff --git a/app/models/user.rb b/app/models/user.rb index 50a5fa1ff..88e66de4d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -437,6 +437,7 @@ def content_changes @content_changes ||= ContentChanges.new(user: self) end + # FIXME: completed vs started # @return [Boolean] def completed_main_feedback? responses.course_feedback.any? diff --git a/app/services/content_integrity.rb b/app/services/content_integrity.rb index e461a1555..4d428cf96 100644 --- a/app/services/content_integrity.rb +++ b/app/services/content_integrity.rb @@ -45,8 +45,9 @@ class ContentIntegrity summative: 'Insufficient summative questions', confidence: 'Insufficient confidence checks', - # NB: disabled until new validity of feedback questions can be asserted - question_answers: 'Question answers are incorrectly formatted', # TODO: which question? + # TODO: validity of feedback questions + # feedback_questions: 'TODO', + factual_questions: 'Factual questions have sufficient options', }.freeze # @return [nil] @@ -163,8 +164,8 @@ def video? end # @return [Boolean] - def question_answers? - mod.questions.all? { |question| question.answer.valid? || question.feedback_question? } + def factual_questions? + mod.questions.select(&:factual_question?).all? { |question| question.answer.valid? } end # @return [Boolean] diff --git a/app/views/errors/service_unavailable.html.slim b/app/views/errors/service_unavailable.html.slim new file mode 100644 index 000000000..383e2a1c8 --- /dev/null +++ b/app/views/errors/service_unavailable.html.slim @@ -0,0 +1,4 @@ +.govuk-grid-row + .govuk-grid-column-two-thirds + h1.govuk-heading-xl + | Service unavailable diff --git a/app/views/errors/unprocessable_entity.html.slim b/app/views/errors/unprocessable_entity.html.slim index 9e874b70e..471f7da68 100644 --- a/app/views/errors/unprocessable_entity.html.slim +++ b/app/views/errors/unprocessable_entity.html.slim @@ -1,6 +1,4 @@ --# FIXME: add missing 422 page content - -/ .govuk-grid-row +.govuk-grid-row .govuk-grid-column-two-thirds h1.govuk-heading-xl | Page not found diff --git a/app/views/layouts/_banner.html.slim b/app/views/layouts/_banner.html.slim index 3b722519e..9741eb19a 100644 --- a/app/views/layouts/_banner.html.slim +++ b/app/views/layouts/_banner.html.slim @@ -1,2 +1,2 @@ = govuk_phase_banner(tag: { text: 'Beta' }, classes: 'noprint') do - == t 'phase_banner', link: govuk_link_to('feedback', Rails.configuration.feedback_url, target: :_blank) + == t 'phase_banner', link: govuk_link_to('feedback', feedback_index_path) diff --git a/app/views/layouts/_footer.html.slim b/app/views/layouts/_footer.html.slim index 0cf08714e..40c310c18 100644 --- a/app/views/layouts/_footer.html.slim +++ b/app/views/layouts/_footer.html.slim @@ -17,5 +17,4 @@ = govuk_link_to t('links.footer.contact_us'), Rails.application.credentials.contact_us, class: 'govuk-footer__link', target: '_blank' li.govuk-footer__list-item - -# TODO: make feedback link internal - = govuk_link_to t('links.footer.feedback'), Rails.configuration.feedback_url, target: '_blank', class: 'govuk-footer__link' + = govuk_link_to t('links.footer.feedback'), feedback_index_path, class: 'govuk-footer__link' diff --git a/config/application.rb b/config/application.rb index eff73bb26..30565ba22 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,7 +44,6 @@ class Application < Rails::Application config.user_password = ENV.fetch('USER_PASSWORD', 'Str0ngPa$$w0rd12') config.bot_token = ENV['BOT_TOKEN'] - config.feedback_url = ENV.fetch('FEEDBACK_URL', '#FEEDBACK_URL_env_var_missing') # TODO: deprecate config.google_analytics_tracking_id = ENV.fetch('TRACKING_ID', '#TRACKING_ID_env_var_missing') config.hotjar_site_id = ENV.fetch('HOTJAR_SITE_ID', '#HOTJAR_SITE_ID_env_var_missing') diff --git a/config/routes.rb b/config/routes.rb index 9031ffb0f..fccd28af7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,6 +7,7 @@ get '404', to: 'errors#not_found', via: :all get '422', to: 'errors#unprocessable_entity', via: :all get '500', to: 'errors#internal_server_error', via: :all + get '503', to: 'errors#service_unavailable', via: :all resources :settings, controller: :settings, only: %i[show create] @@ -26,7 +27,7 @@ get 'check_session_timeout', to: 'timeout#check' get 'extend_session', to: 'timeout#extend' get 'users/timeout', to: 'timeout#timeout_user' - get '/users/sign_out', to: 'users/sessions#destroy' + get 'users/sign_out', to: 'users/sessions#destroy' get 'users/review', to: 'users/sessions#sign_in_test_user' unless Rails.application.live? end @@ -74,7 +75,7 @@ scope module: 'training' do resources :modules, only: %i[show], as: :training_modules do constraints proc { Rails.application.preview? || Rails.application.debug? } do - get '/structure', to: 'debug#show' + get 'structure', to: 'debug#show' end resources :pages, only: %i[index show], path: 'content-pages' diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 09abac58e..ccca98c24 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -13,6 +13,5 @@ services: - CONTENTFUL_MANAGEMENT_TOKEN= - DEBUG=false - BOT_TOKEN=bot_token - - TIMEOUT_MINUTES=25 tty: true stdin_open: true diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index 596d3adc5..a474e4a01 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -3,125 +3,122 @@ RSpec.describe FeedbackController, type: :controller do context 'when user is signed in' do let(:user) { create :user, :registered } - let(:valid_attributes) do - { - id: 'feedback-radiobutton', - training_module: nil, - response: { - answers: %w[Yes], - answers_custom: 'Custom answer', - }, - } - end before { sign_in user } describe 'GET #show' do it 'tracks feedback start' do expect(controller).to receive(:track_feedback_start) - get :show, params: valid_attributes + get :show, params: { id: 'feedback-radiobutton' } end it 'returns a success response' do - get :show, params: valid_attributes - expect(response).to be_successful + get :show, params: { id: 'feedback-radiobutton' } + expect(response).to have_http_status(:success) end end describe 'GET #index' do it 'returns a success response' do get :index - expect(response).to be_successful + expect(response).to have_http_status(:success) end end describe 'POST #update' do context 'with valid params' do - it 'creates a new Response' do + let(:params) do + { + id: 'feedback-radiobutton', + response: { + answers: %w[Yes], + answers_custom: 'Custom answer', + }, + } + end + + it 'is persisted' do expect { - post :update, params: valid_attributes + post :update, params: params }.to change(Response, :count).by(1) end - it 'redirects to the next feedback path' do - post :update, params: valid_attributes - expect(response).to redirect_to(feedback_path('feedback-yesnoandtext')) + it 'redirects to the next question' do + post :update, params: params + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to feedback_path('feedback-yesnoandtext') end it 'tracks feedback complete' do expect(controller).to receive(:track_feedback_complete) - post :update, params: valid_attributes + post :update, params: params end end context 'with invalid params' do - let(:invalid_attributes) do - { id: 1, answers: [''] } + let(:params) do + { + id: 'feedback-radiobutton', + response: { + answers: [''], + } + } end - it 'does not create a new Response' do - expect { - post :update, params: invalid_attributes - }.not_to change(Response, :count) + it 'is not processed' do + post :update, params: params + expect(response).to have_http_status(:unprocessable_entity) end - it 'redirects to the current feedback path' do - post :update, params: invalid_attributes - expect(response).to redirect_to(feedback_path(1)) + it 'is not persisted' do + expect { + post :update, params: params + }.not_to change(Response, :count) end end end end - describe 'feedback_exists?' do - let(:user) { create :user, :registered } - let(:visit) { create :visit } + describe '#feedback_exists?' do + before do + allow(controller).to receive(:current_user).and_return(user) + end - context 'when user feedback exists' do - before do - allow(controller).to receive(:current_user).and_return(user) - allow(user).to receive(:completed_main_feedback?).and_return(true) - end + context 'with registered user' do + let(:user) { create :user, :registered } - it 'is true' do + before do + allow(user).to receive(:completed_main_feedback?).and_return(completed) get :index - expect(controller.feedback_exists?).to be true end - end - context 'when user feedback does not exist' do - before do - allow(controller).to receive(:current_user).and_return(user) - allow(user).to receive(:completed_main_feedback?).and_return(false) + context 'and form started' do + let(:completed) { true } + specify { expect(controller).to be_feedback_exists } end - it 'is false' do - get :index - expect(controller.feedback_exists?).to be false + context 'and form not started' do + let(:completed) { false } + specify { expect(controller).not_to be_feedback_exists } end end - context 'when guest feedback exists' do - before do - allow(controller).to receive(:current_user).and_return(Guest.new(visit: visit)) - allow(controller).to receive(:cookies).and_return({ feedback_complete: 'some-token' }) - end + context 'with guest' do + let(:user) { Guest.new(visit: create(:visit)) } - it 'is true' do + before do + allow(controller).to receive(:cookies).and_return(cookie) get :index - expect(controller.feedback_exists?).to be true end - end - context 'when guest feedback does not exist' do - before do - allow(controller).to receive(:current_user).and_return(Guest.new(visit: visit)) - allow(controller).to receive(:cookies).and_return({}) + context 'and form started' do + let(:cookie) { { feedback_complete: 'some-token' } } + specify { expect(controller).to be_feedback_exists } end - it 'is false' do - get :index - expect(controller.feedback_exists?).to be false + context 'and form not started' do + let(:cookie) { {} } + specify { expect(controller).not_to be_feedback_exists } end end end diff --git a/spec/helpers/link_helper_spec.rb b/spec/helpers/link_helper_spec.rb index 90e6777cb..769021055 100644 --- a/spec/helpers/link_helper_spec.rb +++ b/spec/helpers/link_helper_spec.rb @@ -44,15 +44,15 @@ expect(link).to include 'href="/modules/alpha/content-pages/1-3-4"' end - it 'offers feedback to content authors' do + specify do expect(link).to include 'Next page has not been created' end end - context 'when page is feedback intro' do + context 'when next section is feedback questions' do let(:content) { mod.page_by_name('feedback-intro') } - it 'targets start of feedback questions' do + specify do expect(link).to include 'Give feedback' end end @@ -193,7 +193,7 @@ end end - describe '#link_to_skip' do + describe '#link_to_skip_feedback' do subject(:link) { helper.link_to_skip_feedback } before do diff --git a/spec/system/feedback_internal_spec.rb b/spec/system/feedback_internal_spec.rb deleted file mode 100644 index 8548ad79a..000000000 --- a/spec/system/feedback_internal_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Feedback internal' do - before do - visit '/feedback' - end - - describe 'foo' do - it 'bar' do - expect(page).to have_content 'The purpose of this feedback form is to gather your opinion on the child development training course' - end - end -end diff --git a/spec/system/feedback_external_spec.rb b/spec/system/feedback_spec.rb similarity index 61% rename from spec/system/feedback_external_spec.rb rename to spec/system/feedback_spec.rb index a77d52bbe..40212ceb5 100644 --- a/spec/system/feedback_external_spec.rb +++ b/spec/system/feedback_spec.rb @@ -1,17 +1,15 @@ require 'rails_helper' -RSpec.describe 'Feedback external' do +RSpec.describe 'Feedback' do before do visit '/' end - let(:feedback_url) { Rails.configuration.feedback_url } - describe 'in beta banner' do specify do within '.govuk-phase-banner' do expect(page).to have_text 'This is a new service, your feedback will help us improve it.' - expect(page).to have_link 'feedback', href: feedback_url + expect(page).to have_link 'feedback', href: '/feedback' end end end @@ -19,7 +17,7 @@ describe 'in footer' do specify do within '.govuk-footer' do - expect(page).to have_link 'Feedback', href: feedback_url + expect(page).to have_link 'Feedback', href: '/feedback' end end end From de20e2988c014be849e4ea1d4d58cccba7754486 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Thu, 4 Apr 2024 17:28:48 +0100 Subject: [PATCH 54/95] add system spec for site-wide feedback forms --- app/controllers/feedback_controller.rb | 10 ++- app/views/feedback/thank_you.html.slim | 5 +- config/locales/en.yml | 1 + spec/controllers/feedback_controller_spec.rb | 6 +- ...site-wide-feedback-form-response-guest.yml | 64 +++++++++++++++++ .../site-wide-feedback-form-response-user.yml | 71 +++++++++++++++++++ spec/system/site_wide_feedback_spec.rb | 30 ++++++++ 7 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 spec/support/ast/site-wide-feedback-form-response-guest.yml create mode 100644 spec/support/ast/site-wide-feedback-form-response-user.yml create mode 100644 spec/system/site_wide_feedback_spec.rb diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index a7722a854..ab2a1ef44 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -31,12 +31,10 @@ def feedback_exists? def redirect if content.eql?(mod.pages.last) - if current_user.guest? - feedback_complete_cookie - redirect_to root_path - else - redirect_to feedback_thank_you_path - end + redirect_to feedback_thank_you_path + elsif content.eql?(mod.pages[-2]) && current_user.guest? + redirect_to feedback_thank_you_path + feedback_complete_cookie else redirect_to feedback_path(content.next_item.name) end diff --git a/app/views/feedback/thank_you.html.slim b/app/views/feedback/thank_you.html.slim index 3bae33dbb..2ceb86b62 100644 --- a/app/views/feedback/thank_you.html.slim +++ b/app/views/feedback/thank_you.html.slim @@ -5,4 +5,7 @@ hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' - = govuk_button_link_to t('links.my_modules'), my_modules_path + - if current_user.guest? + = govuk_button_link_to t('links.home'), root_path + - else + = govuk_button_link_to t('links.my_modules'), my_modules_path diff --git a/config/locales/en.yml b/config/locales/en.yml index b115c868c..be90ce166 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -101,6 +101,7 @@ en: previous: Previous links: + home: Go to home my_modules: Go to my modules update_feedback: Update my feedback save: Save diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index a474e4a01..c2d2e15fd 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -61,7 +61,7 @@ id: 'feedback-radiobutton', response: { answers: [''], - } + }, } end @@ -94,11 +94,13 @@ context 'and form started' do let(:completed) { true } + specify { expect(controller).to be_feedback_exists } end context 'and form not started' do let(:completed) { false } + specify { expect(controller).not_to be_feedback_exists } end end @@ -113,11 +115,13 @@ context 'and form started' do let(:cookie) { { feedback_complete: 'some-token' } } + specify { expect(controller).to be_feedback_exists } end context 'and form not started' do let(:cookie) { {} } + specify { expect(controller).not_to be_feedback_exists } end end diff --git a/spec/support/ast/site-wide-feedback-form-response-guest.yml b/spec/support/ast/site-wide-feedback-form-response-guest.yml new file mode 100644 index 000000000..4c1df54b5 --- /dev/null +++ b/spec/support/ast/site-wide-feedback-form-response-guest.yml @@ -0,0 +1,64 @@ +# +# Site wide feedback form +# +--- +- :path: /feedback + :text: Additional feedback + :inputs: + - - :click_on + - Next +- :path: /feedback/feedback-radiobutton + :text: 'Feedback question 1 - Select from following' + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-yesnoandtext + :text: Feedback question 2 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-freetext + :text: Feedback question 3 - Complete the following + :inputs: + - - :make_note + - response-text-input-field + - hello world + - - :click_on + - Next +- :path: /feedback/feedback-radio-otherandtext + :text: Feedback question 4 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-radio-and-freetext + :text: Feedback question 5 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-checkbox-othertextandor + :text: Feedback question 6 - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-checkbox-otherandtext + :text: Feedback question 7 - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/thank-you + :text: Thank you + :inputs: + - - :click_on + - Go to home \ No newline at end of file diff --git a/spec/support/ast/site-wide-feedback-form-response-user.yml b/spec/support/ast/site-wide-feedback-form-response-user.yml new file mode 100644 index 000000000..84b25997b --- /dev/null +++ b/spec/support/ast/site-wide-feedback-form-response-user.yml @@ -0,0 +1,71 @@ +# +# Site wide feedback form - authenticated user +# +--- +- :path: /feedback + :text: Additional feedback + :inputs: + - - :click_on + - Next +- :path: /feedback/feedback-radiobutton + :text: 'Feedback question 1 - Select from following' + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-yesnoandtext + :text: Feedback question 2 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-freetext + :text: Feedback question 3 - Complete the following + :inputs: + - - :make_note + - response-text-input-field + - hello world + - - :click_on + - Next +- :path: /feedback/feedback-radio-otherandtext + :text: Feedback question 4 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-radio-and-freetext + :text: Feedback question 5 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-checkbox-othertextandor + :text: Feedback question 6 - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-checkbox-otherandtext + :text: Feedback question 7 - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-oneoffquestion + :text: Feedback question 8 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/thank-you + :text: Thank you + :inputs: + - - :click_on + - Go to my modules \ No newline at end of file diff --git a/spec/system/site_wide_feedback_spec.rb b/spec/system/site_wide_feedback_spec.rb new file mode 100644 index 000000000..d3f1665dd --- /dev/null +++ b/spec/system/site_wide_feedback_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe 'Site wide feedback' do + context 'when user is not logged in' do + include_context 'with automated path' + let(:fixture) { 'spec/support/ast/site-wide-feedback-form-response-guest.yml' } + + it 'feedback can be completed' do + expect(page).to have_title('Early years child development training : Home page') + end + end + + context 'with authenticated user' do + include_context 'with user' + + context 'when user has not completed feedback' do + include_context 'with automated path' + + let(:fixture) { 'spec/support/ast/site-wide-feedback-form-response-user.yml' } + + it 'feedback can be completed' do + expect(page).to have_content('My modules') + end + end + + context 'when user has completed feedback' do + # TODO: yaml fixture for user without skippable question + end + end +end From f7346fe7cb30da504842a22cc4d280a87c41cc60 Mon Sep 17 00:00:00 2001 From: "jack.coggin" Date: Fri, 5 Apr 2024 15:33:38 +0100 Subject: [PATCH 55/95] update site wide feedback system spec --- app/controllers/feedback_controller.rb | 10 +- app/models/user.rb | 2 +- lib/content_test_schema.rb | 2 +- .../ast/alpha-fail-feedback-form-response.yml | 2 +- spec/support/ast/alpha-fail-response.yml | 2 +- spec/support/ast/alpha-fail.yml | 2 +- .../ast/alpha-pass-feedback-form-response.yml | 2 +- spec/support/ast/alpha-pass-response.yml | 2 +- spec/support/ast/alpha-pass.yml | 2 +- spec/support/ast/schema.rb | 2 +- ... => site-feedback-form-response-guest.yml} | 2 +- ...ite-feedback-form-response-user-update.yml | 136 ++++++++++++++++++ ...l => site-feedback-form-response-user.yml} | 2 +- spec/system/site_wide_feedback_spec.rb | 13 +- ui/e2e_spec.rb | 2 +- .../ast/child-development-and-the-eyfs.yml | 24 ++-- 16 files changed, 174 insertions(+), 33 deletions(-) rename spec/support/ast/{site-wide-feedback-form-response-guest.yml => site-feedback-form-response-guest.yml} (98%) create mode 100644 spec/support/ast/site-feedback-form-response-user-update.yml rename spec/support/ast/{site-wide-feedback-form-response-user.yml => site-feedback-form-response-user.yml} (98%) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index ab2a1ef44..1b54a062a 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -23,7 +23,7 @@ def feedback_exists? if current_user.guest? cookies[:feedback_complete].present? else - current_user.completed_main_feedback? + current_user.started_main_feedback? end end @@ -32,9 +32,9 @@ def feedback_exists? def redirect if content.eql?(mod.pages.last) redirect_to feedback_thank_you_path - elsif content.eql?(mod.pages[-2]) && current_user.guest? + elsif content.next_item.skippable? && (current_user.guest? || current_user.response_for_shared(content.next_item, mod).responded?) redirect_to feedback_thank_you_path - feedback_complete_cookie + feedback_complete_cookie if current_user.guest? else redirect_to feedback_path(content.next_item.name) end @@ -65,8 +65,8 @@ def current_user end # @return [Response] - def current_user_response - @current_user_response ||= current_user.response_for_shared(content, mod) + def current_user_response(question = content) + @current_user_response ||= current_user.response_for_shared(question, mod) end # @return [Hash] diff --git a/app/models/user.rb b/app/models/user.rb index 88e66de4d..93c7f313a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -439,7 +439,7 @@ def content_changes # FIXME: completed vs started # @return [Boolean] - def completed_main_feedback? + def started_main_feedback? responses.course_feedback.any? end diff --git a/lib/content_test_schema.rb b/lib/content_test_schema.rb index f66f4cc7b..119c2a420 100644 --- a/lib/content_test_schema.rb +++ b/lib/content_test_schema.rb @@ -46,7 +46,7 @@ def inputs ] elsif payload[:note] [ - [:make_note, 'note-body-field', payload[:note]], + [:input_text, 'note-body-field', payload[:note]], [:click_on, 'Save and continue'], ] elsif type.match?(/assessment_intro/) diff --git a/spec/support/ast/alpha-fail-feedback-form-response.yml b/spec/support/ast/alpha-fail-feedback-form-response.yml index 65004507b..214e2d7c3 100644 --- a/spec/support/ast/alpha-fail-feedback-form-response.yml +++ b/spec/support/ast/alpha-fail-feedback-form-response.yml @@ -30,7 +30,7 @@ - :path: /modules/alpha/content-pages/1-1-3-1 :text: 1-1-3-1 :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on diff --git a/spec/support/ast/alpha-fail-response.yml b/spec/support/ast/alpha-fail-response.yml index f233e7fdf..fe5b2c04b 100644 --- a/spec/support/ast/alpha-fail-response.yml +++ b/spec/support/ast/alpha-fail-response.yml @@ -30,7 +30,7 @@ - :path: /modules/alpha/content-pages/1-1-3-1 :text: 1-1-3-1 :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on diff --git a/spec/support/ast/alpha-fail.yml b/spec/support/ast/alpha-fail.yml index e7d1ca58e..f5cefa67c 100644 --- a/spec/support/ast/alpha-fail.yml +++ b/spec/support/ast/alpha-fail.yml @@ -30,7 +30,7 @@ - :path: /modules/alpha/content-pages/1-1-3-1 :text: 1-1-3-1 :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on diff --git a/spec/support/ast/alpha-pass-feedback-form-response.yml b/spec/support/ast/alpha-pass-feedback-form-response.yml index 97998040a..fb57d3025 100644 --- a/spec/support/ast/alpha-pass-feedback-form-response.yml +++ b/spec/support/ast/alpha-pass-feedback-form-response.yml @@ -30,7 +30,7 @@ - :path: /modules/alpha/content-pages/1-1-3-1 :text: 1-1-3-1 :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on diff --git a/spec/support/ast/alpha-pass-response.yml b/spec/support/ast/alpha-pass-response.yml index 29806bafd..8fb7b47cd 100644 --- a/spec/support/ast/alpha-pass-response.yml +++ b/spec/support/ast/alpha-pass-response.yml @@ -30,7 +30,7 @@ - :path: /modules/alpha/content-pages/1-1-3-1 :text: 1-1-3-1 :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on diff --git a/spec/support/ast/alpha-pass.yml b/spec/support/ast/alpha-pass.yml index 3dfe96bda..d9a384dd6 100644 --- a/spec/support/ast/alpha-pass.yml +++ b/spec/support/ast/alpha-pass.yml @@ -30,7 +30,7 @@ - :path: /modules/alpha/content-pages/1-1-3-1 :text: 1-1-3-1 :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on diff --git a/spec/support/ast/schema.rb b/spec/support/ast/schema.rb index 7e50dad30..41b1d83c2 100644 --- a/spec/support/ast/schema.rb +++ b/spec/support/ast/schema.rb @@ -1,6 +1,6 @@ require 'content_test_schema' # TODO: remove need to wrap "fill_in" kwargs -def make_note(field, input) +def input_text(field, input) fill_in field, with: input end diff --git a/spec/support/ast/site-wide-feedback-form-response-guest.yml b/spec/support/ast/site-feedback-form-response-guest.yml similarity index 98% rename from spec/support/ast/site-wide-feedback-form-response-guest.yml rename to spec/support/ast/site-feedback-form-response-guest.yml index 4c1df54b5..c9d232e36 100644 --- a/spec/support/ast/site-wide-feedback-form-response-guest.yml +++ b/spec/support/ast/site-feedback-form-response-guest.yml @@ -24,7 +24,7 @@ - :path: /feedback/feedback-freetext :text: Feedback question 3 - Complete the following :inputs: - - - :make_note + - - :input_text - response-text-input-field - hello world - - :click_on diff --git a/spec/support/ast/site-feedback-form-response-user-update.yml b/spec/support/ast/site-feedback-form-response-user-update.yml new file mode 100644 index 000000000..20b5bbd1e --- /dev/null +++ b/spec/support/ast/site-feedback-form-response-user-update.yml @@ -0,0 +1,136 @@ +# +# Site wide feedback form - authenticated user +# +--- +- :path: /feedback + :text: Additional feedback + :inputs: + - - :click_on + - Next +- :path: /feedback/feedback-radiobutton + :text: 'Feedback question 1 - Select from following' + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-yesnoandtext + :text: Feedback question 2 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-freetext + :text: Feedback question 3 - Complete the following + :inputs: + - - :input_text + - response-text-input-field + - hello world + - - :click_on + - Next +- :path: /feedback/feedback-radio-otherandtext + :text: Feedback question 4 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-radio-and-freetext + :text: Feedback question 5 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-checkbox-othertextandor + :text: Feedback question 6 - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-checkbox-otherandtext + :text: Feedback question 7 - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-oneoffquestion + :text: Feedback question 8 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/thank-you + :text: Thank you + :inputs: + - - :click_on + - Go to my modules +- :path: /my-modules + :text: My modules + :inputs: + - - :click_on + - feedback +- :path: /feedback + :text: Additional feedback + :inputs: + - - :click_on + - Update my feedback +- :path: /feedback/feedback-radiobutton + :text: 'Feedback question 1 - Select from following' + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-yesnoandtext + :text: Feedback question 2 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-freetext + :text: Feedback question 3 - Complete the following + :inputs: + - - :input_text + - response-text-input-field + - hello world + - - :click_on + - Next +- :path: /feedback/feedback-radio-otherandtext + :text: Feedback question 4 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-radio-and-freetext + :text: Feedback question 5 - Select from following + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-checkbox-othertextandor + :text: Feedback question 6 - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-checkbox-otherandtext + :text: Feedback question 7 - Select from following + :inputs: + - - :check + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/thank-you + :text: Thank you + :inputs: + - - :click_on + - Go to my modules \ No newline at end of file diff --git a/spec/support/ast/site-wide-feedback-form-response-user.yml b/spec/support/ast/site-feedback-form-response-user.yml similarity index 98% rename from spec/support/ast/site-wide-feedback-form-response-user.yml rename to spec/support/ast/site-feedback-form-response-user.yml index 84b25997b..610dcda97 100644 --- a/spec/support/ast/site-wide-feedback-form-response-user.yml +++ b/spec/support/ast/site-feedback-form-response-user.yml @@ -24,7 +24,7 @@ - :path: /feedback/feedback-freetext :text: Feedback question 3 - Complete the following :inputs: - - - :make_note + - - :input_text - response-text-input-field - hello world - - :click_on diff --git a/spec/system/site_wide_feedback_spec.rb b/spec/system/site_wide_feedback_spec.rb index d3f1665dd..e106293d6 100644 --- a/spec/system/site_wide_feedback_spec.rb +++ b/spec/system/site_wide_feedback_spec.rb @@ -3,7 +3,7 @@ describe 'Site wide feedback' do context 'when user is not logged in' do include_context 'with automated path' - let(:fixture) { 'spec/support/ast/site-wide-feedback-form-response-guest.yml' } + let(:fixture) { 'spec/support/ast/site-feedback-form-response-guest.yml' } it 'feedback can be completed' do expect(page).to have_title('Early years child development training : Home page') @@ -12,11 +12,11 @@ context 'with authenticated user' do include_context 'with user' + include_context 'with automated path' context 'when user has not completed feedback' do - include_context 'with automated path' - let(:fixture) { 'spec/support/ast/site-wide-feedback-form-response-user.yml' } + let(:fixture) { 'spec/support/ast/site-feedback-form-response-user.yml' } it 'feedback can be completed' do expect(page).to have_content('My modules') @@ -24,7 +24,12 @@ end context 'when user has completed feedback' do - # TODO: yaml fixture for user without skippable question + + let(:fixture) { 'spec/support/ast/site-feedback-form-response-user-update.yml' } + + it 'feedback can be updated' do + expect(page).to have_content('My modules') + end end end end diff --git a/ui/e2e_spec.rb b/ui/e2e_spec.rb index 8cd3b0e23..59350b0d2 100644 --- a/ui/e2e_spec.rb +++ b/ui/e2e_spec.rb @@ -7,7 +7,7 @@ YAML.load_file('ui/support/ast/child-development-and-the-eyfs.yml') end - def make_note(field, input) + def input_text(field, input) fill_in field, with: input end diff --git a/ui/support/ast/child-development-and-the-eyfs.yml b/ui/support/ast/child-development-and-the-eyfs.yml index 6528a49f2..6470f10f9 100644 --- a/ui/support/ast/child-development-and-the-eyfs.yml +++ b/ui/support/ast/child-development-and-the-eyfs.yml @@ -24,7 +24,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-1-1-1 :text: How understanding child development benefits the child :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on @@ -43,7 +43,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-1-1-2 :text: How understanding child development benefits the practitioner :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on @@ -62,7 +62,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-1-1-3 :text: How understanding child development benefits the wider support network :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on @@ -71,7 +71,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-1-1-4 :text: The potential impact of limited child development knowledge :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on @@ -90,7 +90,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-1-1-5 :text: How improved child development knowledge supports practice - scaffolding learning :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on @@ -119,7 +119,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-1-1-7 :text: How improved child development knowledge supports practice - effective holistic support :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on @@ -224,7 +224,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-1-2-9 :text: The importance of language skills :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on @@ -275,7 +275,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-2-1-1 :text: Acknowledging children’s individual strengths :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on @@ -300,7 +300,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-2-1-3 :text: Supporting individual potential :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on @@ -309,7 +309,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-2-1-4 :text: Successful enabling environments :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on @@ -338,7 +338,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-2-1-5 :text: 'A balanced approach to learning: child led play' :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on @@ -389,7 +389,7 @@ :path: /modules/child-development-and-the-eyfs/content-pages/1-2-2-1 :text: An effective EYFS :inputs: - - - :make_note + - - :input_text - note-body-field - hello world - - :click_on From eafe11a7b9d3fac8e3059120c3a4e0c3cb4187fd Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Wed, 10 Apr 2024 16:01:09 +0100 Subject: [PATCH 56/95] Passing --- .github/workflows/ci.yml | 5 +- app/controllers/concerns/learning.rb | 6 +- app/controllers/feedback_controller.rb | 61 +++--- .../training/responses_controller.rb | 7 + .../feedback_pagination_decorator.rb | 56 ++++-- app/decorators/next_page_decorator.rb | 6 +- app/decorators/pagination_decorator.rb | 1 - app/forms/form_builder.rb | 4 +- app/helpers/link_helper.rb | 2 +- app/models/data_analysis/feedback_forms.rb | 44 ----- .../data_analysis/module_feedback_forms.rb | 27 +++ app/models/event.rb | 3 + app/models/guest.rb | 18 +- app/models/training/question.rb | 4 +- app/models/user.rb | 24 ++- app/services/dashboard.rb | 2 +- app/views/feedback/index.html.slim | 6 +- app/views/feedback/show.html.slim | 8 +- app/views/training/questions/_debug.html.slim | 4 + cms/migrate/02-create-question.js | 19 +- config/locales/en.yml | 10 +- config/routes.rb | 1 - lib/content_test_schema.rb | 2 +- lib/fill_page_views.rb | 77 -------- lib/tasks/cms.rake | 3 +- spec/config_spec.rb | 2 - spec/controllers/feedback_controller_spec.rb | 32 +-- .../feedback_pagination_decorator_spec.rb | 51 +++-- .../decorators/module_debug_decorator_spec.rb | 21 +- spec/factories/event.rb | 4 - spec/lib/content_test_schema_spec.rb | 22 ++- spec/lib/fill_page_views_spec.rb | 104 ---------- spec/lib/seed_snippets_spec.rb | 2 +- .../data_analysis/feedback_forms_spec.rb | 58 ------ .../module_feedback_forms_spec.rb | 48 +++++ spec/models/guest_spec.rb | 7 +- spec/models/response_spec.rb | 90 ++++++--- spec/services/dashboard_spec.rb | 4 +- .../ast/alpha-fail-feedback-form-response.yml | 182 ------------------ ... => alpha-pass-response-skip-feedback.yml} | 2 +- ... => alpha-pass-response-with-feedback.yml} | 0 ...-response-user.yml => course-feedback.yml} | 12 +- .../ast/site-feedback-form-response-guest.yml | 64 ------ ...ite-feedback-form-response-user-update.yml | 136 ------------- spec/system/course_feedback_spec.rb | 45 +++++ spec/system/event_log_spec.rb | 2 +- spec/system/module_feedback_spec.rb | 53 +++++ spec/system/module_overview_content_spec.rb | 11 +- spec/system/my_learning_spec.rb | 2 +- spec/system/opinion_spec.rb | 65 ------- spec/system/site_wide_feedback_spec.rb | 35 ---- spec/system/summative_assessment_spec.rb | 4 +- 52 files changed, 501 insertions(+), 957 deletions(-) delete mode 100644 app/models/data_analysis/feedback_forms.rb create mode 100644 app/models/data_analysis/module_feedback_forms.rb delete mode 100644 lib/fill_page_views.rb delete mode 100644 spec/lib/fill_page_views_spec.rb delete mode 100644 spec/models/data_analysis/feedback_forms_spec.rb create mode 100644 spec/models/data_analysis/module_feedback_forms_spec.rb delete mode 100644 spec/support/ast/alpha-fail-feedback-form-response.yml rename spec/support/ast/{alpha-pass-response.yml => alpha-pass-response-skip-feedback.yml} (98%) rename spec/support/ast/{alpha-pass-feedback-form-response.yml => alpha-pass-response-with-feedback.yml} (100%) rename spec/support/ast/{site-feedback-form-response-user.yml => course-feedback.yml} (87%) delete mode 100644 spec/support/ast/site-feedback-form-response-guest.yml delete mode 100644 spec/support/ast/site-feedback-form-response-user-update.yml create mode 100644 spec/system/course_feedback_spec.rb create mode 100644 spec/system/module_feedback_spec.rb delete mode 100644 spec/system/opinion_spec.rb delete mode 100644 spec/system/site_wide_feedback_spec.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ac47d18f..b62aeabea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,11 +79,10 @@ jobs: run: bundle exec rails assets:precompile - name: Run test suite - run: bundle exec rspec env: - # TODO: Check envs before merging into main - CONTENTFUL_PREVIEW: true DISABLE_USER_ANSWER: true + CONTENTFUL_PREVIEW: true + run: bundle exec rspec - name: Run rubocop run: bundle exec rubocop diff --git a/app/controllers/concerns/learning.rb b/app/controllers/concerns/learning.rb index 6226020d5..2924076d8 100644 --- a/app/controllers/concerns/learning.rb +++ b/app/controllers/concerns/learning.rb @@ -55,10 +55,10 @@ def module_table ModuleDebugDecorator.new(progress_service).rows end - # @return [PaginationDecorator] + # @return [PaginationDecorator, FeedbackPaginationDecorator] def section_bar - if content.feedback_question? - FeedbackPaginationDecorator.new(content) + if content.feedback_question? || content.previous_item.feedback_question? + FeedbackPaginationDecorator.new(content, current_user) else PaginationDecorator.new(content) end diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 1b54a062a..09a0850b2 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -1,45 +1,56 @@ class FeedbackController < ApplicationController - before_action :track_feedback_start, only: :show - after_action :track_feedback_complete, only: :update helper_method :content, :mod, :current_user_response, - :feedback_exists? - - def show; end + :feedback_complete? def index; end + def show + if question_name.eql? 'thank-you' + feedback_cookie(:completed) + track_feedback_complete + render :thank_you + end + end + def update if save_response! + feedback_cookie(:started) + track_feedback_start redirect else - render 'feedback/show', status: :unprocessable_entity + render :show, status: :unprocessable_entity end end + # @see feedback#index # @return [Boolean] - def feedback_exists? + def feedback_complete? if current_user.guest? - cookies[:feedback_complete].present? + cookies[:course_feedback_completed].present? else - current_user.started_main_feedback? + current_user.completed_course_feedback? end end private def redirect - if content.eql?(mod.pages.last) - redirect_to feedback_thank_you_path - elsif content.next_item.skippable? && (current_user.guest? || current_user.response_for_shared(content.next_item, mod).responded?) - redirect_to feedback_thank_you_path - feedback_complete_cookie if current_user.guest? + if content.last_feedback? + redirect_to feedback_path('thank-you') + elsif skip_next_question? + redirect_to feedback_path(content.next_item.next_item.name) else redirect_to feedback_path(content.next_item.name) end end + # @return [Boolean] + def skip_next_question? + current_user.skip_question?(content.next_item) + end + # @return [Boolean] def save_response! current_user_response.update( @@ -64,18 +75,12 @@ def current_user super || guest end + # @param question [Training::Question] default: current question # @return [Response] def current_user_response(question = content) @current_user_response ||= current_user.response_for_shared(question, mod) end - # @return [Hash] - def feedback_complete_cookie - cookies[:feedback_complete] = { - value: current_user.visit.visit_token, - } - end - # @return [String] def question_name params[:id] @@ -91,16 +96,18 @@ def user_answers Array(response_params[:answers]).compact_blank.map(&:to_i) end + # @param state [Symbol, String] + # @return [Hash] + def feedback_cookie(state) + cookies["course_feedback_#{state}"] = { value: current_user.cookie_token } + end + def track_feedback_start - if content.first_feedback? && feedback_start_untracked? - track('feedback_start') - end + track('feedback_start') if feedback_start_untracked? end def track_feedback_complete - if content.last_feedback? && feedback_complete_untracked? - track('feedback_complete') - end + track('feedback_complete') if feedback_complete_untracked? end def feedback_start_untracked? diff --git a/app/controllers/training/responses_controller.rb b/app/controllers/training/responses_controller.rb index a0990287d..5d1b23332 100644 --- a/app/controllers/training/responses_controller.rb +++ b/app/controllers/training/responses_controller.rb @@ -64,11 +64,18 @@ def redirect if content.formative_question? redirect_to training_module_question_path(mod.name, content.name) + elsif skip_next_question? + redirect_to training_module_page_path(mod.name, content.next_item.next_item.name) else redirect_to training_module_page_path(mod.name, content.next_item.name) end end + # @return [Boolean] + def skip_next_question? + current_user.skip_question?(content.next_item) + end + # @return [Event] Update action def track_question_answer if Rails.application.migrated_answers? diff --git a/app/decorators/feedback_pagination_decorator.rb b/app/decorators/feedback_pagination_decorator.rb index f1ef32c22..6a7a60d6d 100644 --- a/app/decorators/feedback_pagination_decorator.rb +++ b/app/decorators/feedback_pagination_decorator.rb @@ -1,33 +1,53 @@ +# Overrides heading +# Checks if one-off questions should be skipped +# class FeedbackPaginationDecorator < PaginationDecorator # TODO: Add type check for feedback question type - # @!attribute [r] content - # @return [Training::Question] - # param :content, Types::TrainingContent, required: true + + # @!attribute [r] user + # @return [User] + param :user, Types.Instance(User), required: true # @return [String] def heading 'Additional feedback' end - # @return [String] - def section_numbers - I18n.t(:section, scope: :pagination, current: content.submodule - 1, total: section_total - 1) - end - private + # @return [Integer] + def current_page + return super unless skip_question? && after_skippable_question? + + super - 1 + end + # @return [Integer] def page_total - size = content.section_content.size - if content.section_content.any?(&:skippable?) - # don't count skipped page - content.section_content.each do |section_content| - if section_content.feedback_question? && section_content.skippable_question.eql?(true) # && response_for_shared.responded? - size -= 1 - end - end - end + return super unless skip_question? - size + super - 1 + end + + # @return [Boolean] + def after_skippable_question? + content.section_content.index(content) > content.section_content.index(skippable_question) + end + + # @return [Training::Question] + def skippable_question + content.section_content.find(&:skippable?) + end + + # @return [Array] + def other_forms + Training::Module.live.to_a - [content.parent] << Course.config + end + + # @return [Boolean] + def skip_question? + other_forms.any? do |form| + user.response_for_shared(skippable_question, form).responded? + end end end diff --git a/app/decorators/next_page_decorator.rb b/app/decorators/next_page_decorator.rb index cc679c989..c29c85c45 100644 --- a/app/decorators/next_page_decorator.rb +++ b/app/decorators/next_page_decorator.rb @@ -23,7 +23,7 @@ class NextPageDecorator def name if content.interruption_page? mod.content_start.name - elsif skippable_page? + elsif skip_next_question? content.next_item.next_item.name else content.next_item.name @@ -118,7 +118,7 @@ def wip? end # @return [Boolean] - def skippable_page? - !content.interruption_page? && content.next_item.skippable? && user.response_for_shared(content.next_item, mod).responded? + def skip_next_question? + user.skip_question?(content.next_item) end end diff --git a/app/decorators/pagination_decorator.rb b/app/decorators/pagination_decorator.rb index af47be52b..20154b973 100644 --- a/app/decorators/pagination_decorator.rb +++ b/app/decorators/pagination_decorator.rb @@ -38,7 +38,6 @@ def current_page content.section_content.index(content) + 1 end - # TODO: - update for skipped questions # @return [Integer] def page_total content.section_content.size diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index 99c3b9434..f7673245b 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -50,7 +50,7 @@ def other_radio_button(option, text:) end end - # @param option [Training::Answer::Option] + # @option checked [Boolean] # @option text [String] # @return [String] def or_radio_button(text:, checked:) @@ -61,7 +61,7 @@ def or_radio_button(text:, checked:) checked: checked end - # @param option [Training::Answer::Option] + # @option checked [Boolean] # @option text [String] # @return [String] def or_checkbox_button(text:, checked:) diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 63eff4584..19df5f8c2 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -68,7 +68,7 @@ def link_to_retake_or_results(mod) # @return [String] def link_to_skip_feedback - govuk_link_to 'Skip feedback', training_module_page_path(mod.name, mod.thankyou_page.name) + govuk_link_to t('links.feedback.skip'), training_module_page_path(mod.name, mod.thankyou_page.name) end # @return [NextPageDecorator] diff --git a/app/models/data_analysis/feedback_forms.rb b/app/models/data_analysis/feedback_forms.rb deleted file mode 100644 index 73aacdaa0..000000000 --- a/app/models/data_analysis/feedback_forms.rb +++ /dev/null @@ -1,44 +0,0 @@ -module DataAnalysis - class FeedbackForms - include ToCsv - - class << self - # @return [Array] - def column_names - [ - 'Module', - 'Total Responses', - 'Signed in Users', - 'Guest Users', - ] - end - - # @return [Array] - def dashboard - rows = Training::Module.ordered.map { |mod| feedback_for(mod.name, "properties ->> 'training_module_id' = ?") } - rows << feedback_for('feedback', "properties ->> 'controller' = ?") - rows - end - - private - - # @param mod_name [String] the name of the module or "feedback" for site wide. - # @param condition [String] the condition to filter the events. - # @return [Hash] - def feedback_for(mod_name, condition) - events = complete_events.where(condition, mod_name.to_s) - { - mod: mod_name == 'feedback' ? 'site wide' : mod_name, - total: events.count, - signed_in: events.where.not(user_id: nil).count, - guest: events.where(user_id: nil).count, - } - end - - # @return [ActiveRecord::Relation] - def complete_events - Event.where(name: 'feedback_complete') - end - end - end -end diff --git a/app/models/data_analysis/module_feedback_forms.rb b/app/models/data_analysis/module_feedback_forms.rb new file mode 100644 index 000000000..970884c63 --- /dev/null +++ b/app/models/data_analysis/module_feedback_forms.rb @@ -0,0 +1,27 @@ +module DataAnalysis + class ModuleFeedbackForms + include ToCsv + + class << self + # @return [Array] + def column_names + %w[ + Module + Started + Completed + ] + end + + # @return [Array] + def dashboard + Training::Module.live.map do |mod| + { + mod: mod.title, + started: Event.where_module(mod.name).feedback_start.count, + completed: Event.where_module(mod.name).feedback_complete.count, + } + end + end + end + end +end diff --git a/app/models/event.rb b/app/models/event.rb index 7afd171ac..cf43b50fc 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -37,6 +37,9 @@ class Event < ApplicationRecord scope :module_complete, -> { where(name: 'module_complete') } scope :confidence_check_complete, -> { where(name: 'confidence_check_complete') } + scope :feedback_start, -> { where(name: 'feedback_start') } + scope :feedback_complete, -> { where(name: 'feedback_complete') } + # @param mod_names [Array] filter by Training::Module names scope :where_module, lambda { |*mod_names| where("properties -> 'training_module_id' ?| array[:values]", values: mod_names) diff --git a/app/models/guest.rb b/app/models/guest.rb index e0c296feb..acbe916f2 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -1,3 +1,6 @@ +# +# Fallback "current_user" object for visitor feedback forms +# class Guest < Dry::Struct # @!attribute [r] visit # @return [Visit] @@ -8,18 +11,29 @@ def guest? true end + # @param question [Training::Question] + # @return [Boolean] + def skip_question?(question) + question.skippable? && response_for_shared(question).responded? + end + # @param content [Training::Question] feedback questions # @param mod [Course] # @return [Response] - def response_for_shared(content, mod) + def response_for_shared(content, mod = Course.config) responses.find_or_initialize_by( question_type: content.page_type, question_name: content.name, - training_module: mod.nil? ? nil : mod.name, + training_module: mod.name, visit_id: visit.id, ) end + # @return [String] + def cookie_token + visit.visit_token + end + private # @return [Response::ActiveRecord_Relation] diff --git a/app/models/training/question.rb b/app/models/training/question.rb index 483a09ace..5aa5bc652 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -30,7 +30,7 @@ def to_partial_path # @return [Boolean] def multi_select? if feedback_question? - response_type # FIXME: this field smells + multi_select elsif confidence_question? false else @@ -40,7 +40,7 @@ def multi_select? # @return [Boolean] def skippable? - feedback_question? && skippable_question + feedback_question? && skippable end # @return [Boolean] diff --git a/app/models/user.rb b/app/models/user.rb index 93c7f313a..a5faea6d4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -177,6 +177,20 @@ def guest? false end + # @see FeedbackPaginationDecorator + # + # Checks all feedback forms for answers + # + # @param question [Training::Question] + # @return [Boolean] + def skip_question?(question) + return false unless question.skippable? + + (Training::Module.live.to_a << Course.config).any? do |form| + response_for_shared(question, form).responded? + end + end + # @see ResponsesController#response_params # @param content [Training::Question] # @return [UserAnswer, Response] @@ -437,10 +451,14 @@ def content_changes @content_changes ||= ContentChanges.new(user: self) end - # FIXME: completed vs started # @return [Boolean] - def started_main_feedback? - responses.course_feedback.any? + def completed_course_feedback? + responses.course_feedback.count.eql? Course.config.feedback_questions.count + end + + # @return [String] + def cookie_token + visits.last.visit_token end private diff --git a/app/services/dashboard.rb b/app/services/dashboard.rb index e1831369f..e56fd750d 100644 --- a/app/services/dashboard.rb +++ b/app/services/dashboard.rb @@ -33,7 +33,7 @@ class Dashboard { model: 'DataAnalysis::UserModuleCompletion', folder: 'nonlinear', file: 'user_module_completion' }, { model: 'DataAnalysis::UserModuleCompletionCount', folder: 'nonlinear', file: 'user_module_completions_count' }, { model: 'DataAnalysis::ReturningUsers', folder: 'nonlinear', file: 'returning_users' }, - { model: 'DataAnalysis::FeedbackForms', folder: 'feedback', file: 'feedback_forms' }, + { model: 'DataAnalysis::ModuleFeedbackForms', folder: 'feedback', file: 'feedback_forms' }, ].freeze # @return [String] 30-06-2022-09-30 diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index a0c01777e..fd4e9eec1 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -1,11 +1,11 @@ .govuk-grid-row .govuk-grid-column-full - - if feedback_exists? - = m('feedback.feedback_exists') + - if feedback_complete? + = m('feedback.complete') .govuk-button-group / = govuk_button_link_to t('previous_page.previous'), my_modules_path, secondary: true - = govuk_button_link_to t('links.update_feedback'), feedback_path(mod.pages.first.name) + = govuk_button_link_to t('links.feedback.update'), feedback_path(mod.pages.first.name) - else = m('feedback.intro') diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index c6e700d25..58a7d1b12 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -7,13 +7,9 @@ = f.govuk_error_summary - -# a form inside a heading element is not accessible + = render partial: content.to_partial_path, locals: { f: f }, object: current_user_response, as: :response - h1.govuk-heading-xl class='govuk-!-margin-bottom-4' - - = render partial: content.to_partial_path, locals: { f: f }, object: current_user_response, as: :response - - hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' + hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' .govuk-button-group - if content.first_feedback? diff --git a/app/views/training/questions/_debug.html.slim b/app/views/training/questions/_debug.html.slim index a9d647a4c..35ba5007a 100644 --- a/app/views/training/questions/_debug.html.slim +++ b/app/views/training/questions/_debug.html.slim @@ -16,6 +16,10 @@ br | User type: #{current_user.guest? ? 'guest' : 'authenticated user'} br + | Skippable: #{content.skippable?} + br + | Skip next question: #{current_user.skip_question?(content.next_item)} + br | Hint: #{content.hint} br | Or: #{content.or} diff --git a/cms/migrate/02-create-question.js b/cms/migrate/02-create-question.js index d2113449f..63ac6b971 100644 --- a/cms/migrate/02-create-question.js +++ b/cms/migrate/02-create-question.js @@ -1,6 +1,6 @@ module.exports = function(migration) { - const question = migration.createContentType('question_test', { + const question = migration.createContentType('question', { name: 'Question test', displayField: 'name', description: 'Formative, Summative, Confidence or Feedback' @@ -100,15 +100,15 @@ module.exports = function(migration) { ====================== formative and summative are dynamic based off number of correct options confidence are hard-coded - feedback are more nuanced + feedback are controlled by content editors */ - question.createField('response_type', { - name: 'Feedback response type', - type: 'Symbol', + question.createField('multi_select', { + name: 'Multi select', + type: 'Boolean' }) question.createField('skippable', { - name: 'One-off question', + name: 'Skippable', type: 'Boolean', required: true, defaultValue: { @@ -140,11 +140,16 @@ module.exports = function(migration) { /* toggle */ + question.changeFieldControl('multi_select', 'builtin', 'boolean', { + helpText: 'Select multiple options?', + trueLabel: 'yes', + falseLabel: 'no' + }) + question.changeFieldControl('skippable', 'builtin', 'boolean', { helpText: 'Hide once answered?', trueLabel: 'yes', falseLabel: 'no' }) - } diff --git a/config/locales/en.yml b/config/locales/en.yml index be90ce166..bb92f5e28 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -103,7 +103,9 @@ en: links: home: Go to home my_modules: Go to my modules - update_feedback: Update my feedback + feedback: + update: Update my feedback + skip: Skip feedback save: Save continue: Continue cancel: Cancel @@ -585,8 +587,8 @@ en: %{criteria} - # /feedback feedback: + # /feedback intro: | # Give feedback @@ -599,7 +601,8 @@ en: By completing this form you have understood the above and consent to take part. - feedback_exists: | + # /feedback + complete: | # You have already submitted feedback Thank you for helping to improve this training @@ -611,7 +614,6 @@ en: --- - # /feedback/thank-you thank_you: heading: Thank you diff --git a/config/routes.rb b/config/routes.rb index fccd28af7..fb583878b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -85,7 +85,6 @@ end end - get 'feedback/thank-you', to: 'feedback#thank_you' resources :feedback, only: %i[index show update] post 'change', to: 'hook#change' diff --git a/lib/content_test_schema.rb b/lib/content_test_schema.rb index 119c2a420..d42f0315b 100644 --- a/lib/content_test_schema.rb +++ b/lib/content_test_schema.rb @@ -149,6 +149,6 @@ def next_schema # @return [Boolean] def skip? - !pass && (type.match?(/confidence|thank|certificate/) || slug.match?(/feedback/)) + !pass && (type.match?(/results|confidence|thank|certificate/) || slug.match?(/feedback/)) end end diff --git a/lib/fill_page_views.rb b/lib/fill_page_views.rb deleted file mode 100644 index 651301352..000000000 --- a/lib/fill_page_views.rb +++ /dev/null @@ -1,77 +0,0 @@ -# :nocov: -# Assumes gaps in page views due to skipping or revisions to content -# -# Loop over active users and modules and inject page view events for skipped pages -# -class FillPageViews - def call - users.find_each(batch_size: 1_000) do |user| - unless user.registration_complete - log "user [#{user.id}]" - next - end - - tracker = Ahoy::Tracker.new(user: user, controller: 'training/pages') - - training_modules.each do |mod| - progress = ModuleProgress.new(user: user, mod: mod) - - unless progress.furthest_page - log "user [#{user.id}] module [#{mod.position}] - not started" - next - end - - unless progress.skipped? - log "user [#{user.id}] module [#{mod.position}] - not skipped" - next - end - - skipped = 0 - page = progress.furthest_page.name - - mod.content.each do |content| - break if content.eql?(progress.furthest_page.next_item) - - if progress.visited?(content) - next - else - tracker.track('module_content_page', { - uid: content.id, - mod_uid: mod.id, - skipped: true, - id: content.name, - action: 'show', - controller: 'training/pages', - training_module_id: mod.name, - }) - - skipped += 1 - end - end - - log "user [#{user.id}] module [#{mod.position}] - [#{skipped}] skipped before page [#{page}]" - end - end - end - -private - - def users - User.order(:id).all - end - - def training_modules - Training::Module.ordered - end - - # @param message [String] - # @return [String, nil] - def log(message) - if ENV['RAILS_LOG_TO_STDOUT'].present? - Rails.logger.info(message) - elsif ENV['VERBOSE'].present? - puts message - end - end -end -# :nocov: diff --git a/lib/tasks/cms.rake b/lib/tasks/cms.rake index ec4088b6a..c50696f8e 100644 --- a/lib/tasks/cms.rake +++ b/lib/tasks/cms.rake @@ -18,8 +18,7 @@ namespace :eyfs do desc 'Define Contentful entry models' task migrate: :environment do - # Dir[Rails.root.join('cms/migrate/*')].each do |file| - Dir[Rails.root.join('cms/migrate/*question*')].each do |file| + Dir[Rails.root.join('cms/migrate/*')].each do |file| system <<~CMD contentful space migration \ --management-token #{ContentfulRails.configuration.management_token} \ diff --git a/spec/config_spec.rb b/spec/config_spec.rb index 7e3482904..418db469f 100644 --- a/spec/config_spec.rb +++ b/spec/config_spec.rb @@ -8,9 +8,7 @@ expect(config.contentful_environment).to eq 'test' end - # Uncomment this when feedback forms have been published it 'tests against published content' do - skip 'feedback wip' expect(Rails.application).not_to be_preview end diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index c2d2e15fd..45b39fafa 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -7,9 +7,9 @@ before { sign_in user } describe 'GET #show' do - it 'tracks feedback start' do - expect(controller).to receive(:track_feedback_start) - get :show, params: { id: 'feedback-radiobutton' } + it 'tracks feedback complete' do + expect(controller).to receive(:track_feedback_complete) + get :show, params: { id: 'thank-you' } end it 'returns a success response' do @@ -49,8 +49,8 @@ expect(response).to redirect_to feedback_path('feedback-yesnoandtext') end - it 'tracks feedback complete' do - expect(controller).to receive(:track_feedback_complete) + it 'tracks feedback started' do + expect(controller).to receive(:track_feedback_start) post :update, params: params end end @@ -79,7 +79,7 @@ end end - describe '#feedback_exists?' do + describe '#feedback_complete?' do before do allow(controller).to receive(:current_user).and_return(user) end @@ -88,20 +88,20 @@ let(:user) { create :user, :registered } before do - allow(user).to receive(:completed_main_feedback?).and_return(completed) + allow(user).to receive(:completed_course_feedback?).and_return(completed) get :index end - context 'and form started' do + context 'and form completed' do let(:completed) { true } - specify { expect(controller).to be_feedback_exists } + specify { expect(controller).to be_feedback_complete } end - context 'and form not started' do + context 'and form not completed' do let(:completed) { false } - specify { expect(controller).not_to be_feedback_exists } + specify { expect(controller).not_to be_feedback_complete } end end @@ -113,16 +113,16 @@ get :index end - context 'and form started' do - let(:cookie) { { feedback_complete: 'some-token' } } + context 'and form completed' do + let(:cookie) { { course_feedback_completed: 'some-token' } } - specify { expect(controller).to be_feedback_exists } + specify { expect(controller).to be_feedback_complete } end - context 'and form not started' do + context 'and form not completed' do let(:cookie) { {} } - specify { expect(controller).not_to be_feedback_exists } + specify { expect(controller).not_to be_feedback_complete } end end end diff --git a/spec/decorators/feedback_pagination_decorator_spec.rb b/spec/decorators/feedback_pagination_decorator_spec.rb index 77499719f..eeefb5e34 100644 --- a/spec/decorators/feedback_pagination_decorator_spec.rb +++ b/spec/decorators/feedback_pagination_decorator_spec.rb @@ -2,9 +2,10 @@ RSpec.describe FeedbackPaginationDecorator do subject(:decorator) do - described_class.new(content) + described_class.new(content, user) end + let(:user) { create :user } let(:mod) { Training::Module.by_name(:alpha) } let(:content) { mod.page_by_name('feedback-radiobutton') } @@ -13,34 +14,48 @@ end it '#section_numbers' do - expect(decorator.section_numbers).to eq 'Section 3 of 4' + expect(decorator.section_numbers).to eq 'Section 4 of 5' end - it '#page_numbers (should skip the one off question)' do - expect(decorator.page_numbers).to eq 'Page 1 of 8' + it '#page_numbers' do + expect(decorator.page_numbers).to eq 'Page 1 of 9' end it '#percentage' do - expect(decorator.percentage).to eq '13%' + expect(decorator.percentage).to eq '11%' end - describe 'skippable questions' do - let(:content) { mod.page_by_name('feedback-freetext') } + context 'when one-off questions have already been answered' do + before do + create :response, + question_name: 'feedback-oneoffquestion', + training_module: 'bravo', + answers: [1], + correct: true, + user: user, + question_type: 'feedback' + end - context 'when answered' do - before do - create(:response, question_name: content.name, text_input: 'text input') - end + it '#page_numbers' do + expect(decorator.page_numbers).to eq 'Page 1 of 8' + end + end - it '#page_numbers' do - expect(decorator.page_numbers).to eq 'Page 3 of 8' - end + context 'when one-off questions are being answered' do + let(:content) { mod.page_by_name('feedback-oneoffquestion') } + + before do + create :response, + question_name: 'feedback-oneoffquestion', + training_module: 'alpha', + answers: [1], + correct: true, + user: user, + question_type: 'feedback' end - context 'when unanswered' do - it '#page_numbers' do - expect(decorator.page_numbers).to eq 'Page 3 of 8' - end + it '#page_numbers' do + expect(decorator.page_numbers).to eq 'Page 8 of 9' end end end diff --git a/spec/decorators/module_debug_decorator_spec.rb b/spec/decorators/module_debug_decorator_spec.rb index 643215a21..d26c14842 100644 --- a/spec/decorators/module_debug_decorator_spec.rb +++ b/spec/decorators/module_debug_decorator_spec.rb @@ -10,9 +10,24 @@ describe '#rows' do let(:output) { decorator.rows } - xit do - expect(output).to eq [ - [], + it 'has heading' do + expect(output[0]).to eq %w[ + Position Visited Sections Progress Submodule Topic Pages Model Type Name + ] + end + + it 'has rows' do + expect(output[1]).to eq [ + '1st', + 'false', + 'Section 1 of 5', + '25%', + '1', + '0', + 'Page 1 of 4', + 'page', + 'sub_module_intro', + '1-1', ] end end diff --git a/spec/factories/event.rb b/spec/factories/event.rb index d317d8c29..bcb508ac3 100644 --- a/spec/factories/event.rb +++ b/spec/factories/event.rb @@ -5,10 +5,6 @@ properties { {} } time { Time.zone.now } - trait :feedback_complete do - name { 'feedback_complete' } - end - # FIXME: event.user != visit.user association :visit, factory: :visit end diff --git a/spec/lib/content_test_schema_spec.rb b/spec/lib/content_test_schema_spec.rb index 7677bf52d..bf264540f 100644 --- a/spec/lib/content_test_schema_spec.rb +++ b/spec/lib/content_test_schema_spec.rb @@ -6,15 +6,17 @@ let(:alpha) { Training::Module.by_name(:alpha) } let(:ast) do - if Rails.application.migrated_answers? - YAML.load_file(Rails.root.join("spec/support/ast/#{fixture}-response.yml")) - else - YAML.load_file(Rails.root.join("spec/support/ast/#{fixture}.yml")) - end + YAML.load_file(Rails.root.join("spec/support/ast/alpha-#{fixture}.yml")) end context 'when pass is true' do - let(:fixture) { 'alpha-pass-feedback-form' } + let(:fixture) do + if Rails.application.migrated_answers? + 'pass-response-with-feedback' + else + 'pass' + end + end specify do expect(schema.call).to eq ast @@ -22,7 +24,13 @@ end context 'when pass is false' do - let(:fixture) { 'alpha-fail-feedback-form' } + let(:fixture) do + if Rails.application.migrated_answers? + 'fail-response' + else + 'fail' + end + end specify do expect(schema.call(pass: false).compact).to eq ast diff --git a/spec/lib/fill_page_views_spec.rb b/spec/lib/fill_page_views_spec.rb deleted file mode 100644 index fe0b6f9ee..000000000 --- a/spec/lib/fill_page_views_spec.rb +++ /dev/null @@ -1,104 +0,0 @@ -require 'rails_helper' -require 'fill_page_views' - -RSpec.describe FillPageViews do - subject(:service) { described_class.new } - - before do - skip 'CMS conversion WIP' - ENV['VERBOSE'] = 'y' - end - - after do - ENV['VERBOSE'] = nil - end - - include_context 'with progress' - - context 'without skipped pages' do - before do - start_module(alpha) - end - - it 'has no effect' do - expect(Event.count).to be 4 - expect { service.call }.to output(/user \[\d+\] module \[\d+\] - not skipped/).to_stdout_from_any_process - expect(Event.count).to be 4 - end - end - - context 'with noncontiguous skipped pages' do - before do - start_second_submodule(alpha) - end - - let(:pages) { %w[1-1-1 1-1-3] } - - it 'records "skipped" page views' do - expect(Event.count).to be 9 - - pages.each do |page| - Event.where_properties(id: page).first.delete - end - - expect(Event.count).to be 7 - - pages.each do |page| - event = Event.where_properties(id: page).first - expect(event).to be_nil - end - - expect { service.call }.to output(/user \[\d+\] module \[\d+\] - \[2\] skipped before page \[1-2\]/).to_stdout_from_any_process - - expect(Event.count).to be 9 - - pages.each do |page| - event = Event.where_properties(id: page).first - expect(event.properties['skipped']).to be true - end - - expect { service.call }.to output(/user \[\d+\] module \[\d+\] - not skipped/).to_stdout_from_any_process - - expect(Event.count).to be 9 - end - end - - context 'with contiguous skipped pages' do - before do - complete_module(alpha) - end - - let(:pages) { %w[1-2 1-2-1 1-2-1-1 1-2-1-2 1-2-1-3] } - - it 'records "skipped" page views' do - expect(Event.where(name: 'module_start').count).to be 1 - expect(Event.where(name: 'module_content_page').count).to be 26 - expect(Event.where(name: 'module_complete').count).to be 1 - - pages.each do |page| - Event.where_properties(id: page).first.delete - end - - expect(Event.count).to be 23 - - pages.each do |page| - event = Event.where_properties(id: page).first - expect(event).to be_nil - end - - # certificate page - expect { service.call }.to output(/user \[\d+\] module \[\d+\] - \[5\] skipped before page \[1-3-4\]/).to_stdout_from_any_process - - expect(Event.count).to be 28 - - pages.each do |page| - event = Event.where_properties(id: page).first - expect(event.properties['skipped']).to be true - end - - expect { service.call }.to output(/user \[\d+\] module \[\d+\] - not skipped/).to_stdout_from_any_process - - expect(Event.count).to be 28 - end - end -end diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index e8037597f..1081f4c70 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 208 + expect(locales.count).to be 210 end it 'dot separated key -> Page::Resource#name' do diff --git a/spec/models/data_analysis/feedback_forms_spec.rb b/spec/models/data_analysis/feedback_forms_spec.rb deleted file mode 100644 index 1031c7dfa..000000000 --- a/spec/models/data_analysis/feedback_forms_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'rails_helper' - -RSpec.describe DataAnalysis::FeedbackForms do - let(:headers) do - [ - 'Module', - 'Total Responses', - 'Signed in Users', - 'Guest Users', - ] - end - - let(:rows) do - [ - { - mod: 'alpha', - total: 2, - signed_in: 2, - guest: 0, - }, - { - mod: 'bravo', - total: 0, - signed_in: 0, - guest: 0, - }, - { - mod: 'charlie', - total: 0, - signed_in: 0, - guest: 0, - }, - { - mod: 'delta', - total: 0, - signed_in: 0, - guest: 0, - }, - { - mod: 'site wide', - total: 2, - signed_in: 1, - guest: 1, - }, - ] - end - - let(:user) { create(:user, :registered) } - - before do - create :event, :feedback_complete, user_id: user.id, properties: { 'training_module_id' => 'alpha' } - create :event, :feedback_complete, user_id: user.id, properties: { 'training_module_id' => 'alpha' } - create :event, :feedback_complete, user_id: user.id, properties: { 'controller' => 'feedback' } - create :event, :feedback_complete, user_id: nil, properties: { 'controller' => 'feedback' } - end - - it_behaves_like 'a data export model' -end diff --git a/spec/models/data_analysis/module_feedback_forms_spec.rb b/spec/models/data_analysis/module_feedback_forms_spec.rb new file mode 100644 index 000000000..ee8b3a380 --- /dev/null +++ b/spec/models/data_analysis/module_feedback_forms_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +RSpec.describe DataAnalysis::ModuleFeedbackForms do + let(:headers) do + %w[ + Module + Started + Completed + ] + end + + let(:rows) do + [ + { + mod: 'First Training Module', + started: 2, + completed: 2, + }, + { + mod: 'Second Training Module', + started: 1, + completed: 1, + }, + { + mod: 'Third Training Module', + started: 1, + completed: 0, + }, + ] + end + + before do + user_1 = create(:user) + create :event, name: 'feedback_start', user: user_1, properties: { 'training_module_id' => 'alpha' } + create :event, name: 'feedback_complete', user: user_1, properties: { 'training_module_id' => 'alpha' } + create :event, name: 'feedback_start', user: user_1, properties: { 'training_module_id' => 'bravo' } + create :event, name: 'feedback_complete', user: user_1, properties: { 'training_module_id' => 'bravo' } + + user_2 = create(:user) + create :event, name: 'feedback_start', user: user_2, properties: { 'training_module_id' => 'alpha' } + create :event, name: 'feedback_complete', user: user_2, properties: { 'training_module_id' => 'alpha' } + + user_3 = create(:user) + create :event, name: 'feedback_start', user: user_3, properties: { 'training_module_id' => 'charlie' } + end + + it_behaves_like 'a data export model' +end diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb index 043e41544..871676fff 100644 --- a/spec/models/guest_spec.rb +++ b/spec/models/guest_spec.rb @@ -18,17 +18,14 @@ describe '#response_for_shared' do before do - create :response, question_name: content.name # , training_module: nil + create :response, question_name: content.name, training_module: 'course' end let(:content) { Course.config.pages.first } - let(:response) { guest.response_for_shared(content, nil) } + let(:response) { guest.response_for_shared(content) } specify do expect(response).to be_a Response end end - - # describe 'completed_main_feedback?' do - # end end diff --git a/spec/models/response_spec.rb b/spec/models/response_spec.rb index 1552ae425..7874310bc 100644 --- a/spec/models/response_spec.rb +++ b/spec/models/response_spec.rb @@ -1,39 +1,79 @@ require 'rails_helper' RSpec.describe Response, type: :model do - subject(:response) { user.response_for(question) } + let(:user) { create :user } before do skip unless Rails.application.migrated_answers? - response.update!(answers: [1], correct: true) end - let(:user) { create :user } + describe 'feedback validations' do + let(:params) do + { + training_module: 'alpha', + correct: true, + user: user, + question_type: 'feedback', + } + end - let(:headers) do - %w[ - id - user_id - training_module - question_name - answers - correct - created_at - updated_at - question_type - assessment_id - text_input - visit_id - ] - end + # validate answers array + describe '#answers' do + subject(:response) do + build :response, + **params, + question_name: 'feedback-oneoffquestion', + answers: [1] + end - let(:rows) do - [response] - end + specify { expect(response).to be_valid } + end + + # validate answers array unless + # - question options are empty + # - question hint empty + # + describe '#text_input_extra?' do + subject(:response) do + build :response, + **params, + question_name: 'feedback-freetext', + answers: [], + text_input: nil + end - let(:question) do - Training::Module.by_name('alpha').page_by_name('1-1-4-1') + specify { expect(response).to be_valid } + end end - it_behaves_like 'a data export model' + describe 'ToCsv' do + subject(:response) { user.response_for(question) } + + let(:question) do + Training::Module.by_name('alpha').page_by_name('1-1-4-1') + end + let(:headers) do + %w[ + id + user_id + training_module + question_name + answers + correct + created_at + updated_at + question_type + assessment_id + text_input + visit_id + ] + end + let(:rows) { [response] } + + before do + response.update!(answers: [1], correct: true) + end + + it_behaves_like 'a data export model' + end end diff --git a/spec/services/dashboard_spec.rb b/spec/services/dashboard_spec.rb index 5e8d33893..098e77138 100644 --- a/spec/services/dashboard_spec.rb +++ b/spec/services/dashboard_spec.rb @@ -17,8 +17,8 @@ describe '#call' do let(:data_files) { Dir.glob path.join('*/*/*.csv') } - it 'exports 7 database tables' do - expect(data_files.count).to be 21 + it 'exports database tables' do + expect(data_files.count).to be 22 end it 'exports data in CSV format' do diff --git a/spec/support/ast/alpha-fail-feedback-form-response.yml b/spec/support/ast/alpha-fail-feedback-form-response.yml deleted file mode 100644 index 214e2d7c3..000000000 --- a/spec/support/ast/alpha-fail-feedback-form-response.yml +++ /dev/null @@ -1,182 +0,0 @@ -# -# Feedback form for end of module -# ---- -- :path: /modules/alpha/content-pages/what-to-expect - :text: What to expect during the training - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/1-1 - :text: The first submodule - :inputs: - - - :click_on - - Start section -- :path: /modules/alpha/content-pages/1-1-1 - :text: 1-1-1 - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/1-1-2 - :text: 1-1-2 - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/1-1-3 - :text: 1-1-3 - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/1-1-3-1 - :text: 1-1-3-1 - :inputs: - - - :input_text - - note-body-field - - hello world - - - :click_on - - Save and continue -- :path: /modules/alpha/content-pages/1-1-4 - :text: 1-1-4 - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/questionnaires/1-1-4-1 - :text: Question One - Select from following - :inputs: - - - :choose - - response-answers-2-field - - - :click_on - - Next - - - :click_on - - Next -- :path: /modules/alpha/content-pages/1-2 - :text: The second submodule - :inputs: - - - :click_on - - Start section -- :path: /modules/alpha/content-pages/1-2-1 - :text: 1-2-1 - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/questionnaires/1-2-1-1 - :text: Question Two - Select from following - :inputs: - - - :check - - response-answers-2-field - - - :click_on - - Next - - - :click_on - - Next -- :path: /modules/alpha/content-pages/1-2-1-2 - :text: 1-2-1-2 - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/questionnaires/1-2-1-3 - :text: Question Three - Select from following - :inputs: - - - :check - - response-answers-2-field - - - :check - - response-answers-4-field - - - :click_on - - Next - - - :click_on - - Next -- :path: /modules/alpha/content-pages/1-3 - :text: Summary and next steps - :inputs: - - - :click_on - - Start section -- :path: /modules/alpha/content-pages/1-3-1 - :text: Recap - :inputs: - - - :click_on - - Next -- :path: /modules/alpha/content-pages/1-3-2 - :text: End of module test - :inputs: - - - :click_on - - Start test -- :path: /modules/alpha/questionnaires/1-3-2-1 - :text: Question One - Select from following - :inputs: - - - :check - - response-answers-2-field - - - :check - - response-answers-4-field - - - :click_on - - Save and continue -- :path: /modules/alpha/questionnaires/1-3-2-2 - :text: Question Two - Select from following - :inputs: - - - :check - - response-answers-1-field - - - :check - - response-answers-4-field - - - :click_on - - Save and continue -- :path: /modules/alpha/questionnaires/1-3-2-3 - :text: Question Three - Select from following - :inputs: - - - :check - - response-answers-1-field - - - :check - - response-answers-2-field - - - :click_on - - Save and continue -- :path: /modules/alpha/questionnaires/1-3-2-4 - :text: Question Four - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Save and continue -- :path: /modules/alpha/questionnaires/1-3-2-5 - :text: Question Five - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Save and continue -- :path: /modules/alpha/questionnaires/1-3-2-6 - :text: Question Six - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Save and continue -- :path: /modules/alpha/questionnaires/1-3-2-7 - :text: Question Seven - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Save and continue -- :path: /modules/alpha/questionnaires/1-3-2-8 - :text: Question Eight - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Save and continue -- :path: /modules/alpha/questionnaires/1-3-2-9 - :text: Question Nine - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Save and continue -- :path: /modules/alpha/questionnaires/1-3-2-10 - :text: Question Ten - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Finish test -- :path: /modules/alpha/assessment-result/1-3-2-11 - :text: Assessment results - :inputs: - - - :click_on - - Retake test \ No newline at end of file diff --git a/spec/support/ast/alpha-pass-response.yml b/spec/support/ast/alpha-pass-response-skip-feedback.yml similarity index 98% rename from spec/support/ast/alpha-pass-response.yml rename to spec/support/ast/alpha-pass-response-skip-feedback.yml index 8fb7b47cd..cd16d4db2 100644 --- a/spec/support/ast/alpha-pass-response.yml +++ b/spec/support/ast/alpha-pass-response-skip-feedback.yml @@ -1,4 +1,4 @@ -# +# Answer all factual questions correctly and skip feedback # @see ContentTestSchema # --- diff --git a/spec/support/ast/alpha-pass-feedback-form-response.yml b/spec/support/ast/alpha-pass-response-with-feedback.yml similarity index 100% rename from spec/support/ast/alpha-pass-feedback-form-response.yml rename to spec/support/ast/alpha-pass-response-with-feedback.yml diff --git a/spec/support/ast/site-feedback-form-response-user.yml b/spec/support/ast/course-feedback.yml similarity index 87% rename from spec/support/ast/site-feedback-form-response-user.yml rename to spec/support/ast/course-feedback.yml index 610dcda97..536fc940c 100644 --- a/spec/support/ast/site-feedback-form-response-user.yml +++ b/spec/support/ast/course-feedback.yml @@ -1,6 +1,3 @@ -# -# Site wide feedback form - authenticated user -# --- - :path: /feedback :text: Additional feedback @@ -8,7 +5,7 @@ - - :click_on - Next - :path: /feedback/feedback-radiobutton - :text: 'Feedback question 1 - Select from following' + :text: Feedback question 1 - Select from following :inputs: - - :choose - response-answers-1-field @@ -57,6 +54,7 @@ - response-answers-1-field - - :click_on - Next +# skippable -------------------------------------------------------------------- - :path: /feedback/feedback-oneoffquestion :text: Feedback question 8 - Select from following :inputs: @@ -64,8 +62,4 @@ - response-answers-1-field - - :click_on - Next -- :path: /feedback/thank-you - :text: Thank you - :inputs: - - - :click_on - - Go to my modules \ No newline at end of file +# skippable -------------------------------------------------------------------- diff --git a/spec/support/ast/site-feedback-form-response-guest.yml b/spec/support/ast/site-feedback-form-response-guest.yml deleted file mode 100644 index c9d232e36..000000000 --- a/spec/support/ast/site-feedback-form-response-guest.yml +++ /dev/null @@ -1,64 +0,0 @@ -# -# Site wide feedback form -# ---- -- :path: /feedback - :text: Additional feedback - :inputs: - - - :click_on - - Next -- :path: /feedback/feedback-radiobutton - :text: 'Feedback question 1 - Select from following' - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-yesnoandtext - :text: Feedback question 2 - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-freetext - :text: Feedback question 3 - Complete the following - :inputs: - - - :input_text - - response-text-input-field - - hello world - - - :click_on - - Next -- :path: /feedback/feedback-radio-otherandtext - :text: Feedback question 4 - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-radio-and-freetext - :text: Feedback question 5 - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-checkbox-othertextandor - :text: Feedback question 6 - Select from following - :inputs: - - - :check - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-checkbox-otherandtext - :text: Feedback question 7 - Select from following - :inputs: - - - :check - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/thank-you - :text: Thank you - :inputs: - - - :click_on - - Go to home \ No newline at end of file diff --git a/spec/support/ast/site-feedback-form-response-user-update.yml b/spec/support/ast/site-feedback-form-response-user-update.yml deleted file mode 100644 index 20b5bbd1e..000000000 --- a/spec/support/ast/site-feedback-form-response-user-update.yml +++ /dev/null @@ -1,136 +0,0 @@ -# -# Site wide feedback form - authenticated user -# ---- -- :path: /feedback - :text: Additional feedback - :inputs: - - - :click_on - - Next -- :path: /feedback/feedback-radiobutton - :text: 'Feedback question 1 - Select from following' - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-yesnoandtext - :text: Feedback question 2 - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-freetext - :text: Feedback question 3 - Complete the following - :inputs: - - - :input_text - - response-text-input-field - - hello world - - - :click_on - - Next -- :path: /feedback/feedback-radio-otherandtext - :text: Feedback question 4 - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-radio-and-freetext - :text: Feedback question 5 - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-checkbox-othertextandor - :text: Feedback question 6 - Select from following - :inputs: - - - :check - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-checkbox-otherandtext - :text: Feedback question 7 - Select from following - :inputs: - - - :check - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-oneoffquestion - :text: Feedback question 8 - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/thank-you - :text: Thank you - :inputs: - - - :click_on - - Go to my modules -- :path: /my-modules - :text: My modules - :inputs: - - - :click_on - - feedback -- :path: /feedback - :text: Additional feedback - :inputs: - - - :click_on - - Update my feedback -- :path: /feedback/feedback-radiobutton - :text: 'Feedback question 1 - Select from following' - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-yesnoandtext - :text: Feedback question 2 - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-freetext - :text: Feedback question 3 - Complete the following - :inputs: - - - :input_text - - response-text-input-field - - hello world - - - :click_on - - Next -- :path: /feedback/feedback-radio-otherandtext - :text: Feedback question 4 - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-radio-and-freetext - :text: Feedback question 5 - Select from following - :inputs: - - - :choose - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-checkbox-othertextandor - :text: Feedback question 6 - Select from following - :inputs: - - - :check - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/feedback-checkbox-otherandtext - :text: Feedback question 7 - Select from following - :inputs: - - - :check - - response-answers-1-field - - - :click_on - - Next -- :path: /feedback/thank-you - :text: Thank you - :inputs: - - - :click_on - - Go to my modules \ No newline at end of file diff --git a/spec/system/course_feedback_spec.rb b/spec/system/course_feedback_spec.rb new file mode 100644 index 000000000..54b8185c9 --- /dev/null +++ b/spec/system/course_feedback_spec.rb @@ -0,0 +1,45 @@ +require 'rails_helper' + +describe 'Course feedback' do + context 'with unauthenticated user' do + include_context 'with automated path' + let(:fixture) { 'spec/support/ast/course-feedback.yml' } + + it 'returns to homepage once completed' do + expect(page).to have_current_path '/feedback/thank-you' + expect(page).to have_content 'Thank you' + click_on 'Go to home' + expect(page).to have_current_path '/' + end + + context 'when already completed' do + it 'can be updated' do + visit '/feedback' + click_on 'Update my feedback' + expect(page).to have_current_path '/feedback/feedback-radiobutton' + end + end + end + + context 'with authenticated user' do + include_context 'with user' + include_context 'with automated path' + let(:fixture) { 'spec/support/ast/course-feedback.yml' } + + it 'returns to modules page once completed' do + expect(page).to have_current_path '/feedback/thank-you' + expect(page).to have_content 'Thank you' + click_on 'Go to my modules' + expect(page).to have_current_path '/my-modules' + expect(page).to have_content 'My modules' + end + + context 'when already completed' do + it 'can be updated' do + visit '/feedback' + click_on 'Update my feedback' + expect(page).to have_current_path '/feedback/feedback-radiobutton' + end + end + end +end diff --git a/spec/system/event_log_spec.rb b/spec/system/event_log_spec.rb index e1007f903..1aee2015a 100644 --- a/spec/system/event_log_spec.rb +++ b/spec/system/event_log_spec.rb @@ -4,7 +4,7 @@ include_context 'with events' include_context 'with user' include_context 'with automated path' - let(:fixture) { 'spec/support/ast/alpha-pass-response.yml' } + let(:fixture) { 'spec/support/ast/alpha-pass-response-skip-feedback.yml' } describe 'confidence check' do context 'when viewing the first question' do diff --git a/spec/system/module_feedback_spec.rb b/spec/system/module_feedback_spec.rb new file mode 100644 index 000000000..040fe70c6 --- /dev/null +++ b/spec/system/module_feedback_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +RSpec.describe 'Module feedback' do + include_context 'with progress' + include_context 'with user' + + before do + visit '/modules/alpha/questionnaires/feedback-radiobutton' + end + + it 'validates answers' do + click_on 'Next' + expect(page).to have_content 'Please select an answer' + end + + describe 'one-off questions' do + context 'when not already answered' do + it 'pagination counts the question' do + expect(page).to have_content 'Page 1 of 9' + visit '/modules/alpha/questionnaires/feedback-oneoffquestion' + expect(page).to have_content 'Page 8 of 9' + end + end + + context 'when already answered' do + before do + create :response, + question_name: 'feedback-oneoffquestion', + training_module: 'bravo', + answers: [1], + correct: true, + user: user, + question_type: 'feedback' + end + + # FeedbackPaginationDecorator#page_total + it 'pagination does not count the question' do + visit '/modules/alpha/questionnaires/feedback-radiobutton' + expect(page).to have_content 'Page 1 of 8' + end + + # Training::ResponsesController#redirect + it 'skips the question' do + visit '/modules/alpha/questionnaires/feedback-checkbox-otherandtext' + expect(page).to have_content 'Page 7 of 8' + check 'response-answers-1-field' + click_on 'Next' + expect(page).to have_content 'Page 8 of 8' + expect(page).to have_current_path '/modules/alpha/content-pages/1-3-3-5' + end + end + end +end diff --git a/spec/system/module_overview_content_spec.rb b/spec/system/module_overview_content_spec.rb index 5dec0d0b7..dee7e6991 100644 --- a/spec/system/module_overview_content_spec.rb +++ b/spec/system/module_overview_content_spec.rb @@ -9,19 +9,19 @@ end it 'has back button' do - expect(page).to have_link('Back to My modules', href: '/my-modules') + expect(page).to have_link 'Back to My modules', href: '/my-modules' end it 'has the module number and name' do - expect(page).to have_content('Module 1: First Training Module') + expect(page).to have_content 'Module 1: First Training Module' end it 'has the module description' do - expect(page).to have_content('first module description') + expect(page).to have_content 'first module description' end it 'has a call to action button to start the module' do - expect(page).to have_link('Start module') + expect(page).to have_link 'Start module' end it 'has the section headings' do @@ -31,7 +31,8 @@ end it 'hides feedback section' do - expect(page).not_to have_content('Additional feedback') + expect(page).to have_content 'Reflect on your learning' + expect(page).not_to have_content 'Additional feedback' end it 'has the topic names' do diff --git a/spec/system/my_learning_spec.rb b/spec/system/my_learning_spec.rb index 5acc07787..3a596c2e0 100644 --- a/spec/system/my_learning_spec.rb +++ b/spec/system/my_learning_spec.rb @@ -71,7 +71,7 @@ expect(page).not_to have_text 'You have not started any modules.' expect(page).to have_text 'You have read 1 pages' # FIXME: plurality - expect(page).to have_text 'Your progress: 3%' + expect(page).to have_text 'Your progress: 2%' end end end diff --git a/spec/system/opinion_spec.rb b/spec/system/opinion_spec.rb deleted file mode 100644 index 9efe58bdb..000000000 --- a/spec/system/opinion_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'End of module feedback form' do - include_context 'with progress' - include_context 'with user' - - let(:first_question_path) { '/modules/alpha/questionnaires/feedback-radiobutton' } - let(:second_question_path) { '/modules/alpha/questionnaires/feedback-yesnoandtext' } - let(:intro_path) { '/modules/alpha/content-pages/feedback-intro' } - - it 'shows feedback question' do - visit intro_path - expect(page).to have_content('Additional feedback') - click_on 'Give feedback' - expect(page).to have_content('Feedback question 1 - Select from following') - expect(page).to have_content('Strongly agree') - end - - it do - visit second_question_path - expect(page).to have_content('Feedback question 2 - Select from following') - expect(page).to have_content('Yes') - end - - context 'when no answer is submitted' do - it 'displays an error message' do - visit first_question_path - click_on 'Next' - expect(page).to have_content 'Please select an answer' - end - end - - it 'does not link to additional feedback from the module overview page' do - visit '/modules/alpha' - - expect(page).to have_content 'Reflect on your learning' - expect(page).not_to have_link 'Additional feedback' - end - - describe 'skippable questions' do - context 'when not skipped' do - it 'pagination shows all pages' do - visit first_question_path - expect(page).to have_content 'Page 1 of 8' - end - end - - context 'when skipped' do - before do - create :response, - question_name: 'feedback-oneoffquestion', - training_module: 'alpha', - answers: [1], - correct: true, - user: user, - question_type: 'feedback' - end - - it 'pagination does not show skipped pages' do - visit first_question_path - expect(page).to have_content 'Page 1 of 7' - end - end - end -end diff --git a/spec/system/site_wide_feedback_spec.rb b/spec/system/site_wide_feedback_spec.rb deleted file mode 100644 index e106293d6..000000000 --- a/spec/system/site_wide_feedback_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'rails_helper' - -describe 'Site wide feedback' do - context 'when user is not logged in' do - include_context 'with automated path' - let(:fixture) { 'spec/support/ast/site-feedback-form-response-guest.yml' } - - it 'feedback can be completed' do - expect(page).to have_title('Early years child development training : Home page') - end - end - - context 'with authenticated user' do - include_context 'with user' - include_context 'with automated path' - - context 'when user has not completed feedback' do - - let(:fixture) { 'spec/support/ast/site-feedback-form-response-user.yml' } - - it 'feedback can be completed' do - expect(page).to have_content('My modules') - end - end - - context 'when user has completed feedback' do - - let(:fixture) { 'spec/support/ast/site-feedback-form-response-user-update.yml' } - - it 'feedback can be updated' do - expect(page).to have_content('My modules') - end - end - end -end diff --git a/spec/system/summative_assessment_spec.rb b/spec/system/summative_assessment_spec.rb index ed165d59a..45a1ce481 100644 --- a/spec/system/summative_assessment_spec.rb +++ b/spec/system/summative_assessment_spec.rb @@ -4,7 +4,7 @@ include_context 'with progress' include_context 'with user' - let(:fixture) { 'spec/support/ast/alpha-pass-response.yml' } + let(:fixture) { 'spec/support/ast/alpha-pass-response-skip-feedback.yml' } let(:first_question_path) { '/modules/alpha/questionnaires/1-3-2-1' } before do @@ -59,7 +59,7 @@ let(:fixture) do if Rails.application.migrated_answers? - 'spec/support/ast/alpha-pass-response.yml' + 'spec/support/ast/alpha-pass-response-skip-feedback.yml' else 'spec/support/ast/alpha-pass.yml' end From ceab0796f575cd32dc47b0ae5572d67e297bcbe5 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 15 Apr 2024 12:46:33 +0100 Subject: [PATCH 57/95] Refactor and prepare for manual tests --- app/assets/stylesheets/application.scss | 18 ++-- app/controllers/application_controller.rb | 3 +- app/controllers/feedback_controller.rb | 8 +- app/decorators/next_page_decorator.rb | 1 + app/decorators/previous_page_decorator.rb | 6 -- app/forms/form_builder.rb | 9 +- app/models/guest.rb | 8 +- app/models/response.rb | 14 ++- app/models/training/question.rb | 39 ++++---- app/models/user.rb | 2 +- app/views/feedback/_check_boxes.html.slim | 5 +- app/views/feedback/_cta.html.slim | 7 ++ app/views/feedback/_radio_buttons.html.slim | 9 +- app/views/feedback/_text_area.html.slim | 8 +- app/views/feedback/index.html.slim | 21 +++-- app/views/feedback/thank_you.html.slim | 4 +- app/views/home/index.html.slim | 4 + app/views/layouts/application.html.slim | 1 + app/views/layouts/hero.html.slim | 1 + app/views/learning/show.html.slim | 3 + .../training/pages/certificate.html.slim | 3 + app/views/training/questions/_debug.html.slim | 14 +-- app/views/user/show.html.slim | 3 + cms/CONTENTFUL.md | 37 ++++++++ cms/migrate/02-create-question.js | 29 ++++-- config/locales/en.yml | 42 ++++++--- lib/content_test_schema.rb | 2 +- .../application_controller_spec.rb | 27 ++++++ spec/controllers/feedback_controller_spec.rb | 31 +++++-- .../training/responses_controller_spec.rb | 2 +- .../feedback_pagination_decorator_spec.rb | 8 +- spec/decorators/pagination_decorator_spec.rb | 2 +- .../previous_page_decorator_spec.rb | 14 +-- spec/lib/seed_snippets_spec.rb | 2 +- spec/models/course_spec.rb | 8 +- spec/models/page_spec.rb | 2 +- spec/models/response_feedback_spec.rb | 45 +++++++++ spec/models/response_spec.rb | 93 +++++-------------- spec/models/training/module_spec.rb | 2 +- spec/models/training/question_spec.rb | 6 +- spec/models/training/response_spec.rb | 2 +- spec/requests/static_spec.rb | 2 - spec/support/ast/alpha-fail-response.yml | 2 +- spec/support/ast/alpha-fail.yml | 2 +- .../ast/alpha-pass-response-with-feedback.yml | 39 ++++---- spec/support/ast/alpha-pass.yml | 2 +- spec/support/ast/bravo-fail.yml | 3 + spec/support/ast/course-feedback.yml | 65 ++++++++----- spec/system/course_feedback_spec.rb | 59 +++++++++++- spec/system/module_feedback_spec.rb | 10 +- spec/system/page_title_spec.rb | 1 - 51 files changed, 469 insertions(+), 261 deletions(-) create mode 100644 app/views/feedback/_cta.html.slim create mode 100644 spec/controllers/application_controller_spec.rb create mode 100644 spec/models/response_feedback_spec.rb diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 7e76c3330..c577b19b7 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -117,16 +117,22 @@ pre.code-tip { } } -// -------------------------------------------- +#feedback-cta { + padding: govuk-spacing(5); + background-color: $department-for-education-websafe; -.app-sidebar { - padding: govuk-spacing(5) govuk-spacing(3); - background-color: govuk-colour('light-grey'); + * { + color: govuk-colour('white'); + } - *:last-child { - margin: 0; + .govuk-button { + background-color: $department-for-education-websafe; + border-color: govuk-colour('white'); + box-shadow: none; } } +// -------------------------------------------- + .govuk-label { font-weight: normal; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1574e811b..fc3f30218 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -91,7 +91,8 @@ def current_user # @return [Guest, nil] def guest - Guest.new(visit: current_visit) if current_visit.present? + visit = Visit.find_by(visit_token: cookies[:course_feedback_started]) || current_visit + Guest.new(visit: visit) if visit.present? end # @see Auditing diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 09a0850b2..058cf3dcc 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -40,7 +40,11 @@ def redirect if content.last_feedback? redirect_to feedback_path('thank-you') elsif skip_next_question? - redirect_to feedback_path(content.next_item.next_item.name) + if content.next_item.last_feedback? + redirect_to feedback_path('thank-you') + else + redirect_to feedback_path(content.next_item.next_item.name) + end else redirect_to feedback_path(content.next_item.name) end @@ -99,7 +103,7 @@ def user_answers # @param state [Symbol, String] # @return [Hash] def feedback_cookie(state) - cookies["course_feedback_#{state}"] = { value: current_user.cookie_token } + cookies["course_feedback_#{state}"] = { value: current_user.visit_token } end def track_feedback_start diff --git a/app/decorators/next_page_decorator.rb b/app/decorators/next_page_decorator.rb index c29c85c45..bb22711e8 100644 --- a/app/decorators/next_page_decorator.rb +++ b/app/decorators/next_page_decorator.rb @@ -117,6 +117,7 @@ def wip? Rails.application.preview? || Rails.env.test? end + # @note only used if a skippable question follows a non-question # @return [Boolean] def skip_next_question? user.skip_question?(content.next_item) diff --git a/app/decorators/previous_page_decorator.rb b/app/decorators/previous_page_decorator.rb index 78e5d572c..34d078312 100644 --- a/app/decorators/previous_page_decorator.rb +++ b/app/decorators/previous_page_decorator.rb @@ -61,12 +61,6 @@ def skip_previous_question? content.previous_item.skippable? && answered?(content.previous_item) end - # on the post-feedback page with the last feedback question unanswered - # - # - once a feedback question is answered the feedback form is started - # - a response for the last question is therefore sufficient to determine this - # - because you can't get here without answering the last question - # # @return [Boolean] def feedback_not_started? content.thankyou? && !answered?(content.previous_item) diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index f7673245b..f46f539af 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -38,15 +38,20 @@ def question_radio_button(option) # @param option [Training::Answer::Option] # @option text [String] + # @option more [Boolean] # @return [String] - def other_radio_button(option, text:) + def other_radio_button(option, text:, more:) govuk_radio_button :answers, option.id, label: { text: option.label }, link_errors: true, disabled: option.disabled?, checked: option.checked? do - govuk_text_field :text_input, label: { text: text } + if more + govuk_text_area :text_input, label: { text: text } + else + govuk_text_field :text_input, label: { text: text } + end end end diff --git a/app/models/guest.rb b/app/models/guest.rb index acbe916f2..c0f882642 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -6,6 +6,9 @@ class Guest < Dry::Struct # @return [Visit] attribute :visit, Types.Instance(Visit) + # @return [String] + delegate :visit_token, to: :visit + # @return [Boolean] def guest? true @@ -29,11 +32,6 @@ def response_for_shared(content, mod = Course.config) ) end - # @return [String] - def cookie_token - visit.visit_token - end - private # @return [Response::ActiveRecord_Relation] diff --git a/app/models/response.rb b/app/models/response.rb index f759448c7..f6406229e 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -59,17 +59,16 @@ def options # @return [Boolean] def checked_other? question.has_other? && answers.include?(question.options.last.id) - # question.has_other? && question.options.last.checked? end - # @see #options - # Additional "Or" option is given index zero + # @see Question#options # @return [Boolean] def checked_or? - answers.include?(0) - # answers.pop.zero? + question.has_or? && answers.include?(0) end + # Validate that an option is selected unless question only expects text. + # # @return [Boolean] def text_input_only? question.only_text? @@ -78,6 +77,11 @@ def text_input_only? # @return [Boolean] def text_input_extra? question.and_text? && checked_other? + + # was previously + # better in manual tests but breaks spec/system/course_feedback_spec.rb + # fails at feedback-checkbox-other-or + # question.and_text? || checked_other? end # @return [Boolean] diff --git a/app/models/training/question.rb b/app/models/training/question.rb index 5aa5bc652..28277fe92 100644 --- a/app/models/training/question.rb +++ b/app/models/training/question.rb @@ -27,10 +27,15 @@ def to_partial_path end end + # Factual questions: dynamic based on available correct options + # Opinion questions: + # - Feedback (default: false) + # - Confidence (always: false) + # # @return [Boolean] def multi_select? if feedback_question? - multi_select + !!multi_select elsif confidence_question? false else @@ -38,29 +43,21 @@ def multi_select? end end - # @return [Boolean] - def skippable? - feedback_question? && skippable - end - - # @return [Boolean] - def no_options? - feedback_question? && options.empty? - end - - # @return [Boolean] + # @return [Boolean] textarea by itself no validations def only_text? - no_options? && !has_hint? + options.empty? && has_more? end - # @return [Boolean] + # @return [Boolean] "Could you give use reasons for your answer" def and_text? - !no_options? && (has_hint? || has_or?) + options.present? && !multi_select? && has_more? end + # Turns the last "other" option input field into a textarea + # # @return [Boolean] - def has_hint? - feedback_question? && hint.present? + def has_more? + feedback_question? && more.present? end # @return [Boolean] @@ -68,14 +65,16 @@ def has_other? feedback_question? && other.present? end + # Additional "Or" option is appended and given index zero + # # @return [Boolean] def has_or? feedback_question? && self.or.present? end - # @return [Boolean] - def checkbox? - feedback_question? && response_type + # @return [Boolean] default: false + def skippable? + feedback_question? && !!skippable end # @return [Boolean] event tracking diff --git a/app/models/user.rb b/app/models/user.rb index a5faea6d4..917ccf310 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -457,7 +457,7 @@ def completed_course_feedback? end # @return [String] - def cookie_token + def visit_token visits.last.visit_token end diff --git a/app/views/feedback/_check_boxes.html.slim b/app/views/feedback/_check_boxes.html.slim index 98361acdb..6b0629d13 100644 --- a/app/views/feedback/_check_boxes.html.slim +++ b/app/views/feedback/_check_boxes.html.slim @@ -11,5 +11,6 @@ = f.govuk_radio_divider 'Or' = f.or_checkbox_button(text: content.or, checked: response.checked_or?) - - if content.has_hint? - = f.govuk_text_area :text_input, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } + + - if content.has_more? + = f.govuk_text_area :text_input, label: { text: t('feedback.reasons'), class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } diff --git a/app/views/feedback/_cta.html.slim b/app/views/feedback/_cta.html.slim new file mode 100644 index 000000000..b5f7bd8cd --- /dev/null +++ b/app/views/feedback/_cta.html.slim @@ -0,0 +1,7 @@ +#feedback-cta + .dfe-width-container + .govuk-grid-row + .govuk-grid-column-full + = m('feedback.cta') + + = govuk_link_to 'Provide feedback', feedback_index_path, class: 'govuk-button' diff --git a/app/views/feedback/_radio_buttons.html.slim b/app/views/feedback/_radio_buttons.html.slim index ca015d16d..f4410cf58 100644 --- a/app/views/feedback/_radio_buttons.html.slim +++ b/app/views/feedback/_radio_buttons.html.slim @@ -1,10 +1,13 @@ = f.govuk_radio_buttons_fieldset :answers, legend: { text: response.legend } do = f.hidden_field :answers + - if content.skippable? + p.govuk-hint = t('feedback.research') + - response.options.each do |option| - if content.has_other? && option.last? - = f.other_radio_button(option, text: content.other) + = f.other_radio_button(option, text: content.other, more: content.has_more?) - elsif content.has_or? && option.last? = f.question_radio_button(option) @@ -14,5 +17,5 @@ - else = f.question_radio_button(option) - - if content.has_hint? && !content.skippable? - = f.govuk_text_area :text_input, label: { text: content.hint, class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } + - if content.has_more? && !content.has_other? + = f.govuk_text_area :text_input, label: { text: t('feedback.reasons'), class: 'govuk-!-font-weight-bold govuk-!-margin-top-8 govuk-!-margin-bottom-4' } diff --git a/app/views/feedback/_text_area.html.slim b/app/views/feedback/_text_area.html.slim index c387248fc..401ef5aec 100644 --- a/app/views/feedback/_text_area.html.slim +++ b/app/views/feedback/_text_area.html.slim @@ -1,6 +1,2 @@ -= f.govuk_fieldset legend: { text: m(content.legend) } do - - if content.has_hint? - = m(content.hint) - - = f.govuk_text_area :text_input, label: nil, rows: 9 - \ No newline at end of file += f.govuk_fieldset legend: { text: content.legend } do + = f.govuk_text_area :text_input, label: nil, rows: 9 diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index fd4e9eec1..4c47dbad7 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -1,17 +1,20 @@ .govuk-grid-row .govuk-grid-column-full + - if feedback_complete? = m('feedback.complete') + - else + = m('feedback.intro', contact_us: Rails.application.credentials.contact_us) - .govuk-button-group - / = govuk_button_link_to t('previous_page.previous'), my_modules_path, secondary: true - = govuk_button_link_to t('links.feedback.update'), feedback_path(mod.pages.first.name) + hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' - - else - = m('feedback.intro') + .govuk-button-group + - if feedback_complete? - .govuk-button-group - = govuk_button_link_to t('next_page.next'), feedback_path(mod.pages.first.name) + - unless current_user.guest? + = govuk_button_link_to t('previous_page.previous'), my_modules_path, secondary: true + + = govuk_button_link_to t('links.feedback.update'), feedback_path(mod.pages.first.name) - - + - else + = govuk_button_link_to t('next_page.next'), feedback_path(mod.pages.first.name) diff --git a/app/views/feedback/thank_you.html.slim b/app/views/feedback/thank_you.html.slim index 2ceb86b62..52966c89c 100644 --- a/app/views/feedback/thank_you.html.slim +++ b/app/views/feedback/thank_you.html.slim @@ -1,7 +1,7 @@ .govuk-grid-row .govuk-grid-column-full - h1= t('.heading') - p.govuk-body-m= t('.body') + + = m('feedback.thank_you', headings_start_with: 'xl') hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index 678d133e4..8284bf40a 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -4,6 +4,10 @@ - content_for :hero do = render 'hero' +- content_for :cta do + - if current_user + = render 'feedback/cta' + = render 'learning/cms_debug' = render 'debug' diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index f24c29ac1..1e7bdf459 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -43,4 +43,5 @@ html.govuk-template lang='en' = yield + = yield :cta = render 'layouts/footer' diff --git a/app/views/layouts/hero.html.slim b/app/views/layouts/hero.html.slim index 4d816c024..1cb6dfaf1 100644 --- a/app/views/layouts/hero.html.slim +++ b/app/views/layouts/hero.html.slim @@ -48,4 +48,5 @@ html.govuk-template lang='en' main#main-content.govuk-main-wrapper role='main' = yield + = yield :cta = render 'layouts/footer' diff --git a/app/views/learning/show.html.slim b/app/views/learning/show.html.slim index b54830f61..078ef28dc 100644 --- a/app/views/learning/show.html.slim +++ b/app/views/learning/show.html.slim @@ -8,6 +8,9 @@ | My modules p You can complete the Early years child development training in any order. However, to support your understanding of the training, you may find it helpful to complete the modules in order. +- content_for :cta do + = render 'feedback/cta' + = render 'cms_debug' #started.govuk-grid-row diff --git a/app/views/training/pages/certificate.html.slim b/app/views/training/pages/certificate.html.slim index 015abcad8..8a224d50b 100644 --- a/app/views/training/pages/certificate.html.slim +++ b/app/views/training/pages/certificate.html.slim @@ -16,6 +16,9 @@ =< govuk_link_to 'My account', user_path | . +- content_for :cta do + = render 'feedback/cta' + .govuk-grid-row .govuk-grid-column-three-quarters-from-desktop id=('award' if pdf?) diff --git a/app/views/training/questions/_debug.html.slim b/app/views/training/questions/_debug.html.slim index 35ba5007a..b9dc5e6b8 100644 --- a/app/views/training/questions/_debug.html.slim +++ b/app/views/training/questions/_debug.html.slim @@ -14,17 +14,19 @@ hr | FEEDBACK br - | User type: #{current_user.guest? ? 'guest' : 'authenticated user'} + | Or: #{content.or} br - | Skippable: #{content.skippable?} + | Other: #{content.other} br - | Skip next question: #{current_user.skip_question?(content.next_item)} + | More: #{content.more} br - | Hint: #{content.hint} + | User type: #{current_user.guest? ? 'guest' : 'authenticated user'} br - | Or: #{content.or} + | Cookie: #{current_user.visit_token} br - | Other: #{content.other} + | Skippable: #{content.skippable?} + br + | Skip next question: #{current_user.skip_question?(content.next_item)} br | Only text input: #{content.only_text?} br diff --git a/app/views/user/show.html.slim b/app/views/user/show.html.slim index 5229d042f..6eb36d2ce 100644 --- a/app/views/user/show.html.slim +++ b/app/views/user/show.html.slim @@ -48,3 +48,6 @@ = m('my_account.closing.information') = govuk_button_link_to t('my_account.closing.button'), edit_reason_user_close_account_path + +- content_for :cta do + = render 'feedback/cta' diff --git a/cms/CONTENTFUL.md b/cms/CONTENTFUL.md index 7eb828beb..d0cc638bc 100644 --- a/cms/CONTENTFUL.md +++ b/cms/CONTENTFUL.md @@ -223,3 +223,40 @@ Information for onboarding content editors: - `API`: Application Programming Interface. - `Delivery API`: The mechanism that returns published content. - `Preview API`: The mechanism that returns both published and draft content. + + +## Question types + +Questions come in two flavours, factual questions (divided into either `formative` or `summative`) and opinion questions (divided into either `confidence` or `feedback`). + +### Feedback questions + +Feedback questions use additional model fields to control their increased functionality. + +These questions can: + +- be shared and used across multiple training modules +- have user-defined text in an optional input field or textarea +- replace radio buttons and checkboxes completely with a free text input +- be asked only once + +The different permutations are controlled using these fields: + +- answers [Array] +- or [String] +- other [String] +- more [Boolean] +- skippable [Boolean] +- multi_select [Boolean] + +In order to show a text input when the last option is selected, provide a string in the `other` field. +This string becomes the label text. + +If you want to provide the user with more space to write, also turn on the `more` field. +This changes the text input to a textarea field. + +If the question is a feedback question it will not dynamically use the number of correct answers +to determine if it is a radio button or checkbox. +`multi_select` defaults to off using the radio button template. + +If the question needs to be hidden once it is answered, so it is not answered again in another form, enable `skippable`. diff --git a/cms/migrate/02-create-question.js b/cms/migrate/02-create-question.js index 63ac6b971..6f5bd97e3 100644 --- a/cms/migrate/02-create-question.js +++ b/cms/migrate/02-create-question.js @@ -80,19 +80,26 @@ module.exports = function(migration) { /* Feedback Only ---------------------------------------------------------- */ + // the last option has an additional conditional text input question.createField('other', { name: 'Other', type: 'Text', }) + // an extra option is appended with an index of zero question.createField('or', { name: 'Or', type: 'Text', }) - question.createField('hint', { - name: 'Hint', - type: 'Text', + /* + - increases the other input to a text area + - appends an additional textbox + - replaces options with a textbox + */ + question.createField('more', { + name: 'More', + type: 'Boolean' }) /* @@ -109,11 +116,7 @@ module.exports = function(migration) { question.createField('skippable', { name: 'Skippable', - type: 'Boolean', - required: true, - defaultValue: { - 'en-US': false - } + type: 'Boolean' }) /* Interface -------------------------------------------------------------- */ @@ -141,13 +144,19 @@ module.exports = function(migration) { /* toggle */ question.changeFieldControl('multi_select', 'builtin', 'boolean', { - helpText: 'Select multiple options?', + helpText: 'Select multiple options? (default no)', trueLabel: 'yes', falseLabel: 'no' }) question.changeFieldControl('skippable', 'builtin', 'boolean', { - helpText: 'Hide once answered?', + helpText: 'Hide once answered? (default no)', + trueLabel: 'yes', + falseLabel: 'no' + }) + + question.changeFieldControl('more', 'builtin', 'boolean', { + helpText: 'Allow more user input? (default no)', trueLabel: 'yes', falseLabel: 'no' }) diff --git a/config/locales/en.yml b/config/locales/en.yml index bb92f5e28..a0d9b06e8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -31,6 +31,7 @@ en: blank: Choose an option. # Validations ---------------------------------------------------------------- + # https://guides.rubyonrails.org/i18n.html#translations-for-active-record-models activerecord: errors: @@ -40,6 +41,9 @@ en: answers: blank: Please select an answer. invalid: Please select an option. + text_input: + blank: Tell us more!!! # FIXME + user_answer: attributes: answer: @@ -586,38 +590,46 @@ en: This module covers: %{criteria} - + # /feedback feedback: - # /feedback + cta: | + # Can you help us improve this training further? + + We are constantly looking at ways we can improve this training for you and your colleagues. + We really value your feedback. + It will take no more than 5 minutes to answer all the questions. + intro: | # Give feedback The purpose of this feedback form is to gather your opinion on the child development training course that the Department for Education has created for early years practitioners. - For more information on how you data will be used, please view our privacy notice. ADD LINK + For more information on how you data will be used, please view our + {external}[privacy notice](https://www.gov.uk/government/publications/privacy-information-members-of-the-public/privacy-information-members-of-the-public){/external}. Completing this form is voluntary and you can withdraw your feedback at any time. By completing this form you have understood the above and consent to take part. - # /feedback + ## Technical support queries + + If you have any questions about how to use this website or are experiencing any + technical issues, please {external}[use our contact form](%{contact_us}){/external} so that our team can follow up with your enquiry. complete: | # You have already submitted feedback Thank you for helping to improve this training - - ## Technical support queries - - If you have any questions about how to sue this website or are experiencing any - technical issues, please use our contact form ADD LINK so that our team can follow up with your enquiry. - - --- - # /feedback/thank-you - thank_you: - heading: Thank you - body: Thank you for helping to improve this training + thank_you: | + # Thank you + + Thank you for helping to improve this training + # one-off question + research: | + Participation is entirely voluntary, and you can withdraw at any point before, + during or after the research takes place and you do not need to give a reason. + reasons: Could you give use reasons for your answer? # /gov-one/info gov_one_info: diff --git a/lib/content_test_schema.rb b/lib/content_test_schema.rb index d42f0315b..c02f1ff82 100644 --- a/lib/content_test_schema.rb +++ b/lib/content_test_schema.rb @@ -1,6 +1,6 @@ # # Transform basic Content schema into RSpec actionable AST -# +# Answering all factual questions either correctly or incorrectly # class ContentTestSchema extend Dry::Initializer diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb new file mode 100644 index 000000000..8ead9dfdf --- /dev/null +++ b/spec/controllers/application_controller_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.describe ApplicationController, type: :controller do + + describe '#guest' do + subject(:guest) { controller.send(:guest) } + let(:cookie_token) { 'some-token' } + + it 'is nil without a current_visit' do + expect(guest).to be_nil + end + + it 'is a Guest with a current_visit' do + allow(controller).to receive(:current_visit).and_return(create(:visit)) + expect(guest).to be_a Guest + end + + it 'restores a previous visit from a cookie' do + allow(controller).to receive(:current_visit).and_return(create(:visit)) + create(:visit, visit_token: cookie_token) + request.cookies[:course_feedback_started] = cookie_token + expect(guest).to be_a Guest + expect(guest.visit_token).to eq cookie_token + end + end + +end diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index 45b39fafa..a8600c9ba 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -7,13 +7,17 @@ before { sign_in user } describe 'GET #show' do - it 'tracks feedback complete' do - expect(controller).to receive(:track_feedback_complete) - get :show, params: { id: 'thank-you' } + context 'last page' do + it 'is tracked as complete' do + expect(controller).to receive(:track_feedback_complete) + expect(cookies[:course_feedback_completed]).not_to be_present + get :show, params: { id: 'thank-you' } + expect(cookies[:course_feedback_completed]).to be_present + end end it 'returns a success response' do - get :show, params: { id: 'feedback-radiobutton' } + get :show, params: { id: 'feedback-radio-only' } expect(response).to have_http_status(:success) end end @@ -29,10 +33,9 @@ context 'with valid params' do let(:params) do { - id: 'feedback-radiobutton', + id: 'feedback-radio-only', response: { - answers: %w[Yes], - answers_custom: 'Custom answer', + answers: %w[1], }, } end @@ -46,19 +49,21 @@ it 'redirects to the next question' do post :update, params: params expect(response).to have_http_status(:redirect) - expect(response).to redirect_to feedback_path('feedback-yesnoandtext') + expect(response).to redirect_to feedback_path('feedback-checkbox-only') end - it 'tracks feedback started' do + it 'is tracked as started' do expect(controller).to receive(:track_feedback_start) + expect(cookies[:course_feedback_started]).not_to be_present post :update, params: params + expect(cookies[:course_feedback_started]).to be_present end end context 'with invalid params' do let(:params) do { - id: 'feedback-radiobutton', + id: 'feedback-radio-only', response: { answers: [''], }, @@ -75,6 +80,12 @@ post :update, params: params }.not_to change(Response, :count) end + + it 'is not tracked as started' do + expect(controller).not_to receive(:track_feedback_start) + post :update, params: params + expect(cookies[:course_feedback_started]).not_to be_present + end end end end diff --git a/spec/controllers/training/responses_controller_spec.rb b/spec/controllers/training/responses_controller_spec.rb index bd865c23b..a7df10bf7 100644 --- a/spec/controllers/training/responses_controller_spec.rb +++ b/spec/controllers/training/responses_controller_spec.rb @@ -80,7 +80,7 @@ end context 'when the question expects text and is answered' do - let(:question_name) { 'feedback-freetext' } + let(:question_name) { 'feedback-textarea-only' } let(:answers) { [] } context 'with text input' do diff --git a/spec/decorators/feedback_pagination_decorator_spec.rb b/spec/decorators/feedback_pagination_decorator_spec.rb index eeefb5e34..601453e3f 100644 --- a/spec/decorators/feedback_pagination_decorator_spec.rb +++ b/spec/decorators/feedback_pagination_decorator_spec.rb @@ -7,7 +7,7 @@ let(:user) { create :user } let(:mod) { Training::Module.by_name(:alpha) } - let(:content) { mod.page_by_name('feedback-radiobutton') } + let(:content) { mod.page_by_name('feedback-radio-only') } it '#heading' do expect(decorator.heading).to eq 'Additional feedback' @@ -28,7 +28,7 @@ context 'when one-off questions have already been answered' do before do create :response, - question_name: 'feedback-oneoffquestion', + question_name: 'feedback-skippable', training_module: 'bravo', answers: [1], correct: true, @@ -42,11 +42,11 @@ end context 'when one-off questions are being answered' do - let(:content) { mod.page_by_name('feedback-oneoffquestion') } + let(:content) { mod.page_by_name('feedback-skippable') } before do create :response, - question_name: 'feedback-oneoffquestion', + question_name: 'feedback-skippable', training_module: 'alpha', answers: [1], correct: true, diff --git a/spec/decorators/pagination_decorator_spec.rb b/spec/decorators/pagination_decorator_spec.rb index 9b9306d4e..8a76b5ec3 100644 --- a/spec/decorators/pagination_decorator_spec.rb +++ b/spec/decorators/pagination_decorator_spec.rb @@ -25,7 +25,7 @@ end describe 'skippable questions' do - let(:content) { mod.page_by_name('feedback-freetext') } + let(:content) { mod.page_by_name('feedback-textarea-only') } context 'when answered' do before do diff --git a/spec/decorators/previous_page_decorator_spec.rb b/spec/decorators/previous_page_decorator_spec.rb index e18497b71..4f7aa3c8e 100644 --- a/spec/decorators/previous_page_decorator_spec.rb +++ b/spec/decorators/previous_page_decorator_spec.rb @@ -3,7 +3,7 @@ # feedback-intro (opinion_intro) # end-of-module-feedback-1 (feedback) # end-of-module-feedback-3 (feedback) -# feedback-freetext (feedback) +# feedback-textarea-only (feedback) # end-of-module-feedback-5 (feedback) <-- SKIPPABLE # 1-3-3-5 (thankyou) # @@ -54,20 +54,12 @@ end context 'when previous page is skippable' do - # This context is insufficiently prepared - # - # The assertion here is that a special kind of feedback question is asked - # in every form but once answered is never asked again. - # - # Therefore, as we transition through 'alpha' in this spec, we need a scenario - # where the question was answered in the main feedack form or another module. - # let(:content) { mod.page_by_name('1-3-3-5') } context 'and answered' do before do create :response, - question_name: 'feedback-oneoffquestion', + question_name: 'feedback-skippable', training_module: mod.name, answers: [1], correct: true, @@ -75,7 +67,7 @@ question_type: 'feedback' end - specify { expect(decorator.name).to eq 'feedback-checkbox-otherandtext' } + specify { expect(decorator.name).to eq 'feedback-checkbox-other-or' } end end end diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 1081f4c70..366ff607d 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 210 + expect(locales.count).to be 213 end it 'dot separated key -> Page::Resource#name' do diff --git a/spec/models/course_spec.rb b/spec/models/course_spec.rb index f5187a6d1..244a5cb4d 100644 --- a/spec/models/course_spec.rb +++ b/spec/models/course_spec.rb @@ -39,10 +39,10 @@ end it 'page order uing previous_item/next_item' do - expect(pages.first.name).to eq 'feedback-radiobutton' - expect(pages.first.next_item.name).to eq 'feedback-yesnoandtext' - expect(pages.first.next_item.next_item.name).to eq 'feedback-freetext' - expect(pages.first.next_item.previous_item.name).to eq 'feedback-radiobutton' + expect(pages.first.name).to eq 'feedback-radio-only' + expect(pages.first.next_item.name).to eq 'feedback-checkbox-only' + expect(pages.first.next_item.next_item.name).to eq 'feedback-textarea-only' + expect(pages.first.next_item.previous_item.name).to eq 'feedback-radio-only' end end end diff --git a/spec/models/page_spec.rb b/spec/models/page_spec.rb index 662f21cea..394b1710d 100644 --- a/spec/models/page_spec.rb +++ b/spec/models/page_spec.rb @@ -9,7 +9,7 @@ describe '.footer' do specify do - expect(described_class.footer.count).to be 5 + expect(described_class.footer.count).to be 4 end end diff --git a/spec/models/response_feedback_spec.rb b/spec/models/response_feedback_spec.rb new file mode 100644 index 000000000..2738a2751 --- /dev/null +++ b/spec/models/response_feedback_spec.rb @@ -0,0 +1,45 @@ +require 'rails_helper' + +RSpec.describe Response, '#feedback', type: :model do + let(:user) { create :user } + let(:params) do + { + training_module: 'alpha', + correct: true, + user: user, + question_type: 'feedback', + } + end + + before do + skip unless Rails.application.migrated_answers? + end + + # validate answers array + describe '#answers' do + subject(:response) do + build :response, + **params, + question_name: 'feedback-skippable', + answers: [1] + end + + specify { expect(response).to be_valid } + end + + # validate answers array unless + # - question options are empty + # - question hint empty + # + describe '#text_input_extra?' do + subject(:response) do + build :response, + **params, + question_name: 'feedback-textarea-only', + answers: [], + text_input: nil + end + + specify { expect(response).to be_valid } + end +end diff --git a/spec/models/response_spec.rb b/spec/models/response_spec.rb index 7874310bc..c0f85bcc7 100644 --- a/spec/models/response_spec.rb +++ b/spec/models/response_spec.rb @@ -1,79 +1,34 @@ require 'rails_helper' RSpec.describe Response, type: :model do + subject(:response) { user.response_for(question) } + let(:user) { create :user } + let(:question) do + Training::Module.by_name('alpha').page_by_name('1-1-4-1') + end + let(:headers) do + %w[ + id + user_id + training_module + question_name + answers + correct + created_at + updated_at + question_type + assessment_id + text_input + visit_id + ] + end + let(:rows) { [response] } before do skip unless Rails.application.migrated_answers? + response.update!(answers: [1], correct: true) end - describe 'feedback validations' do - let(:params) do - { - training_module: 'alpha', - correct: true, - user: user, - question_type: 'feedback', - } - end - - # validate answers array - describe '#answers' do - subject(:response) do - build :response, - **params, - question_name: 'feedback-oneoffquestion', - answers: [1] - end - - specify { expect(response).to be_valid } - end - - # validate answers array unless - # - question options are empty - # - question hint empty - # - describe '#text_input_extra?' do - subject(:response) do - build :response, - **params, - question_name: 'feedback-freetext', - answers: [], - text_input: nil - end - - specify { expect(response).to be_valid } - end - end - - describe 'ToCsv' do - subject(:response) { user.response_for(question) } - - let(:question) do - Training::Module.by_name('alpha').page_by_name('1-1-4-1') - end - let(:headers) do - %w[ - id - user_id - training_module - question_name - answers - correct - created_at - updated_at - question_type - assessment_id - text_input - visit_id - ] - end - let(:rows) { [response] } - - before do - response.update!(answers: [1], correct: true) - end - - it_behaves_like 'a data export model' - end + it_behaves_like 'a data export model' end diff --git a/spec/models/training/module_spec.rb b/spec/models/training/module_spec.rb index e2e91f2cb..68d69dcca 100644 --- a/spec/models/training/module_spec.rb +++ b/spec/models/training/module_spec.rb @@ -32,7 +32,7 @@ expect(mod.answers_with('foo')).to eq [] # no match # expect(mod.answers_with('')).to eq [] # formative expect(mod.answers_with('Wrong\s.+ 3')).to eq %w[1-3-2-4 1-3-2-5 1-3-2-6 1-3-2-7 1-3-2-8 1-3-2-9 1-3-2-10] # summative - expect(mod.answers_with('NOR')).to eq %w[1-3-3-1 1-3-3-2 1-3-3-3 1-3-3-4 feedback-radiobutton] # confidence + expect(mod.answers_with('NOR')).to eq %w[1-3-3-1 1-3-3-2 1-3-3-3 1-3-3-4] # confidence end end diff --git a/spec/models/training/question_spec.rb b/spec/models/training/question_spec.rb index fa5b8196e..bc0aab6cc 100644 --- a/spec/models/training/question_spec.rb +++ b/spec/models/training/question_spec.rb @@ -99,19 +99,19 @@ context 'when the question is a feedback question' do subject(:question) do - Training::Module.by_name('alpha').page_by_name('feedback-radiobutton') + Training::Module.by_name('alpha').page_by_name('feedback-radio-only') end let(:first_option) { question.options.first } specify do - expect(first_option.label).to eq 'Strongly agree' + expect(first_option.label).to eq 'Option 1' end end context 'when the question is a feedback free text question' do subject(:question) do - Training::Module.by_name('alpha').page_by_name('feedback-freetext') + Training::Module.by_name('alpha').page_by_name('feedback-textarea-only') end specify do diff --git a/spec/models/training/response_spec.rb b/spec/models/training/response_spec.rb index 35b75bc8e..8280d1eeb 100644 --- a/spec/models/training/response_spec.rb +++ b/spec/models/training/response_spec.rb @@ -104,7 +104,7 @@ end context 'with radio buttons for feedback question' do - let(:question_name) { 'feedback-radiobutton' } + let(:question_name) { 'feedback-radio-only' } describe 'and no answer' do let(:answers) { nil } diff --git a/spec/requests/static_spec.rb b/spec/requests/static_spec.rb index 3c25a8a5e..b46764877 100644 --- a/spec/requests/static_spec.rb +++ b/spec/requests/static_spec.rb @@ -10,8 +10,6 @@ specify { expect('/other-problems-signing-in').to be_successful } - specify { expect('/privacy-policy').to be_successful } - specify { expect('/promotional-materials').to be_successful } specify { expect('/sitemap').to be_successful } diff --git a/spec/support/ast/alpha-fail-response.yml b/spec/support/ast/alpha-fail-response.yml index fe5b2c04b..52062f719 100644 --- a/spec/support/ast/alpha-fail-response.yml +++ b/spec/support/ast/alpha-fail-response.yml @@ -1,4 +1,4 @@ -# +# Answer all factual questions incorrectly # @see ContentTestSchema # --- diff --git a/spec/support/ast/alpha-fail.yml b/spec/support/ast/alpha-fail.yml index f5cefa67c..e0bc31513 100644 --- a/spec/support/ast/alpha-fail.yml +++ b/spec/support/ast/alpha-fail.yml @@ -1,4 +1,4 @@ -# +# Answer all factual questions incorrectly # @see ContentTestSchema # --- diff --git a/spec/support/ast/alpha-pass-response-with-feedback.yml b/spec/support/ast/alpha-pass-response-with-feedback.yml index fb57d3025..d922ae964 100644 --- a/spec/support/ast/alpha-pass-response-with-feedback.yml +++ b/spec/support/ast/alpha-pass-response-with-feedback.yml @@ -1,5 +1,7 @@ +# Answer all factual questions correctly # -# Feedback form for end of module +# @see ContentTestSchema +# @note Fixture includes feedback form but answering is not implemented # --- - :path: /modules/alpha/content-pages/what-to-expect @@ -220,43 +222,44 @@ :inputs: - - :click_on - Next -- :path: /modules/alpha/content-pages/feedback-radiobutton - :text: 'Feedback question 1 - Select from following' +- :path: /modules/alpha/content-pages/feedback-radio-only + :text: Feedback radio buttons only :inputs: - - :click_on - Next -- :path: /modules/alpha/content-pages/feedback-yesnoandtext - :text: Feedback question 2 - Select from following +- :path: /modules/alpha/content-pages/feedback-checkbox-only + :text: Feedback checkboxes only :inputs: - - :click_on - Next -- :path: /modules/alpha/content-pages/feedback-freetext - :text: Feedback question 3 - Complete the following +- :path: /modules/alpha/content-pages/feedback-textarea-only + :text: Feedback textarea only :inputs: - - :click_on - Next -- :path: /modules/alpha/content-pages/feedback-radio-otherandtext - :text: Feedback question 4 - Select from following +- :path: /modules/alpha/content-pages/feedback-radio-other-more + :text: Feedback radio buttons with large other + :inputs: :inputs: - - :click_on - Next -- :path: /modules/alpha/content-pages/feedback-radio-and-freetext - :text: Feedback question 5 - Select from following +- :path: /modules/alpha/content-pages/feedback-checkbox-other-more + :text: Feedback checkbox with large other :inputs: - - :click_on - Next -- :path: /modules/alpha/content-pages/feedback-checkbox-othertextandor - :text: Feedback question 6 - Select from following +- :path: /modules/alpha/content-pages/feedback-radio-more + :text: Feedback radio buttons with additional reasons :inputs: - - :click_on - Next -- :path: /modules/alpha/content-pages/feedback-checkbox-otherandtext - :text: Feedback question 7 - Select from following +- :path: /modules/alpha/content-pages/feedback-checkbox-other-or + :text: Feedback checkboxes with Other and Or :inputs: - - :click_on - Next -- :path: /modules/alpha/content-pages/feedback-oneoffquestion - :text: Feedback question 8 - Select from following +- :path: /modules/alpha/content-pages/feedback-skippable + :text: Skippable :inputs: - - :click_on - Next @@ -267,4 +270,4 @@ - View certificate - :path: /modules/alpha/content-pages/1-3-4 :text: Congratulations! - :inputs: [] \ No newline at end of file + :inputs: [] diff --git a/spec/support/ast/alpha-pass.yml b/spec/support/ast/alpha-pass.yml index d9a384dd6..2990e9269 100644 --- a/spec/support/ast/alpha-pass.yml +++ b/spec/support/ast/alpha-pass.yml @@ -1,4 +1,4 @@ -# +# # Answer all factual questions correctly # @see ContentTestSchema # --- diff --git a/spec/support/ast/bravo-fail.yml b/spec/support/ast/bravo-fail.yml index 6bb0e0d3e..fe99fa2e2 100644 --- a/spec/support/ast/bravo-fail.yml +++ b/spec/support/ast/bravo-fail.yml @@ -1,3 +1,6 @@ +# Answer all factual questions incorrectly +# @see ContentTestSchema +# --- - :path: "/modules/bravo/content-pages/what-to-expect" :text: What to expect during the training diff --git a/spec/support/ast/course-feedback.yml b/spec/support/ast/course-feedback.yml index 536fc940c..849af3507 100644 --- a/spec/support/ast/course-feedback.yml +++ b/spec/support/ast/course-feedback.yml @@ -4,59 +4,82 @@ :inputs: - - :click_on - Next -- :path: /feedback/feedback-radiobutton - :text: Feedback question 1 - Select from following +- :path: /feedback/feedback-radio-only + :text: Feedback radio buttons only :inputs: - - :choose - response-answers-1-field - - :click_on - Next -- :path: /feedback/feedback-yesnoandtext - :text: Feedback question 2 - Select from following +- :path: /feedback/feedback-checkbox-only + :text: Feedback checkboxes only :inputs: - - - :choose + - - :check - response-answers-1-field - - :click_on - Next -- :path: /feedback/feedback-freetext - :text: Feedback question 3 - Complete the following +- :path: /feedback/feedback-textarea-only + :text: Feedback textarea only :inputs: - - :input_text - response-text-input-field - - hello world + - free text - - :click_on - Next -- :path: /feedback/feedback-radio-otherandtext - :text: Feedback question 4 - Select from following +- :path: /feedback/feedback-radio-other-more + :text: Feedback radio buttons with large other :inputs: - - :choose - - response-answers-1-field + - response-answers-5-field + - - :input_text + - response-text-input-field + - other text - - :click_on - Next -- :path: /feedback/feedback-radio-and-freetext - :text: Feedback question 5 - Select from following +- :path: /feedback/feedback-checkbox-other-more + :text: Feedback checkbox with large other :inputs: - - - :choose + - - :check - response-answers-1-field + # WIP + # - - :check + # - response-answers-5-field + # - - :input_text + # - response-text-input-field + # - other text - - :click_on - Next -- :path: /feedback/feedback-checkbox-othertextandor - :text: Feedback question 6 - Select from following + +- :path: /feedback/feedback-radio-more + :text: Feedback radio buttons with additional reasons :inputs: - - - :check + - - :choose - response-answers-1-field + # WIP + # - - :input_text + # - response-text-input-field + # - other text - - :click_on - Next -- :path: /feedback/feedback-checkbox-otherandtext - :text: Feedback question 7 - Select from following +- :path: /feedback/feedback-checkbox-other-or + :text: Feedback checkboxes with Other and Or :inputs: - - :check - response-answers-1-field + # OR + # - - :check + # - response-answers-0-field + # OTHER + # - - :check + # - response-answers-5-field + # - - :input_text + # - response-text-input-field + # - other text - - :click_on - Next # skippable -------------------------------------------------------------------- -- :path: /feedback/feedback-oneoffquestion - :text: Feedback question 8 - Select from following +- :path: /feedback/feedback-skippable + :text: Skippable :inputs: - - :choose - response-answers-1-field diff --git a/spec/system/course_feedback_spec.rb b/spec/system/course_feedback_spec.rb index 54b8185c9..8dc164598 100644 --- a/spec/system/course_feedback_spec.rb +++ b/spec/system/course_feedback_spec.rb @@ -12,11 +12,27 @@ expect(page).to have_current_path '/' end + it 'saves all answers' do + expect(Response.course_feedback.count).to be 8 + expect(Response.course_feedback.first).to be_persisted + expect(Response.course_feedback.first.visit).to be_present + end + + describe 'additional text input' do + let(:response) do + Response.course_feedback.find_by(question_name: 'feedback-radio-other-more') + end + + it 'is persisted' do + expect(response.text_input).to eq 'other text' + end + end + context 'when already completed' do it 'can be updated' do visit '/feedback' click_on 'Update my feedback' - expect(page).to have_current_path '/feedback/feedback-radiobutton' + expect(page).to have_current_path '/feedback/feedback-radio-only' end end end @@ -34,11 +50,50 @@ expect(page).to have_content 'My modules' end + it 'saves all answers' do + expect(Response.course_feedback.count).to be 8 + expect(Response.course_feedback.first).to be_persisted + expect(Response.course_feedback.first.user).to be_present + end + + describe 'additional text input' do + let(:response) do + Response.course_feedback.find_by(question_name: 'feedback-radio-other-more') + end + + it 'is persisted' do + expect(response.text_input).to eq 'other text' + end + end + context 'when already completed' do it 'can be updated' do visit '/feedback' click_on 'Update my feedback' - expect(page).to have_current_path '/feedback/feedback-radiobutton' + expect(page).to have_current_path '/feedback/feedback-radio-only' + end + end + end + + describe 'one-off questions' do + include_context 'with user' + + context 'when already answered' do + before do + create :response, + question_name: 'feedback-skippable', + training_module: 'alpha', + answers: [1], + correct: true, + user: user, + question_type: 'feedback' + end + + it 'skips the question' do + visit '/feedback/feedback-checkbox-other-or' + check 'response-answers-1-field' + click_on 'Next' + expect(page).to have_current_path '/feedback/thank-you' end end end diff --git a/spec/system/module_feedback_spec.rb b/spec/system/module_feedback_spec.rb index 040fe70c6..d919baac8 100644 --- a/spec/system/module_feedback_spec.rb +++ b/spec/system/module_feedback_spec.rb @@ -5,7 +5,7 @@ include_context 'with user' before do - visit '/modules/alpha/questionnaires/feedback-radiobutton' + visit '/modules/alpha/questionnaires/feedback-radio-only' end it 'validates answers' do @@ -17,7 +17,7 @@ context 'when not already answered' do it 'pagination counts the question' do expect(page).to have_content 'Page 1 of 9' - visit '/modules/alpha/questionnaires/feedback-oneoffquestion' + visit '/modules/alpha/questionnaires/feedback-skippable' expect(page).to have_content 'Page 8 of 9' end end @@ -25,7 +25,7 @@ context 'when already answered' do before do create :response, - question_name: 'feedback-oneoffquestion', + question_name: 'feedback-skippable', training_module: 'bravo', answers: [1], correct: true, @@ -35,13 +35,13 @@ # FeedbackPaginationDecorator#page_total it 'pagination does not count the question' do - visit '/modules/alpha/questionnaires/feedback-radiobutton' + visit '/modules/alpha/questionnaires/feedback-radio-only' expect(page).to have_content 'Page 1 of 8' end # Training::ResponsesController#redirect it 'skips the question' do - visit '/modules/alpha/questionnaires/feedback-checkbox-otherandtext' + visit '/modules/alpha/questionnaires/feedback-checkbox-other-or' expect(page).to have_content 'Page 7 of 8' check 'response-answers-1-field' click_on 'Next' diff --git a/spec/system/page_title_spec.rb b/spec/system/page_title_spec.rb index f2bcd2285..c112a10ed 100644 --- a/spec/system/page_title_spec.rb +++ b/spec/system/page_title_spec.rb @@ -15,7 +15,6 @@ it { expect(static_path('accessibility-statement')).to have_page_title 'Accessibility statement' } it { expect(static_path('new-registration')).to have_page_title 'Update your registration details' } it { expect(static_path('other-problems-signing-in')).to have_page_title 'Other problems signing in' } - it { expect(static_path('privacy-policy')).to have_page_title 'Privacy policy' } it { expect(static_path('promotional-materials')).to have_page_title 'Promotional materials' } it { expect(static_path('sitemap')).to have_page_title 'Sitemap' } it { expect(static_path('terms-and-conditions')).to have_page_title 'Terms and conditions' } From 17a519817548a4a1238708b48b501ae632f9228f Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 15 Apr 2024 14:52:54 +0100 Subject: [PATCH 58/95] Update caching and docs --- app/controllers/feedback_controller.rb | 13 +--- app/models/course.rb | 2 +- app/models/guest.rb | 5 ++ app/views/feedback/index.html.slim | 4 +- cms/CONTENTFUL.md | 9 ++- spec/controllers/feedback_controller_spec.rb | 70 ++++++++++---------- 6 files changed, 52 insertions(+), 51 deletions(-) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 058cf3dcc..7e9ba3ac2 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -1,8 +1,7 @@ class FeedbackController < ApplicationController helper_method :content, :mod, - :current_user_response, - :feedback_complete? + :current_user_response def index; end @@ -24,16 +23,6 @@ def update end end - # @see feedback#index - # @return [Boolean] - def feedback_complete? - if current_user.guest? - cookies[:course_feedback_completed].present? - else - current_user.completed_course_feedback? - end - end - private def redirect diff --git a/app/models/course.rb b/app/models/course.rb index 7773cd582..4d47d8caf 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -15,7 +15,7 @@ def self.content_type_id # @return [Course] def self.config - fetch_or_store('course') { first } + fetch_or_store(to_key('course')) { first } end # @return [String] mod.name diff --git a/app/models/guest.rb b/app/models/guest.rb index c0f882642..9185aa425 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -32,6 +32,11 @@ def response_for_shared(content, mod = Course.config) ) end + # @return [Boolean] + def completed_course_feedback? + responses.count.eql? Course.config.feedback_questions.count + end + private # @return [Response::ActiveRecord_Relation] diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index 4c47dbad7..459516a17 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -1,7 +1,7 @@ .govuk-grid-row .govuk-grid-column-full - - if feedback_complete? + - if current_user.completed_course_feedback? = m('feedback.complete') - else = m('feedback.intro', contact_us: Rails.application.credentials.contact_us) @@ -9,7 +9,7 @@ hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' .govuk-button-group - - if feedback_complete? + - if current_user.completed_course_feedback? - unless current_user.guest? = govuk_button_link_to t('previous_page.previous'), my_modules_path, secondary: true diff --git a/cms/CONTENTFUL.md b/cms/CONTENTFUL.md index d0cc638bc..e478ddd09 100644 --- a/cms/CONTENTFUL.md +++ b/cms/CONTENTFUL.md @@ -251,12 +251,19 @@ The different permutations are controlled using these fields: In order to show a text input when the last option is selected, provide a string in the `other` field. This string becomes the label text. - If you want to provide the user with more space to write, also turn on the `more` field. This changes the text input to a textarea field. +If instead you want to append a textarea field after to record the reasons for their answer, just enable `more` without `other`. + +To give an alternative `answer` separated by a divider, provide a string in the `or` field. + If the question is a feedback question it will not dynamically use the number of correct answers to determine if it is a radio button or checkbox. `multi_select` defaults to off using the radio button template. If the question needs to be hidden once it is answered, so it is not answered again in another form, enable `skippable`. + +--- + +Cookies: `course_feedback_started` and `course_feedback_completed` diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index a8600c9ba..272394be5 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -90,51 +90,51 @@ end end - describe '#feedback_complete?' do - before do - allow(controller).to receive(:current_user).and_return(user) - end + # xdescribe '#feedback_complete?' do + # before do + # allow(controller).to receive(:current_user).and_return(user) + # end - context 'with registered user' do - let(:user) { create :user, :registered } + # context 'with registered user' do + # let(:user) { create :user, :registered } - before do - allow(user).to receive(:completed_course_feedback?).and_return(completed) - get :index - end + # before do + # allow(user).to receive(:completed_course_feedback?).and_return(completed) + # get :index + # end - context 'and form completed' do - let(:completed) { true } + # context 'and form completed' do + # let(:completed) { true } - specify { expect(controller).to be_feedback_complete } - end + # specify { expect(controller).to be_feedback_complete } + # end - context 'and form not completed' do - let(:completed) { false } + # context 'and form not completed' do + # let(:completed) { false } - specify { expect(controller).not_to be_feedback_complete } - end - end + # specify { expect(controller).not_to be_feedback_complete } + # end + # end - context 'with guest' do - let(:user) { Guest.new(visit: create(:visit)) } + # context 'with guest' do + # let(:user) { Guest.new(visit: create(:visit)) } - before do - allow(controller).to receive(:cookies).and_return(cookie) - get :index - end + # before do + # allow(controller).to receive(:cookies).and_return(cookie) + # get :index + # end - context 'and form completed' do - let(:cookie) { { course_feedback_completed: 'some-token' } } + # context 'and form completed' do + # let(:cookie) { { course_feedback_completed: 'some-token' } } - specify { expect(controller).to be_feedback_complete } - end + # specify { expect(controller).to be_feedback_complete } + # end - context 'and form not completed' do - let(:cookie) { {} } + # context 'and form not completed' do + # let(:cookie) { {} } - specify { expect(controller).not_to be_feedback_complete } - end - end - end + # specify { expect(controller).not_to be_feedback_complete } + # end + # end + # end end From 4d36c886d98e8c6bedd73e03743bc17361cde6f2 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 16 Apr 2024 15:02:44 +0100 Subject: [PATCH 59/95] Add extra integrity check --- app/decorators/pagination_decorator.rb | 3 +-- app/services/content_integrity.rb | 20 +++++++++++--------- cms/migrate/02-create-question.js | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/decorators/pagination_decorator.rb b/app/decorators/pagination_decorator.rb index 20154b973..0e5bb9e0a 100644 --- a/app/decorators/pagination_decorator.rb +++ b/app/decorators/pagination_decorator.rb @@ -10,8 +10,7 @@ class PaginationDecorator # @return [String] def heading - # TODO: Look at improving this so it isn't hardcoded - return 'Summary and next steps' if content.thankyou? + return I18n.t('summary_intro.heading') if content.thankyou? content.section_content.first.heading end diff --git a/app/services/content_integrity.rb b/app/services/content_integrity.rb index 4d428cf96..657b40945 100644 --- a/app/services/content_integrity.rb +++ b/app/services/content_integrity.rb @@ -24,10 +24,8 @@ class ContentIntegrity # @return [Hash{Symbol=>String}] valid as released module CONTENT_VALIDATIONS = { - # type text: 'Missing text pages', video: 'Missing video pages', - formative: 'Missing formative questions', assessment_intro: 'Missing assessment intro page', confidence_intro: 'Missing confidence intro page', recap: 'Missing recap page', @@ -41,13 +39,12 @@ class ContentIntegrity thankyou: 'Penultimate page is wrong type', certificate: 'Last page is wrong type', - # type and frequency + # questions + formative: 'Missing formative questions', + feedback: 'Missing feedback questions', summative: 'Insufficient summative questions', - confidence: 'Insufficient confidence checks', - - # TODO: validity of feedback questions - # feedback_questions: 'TODO', - factual_questions: 'Factual questions have sufficient options', + confidence: 'Insufficient confidence questions', + factual: 'Factual questions have sufficient options', }.freeze # @return [nil] @@ -164,7 +161,7 @@ def video? end # @return [Boolean] - def factual_questions? + def factual? mod.questions.select(&:factual_question?).all? { |question| question.answer.valid? } end @@ -173,6 +170,11 @@ def formative? mod.formative_questions.any? end + # @return [Boolean] + def feedback? + mod.feedback_questions.any? + end + # 'Brain development and how children learn' has fewest # @return [Boolean] def confidence? diff --git a/cms/migrate/02-create-question.js b/cms/migrate/02-create-question.js index 6f5bd97e3..46c9af269 100644 --- a/cms/migrate/02-create-question.js +++ b/cms/migrate/02-create-question.js @@ -1,7 +1,7 @@ module.exports = function(migration) { const question = migration.createContentType('question', { - name: 'Question test', + name: 'Question', displayField: 'name', description: 'Formative, Summative, Confidence or Feedback' }) From 7c4beeff286fcfc08e286a907607398820e4cc07 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 19 Apr 2024 09:41:42 +0100 Subject: [PATCH 60/95] Simplify guest journey and use single cookie --- app/controllers/application_controller.rb | 2 +- app/controllers/feedback_controller.rb | 8 +-- app/services/content_integrity.rb | 5 -- cms/CONTENTFUL.md | 3 - .../application_controller_spec.rb | 5 +- spec/controllers/feedback_controller_spec.rb | 58 ++----------------- 6 files changed, 10 insertions(+), 71 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index fc3f30218..b2bb401ae 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -91,7 +91,7 @@ def current_user # @return [Guest, nil] def guest - visit = Visit.find_by(visit_token: cookies[:course_feedback_started]) || current_visit + visit = Visit.find_by(visit_token: cookies[:course_feedback]) || current_visit Guest.new(visit: visit) if visit.present? end diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 7e9ba3ac2..c0786f382 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -7,7 +7,6 @@ def index; end def show if question_name.eql? 'thank-you' - feedback_cookie(:completed) track_feedback_complete render :thank_you end @@ -15,7 +14,7 @@ def show def update if save_response! - feedback_cookie(:started) + feedback_cookie track_feedback_start redirect else @@ -89,10 +88,9 @@ def user_answers Array(response_params[:answers]).compact_blank.map(&:to_i) end - # @param state [Symbol, String] # @return [Hash] - def feedback_cookie(state) - cookies["course_feedback_#{state}"] = { value: current_user.visit_token } + def feedback_cookie + cookies[:course_feedback] = { value: current_user.visit_token } end def track_feedback_start diff --git a/app/services/content_integrity.rb b/app/services/content_integrity.rb index 657b40945..0e4f2a871 100644 --- a/app/services/content_integrity.rb +++ b/app/services/content_integrity.rb @@ -206,11 +206,6 @@ def confidence_intro? page_by_type_position(type: 'confidence_intro') end - # @return [Boolean] - def feedback? - page_by_type_position(type: 'feedback') - end - # @return [Boolean] def results? page_by_type_position(type: 'assessment_results') diff --git a/cms/CONTENTFUL.md b/cms/CONTENTFUL.md index e478ddd09..f1dc41fd9 100644 --- a/cms/CONTENTFUL.md +++ b/cms/CONTENTFUL.md @@ -264,6 +264,3 @@ to determine if it is a radio button or checkbox. If the question needs to be hidden once it is answered, so it is not answered again in another form, enable `skippable`. ---- - -Cookies: `course_feedback_started` and `course_feedback_completed` diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 8ead9dfdf..48fd3f3c9 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -1,9 +1,9 @@ require 'rails_helper' RSpec.describe ApplicationController, type: :controller do - describe '#guest' do subject(:guest) { controller.send(:guest) } + let(:cookie_token) { 'some-token' } it 'is nil without a current_visit' do @@ -18,10 +18,9 @@ it 'restores a previous visit from a cookie' do allow(controller).to receive(:current_visit).and_return(create(:visit)) create(:visit, visit_token: cookie_token) - request.cookies[:course_feedback_started] = cookie_token + request.cookies[:course_feedback] = cookie_token expect(guest).to be_a Guest expect(guest.visit_token).to eq cookie_token end end - end diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index 272394be5..81a389789 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -7,12 +7,10 @@ before { sign_in user } describe 'GET #show' do - context 'last page' do + context 'with last page' do it 'is tracked as complete' do expect(controller).to receive(:track_feedback_complete) - expect(cookies[:course_feedback_completed]).not_to be_present get :show, params: { id: 'thank-you' } - expect(cookies[:course_feedback_completed]).to be_present end end @@ -54,9 +52,9 @@ it 'is tracked as started' do expect(controller).to receive(:track_feedback_start) - expect(cookies[:course_feedback_started]).not_to be_present + expect(cookies[:course_feedback]).not_to be_present post :update, params: params - expect(cookies[:course_feedback_started]).to be_present + expect(cookies[:course_feedback]).to be_present end end @@ -84,57 +82,9 @@ it 'is not tracked as started' do expect(controller).not_to receive(:track_feedback_start) post :update, params: params - expect(cookies[:course_feedback_started]).not_to be_present + expect(cookies[:course_feedback]).not_to be_present end end end end - - # xdescribe '#feedback_complete?' do - # before do - # allow(controller).to receive(:current_user).and_return(user) - # end - - # context 'with registered user' do - # let(:user) { create :user, :registered } - - # before do - # allow(user).to receive(:completed_course_feedback?).and_return(completed) - # get :index - # end - - # context 'and form completed' do - # let(:completed) { true } - - # specify { expect(controller).to be_feedback_complete } - # end - - # context 'and form not completed' do - # let(:completed) { false } - - # specify { expect(controller).not_to be_feedback_complete } - # end - # end - - # context 'with guest' do - # let(:user) { Guest.new(visit: create(:visit)) } - - # before do - # allow(controller).to receive(:cookies).and_return(cookie) - # get :index - # end - - # context 'and form completed' do - # let(:cookie) { { course_feedback_completed: 'some-token' } } - - # specify { expect(controller).to be_feedback_complete } - # end - - # context 'and form not completed' do - # let(:cookie) { {} } - - # specify { expect(controller).not_to be_feedback_complete } - # end - # end - # end end From d127d1591455265ef35a17e3d758eb168023bd29 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Wed, 8 May 2024 15:18:23 +0100 Subject: [PATCH 61/95] Remove additional input validation --- app/models/response.rb | 12 ------------ spec/models/response_feedback_spec.rb | 16 ---------------- 2 files changed, 28 deletions(-) diff --git a/app/models/response.rb b/app/models/response.rb index f6406229e..7a450420d 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -10,9 +10,7 @@ class Response < ApplicationRecord validates :training_module, presence: true validates :question_type, inclusion: { in: %w[formative summative confidence feedback] } - validates :answers, presence: true, unless: -> { text_input_only? } - validates :text_input, presence: true, if: -> { text_input_extra? } scope :incorrect, -> { where(correct: false) } scope :correct, -> { where(correct: true) } @@ -74,16 +72,6 @@ def text_input_only? question.only_text? end - # @return [Boolean] - def text_input_extra? - question.and_text? && checked_other? - - # was previously - # better in manual tests but breaks spec/system/course_feedback_spec.rb - # fails at feedback-checkbox-other-or - # question.and_text? || checked_other? - end - # @return [Boolean] def archived? archived diff --git a/spec/models/response_feedback_spec.rb b/spec/models/response_feedback_spec.rb index 2738a2751..44ce84e69 100644 --- a/spec/models/response_feedback_spec.rb +++ b/spec/models/response_feedback_spec.rb @@ -26,20 +26,4 @@ specify { expect(response).to be_valid } end - - # validate answers array unless - # - question options are empty - # - question hint empty - # - describe '#text_input_extra?' do - subject(:response) do - build :response, - **params, - question_name: 'feedback-textarea-only', - answers: [], - text_input: nil - end - - specify { expect(response).to be_valid } - end end From 10a15c30f369cb924ce5bcecfce4a8b3fa6f0d71 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Thu, 9 May 2024 09:34:25 +0100 Subject: [PATCH 62/95] - user research preference editing in my account - remove validation for all feedback text inputs --- app/helpers/application_helper.rb | 5 +++++ app/models/user.rb | 10 ++++++++++ app/views/user/show.html.slim | 17 +++++++++++++---- config/locales/en.yml | 26 +++++++++++++++++++------- spec/lib/seed_snippets_spec.rb | 2 +- spec/services/dashboard_spec.rb | 2 +- spec/system/account_page_spec.rb | 13 +++++++++++++ 7 files changed, 62 insertions(+), 13 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f0445b4fa..91e662d53 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -59,4 +59,9 @@ def html_title(*parts) def calculate_module_state CalculateModuleState.new(user: current_user).call end + + # @return [Training::Question] feedback skippable + def user_research_question + Course.config.feedback.find(&:skippable?) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 76caa4c84..3827de740 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -383,6 +383,16 @@ def training_emails_recipient? training_emails || training_emails.nil? end + # return [Boolean] + def research_participant? + response = responses.feedback.find { |preference| preference.question.skippable? } + return false if response.nil? + + option = response.question.options(checked: response.answers).find(&:checked?) + # Dry::Types['params.bool']['Yes'] + option.id.eql?(1) + end + # @return [Boolean] def private_beta_registration_complete? !!private_beta_registration_complete diff --git a/app/views/user/show.html.slim b/app/views/user/show.html.slim index 6eb36d2ce..fed6ec3d8 100644 --- a/app/views/user/show.html.slim +++ b/app/views/user/show.html.slim @@ -39,12 +39,21 @@ = govuk_summary_list do |email_preferences| - email_preferences.with_row do |row| - - row.with_key { m('my_account.email_preferences') } + - row.with_key { m('my_account.email_preferences.heading') } - row.with_value { nil } - email_preferences.with_row do |row| - - row.with_key { 'Email updates about this training course' } - - row.with_value(text: t(current_user.training_emails_recipient?, scope: 'my_account.training_emails'), classes: %w[data-hj-suppress]) - - row.with_action(text: 'Change email preferences', href: edit_training_emails_user_path, html_attributes: { id: :edit_training_emails_user }) + - row.with_key { t('my_account.email_preferences.label') } + - row.with_value(text: t(current_user.training_emails_recipient?, scope: 'my_account.email_preferences.preference'), classes: %w[data-hj-suppress]) + - row.with_action(text: t('my_account.email_preferences.link_text'), href: edit_training_emails_user_path, html_attributes: { id: :edit_training_emails_user }) + + = govuk_summary_list do |research_preferences| + - research_preferences.with_row do |row| + - row.with_key { m('my_account.research_preferences.heading') } + - row.with_value { nil } + - research_preferences.with_row do |row| + - row.with_key { t('my_account.research_preferences.label') } + - row.with_value(text: t(current_user.research_participant?, scope: 'my_account.research_preferences.preference'), classes: %w[data-hj-suppress]) + - row.with_action(text: t('my_account.research_preferences.link_text'), href: feedback_path(user_research_question.name)) = m('my_account.closing.information') = govuk_button_link_to t('my_account.closing.button'), edit_reason_user_close_account_path diff --git a/config/locales/en.yml b/config/locales/en.yml index a0d9b06e8..a0a086667 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -41,8 +41,6 @@ en: answers: blank: Please select an answer. invalid: Please select an option. - text_input: - blank: Tell us more!!! # FIXME user_answer: attributes: @@ -301,11 +299,25 @@ en: name_information: This is the name that will appear on your end of module certificate. You can use this setting to change how your name appears. Changing your name on this account will not affect your GOV.UK One Login setting_details: | ## Your setting details - email_preferences: | - ## Your email preferences - training_emails: - true: You have chosen to receive emails about this training course. - false: You have chosen not to receive emails about this training course. + + email_preferences: + heading: | + ## Your email preferences + label: Email updates about this training course + link_text: Change email preferences + preference: + true: You have chosen to receive emails about this training course. + false: You have chosen not to receive emails about this training course. + + research_preferences: + heading: | + ## Your research preferences + label: Willing to speak to a researcher to improve this service + link_text: Change research preferences + preference: + true: You have chosen to participate in research. + false: You have chosen not to participate in research. + early_years_experience: Time worked in early years password_changed: Password last changed on %{date} diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 366ff607d..30551e222 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 213 + expect(locales.count).to be 219 end it 'dot separated key -> Page::Resource#name' do diff --git a/spec/services/dashboard_spec.rb b/spec/services/dashboard_spec.rb index 6c4b33e20..785665cb4 100644 --- a/spec/services/dashboard_spec.rb +++ b/spec/services/dashboard_spec.rb @@ -18,7 +18,7 @@ let(:data_files) { Dir.glob path.join('*/*/*.csv') } describe 'exported files' do - specify { expect(data_files.count).to be 22 } + specify { expect(data_files.count).to be described_class::DATA_SOURCES.size } end it 'exports data in CSV format' do diff --git a/spec/system/account_page_spec.rb b/spec/system/account_page_spec.rb index 4b1bd87d7..d31bee11f 100644 --- a/spec/system/account_page_spec.rb +++ b/spec/system/account_page_spec.rb @@ -4,6 +4,14 @@ include_context 'with user' before do + create :response, + question_name: 'feedback-skippable', + training_module: 'course', + answers: [1], + correct: true, + user: user, + question_type: 'feedback' + visit '/my-account' end @@ -16,7 +24,12 @@ expect(page).to have_content 'Changing your name on this account will not affect your GOV.UK One Login' expect(page).to have_link 'Change setting details' + expect(page).to have_link 'Change email preferences' + expect(page).to have_text 'You have chosen to receive emails about this training course.' + + expect(page).to have_link 'Change research preferences' + expect(page).to have_text 'You have chosen to participate in research.' expect(page).to have_text 'Closing your account' end From a3465b6b759231291f02f2278014a4a48106577e Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 10 May 2024 10:11:31 +0100 Subject: [PATCH 63/95] Add boolean to user record for UR participation --- app/models/user.rb | 14 +++++++++----- ...40509133351_add_research_preference_to_users.rb | 5 +++++ db/schema.rb | 3 ++- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20240509133351_add_research_preference_to_users.rb diff --git a/app/models/user.rb b/app/models/user.rb index 3827de740..3d2bbe345 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -386,11 +386,15 @@ def training_emails_recipient? # return [Boolean] def research_participant? response = responses.feedback.find { |preference| preference.question.skippable? } - return false if response.nil? - - option = response.question.options(checked: response.answers).find(&:checked?) - # Dry::Types['params.bool']['Yes'] - option.id.eql?(1) + if response.nil? + update(research_participant: false) + false + else + option = response.question.options(checked: response.answers).find(&:checked?) + opt_in = option.id.eql?(1) + update(research_participant: opt_in) + opt_in + end end # @return [Boolean] diff --git a/db/migrate/20240509133351_add_research_preference_to_users.rb b/db/migrate/20240509133351_add_research_preference_to_users.rb new file mode 100644 index 000000000..ee2af644c --- /dev/null +++ b/db/migrate/20240509133351_add_research_preference_to_users.rb @@ -0,0 +1,5 @@ +class AddResearchPreferenceToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :research_participant, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 82bf11bf8..e597f5121 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[7.1].define(version: 2024_03_08_142000) do +ActiveRecord::Schema[7.1].define(version: 2024_05_09_133351) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -214,6 +214,7 @@ t.boolean "early_years_emails" t.string "gov_one_id" t.string "early_years_experience" + t.boolean "research_participant" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["gov_one_id"], name: "index_users_on_gov_one_id", unique: true From ed3b81a41dcb2999007981a2aaa273e16e53d76b Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 13 May 2024 13:27:09 +0100 Subject: [PATCH 64/95] Typo --- config/locales/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index a0a086667..8b4f10b29 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -641,7 +641,7 @@ en: research: | Participation is entirely voluntary, and you can withdraw at any point before, during or after the research takes place and you do not need to give a reason. - reasons: Could you give use reasons for your answer? + reasons: Could you give us reasons for your answer? # /gov-one/info gov_one_info: From 69eb419441b37e8961dd722e1efa3e47ece986cc Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 13 May 2024 13:44:13 +0100 Subject: [PATCH 65/95] Skip question for guests and defend when no current user exists --- app/models/guest.rb | 3 ++- app/views/feedback/index.html.slim | 2 +- app/views/training/questions/_debug.html.slim | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/guest.rb b/app/models/guest.rb index 9185aa425..6b6c9ee57 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -17,7 +17,8 @@ def guest? # @param question [Training::Question] # @return [Boolean] def skip_question?(question) - question.skippable? && response_for_shared(question).responded? + question.name.eql?('prevent-from-completing-training') || + (question.skippable? && response_for_shared(question).responded?) end # @param content [Training::Question] feedback questions diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index 459516a17..ba09aeac2 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -1,7 +1,7 @@ .govuk-grid-row .govuk-grid-column-full - - if current_user.completed_course_feedback? + - if current_user&.completed_course_feedback? = m('feedback.complete') - else = m('feedback.intro', contact_us: Rails.application.credentials.contact_us) diff --git a/app/views/training/questions/_debug.html.slim b/app/views/training/questions/_debug.html.slim index b9dc5e6b8..1ae64a886 100644 --- a/app/views/training/questions/_debug.html.slim +++ b/app/views/training/questions/_debug.html.slim @@ -20,7 +20,7 @@ br | More: #{content.more} br - | User type: #{current_user.guest? ? 'guest' : 'authenticated user'} + | User type: #{current_user&.class.name} br | Cookie: #{current_user.visit_token} br From ddf219250a1df519de21e770b48591fe55b048a5 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 14 May 2024 09:08:55 +0100 Subject: [PATCH 66/95] Refactor main feedback for user vs guest journey --- app/controllers/feedback_controller.rb | 11 +-- app/models/guest.rb | 10 ++- spec/support/ast/course-feedback-guest.yml | 88 +++++++++++++++++++ ...-feedback.yml => course-feedback-user.yml} | 0 spec/system/course_feedback_spec.rb | 18 ++-- 5 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 spec/support/ast/course-feedback-guest.yml rename spec/support/ast/{course-feedback.yml => course-feedback-user.yml} (100%) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index c0786f382..d31486613 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -28,16 +28,17 @@ def redirect if content.last_feedback? redirect_to feedback_path('thank-you') elsif skip_next_question? - if content.next_item.last_feedback? - redirect_to feedback_path('thank-you') - else - redirect_to feedback_path(content.next_item.next_item.name) - end + redirect_to feedback_path(skip_to_question) else redirect_to feedback_path(content.next_item.name) end end + # @return [String] + def skip_to_question + content.next_item.last_feedback? ? 'thank-you' : content.next_item.next_item.name + end + # @return [Boolean] def skip_next_question? current_user.skip_question?(content.next_item) diff --git a/app/models/guest.rb b/app/models/guest.rb index 6b6c9ee57..fadc234e2 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -17,8 +17,7 @@ def guest? # @param question [Training::Question] # @return [Boolean] def skip_question?(question) - question.name.eql?('prevent-from-completing-training') || - (question.skippable? && response_for_shared(question).responded?) + question.skippable? || question.name.eql?('prevent-from-completing-training') end # @param content [Training::Question] feedback questions @@ -35,11 +34,16 @@ def response_for_shared(content, mod = Course.config) # @return [Boolean] def completed_course_feedback? - responses.count.eql? Course.config.feedback_questions.count + guest_questions.count.eql?(responses.count) end private + # @return [Array] + def guest_questions + Course.config.feedback_questions.reject { |question| skip_question?(question) } + end + # @return [Response::ActiveRecord_Relation] def responses Response.course_feedback.where(visit_id: visit.id) diff --git a/spec/support/ast/course-feedback-guest.yml b/spec/support/ast/course-feedback-guest.yml new file mode 100644 index 000000000..37f3325ab --- /dev/null +++ b/spec/support/ast/course-feedback-guest.yml @@ -0,0 +1,88 @@ +--- +- :path: /feedback + :text: Additional feedback + :inputs: + - - :click_on + - Next +- :path: /feedback/feedback-radio-only + :text: Feedback radio buttons only + :inputs: + - - :choose + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-checkbox-only + :text: Feedback checkboxes only + :inputs: + - - :check + - response-answers-1-field + - - :click_on + - Next +- :path: /feedback/feedback-textarea-only + :text: Feedback textarea only + :inputs: + - - :input_text + - response-text-input-field + - free text + - - :click_on + - Next +- :path: /feedback/feedback-radio-other-more + :text: Feedback radio buttons with large other + :inputs: + - - :choose + - response-answers-5-field + - - :input_text + - response-text-input-field + - other text + - - :click_on + - Next +- :path: /feedback/feedback-checkbox-other-more + :text: Feedback checkbox with large other + :inputs: + - - :check + - response-answers-1-field + # WIP + # - - :check + # - response-answers-5-field + # - - :input_text + # - response-text-input-field + # - other text + - - :click_on + - Next + +- :path: /feedback/feedback-radio-more + :text: Feedback radio buttons with additional reasons + :inputs: + - - :choose + - response-answers-1-field + # WIP + # - - :input_text + # - response-text-input-field + # - other text + - - :click_on + - Next +- :path: /feedback/feedback-checkbox-other-or + :text: Feedback checkboxes with Other and Or + :inputs: + - - :check + - response-answers-1-field + # OR + # - - :check + # - response-answers-0-field + # OTHER + # - - :check + # - response-answers-5-field + # - - :input_text + # - response-text-input-field + # - other text + - - :click_on + - Next +# skippable -------------------------------------------------------------------- +# - :path: /feedback/feedback-skippable +# :text: Skippable +# :inputs: +# - - :choose +# - response-answers-1-field +# - - :click_on +# - Next +# skippable -------------------------------------------------------------------- diff --git a/spec/support/ast/course-feedback.yml b/spec/support/ast/course-feedback-user.yml similarity index 100% rename from spec/support/ast/course-feedback.yml rename to spec/support/ast/course-feedback-user.yml diff --git a/spec/system/course_feedback_spec.rb b/spec/system/course_feedback_spec.rb index 8dc164598..c4734ab09 100644 --- a/spec/system/course_feedback_spec.rb +++ b/spec/system/course_feedback_spec.rb @@ -3,7 +3,7 @@ describe 'Course feedback' do context 'with unauthenticated user' do include_context 'with automated path' - let(:fixture) { 'spec/support/ast/course-feedback.yml' } + let(:fixture) { 'spec/support/ast/course-feedback-guest.yml' } it 'returns to homepage once completed' do expect(page).to have_current_path '/feedback/thank-you' @@ -13,8 +13,11 @@ end it 'saves all answers' do - expect(Response.course_feedback.count).to be 8 - expect(Response.course_feedback.first).to be_persisted + expect(Response.course_feedback.count).to be 7 + end + + it 'is linked to a visit not a user' do + expect(Response.course_feedback.first.user).not_to be_present expect(Response.course_feedback.first.visit).to be_present end @@ -33,6 +36,7 @@ visit '/feedback' click_on 'Update my feedback' expect(page).to have_current_path '/feedback/feedback-radio-only' + expect(page).to have_checked_field 'Option 1' end end end @@ -40,7 +44,7 @@ context 'with authenticated user' do include_context 'with user' include_context 'with automated path' - let(:fixture) { 'spec/support/ast/course-feedback.yml' } + let(:fixture) { 'spec/support/ast/course-feedback-user.yml' } it 'returns to modules page once completed' do expect(page).to have_current_path '/feedback/thank-you' @@ -52,7 +56,10 @@ it 'saves all answers' do expect(Response.course_feedback.count).to be 8 - expect(Response.course_feedback.first).to be_persisted + end + + it 'is linked to a user not a visit' do + expect(Response.course_feedback.first.visit).not_to be_present expect(Response.course_feedback.first.user).to be_present end @@ -71,6 +78,7 @@ visit '/feedback' click_on 'Update my feedback' expect(page).to have_current_path '/feedback/feedback-radio-only' + expect(page).to have_checked_field 'Option 1' end end end From c5266e98b66da15d57a41ec6f40ae30196eefbc7 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 20 May 2024 16:53:53 +0100 Subject: [PATCH 67/95] Rework shared content strategy and move next/skip logic out of controllers --- app/controllers/application_controller.rb | 7 +++-- app/controllers/feedback_controller.rb | 14 +--------- .../training/responses_controller.rb | 10 ++----- app/decorators/next_page_decorator.rb | 28 +++++++++++++------ app/helpers/link_helper.rb | 2 +- app/models/concerns/pagination.rb | 10 +++++++ app/models/guest.rb | 5 ++++ app/models/training/content.rb | 7 +++++ app/models/training/module.rb | 4 ++- config/initializers/types.rb | 2 ++ spec/models/concerns/pagination_spec.rb | 24 +++++++++++++--- 11 files changed, 75 insertions(+), 38 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index da5bf519b..f9557d0ae 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -91,8 +91,11 @@ def guest # @see Auditing # @return [Boolean] def user_signed_in? - return false if current_user&.guest? - return true if bot? + if bot? || !current_user&.guest? + true + elsif current_user&.guest? + false + end super end diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index d31486613..a3b691e61 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -27,23 +27,11 @@ def update def redirect if content.last_feedback? redirect_to feedback_path('thank-you') - elsif skip_next_question? - redirect_to feedback_path(skip_to_question) else - redirect_to feedback_path(content.next_item.name) + redirect_to feedback_path(helpers.next_page.name) end end - # @return [String] - def skip_to_question - content.next_item.last_feedback? ? 'thank-you' : content.next_item.next_item.name - end - - # @return [Boolean] - def skip_next_question? - current_user.skip_question?(content.next_item) - end - # @return [Boolean] def save_response! current_user_response.update( diff --git a/app/controllers/training/responses_controller.rb b/app/controllers/training/responses_controller.rb index 5d1b23332..3ae3c3635 100644 --- a/app/controllers/training/responses_controller.rb +++ b/app/controllers/training/responses_controller.rb @@ -6,6 +6,7 @@ module Training class ResponsesController < ApplicationController include Learning + include Pagination before_action :authenticate_registered_user! @@ -64,18 +65,11 @@ def redirect if content.formative_question? redirect_to training_module_question_path(mod.name, content.name) - elsif skip_next_question? - redirect_to training_module_page_path(mod.name, content.next_item.next_item.name) else - redirect_to training_module_page_path(mod.name, content.next_item.name) + redirect_to training_module_page_path(mod.name, helpers.next_page.name) end end - # @return [Boolean] - def skip_next_question? - current_user.skip_question?(content.next_item) - end - # @return [Event] Update action def track_question_answer if Rails.application.migrated_answers? diff --git a/app/decorators/next_page_decorator.rb b/app/decorators/next_page_decorator.rb index bb22711e8..de8beeb3b 100644 --- a/app/decorators/next_page_decorator.rb +++ b/app/decorators/next_page_decorator.rb @@ -10,23 +10,23 @@ class NextPageDecorator # @return [User] option :user, Types.Instance(User), required: true # @!attribute [r] mod - # @return [Training::Module] - option :mod, Types::TrainingModule, required: true + # @return [Course, Training::Module] + option :mod, Types::Parent, required: true # @!attribute [r] content # @return [Training::Page, Training::Question, Training::Video] option :content, Types::TrainingContent, required: true # @!attribute [r] assessment # @return [AssessmentProgress] - option :assessment, required: true + option :assessment # @return [String] def name if content.interruption_page? mod.content_start.name elsif skip_next_question? - content.next_item.next_item.name + (mod.is_a?(Course) && next_item.last_feedback?) ? 'thank-you' : next_next_item.name else - content.next_item.name + next_item.name end end @@ -82,7 +82,7 @@ def answered? # @return [Boolean] def finish? - content.next_item.certificate? + next_item.certificate? end # @return [Boolean] @@ -94,12 +94,12 @@ def test_start? # @return [Boolean] def test_finish? - content.next_item.assessment_results? && !disable_question_submission? + next_item.assessment_results? && !disable_question_submission? end # @return [Boolean] def missing? - content.next_item.eql?(content) && wip? + next_item.eql?(content) && wip? end # @return [Boolean] @@ -120,6 +120,16 @@ def wip? # @note only used if a skippable question follows a non-question # @return [Boolean] def skip_next_question? - user.skip_question?(content.next_item) + user.skip_question?(next_item) + end + + # @return [Training::Page, Training::Question, Training::Video] + def next_next_item + content.with_parent(mod).next_next_item + end + + # @return [Training::Page, Training::Question, Training::Video] + def next_item + content.with_parent(mod).next_item end end diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 19df5f8c2..246376581 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -77,7 +77,7 @@ def next_page user: current_user, mod: mod, content: content, - assessment: assessment_progress_service(mod), + assessment: (mod.is_a?(Training::Module) ? assessment_progress_service(mod) : nil), ) end diff --git a/app/models/concerns/pagination.rb b/app/models/concerns/pagination.rb index 741cf36b6..873e60144 100644 --- a/app/models/concerns/pagination.rb +++ b/app/models/concerns/pagination.rb @@ -32,6 +32,11 @@ def next_item parent.page_by_id(next_item_id) || self end + # @return [nil, Training::Page, Training::Video, Training::Question] + def next_next_item + parent.page_by_id(next_next_item_id) + end + # @return [String] def previous_item_id parent.pages[content_index - 1].id @@ -42,6 +47,11 @@ def next_item_id parent.pages[content_index + 1]&.id end + # @return [String, nil] + def next_next_item_id + parent.pages[content_index + 2]&.id + end + # @return [Array] def section_content if parent.is_a? Training::Module # OPTIMIZE: introduce parent check predicates? diff --git a/app/models/guest.rb b/app/models/guest.rb index fadc234e2..d72acba87 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -14,6 +14,11 @@ def guest? true end + # @return [Boolean] + def course_started? + false + end + # @param question [Training::Question] # @return [Boolean] def skip_question?(question) diff --git a/app/models/training/content.rb b/app/models/training/content.rb index 64580fd2b..8a5431196 100644 --- a/app/models/training/content.rb +++ b/app/models/training/content.rb @@ -16,6 +16,13 @@ def parent @parent ||= Training::Module.by_content_id(id) end + # @param mod [Course, Training::Module] + # @return [Training::Page, Training::Video, Training::Question] + def with_parent(mod) + @parent = mod + self + end + # @return [String] def debug_summary <<~SUMMARY diff --git a/app/models/training/module.rb b/app/models/training/module.rb index 9147407ef..c2e9386bb 100644 --- a/app/models/training/module.rb +++ b/app/models/training/module.rb @@ -39,8 +39,10 @@ def self.by_name(name) end end + # Module content must not be shared and have unique names + # # @param id [String] - # @return [Training::Module] + # @return [Training::Module] first result def self.by_content_id(id) ordered.find { |mod| mod.content.find { |content| content.id.eql?(id) } } end diff --git a/config/initializers/types.rb b/config/initializers/types.rb index ac3927709..e9ffa3111 100644 --- a/config/initializers/types.rb +++ b/config/initializers/types.rb @@ -4,6 +4,8 @@ module Types include Dry.Types() include Dry::Core::Constants + Parent = Instance(Training::Module) | Instance(Course) + TrainingModule = Instance(Training::Module) TrainingContent = Instance(Training::Page) | Instance(Training::Question) | Instance(Training::Video) end diff --git a/spec/models/concerns/pagination_spec.rb b/spec/models/concerns/pagination_spec.rb index 23041eaf5..9267b9c30 100644 --- a/spec/models/concerns/pagination_spec.rb +++ b/spec/models/concerns/pagination_spec.rb @@ -24,15 +24,31 @@ describe '#section?' do it 'returns true if the page defines a section boundary' do - expect(page.section?).to eq(false) - expect(mod.page_by_name('1-2').section?).to eq(true) + expect(page).not_to be_section + expect(mod.page_by_name('1-2')).to be_section end end describe '#subsection?' do it 'returns true if the page defines a subsection boundary' do - expect(page.subsection?).to eq(false) - expect(mod.page_by_name('1-3-2').subsection?).to eq(true) # assessment subsection + expect(page).not_to be_subsection + expect(mod.page_by_name('1-3-2')).to be_subsection # assessment subsection + end + end + + context '#with_parent' do + subject(:page) { mod.page_by_name('feedback-checkbox-other-or') } + + let(:mod) { Training::Module.by_name(:bravo) } + + it 'navigates shared content in the correct parent' do + expect(page.parent.name).to eq 'alpha' + expect(page.with_parent(mod).parent.name).to eq 'bravo' + expect(page.with_parent(mod).next_item.parent.name).to eq 'alpha' + + expect(page.next_item.name).to eq 'feedback-skippable' + expect(page.with_parent(mod).next_item.name).to eq 'feedback-skippable' + expect(page.with_parent(mod).next_next_item.name).to eq '1-3-3-5-bravo' end end end From a034eb60d8cc6363829cda750e7789aa712a2e1d Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 20 May 2024 17:07:34 +0100 Subject: [PATCH 68/95] Type check for user and guest in pagination service --- app/decorators/next_page_decorator.rb | 2 +- config/initializers/types.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/decorators/next_page_decorator.rb b/app/decorators/next_page_decorator.rb index de8beeb3b..ad1eded28 100644 --- a/app/decorators/next_page_decorator.rb +++ b/app/decorators/next_page_decorator.rb @@ -8,7 +8,7 @@ class NextPageDecorator # @!attribute [r] user # @return [User] - option :user, Types.Instance(User), required: true + option :user, Types::User, required: true # @!attribute [r] mod # @return [Course, Training::Module] option :mod, Types::Parent, required: true diff --git a/config/initializers/types.rb b/config/initializers/types.rb index e9ffa3111..970c5e7dd 100644 --- a/config/initializers/types.rb +++ b/config/initializers/types.rb @@ -4,6 +4,7 @@ module Types include Dry.Types() include Dry::Core::Constants + User = Instance(User) | Instance(Guest) Parent = Instance(Training::Module) | Instance(Course) TrainingModule = Instance(Training::Module) From aa54c84742616a20b790e827f902d6e2ff6feb86 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 21 May 2024 09:34:42 +0100 Subject: [PATCH 69/95] - Convert feedback thank-you to a page - Adjust UR preference editing journey --- app/controllers/application_controller.rb | 7 ++---- app/controllers/feedback_controller.rb | 29 +++++++++++++---------- app/decorators/next_page_decorator.rb | 6 ++--- app/models/course.rb | 15 ++++++------ app/models/training/module.rb | 2 +- app/views/feedback/thank_you.html.slim | 3 ++- app/views/user/_debug.html.slim | 2 ++ app/views/user/show.html.slim | 9 +++---- config/initializers/types.rb | 1 - config/locales/en.yml | 5 ---- spec/lib/seed_snippets_spec.rb | 2 +- spec/models/concerns/pagination_spec.rb | 2 +- spec/models/course_spec.rb | 13 ++++++---- spec/services/course_progress_spec.rb | 2 +- spec/system/account_page_spec.rb | 11 +++++++++ 15 files changed, 62 insertions(+), 47 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f9557d0ae..da5bf519b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -91,11 +91,8 @@ def guest # @see Auditing # @return [Boolean] def user_signed_in? - if bot? || !current_user&.guest? - true - elsif current_user&.guest? - false - end + return false if current_user&.guest? + return true if bot? super end diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index a3b691e61..98eabe618 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -6,17 +6,25 @@ class FeedbackController < ApplicationController def index; end def show - if question_name.eql? 'thank-you' + if content_name.eql? 'thank-you' track_feedback_complete render :thank_you end end def update - if save_response! + if research_participation_updated? + if save_response! + flash[:success] = 'Your details have been updated' + redirect_to user_path + else + render :show, status: :unprocessable_entity + end + + elsif save_response! feedback_cookie track_feedback_start - redirect + redirect_to feedback_path(helpers.next_page.name) else render :show, status: :unprocessable_entity end @@ -24,12 +32,9 @@ def update private - def redirect - if content.last_feedback? - redirect_to feedback_path('thank-you') - else - redirect_to feedback_path(helpers.next_page.name) - end + # @return [Boolean] + def research_participation_updated? + current_user_response.question.skippable? && current_user_response.persisted? end # @return [Boolean] @@ -46,9 +51,9 @@ def mod Course.config end - # @return [Training::Question] + # @return [Training::Question, Training::Page] def content - mod.page_by_name(question_name) + mod.page_by_name(content_name) end # @return [User, Guest, nil] @@ -63,7 +68,7 @@ def current_user_response(question = content) end # @return [String] - def question_name + def content_name params[:id] end diff --git a/app/decorators/next_page_decorator.rb b/app/decorators/next_page_decorator.rb index ad1eded28..9f9a79f7d 100644 --- a/app/decorators/next_page_decorator.rb +++ b/app/decorators/next_page_decorator.rb @@ -7,8 +7,8 @@ class NextPageDecorator extend Dry::Initializer # @!attribute [r] user - # @return [User] - option :user, Types::User, required: true + # @return [User, Guest] + option :user, Types.Instance(User) | Types.Instance(Guest), required: true # @!attribute [r] mod # @return [Course, Training::Module] option :mod, Types::Parent, required: true @@ -24,7 +24,7 @@ def name if content.interruption_page? mod.content_start.name elsif skip_next_question? - (mod.is_a?(Course) && next_item.last_feedback?) ? 'thank-you' : next_next_item.name + next_next_item.name else next_item.name end diff --git a/app/models/course.rb b/app/models/course.rb index 4d47d8caf..ded14b2b5 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -33,19 +33,18 @@ def content_subsections Types::EMPTY_ARRAY end - # @return [Array] - def feedback - super.to_a - end - - # @return [Array] with parent + # @return [Array] def pages - feedback.map do |question| + feedback.to_a.map do |question| question.define_singleton_method(:parent) { Course.config } question end end - alias_method :feedback_questions, :pages + + # @return [Array] + def feedback_questions + pages.select(&:feedback_question?) + end # @see Pagination # @return [Training::Question] diff --git a/app/models/training/module.rb b/app/models/training/module.rb index c2e9386bb..ba1d27e9a 100644 --- a/app/models/training/module.rb +++ b/app/models/training/module.rb @@ -90,7 +90,7 @@ def content # @return [Hash{ Integer => Array }] def content_sections - content.slice_before(&:section?).each.with_index(1).to_h.invert + @content_sections ||= content.slice_before(&:section?).each.with_index(1).to_h.invert end # @return [Hash{ Array => Array }] diff --git a/app/views/feedback/thank_you.html.slim b/app/views/feedback/thank_you.html.slim index 52966c89c..712045061 100644 --- a/app/views/feedback/thank_you.html.slim +++ b/app/views/feedback/thank_you.html.slim @@ -1,7 +1,8 @@ .govuk-grid-row .govuk-grid-column-full + h1.govuk-heading-xl= content.heading - = m('feedback.thank_you', headings_start_with: 'xl') + = m(content.body) hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' diff --git a/app/views/user/_debug.html.slim b/app/views/user/_debug.html.slim index ed975149c..c331df2db 100644 --- a/app/views/user/_debug.html.slim +++ b/app/views/user/_debug.html.slim @@ -17,3 +17,5 @@ | training_emails: #{current_user.training_emails} br | early_years_emails: #{current_user.early_years_emails} + br + | research_participant: #{current_user.research_participant} diff --git a/app/views/user/show.html.slim b/app/views/user/show.html.slim index bdd2959d2..e7054a590 100644 --- a/app/views/user/show.html.slim +++ b/app/views/user/show.html.slim @@ -1,8 +1,9 @@ -= render 'debug' - - content_for :page_title do = html_title t('my_account.title') +- content_for :cta do + = render 'feedback/cta' + .govuk-grid-row .govuk-grid-column-full h1.govuk-heading-l Manage your account @@ -56,8 +57,8 @@ - row.with_value(text: t(current_user.research_participant?, scope: 'my_account.research_preferences.preference'), classes: %w[data-hj-suppress]) - row.with_action(text: t('my_account.research_preferences.link_text'), href: feedback_path(user_research_question.name)) + = render 'debug' + = m('my_account.closing.information') = govuk_button_link_to t('my_account.closing.button'), edit_reason_user_close_account_path -- content_for :cta do - = render 'feedback/cta' diff --git a/config/initializers/types.rb b/config/initializers/types.rb index 970c5e7dd..e9ffa3111 100644 --- a/config/initializers/types.rb +++ b/config/initializers/types.rb @@ -4,7 +4,6 @@ module Types include Dry.Types() include Dry::Core::Constants - User = Instance(User) | Instance(Guest) Parent = Instance(Training::Module) | Instance(Course) TrainingModule = Instance(Training::Module) diff --git a/config/locales/en.yml b/config/locales/en.yml index a5dcf6fcf..ced6ad15f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -570,11 +570,6 @@ en: complete: | # You have already submitted feedback - Thank you for helping to improve this training - # /feedback/thank-you - thank_you: | - # Thank you - Thank you for helping to improve this training # one-off question research: | diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 0ffef3033..3a0390eef 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 205 + expect(locales.count).to be 204 end it 'dot separated key -> Page::Resource#name' do diff --git a/spec/models/concerns/pagination_spec.rb b/spec/models/concerns/pagination_spec.rb index 9267b9c30..4d53ae358 100644 --- a/spec/models/concerns/pagination_spec.rb +++ b/spec/models/concerns/pagination_spec.rb @@ -36,7 +36,7 @@ end end - context '#with_parent' do + describe '#with_parent' do subject(:page) { mod.page_by_name('feedback-checkbox-other-or') } let(:mod) { Training::Module.by_name(:bravo) } diff --git a/spec/models/course_spec.rb b/spec/models/course_spec.rb index 244a5cb4d..9f615ef8e 100644 --- a/spec/models/course_spec.rb +++ b/spec/models/course_spec.rb @@ -16,9 +16,14 @@ expect(course.internal_mailbox).to eq 'child-development.training@education.gov.uk' end - it 'feedback' do - expect(course.feedback.count).to eq 8 - expect(course.feedback.first.page_type).to eq 'feedback' + it 'pages' do + expect(course.pages.count).to eq 9 + expect(course.pages.first.page_type).to eq 'feedback' + expect(course.pages.last.page_type).to eq 'text_page' + end + + it 'feedback_questions' do + expect(course.feedback_questions.count).to eq 8 end end @@ -30,7 +35,7 @@ it 'parent has pages' do expect(parent.pages.first).to be_a Training::Question - expect(pages.first.parent.pages.last).to be_a Training::Question + expect(pages.first.parent.pages.last).to be_a Training::Page end it 'pages have a parent' do diff --git a/spec/services/course_progress_spec.rb b/spec/services/course_progress_spec.rb index 9dc54e15a..e953819fe 100644 --- a/spec/services/course_progress_spec.rb +++ b/spec/services/course_progress_spec.rb @@ -117,7 +117,7 @@ draft: false started: false completed: false - last: 1-3-3-5 + last: 1-3-3-5-bravo certificate: 1-3-4 milestone: N/A --- diff --git a/spec/system/account_page_spec.rb b/spec/system/account_page_spec.rb index dcd4b5331..88b2aef0a 100644 --- a/spec/system/account_page_spec.rb +++ b/spec/system/account_page_spec.rb @@ -65,5 +65,16 @@ expect(page).to have_text 'Developer' expect(page).to have_text 'Not applicable' end + + it 'research participation preference' do + expect(page).to have_text 'You have chosen to participate in research.' + click_on 'Change research preferences' + expect(page).to have_current_path '/feedback/feedback-skippable' + choose 'Option 2' + click_button 'Next' + expect(page).to have_current_path '/my-account' + expect(page).to have_text 'You have chosen not to participate in research.' + expect(page).to have_text 'Your details have been updated' + end end end From d825717da5e9ea7e4c3e16bcb9e8d5ed8b3a5a2e Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Thu, 6 Jun 2024 15:40:58 +0100 Subject: [PATCH 70/95] Redact free text feedback on account closure --- app/models/user.rb | 1 + .../registered_user/closing_account_spec.rb | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 3f4bea56b..aad5730ee 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -448,6 +448,7 @@ def redact! closed_at: Time.zone.now, password: 'RedactedUser12!@') + responses.feedback.update_all(text_input: nil) notes.destroy_all end diff --git a/spec/system/registered_user/closing_account_spec.rb b/spec/system/registered_user/closing_account_spec.rb index 8a7c9c45f..343c0d041 100644 --- a/spec/system/registered_user/closing_account_spec.rb +++ b/spec/system/registered_user/closing_account_spec.rb @@ -67,11 +67,16 @@ end context 'when on confirmation page' do - let!(:note) { create(:note) } - before do - user.notes.push(note) - user.save! + user.notes.create(training_module: 'alpha', body: 'this is a note') + user.responses.feedback.create( + training_module: 'course', + question_name: 'feedback-textarea-only', + question_type: 'feedback', + text_input: 'this is feedback', + correct: true, + ) + visit '/my-account/close/confirm' end @@ -96,6 +101,8 @@ expect(user.last_name).to eq 'User' expect(user.email).to have_text 'redacted_user' expect(user.notes.count).to eq 0 + expect(user.responses.feedback.count).to eq 1 + expect(user.responses.feedback.first.text_input).to be_nil expect(user.valid_password?('RedactedUser12!@')).to eq true end end From ef69937ad7ffb18837daffc4ef092c22472facb6 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 7 Jun 2024 09:03:43 +0100 Subject: [PATCH 71/95] Encrypt text input for feedback questions --- app/models/response.rb | 2 ++ spec/system/registered_user/closing_account_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/response.rb b/app/models/response.rb index 7a450420d..14b65e41c 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -8,6 +8,8 @@ class Response < ApplicationRecord belongs_to :assessment, optional: true belongs_to :visit, optional: true + encrypts :text_input + validates :training_module, presence: true validates :question_type, inclusion: { in: %w[formative summative confidence feedback] } validates :answers, presence: true, unless: -> { text_input_only? } diff --git a/spec/system/registered_user/closing_account_spec.rb b/spec/system/registered_user/closing_account_spec.rb index 343c0d041..88657215c 100644 --- a/spec/system/registered_user/closing_account_spec.rb +++ b/spec/system/registered_user/closing_account_spec.rb @@ -68,8 +68,8 @@ context 'when on confirmation page' do before do - user.notes.create(training_module: 'alpha', body: 'this is a note') - user.responses.feedback.create( + user.notes.create!(training_module: 'alpha', body: 'this is a note') + user.responses.feedback.create!( training_module: 'course', question_name: 'feedback-textarea-only', question_type: 'feedback', From 320da8714e7fbc6f6646e9f6cd39e3d49e35d82f Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 7 Jun 2024 10:08:51 +0100 Subject: [PATCH 72/95] Update chrome-path variable for Pa11y workflow --- .github/workflows/pa11y.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pa11y.yml b/.github/workflows/pa11y.yml index 26b810ed4..f0b4c4eb5 100644 --- a/.github/workflows/pa11y.yml +++ b/.github/workflows/pa11y.yml @@ -36,6 +36,7 @@ jobs: cache: npm - name: Install Chrome + id: setup-chrome uses: browser-actions/setup-chrome@latest - name: Install pa11y-ci @@ -48,7 +49,7 @@ jobs: - name: Audit env: - PUPPETEER_EXECUTABLE_PATH: /opt/hostedtoolcache/chromium/latest/x64/chrome + PUPPETEER_EXECUTABLE_PATH: ${{ steps.setup-chrome.outputs.chrome-path }} run: pa11y-ci --sitemap https://${DOMAIN}/sitemap.xml > report.txt - name: Report From e6e4bc809a65ba642b8a4fdbcd9df0325b3b14ed Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 7 Jun 2024 14:11:00 +0100 Subject: [PATCH 73/95] Bump coverage threshold to 93% --- docker-compose.yml | 1 - spec/spec_helper.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5342043ff..dbca3fff8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,6 @@ # # --- -version: '3.8' services: app: container_name: recovery_prod diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 11b62c3a5..7e3ad5b97 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,5 @@ require 'simplecov' -SimpleCov.minimum_coverage 92 +SimpleCov.minimum_coverage 93 SimpleCov.start 'rails' require 'pry' From 29e085cae028239247e2705ed8793ae2b357c34a Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 10 Jun 2024 11:11:46 +0100 Subject: [PATCH 74/95] - change back link to use browser history - prevent duplicate course feedback milestone events --- app/controllers/feedback_controller.rb | 21 +++++++++++++-------- app/views/feedback/show.html.slim | 2 +- spec/system/course_feedback_spec.rb | 10 ++++++++++ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 98eabe618..59b715e98 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -87,19 +87,24 @@ def feedback_cookie cookies[:course_feedback] = { value: current_user.visit_token } end + # @return [Boolean, nil] def track_feedback_start - track('feedback_start') if feedback_start_untracked? + track('feedback_start') if untracked?('feedback_start') end + # @return [Boolean, nil] def track_feedback_complete - track('feedback_complete') if feedback_complete_untracked? + track('feedback_complete') if untracked?('feedback_complete') end - def feedback_start_untracked? - untracked?('feedback_start', training_module_id: mod.name) - end - - def feedback_complete_untracked? - untracked?('feedback_complete', training_module_id: mod.name) + # @param key [String] + # @param params [Hash] + # @return [Boolean] + def untracked?(key) + if current_user.guest? + Event.where(visit_id: current_visit, name: key).empty? + else + Event.where(user_id: current_user, name: key).empty? + end end end diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index 58a7d1b12..6466c27aa 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -3,7 +3,7 @@ = form_with model: current_user_response, url: feedback_path, method: :patch do |f| .govuk-grid-row .govuk-grid-column-two-thirds - = govuk_back_link href: my_modules_path + = govuk_back_link href: url_for(:back) = f.govuk_error_summary diff --git a/spec/system/course_feedback_spec.rb b/spec/system/course_feedback_spec.rb index c4734ab09..cd416e12b 100644 --- a/spec/system/course_feedback_spec.rb +++ b/spec/system/course_feedback_spec.rb @@ -16,6 +16,11 @@ expect(Response.course_feedback.count).to be 7 end + it 'records milestone events' do + expect(Event.feedback_start.count).to be 1 + expect(Event.feedback_complete.count).to be 1 + end + it 'is linked to a visit not a user' do expect(Response.course_feedback.first.user).not_to be_present expect(Response.course_feedback.first.visit).to be_present @@ -58,6 +63,11 @@ expect(Response.course_feedback.count).to be 8 end + it 'records milestone events' do + expect(Event.feedback_start.count).to be 1 + expect(Event.feedback_complete.count).to be 1 + end + it 'is linked to a user not a visit' do expect(Response.course_feedback.first.visit).not_to be_present expect(Response.course_feedback.first.user).to be_present From 8c1220814f8d0d0b74c5a84604459737f808f7cd Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 10 Jun 2024 14:34:08 +0100 Subject: [PATCH 75/95] Fix JS functions merged from main --- app/javascript/application.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/app/javascript/application.js b/app/javascript/application.js index b4c68cfe1..c9fa38acc 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -3,19 +3,8 @@ import '@fortawesome/fontawesome-free/js/all'; import './controllers'; -import { initAll } from "govuk-frontend"; +import { initAll } from 'govuk-frontend'; -function nodeListForEach (nodes, callback) { - if (window.NodeList.prototype.forEach) { - return nodes.forEach(callback) - } - for (var i = 0; i < nodes.length; i++) { - callback.call(window, nodes[i], i, nodes) - } -} - -/* document.addEventListener('turbo:load', function() { initAll(); }) -*/ From 7547af4fd9db3714afa842f8793494834240a34c Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 10 Jun 2024 16:36:35 +0100 Subject: [PATCH 76/95] Retain accessible form options where text can be clicked --- app/forms/form_builder.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/forms/form_builder.rb b/app/forms/form_builder.rb index acdd2d984..dd2f9f05d 100644 --- a/app/forms/form_builder.rb +++ b/app/forms/form_builder.rb @@ -31,7 +31,6 @@ def question_radio_button(option) govuk_radio_button :answers, option.id, label: { text: option.label }, - link_errors: true, disabled: option.disabled?, checked: option.checked? end @@ -44,7 +43,6 @@ def other_radio_button(option, text:, more:) govuk_radio_button :answers, option.id, label: { text: option.label }, - link_errors: true, disabled: option.disabled?, checked: option.checked? do if more @@ -62,7 +60,6 @@ def or_radio_button(text:, checked:) govuk_radio_button :answers, 0, label: { text: text }, - link_errors: true, checked: checked end @@ -73,7 +70,6 @@ def or_checkbox_button(text:, checked:) govuk_check_box :answers, 0, label: { text: text }, - link_errors: true, checked: checked end @@ -83,7 +79,6 @@ def question_check_box(option) govuk_check_box :answers, option.id, label: { text: option.label }, - link_errors: true, disabled: option.disabled?, checked: option.checked? end @@ -95,7 +90,6 @@ def other_check_box(option, text:) govuk_check_box :answers, option.id, label: { text: option.label }, - link_errors: true, disabled: option.disabled?, checked: option.checked? do govuk_text_field :text_input, label: { text: text } From e335ccc5426001a6958a86517f0895e1ef399cf9 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 11 Jun 2024 07:59:05 +0100 Subject: [PATCH 77/95] - Bump coverage back above threshold - Remove old code and locale `notice.email_changed` - Fix docs --- app/controllers/close_accounts_controller.rb | 4 -- app/controllers/feedback_controller.rb | 1 - app/controllers/user_controller.rb | 22 -------- app/models/user.rb | 11 ---- config/locales/en.yml | 6 --- data/KPI.md | 55 ++++++++++---------- spec/forms/application_form_spec.rb | 17 ++++++ spec/lib/seed_snippets_spec.rb | 2 +- spec/models/guest_spec.rb | 6 +++ 9 files changed, 52 insertions(+), 72 deletions(-) create mode 100644 spec/forms/application_form_spec.rb diff --git a/app/controllers/close_accounts_controller.rb b/app/controllers/close_accounts_controller.rb index 426777bec..8dc094248 100644 --- a/app/controllers/close_accounts_controller.rb +++ b/app/controllers/close_accounts_controller.rb @@ -46,8 +46,4 @@ def confirm; end def user_params params.require(:user).permit(:closed_reason, :closed_reason_custom) end - - def user_password_params - params.require(:user).permit(:current_password) - end end diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 59b715e98..66b6930a1 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -98,7 +98,6 @@ def track_feedback_complete end # @param key [String] - # @param params [Hash] # @return [Boolean] def untracked?(key) if current_user.guest? diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index 77b2da40a..fbfe8b624 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -23,28 +23,6 @@ def update_name end end - # @see config/initializers/devise.rb - def update_password - if current_user.update_with_password(user_password_params) - track('user_password_change', success: true) - bypass_sign_in(current_user) - redirect_to user_path, notice: 'Your new password has been saved.' - else - track('user_password_change', success: false) - render :edit_password, status: :unprocessable_entity - end - end - - def update_email - if current_user.update(user_params) - track('user_email_change', success: true) - redirect_to user_path, notice: t('notice.email_changed') - else - track('user_email_change', success: false) - render :edit_email, status: :unprocessable_entity - end - end - def update_training_emails if current_user.update(user_params) track('user_training_emails_change', success: true) diff --git a/app/models/user.rb b/app/models/user.rb index aad5730ee..fa84b646e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -287,12 +287,6 @@ def email_delivery_status notify_callback.to_h.fetch('status', 'unknown') end - # @return [String] - def password_last_changed - timestamp = password_changed_events&.last&.time || created_at - timestamp.to_date&.to_formatted_s(:rfc822) - end - # @return [CourseProgress] course activity query interface def course @course ||= CourseProgress.new(user: self) @@ -383,11 +377,6 @@ def role_other? role_type == 'other' end - # @return [Boolean] - def role_applicable? - role_type != 'Not applicable' - end - # return [Boolean] def training_emails_recipient? training_emails || training_emails.nil? diff --git a/config/locales/en.yml b/config/locales/en.yml index ced6ad15f..d28b438fe 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -258,12 +258,6 @@ en: You may wish to revisit some of the learning before taking the test again. - notice: - email_changed: | - We have sent an email to your new email address with a link to click to confirm the change. - - If you have not received the email after a few minutes, please check your spam folder. - # Pages ---------------------------------------------------------------------- # /my-learning diff --git a/data/KPI.md b/data/KPI.md index d02d482a6..c144ca612 100644 --- a/data/KPI.md +++ b/data/KPI.md @@ -86,33 +86,34 @@ Example event data from the `ahoy_visits` table. ## Transactions -| Done | Feature | Key | Controllers | Path | -| :--- | :--- | :--- | :--- | :--- | -| [x] | Homepage | `home_page` | `HomeController` | `/` | -| [x] | Monitoring progress | `learning_page` | `LearningController` | `/my-modules` | -| [x] | Course overview | `course_overview_page` | `Training::ModulesController` | `/modules` | -| [x] | Module overview | `module_overview_page` | `Training::ModulesController` | `/modules/{alpha}` | -| [x] | Module content | `module_content_page` | `Training::PagesController` | `/modules/{alpha}/content-pages/{1}` | -| [x] | Static page content | `static_page` | `PagesController` | `/example-page` | -| [x] | Account completion | `user_registration` | `Registration::Controller` | `/registration/{attr}` | -| [x] | User profile | `profile_page` | `UserController` | `/my-account` | -| [x] | User name change | `user_name_change` | `UserController` | `/my-account/update-name` | -| [x] | User email change | `user_email_change` | `UserController` | `/my-account/update-email` | -| [x] | User password change | `user_password_change` | `UserController` | `/my-account/update-password` | -| [x] | Email address taken | `email_address_taken` | `RegistrationsController` | `/users/sign-up` | -| [x] | User inactivity logout | `error_page` | `ErrorsController` | `/timeout` | -| [x] | 404 Error | `error_page` | `ErrorsController` | `/404` | -| [x] | 500 Error | `error_page` | `ErrorsController` | `/500` | -| [x] | Module start | `module_start` | `Training::PagesController` | `/modules/{alpha}/content-pages/intro` | -| [x] | Module complete | `module_complete` | `Training::ModulesController` | `/modules/{alpha}/certificate` | -| [x] | Questionnaire answered | `questionnaire_answer` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | -| [x] | Summative assessment start | `summative_assessment_start` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | -| [x] | Summative assessment complete | `summative_assessment_complete` | `Training::AssessmentsController` | `/modules/{alpha}/assessment-results/{path}` | -| [x] | Confidence check start | `confidence_check_start` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | -| [x] | Confidence check complete | `confidence_check_complete` | `Training::PagesController` | `/modules/{alpha}/questionnaires/{path}` | -| [x] | User note created | `user_note_created` | `Training::NotesController` | `/my-account/learning-log` | -| [x] | User note updated | `user_note_updated` | `Training::NotesController` | `/my-account/learning-log` | - +| Active | Feature | Key | Controllers | Path | +| :--- | :--- | :--- | :--- | :--- | +| [x] | Homepage | `home_page` | `HomeController` | `/` | +| [x] | Monitoring progress | `learning_page` | `LearningController` | `/my-modules` | +| [x] | Course overview | `course_overview_page` | `Training::ModulesController` | `/modules` | +| [x] | Module overview | `module_overview_page` | `Training::ModulesController` | `/modules/{alpha}` | +| [x] | Module content | `module_content_page` | `Training::PagesController` | `/modules/{alpha}/content-pages/{1}` | +| [x] | Static page content | `static_page` | `PagesController` | `/example-page` | +| [x] | Account completion | `user_registration` | `Registration::Controller` | `/registration/{attr}` | +| [x] | User profile | `profile_page` | `UserController` | `/my-account` | +| [x] | User name change | `user_name_change` | `UserController` | `/my-account/update-name` | +| [ ] | User email change | `user_email_change` | `UserController` | `/my-account/update-email` | +| [ ] | User password change | `user_password_change` | `UserController` | `/my-account/update-password` | +| [x] | Email address taken | `email_address_taken` | `RegistrationsController` | `/users/sign-up` | +| [x] | User inactivity logout | `error_page` | `ErrorsController` | `/timeout` | +| [x] | 404 Error | `error_page` | `ErrorsController` | `/404` | +| [x] | 500 Error | `error_page` | `ErrorsController` | `/500` | +| [x] | Module start | `module_start` | `Training::PagesController` | `/modules/{alpha}/content-pages/intro` | +| [x] | Module complete | `module_complete` | `Training::ModulesController` | `/modules/{alpha}/certificate` | +| [x] | Questionnaire answered | `questionnaire_answer` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | +| [x] | Summative assessment start | `summative_assessment_start` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | +| [x] | Summative assessment complete | `summative_assessment_complete` | `Training::AssessmentsController` | `/modules/{alpha}/assessment-results/{path}` | +| [x] | Confidence check start | `confidence_check_start` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | +| [x] | Confidence check complete | `confidence_check_complete` | `Training::PagesController` | `/modules/{alpha}/questionnaires/{path}` | +| [x] | User note created | `user_note_created` | `Training::NotesController` | `/my-account/learning-log` | +| [x] | User note updated | `user_note_updated` | `Training::NotesController` | `/my-account/learning-log` | +| [x] | Feedback started | `feedback_start` | `FeedbackController` | `/feedback/{1}` | +| [x] | Feedback completed | `feedback_complete` | `FeedbackController` | `/feedback/thank-you` | ## Metrics diff --git a/spec/forms/application_form_spec.rb b/spec/forms/application_form_spec.rb new file mode 100644 index 000000000..dd1772ed6 --- /dev/null +++ b/spec/forms/application_form_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +RSpec.describe ApplicationForm do + subject(:form) { described_class.new } + + describe '#parent title' do + specify do + expect(form.parent.title).to be_nil + end + end + + describe '#save' do + specify do + expect { form.save }.to raise_error ApplicationForm::FormError + end + end +end diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 3a0390eef..5d93ae395 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 204 + expect(locales.count).to be 203 end it 'dot separated key -> Page::Resource#name' do diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb index 871676fff..f9ed310ef 100644 --- a/spec/models/guest_spec.rb +++ b/spec/models/guest_spec.rb @@ -28,4 +28,10 @@ expect(response).to be_a Response end end + + describe '#course_started?' do + specify do + expect(guest).not_to be_course_started + end + end end From 83171d5f077aa2afd27be658088e6a25f5bafc89 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 11 Jun 2024 11:52:31 +0100 Subject: [PATCH 78/95] Page titles and sitemap --- app/views/feedback/index.html.slim | 3 + app/views/feedback/show.html.slim | 3 + app/views/feedback/thank_you.html.slim | 3 + config/sitemap.rb | 5 ++ spec/system/page_title_spec.rb | 79 +++++++++++++++----------- 5 files changed, 60 insertions(+), 33 deletions(-) diff --git a/app/views/feedback/index.html.slim b/app/views/feedback/index.html.slim index ba09aeac2..684d11145 100644 --- a/app/views/feedback/index.html.slim +++ b/app/views/feedback/index.html.slim @@ -1,3 +1,6 @@ +- content_for :page_title do + = html_title 'Feedback' + .govuk-grid-row .govuk-grid-column-full diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index 6466c27aa..11d6129ab 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -1,3 +1,6 @@ +- content_for :page_title do + = html_title content.name + = render 'training/questions/debug' = form_with model: current_user_response, url: feedback_path, method: :patch do |f| diff --git a/app/views/feedback/thank_you.html.slim b/app/views/feedback/thank_you.html.slim index 712045061..795be81a2 100644 --- a/app/views/feedback/thank_you.html.slim +++ b/app/views/feedback/thank_you.html.slim @@ -1,3 +1,6 @@ +- content_for :page_title do + = html_title 'Thank you' + .govuk-grid-row .govuk-grid-column-full h1.govuk-heading-xl= content.heading diff --git a/config/sitemap.rb b/config/sitemap.rb index 8c0338d3b..a1049401f 100644 --- a/config/sitemap.rb +++ b/config/sitemap.rb @@ -41,6 +41,11 @@ add course_overview_path add experts_path + add feedback_index_path + + Course.config.pages.each do |content| + add feedback_path(content.name) + end Training::Module.live.each do |mod| add about_path(mod.name) diff --git a/spec/system/page_title_spec.rb b/spec/system/page_title_spec.rb index c9a65e0f3..92cb5eee9 100644 --- a/spec/system/page_title_spec.rb +++ b/spec/system/page_title_spec.rb @@ -4,6 +4,10 @@ context 'when user is not authenticated' do it { expect(root_path).to have_page_title 'Home page' } + it { expect(feedback_index_path).to have_page_title 'Feedback' } + it { expect(feedback_path('feedback-radio-only')).to have_page_title 'feedback-radio-only' } + it { expect(feedback_path('thank-you')).to have_page_title 'Thank you' } + it { expect(course_overview_path).to have_page_title 'About training' } it { expect(experts_path).to have_page_title 'The experts' } it { expect(about_path('alpha')).to have_page_title 'First Training Module' } @@ -48,39 +52,48 @@ context 'and viewing module content' do [ - ['what-to-expect', 'First Training Module : What to expect during the training'], - ['1-1', 'First Training Module : The first submodule'], - ['1-1-1', 'First Training Module : 1-1-1'], - ['1-1-2', 'First Training Module : 1-1-2'], - ['1-1-3', 'First Training Module : 1-1-3'], - ['1-1-3-1', 'First Training Module : 1-1-3-1'], - ['1-1-4', 'First Training Module : 1-1-4'], - ['1-2', 'First Training Module : The second submodule'], - ['1-2-1', 'First Training Module : 1-2-1'], - ['1-2-1-1', 'First Training Module : 1-2-1-1'], - ['1-2-1-2', 'First Training Module : 1-2-1-2'], - ['1-2-1-3', 'First Training Module : 1-2-1-3'], - ['1-3', 'First Training Module : Summary and next steps'], - ['1-3-1', 'First Training Module : Recap'], - ['1-3-2', 'First Training Module : End of module test'], - ['1-3-2-1', 'First Training Module : 1-3-2-1'], - ['1-3-2-2', 'First Training Module : 1-3-2-2'], - ['1-3-2-3', 'First Training Module : 1-3-2-3'], - ['1-3-2-4', 'First Training Module : 1-3-2-4'], - ['1-3-2-5', 'First Training Module : 1-3-2-5'], - ['1-3-2-6', 'First Training Module : 1-3-2-6'], - ['1-3-2-7', 'First Training Module : 1-3-2-7'], - ['1-3-2-8', 'First Training Module : 1-3-2-8'], - ['1-3-2-9', 'First Training Module : 1-3-2-9'], - ['1-3-2-10', 'First Training Module : 1-3-2-10'], - ['1-3-2-11', 'First Training Module : Assessment results'], - ['1-3-3', 'First Training Module : Reflect on your learning'], - ['1-3-3-1', 'First Training Module : 1-3-3-1'], - ['1-3-3-2', 'First Training Module : 1-3-3-2'], - ['1-3-3-3', 'First Training Module : 1-3-3-3'], - ['1-3-3-4', 'First Training Module : 1-3-3-4'], - ['1-3-3-5', 'First Training Module : Thank you'], - ['1-3-4', 'First Training Module : Download your certificate'], + ['what-to-expect', 'First Training Module : What to expect during the training'], + ['1-1', 'First Training Module : The first submodule'], + ['1-1-1', 'First Training Module : 1-1-1'], + ['1-1-2', 'First Training Module : 1-1-2'], + ['1-1-3', 'First Training Module : 1-1-3'], + ['1-1-3-1', 'First Training Module : 1-1-3-1'], + ['1-1-4', 'First Training Module : 1-1-4'], + ['1-2', 'First Training Module : The second submodule'], + ['1-2-1', 'First Training Module : 1-2-1'], + ['1-2-1-1', 'First Training Module : 1-2-1-1'], + ['1-2-1-2', 'First Training Module : 1-2-1-2'], + ['1-2-1-3', 'First Training Module : 1-2-1-3'], + ['1-3', 'First Training Module : Summary and next steps'], + ['1-3-1', 'First Training Module : Recap'], + ['1-3-2', 'First Training Module : End of module test'], + ['1-3-2-1', 'First Training Module : 1-3-2-1'], + ['1-3-2-2', 'First Training Module : 1-3-2-2'], + ['1-3-2-3', 'First Training Module : 1-3-2-3'], + ['1-3-2-4', 'First Training Module : 1-3-2-4'], + ['1-3-2-5', 'First Training Module : 1-3-2-5'], + ['1-3-2-6', 'First Training Module : 1-3-2-6'], + ['1-3-2-7', 'First Training Module : 1-3-2-7'], + ['1-3-2-8', 'First Training Module : 1-3-2-8'], + ['1-3-2-9', 'First Training Module : 1-3-2-9'], + ['1-3-2-10', 'First Training Module : 1-3-2-10'], + ['1-3-2-11', 'First Training Module : Assessment results'], + ['1-3-3', 'First Training Module : Reflect on your learning'], + ['1-3-3-1', 'First Training Module : 1-3-3-1'], + ['1-3-3-2', 'First Training Module : 1-3-3-2'], + ['1-3-3-3', 'First Training Module : 1-3-3-3'], + ['1-3-3-4', 'First Training Module : 1-3-3-4'], + ['feedback-intro', 'First Training Module : Additional feedback'], + ['feedback-radio-only', 'First Training Module : feedback-radio-only'], + ['feedback-checkbox-only', 'First Training Module : feedback-checkbox-only'], + ['feedback-textarea-only', 'First Training Module : feedback-textarea-only'], + ['feedback-radio-other-more', 'First Training Module : feedback-radio-other-more'], + ['feedback-checkbox-other-more', 'First Training Module : feedback-checkbox-other-more'], + ['feedback-radio-more', 'First Training Module : feedback-radio-more'], + ['feedback-checkbox-other-or', 'First Training Module : feedback-checkbox-other-or'], + ['feedback-skippable', 'First Training Module : feedback-skippable'], + ['1-3-3-5', 'First Training Module : Thank you'], + ['1-3-4', 'First Training Module : Download your certificate'], ].each do |page, title| describe "/modules/alpha/content-pages/#{page}" do let(:path) do From c2c80a003a5d3b77caa350f8035471cc9e6c5bbc Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 11 Jun 2024 12:01:34 +0100 Subject: [PATCH 79/95] full width rule for feedback questions --- app/views/feedback/show.html.slim | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index 11d6129ab..23409525e 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -12,6 +12,7 @@ = render partial: content.to_partial_path, locals: { f: f }, object: current_user_response, as: :response + .govuk-grid-column-full hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' .govuk-button-group From a9d27c70f8b7b083889156cf6efc2cc371ccc968 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 11 Jun 2024 12:48:17 +0100 Subject: [PATCH 80/95] Fix previous button with skipped pages and guest users --- app/decorators/previous_page_decorator.rb | 38 ++++++++++++++--------- app/models/concerns/pagination.rb | 10 ++++++ app/views/feedback/show.html.slim | 2 +- spec/models/concerns/pagination_spec.rb | 4 ++- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/app/decorators/previous_page_decorator.rb b/app/decorators/previous_page_decorator.rb index 34d078312..1a688b9d4 100644 --- a/app/decorators/previous_page_decorator.rb +++ b/app/decorators/previous_page_decorator.rb @@ -6,11 +6,11 @@ class PreviousPageDecorator extend Dry::Initializer # @!attribute [r] user - # @return [User] - option :user, Types.Instance(User), required: true + # @return [User, Guest] + option :user, Types.Instance(User) | Types.Instance(Guest), required: true # @!attribute [r] mod - # @return [Training::Module] - option :mod, Types::TrainingModule, required: true + # @return [Course, Training::Module] + option :mod, Types::Parent, required: true # @!attribute [r] content # @return [Training::Page, Training::Question, Training::Video] option :content, Types::TrainingContent, required: true @@ -18,11 +18,11 @@ class PreviousPageDecorator # @return [String] def name if skip_previous_question? - content.previous_item.previous_item.name + previous_previous_item.name elsif feedback_not_started? mod.feedback_questions.first.previous_item.name else - content.previous_item.name + previous_item.name end end @@ -44,6 +44,16 @@ def label I18n.t(:previous_page) end + # @return [Boolean] + def content_section? + content.section? && !content.feedback_question? + end + + # @return [Boolean] + def skip_previous_question? + user.skip_question?(previous_item) + end + # @return [Boolean] def answered?(question) return false unless question.feedback_question? @@ -52,17 +62,17 @@ def answered?(question) end # @return [Boolean] - def content_section? - content.section? && !content.feedback_question? + def feedback_not_started? + content.thankyou? && !answered?(previous_item) end - # @return [Boolean] - def skip_previous_question? - content.previous_item.skippable? && answered?(content.previous_item) + # @return [Training::Page, Training::Question, Training::Video] + def previous_previous_item + content.with_parent(mod).previous_previous_item end - # @return [Boolean] - def feedback_not_started? - content.thankyou? && !answered?(content.previous_item) + # @return [Training::Page, Training::Question, Training::Video] + def previous_item + content.with_parent(mod).previous_item end end diff --git a/app/models/concerns/pagination.rb b/app/models/concerns/pagination.rb index 873e60144..c1759ea8d 100644 --- a/app/models/concerns/pagination.rb +++ b/app/models/concerns/pagination.rb @@ -37,11 +37,21 @@ def next_next_item parent.page_by_id(next_next_item_id) end + # @return [nil, Training::Page, Training::Video, Training::Question] + def previous_previous_item + parent.page_by_id(previous_previous_item_id) + end + # @return [String] def previous_item_id parent.pages[content_index - 1].id end + # @return [String, nil] + def previous_previous_item_id + parent.pages[content_index - 2]&.id + end + # @return [String, nil] def next_item_id parent.pages[content_index + 1]&.id diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index 23409525e..542764615 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -19,6 +19,6 @@ - if content.first_feedback? = govuk_button_link_to t('previous_page.previous'), feedback_index_path, secondary: true - else - = govuk_button_link_to t('previous_page.previous'), feedback_path(content.previous_item.name), secondary: true + = govuk_button_link_to t('previous_page.previous'), feedback_path(previous_page.name), secondary: true = f.govuk_submit t('next_page.next') diff --git a/spec/models/concerns/pagination_spec.rb b/spec/models/concerns/pagination_spec.rb index 4d53ae358..92d90d966 100644 --- a/spec/models/concerns/pagination_spec.rb +++ b/spec/models/concerns/pagination_spec.rb @@ -44,11 +44,13 @@ it 'navigates shared content in the correct parent' do expect(page.parent.name).to eq 'alpha' expect(page.with_parent(mod).parent.name).to eq 'bravo' - expect(page.with_parent(mod).next_item.parent.name).to eq 'alpha' expect(page.next_item.name).to eq 'feedback-skippable' expect(page.with_parent(mod).next_item.name).to eq 'feedback-skippable' expect(page.with_parent(mod).next_next_item.name).to eq '1-3-3-5-bravo' + + expect(page.with_parent(mod).previous_item.name).to eq 'feedback-radio-more' + expect(page.with_parent(mod).previous_previous_item.name).to eq 'feedback-checkbox-other-more' end end end From 4c2ab480380a00aeb2ce6b8d27e51d1291633ded Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Wed, 12 Jun 2024 12:00:39 +0100 Subject: [PATCH 81/95] Export guest and registered user feedback responses --- .../data_analysis/guest_feedback_scores.rb | 27 +++++ .../data_analysis/user_feedback_scores.rb | 50 +++++++++ app/models/response.rb | 2 + app/models/user.rb | 13 ++- app/services/dashboard.rb | 2 + spec/factories/users.rb | 6 +- .../guest_feedback_scores_spec.rb | 41 +++++++ .../user_feedback_scores_spec.rb | 102 ++++++++++++++++++ 8 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 app/models/data_analysis/guest_feedback_scores.rb create mode 100644 app/models/data_analysis/user_feedback_scores.rb create mode 100644 spec/models/data_analysis/guest_feedback_scores_spec.rb create mode 100644 spec/models/data_analysis/user_feedback_scores_spec.rb diff --git a/app/models/data_analysis/guest_feedback_scores.rb b/app/models/data_analysis/guest_feedback_scores.rb new file mode 100644 index 000000000..8a50e441b --- /dev/null +++ b/app/models/data_analysis/guest_feedback_scores.rb @@ -0,0 +1,27 @@ +module DataAnalysis + class GuestFeedbackScores + include ToCsv + + class << self + # @return [Array] + def column_names + %w[ + Guest + Question + Answers + ] + end + + # @return [Array Mixed}>] + def dashboard + Response.visitor.feedback.select( + :visit_id, + :question_name, + :answers, + ).map do |user| + user.attributes.symbolize_keys.except(:id) + end + end + end + end +end diff --git a/app/models/data_analysis/user_feedback_scores.rb b/app/models/data_analysis/user_feedback_scores.rb new file mode 100644 index 000000000..6e253a54e --- /dev/null +++ b/app/models/data_analysis/user_feedback_scores.rb @@ -0,0 +1,50 @@ +module DataAnalysis + class UserFeedbackScores + include ToCsv + + class << self + # @return [Array] + def column_names + [ + 'User ID', + 'Role', + 'Role Other', + 'Setting', + 'Setting Other', + 'Local Authority', + 'Years Experience', + 'Module', + 'Question', + 'Answers', + ] + end + + # @return [Array Mixed}>] + def dashboard + User.with_feedback.order(:user_id).select(*agreed_attributes).map do |user| + user.attributes.symbolize_keys.except(:id) + end + end + + private + + # @note Personally identifiable information must not be revealed + # + # @return [Array] + def agreed_attributes + %i[ + user_id + role_type + role_type_other + setting_type + setting_type_other + local_authority + early_years_experience + training_module + question_name + answers + ] + end + end + end +end diff --git a/app/models/response.rb b/app/models/response.rb index 14b65e41c..93484dba5 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -17,6 +17,8 @@ class Response < ApplicationRecord scope :incorrect, -> { where(correct: false) } scope :correct, -> { where(correct: true) } + scope :visitor, -> { where(user_id: nil) } + scope :ungraded, -> { where(graded: false) } scope :graded, -> { where(graded: true) } diff --git a/app/models/user.rb b/app/models/user.rb index fa84b646e..c5ce01472 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -66,13 +66,16 @@ def test_user? devise :database_authenticatable, :rememberable, :lockable, :timeoutable, :omniauthable, omniauth_providers: [:openid_connect] - has_many :responses - has_many :user_answers - if Rails.application.migrated_answers? + has_many :responses + has_many :feedback_responses, -> { where(question_type: 'feedback') }, class_name: 'Response' has_many :assessments + + # feedback + scope :with_feedback, -> { joins(:responses).merge(Response.feedback) } else has_many :user_assessments + has_many :user_answers end has_many :visits @@ -475,6 +478,10 @@ def visit_token visits.last.visit_token end + def feedback_attributes + data_attributes + end + private # @return [Hash] diff --git a/app/services/dashboard.rb b/app/services/dashboard.rb index 3ddfd6cad..ea2653619 100644 --- a/app/services/dashboard.rb +++ b/app/services/dashboard.rb @@ -35,6 +35,8 @@ class Dashboard { model: 'DataAnalysis::UserModuleCompletionCount', folder: 'nonlinear', file: 'user_module_completions_count' }, { model: 'DataAnalysis::ReturningUsers', folder: 'nonlinear', file: 'returning_users' }, { model: 'DataAnalysis::ModuleFeedbackForms', folder: 'feedback', file: 'feedback_forms' }, + { model: 'DataAnalysis::UserFeedbackScores', folder: 'feedback', file: 'feedback_user_scores' }, + { model: 'DataAnalysis::GuestFeedbackScores', folder: 'feedback', file: 'feedback_guest_scores' }, ].freeze # @return [String] 30-06-2022-09-30 diff --git a/spec/factories/users.rb b/spec/factories/users.rb index b66b37932..f1ad95aab 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -40,10 +40,12 @@ trait :agency_childminder do registered setting_type_id { 'childminder_agency' } - setting_type { 'Childminder as part of an agency' } # Why are we persisting the titles? + setting_type { 'Childminder as part of an agency' } setting_type_other { nil } role_type { 'Childminder' } role_type_other { nil } + early_years_experience { '6-9' } + local_authority { 'Hertfordshire' } end trait :independent_childminder do @@ -53,6 +55,8 @@ setting_type_other { nil } role_type { 'Childminder' } role_type_other { nil } + early_years_experience { '0-2' } + local_authority { 'Leeds' } end trait :team_member do diff --git a/spec/models/data_analysis/guest_feedback_scores_spec.rb b/spec/models/data_analysis/guest_feedback_scores_spec.rb new file mode 100644 index 000000000..bd9af855b --- /dev/null +++ b/spec/models/data_analysis/guest_feedback_scores_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +RSpec.describe DataAnalysis::GuestFeedbackScores do + let(:user) { create :user, :registered } + let(:guest) { Guest.new(visit: Visit.new) } + let(:headers) do + %w[ + Guest + Question + Answers + ] + end + let(:rows) do + [ + { + visit_id: guest.visit.id, + question_name: 'feedback-radio-only', + answers: [1], + }, + { + visit_id: guest.visit.id, + question_name: 'feedback-checkbox-only', + answers: [1, 2], + }, + ] + end + + before do + skip unless Rails.application.migrated_answers? + + # users not included + create(:response, question_type: 'feedback', training_module: 'course', question_name: 'feedback-checkbox-only', answers: [1, 2]) + create(:response, question_type: 'feedback', training_module: 'alpha', question_name: 'feedback-textarea-only', text_input: 'opinion', answers: []) + + # guest responses + create(:response, user: nil, visit: guest.visit, question_type: 'feedback', training_module: 'course', question_name: 'feedback-radio-only', answers: [1]) + create(:response, user: nil, visit: guest.visit, question_type: 'feedback', training_module: 'course', question_name: 'feedback-checkbox-only', answers: [1, 2]) + end + + it_behaves_like 'a data export model' +end diff --git a/spec/models/data_analysis/user_feedback_scores_spec.rb b/spec/models/data_analysis/user_feedback_scores_spec.rb new file mode 100644 index 000000000..fd33baeb2 --- /dev/null +++ b/spec/models/data_analysis/user_feedback_scores_spec.rb @@ -0,0 +1,102 @@ +require 'rails_helper' + +RSpec.describe DataAnalysis::UserFeedbackScores do + let(:user_1) { create :user, :agency_childminder } + let(:user_2) { create :user, :independent_childminder } + let(:headers) do + [ + 'User ID', + 'Role', + 'Role Other', + 'Setting', + 'Setting Other', + 'Local Authority', + 'Years Experience', + 'Module', + 'Question', + 'Answers', + ] + end + let(:rows) do + [ + { + user_id: user_1.id, + role_type: 'Childminder', + role_type_other: nil, + setting_type: 'Childminder as part of an agency', + setting_type_other: nil, + local_authority: 'Hertfordshire', + early_years_experience: '6-9', + training_module: 'course', + question_name: 'feedback-textarea-only', + answers: [], + }, + { + user_id: user_1.id, + role_type: 'Childminder', + role_type_other: nil, + setting_type: 'Childminder as part of an agency', + setting_type_other: nil, + local_authority: 'Hertfordshire', + early_years_experience: '6-9', + training_module: 'course', + question_name: 'feedback-checkbox-only', + answers: [1, 2], + }, + { + user_id: user_2.id, + role_type: 'Childminder', + role_type_other: nil, + setting_type: 'Independent childminder', + setting_type_other: nil, + local_authority: 'Leeds', + early_years_experience: '0-2', + training_module: 'alpha', + question_name: 'feedback-textarea-only', + answers: [], + }, + { + user_id: user_2.id, + role_type: 'Childminder', + role_type_other: nil, + setting_type: 'Independent childminder', + setting_type_other: nil, + local_authority: 'Leeds', + early_years_experience: '0-2', + training_module: 'alpha', + question_name: 'feedback-checkbox-only', + answers: [1, 2, 3, 4], + }, + { + user_id: user_2.id, + role_type: 'Childminder', + role_type_other: nil, + setting_type: 'Independent childminder', + setting_type_other: nil, + local_authority: 'Leeds', + early_years_experience: '0-2', + training_module: 'alpha', + question_name: 'feedback-radio-only', + answers: [1], + }, + ] + end + + before do + skip unless Rails.application.migrated_answers? + + # guests not included + create(:response, user: nil, visit: Visit.new, question_type: 'feedback', training_module: 'course', question_name: 'feedback-radio-only', answers: [1]) + + # course + create(:response, user: user_1, question_type: 'feedback', training_module: 'course', question_name: 'feedback-checkbox-only', answers: [1, 2]) + create(:response, user: user_1, question_type: 'feedback', training_module: 'course', question_name: 'feedback-textarea-only', text_input: 'potential PII', answers: []) + + # module + create(:response, user: user_2, question_type: 'feedback', training_module: 'alpha', question_name: 'feedback-radio-only', answers: [1]) + create(:response, user: user_2, question_type: 'feedback', training_module: 'alpha', question_name: 'feedback-checkbox-only', answers: [1, 2, 3, 4]) + create(:response, user: user_2, question_type: 'feedback', training_module: 'alpha', question_name: 'feedback-textarea-only', text_input: 'potential PII', answers: []) + end + + it_behaves_like 'a data export model' +end From 9f3dbb5cac2063af2aaa6979d5dc90778315e546 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Wed, 12 Jun 2024 16:35:40 +0100 Subject: [PATCH 82/95] Consistent spec order --- .../data_analysis/guest_feedback_scores.rb | 2 +- .../data_analysis/user_feedback_scores.rb | 6 ++--- .../guest_feedback_scores_spec.rb | 10 ++++---- .../user_feedback_scores_spec.rb | 24 +++++++++---------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/models/data_analysis/guest_feedback_scores.rb b/app/models/data_analysis/guest_feedback_scores.rb index 8a50e441b..1d0594eb1 100644 --- a/app/models/data_analysis/guest_feedback_scores.rb +++ b/app/models/data_analysis/guest_feedback_scores.rb @@ -14,7 +14,7 @@ def column_names # @return [Array Mixed}>] def dashboard - Response.visitor.feedback.select( + Response.visitor.feedback.order(:visit_id, :question_name).select( :visit_id, :question_name, :answers, diff --git a/app/models/data_analysis/user_feedback_scores.rb b/app/models/data_analysis/user_feedback_scores.rb index 6e253a54e..04caf357e 100644 --- a/app/models/data_analysis/user_feedback_scores.rb +++ b/app/models/data_analysis/user_feedback_scores.rb @@ -8,9 +8,9 @@ def column_names [ 'User ID', 'Role', - 'Role Other', + 'Custom Role', 'Setting', - 'Setting Other', + 'Custom Setting', 'Local Authority', 'Years Experience', 'Module', @@ -21,7 +21,7 @@ def column_names # @return [Array Mixed}>] def dashboard - User.with_feedback.order(:user_id).select(*agreed_attributes).map do |user| + User.with_feedback.order(:user_id, :question_name).select(*agreed_attributes).map do |user| user.attributes.symbolize_keys.except(:id) end end diff --git a/spec/models/data_analysis/guest_feedback_scores_spec.rb b/spec/models/data_analysis/guest_feedback_scores_spec.rb index bd9af855b..c84e942c0 100644 --- a/spec/models/data_analysis/guest_feedback_scores_spec.rb +++ b/spec/models/data_analysis/guest_feedback_scores_spec.rb @@ -13,15 +13,15 @@ let(:rows) do [ { - visit_id: guest.visit.id, - question_name: 'feedback-radio-only', - answers: [1], - }, - { visit_id: guest.visit.id, question_name: 'feedback-checkbox-only', answers: [1, 2], }, + { + visit_id: guest.visit.id, + question_name: 'feedback-radio-only', + answers: [1], + }, ] end diff --git a/spec/models/data_analysis/user_feedback_scores_spec.rb b/spec/models/data_analysis/user_feedback_scores_spec.rb index fd33baeb2..af309d3ba 100644 --- a/spec/models/data_analysis/user_feedback_scores_spec.rb +++ b/spec/models/data_analysis/user_feedback_scores_spec.rb @@ -7,9 +7,9 @@ [ 'User ID', 'Role', - 'Role Other', + 'Custom Role', 'Setting', - 'Setting Other', + 'Custom Setting', 'Local Authority', 'Years Experience', 'Module', @@ -28,8 +28,8 @@ local_authority: 'Hertfordshire', early_years_experience: '6-9', training_module: 'course', - question_name: 'feedback-textarea-only', - answers: [], + question_name: 'feedback-checkbox-only', + answers: [1, 2], }, { user_id: user_1.id, @@ -40,8 +40,8 @@ local_authority: 'Hertfordshire', early_years_experience: '6-9', training_module: 'course', - question_name: 'feedback-checkbox-only', - answers: [1, 2], + question_name: 'feedback-textarea-only', + answers: [], }, { user_id: user_2.id, @@ -52,8 +52,8 @@ local_authority: 'Leeds', early_years_experience: '0-2', training_module: 'alpha', - question_name: 'feedback-textarea-only', - answers: [], + question_name: 'feedback-checkbox-only', + answers: [1, 2, 3, 4], }, { user_id: user_2.id, @@ -64,8 +64,8 @@ local_authority: 'Leeds', early_years_experience: '0-2', training_module: 'alpha', - question_name: 'feedback-checkbox-only', - answers: [1, 2, 3, 4], + question_name: 'feedback-radio-only', + answers: [1], }, { user_id: user_2.id, @@ -76,8 +76,8 @@ local_authority: 'Leeds', early_years_experience: '0-2', training_module: 'alpha', - question_name: 'feedback-radio-only', - answers: [1], + question_name: 'feedback-textarea-only', + answers: [], }, ] end From ad57f831ce52b6e7d2c4bfc17b2a7532a8932793 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Thu, 13 Jun 2024 09:55:34 +0100 Subject: [PATCH 83/95] Tidy and fix UR research preference check your answers --- app/models/user.rb | 18 ++++++++------ spec/system/account_page_spec.rb | 42 ++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index a12bbe3b3..ec42066d2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -380,22 +380,19 @@ def role_other? role_type == 'other' end - # return [Boolean] + # @return [Boolean] def training_emails_recipient? training_emails || training_emails.nil? end - # return [Boolean] + # @return [Boolean] def research_participant? - response = responses.feedback.find { |preference| preference.question.skippable? } - if response.nil? + if user_research_response.nil? update(research_participant: false) false else - option = response.question.options(checked: response.answers).find(&:checked?) - opt_in = option.id.eql?(1) - update(research_participant: opt_in) - opt_in + update(research_participant: user_research_response.answers.eql?([1])) + research_participant end end @@ -490,6 +487,11 @@ def feedback_attributes private + # @return [Response] + def user_research_response + responses.feedback.find { |response| response.question.skippable? } + end + # @return [Hash] def data_attributes DASHBOARD_ATTRS.map { |field| { field => send(field) } }.reduce(&:merge) diff --git a/spec/system/account_page_spec.rb b/spec/system/account_page_spec.rb index 88b2aef0a..7edf13767 100644 --- a/spec/system/account_page_spec.rb +++ b/spec/system/account_page_spec.rb @@ -4,14 +4,6 @@ include_context 'with user' before do - create :response, - question_name: 'feedback-skippable', - training_module: 'course', - answers: [1], - correct: true, - user: user, - question_type: 'feedback' - visit '/my-account' end @@ -29,7 +21,7 @@ expect(page).to have_text 'You have chosen to receive emails about this training course.' expect(page).to have_link 'Change research preferences' - expect(page).to have_text 'You have chosen to participate in research.' + expect(page).to have_text 'You have chosen not to participate in research.' expect(page).to have_text 'Closing your account' end @@ -66,15 +58,29 @@ expect(page).to have_text 'Not applicable' end - it 'research participation preference' do - expect(page).to have_text 'You have chosen to participate in research.' - click_on 'Change research preferences' - expect(page).to have_current_path '/feedback/feedback-skippable' - choose 'Option 2' - click_button 'Next' - expect(page).to have_current_path '/my-account' - expect(page).to have_text 'You have chosen not to participate in research.' - expect(page).to have_text 'Your details have been updated' + describe 'research participation preference' do + before do + create :response, + question_name: 'feedback-skippable', + training_module: 'course', + answers: [1], + correct: true, + user: user, + question_type: 'feedback' + + visit '/my-account' + end + + it 'changes response' do + expect(page).to have_text 'You have chosen to participate in research.' + click_on 'Change research preferences' + expect(page).to have_current_path '/feedback/feedback-skippable' + choose 'Option 2' + click_button 'Next' + expect(page).to have_current_path '/my-account' + expect(page).to have_text 'You have chosen not to participate in research.' + expect(page).to have_text 'Your details have been updated' + end end end end From b159f4f5755b499094a24a3b2e6860928baef4f8 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Thu, 13 Jun 2024 11:32:52 +0100 Subject: [PATCH 84/95] Refactor and simplify research_participant? Ensure assessment data exports without errors by adding scope Replace submission buttons for altering research participation preference --- app/models/data_analysis/modules_per_month.rb | 2 +- app/models/user.rb | 10 +++------- app/views/feedback/show.html.slim | 11 +++++++---- spec/models/data_analysis/modules_per_month_spec.rb | 1 + spec/system/account_page_spec.rb | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/models/data_analysis/modules_per_month.rb b/app/models/data_analysis/modules_per_month.rb index cefe79fa5..32b276a13 100644 --- a/app/models/data_analysis/modules_per_month.rb +++ b/app/models/data_analysis/modules_per_month.rb @@ -31,7 +31,7 @@ def dashboard # @return [Hash] def assessments_by_month if Rails.application.migrated_answers? - Assessment.all.group_by { |assessment| assessment.completed_at.strftime('%B %Y') } + Assessment.complete.group_by { |assessment| assessment.completed_at.strftime('%B %Y') } else UserAssessment.all.group_by { |assessment| assessment.created_at.strftime('%B %Y') } end diff --git a/app/models/user.rb b/app/models/user.rb index ec42066d2..d03397148 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -387,13 +387,9 @@ def training_emails_recipient? # @return [Boolean] def research_participant? - if user_research_response.nil? - update(research_participant: false) - false - else - update(research_participant: user_research_response.answers.eql?([1])) - research_participant - end + preference = user_research_response.nil? ? false : user_research_response.answers.eql?([1]) + update(research_participant: preference) + research_participant end # @return [Boolean] diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index 542764615..f32a3aa59 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -16,9 +16,12 @@ hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' .govuk-button-group - - if content.first_feedback? - = govuk_button_link_to t('previous_page.previous'), feedback_index_path, secondary: true + - if !current_user.guest? && current_user.events.last.name.eql?('profile_page') + = f.govuk_submit t('links.save') - else - = govuk_button_link_to t('previous_page.previous'), feedback_path(previous_page.name), secondary: true + - if content.first_feedback? + = govuk_button_link_to t('previous_page.previous'), feedback_index_path, secondary: true + - else + = govuk_button_link_to t('previous_page.previous'), feedback_path(previous_page.name), secondary: true - = f.govuk_submit t('next_page.next') + = f.govuk_submit t('next_page.next') diff --git a/spec/models/data_analysis/modules_per_month_spec.rb b/spec/models/data_analysis/modules_per_month_spec.rb index 0b03535af..750f0e4da 100644 --- a/spec/models/data_analysis/modules_per_month_spec.rb +++ b/spec/models/data_analysis/modules_per_month_spec.rb @@ -50,6 +50,7 @@ create :assessment, :passed, user: user_1, completed_at: Time.zone.local(2023, 1, 1) create :assessment, :failed, user: user_1, completed_at: Time.zone.local(2023, 2, 1) create :assessment, :passed, user: user_2, completed_at: Time.zone.local(2023, 3, 1) + create :assessment, user: user_2, training_module: 'bravo' else create(:user_assessment, :passed, user_id: user_1.id, score: 100, module: 'alpha', created_at: Time.zone.local(2023, 1, 1)) create(:user_assessment, :failed, user_id: user_1.id, score: 0, module: 'alpha', created_at: Time.zone.local(2023, 2, 1)) diff --git a/spec/system/account_page_spec.rb b/spec/system/account_page_spec.rb index 7edf13767..2775992fd 100644 --- a/spec/system/account_page_spec.rb +++ b/spec/system/account_page_spec.rb @@ -76,7 +76,7 @@ click_on 'Change research preferences' expect(page).to have_current_path '/feedback/feedback-skippable' choose 'Option 2' - click_button 'Next' + click_button 'Save' expect(page).to have_current_path '/my-account' expect(page).to have_text 'You have chosen not to participate in research.' expect(page).to have_text 'Your details have been updated' From 4b238e740abf2f6e52e6c75f9de9b2d2c714ce00 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 14 Jun 2024 12:06:25 +0100 Subject: [PATCH 85/95] Tweak UR redirects --- app/controllers/feedback_controller.rb | 7 +-- app/models/guest.rb | 5 ++ app/models/user.rb | 5 ++ app/services/module_progress.rb | 15 ------ app/views/feedback/show.html.slim | 2 +- data/KPI.md | 57 +++++++++----------- lib/tasks/eyfs.rake | 6 --- spec/controllers/feedback_controller_spec.rb | 21 ++++++-- 8 files changed, 54 insertions(+), 64 deletions(-) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 66b6930a1..770a29aab 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -13,7 +13,7 @@ def show end def update - if research_participation_updated? + if current_user.profile_updated? if save_response! flash[:success] = 'Your details have been updated' redirect_to user_path @@ -32,11 +32,6 @@ def update private - # @return [Boolean] - def research_participation_updated? - current_user_response.question.skippable? && current_user_response.persisted? - end - # @return [Boolean] def save_response! current_user_response.update( diff --git a/app/models/guest.rb b/app/models/guest.rb index d72acba87..4d9effb3e 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -19,6 +19,11 @@ def course_started? false end + # @return [Boolean] + def profile_updated? + false + end + # @param question [Training::Question] # @return [Boolean] def skip_question?(question) diff --git a/app/models/user.rb b/app/models/user.rb index d03397148..a33de5c98 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -449,6 +449,11 @@ def dashboard_row data_attributes.dup.merge(module_ttc) end + # @return [Boolean] + def profile_updated? + events.any? && events.last.name.eql?('profile_page') + end + # @return [Boolean] def completed_available_modules? available_modules = ModuleRelease.pluck(:name) diff --git a/app/services/module_progress.rb b/app/services/module_progress.rb index c66f93516..c12152555 100644 --- a/app/services/module_progress.rb +++ b/app/services/module_progress.rb @@ -39,24 +39,9 @@ def furthest_page # @return [Training::Page, Training::Question, Training::Video] def resume_page - # unvisited.first&.previous_item || mod.first_content_page mod.page_by_name(milestone) || mod.first_content_page end - # Identify new content that has not been seen and would effect module state - # - # @see FillPageViews task - # @return [Boolean] - def skipped? - if unvisited.none? - false - elsif completed? && unvisited.any? # seen last content page but has gaps - true - elsif gaps? - true - end - end - # @see CourseProgress # @return [Boolean] def completed? diff --git a/app/views/feedback/show.html.slim b/app/views/feedback/show.html.slim index f32a3aa59..cd9aa0a2e 100644 --- a/app/views/feedback/show.html.slim +++ b/app/views/feedback/show.html.slim @@ -16,7 +16,7 @@ hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class='govuk-!-margin-top-4' .govuk-button-group - - if !current_user.guest? && current_user.events.last.name.eql?('profile_page') + - if current_user.profile_updated? = f.govuk_submit t('links.save') - else - if content.first_feedback? diff --git a/data/KPI.md b/data/KPI.md index c144ca612..19227977f 100644 --- a/data/KPI.md +++ b/data/KPI.md @@ -32,9 +32,6 @@ Event.where(name: 'module_content_page').where_properties(seed: true).count #=> 8680 ``` -- `skipped: true` to preserve a user's progress when new module content pages are injected ([FillPageViews](../../FillPageViews)). - This will be necessary until content versioning is implemented. - - `cloned: true` where named events such as `module_start` are created for users who have already started the module. #### Event Dictionary @@ -86,34 +83,32 @@ Example event data from the `ahoy_visits` table. ## Transactions -| Active | Feature | Key | Controllers | Path | -| :--- | :--- | :--- | :--- | :--- | -| [x] | Homepage | `home_page` | `HomeController` | `/` | -| [x] | Monitoring progress | `learning_page` | `LearningController` | `/my-modules` | -| [x] | Course overview | `course_overview_page` | `Training::ModulesController` | `/modules` | -| [x] | Module overview | `module_overview_page` | `Training::ModulesController` | `/modules/{alpha}` | -| [x] | Module content | `module_content_page` | `Training::PagesController` | `/modules/{alpha}/content-pages/{1}` | -| [x] | Static page content | `static_page` | `PagesController` | `/example-page` | -| [x] | Account completion | `user_registration` | `Registration::Controller` | `/registration/{attr}` | -| [x] | User profile | `profile_page` | `UserController` | `/my-account` | -| [x] | User name change | `user_name_change` | `UserController` | `/my-account/update-name` | -| [ ] | User email change | `user_email_change` | `UserController` | `/my-account/update-email` | -| [ ] | User password change | `user_password_change` | `UserController` | `/my-account/update-password` | -| [x] | Email address taken | `email_address_taken` | `RegistrationsController` | `/users/sign-up` | -| [x] | User inactivity logout | `error_page` | `ErrorsController` | `/timeout` | -| [x] | 404 Error | `error_page` | `ErrorsController` | `/404` | -| [x] | 500 Error | `error_page` | `ErrorsController` | `/500` | -| [x] | Module start | `module_start` | `Training::PagesController` | `/modules/{alpha}/content-pages/intro` | -| [x] | Module complete | `module_complete` | `Training::ModulesController` | `/modules/{alpha}/certificate` | -| [x] | Questionnaire answered | `questionnaire_answer` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | -| [x] | Summative assessment start | `summative_assessment_start` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | -| [x] | Summative assessment complete | `summative_assessment_complete` | `Training::AssessmentsController` | `/modules/{alpha}/assessment-results/{path}` | -| [x] | Confidence check start | `confidence_check_start` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | -| [x] | Confidence check complete | `confidence_check_complete` | `Training::PagesController` | `/modules/{alpha}/questionnaires/{path}` | -| [x] | User note created | `user_note_created` | `Training::NotesController` | `/my-account/learning-log` | -| [x] | User note updated | `user_note_updated` | `Training::NotesController` | `/my-account/learning-log` | -| [x] | Feedback started | `feedback_start` | `FeedbackController` | `/feedback/{1}` | -| [x] | Feedback completed | `feedback_complete` | `FeedbackController` | `/feedback/thank-you` | +| Feature | Key | Controllers | Path | +| :--- | :--- | :--- | :--- | +| Homepage | `home_page` | `HomeController` | `/` | +| Monitoring progress | `learning_page` | `LearningController` | `/my-modules` | +| Course overview | `course_overview_page` | `Training::ModulesController` | `/modules` | +| Module overview | `module_overview_page` | `Training::ModulesController` | `/modules/{alpha}` | +| Module content | `module_content_page` | `Training::PagesController` | `/modules/{alpha}/content-pages/{1}` | +| Static page content | `static_page` | `PagesController` | `/example-page` | +| Account completion | `user_registration` | `Registration::Controller` | `/registration/{attr}` | +| User profile | `profile_page` | `UserController` | `/my-account` | +| User name change | `user_name_change` | `UserController` | `/my-account/update-name` | +| Email address taken | `email_address_taken` | `RegistrationsController` | `/users/sign-up` | +| User inactivity logout | `error_page` | `ErrorsController` | `/timeout` | +| 404 Error | `error_page` | `ErrorsController` | `/404` | +| 500 Error | `error_page` | `ErrorsController` | `/500` | +| Module start | `module_start` | `Training::PagesController` | `/modules/{alpha}/content-pages/intro` | +| Module complete | `module_complete` | `Training::ModulesController` | `/modules/{alpha}/certificate` | +| Questionnaire answered | `questionnaire_answer` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | +| Summative assessment start | `summative_assessment_start` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | +| Summative assessment complete | `summative_assessment_complete` | `Training::AssessmentsController` | `/modules/{alpha}/assessment-results/{path}` | +| Confidence check start | `confidence_check_start` | `Training::QuestionsController` | `/modules/{alpha}/questionnaires/{path}` | +| Confidence check complete | `confidence_check_complete` | `Training::PagesController` | `/modules/{alpha}/questionnaires/{path}` | +| User note created | `user_note_created` | `Training::NotesController` | `/my-account/learning-log` | +| User note updated | `user_note_updated` | `Training::NotesController` | `/my-account/learning-log` | +| Feedback started | `feedback_start` | `FeedbackController` | `/feedback/{1}` | +| Feedback completed | `feedback_complete` | `FeedbackController` | `/feedback/thank-you` | ## Metrics diff --git a/lib/tasks/eyfs.rake b/lib/tasks/eyfs.rake index 3f4b10c82..e78622ae3 100644 --- a/lib/tasks/eyfs.rake +++ b/lib/tasks/eyfs.rake @@ -24,12 +24,6 @@ namespace :eyfs do end namespace :jobs do - # NB: Not yet CMS compatible - desc 'Add page view events for injected module items' - task plug_content: :environment do - FillPageViewsJob.enqueue - end - # Queueing a dashboard job via Rake inverts the default and will not # upload files to Looker Studio unless explicitly requested. # diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index 81a389789..e669ce863 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -31,7 +31,7 @@ context 'with valid params' do let(:params) do { - id: 'feedback-radio-only', + id: 'feedback-skippable', response: { answers: %w[1], }, @@ -44,10 +44,21 @@ }.to change(Response, :count).by(1) end - it 'redirects to the next question' do - post :update, params: params - expect(response).to have_http_status(:redirect) - expect(response).to redirect_to feedback_path('feedback-checkbox-only') + context 'and first response' do + it 'redirects to the next feedback content' do + post :update, params: params + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to feedback_path('thank-you') + end + end + + context 'and subsequent responses' do + it 'redirects to the profile page' do + create :event, user: user, name: 'profile_page' + post :update, params: params + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to user_path + end end it 'is tracked as started' do From ce747eb54f5f6b19ba834593de77ac2228c5d951 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 14 Jun 2024 16:43:08 +0100 Subject: [PATCH 86/95] Port skippable answer to the main form for editing --- app/controllers/feedback_controller.rb | 24 +++++++++++++------- app/models/guest.rb | 5 ++++ app/models/user.rb | 4 ++-- app/views/feedback/_text_area.html.slim | 2 +- spec/controllers/feedback_controller_spec.rb | 20 ++++++++++++++++ 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 770a29aab..379a43ed7 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -1,4 +1,6 @@ class FeedbackController < ApplicationController + before_action :research_participation, only: :show + helper_method :content, :mod, :current_user_response @@ -13,14 +15,9 @@ def show end def update - if current_user.profile_updated? - if save_response! - flash[:success] = 'Your details have been updated' - redirect_to user_path - else - render :show, status: :unprocessable_entity - end - + if current_user.profile_updated? && save_response! + flash[:success] = 'Your details have been updated' + redirect_to user_path elsif save_response! feedback_cookie track_feedback_start @@ -32,6 +29,17 @@ def update private + # @note + # associate the user research participation question to the course form + # if answered during a training module + # + def research_participation + response = current_user.user_research_response + if content.skippable? && response.present? && !response.training_module.eql?('course') + response.update(training_module: 'course') + end + end + # @return [Boolean] def save_response! current_user_response.update( diff --git a/app/models/guest.rb b/app/models/guest.rb index 4d9effb3e..b9ff346c1 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -24,6 +24,11 @@ def profile_updated? false end + # @return [nil] + def user_research_response + nil + end + # @param question [Training::Question] # @return [Boolean] def skip_question?(question) diff --git a/app/models/user.rb b/app/models/user.rb index 5b02bf0ef..7a50020a3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -487,13 +487,13 @@ def feedback_attributes data_attributes end -private - # @return [Response] def user_research_response responses.feedback.find { |response| response.question.skippable? } end +private + # @return [Hash] def data_attributes DASHBOARD_ATTRS.map { |field| { field => send(field) } }.reduce(&:merge) diff --git a/app/views/feedback/_text_area.html.slim b/app/views/feedback/_text_area.html.slim index 401ef5aec..1a5b132fa 100644 --- a/app/views/feedback/_text_area.html.slim +++ b/app/views/feedback/_text_area.html.slim @@ -1,2 +1,2 @@ = f.govuk_fieldset legend: { text: content.legend } do - = f.govuk_text_area :text_input, label: nil, rows: 9 + = f.govuk_text_area :text_input, rows: 9, label: nil, area: { label: content.legend } diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index e669ce863..abac0767a 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -18,6 +18,26 @@ get :show, params: { id: 'feedback-radio-only' } expect(response).to have_http_status(:success) end + + context 'when the shared question was answered during a training module' do + let(:answer) do + create :response, + user: user, + training_module: 'alpha', + question_name: 'feedback-skippable', + question_type: 'feedback', + answers: [1] # yes / participate + end + + it 'moved to the main form' do + expect { + get :show, params: { id: 'feedback-skippable' } + }.to change { + answer.reload.training_module + }.from('alpha').to('course') + end + end + end describe 'GET #index' do From 14390ec072ed09a7d1a9258960c1b730942d8e93 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 18 Jun 2024 08:57:12 +0100 Subject: [PATCH 87/95] *typo --- app/views/feedback/_text_area.html.slim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/feedback/_text_area.html.slim b/app/views/feedback/_text_area.html.slim index 1a5b132fa..9f349b208 100644 --- a/app/views/feedback/_text_area.html.slim +++ b/app/views/feedback/_text_area.html.slim @@ -1,2 +1,2 @@ = f.govuk_fieldset legend: { text: content.legend } do - = f.govuk_text_area :text_input, rows: 9, label: nil, area: { label: content.legend } + = f.govuk_text_area :text_input, rows: 9, label: nil, aria: { label: content.legend } From 78f6cd9011a432d4e13c96a4ff0dd693f8a98e45 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Tue, 18 Jun 2024 11:36:24 +0100 Subject: [PATCH 88/95] Add timestamps to exported data --- app/controllers/feedback_controller.rb | 2 +- .../data_analysis/guest_feedback_scores.rb | 13 +++- .../data_analysis/user_feedback_scores.rb | 13 +++- spec/controllers/feedback_controller_spec.rb | 11 ++- .../guest_feedback_scores_spec.rb | 40 +++++++++-- .../user_feedback_scores_spec.rb | 67 +++++++++++++++++-- 6 files changed, 126 insertions(+), 20 deletions(-) diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 379a43ed7..551e4204e 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -36,7 +36,7 @@ def update def research_participation response = current_user.user_research_response if content.skippable? && response.present? && !response.training_module.eql?('course') - response.update(training_module: 'course') + response.update!(training_module: 'course') end end diff --git a/app/models/data_analysis/guest_feedback_scores.rb b/app/models/data_analysis/guest_feedback_scores.rb index 1d0594eb1..661d40e95 100644 --- a/app/models/data_analysis/guest_feedback_scores.rb +++ b/app/models/data_analysis/guest_feedback_scores.rb @@ -9,6 +9,8 @@ def column_names Guest Question Answers + Created + Updated ] end @@ -18,10 +20,19 @@ def dashboard :visit_id, :question_name, :answers, + :created_at, + :updated_at, ).map do |user| - user.attributes.symbolize_keys.except(:id) + decorator.call user.attributes.symbolize_keys.except(:id) end end + + private + + # @return [CoercionDecorator] + def decorator + @decorator ||= CoercionDecorator.new + end end end end diff --git a/app/models/data_analysis/user_feedback_scores.rb b/app/models/data_analysis/user_feedback_scores.rb index 04caf357e..25daad6a3 100644 --- a/app/models/data_analysis/user_feedback_scores.rb +++ b/app/models/data_analysis/user_feedback_scores.rb @@ -16,23 +16,30 @@ def column_names 'Module', 'Question', 'Answers', + 'Created', + 'Updated', ] end # @return [Array Mixed}>] def dashboard User.with_feedback.order(:user_id, :question_name).select(*agreed_attributes).map do |user| - user.attributes.symbolize_keys.except(:id) + decorator.call user.attributes.symbolize_keys.except(:id) end end private + # @return [CoercionDecorator] + def decorator + @decorator ||= CoercionDecorator.new + end + # @note Personally identifiable information must not be revealed # # @return [Array] def agreed_attributes - %i[ + %w[ user_id role_type role_type_other @@ -43,6 +50,8 @@ def agreed_attributes training_module question_name answers + responses.created_at + responses.updated_at ] end end diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb index abac0767a..965efd9a3 100644 --- a/spec/controllers/feedback_controller_spec.rb +++ b/spec/controllers/feedback_controller_spec.rb @@ -22,11 +22,11 @@ context 'when the shared question was answered during a training module' do let(:answer) do create :response, - user: user, - training_module: 'alpha', - question_name: 'feedback-skippable', - question_type: 'feedback', - answers: [1] # yes / participate + user: user, + training_module: 'alpha', + question_name: 'feedback-skippable', + question_type: 'feedback', + answers: [1] # yes / participate end it 'moved to the main form' do @@ -37,7 +37,6 @@ }.from('alpha').to('course') end end - end describe 'GET #index' do diff --git a/spec/models/data_analysis/guest_feedback_scores_spec.rb b/spec/models/data_analysis/guest_feedback_scores_spec.rb index c84e942c0..7c00c82bf 100644 --- a/spec/models/data_analysis/guest_feedback_scores_spec.rb +++ b/spec/models/data_analysis/guest_feedback_scores_spec.rb @@ -8,6 +8,8 @@ Guest Question Answers + Created + Updated ] end let(:rows) do @@ -16,11 +18,15 @@ visit_id: guest.visit.id, question_name: 'feedback-checkbox-only', answers: [1, 2], + created_at: '2024-01-02 00:00:00', + updated_at: '2024-01-03 00:00:00', }, { visit_id: guest.visit.id, question_name: 'feedback-radio-only', answers: [1], + created_at: '2023-01-01 00:00:00', + updated_at: '2023-01-01 00:00:00', }, ] end @@ -29,12 +35,38 @@ skip unless Rails.application.migrated_answers? # users not included - create(:response, question_type: 'feedback', training_module: 'course', question_name: 'feedback-checkbox-only', answers: [1, 2]) - create(:response, question_type: 'feedback', training_module: 'alpha', question_name: 'feedback-textarea-only', text_input: 'opinion', answers: []) + create(:response, + question_type: 'feedback', + training_module: 'course', + question_name: 'feedback-checkbox-only', + answers: [1, 2]) + create(:response, + question_type: 'feedback', + training_module: 'alpha', + question_name: 'feedback-textarea-only', + text_input: 'opinion', + answers: []) # guest responses - create(:response, user: nil, visit: guest.visit, question_type: 'feedback', training_module: 'course', question_name: 'feedback-radio-only', answers: [1]) - create(:response, user: nil, visit: guest.visit, question_type: 'feedback', training_module: 'course', question_name: 'feedback-checkbox-only', answers: [1, 2]) + create(:response, + user: nil, + visit: guest.visit, + question_type: 'feedback', + training_module: 'course', + question_name: 'feedback-radio-only', + answers: [1], + created_at: Time.zone.local(2023, 1, 1), + updated_at: Time.zone.local(2023, 1, 1)) + + create(:response, + user: nil, + visit: guest.visit, + question_type: 'feedback', + training_module: 'course', + question_name: 'feedback-checkbox-only', + answers: [1, 2], + created_at: Time.zone.local(2024, 1, 2), + updated_at: Time.zone.local(2024, 1, 3)) end it_behaves_like 'a data export model' diff --git a/spec/models/data_analysis/user_feedback_scores_spec.rb b/spec/models/data_analysis/user_feedback_scores_spec.rb index af309d3ba..22a2a56e8 100644 --- a/spec/models/data_analysis/user_feedback_scores_spec.rb +++ b/spec/models/data_analysis/user_feedback_scores_spec.rb @@ -15,6 +15,8 @@ 'Module', 'Question', 'Answers', + 'Created', + 'Updated', ] end let(:rows) do @@ -30,6 +32,8 @@ training_module: 'course', question_name: 'feedback-checkbox-only', answers: [1, 2], + created_at: '2023-01-01 00:00:00', + updated_at: '2023-01-01 00:00:00', }, { user_id: user_1.id, @@ -42,6 +46,8 @@ training_module: 'course', question_name: 'feedback-textarea-only', answers: [], + created_at: '2023-01-01 00:00:00', + updated_at: '2023-01-01 00:00:00', }, { user_id: user_2.id, @@ -54,6 +60,8 @@ training_module: 'alpha', question_name: 'feedback-checkbox-only', answers: [1, 2, 3, 4], + created_at: '2023-01-01 00:00:00', + updated_at: '2023-01-01 00:00:00', }, { user_id: user_2.id, @@ -66,6 +74,8 @@ training_module: 'alpha', question_name: 'feedback-radio-only', answers: [1], + created_at: '2023-01-01 00:00:00', + updated_at: '2023-01-01 00:00:00', }, { user_id: user_2.id, @@ -78,6 +88,8 @@ training_module: 'alpha', question_name: 'feedback-textarea-only', answers: [], + created_at: '2023-01-01 00:00:00', + updated_at: '2023-01-01 00:00:00', }, ] end @@ -86,16 +98,59 @@ skip unless Rails.application.migrated_answers? # guests not included - create(:response, user: nil, visit: Visit.new, question_type: 'feedback', training_module: 'course', question_name: 'feedback-radio-only', answers: [1]) + create(:response, + user: nil, + visit: Visit.new, + question_type: 'feedback', + training_module: 'course', + question_name: 'feedback-radio-only', + answers: [1]) # course - create(:response, user: user_1, question_type: 'feedback', training_module: 'course', question_name: 'feedback-checkbox-only', answers: [1, 2]) - create(:response, user: user_1, question_type: 'feedback', training_module: 'course', question_name: 'feedback-textarea-only', text_input: 'potential PII', answers: []) + create(:response, + user: user_1, + question_type: 'feedback', + training_module: 'course', + question_name: 'feedback-checkbox-only', + answers: [1, 2], + created_at: Time.zone.local(2023, 1, 1), + updated_at: Time.zone.local(2023, 1, 1)) + create(:response, + user: user_1, + question_type: 'feedback', + training_module: 'course', + question_name: 'feedback-textarea-only', + text_input: 'potential PII', + answers: [], + created_at: Time.zone.local(2023, 1, 1), + updated_at: Time.zone.local(2023, 1, 1)) # module - create(:response, user: user_2, question_type: 'feedback', training_module: 'alpha', question_name: 'feedback-radio-only', answers: [1]) - create(:response, user: user_2, question_type: 'feedback', training_module: 'alpha', question_name: 'feedback-checkbox-only', answers: [1, 2, 3, 4]) - create(:response, user: user_2, question_type: 'feedback', training_module: 'alpha', question_name: 'feedback-textarea-only', text_input: 'potential PII', answers: []) + create(:response, + user: user_2, + question_type: 'feedback', + training_module: 'alpha', + question_name: 'feedback-radio-only', + answers: [1], + created_at: Time.zone.local(2023, 1, 1), + updated_at: Time.zone.local(2023, 1, 1)) + create(:response, + user: user_2, + question_type: 'feedback', + training_module: 'alpha', + question_name: 'feedback-checkbox-only', + answers: [1, 2, 3, 4], + created_at: Time.zone.local(2023, 1, 1), + updated_at: Time.zone.local(2023, 1, 1)) + create(:response, + user: user_2, + question_type: 'feedback', + training_module: 'alpha', + question_name: 'feedback-textarea-only', + text_input: 'potential PII', + answers: [], + created_at: Time.zone.local(2023, 1, 1), + updated_at: Time.zone.local(2023, 1, 1)) end it_behaves_like 'a data export model' From 724e67b9d6b54ff7c2b30fa2d8f73253d88c55d9 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 21 Jun 2024 09:33:19 +0100 Subject: [PATCH 89/95] DPO comment reminder --- app/models/response.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/response.rb b/app/models/response.rb index 93484dba5..a716b8143 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -8,6 +8,7 @@ class Response < ApplicationRecord belongs_to :assessment, optional: true belongs_to :visit, optional: true + # FIXME: remove encryption before release? encrypts :text_input validates :training_module, presence: true From 95c7948f67bc84c870e4ef98a3cb3db629b7b5ab Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Mon, 24 Jun 2024 13:41:52 +0100 Subject: [PATCH 90/95] Store free text response as plain text --- app/models/response.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/models/response.rb b/app/models/response.rb index a716b8143..687155399 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -8,9 +8,6 @@ class Response < ApplicationRecord belongs_to :assessment, optional: true belongs_to :visit, optional: true - # FIXME: remove encryption before release? - encrypts :text_input - validates :training_module, presence: true validates :question_type, inclusion: { in: %w[formative summative confidence feedback] } validates :answers, presence: true, unless: -> { text_input_only? } From 17c9b12f3a1fd016668571560e6bd51bea0112b3 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Wed, 26 Jun 2024 08:47:03 +0100 Subject: [PATCH 91/95] Progress service notes --- app/services/module_progress.rb | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/app/services/module_progress.rb b/app/services/module_progress.rb index c12152555..fb9e6c403 100644 --- a/app/services/module_progress.rb +++ b/app/services/module_progress.rb @@ -1,10 +1,11 @@ +# OPTIMIZE: N+1 query +# # Overall module progress: # - whether a page was visited -# - whether a page was skipped +# - whether any/all/no pages in a section were visited # - whether key events have been recorded (start/complete) # - the last page visited # - the furthest page visited -# - the furthest page visited # class ModuleProgress extend Dry::Initializer @@ -109,12 +110,6 @@ def successful_attempt? end end - # In progress modules with new pages that have been skipped - # @return [Boolean] - def gaps? - (unvisited.first.id..unvisited.last.id).count != unvisited.map(&:id).count - end - # @return [Array] def visited mod.content.select { |page| visited?(page) } From c861a56611dcbf36779a4210194eab7e0bec0442 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Thu, 4 Jul 2024 11:21:39 +0100 Subject: [PATCH 92/95] Replace old variable in stylesheets --- app/assets/stylesheets/application.scss | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 3cc4a1f71..e7973d2c9 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -103,23 +103,22 @@ pre.code-tip { font-family: monospace !important; } } +// -------------------------------------------- #feedback-cta { padding: govuk-spacing(5); - background-color: $department-for-education-websafe; + background-color: govuk-organisation-colour('department-for-education'); * { color: govuk-colour('white'); } .govuk-button { - background-color: $department-for-education-websafe; + background-color: govuk-organisation-colour('department-for-education'); border-color: govuk-colour('white'); box-shadow: none; } } -// -------------------------------------------- - .govuk-label { font-weight: normal; From b870adda4ece1c736467eb6b2f7aafc891a3f3b1 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 5 Jul 2024 07:58:10 +0100 Subject: [PATCH 93/95] Tidy --- .env.example | 5 ----- .github/workflows/ci.yml | 4 ---- app/services/content_integrity.rb | 9 ++------- config/sitemap.rb | 21 +++++++-------------- docker-compose.yml | 1 - 5 files changed, 9 insertions(+), 31 deletions(-) diff --git a/.env.example b/.env.example index dcc48b9ce..cd3200131 100644 --- a/.env.example +++ b/.env.example @@ -48,9 +48,6 @@ POSTGRES_PASSWORD= # PGHOST=localhost # PGDATABASE=early_years_foundation_recovery_test -# user research -FEEDBACK_URL= - # Account unlock wait duration if :time is enabled UNLOCK_IN_MINUTES= @@ -82,8 +79,6 @@ CONTENTFUL_PREVIEW_TOKEN= CONTENTFUL_ENVIRONMENT=test # master, staging CONTENTFUL_PREVIEW=true -# Contentful DB migration flags -DISABLE_USER_ANSWER=true # Opt-in end-to-end testing (inactive for review apps) E2E=true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b62aeabea..cf00acbed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,6 @@ jobs: --health-timeout 5s --health-retries 5 - steps: - name: Checkout Code @@ -79,9 +78,6 @@ jobs: run: bundle exec rails assets:precompile - name: Run test suite - env: - DISABLE_USER_ANSWER: true - CONTENTFUL_PREVIEW: true run: bundle exec rspec - name: Run rubocop diff --git a/app/services/content_integrity.rb b/app/services/content_integrity.rb index 0e4f2a871..24f0de719 100644 --- a/app/services/content_integrity.rb +++ b/app/services/content_integrity.rb @@ -1,24 +1,19 @@ -# Validate whether a module's content meets minimum functional requirements +# Validate module content meets minimum functional requirements # class ContentIntegrity extend Dry::Initializer option :module_name, Types::String - # NB: Able to be validated in the CMS editor - # # @return [Hash{Symbol=>String}] valid as upcoming module MODULE_VALIDATIONS = { upcoming: 'Missing upcoming text', about: 'Missing about text', description: 'Missing description text', - criteria: 'Missing criteria list', outcomes: 'Missing outcomes list', - duration: 'Missing duration number', position: 'Missing position number', - thumbnail: 'Missing thumbnail image', }.freeze @@ -55,7 +50,7 @@ def call log "#{module_name.upcase}: " + (valid? ? 'pass' : 'fail') end - # @return [Boolean] Validate modules with content + # @return [Boolean] def valid? (module_results + content_results).all? && mod.pages? end diff --git a/config/sitemap.rb b/config/sitemap.rb index ee3f8879d..83b9a6f74 100644 --- a/config/sitemap.rb +++ b/config/sitemap.rb @@ -36,8 +36,9 @@ # errors add '/404' - # add '/422' + add '/422' add '/500' + add '/503' add course_overview_path add experts_path @@ -47,21 +48,14 @@ add feedback_path(content.name) end - Training::Module.live.each do |mod| - add about_path(mod.name) - end - add new_user_session_path - # private pages - # ------------------------------------------ - # account add user_path # edit registration/account add edit_registration_terms_and_conditions_path - add edit_registration_name_path # unless Rails.application.gov_one_login? + add edit_registration_name_path add edit_registration_setting_type_path add edit_registration_setting_type_other_path add edit_registration_local_authority_path @@ -80,13 +74,12 @@ add my_modules_path add user_notes_path + # course content if Training::Module.live.any? - # Course common start page - mod = Training::Module.live.first - add training_module_page_path(mod.name, mod.pages.first.name) + mod = Training::Module.live.sample + add about_path(mod.name) - # Course content random module - Training::Module.live.sample.content.each do |page| + mod.content.each do |page| if page.is_question? add training_module_question_path(page.parent.name, page.name) elsif page.assessment_results? diff --git a/docker-compose.yml b/docker-compose.yml index dbca3fff8..fb7fd526a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,6 @@ services: - RAILS_MASTER_KEY - RAILS_LOG_TO_STDOUT - PROXY_CERT - - GOV_ONE_LOGIN depends_on: - db ports: From 3e45fb265fa8bfee6ab96be2e32fa38f3b49de49 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 5 Jul 2024 08:09:40 +0100 Subject: [PATCH 94/95] Adjust coverage threshold --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7e3ad5b97..11b62c3a5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,5 @@ require 'simplecov' -SimpleCov.minimum_coverage 93 +SimpleCov.minimum_coverage 92 SimpleCov.start 'rails' require 'pry' From 125960e14f744cbd4b4344cd3a6bdef20c6113e8 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton Date: Fri, 5 Jul 2024 08:39:07 +0100 Subject: [PATCH 95/95] Add missing error page title --- app/views/errors/service_unavailable.html.slim | 3 +++ app/views/errors/unprocessable_entity.html.slim | 10 ---------- config/routes.rb | 1 - config/sitemap.rb | 1 - 4 files changed, 3 insertions(+), 12 deletions(-) delete mode 100644 app/views/errors/unprocessable_entity.html.slim diff --git a/app/views/errors/service_unavailable.html.slim b/app/views/errors/service_unavailable.html.slim index 383e2a1c8..e595c414c 100644 --- a/app/views/errors/service_unavailable.html.slim +++ b/app/views/errors/service_unavailable.html.slim @@ -1,3 +1,6 @@ +- content_for :page_title do + = html_title 'Service unavailable' + .govuk-grid-row .govuk-grid-column-two-thirds h1.govuk-heading-xl diff --git a/app/views/errors/unprocessable_entity.html.slim b/app/views/errors/unprocessable_entity.html.slim deleted file mode 100644 index 471f7da68..000000000 --- a/app/views/errors/unprocessable_entity.html.slim +++ /dev/null @@ -1,10 +0,0 @@ -.govuk-grid-row - .govuk-grid-column-two-thirds - h1.govuk-heading-xl - | Page not found - p.govuk-body - | If you typed the web address, check it is correct. - p.govuk-body - | If you pasted the web address, check you copied the entire address. - p.govuk-body - | If the web address is correct or you selected a link or button, contact Early years child development training to report a fault with the service. diff --git a/config/routes.rb b/config/routes.rb index 189c8e525..f7c58d52d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,7 +5,6 @@ get 'my-modules', to: 'learning#show' # @see User#course get '404', to: 'errors#not_found', via: :all - get '422', to: 'errors#unprocessable_entity', via: :all get '500', to: 'errors#internal_server_error', via: :all get '503', to: 'errors#service_unavailable', via: :all diff --git a/config/sitemap.rb b/config/sitemap.rb index 83b9a6f74..158b971e4 100644 --- a/config/sitemap.rb +++ b/config/sitemap.rb @@ -36,7 +36,6 @@ # errors add '/404' - add '/422' add '/500' add '/503'