diff --git a/.env.test b/.env.test index 1ed0b00d1..3a665ad35 100644 --- a/.env.test +++ b/.env.test @@ -14,6 +14,6 @@ CONTENTFUL_URL=cdn.contentful.com CONTENTFUL_SPACE=test CONTENTFUL_ENVIRONMENT=master CONTENTFUL_ACCESS_TOKEN=123 -CONTENTFUL_PLANNING_START_ENTRY_ID=1UjQurSOi5MWkcRuGxdXZS +CONTENTFUL_PLANNING_START_ENTRY_ID=contentful-starting-step CONTENTFUL_PREVIEW_APP=false CONTENTFUL_ENTRY_CACHING=false diff --git a/CHANGELOG.md b/CHANGELOG.md index 260c36446..9c96cc783 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog 1.0.0]. ## [Unreleased] +## [release-005] - 2021-1-19 + +- users can see an initial slice of their specification as HTML at the end of their journey +- users can download their specification as a document in the .docx format +- checkbox answers are editable +- radio and select questions are created with the new `ExtendedOptions` field +- questions are now all loaded at the start of a journey, rather than step by step +- users start the journey at the task list instead of the first question +- check your answers pattern has been replaced by a task list +- radio questions can be configured to ask the user for additional text +- update the service name to the latest decision + ## [release-004] - 2020-12-17 - fix primary key type on long_text_answers table to UUID @@ -51,7 +63,8 @@ Contentful fixture - Contentful can redirect users to preview endpoints - users can be asked to answer a long text question -[unreleased]: https://github.com/DFE-Digital/buy-for-your-school/compare/release-004...HEAD +[unreleased]: https://github.com/DFE-Digital/buy-for-your-school/compare/release-005...HEAD +[release-005]: https://github.com/DFE-Digital/buy-for-your-school/compare/release-004...release-005 [release-004]: https://github.com/DFE-Digital/buy-for-your-school/compare/release-003...release-004 [release-003]: https://github.com/DFE-Digital/buy-for-your-school/compare/release-002...release-003 [release-002]: https://github.com/DFE-Digital/buy-for-your-school/compare/release-001...release-002 diff --git a/Gemfile b/Gemfile index f8ffc488b..9f4980330 100644 --- a/Gemfile +++ b/Gemfile @@ -10,15 +10,17 @@ gem "coffee-rails", "~> 5.0" gem "contentful", "~> 2.15" gem "govuk_design_system_formbuilder", "~> 2.1" gem "high_voltage" +gem "htmltoword" gem "jbuilder", "~> 2.5" gem "jquery-rails" +gem "liquid" gem "pg" gem "mini_racer" gem "puma", "~> 5.1" gem "redis", "~> 4.2" gem "redis-namespace" gem "rollbar" -gem "rails", "~> 6.1.0" +gem "rails", "~> 6.1.1" gem "sass-rails", "~> 6.0" gem "sidekiq", "~> 6.1" gem "sidekiq-cron", "~> 1.2" @@ -27,7 +29,7 @@ gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] gem "uglifier", ">= 1.3.0" group :development do - gem "listen", ">= 3.0.5", "< 3.4" + gem "listen", ">= 3.0.5", "< 3.5" gem "spring" gem "spring-watcher-listen", "~> 2.0.0" gem "web-console", ">= 3.3.0" diff --git a/Gemfile.lock b/Gemfile.lock index 3811a422a..506206498 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,60 +1,60 @@ GEM remote: https://rubygems.org/ specs: - actioncable (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) + actioncable (6.1.1) + actionpack (= 6.1.1) + activesupport (= 6.1.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) + actionmailbox (6.1.1) + actionpack (= 6.1.1) + activejob (= 6.1.1) + activerecord (= 6.1.1) + activestorage (= 6.1.1) + activesupport (= 6.1.1) mail (>= 2.7.1) - actionmailer (6.1.0) - actionpack (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activesupport (= 6.1.0) + actionmailer (6.1.1) + actionpack (= 6.1.1) + actionview (= 6.1.1) + activejob (= 6.1.1) + activesupport (= 6.1.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.0) - actionview (= 6.1.0) - activesupport (= 6.1.0) + actionpack (6.1.1) + actionview (= 6.1.1) + activesupport (= 6.1.1) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.0) - actionpack (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) + actiontext (6.1.1) + actionpack (= 6.1.1) + activerecord (= 6.1.1) + activestorage (= 6.1.1) + activesupport (= 6.1.1) nokogiri (>= 1.8.5) - actionview (6.1.0) - activesupport (= 6.1.0) + actionview (6.1.1) + activesupport (= 6.1.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.0) - activesupport (= 6.1.0) + activejob (6.1.1) + activesupport (= 6.1.1) globalid (>= 0.3.6) - activemodel (6.1.0) - activesupport (= 6.1.0) - activerecord (6.1.0) - activemodel (= 6.1.0) - activesupport (= 6.1.0) - activestorage (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activesupport (= 6.1.0) + activemodel (6.1.1) + activesupport (= 6.1.1) + activerecord (6.1.1) + activemodel (= 6.1.1) + activesupport (= 6.1.1) + activestorage (6.1.1) + actionpack (= 6.1.1) + activejob (= 6.1.1) + activerecord (= 6.1.1) + activesupport (= 6.1.1) marcel (~> 0.3.1) mimemagic (~> 0.3.2) - activesupport (6.1.0) + activesupport (6.1.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -70,7 +70,7 @@ GEM bindex (0.8.1) bootsnap (1.5.1) msgpack (~> 1.0) - brakeman (4.10.0) + brakeman (4.10.1) builder (3.2.4) bullet (6.1.2) activesupport (>= 3.0.0) @@ -99,11 +99,12 @@ GEM contentful (2.15.4) http (> 0.8, < 5.0) multi_json (~> 1) - crack (0.4.4) + crack (0.4.5) + rexml crass (1.0.6) database_cleaner (1.8.5) diff-lcs (1.4.4) - docile (1.3.2) + docile (1.3.4) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) @@ -121,7 +122,7 @@ GEM railties (>= 5.0.0) faker (2.15.1) i18n (>= 1.6, < 2) - ffi (1.13.1) + ffi (1.14.2) ffi-compiler (1.0.1) ffi (>= 1.0.0) rake @@ -130,12 +131,16 @@ GEM raabro (~> 1.4) globalid (0.4.2) activesupport (>= 4.2.0) - govuk_design_system_formbuilder (2.1.5) - actionview (>= 5.2) - activemodel (>= 5.2) - activesupport (>= 5.2) + govuk_design_system_formbuilder (2.1.6) + actionview (~> 6.1.0, >= 6.1) + activemodel (~> 6.1.0, >= 6.1) + activesupport (~> 6.1.0, >= 6.1) hashdiff (1.0.1) high_voltage (3.1.2) + htmltoword (1.1.1) + actionpack + nokogiri + rubyzip (>= 1.0) http (4.4.1) addressable (~> 2.3) http-cookie (~> 1.0) @@ -146,7 +151,7 @@ GEM http-form_data (2.3.0) http-parser (1.2.1) ffi-compiler (>= 1.0, < 2.0) - i18n (1.8.5) + i18n (1.8.7) concurrent-ruby (~> 1.0) jbuilder (2.10.1) activesupport (>= 5.0.0) @@ -157,7 +162,8 @@ GEM launchy (2.5.0) addressable (~> 2.7) libv8 (8.4.255.0) - listen (3.3.3) + liquid (5.0.0) + listen (3.4.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) loofah (2.8.0) @@ -170,18 +176,20 @@ GEM method_source (1.0.0) mimemagic (0.3.5) mini_mime (1.0.2) - mini_portile2 (2.4.0) + mini_portile2 (2.5.0) mini_racer (0.3.1) libv8 (~> 8.4.255) - minitest (5.14.2) - mock_redis (0.26.0) + minitest (5.14.3) + mock_redis (0.27.3) + ruby2_keywords msgpack (1.3.3) multi_json (1.15.0) nio4r (2.5.4) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) + nokogiri (1.11.1) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) parallel (1.20.1) - parser (2.7.2.0) + parser (3.0.0.0) ast (~> 2.4.1) pg (1.2.3) pry (0.13.1) @@ -191,23 +199,24 @@ GEM puma (5.1.1) nio4r (~> 2.0) raabro (1.4.0) + racc (1.5.2) rack (2.2.3) rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.1.0) - actioncable (= 6.1.0) - actionmailbox (= 6.1.0) - actionmailer (= 6.1.0) - actionpack (= 6.1.0) - actiontext (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activemodel (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) + rails (6.1.1) + actioncable (= 6.1.1) + actionmailbox (= 6.1.1) + actionmailer (= 6.1.1) + actionpack (= 6.1.1) + actiontext (= 6.1.1) + actionview (= 6.1.1) + activejob (= 6.1.1) + activemodel (= 6.1.1) + activerecord (= 6.1.1) + activestorage (= 6.1.1) + activesupport (= 6.1.1) bundler (>= 1.15.0) - railties (= 6.1.0) + railties (= 6.1.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -215,14 +224,14 @@ GEM rails-html-sanitizer (1.3.0) loofah (~> 2.3) rails_layout (1.0.42) - railties (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) + railties (6.1.1) + actionpack (= 6.1.1) + activesupport (= 6.1.1) method_source rake (>= 0.8.7) thor (~> 1.0) rainbow (3.0.0) - rake (13.0.1) + rake (13.0.3) rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) @@ -232,38 +241,39 @@ GEM regexp_parser (1.8.2) rexml (3.2.4) rollbar (3.1.1) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.1) + rspec-support (~> 3.10.0) + rspec-rails (4.0.2) actionpack (>= 4.2) activesupport (>= 4.2) railties (>= 4.2) - rspec-core (~> 3.9) - rspec-expectations (~> 3.9) - rspec-mocks (~> 3.9) - rspec-support (~> 3.9) - rspec-support (3.9.3) - rubocop (1.4.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) + rspec-support (3.10.1) + rubocop (1.7.0) parallel (~> 1.10) parser (>= 2.7.1.5) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8) + regexp_parser (>= 1.8, < 3.0) rexml - rubocop-ast (>= 1.1.1) + rubocop-ast (>= 1.2.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (1.3.0) + rubocop-ast (1.4.0) parser (>= 2.7.1.5) - rubocop-performance (1.9.1) + rubocop-performance (1.9.2) rubocop (>= 0.90.0, < 2.0) rubocop-ast (>= 0.4.0) - ruby-progressbar (1.10.1) + ruby-progressbar (1.11.0) + ruby2_keywords (0.0.2) rubyzip (2.3.0) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) @@ -278,7 +288,7 @@ GEM selenium-webdriver (3.142.7) childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) - shoulda-matchers (4.4.1) + shoulda-matchers (4.5.0) activesupport (>= 4.2.0) sidekiq (6.1.2) connection_pool (>= 2.2.2) @@ -287,7 +297,7 @@ GEM sidekiq-cron (1.2.0) fugit (~> 1.1) sidekiq (>= 4.2.1) - simplecov (0.20.0) + simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) @@ -306,15 +316,15 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - standard (0.10.2) - rubocop (= 1.4.2) - rubocop-performance (= 1.9.1) + standard (0.11.0) + rubocop (= 1.7.0) + rubocop-performance (= 1.9.2) thor (1.0.1) tilt (2.0.10) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - tzinfo (2.0.3) + tzinfo (2.0.4) concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) @@ -328,7 +338,7 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webmock (3.10.0) + webmock (3.11.1) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -358,16 +368,18 @@ DEPENDENCIES faker govuk_design_system_formbuilder (~> 2.1) high_voltage + htmltoword jbuilder (~> 2.5) jquery-rails launchy - listen (>= 3.0.5, < 3.4) + liquid + listen (>= 3.0.5, < 3.5) mini_racer mock_redis pg pry puma (~> 5.1) - rails (~> 6.1.0) + rails (~> 6.1.1) rails_layout redis (~> 4.2) redis-namespace diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss deleted file mode 100644 index 89a3ce563..000000000 --- a/app/assets/stylesheets/application.css.scss +++ /dev/null @@ -1,24 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's - * vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . - *= require_self - */ - -$govuk-font-url-function: "image-url"; -$govuk-image-url-function: "font-url"; - -@import "govuk-frontend/govuk/all"; - -.preview-tag { - background-color: $govuk-error-colour; -} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss new file mode 100644 index 000000000..6d998b6ae --- /dev/null +++ b/app/assets/stylesheets/application.scss @@ -0,0 +1,10 @@ +$govuk-font-url-function: 'font-url'; +$govuk-image-url-function: 'image-url'; + +@import "base/frontend"; +@import "components/specification"; +@import "components/task-list"; + +.preview-tag { + background-color: $govuk-error-colour; +} diff --git a/app/assets/stylesheets/base/_frontend.scss b/app/assets/stylesheets/base/_frontend.scss new file mode 100644 index 000000000..0c80542e0 --- /dev/null +++ b/app/assets/stylesheets/base/_frontend.scss @@ -0,0 +1 @@ +@import "govuk-frontend/govuk/all"; diff --git a/app/assets/stylesheets/components/_specification.scss b/app/assets/stylesheets/components/_specification.scss new file mode 100644 index 000000000..a934a1ec6 --- /dev/null +++ b/app/assets/stylesheets/components/_specification.scss @@ -0,0 +1,18 @@ +#specification { + h2 { + @extend .govuk-heading-m; + } + + h3 { + @extend .govuk-heading-s; + } + + p { + @extend .govuk-body; + } + + ol { + @extend .govuk-list; + @extend .govuk-list--number; + } +} diff --git a/app/assets/stylesheets/components/_task-list.scss b/app/assets/stylesheets/components/_task-list.scss new file mode 100644 index 000000000..b22514006 --- /dev/null +++ b/app/assets/stylesheets/components/_task-list.scss @@ -0,0 +1,69 @@ +// taken from https://github.com/alphagov/govuk-prototype-kit/blob/027ecc48a7b20862e68a83552e08f47696794cd4/app/assets/sass/patterns/_task-list.scss + +.app-task-list { + list-style-type: none; + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + @include govuk-media-query($from: tablet) { + min-width: 550px; + } +} + +.app-task-list__section { + display: table; + @include govuk-font($size:24, $weight: bold); +} + +.app-task-list__section-number { + display: table-cell; + + @include govuk-media-query($from: tablet) { + min-width: govuk-spacing(6); + padding-right: 0; + } +} + +.app-task-list__items { + @include govuk-font($size: 19); + @include govuk-responsive-margin(9, "bottom"); + list-style: none; + padding-left: 0; + @include govuk-media-query($from: tablet) { + padding-left: govuk-spacing(6); + } +} + +.app-task-list__item { + border-bottom: 1px solid $govuk-border-colour; + margin-bottom: 0 !important; + padding-top: govuk-spacing(2); + padding-bottom: govuk-spacing(2); + @include govuk-clearfix; +} + +.app-task-list__item:first-child { + border-top: 1px solid $govuk-border-colour; +} + +.app-task-list__task-name { + display: block; + @include govuk-media-query($from: 450px) { + float: left; + } +} + +// The `app-task-list__task-completed` class was previously used on the task +// list for the completed tag (changed in 86c90ec) – it's still included here to +// avoid breaking task lists in existing prototypes. +.app-task-list__tag, +.app-task-list__task-completed { + margin-top: govuk-spacing(2); + margin-bottom: govuk-spacing(1); + + @include govuk-media-query($from: 450px) { + float: right; + margin-top: 0; + margin-bottom: 0; + } +} diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index 1d1bd3c52..745f9c959 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -17,11 +17,7 @@ def create if @answer.valid? @answer.save - if @journey.next_entry_id.present? - redirect_to new_journey_step_path(@journey) - else - redirect_to journey_path(@journey) - end + redirect_to journey_path(@journey) else render "steps/#{@step.contentful_type}", locals: {layout: "steps/new_form_wrapper"} end @@ -32,7 +28,12 @@ def update @step = Step.find(step_id) @answer = @step.answer - @answer.assign_attributes(answer_params) + case @step.contentful_type + when "checkboxes" + @answer.assign_attributes(checkbox_params) + else + @answer.assign_attributes(answer_params) + end if @answer.valid? @answer.save @@ -53,7 +54,7 @@ def step_id end def answer_params - params.require(:answer).permit(:response) + params.require(:answer).permit(:response, :further_information) end def checkbox_params diff --git a/app/controllers/journeys_controller.rb b/app/controllers/journeys_controller.rb index 3b8815076..d0ed713e3 100644 --- a/app/controllers/journeys_controller.rb +++ b/app/controllers/journeys_controller.rb @@ -1,16 +1,59 @@ # frozen_string_literal: true class JourneysController < ApplicationController + rescue_from FindLiquidTemplate::InvalidLiquidSyntax do |exception| + render "errors/specification_template_invalid", status: 500, locals: {error: exception} + end + + rescue_from CreateJourneyStep::UnexpectedContentfulModel do |exception| + render "errors/unexpected_contentful_model", status: 500 + end + + rescue_from CreateJourneyStep::UnexpectedContentfulStepType do |exception| + render "errors/unexpected_contentful_step_type", status: 500 + end + + rescue_from BuildJourneyOrder::MissingEntryDetected do |exception| + render "errors/contentful_entry_not_found", status: 500 + end + def new - journey = Journey.create(category: "catering", next_entry_id: ENV["CONTENTFUL_PLANNING_START_ENTRY_ID"]) - redirect_to new_journey_step_path(journey) + journey = CreateJourney.new(category: "catering").call + redirect_to journey_path(journey) end def show @journey = Journey.includes( - steps: [:radio_answer, :short_text_answer, :long_text_answer] + steps: [:radio_answer, :short_text_answer, :long_text_answer, :single_date_answer, :checkbox_answers] ).find(journey_id) @steps = @journey.steps.map { |step| StepPresenter.new(step) } + + # TODO: Move this logic into a tested class along with a Presenter factory + @answers = @journey.steps.that_are_questions.each_with_object({}) { |step, hash| + answer = case step.answer.class.name + when "ShortTextAnswer" then ShortTextAnswerPresenter.new(step.answer) + when "LongTextAnswer" then LongTextAnswerPresenter.new(step.answer) + when "RadioAnswer" then RadioAnswerPresenter.new(step.answer) + when "SingleDateAnswer" then SingleDateAnswerPresenter.new(step.answer) + when "CheckboxAnswers" then CheckboxesAnswerPresenter.new(step.answer) + else + step.answer + end + hash["answer_#{step.contentful_id}"] = answer&.response.to_s + } + + @specification_template = Liquid::Template.parse( + @journey.liquid_template, error_mode: :strict + ) + + specification_html = @specification_template.render(@answers) + + respond_to do |format| + format.html + format.docx do + render docx: "specification.docx", content: specification_html, layout: "specficiation" + end + end end private diff --git a/app/controllers/preview/entries_controller.rb b/app/controllers/preview/entries_controller.rb index f9982eaac..dcd4db657 100644 --- a/app/controllers/preview/entries_controller.rb +++ b/app/controllers/preview/entries_controller.rb @@ -1,7 +1,12 @@ class Preview::EntriesController < ApplicationController def show - @journey = Journey.create(category: "catering", next_entry_id: entry_id) - redirect_to new_journey_step_path(@journey) + @journey = Journey.create(category: "catering") + contentful_entry = GetContentfulEntry.new(entry_id: entry_id).call + @step = CreateJourneyStep.new( + journey: @journey, contentful_entry: contentful_entry + ).call + + redirect_to journey_step_path(@journey, @step) end private diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index f25f4da6b..6984327a4 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -1,18 +1,6 @@ # frozen_string_literal: true class StepsController < ApplicationController - rescue_from GetContentfulEntry::EntryNotFound do |exception| - render "errors/contentful_entry_not_found", status: 500 - end - - rescue_from CreateJourneyStep::UnexpectedContentfulModel do |exception| - render "errors/unexpected_contentful_model", status: 500 - end - - rescue_from CreateJourneyStep::UnexpectedContentfulStepType do |exception| - render "errors/unexpected_contentful_step_type", status: 500 - end - def new @journey = Journey.find(journey_id) diff --git a/app/helpers/step_helper.rb b/app/helpers/step_helper.rb index 5b7ced134..c67f11950 100644 --- a/app/helpers/step_helper.rb +++ b/app/helpers/step_helper.rb @@ -1,12 +1,6 @@ # frozen_string_literal: true module StepHelper - def radio_options(array_of_options:) - array_of_options.map { |option| - OpenStruct.new(id: option.downcase, name: option.titleize) - } - end - def checkbox_options(array_of_options:) array_of_options.map { |option| OpenStruct.new(id: option.downcase, name: option.titleize) diff --git a/app/models/journey.rb b/app/models/journey.rb index 7b35f9c8e..ec827d682 100644 --- a/app/models/journey.rb +++ b/app/models/journey.rb @@ -1,4 +1,6 @@ class Journey < ApplicationRecord self.implicit_order_column = "created_at" has_many :steps + + validates :liquid_template, presence: true end diff --git a/app/models/step.rb b/app/models/step.rb index dc464aeb3..d1cab14c7 100644 --- a/app/models/step.rb +++ b/app/models/step.rb @@ -8,6 +8,8 @@ class Step < ApplicationRecord has_one :single_date_answer has_one :checkbox_answers + scope :that_are_questions, -> { where(contentful_model: "question") } + def answer @answer ||= radio_answer || @@ -21,4 +23,8 @@ def primary_call_to_action_text return I18n.t("generic.button.next") unless super.present? super end + + def options_list + options.map { |hash| hash["value"] } + end end diff --git a/app/presenters/checkboxes_answer_presenter.rb b/app/presenters/checkboxes_answer_presenter.rb new file mode 100644 index 000000000..5da1e066a --- /dev/null +++ b/app/presenters/checkboxes_answer_presenter.rb @@ -0,0 +1,5 @@ +class CheckboxesAnswerPresenter < SimpleDelegator + def response + super.reject(&:blank?).map(&:capitalize).join(", ") + end +end diff --git a/app/presenters/long_text_answer_presenter.rb b/app/presenters/long_text_answer_presenter.rb new file mode 100644 index 000000000..e16c61fb0 --- /dev/null +++ b/app/presenters/long_text_answer_presenter.rb @@ -0,0 +1,7 @@ +class LongTextAnswerPresenter < SimpleDelegator + include ActionView::Helpers::TextHelper + + def response + simple_format(super) + end +end diff --git a/app/presenters/radio_answer_presenter.rb b/app/presenters/radio_answer_presenter.rb new file mode 100644 index 000000000..854044543 --- /dev/null +++ b/app/presenters/radio_answer_presenter.rb @@ -0,0 +1,7 @@ +class RadioAnswerPresenter < SimpleDelegator + def response + return super.capitalize if further_information.blank? + + "#{super.capitalize} - #{further_information}" + end +end diff --git a/app/presenters/short_text_answer_presenter.rb b/app/presenters/short_text_answer_presenter.rb new file mode 100644 index 000000000..fa5bb3496 --- /dev/null +++ b/app/presenters/short_text_answer_presenter.rb @@ -0,0 +1,2 @@ +class ShortTextAnswerPresenter < SimpleDelegator +end diff --git a/app/presenters/single_date_answer_presenter.rb b/app/presenters/single_date_answer_presenter.rb new file mode 100644 index 000000000..1bd0942d7 --- /dev/null +++ b/app/presenters/single_date_answer_presenter.rb @@ -0,0 +1,5 @@ +class SingleDateAnswerPresenter < SimpleDelegator + def response + I18n.l(super) + end +end diff --git a/app/services/build_journey_order.rb b/app/services/build_journey_order.rb index b2de6141d..4a5293e4b 100644 --- a/app/services/build_journey_order.rb +++ b/app/services/build_journey_order.rb @@ -3,6 +3,8 @@ class RepeatEntryDetected < StandardError; end class TooManyChainedEntriesDetected < StandardError; end + class MissingEntryDetected < StandardError; end + ENTRY_JOURNEY_MAX_LENGTH = 50 attr_accessor :entries, :starting_entry_id @@ -27,7 +29,12 @@ def entry_lookup_hash end def recursive_path(entry_lookup:, next_entry_id:, entries:) - entry = entry_lookup.fetch(next_entry_id, nil) + begin + entry = entry_lookup.fetch(next_entry_id) + rescue KeyError + send_rollbar_error(message: "A specified Contentful entry was not found", entry_id: next_entry_id) + raise MissingEntryDetected.new(next_entry_id) + end if entries.include?(entry) send_rollbar_error(message: "A repeated Contentful entry was found in the same journey", entry_id: entry.id) diff --git a/app/services/create_journey.rb b/app/services/create_journey.rb new file mode 100644 index 000000000..3322af4c8 --- /dev/null +++ b/app/services/create_journey.rb @@ -0,0 +1,30 @@ +class CreateJourney + attr_accessor :category + + def initialize(category:) + self.category = category + end + + def call + journey = Journey.create( + category: category, + next_entry_id: ENV["CONTENTFUL_PLANNING_START_ENTRY_ID"], + liquid_template: liquid_template + ) + entries = GetAllContentfulEntries.new.call + question_entries = BuildJourneyOrder.new( + entries: entries.to_a, + starting_entry_id: ENV["CONTENTFUL_PLANNING_START_ENTRY_ID"] + ).call + question_entries.each do |entry| + CreateJourneyStep.new( + journey: journey, contentful_entry: entry + ).call + end + journey + end + + private def liquid_template + FindLiquidTemplate.new(category: category).call + end +end diff --git a/app/services/create_journey_step.rb b/app/services/create_journey_step.rb index e4da8b37a..08c7e0415 100644 --- a/app/services/create_journey_step.rb +++ b/app/services/create_journey_step.rb @@ -89,8 +89,8 @@ def body end def options - return nil unless contentful_entry.respond_to?(:options) - contentful_entry.options + return nil unless contentful_entry.respond_to?(:extended_options) + contentful_entry.extended_options end def step_type diff --git a/app/services/find_liquid_template.rb b/app/services/find_liquid_template.rb new file mode 100644 index 000000000..412667553 --- /dev/null +++ b/app/services/find_liquid_template.rb @@ -0,0 +1,36 @@ +class FindLiquidTemplate + class InvalidLiquidSyntax < StandardError; end + + attr_accessor :category, :environment + + def initialize(category:, environment: Rails.env) + self.category = category + self.environment = environment + end + + def call + validate_liquid + file + rescue Liquid::SyntaxError => error + send_rollbar_error + raise InvalidLiquidSyntax.new(message: error.message) + end + + private def file + @file ||= File.read("lib/specification_templates/#{category}.#{environment}.liquid") + end + + private def validate_liquid + Liquid::Template.parse(file, error_mode: :strict) + end + + private def send_rollbar_error + Rollbar.error( + "A user couldn't start a journey because of an invalid Specification", + contentful_url: ENV["CONTENTFUL_URL"], + contentful_space_id: ENV["CONTENTFUL_SPACE"], + contentful_environment: ENV["CONTENTFUL_ENVIRONMENT"], + category: category + ) + end +end diff --git a/app/views/errors/specification_template_invalid.html.erb b/app/views/errors/specification_template_invalid.html.erb new file mode 100644 index 000000000..ca4cbaca5 --- /dev/null +++ b/app/views/errors/specification_template_invalid.html.erb @@ -0,0 +1,6 @@ +<%= content_for :title, I18n.t("errors.specification_template_invalid.page_title") %> + +

<%= I18n.t("errors.specification_template_invalid.page_title") %>

+

+ <%= I18n.t("errors.specification_template_invalid.page_body") %> +

diff --git a/app/views/journeys/_checkboxes_answer.html.erb b/app/views/journeys/_checkboxes_answer.html.erb index 5bb8d711b..6ad4e8592 100644 --- a/app/views/journeys/_checkboxes_answer.html.erb +++ b/app/views/journeys/_checkboxes_answer.html.erb @@ -1,5 +1,5 @@
<%= step.title %>
-
<%= answer.response.reject(&:blank?).map(&:capitalize).join(", ") %>
+
<%= CheckboxesAnswerPresenter.new(step.answer).response %>
<%= link_to I18n.t("generic.button.change_answer"), edit_journey_step_path(@journey, step), class: "govuk-link" %>
diff --git a/app/views/journeys/_long_text_answer.html.erb b/app/views/journeys/_long_text_answer.html.erb index 6f13b8a51..16fb50a6d 100644 --- a/app/views/journeys/_long_text_answer.html.erb +++ b/app/views/journeys/_long_text_answer.html.erb @@ -1,5 +1,5 @@
<%= step.title %>
-
<%= simple_format(step.answer.response) %>
+
<%= LongTextAnswerPresenter.new(step.answer).response %>
<%= link_to I18n.t("generic.button.change_answer"), edit_journey_step_path(@journey, step), class: "govuk-link" %>
diff --git a/app/views/journeys/_radios_answer.html.erb b/app/views/journeys/_radios_answer.html.erb index feb47caec..cba960f4f 100644 --- a/app/views/journeys/_radios_answer.html.erb +++ b/app/views/journeys/_radios_answer.html.erb @@ -1,5 +1,5 @@
<%= step.title %>
-
<%= answer.response.capitalize %>
+
<%= RadioAnswerPresenter.new(step.answer).response %>
<%= link_to I18n.t("generic.button.change_answer"), edit_journey_step_path(@journey, step), class: "govuk-link" %>
diff --git a/app/views/journeys/_short_text_answer.html.erb b/app/views/journeys/_short_text_answer.html.erb index 2f009b423..3305e2ac2 100644 --- a/app/views/journeys/_short_text_answer.html.erb +++ b/app/views/journeys/_short_text_answer.html.erb @@ -1,5 +1,5 @@
<%= step.title %>
-
<%= answer.response %>
+
<%= ShortTextAnswerPresenter.new(step.answer).response %>
<%= link_to I18n.t("generic.button.change_answer"), edit_journey_step_path(@journey, step), class: "govuk-link" %>
diff --git a/app/views/journeys/_single_date_answer.html.erb b/app/views/journeys/_single_date_answer.html.erb index 2e008f4cc..3358bb07c 100644 --- a/app/views/journeys/_single_date_answer.html.erb +++ b/app/views/journeys/_single_date_answer.html.erb @@ -1,5 +1,5 @@
<%= step.title %>
-
<%= I18n.l(answer.response) %>
+
<%= SingleDateAnswerPresenter.new(step.answer).response %>
<%= link_to I18n.t("generic.button.change_answer"), edit_journey_step_path(@journey, step), class: "govuk-link" %>
diff --git a/app/views/journeys/_specification.html.erb b/app/views/journeys/_specification.html.erb new file mode 100644 index 000000000..6fc7ca90e --- /dev/null +++ b/app/views/journeys/_specification.html.erb @@ -0,0 +1,8 @@ + + + + +

Tender specification

+ <%= yield %> + + diff --git a/app/views/journeys/show.html.erb b/app/views/journeys/show.html.erb index ceb4f5d5f..fc549a8ed 100644 --- a/app/views/journeys/show.html.erb +++ b/app/views/journeys/show.html.erb @@ -1,10 +1,30 @@ <%= content_for :title, @journey.category.capitalize %>

<%= @journey.category.capitalize %>

-
- <% @steps.each do |step| %> - <% if step.question? %> - <%= render "#{step.contentful_type}_answer", step: step, answer: step.answer %> - <% end %> - <% end %> -
+
    +
  1. + +
  2. +
+ +

<%= I18n.t("journey.specification.header") %>

+<%= link_to "Download (.docx)", journey_path(@journey, format: :docx), class: "govuk-button" %> +<%= @specification_template.render(@answers).html_safe %> diff --git a/app/views/steps/checkboxes.html.erb b/app/views/steps/checkboxes.html.erb index 63d43274f..60bb18963 100644 --- a/app/views/steps/checkboxes.html.erb +++ b/app/views/steps/checkboxes.html.erb @@ -1,8 +1,8 @@ <%= render layout: layout do |f| %> <%= f.govuk_collection_check_boxes :response, - checkbox_options(array_of_options: @step.options), + checkbox_options(array_of_options: @step.options_list), :id, :name, - legend: { text: @step.title, size: "xl" } + legend: { text: @step.title, size: "l" } %> <% end %> diff --git a/app/views/steps/long_text.html.erb b/app/views/steps/long_text.html.erb index 5933f8d45..4f4984116 100644 --- a/app/views/steps/long_text.html.erb +++ b/app/views/steps/long_text.html.erb @@ -1,6 +1,6 @@ <%= render layout: layout do |f| %> <%= f.govuk_text_area :response, - label: { text: @step.title, size: 'xl' }, + label: { text: @step.title, size: "l" }, hint: { text: @step.help_text }, width: "one-third", rows: 9 diff --git a/app/views/steps/paragraphs.html.erb b/app/views/steps/paragraphs.html.erb index 67a00db56..39f541597 100644 --- a/app/views/steps/paragraphs.html.erb +++ b/app/views/steps/paragraphs.html.erb @@ -4,4 +4,4 @@
<%= simple_format(@step.body, wrapper_tag: "p", class: "govuk-body") %>
-<%= link_to @step.primary_call_to_action_text, new_journey_step_path, class: "govuk-button" %> +<%= link_to @step.primary_call_to_action_text, journey_path(@journey), class: "govuk-button" %> diff --git a/app/views/steps/radios.html.erb b/app/views/steps/radios.html.erb index d0dcfb0ce..b3ce99def 100644 --- a/app/views/steps/radios.html.erb +++ b/app/views/steps/radios.html.erb @@ -1,12 +1,20 @@ <%= render layout: layout do |f| %> <%= f.hidden_field :response, value: nil %> - <%= f.govuk_collection_radio_buttons :response, - radio_options(array_of_options: @step.options), - :id, - ->(option) { option.name }, - :description, - legend: { text: @step.title, size: 'xl' }, - hint: { text: @step.help_text }, - inline: false - %> + <%= f.govuk_radio_buttons_fieldset(:response, legend: { size: "l", text: @step.title }) do %> + <% if @step.help_text.present? %> + + <%= @step.help_text %> + + <% end %> + + <% @step.options.each do |option| %> + <% if option["display_further_information"] == true %> + <%= f.govuk_radio_button :response, option["value"].downcase, label: { text: option["value"] }, hint: { text: option["help_text"] } do %> + <%= f.govuk_text_area :further_information, rows: 1, label: { text: option["further_information_help_text"] } %> + <% end %> + <% else %> + <%= f.govuk_radio_button :response, option["value"].downcase, label: { text: option["value"] }, hint: { text: option["help_text"] } %> + <% end %> + <% end %> + <% end %> <% end %> diff --git a/app/views/steps/short_text.html.erb b/app/views/steps/short_text.html.erb index 3d414cc28..172c6649d 100644 --- a/app/views/steps/short_text.html.erb +++ b/app/views/steps/short_text.html.erb @@ -1,6 +1,6 @@ <%= render layout: layout do |f| %> <%= f.govuk_text_field :response, - label: { text: @step.title, size: 'xl' }, + label: { text: @step.title, size: "l" }, hint: { text: @step.help_text }, width: "one-third" %> diff --git a/app/views/steps/single_date.html.erb b/app/views/steps/single_date.html.erb index 0cc149ca1..b09581207 100644 --- a/app/views/steps/single_date.html.erb +++ b/app/views/steps/single_date.html.erb @@ -1,6 +1,6 @@ <%= render layout: layout do |f| %> <%= f.govuk_date_field :response, - legend: { text: @step.title, size: "xl" }, + legend: { text: @step.title, size: "l" }, hint: { text: @step.help_text } %> <% end %> diff --git a/config/environments/test.rb b/config/environments/test.rb index e5e40667f..87d513e60 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -63,5 +63,6 @@ Bullet.add_whitelist type: :unused_eager_loading, class_name: "Step", association: :short_text_answer Bullet.add_whitelist type: :unused_eager_loading, class_name: "Step", association: :long_text_answer Bullet.add_whitelist type: :unused_eager_loading, class_name: "Step", association: :single_date_answer + Bullet.add_whitelist type: :unused_eager_loading, class_name: "Step", association: :checkbox_answers end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 64910dae2..600ad125b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,6 +1,6 @@ en: app: - name: Buy for your school + name: Get help buying for schools date: formats: default: "%-d %b %Y" @@ -36,6 +36,13 @@ en: before_you_start_body: "It will take around 3 minutes to complete the process" errors: contentful_entry_not_found: "An unexpected error occurred. The starting step has been revoked by the content team." + journey: + specification: + header: "Your specification" + task_list: + status: + not_started: Not started + completed: Completed journey_map: page_title: "Contentful entry map" errors: @@ -54,3 +61,6 @@ en: too_many_steps_in_the_contentful_journey: page_title: "An unexpected error occurred" page_body: "More than %{step_count} steps were found in the Contentful journey. Is the journey missing an end? The last Entry ID was: %{entry_id}" + specification_template_invalid: + page_title: "An unexpected error occurred" + page_body: "The service has had a problem trying to retrieve a working Specification template. The team have been notified of this problem and you should be able to retry shortly." diff --git a/db/migrate/20210107122307_add_liquid_template_to_journey.rb b/db/migrate/20210107122307_add_liquid_template_to_journey.rb new file mode 100644 index 000000000..3cdb390f1 --- /dev/null +++ b/db/migrate/20210107122307_add_liquid_template_to_journey.rb @@ -0,0 +1,6 @@ +class AddLiquidTemplateToJourney < ActiveRecord::Migration[6.1] + def change + # Journeys already exist in real environments from our tests, we can delete them all and set the null validation + add_column :journeys, :liquid_template, :jsonb, null: false + end +end diff --git a/db/migrate/20210118110545_change_options_to_jsonb.rb b/db/migrate/20210118110545_change_options_to_jsonb.rb new file mode 100644 index 000000000..1b795729e --- /dev/null +++ b/db/migrate/20210118110545_change_options_to_jsonb.rb @@ -0,0 +1,6 @@ +class ChangeOptionsToJsonb < ActiveRecord::Migration[6.1] + def change + remove_column :steps, :options + add_column :steps, :options, :jsonb + end +end diff --git a/db/migrate/20210118123111_add_further_information_to_radio_answer.rb b/db/migrate/20210118123111_add_further_information_to_radio_answer.rb new file mode 100644 index 000000000..523454532 --- /dev/null +++ b/db/migrate/20210118123111_add_further_information_to_radio_answer.rb @@ -0,0 +1,5 @@ +class AddFurtherInformationToRadioAnswer < ActiveRecord::Migration[6.1] + def change + add_column :radio_answers, :further_information, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index e24feb3ef..17fe498e7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_12_16_160028) do +ActiveRecord::Schema.define(version: 2021_01_18_123111) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" @@ -30,6 +30,7 @@ t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "next_entry_id" + t.jsonb "liquid_template" end create_table "long_text_answers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -45,6 +46,7 @@ t.string "response", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.text "further_information" t.index ["step_id"], name: "index_radio_answers_on_step_id" end @@ -69,7 +71,6 @@ t.string "title", null: false t.string "help_text" t.string "contentful_type", null: false - t.string "options", array: true t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.text "body" @@ -77,6 +78,7 @@ t.string "primary_call_to_action_text" t.string "contentful_id", null: false t.jsonb "raw", null: false + t.jsonb "options" t.index ["journey_id"], name: "index_steps_on_journey_id" end diff --git a/docker-compose.yml b/docker-compose.yml index a68d0031a..563dd002d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: args: RAILS_ENV: "development" BUNDLE_EXTRA_GEM_GROUPS: "development test" - command: bundle exec rails s -p 3000 -b '0.0.0.0' + command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" ports: - "3000:3000" image: "buy_for_your_school:dev" diff --git a/lib/specification_templates/catering.development.liquid b/lib/specification_templates/catering.development.liquid new file mode 100644 index 000000000..5ba71c4e5 --- /dev/null +++ b/lib/specification_templates/catering.development.liquid @@ -0,0 +1,36 @@ +
+
+

Menus and ordering

+

Food standards

+
    +
  1. +

    It will be the suppliers responsibility to ensure that all food served within the school day complies with both current and future government legislation and guidelines on the provision of healthy school meals.

    +

    The supplier should encourage the use of seasonal produce and promotion of healthy eating to pupils wherever practical and desirable.

    +

    It will be the suppliers responsibility to comply fully with DfE food and nutrient based standards, and to promote and comply with this policy throughout the contract term through effective menu planning.

    +
  2. + + {% if answer_NxJWpbiFeEAmvcw17EysX %} +
  3. +

    The school also requires the service to comply with the following non-mandatory food standards or schemes:

    +

    {{answer_NxJWpbiFeEAmvcw17EysX}}

    +
  4. + {% endif %} +
  5. +

    The supplier must work with the school to provide safe and enjoyable meals for any pupils with allergies or intolerances, ensuring that the ingredients, preparation and handling of food for those children are completely allergen-free.

    +
  6. + + {% if answer_5xxbqrasSXH9x9Lt3YhRkX contains "yes" %} +
  7. +

    The supplier is required to track allergen information through their supply chain and must be able to demonstrate their allergen tracking plan.

    +
  8. + {% endif %} + + {% if answer_4HfGoYL6s806Azx93jp5IY %} +
  9. +

    The school has the following objectives around the quantity, quality or variety of food offered, beyond the mandatory standards:

    +

    {{answer_4HfGoYL6s806Azx93jp5IY}}

    +
  10. + {% endif %} +
+
+
diff --git a/lib/specification_templates/catering.production.liquid b/lib/specification_templates/catering.production.liquid new file mode 100644 index 000000000..6d06700c5 --- /dev/null +++ b/lib/specification_templates/catering.production.liquid @@ -0,0 +1,56 @@ +
+
+

Menus and ordering

+

Food standards

+
    + +
  1. +

    It will be the suppliers responsibility to ensure that all food served within the school day complies with both current and future government legislation and guidelines on the provision of healthy school meals.

    +

    The supplier should encourage the use of seasonal produce and promotion of healthy eating to pupils wherever practical and desirable.

    +

    It will be the suppliers responsibility to comply fully with DfE food and nutrient based standards, and to promote and comply with this policy throughout the contract term through effective menu planning.

    +
  2. + + {% if answer_2fVajdGxgwD58vt4VvAI9Y %} +
  3. +

    The school also requires the service to comply with the following non-mandatory food standards or schemes:

    +

    {{answer_2fVajdGxgwD58vt4VvAI9Y}}

    +
  4. + {% endif %} + +
  5. +

    The supplier must work with the school to provide safe and enjoyable meals for any pupils with allergies or intolerances, ensuring that the ingredients, preparation and handling of food for those children are completely allergen-free.

    +
  6. + +
  7. +

    All food and drink must comply with food labelling law, which says you must provide information to customers on any of the 14 allergens used as ingredients in foods you make and sell. It is important that all staff receive training and information on the 14 allergens contained in food.

    +
  8. + + {% if answer_ypxhCAkhp2qmFiapHpVpK contains "Yes" %} +
  9. +

    All ingredients, handling and preparation of food and drink provided by the supplier must be free from:

    +

    {{answer_ypxhCAkhp2qmFiapHpVpK}}

    +
  10. + {% endif %} + + {% if answer_5tq13woHxVnzfOZFixoqku contains "Yes" %} +
  11. +

    The supplier is required to track allergen information through their supply chain and must be able to demonstrate their allergen tracking plan.

    +
  12. + {% endif %} + + {% if answer_17OxghBLlrGQypc5NflRSW contains "Yes" %} +
  13. +

    The school has the following requirements for how the School Food Standards are met:

    +

    {{answer_17OxghBLlrGQypc5NflRSW}}

    +
  14. + {% endif %} + + {% if answer_nJBe4ba05Av5JfKEFIxn7 %} +
  15. +

    The school has the following objectives around the quantity, quality or variety of food offered, beyond the mandatory standards:

    +

    {{answer_nJBe4ba05Av5JfKEFIxn7}}

    +
  16. + {% endif %} +
+
+
diff --git a/lib/specification_templates/catering.test.liquid b/lib/specification_templates/catering.test.liquid new file mode 100644 index 000000000..340b3c934 --- /dev/null +++ b/lib/specification_templates/catering.test.liquid @@ -0,0 +1,19 @@ +
+
+

Menus and ordering

+

Food standards

+
    +
  1. +

    It will be the suppliers responsibility to ensure that all food served within the school day complies with both current and future government legislation and guidelines on the provision of healthy school meals.

    +

    The supplier should encourage the use of seasonal produce and promotion of healthy eating to pupils wherever practical and desirable.

    +

    It will be the suppliers responsibility to comply fully with DfE food and nutrient based standards, and to promote and comply with this policy throughout the contract term through effective menu planning.

    +
  2. + {% if answer_NxJWpbiFeEAmvcw17EysX %} +
  3. +

    The school also requires the service to comply with the following non-mandatory food standards or schemes:

    +

    {{answer_NxJWpbiFeEAmvcw17EysX}}

    +
  4. + {% endif %} + +
+
diff --git a/package-lock.json b/package-lock.json index c80baaba2..95dc44d75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,9 +4,9 @@ "lockfileVersion": 1, "dependencies": { "govuk-frontend": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-3.10.1.tgz", - "integrity": "sha512-2x6B0jV1pTx4Alxm3MY222BfIQpg+ctUYaUzyBjmNkisWKdRmCP61oyFXklZgqXMjvaN+oo3rRbAALrxFTeeig==" + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/govuk-frontend/-/govuk-frontend-3.10.2.tgz", + "integrity": "sha512-MpMymgLsKoMw40MggZ0XLCAj1FY5N2s8Pf3aQR+k0cZOsegjLsnejxNfEB9qEl9jcma2fiiVcvsEZ+Ipo+Oo2g==" } } } diff --git a/package.json b/package.json index a997cf647..ade382d33 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,6 @@ "name": "rails-template", "private": true, "dependencies": { - "govuk-frontend": "^3.10.1" + "govuk-frontend": "^3.10.2" } } diff --git a/spec/factories/answer.rb b/spec/factories/answer.rb index 6b89a1498..48e361a25 100644 --- a/spec/factories/answer.rb +++ b/spec/factories/answer.rb @@ -3,6 +3,7 @@ association :step, factory: :step, contentful_type: "radios", contentful_model: "question" response { "Green" } + further_information { nil } end factory :short_text_answer do @@ -18,13 +19,13 @@ end factory :single_date_answer do - association :step, factory: :step, contentful_type: "single_date" + association :step, factory: :step, contentful_type: "single_date", contentful_model: "question" response { 1.year.from_now } end factory :checkbox_answers do - association :step, factory: :step, contentful_type: "checkboxes" + association :step, factory: :step, options: [{"value" => "Breakfast"}, {"value" => "Lunch"}], contentful_type: "checkboxes", contentful_model: "question" response { ["breakfast", "lunch", ""] } end diff --git a/spec/factories/journey.rb b/spec/factories/journey.rb index 00bf406d8..c24800da3 100644 --- a/spec/factories/journey.rb +++ b/spec/factories/journey.rb @@ -2,6 +2,7 @@ factory :journey do category { "catering" } next_entry_id { "47EI2X2T5EDTpJX9WjRR9p" } + liquid_template { "Your answer was {{ answer_47EI2X2T5EDTpJX9WjRR9p }}" } trait :catering do category { "catering" } diff --git a/spec/factories/step.rb b/spec/factories/step.rb index e1ed7a3b0..016694639 100644 --- a/spec/factories/step.rb +++ b/spec/factories/step.rb @@ -2,13 +2,13 @@ factory :step do title { "What is your favourite colour?" } help_text { "Choose the primary colour closest to your choice" } - raw { "{\"sys\":{\"id\"=>\"123\"}}" } + raw { {"sys": {"id" => "123"}} } contentful_id { "123" } association :journey, factory: :journey trait :radio do - options { ["Red", "Green", "Blue"] } + options { [{"value" => "Red"}, {"value" => "Green"}, {"value" => "Blue"}] } contentful_model { "question" } contentful_type { "radios" } association :radio_answer @@ -36,7 +36,7 @@ end trait :checkbox_answers do - options { ["Brown", "Gold"] } + options { [{"value" => "Brown"}, {"value" => "Gold"}] } contentful_model { "question" } contentful_type { "checkboxes" } association :checkbox_answers @@ -46,7 +46,6 @@ options { nil } contentful_model { "staticContent" } contentful_type { "paragraphs" } - association :paragraph_content end end end diff --git a/spec/features/visitors/anyone_can_complete_a_journey_spec.rb b/spec/features/visitors/anyone_can_complete_a_journey_spec.rb index c27a39bd1..c175e424a 100644 --- a/spec/features/visitors/anyone_can_complete_a_journey_spec.rb +++ b/spec/features/visitors/anyone_can_complete_a_journey_spec.rb @@ -3,35 +3,36 @@ feature "Anyone can start a journey" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "1UjQurSOi5MWkcRuGxdXZS" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end end scenario "Start page includes a call to action" do - stub_get_contentful_entry + stub_get_contentful_entries( + entry_id: "contentful-starting-step", + fixture_filename: "closed-path-with-multiple-example.json" + ) visit root_path click_on(I18n.t("generic.button.start")) - expect(page).to have_content("Which service do you need?") - expect(page).to have_content("Tell us which service you need.") expect(page).to have_content("Catering") - expect(page).to have_content("Cleaning") - - choose("Catering") - - click_on(I18n.t("generic.button.next")) + expect(page).to have_content("Which service do you need?") + expect(page).to have_content("Not started") end scenario "an answer must be provided" do - stub_get_contentful_entry + stub_get_contentful_entries( + entry_id: "contentful-starting-step", + fixture_filename: "closed-path-with-multiple-example.json" + ) + journey = CreateJourney.new(category: "catering").call + step = journey.steps.find_by(contentful_id: "contentful-radio-question") - visit root_path - - click_on(I18n.t("generic.button.start")) + visit journey_step_path(journey, step) # Omit a choice @@ -40,113 +41,81 @@ expect(page).to have_content("can't be blank") end - context "when the starter step has a next step" do - around do |example| - ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "47EI2X2T5EDTpJX9WjRR9p" - ) do - example.run - end - end - - scenario "there are 2 steps to answer" do - visit root_path - - stub_get_contentful_entry( - entry_id: "47EI2X2T5EDTpJX9WjRR9p", - fixture_filename: "has-next-question-example.json" - ) - click_on(I18n.t("generic.button.start")) - - choose("Catering") - - stub_get_contentful_entry( - entry_id: "5lYcZs1ootDrOnk09LDLZg", - fixture_filename: "no-next-question-example.json" - ) - - click_on(I18n.t("generic.button.next")) - - choose("Stationary") - click_on(I18n.t("generic.button.next")) - - expect(page).to have_content("Catering") - expect(page).to have_content("Stationary") - end - end - context "when the Contentful model is of type question" do context "when Contentful entry is of type short_text" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "hfjJgWRg4xiiiImwVRDtZ" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end end scenario "user can answer using free text" do - stub_get_contentful_entry( - entry_id: "hfjJgWRg4xiiiImwVRDtZ", - fixture_filename: "short-text-question-example.json" + stub_get_contentful_entries( + entry_id: "contentful-starting-step", + fixture_filename: "closed-path-with-multiple-example.json" ) + journey = CreateJourney.new(category: "catering").call + step = journey.steps.find_by(contentful_id: "contentful-short-text-question") - visit root_path - click_on(I18n.t("generic.button.start")) + visit journey_step_path(journey, step) fill_in "answer[response]", with: "email@example.com" click_on(I18n.t("generic.button.next")) - expect(page).to have_content("email@example") + click_on(step.title) + + expect(find_field("answer-response-field").value).to eql("email@example.com") end end context "when Contentful entry is of type long_text" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "2jWIO1MrVIya9NZrFWT4e" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end end scenario "user can answer using free text with multiple lines" do - stub_get_contentful_entry( - entry_id: "2jWIO1MrVIya9NZrFWT4e", - fixture_filename: "long-text-question-example.json" + stub_get_contentful_entries( + entry_id: "contentful-starting-step", + fixture_filename: "closed-path-with-multiple-example.json" ) + journey = CreateJourney.new(category: "catering").call + step = journey.steps.find_by(contentful_id: "contentful-long-text-question") - visit root_path - click_on(I18n.t("generic.button.start")) + visit journey_step_path(journey, step) - fill_in "answer[response]", with: "We would like a supplier to provide catering from September 2020.\r\nThey must be able to supply us for 3 years minumum." + fill_in "answer[response]", with: "We would like a supplier to provide catering from September 2020.\nThey must be able to supply us for 3 years minumum." click_on(I18n.t("generic.button.next")) - within(".govuk-summary-list") do - paragraphs_elements = find_all("p") - expect(paragraphs_elements.first.text).to have_content("We would like a supplier to provide catering from September 2020.") - expect(paragraphs_elements.last.text).to have_content("They must be able to supply us for 3 years minumum.") - end + click_on(step.title) + + expect(find_field("answer-response-field").value).to eql("We would like a supplier to provide catering from September 2020.\r\nThey must be able to supply us for 3 years minumum.") end end context "when Contentful entry is of type single_date" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "55G5kpCLLL3h5yBQLiVlYy" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end end scenario "user can answer using a date input" do - stub_get_contentful_entry( - entry_id: "55G5kpCLLL3h5yBQLiVlYy", - fixture_filename: "single-date-example.json" + stub_get_contentful_entries( + entry_id: "contentful-starting-step", + fixture_filename: "closed-path-with-multiple-example.json" ) + journey = CreateJourney.new(category: "catering").call + step = journey.steps.find_by(contentful_id: "contentful-single-date-question") - visit root_path - click_on(I18n.t("generic.button.start")) + visit journey_step_path(journey, step) fill_in "answer[response(3i)]", with: "12" fill_in "answer[response(2i)]", with: "8" @@ -154,34 +123,78 @@ click_on(I18n.t("generic.button.next")) - expect(page).to have_content("12 Aug 2020") + click_on(step.title) + + expect(find_field("answer_response_3i").value).to eql("12") + expect(find_field("answer_response_2i").value).to eql("8") + expect(find_field("answer_response_1i").value).to eql("2020") end end context "when Contentful entry is of type checkboxes" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "1DqhwF2XkJJ0Um6NSweWlZ" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end end scenario "user can select multiple answers" do - stub_get_contentful_entry( - entry_id: "1DqhwF2XkJJ0Um6NSweWlZ", - fixture_filename: "checkbox-example.json" + stub_get_contentful_entries( + entry_id: "contentful-starting-step", + fixture_filename: "closed-path-with-multiple-example.json" ) + journey = CreateJourney.new(category: "catering").call + step = journey.steps.find_by(contentful_id: "contentful-checkboxes-question") - visit root_path - click_on(I18n.t("generic.button.start")) + visit journey_step_path(journey, step) check "Breakfast" check "Lunch" click_on(I18n.t("generic.button.next")) - expect(page).to have_content("Breakfast, Lunch") + click_on(step.title) + + expect(page).to have_checked_field("answer-response-breakfast-field") + expect(page).to have_checked_field("answer-response-lunch-field") + end + end + + context "when Contentful entry is of type radios" do + context "when extra configuration is passed to collect further info" do + around do |example| + ClimateControl.modify( + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" + ) do + example.run + end + end + + scenario "asks the user for further information" do + stub_get_contentful_entries( + entry_id: "contentful-starting-step", + fixture_filename: "closed-path-with-multiple-example.json" + ) + journey = CreateJourney.new(category: "catering").call + step = journey.steps.find_by(contentful_id: "contentful-radio-question") + + visit journey_step_path(journey, step) + + click_on(I18n.t("generic.button.start")) + + choose("Catering") + fill_in "answer[further_information]", with: "The school needs the kitchen cleaned once a day" + + click_on(I18n.t("generic.button.next")) + + click_on(step.title) + + expect(page).to have_checked_field("Catering") + expect(find_field("answer-further-information-field").value) + .to eql("The school needs the kitchen cleaned once a day") + end end end end @@ -190,20 +203,21 @@ context "when Contentful entry is of type paragraphs" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "5kZ9hIFDvNCEhjWs72SFwj" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end end scenario "user can read static content and proceed without answering" do - stub_get_contentful_entry( - entry_id: "5kZ9hIFDvNCEhjWs72SFwj", - fixture_filename: "static-content-example.json" + stub_get_contentful_entries( + entry_id: "contentful-starting-step", + fixture_filename: "closed-path-with-multiple-example.json" ) + journey = CreateJourney.new(category: "catering").call + step = journey.steps.find_by(contentful_id: "contentful-starting-step") - visit root_path - click_on(I18n.t("generic.button.start")) + visit journey_step_path(journey, step) expect(page).to have_content("When you should start") @@ -223,21 +237,19 @@ context "when Contentful entry model wasn't an expected type" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "6EKsv389ETYcQql3htK3Z2" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-unexpected-model" ) do example.run end end scenario "returns an error message" do - stub_get_contentful_entry( - entry_id: "6EKsv389ETYcQql3htK3Z2", - fixture_filename: "an-unexpected-model-example.json" + stub_get_contentful_entries( + entry_id: "contentful-unexpected-model", + fixture_filename: "path-with-unexpected-model.json" ) - visit root_path - - click_on(I18n.t("generic.button.start")) + visit new_journey_path expect(page).to have_content(I18n.t("errors.unexpected_contentful_model.page_title")) expect(page).to have_content(I18n.t("errors.unexpected_contentful_model.page_body")) @@ -247,39 +259,58 @@ context "when the Contentful Entry wasn't an expected step type" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "8as7df68uhasdnuasdf" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-unexpected-step-type" ) do example.run end end scenario "returns an error message" do - stub_get_contentful_entry( - entry_id: "8as7df68uhasdnuasdf", - fixture_filename: "an-unexpected-question-type-example.json" + stub_get_contentful_entries( + entry_id: "contentful-unexpected-step-type", + fixture_filename: "path-with-unexpected-step-type.json" ) - visit root_path - - click_on(I18n.t("generic.button.start")) + visit new_journey_path expect(page).to have_content(I18n.t("errors.unexpected_contentful_step_type.page_title")) expect(page).to have_content(I18n.t("errors.unexpected_contentful_step_type.page_body")) end end - scenario "a Contentful entry_id does not exist" do - contentful_client = stub_contentful_client + context "when the starting entry id doesn't exist" do + around do |example| + ClimateControl.modify( + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-fake-entry-id" + ) do + example.run + end + end - allow(contentful_client).to receive(:entry) - .with(anything) - .and_raise(GetContentfulEntry::EntryNotFound.new("The following Contentful error could not be found: sss ")) + scenario "a Contentful entry_id does not exist" do + stub_get_contentful_entries( + entry_id: "contentful-fake-entry-id", + fixture_filename: "closed-path-with-multiple-example.json" + ) - visit root_path + visit new_journey_path - click_on(I18n.t("generic.button.start")) + expect(page).to have_content(I18n.t("errors.contentful_entry_not_found.page_title")) + expect(page).to have_content(I18n.t("errors.contentful_entry_not_found.page_body")) + end + end + + context "when the Liquid template was invalid" do + it "raises an error" do + fake_liquid_template = File.read("#{Rails.root}/spec/fixtures/specification_templates/invalid.liquid") + allow_any_instance_of(FindLiquidTemplate).to receive(:file).and_return(fake_liquid_template) + + visit root_path + + click_on(I18n.t("generic.button.start")) - expect(page).to have_content(I18n.t("errors.contentful_entry_not_found.page_title")) - expect(page).to have_content(I18n.t("errors.contentful_entry_not_found.page_body")) + expect(page).to have_content(I18n.t("errors.specification_template_invalid.page_title")) + expect(page).to have_content(I18n.t("errors.specification_template_invalid.page_body")) + end end end diff --git a/spec/features/visitors/anyone_can_download_their_catering_specification_spec.rb b/spec/features/visitors/anyone_can_download_their_catering_specification_spec.rb new file mode 100644 index 000000000..91ae26a98 --- /dev/null +++ b/spec/features/visitors/anyone_can_download_their_catering_specification_spec.rb @@ -0,0 +1,29 @@ +feature "Users can see their catering specification" do + scenario "HTML" do + journey = create(:journey, :catering, liquid_template: stub_liquid_template) + step = create(:step, :long_text, long_text_answer: nil, journey: journey, contentful_id: "NxJWpbiFeEAmvcw17EysX") + _answer = create(:long_text_answer, step: step, response: "Red tractor") + + visit journey_path(journey) + + expect(page).to have_content(I18n.t("journey.specification.header")) + + click_on("Download (.docx)") + + expect(page.response_headers["Content-Type"]).to eql("application/vnd.openxmlformats-officedocument.wordprocessingml.document") + header = page.response_headers["Content-Disposition"] + expect(header).to match(/^attachment/) + expect(header).to match(/filename="specification.docx"/) + end + + def stub_liquid_template + fake_liquid_template = File.read("#{Rails.root}/spec/fixtures/specification_templates/food_catering.liquid") + + finder = instance_double(FindLiquidTemplate) + allow(FindLiquidTemplate).to receive(:new).with(category: "catering") + .and_return(finder) + allow(finder).to receive(:call).and_return(fake_liquid_template) + + fake_liquid_template + end +end diff --git a/spec/features/visitors/anyone_can_edit_their_answers_spec.rb b/spec/features/visitors/anyone_can_edit_their_answers_spec.rb index ee64ff27d..73b8b0ede 100644 --- a/spec/features/visitors/anyone_can_edit_their_answers_spec.rb +++ b/spec/features/visitors/anyone_can_edit_their_answers_spec.rb @@ -2,30 +2,71 @@ feature "Users can edit their answers" do let(:answer) { create(:short_text_answer, response: "answer") } - scenario "The journey summary page displays a change link" do - visit journey_path(answer.step.journey) - expect(page).to have_content("answer") - expect(page).to have_content("Change") + context "when the question is short_text" do + let(:answer) { create(:short_text_answer, response: "answer") } + + scenario "The edited answer is saved" do + visit journey_path(answer.step.journey) + + click_on(answer.step.title) + + fill_in "answer[response]", with: "email@example.com" + + click_on(I18n.t("generic.button.update")) + + click_on(answer.step.title) + + expect(find_field("answer-response-field").value).to eql("email@example.com") + end end - scenario "The edited answer is saved" do - visit journey_path(answer.step.journey) + context "when the question is single_date" do + let(:answer) { create(:single_date_answer, response: 1.year.ago) } + + scenario "The edited answer is saved" do + visit journey_path(answer.step.journey) + + click_on(answer.step.title) - click_on(I18n.t("generic.button.change_answer")) + fill_in "answer[response(3i)]", with: "12" + fill_in "answer[response(2i)]", with: "8" + fill_in "answer[response(1i)]", with: "2020" - fill_in "answer[response]", with: "email@example.com" + click_on(I18n.t("generic.button.update")) - click_on(I18n.t("generic.button.update")) + click_on(answer.step.title) - expect(page).to have_content("email@example.com") + expect(find_field("answer_response_3i").value).to eql("12") + expect(find_field("answer_response_2i").value).to eql("8") + expect(find_field("answer_response_1i").value).to eql("2020") + end + end + + context "when the question is checkbox_answers" do + let(:answer) { create(:checkbox_answers, response: ["breakfast", "lunch", ""]) } + + scenario "The edited answer is saved" do + visit journey_path(answer.step.journey) + + click_on(answer.step.title) + + uncheck "Breakfast" + + click_on(I18n.t("generic.button.update")) + + click_on(answer.step.title) + + expect(page).not_to have_checked_field("answer-response-breakfast-field") + expect(page).to have_checked_field("answer-response-lunch-field") + end end context "An error is thrown" do scenario "When an answer is invalid" do visit journey_path(answer.step.journey) - click_on(I18n.t("generic.button.change_answer")) + click_on(answer.step.title) fill_in "answer[response]", with: "" diff --git a/spec/features/visitors/anyone_can_see_a_planning_start_page_spec.rb b/spec/features/visitors/anyone_can_see_a_planning_start_page_spec.rb index 963546264..31203aa32 100644 --- a/spec/features/visitors/anyone_can_see_a_planning_start_page_spec.rb +++ b/spec/features/visitors/anyone_can_see_a_planning_start_page_spec.rb @@ -1,3 +1,5 @@ +require "rails_helper" + feature "Users can see a start page for planning their purchase" do scenario "Start page content is shown on the root path" do visit root_path diff --git a/spec/features/visitors/anyone_can_see_the_map_of_journey_steps_page_spec.rb b/spec/features/visitors/anyone_can_see_the_map_of_journey_steps_page_spec.rb index 44ea047b6..cfde7b168 100644 --- a/spec/features/visitors/anyone_can_see_the_map_of_journey_steps_page_spec.rb +++ b/spec/features/visitors/anyone_can_see_the_map_of_journey_steps_page_spec.rb @@ -1,7 +1,7 @@ feature "Users can see all the steps of a journey" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "5kZ9hIFDvNCEhjWs72SFwj" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end @@ -9,7 +9,7 @@ scenario "Multiple journey steps" do stub_get_contentful_entries( - entry_id: "5kZ9hIFDvNCEhjWs72SFwj", + entry_id: "contentful-starting-step", fixture_filename: "closed-path-with-multiple-example.json" ) @@ -20,10 +20,10 @@ within(".govuk-list") do list_items = find_all("li") within(list_items.first) do - expect(page).to have_link("When you should start", href: "https://app.contentful.com/spaces/#{ENV["CONTENTFUL_SPACE"]}/environments/#{ENV["CONTENTFUL_ENVIRONMENT"]}/entries/5kZ9hIFDvNCEhjWs72SFwj") + expect(page).to have_link("When you should start", href: "https://app.contentful.com/spaces/#{ENV["CONTENTFUL_SPACE"]}/environments/#{ENV["CONTENTFUL_ENVIRONMENT"]}/entries/contentful-starting-step") end within(list_items.last) do - expect(page).to have_link("Which service do you need?", href: "https://app.contentful.com/spaces/#{ENV["CONTENTFUL_SPACE"]}/environments/#{ENV["CONTENTFUL_ENVIRONMENT"]}/entries/hfjJgWRg4xiiiImwVRDtZ") + expect(page).to have_link("Everyday services that are required and need to be considered", href: "https://app.contentful.com/spaces/#{ENV["CONTENTFUL_SPACE"]}/environments/#{ENV["CONTENTFUL_ENVIRONMENT"]}/entries/contentful-checkboxes-question") end end end @@ -32,7 +32,7 @@ context "when the same entry is found twice" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "5kZ9hIFDvNCEhjWs72SFwj" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end @@ -40,7 +40,7 @@ it "returns an error message" do stub_get_contentful_entries( - entry_id: "5kZ9hIFDvNCEhjWs72SFwj", + entry_id: "contentful-starting-step", fixture_filename: "repeat-entry-example.json" ) @@ -48,7 +48,7 @@ expect(page).to have_content(I18n.t("errors.repeat_step_in_the_contentful_journey.page_title")) expect(page).to have_content( - I18n.t("errors.repeat_step_in_the_contentful_journey.page_body", entry_id: "5kZ9hIFDvNCEhjWs72SFwj") + I18n.t("errors.repeat_step_in_the_contentful_journey.page_body", entry_id: "contentful-starting-step") ) end end @@ -56,7 +56,7 @@ context "when the chain becomes obviously too long" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "5kZ9hIFDvNCEhjWs72SFwj" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end @@ -65,7 +65,7 @@ it "returns an error message" do stub_const("BuildJourneyOrder::ENTRY_JOURNEY_MAX_LENGTH", 1) stub_get_contentful_entries( - entry_id: "hfjJgWRg4xiiiImwVRDtZ", + entry_id: "contentful-radio-question", fixture_filename: "closed-path-with-multiple-example.json" ) @@ -73,7 +73,7 @@ expect(page).to have_content(I18n.t("errors.too_many_steps_in_the_contentful_journey.page_title")) expect(page).to have_content( - I18n.t("errors.too_many_steps_in_the_contentful_journey.page_body", entry_id: "hfjJgWRg4xiiiImwVRDtZ", step_count: 1) + I18n.t("errors.too_many_steps_in_the_contentful_journey.page_body", entry_id: "contentful-radio-question", step_count: 1) ) end end diff --git a/spec/features/visitors/anyone_can_see_their_catering_specification_spec.rb b/spec/features/visitors/anyone_can_see_their_catering_specification_spec.rb new file mode 100644 index 000000000..b0ec0e1ee --- /dev/null +++ b/spec/features/visitors/anyone_can_see_their_catering_specification_spec.rb @@ -0,0 +1,34 @@ +feature "Users can see their catering specification" do + scenario "HTML" do + liquid_template = stub_liquid_template(filename: "food_catering.liquid") + journey = create(:journey, :catering, liquid_template: liquid_template) + step = create(:step, :long_text, long_text_answer: nil, journey: journey, contentful_id: "NxJWpbiFeEAmvcw17EysX") + answer = create(:long_text_answer, step: step, response: "Red tractor") + + visit journey_path(journey) + + expect(page).to have_content(I18n.t("journey.specification.header")) + + within("article#specification") do + expect(page).to have_content("Menus and ordering") + expect(page).to have_content("Food standards") + expect(page).to have_content("The school also requires the service to comply with the following non-mandatory food standards or schemes:") + expect(page).to have_content(answer.response) + end + end + + scenario "renders responses that need extra formatting" do + liquid_template = stub_liquid_template(filename: "food_catering.liquid") + journey = create(:journey, :catering, liquid_template: liquid_template) + step = create(:step, :radio, radio_answer: nil, journey: journey, contentful_id: "NxJWpbiFeEAmvcw17EysX") + _answer = create(:radio_answer, step: step, response: "Red tractor", further_information: "Lots more detail") + + visit journey_path(journey) + + expect(page).to have_content(I18n.t("journey.specification.header")) + + within("article#specification") do + expect(page).to have_content("Red tractor - Lots more detail") + end + end +end diff --git a/spec/features/visitors/anyone_can_view_a_list_of_tasks_spec.rb b/spec/features/visitors/anyone_can_view_a_list_of_tasks_spec.rb new file mode 100644 index 000000000..908283b28 --- /dev/null +++ b/spec/features/visitors/anyone_can_view_a_list_of_tasks_spec.rb @@ -0,0 +1,13 @@ +require "rails_helper" + +feature "Users can view the task list" do + context "When a question has been answered" do + let(:answer) { create(:short_text_answer, response: "answer") } + + scenario "The task is marked as completed" do + visit journey_path(answer.step.journey) + + expect(page).to have_content(I18n.t("task_list.status.completed")) + end + end +end diff --git a/spec/fixtures/contentful/checkbox-example.json b/spec/fixtures/contentful/checkbox-example.json index d606d24f7..71408ac83 100644 --- a/spec/fixtures/contentful/checkbox-example.json +++ b/spec/fixtures/contentful/checkbox-example.json @@ -32,10 +32,10 @@ "slug": "/everyday-services", "type": "checkboxes", "title": "Everyday services that are required and need to be considered", - "options": [ - "breakfast", - "lunch", - "dinner" + "extendedOptions": [ + { "value": "breakfast" }, + { "value": "lunch" }, + { "value": "dinner" } ] } } diff --git a/spec/fixtures/contentful/closed-path-with-multiple-example.json b/spec/fixtures/contentful/closed-path-with-multiple-example.json index 23b0b4e99..c2ea024e6 100644 --- a/spec/fixtures/contentful/closed-path-with-multiple-example.json +++ b/spec/fixtures/contentful/closed-path-with-multiple-example.json @@ -15,7 +15,7 @@ "id": "rwl7tyzv9sys" } }, - "id": "5kZ9hIFDvNCEhjWs72SFwj", + "id": "contentful-starting-step", "type": "Entry", "createdAt": "2020-12-02T10:48:35.748Z", "updatedAt": "2020-12-02T10:48:35.748Z", @@ -45,7 +45,7 @@ "sys": { "type": "Link", "linkType": "Entry", - "id": "hfjJgWRg4xiiiImwVRDtZ" + "id": "contentful-radio-question" } } } @@ -59,7 +59,7 @@ "id": "rwl7tyzv9sys" } }, - "id": "hfjJgWRg4xiiiImwVRDtZ", + "id": "contentful-radio-question", "type": "Entry", "createdAt": "2020-11-04T12:28:30.442Z", "updatedAt": "2020-11-26T16:39:54.188Z", @@ -85,9 +85,200 @@ "title": "Which service do you need?", "helpText": "Tell us which service you need.", "type": "radios", - "options": [ - "Catering", - "Cleaning" + "extendedOptions": [ + { + "value": "Catering", + "display_further_information": true, + "further_information_help_text": "Do a thing" + }, + { + "value": "Cleaning" + } + ], + "next": { + "sys": { + "type": "Link", + "linkType": "Entry", + "id": "contentful-short-text-question" + } + } + } + }, + { + "sys": { + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "rwl7tyzv9sys" + } + }, + "id": "contentful-short-text-question", + "type": "Entry", + "createdAt": "2020-11-05T16:42:58.340Z", + "updatedAt": "2020-11-05T16:42:58.340Z", + "environment": { + "sys": { + "id": "master", + "type": "Link", + "linkType": "Environment" + } + }, + "revision": 1, + "contentType": { + "sys": { + "type": "Link", + "linkType": "ContentType", + "id": "question" + } + }, + "locale": "en-US" + }, + "fields": { + "slug": "/what-email-address-did-you-use", + "title": "What email address did you use?", + "helpText": "This will help us recover your existing plans", + "type": "short text", + "next": { + "sys": { + "type": "Link", + "linkType": "Entry", + "id": "contentful-long-text-question" + } + } + } + }, + { + "sys": { + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "rwl7tyzv9sys" + } + }, + "id": "contentful-long-text-question", + "type": "Entry", + "createdAt": "2020-11-11T11:26:20.994Z", + "updatedAt": "2020-11-11T11:26:20.994Z", + "environment": { + "sys": { + "id": "develop", + "type": "Link", + "linkType": "Environment" + } + }, + "revision": 1, + "contentType": { + "sys": { + "type": "Link", + "linkType": "ContentType", + "id": "question" + } + }, + "locale": "en-US" + }, + "fields": { + "slug": "/describe-what-you-need", + "title": "Describe what you need", + "helpText": "Briefly describe what you are looking to procure from your supplier.", + "type": "long text", + "next": { + "sys": { + "type": "Link", + "linkType": "Entry", + "id": "contentful-single-date-question" + } + } + } + }, + { + "sys": { + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "rwl7tyzv9sys" + } + }, + "id": "contentful-single-date-question", + "type": "Entry", + "createdAt": "2020-12-08T10:43:19.102Z", + "updatedAt": "2020-12-08T10:43:19.102Z", + "environment": { + "sys": { + "id": "develop", + "type": "Link", + "linkType": "Environment" + } + }, + "revision": 1, + "contentType": { + "sys": { + "type": "Link", + "linkType": "ContentType", + "id": "question" + } + }, + "locale": "en-US" + }, + "fields": { + "slug": "/starting-date", + "type": "single date", + "title": "When will this start?", + "next": { + "sys": { + "type": "Link", + "linkType": "Entry", + "id": "contentful-checkboxes-question" + } + } + } + }, + { + "sys": { + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "rwl7tyzv9sys" + } + }, + "id": "contentful-checkboxes-question", + "type": "Entry", + "createdAt": "2020-12-09T11:53:52.744Z", + "updatedAt": "2020-12-09T11:53:52.744Z", + "environment": { + "sys": { + "id": "develop", + "type": "Link", + "linkType": "Environment" + } + }, + "revision": 1, + "contentType": { + "sys": { + "type": "Link", + "linkType": "ContentType", + "id": "question" + } + }, + "locale": "en-US" + }, + "fields": { + "slug": "/everyday-services", + "type": "checkboxes", + "title": "Everyday services that are required and need to be considered", + "extendedOptions": [ + { + "value": "breakfast" + }, + { + "value": "lunch" + }, + { + "value": "dinner" + } ] } } diff --git a/spec/fixtures/contentful/extended-radio-question-example.json b/spec/fixtures/contentful/extended-radio-question-example.json new file mode 100644 index 000000000..fd02a3b48 --- /dev/null +++ b/spec/fixtures/contentful/extended-radio-question-example.json @@ -0,0 +1,51 @@ +{ + "sys": { + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "jspwts36h1os" + } + }, + "id": "contentful-starting-step", + "type": "Entry", + "createdAt": "2020-09-07T10:56:40.585Z", + "updatedAt": "2020-09-14T22:16:54.633Z", + "environment": { + "sys": { + "id": "master", + "type": "Link", + "linkType": "Environment" + } + }, + "revision": 7, + "contentType": { + "sys": { + "type": "Link", + "linkType": "ContentType", + "id": "question" + } + }, + "locale": "en-US" + }, + "fields": { + "slug": "/which-service", + "title": "Which service do you need?", + "helpText": "Tell us which service you need.", + "type": "radios", + "extendedOptions": [ + { + "value": "Catering", + "help_text": "", + "display_further_information": true, + "further_information_help_text": "Briefly describe why you need catering" + }, + { + "value": "Cleaning", + "help_text": "All interior areas of the school only", + "display_further_information": false, + "further_information_help_text": "Briefly describe why you need cleaning" + } + ] + } +} diff --git a/spec/fixtures/contentful/has-next-question-example.json b/spec/fixtures/contentful/has-next-question-example.json index f5993f55a..2e07763ab 100644 --- a/spec/fixtures/contentful/has-next-question-example.json +++ b/spec/fixtures/contentful/has-next-question-example.json @@ -33,9 +33,13 @@ "title": "Which service do you need?", "helpText": "Tell us which service you need.", "type": "radios", - "options": [ - "Catering", - "Cleaning" + "extendedOptions": [ + { + "value": "Catering" + }, + { + "value": "Cleaning" + } ], "next": { "sys": { diff --git a/spec/fixtures/contentful/long-text-question-example.json b/spec/fixtures/contentful/long-text-question-example.json deleted file mode 100644 index fde170404..000000000 --- a/spec/fixtures/contentful/long-text-question-example.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "rwl7tyzv9sys" - } - }, - "id": "2jWIO1MrVIya9NZrFWT4e", - "type": "Entry", - "createdAt": "2020-11-11T11:26:20.994Z", - "updatedAt": "2020-11-11T11:26:20.994Z", - "environment": { - "sys": { - "id": "develop", - "type": "Link", - "linkType": "Environment" - } - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "question" - } - }, - "locale": "en-US" - }, - "fields": { - "slug": "/describe-what-you-need", - "title": "Describe what you need", - "helpText": "Briefly describe what you are looking to procure from your supplier.", - "type": "long text" - } -} diff --git a/spec/fixtures/contentful/multiple-entries-example.json b/spec/fixtures/contentful/multiple-entries-example.json index ec1d49501..4ad585640 100644 --- a/spec/fixtures/contentful/multiple-entries-example.json +++ b/spec/fixtures/contentful/multiple-entries-example.json @@ -15,7 +15,7 @@ "id": "rwl7tyzv9sys" } }, - "id": "5kZ9hIFDvNCEhjWs72SFwj", + "id": "contentful-starting-step", "type": "Entry", "createdAt": "2020-12-02T10:48:35.748Z", "updatedAt": "2020-12-02T10:48:35.748Z", @@ -85,17 +85,14 @@ "title": "Which service do you need?", "helpText": "Tell us which service you need.", "type": "radios", - "options": [ - "Catering", - "Cleaning" - ], - "next": { - "sys": { - "type": "Link", - "linkType": "Entry", - "id": "52Ni9UFvZj8BYXSbhs373C" - } - } + "extendedOptions": [ + { + "value": "Catering" + }, + { + "value": "Cleaning" + } + ] } } ] diff --git a/spec/fixtures/contentful/no-next-question-example.json b/spec/fixtures/contentful/no-next-question-example.json index c90cba585..677a48d26 100644 --- a/spec/fixtures/contentful/no-next-question-example.json +++ b/spec/fixtures/contentful/no-next-question-example.json @@ -33,9 +33,13 @@ "title": "Which goods do you need?", "helpText": "Tell us which goods you need.", "type": "radios", - "options": [ - "Stationary", - "IT supplies" + "extendedOptions": [ + { + "value": "Stationary" + }, + { + "value": "IT supplies" + } ] } } diff --git a/spec/fixtures/contentful/no-primary-button-example.json b/spec/fixtures/contentful/no-primary-button-example.json index 07f995490..a46ec391f 100644 --- a/spec/fixtures/contentful/no-primary-button-example.json +++ b/spec/fixtures/contentful/no-primary-button-example.json @@ -7,7 +7,7 @@ "id": "rwl7tyzv9sys" } }, - "id": "5kZ9hIFDvNCEhjWs72SFwj", + "id": "contentful-starting-step", "type": "Entry", "createdAt": "2020-12-02T10:48:35.748Z", "updatedAt": "2020-12-07T17:04:21.854Z", diff --git a/spec/fixtures/contentful/path-with-unexpected-model.json b/spec/fixtures/contentful/path-with-unexpected-model.json new file mode 100644 index 000000000..4701e030f --- /dev/null +++ b/spec/fixtures/contentful/path-with-unexpected-model.json @@ -0,0 +1,45 @@ +{ + "sys": { + "type": "Array" + }, + "total": 2, + "skip": 0, + "limit": 100, + "items": [ + { + "sys": { + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "jspwts36h1os" + } + }, + "id": "contentful-unexpected-model", + "type": "Entry", + "createdAt": "2020-09-02T15:56:05.472Z", + "updatedAt": "2020-09-02T16:19:00.652Z", + "environment": { + "sys": { + "id": "master", + "type": "Link", + "linkType": "Environment" + } + }, + "revision": 2, + "contentType": { + "sys": { + "type": "Link", + "linkType": "ContentType", + "id": "unmanagedPage" + } + }, + "locale": "en-US" + }, + "fields": { + "slug": "/check-your-answers", + "type": "radios" + } + } + ] +} diff --git a/spec/fixtures/contentful/path-with-unexpected-step-type.json b/spec/fixtures/contentful/path-with-unexpected-step-type.json new file mode 100644 index 000000000..d1f353021 --- /dev/null +++ b/spec/fixtures/contentful/path-with-unexpected-step-type.json @@ -0,0 +1,45 @@ +{ + "sys": { + "type": "Array" + }, + "total": 2, + "skip": 0, + "limit": 100, + "items": [ + { + "sys": { + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "jspwts36h1os" + } + }, + "id": "contentful-unexpected-step-type", + "type": "Entry", + "createdAt": "2020-09-02T15:56:05.472Z", + "updatedAt": "2020-09-02T16:19:00.652Z", + "environment": { + "sys": { + "id": "master", + "type": "Link", + "linkType": "Environment" + } + }, + "revision": 2, + "contentType": { + "sys": { + "type": "Link", + "linkType": "ContentType", + "id": "question" + } + }, + "locale": "en-US" + }, + "fields": { + "slug": "/check-your-answers", + "type": "telepathy" + } + } + ] +} diff --git a/spec/fixtures/contentful/primary-button-example.json b/spec/fixtures/contentful/primary-button-example.json index 5e80952ab..20c09d601 100644 --- a/spec/fixtures/contentful/primary-button-example.json +++ b/spec/fixtures/contentful/primary-button-example.json @@ -7,7 +7,7 @@ "id": "rwl7tyzv9sys" } }, - "id": "5kZ9hIFDvNCEhjWs72SFwj", + "id": "contentful-starting-step", "type": "Entry", "createdAt": "2020-12-02T10:48:35.748Z", "updatedAt": "2020-12-07T17:04:21.854Z", diff --git a/spec/fixtures/contentful/radio-question-example.json b/spec/fixtures/contentful/radio-question-example.json index 75f56ca5e..ddf2ba23b 100644 --- a/spec/fixtures/contentful/radio-question-example.json +++ b/spec/fixtures/contentful/radio-question-example.json @@ -7,7 +7,7 @@ "id": "jspwts36h1os" } }, - "id": "1UjQurSOi5MWkcRuGxdXZS", + "id": "contentful-radio-question", "type": "Entry", "createdAt": "2020-09-07T10:56:40.585Z", "updatedAt": "2020-09-14T22:16:54.633Z", @@ -33,9 +33,13 @@ "title": "Which service do you need?", "helpText": "Tell us which service you need.", "type": "radios", - "options": [ - "Catering", - "Cleaning" + "extendedOptions": [ + { + "value": "Catering" + }, + { + "value": "Cleaning" + } ] } } diff --git a/spec/fixtures/contentful/repeat-entry-example.json b/spec/fixtures/contentful/repeat-entry-example.json index 72abdf553..00bdd448f 100644 --- a/spec/fixtures/contentful/repeat-entry-example.json +++ b/spec/fixtures/contentful/repeat-entry-example.json @@ -15,7 +15,7 @@ "id": "rwl7tyzv9sys" } }, - "id": "5kZ9hIFDvNCEhjWs72SFwj", + "id": "contentful-starting-step", "type": "Entry", "createdAt": "2020-12-02T10:48:35.748Z", "updatedAt": "2020-12-02T10:48:35.748Z", @@ -85,15 +85,19 @@ "title": "Which service do you need?", "helpText": "Tell us which service you need.", "type": "radios", - "options": [ - "Catering", - "Cleaning" + "extendedOptions": [ + { + "value": "Catering" + }, + { + "value": "Cleaning" + } ], "next": { "sys": { "type": "Link", "linkType": "Entry", - "id": "5kZ9hIFDvNCEhjWs72SFwj" + "id": "contentful-starting-step" } } } @@ -107,7 +111,7 @@ "id": "rwl7tyzv9sys" } }, - "id": "5kZ9hIFDvNCEhjWs72SFwj", + "id": "contentful-starting-step", "type": "Entry", "createdAt": "2020-12-02T10:48:35.748Z", "updatedAt": "2020-12-02T10:48:35.748Z", diff --git a/spec/fixtures/contentful/single-date-example.json b/spec/fixtures/contentful/single-date-example.json deleted file mode 100644 index 752133c8f..000000000 --- a/spec/fixtures/contentful/single-date-example.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "rwl7tyzv9sys" - } - }, - "id": "55G5kpCLLL3h5yBQLiVlYy", - "type": "Entry", - "createdAt": "2020-12-08T10:43:19.102Z", - "updatedAt": "2020-12-08T10:43:19.102Z", - "environment": { - "sys": { - "id": "develop", - "type": "Link", - "linkType": "Environment" - } - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "question" - } - }, - "locale": "en-US" - }, - "fields": { - "slug": "/starting-date", - "type": "single date", - "title": "When will this start?" - } -} diff --git a/spec/fixtures/contentful/static-content-example.json b/spec/fixtures/contentful/static-content-example.json index cbd6d0c53..a09b1e972 100644 --- a/spec/fixtures/contentful/static-content-example.json +++ b/spec/fixtures/contentful/static-content-example.json @@ -7,7 +7,7 @@ "id": "rwl7tyzv9sys" } }, - "id": "5kZ9hIFDvNCEhjWs72SFwj", + "id": "contentful-starting-step", "type": "Entry", "createdAt": "2020-12-02T10:48:35.748Z", "updatedAt": "2020-12-02T10:48:35.748Z", diff --git a/spec/fixtures/specification_templates/basic_catering.liquid b/spec/fixtures/specification_templates/basic_catering.liquid new file mode 100644 index 000000000..b360aea88 --- /dev/null +++ b/spec/fixtures/specification_templates/basic_catering.liquid @@ -0,0 +1,7 @@ +
+ {% if answer_contentful-starting-step %} +
+

I'm the first article and should be seen

+
+ {% endif %} +
diff --git a/spec/fixtures/specification_templates/food_catering.liquid b/spec/fixtures/specification_templates/food_catering.liquid new file mode 100644 index 000000000..9b95931af --- /dev/null +++ b/spec/fixtures/specification_templates/food_catering.liquid @@ -0,0 +1,33 @@ +
+
+

Menus and ordering

+

Food standards

+
    +
  1. +

    It will be the suppliers responsibility to ensure that all food served within the school day complies with both current and future government legislation and guidelines on the provision of healthy school meals.

    +

    The supplier should encourage the use of seasonal produce and promotion of healthy eating to pupils wherever practical and desirable.

    +

    It will be the suppliers responsibility to comply fully with DfE food and nutrient based standards, and to promote and comply with this policy throughout the contract term through effective menu planning.

    +
  2. + {% if answer_NxJWpbiFeEAmvcw17EysX %} +
  3. +

    The school also requires the service to comply with the following non-mandatory food standards or schemes:

    +

    {{answer_NxJWpbiFeEAmvcw17EysX}}

    +
  4. + {% endif %} +
  5. +

    The supplier must work with the school to provide safe and enjoyable meals for any pupils with allergies or intolerances, ensuring that the ingredients, preparation and handling of food for those children are completely allergen-free.

    +
  6. + {% if answer_5xxbqrasSXH9x9Lt3YhRkX contains "yes" %} +
  7. +

    The supplier is required to track allergen information through their supply chain and must be able to demonstrate their allergen tracking plan.

    +
  8. + {% endif %} + {% if answer_4HfGoYL6s806Azx93jp5IY %} +
  9. +

    The school has the following objectives around the quantity, quality or variety of food offered, beyond the mandatory standards:

    +

    {{answer_4HfGoYL6s806Azx93jp5IY}}

    +
  10. + {% endif %} + +
+
diff --git a/spec/fixtures/specification_templates/invalid.liquid b/spec/fixtures/specification_templates/invalid.liquid new file mode 100644 index 000000000..c5249bdb1 --- /dev/null +++ b/spec/fixtures/specification_templates/invalid.liquid @@ -0,0 +1,5 @@ +
+ {{% if "123" %}} + { invalid interpolation syntax } + {% endiffffff %} +
diff --git a/spec/helpers/step_helper_spec.rb b/spec/helpers/step_helper_spec.rb index aa5172f53..0603279db 100644 --- a/spec/helpers/step_helper_spec.rb +++ b/spec/helpers/step_helper_spec.rb @@ -1,17 +1,6 @@ require "rails_helper" RSpec.describe StepHelper, type: :helper do - describe "#radio_options" do - it "returns an array of OpenStruct objects" do - options = ["green", "yellow"] - result = helper.radio_options(array_of_options: options) - expect(result).to match([ - OpenStruct.new(id: "green", name: "Green"), - OpenStruct.new(id: "yellow", name: "Yellow") - ]) - end - end - describe "#checkbox_options" do it "returns an array of OpenStruct objects" do options = ["red", "blue"] diff --git a/spec/jobs/warm_entry_cache_job_spec.rb b/spec/jobs/warm_entry_cache_job_spec.rb index 7de1f74b3..158dbbeb2 100644 --- a/spec/jobs/warm_entry_cache_job_spec.rb +++ b/spec/jobs/warm_entry_cache_job_spec.rb @@ -8,7 +8,7 @@ around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "5kZ9hIFDvNCEhjWs72SFwj", + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step", CONTENTFUL_ENTRY_CACHING: "true" ) do example.run @@ -24,20 +24,20 @@ it "asks GetAllContentfulEntries for the Contentful entries" do stub_get_contentful_entries( - entry_id: "5kZ9hIFDvNCEhjWs72SFwj", + entry_id: "contentful-starting-step", fixture_filename: "multiple-entries-example.json" ) described_class.perform_later perform_enqueued_jobs - expect(RedisCache.redis.get("contentful:entry:5kZ9hIFDvNCEhjWs72SFwj")) + expect(RedisCache.redis.get("contentful:entry:contentful-starting-step")) .to eql( - "\"{\\\"sys\\\":{\\\"space\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Space\\\",\\\"id\\\":\\\"rwl7tyzv9sys\\\"}},\\\"id\\\":\\\"5kZ9hIFDvNCEhjWs72SFwj\\\",\\\"type\\\":\\\"Entry\\\",\\\"createdAt\\\":\\\"2020-12-02T10:48:35.748Z\\\",\\\"updatedAt\\\":\\\"2020-12-02T10:48:35.748Z\\\",\\\"environment\\\":{\\\"sys\\\":{\\\"id\\\":\\\"develop\\\",\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Environment\\\"}},\\\"revision\\\":1,\\\"contentType\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"ContentType\\\",\\\"id\\\":\\\"staticContent\\\"}},\\\"locale\\\":\\\"en-US\\\"},\\\"fields\\\":{\\\"slug\\\":\\\"/timelines\\\",\\\"title\\\":\\\"When you should start\\\",\\\"body\\\":\\\"Procuring a new catering contract can take up to 6 months to consult, create, review and award. \\\\n\\\\nUsually existing contracts start and end in the month of September. We recommend starting this process around March.\\\",\\\"type\\\":\\\"paragraphs\\\",\\\"next\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Entry\\\",\\\"id\\\":\\\"hfjJgWRg4xiiiImwVRDtZ\\\"}}}}\"" + "\"{\\\"sys\\\":{\\\"space\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Space\\\",\\\"id\\\":\\\"rwl7tyzv9sys\\\"}},\\\"id\\\":\\\"contentful-starting-step\\\",\\\"type\\\":\\\"Entry\\\",\\\"createdAt\\\":\\\"2020-12-02T10:48:35.748Z\\\",\\\"updatedAt\\\":\\\"2020-12-02T10:48:35.748Z\\\",\\\"environment\\\":{\\\"sys\\\":{\\\"id\\\":\\\"develop\\\",\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Environment\\\"}},\\\"revision\\\":1,\\\"contentType\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"ContentType\\\",\\\"id\\\":\\\"staticContent\\\"}},\\\"locale\\\":\\\"en-US\\\"},\\\"fields\\\":{\\\"slug\\\":\\\"/timelines\\\",\\\"title\\\":\\\"When you should start\\\",\\\"body\\\":\\\"Procuring a new catering contract can take up to 6 months to consult, create, review and award. \\\\n\\\\nUsually existing contracts start and end in the month of September. We recommend starting this process around March.\\\",\\\"type\\\":\\\"paragraphs\\\",\\\"next\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Entry\\\",\\\"id\\\":\\\"hfjJgWRg4xiiiImwVRDtZ\\\"}}}}\"" ) expect(RedisCache.redis.get("contentful:entry:hfjJgWRg4xiiiImwVRDtZ")) .to eql( - "\"{\\\"sys\\\":{\\\"space\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Space\\\",\\\"id\\\":\\\"rwl7tyzv9sys\\\"}},\\\"id\\\":\\\"hfjJgWRg4xiiiImwVRDtZ\\\",\\\"type\\\":\\\"Entry\\\",\\\"createdAt\\\":\\\"2020-11-04T12:28:30.442Z\\\",\\\"updatedAt\\\":\\\"2020-11-26T16:39:54.188Z\\\",\\\"environment\\\":{\\\"sys\\\":{\\\"id\\\":\\\"develop\\\",\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Environment\\\"}},\\\"revision\\\":6,\\\"contentType\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"ContentType\\\",\\\"id\\\":\\\"question\\\"}},\\\"locale\\\":\\\"en-US\\\"},\\\"fields\\\":{\\\"slug\\\":\\\"/dev-start-which-service\\\",\\\"title\\\":\\\"Which service do you need?\\\",\\\"helpText\\\":\\\"Tell us which service you need.\\\",\\\"type\\\":\\\"radios\\\",\\\"options\\\":[\\\"Catering\\\",\\\"Cleaning\\\"],\\\"next\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Entry\\\",\\\"id\\\":\\\"52Ni9UFvZj8BYXSbhs373C\\\"}}}}\"" + "\"{\\\"sys\\\":{\\\"space\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Space\\\",\\\"id\\\":\\\"rwl7tyzv9sys\\\"}},\\\"id\\\":\\\"hfjJgWRg4xiiiImwVRDtZ\\\",\\\"type\\\":\\\"Entry\\\",\\\"createdAt\\\":\\\"2020-11-04T12:28:30.442Z\\\",\\\"updatedAt\\\":\\\"2020-11-26T16:39:54.188Z\\\",\\\"environment\\\":{\\\"sys\\\":{\\\"id\\\":\\\"develop\\\",\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Environment\\\"}},\\\"revision\\\":6,\\\"contentType\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"ContentType\\\",\\\"id\\\":\\\"question\\\"}},\\\"locale\\\":\\\"en-US\\\"},\\\"fields\\\":{\\\"slug\\\":\\\"/dev-start-which-service\\\",\\\"title\\\":\\\"Which service do you need?\\\",\\\"helpText\\\":\\\"Tell us which service you need.\\\",\\\"type\\\":\\\"radios\\\",\\\"extendedOptions\\\":[{\\\"value\\\":\\\"Catering\\\"},{\\\"value\\\":\\\"Cleaning\\\"}]}}\"" ) end @@ -48,25 +48,25 @@ .and_raise(BuildJourneyOrder::RepeatEntryDetected) stub_get_contentful_entries( - entry_id: "5kZ9hIFDvNCEhjWs72SFwj", + entry_id: "contentful-starting-step", fixture_filename: "repeat-entry-example.json" ) described_class.perform_later perform_enqueued_jobs - expect(RedisCache.redis).not_to receive(:set).with("contentful:entry:5kZ9hIFDvNCEhjWs72SFwj", anything) + expect(RedisCache.redis).not_to receive(:set).with("contentful:entry:contentful-starting-step", anything) end it "extends the TTL on all existing items by 24 hours" do - RedisCache.redis.set("contentful:entry:5kZ9hIFDvNCEhjWs72SFwj", "\"{\\}\"") + RedisCache.redis.set("contentful:entry:contentful-starting-step", "\"{\\}\"") allow_any_instance_of(BuildJourneyOrder) .to receive(:call) .and_raise(BuildJourneyOrder::RepeatEntryDetected) stub_get_contentful_entries( - entry_id: "5kZ9hIFDvNCEhjWs72SFwj", + entry_id: "contentful-starting-step", fixture_filename: "repeat-entry-example.json" ) @@ -76,7 +76,7 @@ expect(RedisCache.redis) .to receive(:expire) - .with("contentful:entry:5kZ9hIFDvNCEhjWs72SFwj", ttl_extension + existing_ttl) + .with("contentful:entry:contentful-starting-step", ttl_extension + existing_ttl) described_class.perform_later perform_enqueued_jobs diff --git a/spec/models/journey_spec.rb b/spec/models/journey_spec.rb index ac0fbf5de..1d9cb3d78 100644 --- a/spec/models/journey_spec.rb +++ b/spec/models/journey_spec.rb @@ -3,6 +3,10 @@ RSpec.describe Journey, type: :model do it { should have_many(:steps) } + describe "validations" do + it { is_expected.to validate_presence_of(:liquid_template) } + end + it "captures the category" do journey = build(:journey, :catering) expect(journey.category).to eql("catering") diff --git a/spec/models/step_spec.rb b/spec/models/step_spec.rb index 54c8a1956..77a164794 100644 --- a/spec/models/step_spec.rb +++ b/spec/models/step_spec.rb @@ -13,11 +13,11 @@ :radio, title: "foo", help_text: "bar", - options: ["baz", "boo"]) + options: [{"value" => "baz"}, {"value" => "boo"}]) expect(step.title).to eql("foo") expect(step.help_text).to eql("bar") - expect(step.options).to eql(["baz", "boo"]) + expect(step.options).to eql([{"value" => "baz"}, {"value" => "boo"}]) end it "captures the raw contentful response" do @@ -25,6 +25,16 @@ expect(step.raw).to eql({"foo" => "bar"}) end + describe "#that_are_questions" do + it "only returns steps that have the question contentful_model" do + question_step = create(:step, :radio) + static_content_step = create(:step, :static_content) + + expect(described_class.that_are_questions).to include(question_step) + expect(described_class.that_are_questions).not_to include(static_content_step) + end + end + describe "#answer" do context "when a RadioAnswer is associated to the step" do it "returns the RadioAnswer object" do @@ -66,4 +76,15 @@ end end end + + describe "#options" do + # TODO: This will need updating when options are set on the step with the new format + it "returns a hash of options" do + step = build(:step, + :radio, + options: [{"value" => "foo", "other_config" => false}]) + + expect(step.options).to eql([{"value" => "foo", "other_config" => false}]) + end + end end diff --git a/spec/presenters/checkboxes_answer_presenter_spec.rb b/spec/presenters/checkboxes_answer_presenter_spec.rb new file mode 100644 index 000000000..13ab392c1 --- /dev/null +++ b/spec/presenters/checkboxes_answer_presenter_spec.rb @@ -0,0 +1,11 @@ +require "rails_helper" + +RSpec.describe CheckboxesAnswerPresenter do + describe "#response" do + it "returns all none nil values, capitalised as a comma separated string" do + step = build(:checkbox_answers, response: ["yes", "no", ""]) + presenter = described_class.new(step) + expect(presenter.response).to eq("Yes, No") + end + end +end diff --git a/spec/presenters/long_text_answer_presenter_spec.rb b/spec/presenters/long_text_answer_presenter_spec.rb new file mode 100644 index 000000000..dce4bfa99 --- /dev/null +++ b/spec/presenters/long_text_answer_presenter_spec.rb @@ -0,0 +1,19 @@ +require "rails_helper" + +RSpec.describe LongTextAnswerPresenter do + describe "#response" do + it "returns the response in HTML using the Rails simple_format method" do + step = build(:long_text_answer, response: "Lots of text") + presenter = described_class.new(step) + expect(presenter.response).to eq("

Lots of text

") + end + + context "when the text includes line breaks" do + it "returns 2 HTML paragraphs" do + step = build(:long_text_answer, response: "First line\r\nSecond line") + presenter = described_class.new(step) + expect(presenter.response).to eq("

First line\n
Second line

") + end + end + end +end diff --git a/spec/presenters/radio_answer_presenter_spec.rb b/spec/presenters/radio_answer_presenter_spec.rb new file mode 100644 index 000000000..edb83426d --- /dev/null +++ b/spec/presenters/radio_answer_presenter_spec.rb @@ -0,0 +1,19 @@ +require "rails_helper" + +RSpec.describe RadioAnswerPresenter do + describe "#response" do + it "returns the option chosen" do + step = build(:radio_answer, response: "Yes", further_information: "") + presenter = described_class.new(step) + expect(presenter.response).to eq("Yes") + end + + context "when further information is provided" do + it "returns the option chosen and the further information" do + step = build(:radio_answer, response: "Yes", further_information: "This is really important") + presenter = described_class.new(step) + expect(presenter.response).to eq("Yes - This is really important") + end + end + end +end diff --git a/spec/presenters/short_text_answer_presenter_spec.rb b/spec/presenters/short_text_answer_presenter_spec.rb new file mode 100644 index 000000000..69c223f25 --- /dev/null +++ b/spec/presenters/short_text_answer_presenter_spec.rb @@ -0,0 +1,11 @@ +require "rails_helper" + +RSpec.describe ShortTextAnswerPresenter do + describe "#response" do + it "returns the response" do + step = build(:short_text_answer, response: "A little of text") + presenter = described_class.new(step) + expect(presenter.response).to eq("A little of text") + end + end +end diff --git a/spec/presenters/single_date_answer_presenter_spec.rb b/spec/presenters/single_date_answer_presenter_spec.rb new file mode 100644 index 000000000..5e0771fb3 --- /dev/null +++ b/spec/presenters/single_date_answer_presenter_spec.rb @@ -0,0 +1,11 @@ +require "rails_helper" + +RSpec.describe SingleDateAnswerPresenter do + describe "#response" do + it "returns the response formatted as a date" do + step = build(:single_date_answer, response: Date.new(2000, 12, 30)) + presenter = described_class.new(step) + expect(presenter.response).to eq("30 Dec 2000") + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index a57abfa97..f4b3d5b75 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -34,6 +34,7 @@ end RSpec.configure do |config| config.include ContentfulHelpers + config.include LiquidHelpers config.include ActiveSupport::Testing::TimeHelpers # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures diff --git a/spec/requests/contentful_caching_spec.rb b/spec/requests/contentful_caching_spec.rb index ef2843584..c96a38676 100644 --- a/spec/requests/contentful_caching_spec.rb +++ b/spec/requests/contentful_caching_spec.rb @@ -10,10 +10,10 @@ end it "checks the Redis cache instead of making an external request" do - journey = create(:journey, next_entry_id: "1UjQurSOi5MWkcRuGxdXZS") + journey = create(:journey, next_entry_id: "contentful-radio-question") raw_response = File.read("#{Rails.root}/spec/fixtures/contentful/radio-question-example.json") - RedisCache.redis.set("contentful:entry:1UjQurSOi5MWkcRuGxdXZS", JSON.dump(raw_response)) + RedisCache.redis.set("contentful:entry:contentful-radio-question", JSON.dump(raw_response)) expect_any_instance_of(Contentful::Client).not_to receive(:entry) @@ -21,40 +21,39 @@ expect(response).to have_http_status(:found) - RedisCache.redis.del("contentful:entry:1UjQurSOi5MWkcRuGxdXZS") + RedisCache.redis.del("contentful:entry:contentful-radio-question") end it "stores the external contentful response in the cache" do - journey = create(:journey, next_entry_id: "1UjQurSOi5MWkcRuGxdXZS") - raw_response = File.read("#{Rails.root}/spec/fixtures/contentful/radio-question-example.json") + journey = create(:journey, next_entry_id: "contentful-radio-question") stub_get_contentful_entry( - entry_id: "1UjQurSOi5MWkcRuGxdXZS", + entry_id: "contentful-radio-question", fixture_filename: "radio-question-example.json" ) get new_journey_step_path(journey) - expect(RedisCache.redis.get("contentful:entry:1UjQurSOi5MWkcRuGxdXZS")) - .to eq(JSON.dump(raw_response.to_json)) + expect(RedisCache.redis.get("contentful:entry:contentful-radio-question")) + .to eq("\"{\\\"sys\\\":{\\\"space\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Space\\\",\\\"id\\\":\\\"jspwts36h1os\\\"}},\\\"id\\\":\\\"contentful-radio-question\\\",\\\"type\\\":\\\"Entry\\\",\\\"createdAt\\\":\\\"2020-09-07T10:56:40.585Z\\\",\\\"updatedAt\\\":\\\"2020-09-14T22:16:54.633Z\\\",\\\"environment\\\":{\\\"sys\\\":{\\\"id\\\":\\\"master\\\",\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"Environment\\\"}},\\\"revision\\\":7,\\\"contentType\\\":{\\\"sys\\\":{\\\"type\\\":\\\"Link\\\",\\\"linkType\\\":\\\"ContentType\\\",\\\"id\\\":\\\"question\\\"}},\\\"locale\\\":\\\"en-US\\\"},\\\"fields\\\":{\\\"slug\\\":\\\"/which-service\\\",\\\"title\\\":\\\"Which service do you need?\\\",\\\"helpText\\\":\\\"Tell us which service you need.\\\",\\\"type\\\":\\\"radios\\\",\\\"extendedOptions\\\":[{\\\"value\\\":\\\"Catering\\\"},{\\\"value\\\":\\\"Cleaning\\\"}]}}\"") - RedisCache.redis.del("contentful:entry:1UjQurSOi5MWkcRuGxdXZS") + RedisCache.redis.del("contentful:entry:contentful-radio-question") end it "sets a TTL to 72 hours by default" do - journey = create(:journey, next_entry_id: "1UjQurSOi5MWkcRuGxdXZS") + journey = create(:journey, next_entry_id: "contentful-radio-question") stub_get_contentful_entry( - entry_id: "1UjQurSOi5MWkcRuGxdXZS", + entry_id: "contentful-radio-question", fixture_filename: "radio-question-example.json" ) freeze_time do get new_journey_step_path(journey) - expect(RedisCache.redis.ttl("contentful:entry:1UjQurSOi5MWkcRuGxdXZS")) + expect(RedisCache.redis.ttl("contentful:entry:contentful-radio-question")) .to eq(60 * 60 * 72) end - RedisCache.redis.del("contentful:entry:1UjQurSOi5MWkcRuGxdXZS") + RedisCache.redis.del("contentful:entry:contentful-radio-question") end context "when caching has been disabled in ENV" do @@ -67,9 +66,9 @@ end it "does not interact with the redis cache" do - journey = create(:journey, next_entry_id: "1UjQurSOi5MWkcRuGxdXZS") + journey = create(:journey, next_entry_id: "contentful-radio-question") stub_get_contentful_entry( - entry_id: "1UjQurSOi5MWkcRuGxdXZS", + entry_id: "contentful-radio-question", fixture_filename: "radio-question-example.json" ) diff --git a/spec/requests/entry_preview_spec.rb b/spec/requests/entry_preview_spec.rb index 2054796bf..916e3b7cd 100644 --- a/spec/requests/entry_preview_spec.rb +++ b/spec/requests/entry_preview_spec.rb @@ -5,12 +5,20 @@ entry_id = "123" fake_journey = create(:journey) expect(Journey).to receive(:create) - .with(category: anything, next_entry_id: entry_id) + .with(category: anything) .and_return(fake_journey) + fake_get_contentful_entry = instance_double(Contentful::Entry) + allow_any_instance_of(GetContentfulEntry).to receive(:call) + .and_return(fake_get_contentful_entry) + + fake_step = create(:step, :radio) + allow_any_instance_of(CreateJourneyStep).to receive(:call) + .and_return(fake_step) + get "/preview/entries/#{entry_id}" expect(response).to have_http_status(:found) - expect(response).to redirect_to("/journeys/#{fake_journey.id}/steps/new") + expect(response).to redirect_to("/journeys/#{fake_journey.id}/steps/#{fake_step.id}") end end diff --git a/spec/services/build_journey_order_spec.rb b/spec/services/build_journey_order_spec.rb index f4ce4ea2a..e00600fb3 100644 --- a/spec/services/build_journey_order_spec.rb +++ b/spec/services/build_journey_order_spec.rb @@ -4,31 +4,47 @@ describe "#call" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "5kZ9hIFDvNCEhjWs72SFwj" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end end - context "when there are multiple entries" do - it "returns each ID in the order they appear" do + context "when the journey includes a node which doesn't exist" do + around do |example| + ClimateControl.modify( + CONTENTFUL_PLANNING_START_ENTRY_ID: "fake-id" + ) do + example.run + end + end + + it "raises a rollbar event" do fake_entries = fake_contentful_entry_array( - contentful_fixture_filename: "closed-path-with-multiple-example.json" + contentful_fixture_filename: "repeat-entry-example.json" ) - result = described_class.new( - entries: fake_entries, - starting_entry_id: "5kZ9hIFDvNCEhjWs72SFwj" - ).call + expect(Rollbar).to receive(:error) + .with("A specified Contentful entry was not found", + contentful_url: ENV["CONTENTFUL_URL"], + contentful_space_id: ENV["CONTENTFUL_SPACE"], + contentful_environment: ENV["CONTENTFUL_ENVIRONMENT"], + contentful_entry_id: "fake-id") + .and_call_original - expect(result).to match([fake_entries.first, fake_entries.last]) + expect { + described_class.new( + entries: fake_entries, + starting_entry_id: "fake-id" + ).call + }.to raise_error(BuildJourneyOrder::MissingEntryDetected) end end context "when the journey visits the same node twice" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "5kZ9hIFDvNCEhjWs72SFwj" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end @@ -44,13 +60,13 @@ contentful_url: ENV["CONTENTFUL_URL"], contentful_space_id: ENV["CONTENTFUL_SPACE"], contentful_environment: ENV["CONTENTFUL_ENVIRONMENT"], - contentful_entry_id: "5kZ9hIFDvNCEhjWs72SFwj") + contentful_entry_id: "contentful-starting-step") .and_call_original expect { described_class.new( entries: fake_entries, - starting_entry_id: "5kZ9hIFDvNCEhjWs72SFwj" + starting_entry_id: "contentful-starting-step" ).call }.to raise_error(BuildJourneyOrder::RepeatEntryDetected) end @@ -59,7 +75,7 @@ context "when the journey visits more than the maximum permitted number of entries" do around do |example| ClimateControl.modify( - CONTENTFUL_PLANNING_START_ENTRY_ID: "5kZ9hIFDvNCEhjWs72SFwj" + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" ) do example.run end @@ -79,13 +95,13 @@ contentful_url: ENV["CONTENTFUL_URL"], contentful_space_id: ENV["CONTENTFUL_SPACE"], contentful_environment: ENV["CONTENTFUL_ENVIRONMENT"], - contentful_entry_id: "hfjJgWRg4xiiiImwVRDtZ") + contentful_entry_id: "contentful-radio-question") .and_call_original expect { described_class.new( entries: fake_entries, - starting_entry_id: "5kZ9hIFDvNCEhjWs72SFwj" + starting_entry_id: "contentful-starting-step" ).call }.to raise_error(BuildJourneyOrder::TooManyChainedEntriesDetected) end diff --git a/spec/services/create_journey_spec.rb b/spec/services/create_journey_spec.rb new file mode 100644 index 000000000..7e1259b02 --- /dev/null +++ b/spec/services/create_journey_spec.rb @@ -0,0 +1,40 @@ +require "rails_helper" + +RSpec.describe CreateJourney do + around do |example| + ClimateControl.modify( + CONTENTFUL_PLANNING_START_ENTRY_ID: "contentful-starting-step" + ) do + example.run + end + end + + describe "#call" do + it "creates a new journey" do + stub_get_contentful_entries( + entry_id: "contentful-starting-step", + fixture_filename: "closed-path-with-multiple-example.json" + ) + expect { described_class.new(category: "catering").call } + .to change { Journey.count }.by(1) + expect(Journey.last.category).to eql("catering") + end + + it "stores a copy of the Liquid template" do + stub_get_contentful_entries( + entry_id: "contentful-starting-step", + fixture_filename: "closed-path-with-multiple-example.json" + ) + fake_liquid_template = File.read("#{Rails.root}/spec/fixtures/specification_templates/basic_catering.liquid") + finder = instance_double(FindLiquidTemplate) + allow(FindLiquidTemplate).to receive(:new).with(category: "catering") + .and_return(finder) + allow(finder).to receive(:call).and_return(fake_liquid_template) + + described_class.new(category: "catering").call + + expect(Journey.last.liquid_template) + .to eql("
\n {% if answer_contentful-starting-step %}\n
\n

I'm the first article and should be seen

\n
\n {% endif %}\n
\n") + end + end +end diff --git a/spec/services/create_journey_step_spec.rb b/spec/services/create_journey_step_spec.rb index 4d08d6028..f6198446e 100644 --- a/spec/services/create_journey_step_spec.rb +++ b/spec/services/create_journey_step_spec.rb @@ -13,14 +13,14 @@ expect(step.title).to eq("Which service do you need?") expect(step.help_text).to eq("Tell us which service you need.") - expect(step.contentful_id).to eq("1UjQurSOi5MWkcRuGxdXZS") + expect(step.contentful_id).to eq("contentful-radio-question") expect(step.contentful_model).to eq("question") expect(step.contentful_type).to eq("radios") - expect(step.options).to eq(["Catering", "Cleaning"]) + expect(step.options).to eq([{"value" => "Catering"}, {"value" => "Cleaning"}]) expect(step.raw).to eq( "fields" => { "helpText" => "Tell us which service you need.", - "options" => ["Catering", "Cleaning"], + "extendedOptions" => [{"value" => "Catering"}, {"value" => "Cleaning"}], "slug" => "/which-service", "title" => "Which service do you need?", "type" => "radios" @@ -41,7 +41,7 @@ "type" => "Link" } }, - "id" => "1UjQurSOi5MWkcRuGxdXZS", + "id" => "contentful-radio-question", "locale" => "en-US", "revision" => 7, "space" => { diff --git a/spec/services/find_liquid_template_spec.rb b/spec/services/find_liquid_template_spec.rb new file mode 100644 index 000000000..135ddbb6a --- /dev/null +++ b/spec/services/find_liquid_template_spec.rb @@ -0,0 +1,56 @@ +require "rails_helper" + +RSpec.describe FindLiquidTemplate do + describe "#call" do + context "when the Liquid contents are invalid (using the gems own parser)" do + it "raises an error" do + fake_liquid_template = File.read("#{Rails.root}/spec/fixtures/specification_templates/invalid.liquid") + + finder = described_class.new(category: "catering") + allow(finder).to receive(:file).and_return(fake_liquid_template) + + expect { finder.call }.to raise_error(FindLiquidTemplate::InvalidLiquidSyntax) + end + + it "sends an error to rollbar" do + fake_liquid_template = File.read("#{Rails.root}/spec/fixtures/specification_templates/invalid.liquid") + + finder = described_class.new(category: "catering") + allow(finder).to receive(:file).and_return(fake_liquid_template) + + expect(Rollbar).to receive(:error) + .with("A user couldn't start a journey because of an invalid Specification", + contentful_url: ENV["CONTENTFUL_URL"], + contentful_space_id: ENV["CONTENTFUL_SPACE"], + contentful_environment: ENV["CONTENTFUL_ENVIRONMENT"], + category: "catering").and_call_original + + expect { finder.call }.to raise_error(FindLiquidTemplate::InvalidLiquidSyntax) + end + end + + context "when in development" do + it "loads the development liquid template" do + category = "catering" + expect(File).to receive(:read).with("lib/specification_templates/#{category}.development.liquid").at_least(:once) + described_class.new(category: category, environment: "development").call + end + end + + context "when in staging" do + it "loads the staging liquid template" do + category = "catering" + expect(File).to receive(:read).with("lib/specification_templates/#{category}.staging.liquid").at_least(:once) + described_class.new(category: category, environment: "staging").call + end + end + + context "when in production" do + it "loads the production liquid template" do + category = "catering" + expect(File).to receive(:read).with("lib/specification_templates/#{category}.production.liquid").at_least(:once) + described_class.new(category: category, environment: "production").call + end + end + end +end diff --git a/spec/support/contentful_helpers.rb b/spec/support/contentful_helpers.rb index 7d0389c6b..d9bb11f38 100644 --- a/spec/support/contentful_helpers.rb +++ b/spec/support/contentful_helpers.rb @@ -1,22 +1,17 @@ module ContentfulHelpers def stub_get_contentful_entry( - entry_id: "1UjQurSOi5MWkcRuGxdXZS", + entry_id: "contentful-radio-question", fixture_filename: "radio-question-example.json" ) - raw_response = File.read("#{Rails.root}/spec/fixtures/contentful/#{fixture_filename}") - contentful_connector = stub_contentful_connector contentful_response = fake_contentful_entry(contentful_fixture_filename: fixture_filename) allow(contentful_connector).to receive(:get_entry_by_id) .with(entry_id) .and_return(contentful_response) - - allow(contentful_response).to receive(:raw) - .and_return(raw_response) end def stub_get_contentful_entries( - entry_id: "5kZ9hIFDvNCEhjWs72SFwj", + entry_id: "contentful-starting-step", fixture_filename: "multiple-entries-example.json" ) raw_response = File.read("#{Rails.root}/spec/fixtures/contentful/#{fixture_filename}") @@ -55,7 +50,7 @@ def fake_contentful_entry(contentful_fixture_filename:) title: hash_response.dig("fields", "title"), help_text: hash_response.dig("fields", "helpText"), body: hash_response.dig("fields", "body"), - options: hash_response.dig("fields", "options"), + extended_options: hash_response.dig("fields", "extendedOptions"), type: hash_response.dig("fields", "type"), next: double(id: hash_response.dig("fields", "next", "sys", "id")), primary_call_to_action: hash_response.dig("fields", "primaryCallToAction"), diff --git a/spec/support/liquid_helpers.rb b/spec/support/liquid_helpers.rb new file mode 100644 index 000000000..157c1fd33 --- /dev/null +++ b/spec/support/liquid_helpers.rb @@ -0,0 +1,12 @@ +module LiquidHelpers + def stub_liquid_template(filename: "basic_catering.liquid") + fake_liquid_template = File.read("#{Rails.root}/spec/fixtures/specification_templates/#{filename}") + + finder = instance_double(FindLiquidTemplate) + allow(FindLiquidTemplate).to receive(:new).with(category: "catering") + .and_return(finder) + allow(finder).to receive(:call).and_return(fake_liquid_template) + + fake_liquid_template + end +end