diff --git a/app/controllers/case_logs_controller.rb b/app/controllers/case_logs_controller.rb index c2b4edc28..6abd5371a 100644 --- a/app/controllers/case_logs_controller.rb +++ b/app/controllers/case_logs_controller.rb @@ -58,13 +58,13 @@ class CaseLogsController < ApplicationController form = FormHandler.instance.get_form("2021_2022") @case_log = CaseLog.find(params[:id]) @case_log.page = params[:case_log][:page] - responses_for_page = responses_for_page(@case_log.page) + page = form.get_page(@case_log.page) + responses_for_page = responses_for_page(page) if @case_log.update(responses_for_page) && @case_log.has_no_unresolved_soft_errors? - redirect_path = form.next_page_redirect_path(@case_log.page, @case_log) + redirect_path = form.next_page_redirect_path(page, @case_log) redirect_to(send(redirect_path, @case_log)) else - page_info = form.all_pages[@case_log.page] - render "form/page", locals: { form: form, page_key: @case_log.page, page_info: page_info }, status: :unprocessable_entity + render "form/page", locals: { form: form, page: page }, status: :unprocessable_entity end end @@ -84,15 +84,15 @@ class CaseLogsController < ApplicationController form = FormHandler.instance.get_form("2021_2022") @case_log = CaseLog.find(params[:case_log_id]) current_url = request.env["PATH_INFO"] - subsection = current_url.split("/")[-2] + subsection = form.get_subsection(current_url.split("/")[-2]) render "form/check_answers", locals: { subsection: subsection, form: form } end form = FormHandler.instance.get_form("2021_2022") - form.all_pages.map do |page_key, page_info| - define_method(page_key) do |_errors = {}| + form.pages.map do |page| + define_method(page.id) do |_errors = {}| @case_log = CaseLog.find(params[:case_log_id]) - render "form/page", locals: { form: form, page_key: page_key, page_info: page_info } + render "form/page", locals: { form: form, page: page } end end @@ -101,29 +101,28 @@ private API_ACTIONS = %w[create show update destroy].freeze def responses_for_page(page) - form = FormHandler.instance.get_form("2021_2022") - form.expected_responses_for_page(page).each_with_object({}) do |(question_key, question_info), result| - question_params = params["case_log"][question_key] - if question_info["type"] == "date" - day = params["case_log"]["#{question_key}(3i)"] - month = params["case_log"]["#{question_key}(2i)"] - year = params["case_log"]["#{question_key}(1i)"] + page.expected_responses.each_with_object({}) do |(question), result| + question_params = params["case_log"][question.id] + if question.type == "date" + day = params["case_log"]["#{question.id}(3i)"] + month = params["case_log"]["#{question.id}(2i)"] + year = params["case_log"]["#{question.id}(1i)"] next unless [day, month, year].any?(&:present?) - result[question_key] = if day.to_i.between?(1, 31) && month.to_i.between?(1, 12) && year.to_i.between?(2000, 2200) - Date.new(year.to_i, month.to_i, day.to_i) - else - Date.new(0, 1, 1) - end + result[question.id] = if day.to_i.between?(1, 31) && month.to_i.between?(1, 12) && year.to_i.between?(2000, 2200) + Date.new(year.to_i, month.to_i, day.to_i) + else + Date.new(0, 1, 1) + end end next unless question_params - if %w[checkbox validation_override].include?(question_info["type"]) - question_info["answer_options"].keys.reject { |x| x.match(/divider/) }.each do |option| + if %w[checkbox validation_override].include?(question.type) + question.answer_options.keys.reject { |x| x.match(/divider/) }.each do |option| result[option] = question_params.include?(option) ? 1 : 0 end else - result[question_key] = question_params + result[question.id] = question_params end result end diff --git a/app/controllers/soft_validations_controller.rb b/app/controllers/soft_validations_controller.rb index ad28ea25e..4f9881de6 100644 --- a/app/controllers/soft_validations_controller.rb +++ b/app/controllers/soft_validations_controller.rb @@ -1,9 +1,9 @@ class SoftValidationsController < ApplicationController def show @case_log = CaseLog.find(params[:case_log_id]) - page_key = request.env["PATH_INFO"].split("/")[-2] + page_id = request.env["PATH_INFO"].split("/")[-2] form = FormHandler.instance.get_form("2021_2022") - page = form.all_pages[page_key] + page = form.get_page(page_id) if page_requires_soft_validation_override?(page) errors = @case_log.soft_errors.values.first render json: { show: true, label: errors.message, hint: errors.hint_text } @@ -15,6 +15,6 @@ class SoftValidationsController < ApplicationController private def page_requires_soft_validation_override?(page) - @case_log.soft_errors.present? && @case_log.soft_errors.keys.first == page["soft_validations"]&.keys&.first + @case_log.soft_errors.present? && @case_log.soft_errors.keys.first == page.soft_validations&.first&.id end end diff --git a/app/helpers/check_answers_helper.rb b/app/helpers/check_answers_helper.rb index fed58b71e..ba0237f58 100644 --- a/app/helpers/check_answers_helper.rb +++ b/app/helpers/check_answers_helper.rb @@ -1,57 +1,20 @@ module CheckAnswersHelper - def total_answered_questions(subsection, case_log, form) - total_questions(subsection, case_log, form).keys.count do |question_key| - case_log[question_key].present? - end - end - - def total_number_of_questions(subsection, case_log, form) - total_questions(subsection, case_log, form).count - end - - def total_questions(subsection, case_log, form) - questions = form.questions_for_subsection(subsection) - form.filter_conditional_questions(questions, case_log) - end - - def get_next_page_name(form, page_name, case_log) - page = form.all_pages[page_name] - if page.key?("conditional_route_to") - page["conditional_route_to"].each do |conditional_page_name, condition| - unless condition.any? { |field, value| case_log[field].blank? || !value.include?(case_log[field]) } - return conditional_page_name - end - end + def display_answered_questions_summary(subsection, case_log) + total = subsection.applicable_questions_count(case_log) + answered = subsection.answered_questions_count(case_log) + if total == answered + '
You answered all the questions
'.html_safe + else + "You answered #{answered} of #{total} questions
+ #{create_next_missing_question_link(subsection, case_log)}".html_safe end - form.next_page(page_name) end - def create_update_answer_link(question_title, question_info, case_log, form) - page = form.page_for_question(question_title) - link_name = if question_info["type"] == "checkbox" - question_info["answer_options"].keys.any? { |key| case_log[key] == "Yes" } ? "Change" : "Answer" - else - case_log[question_title].blank? ? "Answer" : "Change" - end - link_to(link_name, "/case_logs/#{case_log.id}/#{page}", class: "govuk-link").html_safe - end +private - def create_next_missing_question_link(case_log_id, subsection, case_log, form) - pages_to_fill_in = [] - form.pages_for_subsection(subsection).each do |page_title, page_info| - page_info["questions"].any? { |question| case_log[question].blank? } - pages_to_fill_in << page_title - end - url = "/case_logs/#{case_log_id}/#{pages_to_fill_in.first}" + def create_next_missing_question_link(subsection, case_log) + pages_to_fill_in = subsection.unanswered_questions(case_log).map(&:page) + url = "/case_logs/#{case_log.id}/#{pages_to_fill_in.first.id}" link_to("Answer the missing questions", url, class: "govuk-link").html_safe end - - def display_answered_questions_summary(subsection, case_log, form) - if total_answered_questions(subsection, case_log, form) == total_number_of_questions(subsection, case_log, form) - 'You answered all the questions
'.html_safe - else - "You answered #{total_answered_questions(subsection, case_log, form)} of #{total_number_of_questions(subsection, case_log, form)} questions
- #{create_next_missing_question_link(case_log['id'], subsection, case_log, form)}".html_safe - end - end end diff --git a/app/helpers/conditional_questions_helper.rb b/app/helpers/conditional_questions_helper.rb index c77119243..ec9ccae60 100644 --- a/app/helpers/conditional_questions_helper.rb +++ b/app/helpers/conditional_questions_helper.rb @@ -1,11 +1,9 @@ module ConditionalQuestionsHelper def conditional_questions_for_page(page) - page["questions"].values.map { |question| - question["conditional_for"] - }.compact.map(&:keys).flatten + page.questions.map(&:conditional_for).compact.map(&:keys).flatten end - def display_question_key_div(page_info, question_key) - "style='display:none;'".html_safe if conditional_questions_for_page(page_info).include?(question_key) + def display_question_key_div(page, question) + "style='display:none;'".html_safe if conditional_questions_for_page(page).include?(question.id) end end diff --git a/app/helpers/question_attribute_helper.rb b/app/helpers/question_attribute_helper.rb index 1010591f0..d292239d8 100644 --- a/app/helpers/question_attribute_helper.rb +++ b/app/helpers/question_attribute_helper.rb @@ -10,23 +10,23 @@ module QuestionAttributeHelper private def numeric_question_html_attributes(question) - return {} if question["fields-to-add"].blank? || question["result-field"].blank? + return {} if question.fields_to_add.blank? || question.result_field.blank? { "data-controller": "numeric-question", "data-action": "numeric-question#calculateFields", - "data-target": "case-log-#{question['result-field'].to_s.dasherize}-field", - "data-calculated": question["fields-to-add"].to_json, + "data-target": "case-log-#{question.result_field.to_s.dasherize}-field", + "data-calculated": question.fields_to_add.to_json, } end def conditional_html_attributes(question) - return {} if question["conditional_for"].blank? + return {} if question.conditional_for.blank? { "data-controller": "conditional-question", "data-action": "conditional-question#displayConditional", - "data-info": question["conditional_for"].to_json, + "data-info": question.conditional_for.to_json, } end end diff --git a/app/helpers/tasklist_helper.rb b/app/helpers/tasklist_helper.rb index 777cc598a..d31257f25 100644 --- a/app/helpers/tasklist_helper.rb +++ b/app/helpers/tasklist_helper.rb @@ -14,40 +14,30 @@ module TasklistHelper }.freeze def get_next_incomplete_section(form, case_log) - subsections = form.all_subsections.keys - subsections.find { |subsection| is_incomplete?(subsection, case_log, form) } + form.subsections.find { |subsection| subsection.is_incomplete?(case_log) } end def get_subsections_count(form, case_log, status = :all) - subsections = form.all_subsections.keys - return subsections.count if status == :all + return form.subsections.count if status == :all - subsections.count { |subsection| form.subsection_status(subsection, case_log) == status } + form.subsections.count { |subsection| subsection.status(case_log) == status } end - def get_first_page_or_check_answers(subsection, case_log, form) - path = if is_started?(subsection, case_log, form) - "case_log_#{subsection}_check_answers_path" + def first_page_or_check_answers(subsection, case_log) + path = if subsection.is_started?(case_log) + "case_log_#{subsection.id}_check_answers_path" else - "case_log_#{form.first_page_for_subsection(subsection)}_path" + "case_log_#{subsection.pages.first.id}_path" end send(path, case_log) end - def subsection_link(subsection_key, subsection_value, status, form, case_log) - next_page_path = status != :cannot_start_yet ? get_first_page_or_check_answers(subsection_key, case_log, form) : "#" - link_to(subsection_value["label"], next_page_path, class: "task-name govuk-link") - end - -private - - def is_incomplete?(subsection, case_log, form) - status = form.subsection_status(subsection, case_log) - %i[not_started in_progress].include?(status) - end - - def is_started?(subsection, case_log, form) - status = form.subsection_status(subsection, case_log) - %i[in_progress completed].include?(status) + def subsection_link(subsection, case_log) + next_page_path = if subsection.status(case_log) != :cannot_start_yet + first_page_or_check_answers(subsection, case_log) + else + "#" + end + link_to(subsection.label, next_page_path, class: "task-name govuk-link") end end diff --git a/app/models/form.rb b/app/models/form.rb index 6c3f5672f..31a264ea5 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -1,75 +1,34 @@ class Form - attr_reader :form_definition + attr_reader :form_definition, :sections, :subsections, :pages, :questions def initialize(form_path) raise "No form definition file exists for given year".freeze unless File.exist?(form_path) @form_definition = JSON.parse(File.open(form_path).read) + @sections = form_definition["sections"].map { |id, s| Form::Section.new(id, s, self) } + @subsections = sections.flat_map(&:subsections) + @pages = subsections.flat_map(&:pages) + @questions = pages.flat_map(&:questions) end - # Returns a hash with sections as keys - def all_sections - @all_sections ||= @form_definition["sections"] + def get_subsection(id) + subsections.find { |s| s.id == id } end - # Returns a hash with subsections as keys - def all_subsections - @all_subsections ||= all_sections.map { |_section_key, section_value| - section_value["subsections"] - }.reduce(:merge) - end - - # Returns a hash with pages as keys - def all_pages - @all_pages ||= all_subsections.map { |_subsection_key, subsection_value| - subsection_value["pages"] - }.reduce(:merge) - end - - # Returns a hash with the pages of a subsection as keys - def pages_for_subsection(subsection) - all_subsections[subsection]["pages"] - end - - # Returns a hash with the questions as keys - def questions_for_page(page) - all_pages[page]["questions"] - end - - # Returns a hash with the questions as keys - def questions_for_subsection(subsection) - pages_for_subsection(subsection).map { |title, _value| questions_for_page(title) }.reduce(:merge) - end - - # Returns a hash with soft validation questions as keys - def soft_validations_for_page(page) - all_pages[page]["soft_validations"] - end - - def expected_responses_for_page(page) - questions_for_page(page).merge(soft_validations_for_page(page) || {}) - end - - def first_page_for_subsection(subsection) - pages_for_subsection(subsection).keys.first + def get_page(id) + pages.find { |p| p.id == id } end def subsection_for_page(page) - all_subsections.find { |_subsection_key, subsection_value| - subsection_value["pages"].key?(page) - }.first - end - - def page_for_question(question) - all_pages.find { |_page_key, page_value| page_value["questions"].key?(question) }.first + subsections.find { |s| s.pages.find { |p| p.id == page.id } } end def next_page(page, case_log) - subsection = subsection_for_page(page) - page_idx = pages_for_subsection(subsection).keys.index(page) - nxt_page = pages_for_subsection(subsection).keys[page_idx + 1] + page_ids = subsection_for_page(page).pages.map(&:id) + page_idx = page_ids.index(page.id) + nxt_page = get_page(page_ids[page_idx + 1]) return :check_answers if nxt_page.nil? - return nxt_page if page_routed_to?(nxt_page, case_log) + return nxt_page.id if nxt_page.routed_to?(case_log) next_page(nxt_page, case_log) end @@ -77,95 +36,16 @@ class Form def next_page_redirect_path(page, case_log) nxt_page = next_page(page, case_log) if nxt_page == :check_answers - subsection = subsection_for_page(page) - "case_log_#{subsection}_check_answers_path" + "case_log_#{subsection_for_page(page).id}_check_answers_path" else "case_log_#{nxt_page}_path" end end - def all_questions - @all_questions ||= all_pages.map { |_page_key, page_value| - page_value["questions"] - }.reduce(:merge) - end - - def filter_conditional_questions(questions, case_log) - applicable_questions = questions - - questions.each do |k, question| - unless page_routed_to?(page_for_question(k), case_log) - applicable_questions = applicable_questions.reject { |z| z == k } - end - - question.fetch("conditional_for", []).each do |conditional_question_key, condition| - if condition_not_met(case_log, k, question, condition) - applicable_questions = applicable_questions.reject { |z| z == conditional_question_key } - end - end - end - applicable_questions - end - - def page_routed_to?(page, case_log) - return true unless (conditions = page_dependencies(page)) - - conditions.all? do |question, value| - case_log[question].present? && case_log[question] == value - end - end - - def page_dependencies(page) - all_pages[page]["depends_on"] - end - - def subsection_dependencies_met?(subsection_name, case_log) - conditions = all_subsections[subsection_name]["depends_on"] - return true unless conditions - - conditions.all? do |subsection, status| - subsection_status(subsection, case_log) == status.to_sym - end - end - - def subsection_status(subsection_name, case_log) - unless subsection_dependencies_met?(subsection_name, case_log) - return :cannot_start_yet - end - - questions = questions_for_subsection(subsection_name) - applicable_questions = filter_conditional_questions(questions, case_log).keys - return :not_started if applicable_questions.all? { |question| case_log[question].blank? } - return :completed if applicable_questions.all? { |question| case_log[question].present? } - - :in_progress - end - - def condition_not_met(case_log, question_key, question, condition) - case question["type"] - when "numeric" - operator = condition[/[<>=]+/].to_sym - operand = condition[/\d+/].to_i - case_log[question_key].blank? || !case_log[question_key].send(operator, operand) - when "text" - case_log[question_key].blank? || !condition.include?(case_log[question_key]) - when "radio" - case_log[question_key].blank? || !condition.include?(case_log[question_key]) - when "select" - case_log[question_key].blank? || !condition.include?(case_log[question_key]) - else - raise "Not implemented yet" - end - end - - def get_answer_label(case_log, question_title) - question = all_questions[question_title] - if question["type"] == "checkbox" - answer = [] - question["answer_options"].each { |key, value| case_log[key] == "Yes" ? answer << value : nil } - return answer.join(", ") - end - - case_log[question_title] + def conditional_question_conditions + conditions = questions.map { |q| Hash(q.id => q.conditional_for) if q.conditional_for.present? }.compact + conditions.map { |c| + c.map { |k, v| v.keys.map { |key| Hash(from: k, to: key, cond: v[key]) } } + }.flatten end end diff --git a/app/models/form/page.rb b/app/models/form/page.rb new file mode 100644 index 000000000..73a1ee143 --- /dev/null +++ b/app/models/form/page.rb @@ -0,0 +1,30 @@ +class Form::Page + attr_accessor :id, :header, :description, :questions, :soft_validations, + :depends_on, :subsection + + def initialize(id, hsh, subsection) + @id = id + @header = hsh["header"] + @description = hsh["description"] + @questions = hsh["questions"].map { |q_id, q| Form::Question.new(q_id, q, self) } + @depends_on = hsh["depends_on"] + @soft_validations = hsh["soft_validations"]&.map { |v_id, s| Form::Question.new(v_id, s, self) } + @subsection = subsection + end + + def expected_responses + questions + (soft_validations || []) + end + + def has_soft_validations? + soft_validations.present? + end + + def routed_to?(case_log) + return true unless depends_on + + depends_on.all? do |question, value| + case_log[question].present? && case_log[question] == value + end + end +end diff --git a/app/models/form/question.rb b/app/models/form/question.rb new file mode 100644 index 000000000..2362441c8 --- /dev/null +++ b/app/models/form/question.rb @@ -0,0 +1,80 @@ +class Form::Question + attr_accessor :id, :header, :hint_text, :description, :questions, + :type, :min, :max, :step, :fields_to_add, :result_field, + :conditional_for, :readonly, :answer_options, :page, :check_answer_label + + def initialize(id, hsh, page) + @id = id + @check_answer_label = hsh["check_answer_label"] + @header = hsh["header"] + @hint_text = hsh["hint_text"] + @type = hsh["type"] + @min = hsh["min"] + @max = hsh["max"] + @step = hsh["step"] + @fields_to_add = hsh["fields-to-add"] + @result_field = hsh["result-field"] + @readonly = hsh["readonly"] + @answer_options = hsh["answer_options"] + @conditional_for = hsh["conditional_for"] + @page = page + end + + delegate :subsection, to: :page + delegate :form, to: :subsection + + def answer_label(case_log) + return checkbox_answer_label(case_log) if type == "checkbox" + + case_log[id].to_s + end + + def read_only? + !!readonly + end + + def conditional_on + @conditional_on ||= form.conditional_question_conditions.select do |condition| + condition[:to] == id + end + end + + def enabled?(case_log) + return true if conditional_on.blank? + + conditional_on.map { |condition| evaluate_condition(condition, case_log) }.all? + end + + def update_answer_link_name(case_log) + if type == "checkbox" + answer_options.keys.any? { |key| case_log[key] == "Yes" } ? "Change" : "Answer" + else + case_log[id].blank? ? "Answer" : "Change" + end + end + +private + + def checkbox_answer_label(case_log) + answer = [] + answer_options.each { |key, value| case_log[key] == "Yes" ? answer << value : nil } + answer.join(", ") + end + + def evaluate_condition(condition, case_log) + case page.questions.find { |q| q.id == condition[:from] }.type + when "numeric" + operator = condition[:cond][/[<>=]+/].to_sym + operand = condition[:cond][/\d+/].to_i + case_log[condition[:from]].present? && case_log[condition[:from]].send(operator, operand) + when "text" + case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]]) + when "radio" + case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]]) + when "select" + case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]]) + else + raise "Not implemented yet" + end + end +end diff --git a/app/models/form/section.rb b/app/models/form/section.rb new file mode 100644 index 000000000..477fc9f18 --- /dev/null +++ b/app/models/form/section.rb @@ -0,0 +1,10 @@ +class Form::Section + attr_accessor :id, :label, :subsections, :form + + def initialize(id, hsh, form) + @id = id + @label = hsh["label"] + @form = form + @subsections = hsh["subsections"].map { |s_id, s| Form::Subsection.new(s_id, s, self) } + end +end diff --git a/app/models/form/subsection.rb b/app/models/form/subsection.rb new file mode 100644 index 000000000..19700d935 --- /dev/null +++ b/app/models/form/subsection.rb @@ -0,0 +1,65 @@ +class Form::Subsection + attr_accessor :id, :label, :section, :pages, :depends_on, :form + + def initialize(id, hsh, section) + @id = id + @label = hsh["label"] + @depends_on = hsh["depends_on"] + @pages = hsh["pages"].map { |s_id, p| Form::Page.new(s_id, p, self) } + @section = section + end + + delegate :form, to: :section + + def questions + @questions ||= pages.flat_map(&:questions) + end + + def enabled?(case_log) + return true unless depends_on + + depends_on.all? do |subsection_id, dependent_status| + form.get_subsection(subsection_id).status(case_log) == dependent_status.to_sym + end + end + + def status(case_log) + unless enabled?(case_log) + return :cannot_start_yet + end + + qs = applicable_questions(case_log) + return :not_started if qs.all? { |question| case_log[question.id].blank? } + return :completed if qs.all? { |question| case_log[question.id].present? } + + :in_progress + end + + def is_incomplete?(case_log) + %i[not_started in_progress].include?(status(case_log)) + end + + def is_started?(case_log) + %i[in_progress completed].include?(status(case_log)) + end + + def applicable_questions_count(case_log) + applicable_questions(case_log).count + end + + def answered_questions_count(case_log) + answered_questions(case_log).count + end + + def applicable_questions(case_log) + questions.select { |q| q.page.routed_to?(case_log) && q.enabled?(case_log) } + end + + def answered_questions(case_log) + applicable_questions(case_log).select { |question| case_log[question.id].present? } + end + + def unanswered_questions(case_log) + applicable_questions(case_log) - answered_questions(case_log) + end +end diff --git a/app/validations/household_validations.rb b/app/validations/household_validations.rb index d7724e85a..6286f2884 100644 --- a/app/validations/household_validations.rb +++ b/app/validations/household_validations.rb @@ -66,7 +66,7 @@ module HouseholdValidations return unless record.age1 if !record.age1.is_a?(Integer) || record.age1 < 16 || record.age1 > 120 - record.errors.add "age1", "Tenant age must be an integer between 16 and 120" + record.errors.add :age1, "Tenant age must be an integer between 16 and 120" end end diff --git a/app/views/case_logs/_tasklist.html.erb b/app/views/case_logs/_tasklist.html.erb index c46f5d364..87eb00a7b 100644 --- a/app/views/case_logs/_tasklist.html.erb +++ b/app/views/case_logs/_tasklist.html.erb @@ -1,17 +1,16 @@