From de5fac978769709cc0107abdf5ab5658f21f882e Mon Sep 17 00:00:00 2001 From: vishwagandhi1610 Date: Thu, 14 Mar 2024 19:51:08 +0000 Subject: [PATCH 01/12] Added uploadfile question type --- .../api/v1/questions_controller.rb | 1 + app/models/upload_file.rb | 72 ++++++++++++++++++ .../20240313201447_create_question_types.rb | 9 +++ ...240313214058_create_questionnaire_types.rb | 9 +++ db/schema.rb | 18 ++++- spec/models/upload_file_spec.rb | 75 +++++++++++++++++++ 6 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 app/models/upload_file.rb create mode 100644 db/migrate/20240313201447_create_question_types.rb create mode 100644 db/migrate/20240313214058_create_questionnaire_types.rb create mode 100644 spec/models/upload_file_spec.rb diff --git a/app/controllers/api/v1/questions_controller.rb b/app/controllers/api/v1/questions_controller.rb index 10c26875d..4ca568cc3 100644 --- a/app/controllers/api/v1/questions_controller.rb +++ b/app/controllers/api/v1/questions_controller.rb @@ -26,6 +26,7 @@ def create question = questionnaire.questions.build( txt: params[:txt], question_type: params[:question_type], + seq: params[:seq], break_before: true ) diff --git a/app/models/upload_file.rb b/app/models/upload_file.rb new file mode 100644 index 000000000..061d34356 --- /dev/null +++ b/app/models/upload_file.rb @@ -0,0 +1,72 @@ +# app/models/upload_file.rb +class UploadFile < Question + def edit(_count) + { + action: 'edit', + elements: [ + { + type: 'link', + text: 'Remove', + href: "/questions/#{id}", + method: 'delete' + }, + { + type: 'input', + input_type: 'text', + name: "question[#{id}][seq]", + id: "question_#{id}_seq", + value: seq.to_s + }, + { + type: 'input', + input_type: 'text', + name: "question[#{id}][id]", + id: "question_#{id}", + value: id.to_s + }, + { + type: 'textarea', + cols: 50, + rows: 1, + name: "question[#{id}][txt]", + id: "question_#{id}_txt", + placeholder: 'Edit question content here', + value: txt + }, + { + type: 'input', + input_type: 'text', + size: 10, + name: "question[#{id}][question_type]", + id: "question_#{id}_question_type", + value: question_type, + disabled: true + } + ] + }.to_json + end + + def view_question_text + { + action: 'view_question_text', + elements: [ + { type: 'text', value: txt }, + { type: 'text', value: question_type }, + { type: 'text', value: weight.to_s }, + { type: 'text', value: id.to_s }, + { type: 'text', value: '—' } # Placeholder for non-applicable fields + ] + }.to_json + end + + + # Implement this method for completing a question + def complete(count, answer = nil) + # Implement the logic for completing a question + end + + # Implement this method for viewing a completed question by a student + def view_completed_question(count, files) + # Implement the logic for viewing a completed question by a student + end +end diff --git a/db/migrate/20240313201447_create_question_types.rb b/db/migrate/20240313201447_create_question_types.rb new file mode 100644 index 000000000..f07640891 --- /dev/null +++ b/db/migrate/20240313201447_create_question_types.rb @@ -0,0 +1,9 @@ +class CreateQuestionTypes < ActiveRecord::Migration[7.0] + def change + create_table :question_types do |t| + t.string :name + + t.timestamps + end + end +end diff --git a/db/migrate/20240313214058_create_questionnaire_types.rb b/db/migrate/20240313214058_create_questionnaire_types.rb new file mode 100644 index 000000000..d00c2c947 --- /dev/null +++ b/db/migrate/20240313214058_create_questionnaire_types.rb @@ -0,0 +1,9 @@ +class CreateQuestionnaireTypes < ActiveRecord::Migration[7.0] + def change + create_table :questionnaire_types do |t| + t.string :name + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0ef599c47..cb0bf3dc0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_04_27_171632) do +ActiveRecord::Schema[7.0].define(version: 2024_03_13_214058) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -142,6 +142,18 @@ t.index ["user_id"], name: "index_participants_on_user_id" end + create_table "question_types", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "questionnaire_types", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "questionnaires", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name" t.integer "instructor_id" @@ -165,10 +177,11 @@ t.boolean "break_before" t.string "max_label" t.string "min_label" - t.integer "questionnaire_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "questionnaire_id", null: false t.index ["questionnaire_id"], name: "fk_question_questionnaires" + t.index ["questionnaire_id"], name: "index_questions_on_questionnaire_id" end create_table "response_maps", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -246,6 +259,7 @@ add_foreign_key "courses", "users", column: "instructor_id" add_foreign_key "participants", "assignments" add_foreign_key "participants", "users" + add_foreign_key "questions", "questionnaires" add_foreign_key "roles", "roles", column: "parent_id", on_delete: :cascade add_foreign_key "ta_mappings", "courses" add_foreign_key "ta_mappings", "users" diff --git a/spec/models/upload_file_spec.rb b/spec/models/upload_file_spec.rb new file mode 100644 index 000000000..97897ada0 --- /dev/null +++ b/spec/models/upload_file_spec.rb @@ -0,0 +1,75 @@ +require 'rails_helper' + +RSpec.describe UploadFile do + # Adjust the let statement to not include 'type' and 'weight' if they are not recognized attributes. + let(:upload_file) { UploadFile.new(id: 1, seq: '1', txt: 'Sample question content', question_type: 'UploadFile') } + + describe '#edit' do + context 'when given a count' do + let(:json_response) { JSON.parse(upload_file.edit(1)) } + + it 'returns JSON with a correct structure for editing' do + expect(json_response["action"]).to eq('edit') + expect(json_response["elements"]).to be_an(Array) + end + + it 'includes a "Remove" link in the elements' do + link_element = json_response["elements"].find { |el| el["text"] == "Remove" } + expect(link_element).not_to be_nil + expect(link_element["href"]).to include("/questions/1") + end + + it 'includes an input field for the sequence' do + seq_element = json_response["elements"].find { |el| el["name"]&.include?("seq") } + expect(seq_element).not_to be_nil + expect(seq_element["value"]).to eq('1') + end + + it 'includes an input field for the id' do + id_element = json_response["elements"].find { |el| el["name"]&.include?("id") } + expect(id_element).not_to be_nil + expect(id_element["value"]).to eq('1') + end + + it 'includes a textarea for editing the question content' do + textarea_element = json_response["elements"].find { |el| el["name"]&.include?("txt") } + expect(textarea_element).not_to be_nil + expect(textarea_element["value"]).to eq('Sample question content') + end + + it 'includes an input field for the question type, disabled' do + type_element = json_response["elements"].find { |el| el["name"]&.include?("question_type") } + expect(type_element).not_to be_nil + expect(type_element["disabled"]).to be true + end + + it 'does not include an explicit cell for weight, as it is not applicable' do + weight_element = json_response["elements"].none? { |el| el.has_key?("weight") } + expect(weight_element).to be true + end + end + end + + describe '#view_question_text' do + context 'when given valid input' do + let(:json_response) { JSON.parse(upload_file.view_question_text) } + + it 'returns JSON for displaying a question' do + expect(json_response["action"]).to eq('view_question_text') + expect(json_response["elements"].map { |el| el["value"] }).to include('Sample question content', 'UploadFile', "", '1','—') + end + end + end + + describe '#complete' do + it 'implements the logic for completing a question' do + # Write your test for the complete method logic here. + end + end + + describe '#view_completed_question' do + it 'implements the logic for viewing a completed question by a student' do + # Write your test for the view_completed_question method logic here. + end + end +end From c814847c4516ab2841bd6c2c4e8051af322d3827 Mon Sep 17 00:00:00 2001 From: vishwagandhi1610 Date: Thu, 14 Mar 2024 20:34:19 +0000 Subject: [PATCH 02/12] added text_response question type --- app/models/text_response.rb | 64 +++++++++++++++++++++++++++++++ spec/models/text_response_spec.rb | 32 ++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 app/models/text_response.rb create mode 100644 spec/models/text_response_spec.rb diff --git a/app/models/text_response.rb b/app/models/text_response.rb new file mode 100644 index 000000000..a26912efd --- /dev/null +++ b/app/models/text_response.rb @@ -0,0 +1,64 @@ +class TextResponse < Question + validates :size, presence: true + + def edit(_count) + { + action: 'edit', + elements: [ + { + type: 'link', + text: 'Remove', + href: "/questions/#{id}", + method: 'delete' + }, + { + type: 'input', + input_type: 'text', + size: 6, + name: "question[#{id}][seq]", + id: "question_#{id}_seq", + value: seq.to_s + }, + { + type: 'textarea', + cols: 50, + rows: 1, + name: "question[#{id}][txt]", + id: "question_#{id}_txt", + placeholder: 'Edit question content here', + value: txt + }, + { + type: 'input', + input_type: 'text', + size: 10, + name: "question[#{id}][question_type]", + id: "question_#{id}_question_type", + value: question_type, + disabled: true + }, + { + type: 'input', + input_type: 'text', + size: 6, + name: "question[#{id}][size]", + id: "question_#{id}_size", + value: size, + label: 'Text area size' + } + ] + }.to_json + end + + def view_question_text + { + action: 'view_question_text', + elements: [ + { type: 'text', value: txt }, + { type: 'text', value: question_type }, + { type: 'text', value: weight.to_s }, + { type: 'text', value: '—' } + ] + }.to_json + end +end diff --git a/spec/models/text_response_spec.rb b/spec/models/text_response_spec.rb new file mode 100644 index 000000000..f326e9653 --- /dev/null +++ b/spec/models/text_response_spec.rb @@ -0,0 +1,32 @@ +require 'rails_helper' + +RSpec.describe TextResponse, type: :model do + let(:text_response) { TextResponse.create(seq: '001', txt: 'Sample question content', question_type: 'TextResponse', size: 'medium', weight: 10) } + + describe '#edit' do + let(:result) { JSON.parse(text_response.edit(1)) } + + it 'returns JSON for editing with correct action' do + expect(result["action"]).to eq('edit') + end + + it 'includes elements for editing question' do + expect(result["elements"].length).to be > 0 + end + end + + describe '#view_question_text' do + let(:result) { JSON.parse(text_response.view_question_text) } + + it 'returns JSON for viewing question text with correct action' do + expect(result["action"]).to eq('view_question_text') + end + + it 'includes the question text, question_type, and weight in elements' do + expect(result["elements"].any? { |e| e["value"] == 'Sample question content' }).to be true + expect(result["elements"].any? { |e| e["value"] == 'TextResponse' }).to be true + expect(result["elements"].any? { |e| e["value"].match?(/^\d+$/) }).to be true + end + end + +end From 023044dffb2306257b5c3748af3543da809bdd46 Mon Sep 17 00:00:00 2001 From: vishwagandhi1610 Date: Thu, 14 Mar 2024 23:44:50 +0000 Subject: [PATCH 03/12] added textarea and textfield models and tests --- app/models/text_area.rb | 22 +++++++++++++++++ app/models/text_field.rb | 40 +++++++++++++++++++++++++++++++ spec/models/text_area_spec.rb | 40 +++++++++++++++++++++++++++++++ spec/models/text_field_spec.rb | 43 ++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 app/models/text_area.rb create mode 100644 app/models/text_field.rb create mode 100644 spec/models/text_area_spec.rb create mode 100644 spec/models/text_field_spec.rb diff --git a/app/models/text_area.rb b/app/models/text_area.rb new file mode 100644 index 000000000..d9173ecf3 --- /dev/null +++ b/app/models/text_area.rb @@ -0,0 +1,22 @@ +class TextArea < Question + def complete(count, answer = nil) + { + action: 'complete', + data: { + count: count, + comment: answer&.comments, + size: size || '70,1', # Assuming '70,1' is the default size + } + }.to_json + end + + def view_completed_question(count, answer) + { + action: 'view_completed_question', + data: { + count: count, + comment: answer.comments + } + }.to_json + end +end diff --git a/app/models/text_field.rb b/app/models/text_field.rb new file mode 100644 index 000000000..fac966d77 --- /dev/null +++ b/app/models/text_field.rb @@ -0,0 +1,40 @@ +class TextField < Question + validates :size, presence: true + + def complete(count, answer = nil) + { + action: 'complete', + data: { + label: "Question ##{count}", + type: 'text', + name: "response[answers][#{id}]", + id: "responses_#{id}", + value: answer&.comments + } + }.to_json + end + + def view_completed_question(count, files) + if question_type == 'TextField' && break_before + { + action: 'view_completed_question', + data: { + type: 'text', + label: "Completed Question ##{count}", + value: txt, + break_before: break_before + } + }.to_json + else + { + action: 'view_completed_question', + data: { + type: 'text', + label: "Completed Question ##{count}", + value: txt, + break_before: break_before + } + }.to_json + end + end +end diff --git a/spec/models/text_area_spec.rb b/spec/models/text_area_spec.rb new file mode 100644 index 000000000..05e8f174f --- /dev/null +++ b/spec/models/text_area_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +RSpec.describe TextArea do + let(:text_area) { TextArea.create(size: '34,1') } + let!(:answer) { Answer.create(comments: 'test comment') } + + describe '#complete' do + context 'when count is provided' do + it 'generates JSON for a textarea input' do + result = JSON.parse(text_area.complete(1)) + expect(result['action']).to eq('complete') + expect(result['data']['count']).to eq(1) + expect(result['data']['size']).to eq('34,1') + end + + it 'includes any existing comments in the textarea input' do + result = JSON.parse(text_area.complete(1, answer)) + expect(result['data']['comment']).to eq('test comment') + end + end + + context 'when count is not provided' do + it 'generates JSON with default size for the textarea input' do + text_area = TextArea.create(size: nil) + result = JSON.parse(text_area.complete(nil)) + expect(result['data']['size']).to eq('70,1') + end + end + end + + describe 'view_completed_question' do + context 'when given a count and an answer' do + it 'returns the formatted JSON for the completed question' do + result = JSON.parse(text_area.view_completed_question(1, answer)) + expect(result['action']).to eq('view_completed_question') + expect(result['data']['comment']).to eq('test comment') + end + end + end +end diff --git a/spec/models/text_field_spec.rb b/spec/models/text_field_spec.rb new file mode 100644 index 000000000..66f52b830 --- /dev/null +++ b/spec/models/text_field_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +RSpec.describe TextField, type: :model do + let(:question) { TextField.create(txt: 'Sample Text Question', question_type: 'TextField', size: 'medium', break_before: true) } + let(:answer) { Answer.new(comments: 'This is a sample answer.') } + + describe '#complete' do + context 'when count is provided' do + it 'returns JSON data for a paragraph with a label and input fields' do + result = JSON.parse(question.complete(1)) + expect(result['action']).to eq('complete') + expect(result['data']['type']).to eq('text') + expect(result['data']['name']).to eq("response[answers][#{question.id}]") + end + end + + context 'when count and answer are provided' do + it 'returns JSON data with pre-filled comment value' do + result = JSON.parse(question.complete(1, answer)) + expect(result['data']['value']).to eq(answer.comments) + end + end + end + + describe '#view_completed_question' do + context "when the question type is 'TextField' and break_before is true" do + it 'returns the formatted JSON for the completed question' do + result = JSON.parse(question.view_completed_question(1, [])) + expect(result['data']['type']).to eq('text') + expect(result['data']['break_before']).to be true + end + end + + context "when the question type is not 'TextField' or break_before is false" do + let(:non_text_field_question) { TextField.create(txt: 'Non-text question', question_type: 'NotTextField', size: 'small', break_before: false) } + + it 'returns the formatted JSON for the completed question' do + result = JSON.parse(non_text_field_question.view_completed_question(1, [])) + expect(result['data']['break_before']).to be false + end + end + end +end From 0a75f19412ef79300734cd84803fa6a408ee9dd1 Mon Sep 17 00:00:00 2001 From: vishwagandhi1610 Date: Fri, 15 Mar 2024 19:37:46 +0000 Subject: [PATCH 04/12] added question_advice.rb and criterion.rb model and criterion_rspec.rb --- app/models/criterion.rb | 81 +++++++++++++++++++ app/models/question_advice.rb | 22 +++++ .../20240315004809_create_question_advices.rb | 11 +++ db/schema.rb | 12 ++- spec/models/criterion_spec.rb | 77 ++++++++++++++++++ 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 app/models/criterion.rb create mode 100644 app/models/question_advice.rb create mode 100644 db/migrate/20240315004809_create_question_advices.rb create mode 100644 spec/models/criterion_spec.rb diff --git a/app/models/criterion.rb b/app/models/criterion.rb new file mode 100644 index 000000000..bca885b87 --- /dev/null +++ b/app/models/criterion.rb @@ -0,0 +1,81 @@ +class Criterion < ScoredQuestion + validates :size, presence: true + + def edit(_count) + { + remove_link: "/questions/#{id}", + sequence_input: seq.to_s, + question_text: txt, + question_type: question_type, + weight: weight.to_s, + size: size.to_s, + max_label: max_label, + min_label: min_label + } + end + + def view_question_text + question_data = { + text: txt, + question_type: question_type, + weight: weight, + score_range: "#{questionnaire.min_question_score} to #{questionnaire.max_question_score}" + } + + question_data[:score_range] = "(#{min_label}) " + question_data[:score_range] + " (#{max_label})" if max_label && min_label + question_data + end + + def complete(count, answer = nil, questionnaire_min, questionnaire_max, dropdown_or_scale) + question_advices = QuestionAdvice.to_json_by_question_id(id) + advice_total_length = question_advices.sum { |advice| advice.advice.length unless advice.advice.blank? } + + response_options = if dropdown_or_scale == 'dropdown' + dropdown_criterion_question(count, answer, questionnaire_min, questionnaire_max) + elsif dropdown_or_scale == 'scale' + scale_criterion_question(count, answer, questionnaire_min, questionnaire_max) + end + + advice_section = question_advices.empty? || advice_total_length.zero? ? nil : advices_criterion_question(count, question_advices) + + { + label: txt, + advice: advice_section, + response_options: response_options + }.compact # Use .compact to remove nil values + end + + # Assuming now these methods should be public based on the test cases + def dropdown_criterion_question(count, answer, questionnaire_min, questionnaire_max) + options = (questionnaire_min..questionnaire_max).map do |score| + option = { value: score, label: score.to_s } + option[:selected] = 'selected' if answer && score == answer.answer + option + end + { type: 'dropdown', options: options, current_answer: answer.try(:answer), comments: answer.try(:comments) } + end + + def scale_criterion_question(count, answer, questionnaire_min, questionnaire_max) + { + type: 'scale', + min: questionnaire_min, + max: questionnaire_max, + current_answer: answer.try(:answer), + comments: answer.try(:comments), + min_label: min_label, + max_label: max_label, + size: size + } + end + + private + + def advices_criterion_question(count, question_advices) + question_advices.map do |advice| + { + score: advice.score, + advice: advice.advice + } + end + end +end diff --git a/app/models/question_advice.rb b/app/models/question_advice.rb new file mode 100644 index 000000000..e4cc5bdd3 --- /dev/null +++ b/app/models/question_advice.rb @@ -0,0 +1,22 @@ +class QuestionAdvice < ApplicationRecord + belongs_to :question + def self.export_fields(_options) + QuestionAdvice.columns.map(&:name) + end + + def self.export(csv, parent_id, _options) + questionnaire = Questionnaire.find(parent_id) + questionnaire.questions.each do |question| + QuestionAdvice.where(question_id: question.id).each do |advice| + csv << advice.attributes.values + end + end + end + + def self.to_json_by_question_id(question_id) + question_advices = QuestionAdvice.where(question_id: question_id).order(:id) + question_advices.map do |advice| + { score: advice.score, advice: advice.advice } + end + end +end diff --git a/db/migrate/20240315004809_create_question_advices.rb b/db/migrate/20240315004809_create_question_advices.rb new file mode 100644 index 000000000..4b2c128e7 --- /dev/null +++ b/db/migrate/20240315004809_create_question_advices.rb @@ -0,0 +1,11 @@ +class CreateQuestionAdvices < ActiveRecord::Migration[7.0] + def change + create_table :question_advices do |t| + t.references :question, null: false, foreign_key: true + t.integer :score + t.text :advice + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index cb0bf3dc0..4d6cb5d47 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_03_13_214058) do +ActiveRecord::Schema[7.0].define(version: 2024_03_15_004809) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -142,6 +142,15 @@ t.index ["user_id"], name: "index_participants_on_user_id" end + create_table "question_advices", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.bigint "question_id", null: false + t.integer "score" + t.text "advice" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["question_id"], name: "index_question_advices_on_question_id" + end + create_table "question_types", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false @@ -259,6 +268,7 @@ add_foreign_key "courses", "users", column: "instructor_id" add_foreign_key "participants", "assignments" add_foreign_key "participants", "users" + add_foreign_key "question_advices", "questions" add_foreign_key "questions", "questionnaires" add_foreign_key "roles", "roles", column: "parent_id", on_delete: :cascade add_foreign_key "ta_mappings", "courses" diff --git a/spec/models/criterion_spec.rb b/spec/models/criterion_spec.rb new file mode 100644 index 000000000..8adc5045b --- /dev/null +++ b/spec/models/criterion_spec.rb @@ -0,0 +1,77 @@ +require 'rails_helper' + +RSpec.describe Criterion, type: :model do + let(:questionnaire) { Questionnaire.new(min_question_score: 0, max_question_score: 5) } + let(:criterion) { Criterion.new(id: 1, question_type: 'Criterion', seq: 1.0, txt: 'test txt', weight: 1, questionnaire: questionnaire) } + let(:answer_no_comments) { Answer.new(answer: 8) } + let(:answer_comments) { Answer.new(answer: 3, comments: 'text comments') } + + describe '#view_question_text' do + it 'returns the JSON' do + json = criterion.view_question_text + expected_json = { + text: 'test txt', + question_type: 'Criterion', + weight: 1, + score_range: '0 to 5' + } + expect(json).to eq(expected_json) + end + end + + describe '#complete' do + it 'returns JSON without answer and no dropdown or scale specified' do + json = criterion.complete(0, nil, 0, 5) + expected_json = { + label: 'test txt' + } + expect(json).to include(expected_json) + end + + it 'returns JSON with a dropdown, including answer options' do + json = criterion.complete(0, nil, 0, 5, 'dropdown') + expected_options = (0..5).map { |score| { value: score, label: score.to_s } } + expected_json = { + label: 'test txt', + response_options: { + type: 'dropdown', + comments: nil, + current_answer: nil, + options: expected_options, + } + } + expect(json).to include(expected_json) + end + end + + describe '#dropdown_criterion_question' do + it 'returns JSON for a dropdown without an answer selected' do + json = criterion.dropdown_criterion_question(0, nil, 0, 5) + expected_options = (0..5).map { |score| { value: score, label: score.to_s } } + expected_json = { + type: 'dropdown', + comments: nil, + current_answer: nil, + options: expected_options, + } + expect(json).to eq(expected_json) + end + end + + describe '#scale_criterion_question' do + it 'returns JSON for a scale question without an answer selected' do + json = criterion.scale_criterion_question(0, nil, 0, 5) + expected_json = { + type: 'scale', + min: 0, + max: 5, + comments: nil, + current_answer: nil, + min_label: nil, + max_label: nil, + size: nil, + } + expect(json).to eq(expected_json) + end + end +end From 640154c69be41f3d9e8938d6a36738f85a997c4d Mon Sep 17 00:00:00 2001 From: vishwagandhi1610 Date: Sun, 24 Mar 2024 19:57:38 +0000 Subject: [PATCH 05/12] changed params --- app/models/criterion.rb | 10 +++++----- app/models/text_area.rb | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/criterion.rb b/app/models/criterion.rb index bca885b87..eb22880fa 100644 --- a/app/models/criterion.rb +++ b/app/models/criterion.rb @@ -1,7 +1,7 @@ class Criterion < ScoredQuestion validates :size, presence: true - def edit(_count) + def edit { remove_link: "/questions/#{id}", sequence_input: seq.to_s, @@ -26,7 +26,7 @@ def view_question_text question_data end - def complete(count, answer = nil, questionnaire_min, questionnaire_max, dropdown_or_scale) + def complete(count,answer = nil, questionnaire_min, questionnaire_max, dropdown_or_scale) question_advices = QuestionAdvice.to_json_by_question_id(id) advice_total_length = question_advices.sum { |advice| advice.advice.length unless advice.advice.blank? } @@ -46,7 +46,7 @@ def complete(count, answer = nil, questionnaire_min, questionnaire_max, dropdown end # Assuming now these methods should be public based on the test cases - def dropdown_criterion_question(count, answer, questionnaire_min, questionnaire_max) + def dropdown_criterion_question(count,answer, questionnaire_min, questionnaire_max) options = (questionnaire_min..questionnaire_max).map do |score| option = { value: score, label: score.to_s } option[:selected] = 'selected' if answer && score == answer.answer @@ -55,7 +55,7 @@ def dropdown_criterion_question(count, answer, questionnaire_min, questionnaire_ { type: 'dropdown', options: options, current_answer: answer.try(:answer), comments: answer.try(:comments) } end - def scale_criterion_question(count, answer, questionnaire_min, questionnaire_max) + def scale_criterion_question(count,answer, questionnaire_min, questionnaire_max) { type: 'scale', min: questionnaire_min, @@ -70,7 +70,7 @@ def scale_criterion_question(count, answer, questionnaire_min, questionnaire_max private - def advices_criterion_question(count, question_advices) + def advices_criterion_question(question_advices) question_advices.map do |advice| { score: advice.score, diff --git a/app/models/text_area.rb b/app/models/text_area.rb index d9173ecf3..14dd7d286 100644 --- a/app/models/text_area.rb +++ b/app/models/text_area.rb @@ -1,5 +1,5 @@ class TextArea < Question - def complete(count, answer = nil) + def complete(count,answer = nil) { action: 'complete', data: { From 6b978c10153c266357d56640f219acd6f098c006 Mon Sep 17 00:00:00 2001 From: vishwagandhi1610 Date: Sun, 7 Apr 2024 22:17:30 +0000 Subject: [PATCH 06/12] added checkbox.rb and rspec tests --- app/models/checkbox.rb | 172 ++++++++++++++++++++++++++++++++ app/models/unscored_question.rb | 9 ++ spec/models/checkbox_spec.rb | 77 ++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 app/models/checkbox.rb create mode 100644 app/models/unscored_question.rb create mode 100644 spec/models/checkbox_spec.rb diff --git a/app/models/checkbox.rb b/app/models/checkbox.rb new file mode 100644 index 000000000..50b7e3b74 --- /dev/null +++ b/app/models/checkbox.rb @@ -0,0 +1,172 @@ +class Checkbox < UnscoredQuestion + def edit(count) + { + remove_button: edit_remove_button(count), + seq: edit_seq(count), + question: edit_question(count), + type: edit_type(count), + weight: edit_weight(count) + } + end + + def edit_remove_button(count) + { + type: 'remove_button', + action: 'delete', + href: "/questions/#{id}", + text: 'Remove' + } + end + + def edit_seq(count) + { + type: 'seq', + input_size: 6, + value: seq, + name: "question[#{id}][seq]", + id: "question_#{id}_seq" + } + end + + def edit_question(count) + { + type: 'textarea', + cols: 50, + rows: 1, + name: "question[#{id}][txt]", + id: "question_#{id}_txt", + placeholder: 'Edit question content here', + content: txt + } + end + + def edit_type(count) + { + type: 'text', + input_size: 10, + disabled: true, + value: question_type, + name: "question[#{id}][type]", + id: "question_#{id}_type" + } + end + + def edit_weight(count) + { + type: 'weight', + placeholder: 'UnscoredQuestion does not need weight' + } + end + + + def view_question_text + { + content: txt, + type: question_type, + weight: weight.to_s, + checked_state: 'Checked/Unchecked' + } + end + + def complete(count, answer = nil) + { + previous_question: check_previous_question, + inputs: [ + complete_first_second_input(count, answer), + complete_third_input(count, answer) + ], + label: { + for: "responses_#{count}", + text: txt + }, + script: complete_script(count), + if_column_header: complete_if_column_header + } + end + + def check_previous_question + prev_question = Question.where('seq < ?', seq).order(:seq).last + { + type: prev_question&.type == 'ColumnHeader' ? 'ColumnHeader' : 'other' + } + end + + def complete_first_second_input(count, answer = nil) + [ + { + id: "responses_#{count}_comments", + name: "responses[#{count}][comment]", + type: 'hidden', + value: '' + }, + { + id: "responses_#{count}_score", + name: "responses[#{count}][score]", + type: 'hidden', + value: answer&.answer == 1 ? '1' : '0' + } + ] + end + + def complete_third_input(count, answer = nil) + { + id: "responses_#{count}_checkbox", + type: 'checkbox', + onchange: "checkbox#{count}Changed()", + checked: answer&.answer == 1 + } + end + + def complete_script(count) + "function checkbox#{count}Changed() { var checkbox = jQuery('#responses_#{count}_checkbox'); var response_score = jQuery('#responses_#{count}_score'); if (checkbox.is(':checked')) { response_score.val('1'); } else { response_score.val('0'); }}" + end + + def complete_if_column_header + next_question = Question.where('seq > ?', seq).order(:seq).first + if next_question + case next_question.question_type + when 'ColumnHeader' + 'end_of_column_header' + when 'SectionHeader', 'TableHeader' + 'end_of_section_or_table' + else + 'continue' + end + else + 'end' + end + end + + def view_completed_question(count, answer) + { + previous_question: check_previous_question, + answer: view_completed_question_answer(count, answer), + if_column_header: view_completed_question_if_column_header + } + end + + def view_completed_question_answer(count, answer) + { + number: count, + image: answer.answer == 1 ? 'Check-icon.png' : 'delete_icon.png', + content: txt, + bold: true + } + end + + def view_completed_question_if_column_header + next_question = Question.where('seq > ?', seq).order(:seq).first + if next_question + case next_question.question_type + when 'ColumnHeader' + 'end_of_column_header' + when 'TableHeader' + 'end_of_table_header' + else + 'continue' + end + else + 'end' + end + end +end diff --git a/app/models/unscored_question.rb b/app/models/unscored_question.rb new file mode 100644 index 000000000..ce754d763 --- /dev/null +++ b/app/models/unscored_question.rb @@ -0,0 +1,9 @@ +class UnscoredQuestion < ChoiceQuestion + def edit; end + + def view_question_text; end + + def complete; end + + def view_completed_question; end +end \ No newline at end of file diff --git a/spec/models/checkbox_spec.rb b/spec/models/checkbox_spec.rb new file mode 100644 index 000000000..31140a7c3 --- /dev/null +++ b/spec/models/checkbox_spec.rb @@ -0,0 +1,77 @@ +require 'rails_helper' + +RSpec.describe Checkbox do + let!(:checkbox) { Checkbox.new(id: 10, question_type: 'Checkbox', seq: 1.0, txt: 'test txt', weight: 11) } + let!(:answer) { Answer.new(answer: 1) } + + describe '#edit' do + it 'returns the JSON' do + json = checkbox.edit(0) + expected_json = { + remove_button: { type: 'remove_button', action: 'delete', href: "/questions/10", text: 'Remove' }, + seq: { type: 'seq', input_size: 6, value: 1.0, name: "question[10][seq]", id: "question_10_seq" }, + question: { type: 'textarea', cols: 50, rows: 1, name: "question[10][txt]", id: "question_10_txt", placeholder: 'Edit question content here', content: 'test txt' }, + type: { type: 'text', input_size: 10, disabled: true, value: 'Checkbox', name: "question[10][type]", id: "question_10_type" }, + weight: { type: 'weight', placeholder: 'UnscoredQuestion does not need weight' } + } + expect(json).to eq(expected_json) + end + end + + describe '#complete' do + let(:checkbox) { Checkbox.new(id: 10, question_type: 'Checkbox', seq: 1.0, txt: 'test txt', weight: 11) } + let(:count) { 1 } + let(:answer) { OpenStruct.new(answer: 1) } # Mocking Answer object + + context 'when an answer is provided' do + it 'returns the expected completion structure' do + result = checkbox.complete(count, answer) + + expect(result[:previous_question]).to be_present + expect(result[:inputs]).to be_an(Array) + expect(result[:label]).to include(text: checkbox.txt) + expect(result[:script]).to include("checkbox#{count}Changed()") + expect(result[:inputs].last[:checked]).to be true + end + end + + context 'when no answer is provided' do + let(:answer) { OpenStruct.new(answer: nil) } + + it 'returns a structure with the checkbox not checked' do + result = checkbox.complete(count, answer) + expect(result[:inputs].last[:checked]).to be false + end + end + end + + describe '#view_question_text' do + it 'returns the JSON' do + json = checkbox.view_question_text + expected_json = { + content: 'test txt', + type: 'Checkbox', + weight: '11', + checked_state: 'Checked/Unchecked' + } + expect(json).to eq(expected_json) + end + end + + describe '#view_completed_question' do + it 'returns the JSON' do + json = checkbox.view_completed_question(0, answer) + expected_json = { + previous_question: { type: 'other' }, + answer: { + number: 0, + image: 'Check-icon.png', + content: 'test txt', + bold: true + }, + if_column_header: 'continue' + } + expect(json).to eq(expected_json) + end + end +end From d0a8e7366ce026ec0552030f66acfcbe12ea2c51 Mon Sep 17 00:00:00 2001 From: vishwagandhi1610 Date: Mon, 8 Apr 2024 16:33:14 +0000 Subject: [PATCH 07/12] added scale model and rspec tests --- app/helpers/question_helper.rb | 19 ++++++++ app/models/scale.rb | 35 +++++++++++++++ spec/models/scale_spec.rb | 82 ++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 app/helpers/question_helper.rb create mode 100644 app/models/scale.rb create mode 100644 spec/models/scale_spec.rb diff --git a/app/helpers/question_helper.rb b/app/helpers/question_helper.rb new file mode 100644 index 000000000..4c6e1bc26 --- /dev/null +++ b/app/helpers/question_helper.rb @@ -0,0 +1,19 @@ +module QuestionHelper + def edit_common(label, input_value, min_question_score, max_question_score, weight, type) + { + form: true, + label: label, + input_type: 'text', + input_name: 'question', + input_value: input_value, + min_question_score: min_question_score, + max_question_score: max_question_score, + weight: weight, + type: type + } + end + + def view_question_text_common(text, type, weight, score_range) + { text: text, type: type, weight: weight, score_range: score_range } + end +end \ No newline at end of file diff --git a/app/models/scale.rb b/app/models/scale.rb new file mode 100644 index 000000000..7e35b3504 --- /dev/null +++ b/app/models/scale.rb @@ -0,0 +1,35 @@ +class Scale < ScoredQuestion + include QuestionHelper + + attr_accessor :txt, :type, :weight, :min_label, :max_label, :answer, :min_question_score, :max_question_score + + def edit + edit_common('Question:', 'Scale Question', min_question_score, max_question_score , weight, type).to_json + end + + def view_question_text + view_question_text_common(txt, type, weight, score_range).to_json + end + + def complete + options = (@min_question_score..@max_question_score).map do |option| + { value: option, selected: (option == answer) } + end + { scale_options: options }.to_json + end + + def view_completed_question(options = {}) + if options[:count] && options[:answer] && options[:questionnaire_max] + { count: options[:count], answer: options[:answer], questionnaire_max: options[:questionnaire_max] }.to_json + else + { message: 'Question not answered.' }.to_json + end + end + + private + + def score_range + min_label.nil? && max_label.nil? ? "#{@min_question_score} to #{@max_question_score}" : + "#{min_label} #{@min_question_score} to #{@max_question_score} #{max_label}" + end +end \ No newline at end of file diff --git a/spec/models/scale_spec.rb b/spec/models/scale_spec.rb new file mode 100644 index 000000000..ce6bfcacb --- /dev/null +++ b/spec/models/scale_spec.rb @@ -0,0 +1,82 @@ + +require 'rails_helper' + +RSpec.describe Scale, type: :model do + + subject { Scale.new } + + before do + subject.txt = "Rate your experience" + subject.type = "Scale" + subject.weight = 1 + subject.min_label = "Poor" + subject.max_label = "Excellent" + subject.min_question_score = 1 + subject.max_question_score = 5 + subject.answer = 3 + end + + describe "#edit" do + + it 'returns a JSON object with question text, type, weight, and score range' do + scale = Scale.new(txt: 'Scale Question', type: 'scale', weight: 2, min_question_score: 0, max_question_score: 10) + + json_result = scale.edit + + expected_result = { + form: true, + label: "Question:", + input_type: "text", + input_name: "question", + input_value: "Scale Question", + min_question_score: 0, + max_question_score: 10, + weight: 2, + type: 'scale' + }.to_json + expect(json_result).to eq(expected_result) + end + end + + describe "#view_question_text" do + it "returns JSON containing the question text" do + expected_json = { + text: "Rate your experience", + type: "Scale", + weight: 1, + score_range: "Poor 1 to 5 Excellent" + }.to_json + expect(subject.view_question_text).to eq(expected_json) + end + end + + describe "#complete" do + it "returns JSON with scale options" do + expected_json = { scale_options: [ + { value: 1, selected: false }, + { value: 2, selected: false }, + { value: 3, selected: true }, + { value: 4, selected: false }, + { value: 5, selected: false } + ] }.to_json + expect(subject.complete).to eq(expected_json) + end + end + + describe "#view_completed_question" do + context "when the question has been answered" do + it "returns JSON with the count, answer, and questionnaire_max" do + options = { count: 10, answer: 3, questionnaire_max: 50 } + expected_json = options.to_json + expect(subject.view_completed_question(options)).to eq(expected_json) + end + end + + context "when the question has not been answered" do + it "returns a message indicating the question was not answered" do + expected_json = { message: "Question not answered." }.to_json + expect(subject.view_completed_question).to eq(expected_json) + end + end + end +end From 26ae5f5b6d7b1983306181655c0479f8bf2a2f57 Mon Sep 17 00:00:00 2001 From: vishwagandhi1610 Date: Mon, 8 Apr 2024 22:41:00 +0000 Subject: [PATCH 08/12] added dropdown model and rspec tests --- app/helpers/question_helper.rb | 4 +- app/models/dropdown.rb | 26 ++++++++ app/models/scale.rb | 2 +- spec/models/dropdown_spec.rb | 107 +++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 app/models/dropdown.rb create mode 100644 spec/models/dropdown_spec.rb diff --git a/app/helpers/question_helper.rb b/app/helpers/question_helper.rb index 4c6e1bc26..f26a9fd5c 100644 --- a/app/helpers/question_helper.rb +++ b/app/helpers/question_helper.rb @@ -1,11 +1,11 @@ module QuestionHelper - def edit_common(label, input_value, min_question_score, max_question_score, weight, type) + def edit_common(label = nil,min_question_score = nil, max_question_score = nil, txt ,weight, type) { form: true, label: label, input_type: 'text', input_name: 'question', - input_value: input_value, + input_value: txt, min_question_score: min_question_score, max_question_score: max_question_score, weight: weight, diff --git a/app/models/dropdown.rb b/app/models/dropdown.rb new file mode 100644 index 000000000..0fbb6195d --- /dev/null +++ b/app/models/dropdown.rb @@ -0,0 +1,26 @@ +class Dropdown < UnscoredQuestion + include QuestionHelper + + attr_accessor :txt, :type, :count, :weight + def edit(count) + edit_common("Question #{count}:", txt , weight, type).to_json + end + + def view_question_text + view_question_text_common(txt, type, weight, 'N/A').to_json + end + + def complete(count, answer = nil) + options = (1..count).map { |option| { value: option, selected: (option == answer.to_i) } } + { dropdown_options: options }.to_json + end + + def complete_for_alternatives(alternatives, answer) + options = alternatives.map { |alt| { value: alt, selected: (alt == answer) } } + { dropdown_options: options }.to_json + end + + def view_completed_question + { selected_option: (count && answer) ? "#{answer} (out of #{count})" : 'Question not answered.' }.to_json + end +end \ No newline at end of file diff --git a/app/models/scale.rb b/app/models/scale.rb index 7e35b3504..be6343769 100644 --- a/app/models/scale.rb +++ b/app/models/scale.rb @@ -4,7 +4,7 @@ class Scale < ScoredQuestion attr_accessor :txt, :type, :weight, :min_label, :max_label, :answer, :min_question_score, :max_question_score def edit - edit_common('Question:', 'Scale Question', min_question_score, max_question_score , weight, type).to_json + edit_common('Question:', min_question_score, max_question_score , txt, weight, type).to_json end def view_question_text diff --git a/spec/models/dropdown_spec.rb b/spec/models/dropdown_spec.rb new file mode 100644 index 000000000..605980f31 --- /dev/null +++ b/spec/models/dropdown_spec.rb @@ -0,0 +1,107 @@ +require 'rails_helper' + +RSpec.describe Dropdown, type: :model do + describe '#edit' do + context 'when given a count' do + it 'returns a JSON object with the edit form for a question' do + dropdown = Dropdown.new(txt: "Some Text", type: "dropdown", weight: 1) + json_result = dropdown.edit(5) + + expected_result = { + form: true, + label: "Question 5:", + input_type: "text", + input_name: "question", + input_value: "Some Text", + min_question_score: nil, + max_question_score: nil, + weight: 1, + type: 'dropdown' + }.to_json + expect(json_result).to eq(expected_result) + end + end + end + + describe '#view_question_text' do + let(:dropdown) { Dropdown.new } + context 'when given valid inputs' do + it 'returns the JSON for displaying the question text, type, weight, and score range' do + allow(dropdown).to receive(:txt).and_return("Question 1") + allow(dropdown).to receive(:type).and_return("Multiple Choice") + allow(dropdown).to receive(:weight).and_return(1) + expected_json = { + text: "Question 1", + type: "Multiple Choice", + weight: 1, + score_range: "N/A" + }.to_json + expect(dropdown.view_question_text).to eq(expected_json) + end + end + end + + describe '#complete' do + let(:dropdown) { Dropdown.new } + context 'when count is provided' do + it 'generates JSON for a select input with the given count' do + count = 3 + expected_json = { + dropdown_options: [ + { value: 1, selected: false }, + { value: 2, selected: false }, + { value: 3, selected: false } + ] + }.to_json + expect(dropdown.complete(count)).to eq(expected_json) + end + end + + context 'when answer is provided' do + it 'generates JSON with the provided answer selected' do + count = 3 + answer = 2 + expected_json = { + dropdown_options: [ + { value: 1, selected: false }, + { value: 2, selected: true }, + { value: 3, selected: false } + ] + }.to_json + expect(dropdown.complete(count, answer)).to eq(expected_json) + end + end + + context 'when answer is not provided' do + it 'generates JSON without any answer selected' do + count = 3 + expected_json = { + dropdown_options: [ + { value: 1, selected: false }, + { value: 2, selected: false }, + { value: 3, selected: false } + ] + }.to_json + expect(dropdown.complete(count)).to eq(expected_json) + end + end + end + + describe '#complete_for_alternatives' do + let(:dropdown) { Dropdown.new } + context 'when given an array of alternatives and an answer' do + it 'returns JSON options with the selected alternative marked' do + alternatives = [1, 2, 3] + answer = 2 + expected_json = { + dropdown_options: [ + { value: 1, selected: false }, + { value: 2, selected: true }, + { value: 3, selected: false } + ] + }.to_json + expect(dropdown.complete_for_alternatives(alternatives, answer)).to eq(expected_json) + end + end + end +end From 84286fdee090218551954824e0d1d268421f2ad3 Mon Sep 17 00:00:00 2001 From: vishwagandhi1610 Date: Tue, 9 Apr 2024 00:16:44 +0000 Subject: [PATCH 09/12] added multiplechoice and rspec tests --- app/models/multiple_choice_checkbox.rb | 73 +++++++++++++++++++ app/models/quiz_question.rb | 30 ++++++++ app/models/quiz_question_choice.rb | 3 + ...0409000041_create_quiz_question_choices.rb | 11 +++ db/schema.rb | 10 ++- spec/models/multiple_choice_checkbox_spec.rb | 69 ++++++++++++++++++ 6 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 app/models/multiple_choice_checkbox.rb create mode 100644 app/models/quiz_question.rb create mode 100644 app/models/quiz_question_choice.rb create mode 100644 db/migrate/20240409000041_create_quiz_question_choices.rb create mode 100644 spec/models/multiple_choice_checkbox_spec.rb diff --git a/app/models/multiple_choice_checkbox.rb b/app/models/multiple_choice_checkbox.rb new file mode 100644 index 000000000..4d49d8382 --- /dev/null +++ b/app/models/multiple_choice_checkbox.rb @@ -0,0 +1,73 @@ +require 'json' + +class MultipleChoiceCheckbox < QuizQuestion + def edit + quiz_question_choices = QuizQuestionChoice.where(question_id: id) + + data = { + id: id, + question_text: txt, + weight: weight, + choices: quiz_question_choices.each_with_index.map do |choice, index| + { + id: choice.id, + text: choice.txt, + is_correct: choice.iscorrect, + position: index + 1 + } + end + } + + data.to_json + end + + def complete + quiz_question_choices = QuizQuestionChoice.where(question_id: id) + + data = { + id: id, + question_text: txt, + choices: quiz_question_choices.map do |choice| + { text: choice.txt } + end + } + + data.to_json + end + + def view_completed_question(user_answer) + quiz_question_choices = QuizQuestionChoice.where(question_id: id) + + data = { + question_choices: quiz_question_choices.map do |choice| + { + text: choice.txt, + is_correct: choice.iscorrect + } + end, + user_answers: user_answer.map do |answer| + { + is_correct: answer.answer == 1, + comments: answer.comments + } + end + } + + data.to_json + end + + def isvalid(choice_info) + error_message = nil + error_message = 'Please make sure all questions have text' if txt.blank? + + correct_count = choice_info.count { |_idx, value| value[:iscorrect] == '1' } + + if correct_count.zero? + error_message = 'Please select a correct answer for all questions' + elsif correct_count == 1 + error_message = 'A multiple-choice checkbox question should have more than one correct answer.' + end + + { valid: error_message.nil?, error: error_message }.to_json + end +end diff --git a/app/models/quiz_question.rb b/app/models/quiz_question.rb new file mode 100644 index 000000000..db55527d6 --- /dev/null +++ b/app/models/quiz_question.rb @@ -0,0 +1,30 @@ +require 'json' + +class QuizQuestion < Question + has_many :quiz_question_choices, class_name: 'QuizQuestionChoice', foreign_key: 'question_id', inverse_of: false, dependent: :nullify + + def edit + end + + def view_question_text + choices = quiz_question_choices.map do |choice| + { + text: choice.txt, + is_correct: choice.iscorrect? + } + end + + { + question_text: txt, + question_type: type, + question_weight: weight, + choices: choices + }.to_json + end + + def complete + end + + def view_completed_question(user_answer = nil) + end +end \ No newline at end of file diff --git a/app/models/quiz_question_choice.rb b/app/models/quiz_question_choice.rb new file mode 100644 index 000000000..af6106cd9 --- /dev/null +++ b/app/models/quiz_question_choice.rb @@ -0,0 +1,3 @@ +class QuizQuestionChoice < ApplicationRecord + belongs_to :question, dependent: :destroy +end \ No newline at end of file diff --git a/db/migrate/20240409000041_create_quiz_question_choices.rb b/db/migrate/20240409000041_create_quiz_question_choices.rb new file mode 100644 index 000000000..c1379f585 --- /dev/null +++ b/db/migrate/20240409000041_create_quiz_question_choices.rb @@ -0,0 +1,11 @@ +class CreateQuizQuestionChoices < ActiveRecord::Migration[7.0] + def change + create_table :quiz_question_choices, id: :integer, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=latin1" do |t| + t.integer :question_id + t.text :txt + t.boolean :iscorrect, default: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4d6cb5d47..2c3cd7321 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_03_15_004809) do +ActiveRecord::Schema[7.0].define(version: 2024_04_09_000041) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -193,6 +193,14 @@ t.index ["questionnaire_id"], name: "index_questions_on_questionnaire_id" end + create_table "quiz_question_choices", id: :integer, charset: "latin1", force: :cascade do |t| + t.integer "question_id" + t.text "txt" + t.boolean "iscorrect", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "response_maps", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.integer "reviewed_object_id", default: 0, null: false t.integer "reviewer_id", default: 0, null: false diff --git a/spec/models/multiple_choice_checkbox_spec.rb b/spec/models/multiple_choice_checkbox_spec.rb new file mode 100644 index 000000000..819dc05b6 --- /dev/null +++ b/spec/models/multiple_choice_checkbox_spec.rb @@ -0,0 +1,69 @@ +require 'rails_helper' + +RSpec.describe MultipleChoiceCheckbox, type: :model do + let(:multiple_choice_checkbox) { MultipleChoiceCheckbox.new(id: 1, txt: 'Test question:', weight: 1) } # Adjust as needed + + describe '#edit' do + it 'returns the JSON structure for editing' do + qc = instance_double('QuizQuestionChoice', iscorrect: true, txt: 'question text', id: 1) + allow(QuizQuestionChoice).to receive(:where).with(question_id: 1).and_return([qc, qc, qc, qc]) + + expected_structure = { + "id" => 1, + "question_text" => "Test question:", + "weight" => 1, + "choices" => [ + {"id" => 1, "text" => "question text", "is_correct" => true, "position" => 1}, + {"id" => 1, "text" => "question text", "is_correct" => true, "position" => 2}, + {"id" => 1, "text" => "question text", "is_correct" => true, "position" => 3}, + {"id" => 1, "text" => "question text", "is_correct" => true, "position" => 4} + ] + }.to_json + + expect(multiple_choice_checkbox.edit).to eq(expected_structure) + end + end + + describe '#isvalid' do + context 'when the question itself does not have txt' do + it 'returns a JSON with error message' do + allow(multiple_choice_checkbox).to receive_messages(txt: '', id: 1) + questions = { '1' => { txt: 'question text', iscorrect: '1' }, '2' => { txt: 'question text', iscorrect: '1' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } + expected_response = { valid: false, error: 'Please make sure all questions have text' }.to_json + expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) + end + end + + context 'when a choice does not have txt' do + it 'returns a JSON with error message' do + questions = { '1' => { txt: '', iscorrect: '1' }, '2' => { txt: '', iscorrect: '1' }, '3' => { txt: '', iscorrect: '0' }, '4' => { txt: '', iscorrect: '0' } } + expected_response = { valid: true, error: nil }.to_json + expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) + end + end + + context 'when no choices are correct' do + it 'returns a JSON with error message' do + questions = { '1' => { txt: 'question text', iscorrect: '0' }, '2' => { txt: 'question text', iscorrect: '0' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } + expected_response = { valid: false, error: 'Please select a correct answer for all questions' }.to_json + expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) + end + end + + context 'when only one choice is correct' do + it 'returns a JSON with error message' do + questions = { '1' => { txt: 'question text', iscorrect: '1' }, '2' => { txt: 'question text', iscorrect: '0' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } + expected_response = { valid: false, error: 'A multiple-choice checkbox question should have more than one correct answer.' }.to_json + expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) + end + end + + context 'when 2 choices are correct' do + it 'returns valid status' do + questions = { '1' => { txt: 'question text', iscorrect: '1' }, '2' => { txt: 'question text', iscorrect: '1' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } + expected_response = { valid: true, error: nil}.to_json + expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) + end + end + end +end From f8e7a6cf8a49527d5a6fcf60fa23c9e1e22a37f7 Mon Sep 17 00:00:00 2001 From: vishwagandhi1610 Date: Tue, 9 Apr 2024 00:44:52 +0000 Subject: [PATCH 10/12] added multiplechoiceradio model and spec tests --- app/models/multiple_choice_radio.rb | 88 ++++++++++++++++++++++ spec/models/multiple_choice_radio_spec.rb | 92 +++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 app/models/multiple_choice_radio.rb create mode 100644 spec/models/multiple_choice_radio_spec.rb diff --git a/app/models/multiple_choice_radio.rb b/app/models/multiple_choice_radio.rb new file mode 100644 index 000000000..032abe1a9 --- /dev/null +++ b/app/models/multiple_choice_radio.rb @@ -0,0 +1,88 @@ +require 'json' + +class MultipleChoiceRadio < QuizQuestion + def edit + quiz_question_choices = QuizQuestionChoice.where(question_id: id) + + choices = quiz_question_choices.map.with_index(1) do |choice, index| + { + id: choice.id, + text: choice.txt, + is_correct: choice.iscorrect, + position: index + } + end + + { + id: id, + question_text: txt, + question_weight: weight, + choices: choices + }.to_json + end + + def complete + quiz_question_choices = QuizQuestionChoice.where(question_id: id) + + choices = quiz_question_choices.map.with_index(1) do |choice, index| + { + id: choice.id, + text: choice.txt, + position: index + } + end + + { + question_id: id, + question_text: txt, + choices: choices + }.to_json + end + + def view_completed_question(user_answer) + quiz_question_choices = QuizQuestionChoice.where(question_id: id) + + choices = quiz_question_choices.map do |choice| + { + text: choice.txt, + is_correct: choice.iscorrect + } + end + + user_response = { + answer: user_answer.first.comments, + is_correct: user_answer.first.answer == 1 + } + + { + question_text: txt, + choices: choices, + user_response: user_response + }.to_json + end + + def isvalid(choice_info) + valid = true + error_message = nil + + if txt.blank? + valid = false + error_message = 'Please make sure all questions have text' + elsif choice_info.values.any? { |choice| choice[:txt].blank? } + valid = false + error_message = 'Please make sure every question has text for all options' + end + + correct_count = choice_info.count { |_idx, choice| choice[:iscorrect] == '1' } + + if correct_count != 1 + valid = false + error_message = 'Please select exactly one correct answer for the question' + end + + { + valid: valid, + error: error_message + }.to_json + end +end diff --git a/spec/models/multiple_choice_radio_spec.rb b/spec/models/multiple_choice_radio_spec.rb new file mode 100644 index 000000000..ae6c7b7d5 --- /dev/null +++ b/spec/models/multiple_choice_radio_spec.rb @@ -0,0 +1,92 @@ +require 'rails_helper' + +RSpec.describe MultipleChoiceRadio, type: :model do + let(:multiple_choice_radio) { MultipleChoiceRadio.new(id: 1, txt: 'Test question', weight: 1) } + let(:quiz_question_choices) do + [ + instance_double('QuizQuestionChoice', id: 1, txt: 'Choice 1', iscorrect: true), + instance_double('QuizQuestionChoice', id: 2, txt: 'Choice 2', iscorrect: false) + ] + end + + before do + allow(QuizQuestionChoice).to receive(:where).with(question_id: 1).and_return(quiz_question_choices) + end + + describe '#edit' do + context 'when editing a quiz question' do + it 'returns JSON for the question edit form' do + expected_json = { + id: 1, + question_text: 'Test question', + question_weight: 1, + choices: [ + {id: 1, text: 'Choice 1', is_correct: true, position: 1}, + {id: 2, text: 'Choice 2', is_correct: false, position: 2} + ] + }.to_json + + expect(multiple_choice_radio.edit).to eq(expected_json) + end + end + end + + describe '#complete' do + context 'when given a valid question id' do + it 'returns JSON for a quiz question with choices' do + expected_json = { + question_id: 1, + question_text: 'Test question', + choices: [ + {id: 1, text: 'Choice 1', position: 1}, + {id: 2, text: 'Choice 2', position: 2} + ] + }.to_json + + expect(multiple_choice_radio.complete).to eq(expected_json) + end + end + end + + describe "#view_completed_question" do + let(:user_answer) { [instance_double('UserAnswer', answer: 1, comments: 'Choice 1')] } + + context "when user answer is correct" do + it "includes correctness in the response" do + expected_json = { + question_text: 'Test question', + choices: [ + {text: 'Choice 1', is_correct: true}, + {text: 'Choice 2', is_correct: false} + ], + user_response: {answer: 'Choice 1', is_correct: true} + }.to_json + + expect(multiple_choice_radio.view_completed_question(user_answer)).to eq(expected_json) + end + end + end + + describe '#isvalid' do + context 'when choice_info is valid' do + it 'returns valid status' do + choice_info = { + '0' => { txt: 'Choice 1', iscorrect: '0' }, + '1' => { txt: 'Choice 2', iscorrect: '1' } + } + expected_response = { valid: true, error: nil }.to_json + + expect(multiple_choice_radio.isvalid(choice_info)).to eq(expected_response) + end + end + + context 'when choice_info has empty text for an option' do + it 'returns an error message' do + choice_info = {'0' => {txt: '', iscorrect: '1'}, '1' => {txt: 'Choice 2', iscorrect: '0'}} + expected_response = {valid: false, error: 'Please make sure every question has text for all options'}.to_json + + expect(multiple_choice_radio.isvalid(choice_info)).to eq(expected_response) + end + end + end +end From 88e6c22d7970224e540bab12c31487043ec8caef Mon Sep 17 00:00:00 2001 From: jshah22 Date: Mon, 8 Apr 2024 21:21:12 -0400 Subject: [PATCH 11/12] comments and code updated --- app/helpers/question_helper.rb | 29 +-- app/models/checkbox.rb | 167 ++++++++++-------- app/models/criterion.rb | 85 +++++---- app/models/dropdown.rb | 19 +- app/models/multiple_choice_checkbox.rb | 41 ++++- app/models/question_advice.rb | 15 +- app/models/quiz_question.rb | 25 ++- app/models/quiz_question_choice.rb | 6 +- app/models/scale.rb | 16 +- app/models/text_area.rb | 36 ++-- app/models/text_field.rb | 50 +++--- app/models/text_response.rb | 80 +++++---- app/models/unscored_question.rb | 10 +- app/models/upload_file.rb | 81 ++++----- .../20240315004809_create_question_advices.rb | 10 ++ ...0409000041_create_quiz_question_choices.rb | 11 +- spec/models/checkbox_spec.rb | 33 +++- spec/models/criterion_spec.rb | 65 ++++--- spec/models/dropdown_spec.rb | 42 ++++- spec/models/multiple_choice_checkbox_spec.rb | 28 ++- spec/models/scale_spec.rb | 23 ++- spec/models/text_area_spec.rb | 6 + spec/models/text_field_spec.rb | 9 +- spec/models/text_response_spec.rb | 10 +- spec/models/upload_file_spec.rb | 16 +- 25 files changed, 593 insertions(+), 320 deletions(-) diff --git a/app/helpers/question_helper.rb b/app/helpers/question_helper.rb index f26a9fd5c..f957490d4 100644 --- a/app/helpers/question_helper.rb +++ b/app/helpers/question_helper.rb @@ -1,19 +1,26 @@ +# Module QuestionHelper provides common functionalities for question-related operations module QuestionHelper - def edit_common(label = nil,min_question_score = nil, max_question_score = nil, txt ,weight, type) + def edit_common(label, input_value, min_question_score, max_question_score, weight, type) { - form: true, - label: label, - input_type: 'text', - input_name: 'question', - input_value: txt, - min_question_score: min_question_score, - max_question_score: max_question_score, - weight: weight, - type: type + form: true, # Indicates the presence of a form + label: label, # Label for the question input field + input_type: 'text', # Type of the input field (text) + input_name: 'question', # Name attribute for the input field + input_value: input_value, # Current value for the input field + min_question_score: min_question_score, # Minimum score for the question + max_question_score: max_question_score, # Maximum score for the question + weight: weight, # Weight/importance of the question + type: type # Type of the question } end def view_question_text_common(text, type, weight, score_range) - { text: text, type: type, weight: weight, score_range: score_range } + { + text: text, # Text of the question + type: type, # Type of the question + weight: weight, # Weight/importance of the question + score_range: score_range # Score range for the question + } end + end \ No newline at end of file diff --git a/app/models/checkbox.rb b/app/models/checkbox.rb index 50b7e3b74..1271bef95 100644 --- a/app/models/checkbox.rb +++ b/app/models/checkbox.rb @@ -1,172 +1,189 @@ +# Inherits from UnscoredQuestion, defining behaviors specific to checkbox questions class Checkbox < UnscoredQuestion + + # Builds the edit view structure for a checkbox question def edit(count) { - remove_button: edit_remove_button(count), - seq: edit_seq(count), - question: edit_question(count), - type: edit_type(count), - weight: edit_weight(count) + remove_button: edit_remove_button(count), # Structure for the remove button + seq: edit_seq(count), # Structure for question sequence input + question: edit_question(count), # Structure for editing the question content + type: edit_type(count), # Structure for displaying the question type + weight: edit_weight(count) # Structure for displaying the weight (not applicable for UnscoredQuestion) } end + # Defines the remove button component in the edit view def edit_remove_button(count) { - type: 'remove_button', - action: 'delete', - href: "/questions/#{id}", - text: 'Remove' + type: 'remove_button', # Component type + action: 'delete', # Action associated with the button + href: "/questions/#{id}", # URL for the delete action + text: 'Remove' # Button text } end + # Defines the sequence input component in the edit view def edit_seq(count) { - type: 'seq', - input_size: 6, - value: seq, - name: "question[#{id}][seq]", - id: "question_#{id}_seq" + type: 'seq', # Component type + input_size: 6, # Size attribute of the input + value: seq, # Current sequence value + name: "question[#{id}][seq]", # Input name + id: "question_#{id}_seq" # Input id } end + # Defines the question content editing component in the edit view def edit_question(count) { - type: 'textarea', - cols: 50, - rows: 1, - name: "question[#{id}][txt]", - id: "question_#{id}_txt", - placeholder: 'Edit question content here', - content: txt + type: 'textarea', # Component type + cols: 50, # Columns attribute of the textarea + rows: 1, # Rows attribute of the textarea + name: "question[#{id}][txt]", # Textarea name + id: "question_#{id}_txt", # Textarea id + placeholder: 'Edit question content here', # Placeholder text + content: txt # Current question content } end + # Defines the question type display component in the edit view def edit_type(count) { - type: 'text', - input_size: 10, - disabled: true, - value: question_type, - name: "question[#{id}][type]", - id: "question_#{id}_type" + type: 'text', # Component type + input_size: 10, # Size attribute of the input + disabled: true, # Disabled attribute to make it read-only + value: question_type, # Current question type + name: "question[#{id}][type]", # Input name + id: "question_#{id}_type" # Input id } end + # Defines the weight display component in the edit view (not applicable for UnscoredQuestion) def edit_weight(count) { - type: 'weight', - placeholder: 'UnscoredQuestion does not need weight' + type: 'weight', # Component type + placeholder: 'UnscoredQuestion does not need weight' # Placeholder text indicating weight is not applicable } end - + # Builds the structure to view question text and related details def view_question_text { - content: txt, - type: question_type, - weight: weight.to_s, - checked_state: 'Checked/Unchecked' + content: txt, # Question content + type: question_type, # Question type + weight: weight.to_s, # Question weight as a string + checked_state: 'Checked/Unchecked' # Placeholder for checked state description } end + # Constructs the completion structure for a checkbox question, including inputs and scripts def complete(count, answer = nil) { - previous_question: check_previous_question, - inputs: [ - complete_first_second_input(count, answer), - complete_third_input(count, answer) + previous_question: check_previous_question, # Checks and indicates if the previous question is a column header + inputs: [ # Array of input structures for the question + complete_first_second_input(count, answer), # Hidden inputs for comments and score + complete_third_input(count, answer) # Checkbox input for the actual question response ], - label: { - for: "responses_#{count}", - text: txt + label: { # Label structure for the checkbox + for: "responses_#{count}", # Associated input id + text: txt # Question text as the label }, - script: complete_script(count), - if_column_header: complete_if_column_header + script: complete_script(count), # Script to handle checkbox change events + if_column_header: complete_if_column_header # Indicates if the next question is a column header or other special type } end + # Checks if the previous question is a column header and returns the appropriate structure def check_previous_question - prev_question = Question.where('seq < ?', seq).order(:seq).last + prev_question = Question.where('seq < ?', seq).order(:seq).last # Finds the last question before this one by sequence { - type: prev_question&.type == 'ColumnHeader' ? 'ColumnHeader' : 'other' + type: prev_question&.type == 'ColumnHeader' ? 'ColumnHeader' : 'other' # Indicates if the previous question is a column header } end + # Hidden inputs for comments and score, used in the complete structure def complete_first_second_input(count, answer = nil) [ { - id: "responses_#{count}_comments", - name: "responses[#{count}][comment]", - type: 'hidden', - value: '' + id: "responses_#{count}_comments", # Input id for comments + name: "responses[#{count}][comment]", # Input name for comments + type: 'hidden', # Input type hidden + value: '' # Default value }, { - id: "responses_#{count}_score", - name: "responses[#{count}][score]", - type: 'hidden', - value: answer&.answer == 1 ? '1' : '0' + id: "responses_#{count}_score", # Input id for score + name: "responses[#{count}][score]", # Input name for score + type: 'hidden', # Input type hidden + value: answer&.answer == 1 ? '1' : '0' # Value based on answer } ] end + # Checkbox input for the actual question response in the complete structure def complete_third_input(count, answer = nil) { - id: "responses_#{count}_checkbox", - type: 'checkbox', - onchange: "checkbox#{count}Changed()", - checked: answer&.answer == 1 + id: "responses_#{count}_checkbox", # Checkbox id + type: 'checkbox', # Input type checkbox + onchange: "checkbox#{count}Changed()", # Onchange script function + checked: answer&.answer == 1 # Checked state based on answer } end + # JavaScript function to update the hidden score input based on checkbox state def complete_script(count) "function checkbox#{count}Changed() { var checkbox = jQuery('#responses_#{count}_checkbox'); var response_score = jQuery('#responses_#{count}_score'); if (checkbox.is(':checked')) { response_score.val('1'); } else { response_score.val('0'); }}" end + # Determines the flow after this question based on the type of the next question def complete_if_column_header - next_question = Question.where('seq > ?', seq).order(:seq).first + next_question = Question.where('seq > ?', seq).order(:seq).first # Finds the first question after this one by sequence if next_question case next_question.question_type - when 'ColumnHeader' + when 'ColumnHeader' # Next question is a column header 'end_of_column_header' - when 'SectionHeader', 'TableHeader' + when 'SectionHeader', 'TableHeader' # Next question is a section or table header 'end_of_section_or_table' - else + else # Continues with more questions of standard types 'continue' end - else + else # This is the last question 'end' end end + # Builds the structure for viewing a completed question, including previous question check and answer details def view_completed_question(count, answer) { - previous_question: check_previous_question, - answer: view_completed_question_answer(count, answer), - if_column_header: view_completed_question_if_column_header + previous_question: check_previous_question, # Checks and indicates if the previous question is a column header + answer: view_completed_question_answer(count, answer), # Structure for displaying the given answer + if_column_header: view_completed_question_if_column_header # Indicates if the next question is a column header or other special type } end + # Structure for displaying the given answer in the completed question view def view_completed_question_answer(count, answer) { - number: count, - image: answer.answer == 1 ? 'Check-icon.png' : 'delete_icon.png', - content: txt, - bold: true + number: count, # Question number + image: answer.answer == 1 ? 'Check-icon.png' : 'delete_icon.png', # Icon based on answer + content: txt, # Question content + bold: true # Bold formatting for display } end + # Determines the flow after this question in the completed question view based on the type of the next question def view_completed_question_if_column_header - next_question = Question.where('seq > ?', seq).order(:seq).first + next_question = Question.where('seq > ?', seq).order(:seq).first # Finds the first question after this one by sequence if next_question case next_question.question_type - when 'ColumnHeader' + when 'ColumnHeader' # Next question is a column header 'end_of_column_header' - when 'TableHeader' + when 'TableHeader' # Next question is a table header 'end_of_table_header' - else + else # Continues with more questions of standard types 'continue' end - else + else # This is the last question 'end' end end -end +end \ No newline at end of file diff --git a/app/models/criterion.rb b/app/models/criterion.rb index eb22880fa..8e9f7b7ac 100644 --- a/app/models/criterion.rb +++ b/app/models/criterion.rb @@ -1,81 +1,100 @@ +# Inherits from ScoredQuestion, representing a question type with a scoring criterion class Criterion < ScoredQuestion + # Ensure the presence of the size attribute for a Criterion instance validates :size, presence: true + # Method to construct the structure needed for editing a criterion question def edit { - remove_link: "/questions/#{id}", - sequence_input: seq.to_s, - question_text: txt, - question_type: question_type, - weight: weight.to_s, - size: size.to_s, - max_label: max_label, - min_label: min_label + remove_link: "/questions/#{id}", # Link to remove the question + sequence_input: seq.to_s, # The sequence number of the question as a string + question_text: txt, # The text content of the question + question_type: question_type, # The type of the question, e.g., 'Criterion' + weight: weight.to_s, # The weight of the question as a string + size: size.to_s, # The size attribute of the criterion question as a string + max_label: max_label, # The label for the maximum score + min_label: min_label # The label for the minimum score } end + # Method to construct the structure needed for viewing the text of a criterion question def view_question_text question_data = { - text: txt, - question_type: question_type, - weight: weight, - score_range: "#{questionnaire.min_question_score} to #{questionnaire.max_question_score}" + text: txt, # The text content of the question + question_type: question_type, # The type of the question + weight: weight, # The weight of the question + score_range: "#{questionnaire.min_question_score} to #{questionnaire.max_question_score}" # The score range from the associated questionnaire } - question_data[:score_range] = "(#{min_label}) " + question_data[:score_range] + " (#{max_label})" if max_label && min_label + # Append min and max labels to the score range if they are present + if max_label && min_label + question_data[:score_range] = "(#{min_label}) " + question_data[:score_range] + " (#{max_label})" + end + question_data end - def complete(count,answer = nil, questionnaire_min, questionnaire_max, dropdown_or_scale) + # Method to construct the response structure for a criterion question + def complete(count, answer = nil, questionnaire_min, questionnaire_max, dropdown_or_scale) + # Retrieve advices for the question and calculate their total length question_advices = QuestionAdvice.to_json_by_question_id(id) advice_total_length = question_advices.sum { |advice| advice.advice.length unless advice.advice.blank? } + # Determine the response options based on whether it's a dropdown or scale type response_options = if dropdown_or_scale == 'dropdown' dropdown_criterion_question(count, answer, questionnaire_min, questionnaire_max) elsif dropdown_or_scale == 'scale' scale_criterion_question(count, answer, questionnaire_min, questionnaire_max) end + # Construct the advice section if there are any advices advice_section = question_advices.empty? || advice_total_length.zero? ? nil : advices_criterion_question(count, question_advices) + # Construct and return the final structure, removing any nil values with .compact { - label: txt, - advice: advice_section, - response_options: response_options - }.compact # Use .compact to remove nil values + label: txt, # The text of the question + advice: advice_section, # The advice section, if applicable + response_options: response_options # The response options for the question + }.compact end - # Assuming now these methods should be public based on the test cases - def dropdown_criterion_question(count,answer, questionnaire_min, questionnaire_max) + # Method to generate dropdown options for a criterion question + def dropdown_criterion_question(count, answer, questionnaire_min, questionnaire_max) + # Generate a list of options based on the min and max scores from the questionnaire options = (questionnaire_min..questionnaire_max).map do |score| - option = { value: score, label: score.to_s } - option[:selected] = 'selected' if answer && score == answer.answer + option = { value: score, label: score.to_s } # Each option has a value and label + option[:selected] = 'selected' if answer && score == answer.answer # Mark the option as selected if it matches the answer option end + # Return the structure for a dropdown criterion question { type: 'dropdown', options: options, current_answer: answer.try(:answer), comments: answer.try(:comments) } end - def scale_criterion_question(count,answer, questionnaire_min, questionnaire_max) + # Method to generate scale options for a criterion question + def scale_criterion_question(count, answer, questionnaire_min, questionnaire_max) + # Return the structure for a scale criterion question { type: 'scale', - min: questionnaire_min, - max: questionnaire_max, - current_answer: answer.try(:answer), - comments: answer.try(:comments), - min_label: min_label, - max_label: max_label, - size: size + min: questionnaire_min, # Minimum score + max: questionnaire_max, # Maximum score + current_answer: answer.try(:answer), # Current answer if available + comments: answer.try(:comments), # Any comments on the answer + min_label: min_label, # Label for the minimum score + max_label: max_label, # Label for the maximum score + size: size # Size of the scale } end private + # Method to structure advices for a criterion question def advices_criterion_question(question_advices) + # Map each advice to a structure containing its score and advice content question_advices.map do |advice| { - score: advice.score, - advice: advice.advice + score: advice.score, # The score associated with the advice + advice: advice.advice # The advice content } end end -end +end \ No newline at end of file diff --git a/app/models/dropdown.rb b/app/models/dropdown.rb index 0fbb6195d..4295b6e18 100644 --- a/app/models/dropdown.rb +++ b/app/models/dropdown.rb @@ -1,26 +1,43 @@ +# Inherits from UnscoredQuestion and includes QuestionHelper to utilize common functionality class Dropdown < UnscoredQuestion include QuestionHelper + # Define accessible attributes for instances of Dropdown attr_accessor :txt, :type, :count, :weight + + # Method to prepare and return the JSON structure for editing a dropdown question def edit(count) - edit_common("Question #{count}:", txt , weight, type).to_json + # Utilizes edit_common method from QuestionHelper with specified parameters + # and converts the result to JSON format + edit_common("Question #{count}:", txt, weight, type).to_json end + # Method to view the text and details of a dropdown question def view_question_text + # Utilizes view_question_text_common method from QuestionHelper with specified parameters + # and converts the result to JSON format. 'N/A' signifies that scoring is not applicable view_question_text_common(txt, type, weight, 'N/A').to_json end + # Method to prepare the dropdown options for completing a question, marking the selected option def complete(count, answer = nil) + # Generates a list of options based on the count, marking the selected option based on the answer options = (1..count).map { |option| { value: option, selected: (option == answer.to_i) } } + # Returns the dropdown options in JSON format { dropdown_options: options }.to_json end + # Similar to the complete method but uses predefined alternatives instead of a numeric range def complete_for_alternatives(alternatives, answer) + # Generates a list of options from the alternatives, marking the selected option based on the answer options = alternatives.map { |alt| { value: alt, selected: (alt == answer) } } + # Returns the dropdown options in JSON format { dropdown_options: options }.to_json end + # Method to display the selected option of a completed question def view_completed_question + # Constructs a response indicating the selected option or that the question was not answered { selected_option: (count && answer) ? "#{answer} (out of #{count})" : 'Question not answered.' }.to_json end end \ No newline at end of file diff --git a/app/models/multiple_choice_checkbox.rb b/app/models/multiple_choice_checkbox.rb index 4d49d8382..a3d73801e 100644 --- a/app/models/multiple_choice_checkbox.rb +++ b/app/models/multiple_choice_checkbox.rb @@ -1,51 +1,68 @@ require 'json' +# Define a class for multiple-choice checkbox questions, inheriting from QuizQuestion class MultipleChoiceCheckbox < QuizQuestion + + # Method to prepare data for editing a question def edit + # Fetch choices associated with this question quiz_question_choices = QuizQuestionChoice.where(question_id: id) + # Construct a hash with question data and its choices data = { - id: id, - question_text: txt, - weight: weight, + id: id, # Question ID + question_text: txt, # Question text + weight: weight, # Question weight choices: quiz_question_choices.each_with_index.map do |choice, index| + # Map each choice to a hash with choice details { - id: choice.id, - text: choice.txt, - is_correct: choice.iscorrect, - position: index + 1 + id: choice.id, # Choice ID + text: choice.txt, # Choice text + is_correct: choice.iscorrect, # Indicates if the choice is correct + position: index + 1 # Position of the choice } end } + # Convert the hash to a JSON string data.to_json end + # Method to prepare data for completing a question def complete + # Fetch choices associated with this question quiz_question_choices = QuizQuestionChoice.where(question_id: id) + # Construct a hash with question data and its choices (without correct answers) data = { - id: id, - question_text: txt, + id: id, # Question ID + question_text: txt, # Question text choices: quiz_question_choices.map do |choice| + # Map each choice to a hash with only the choice text { text: choice.txt } end } + # Convert the hash to a JSON string data.to_json end + # Method to display a completed question along with user answers def view_completed_question(user_answer) + # Fetch choices associated with this question quiz_question_choices = QuizQuestionChoice.where(question_id: id) + # Construct a hash with the choices and user answers data = { question_choices: quiz_question_choices.map do |choice| + # Map each choice to a hash with choice details and correct status { text: choice.txt, is_correct: choice.iscorrect } end, user_answers: user_answer.map do |answer| + # Map each user answer to a hash with its correctness and comments { is_correct: answer.answer == 1, comments: answer.comments @@ -53,21 +70,27 @@ def view_completed_question(user_answer) end } + # Convert the hash to a JSON string data.to_json end + # Method to validate a choice selection for the question def isvalid(choice_info) error_message = nil + # Ensure the question text is not blank error_message = 'Please make sure all questions have text' if txt.blank? + # Count the number of correct answers correct_count = choice_info.count { |_idx, value| value[:iscorrect] == '1' } + # Set error messages based on the number of correct answers if correct_count.zero? error_message = 'Please select a correct answer for all questions' elsif correct_count == 1 error_message = 'A multiple-choice checkbox question should have more than one correct answer.' end + # Return a hash indicating whether the choices are valid and any error message { valid: error_message.nil?, error: error_message }.to_json end end diff --git a/app/models/question_advice.rb b/app/models/question_advice.rb index e4cc5bdd3..cb4b30a46 100644 --- a/app/models/question_advice.rb +++ b/app/models/question_advice.rb @@ -1,22 +1,35 @@ +# Inherits from ApplicationRecord, representing advices associated with questions in a questionnaire class QuestionAdvice < ApplicationRecord + # Establishes a belongs_to association with the Question model belongs_to :question + + # Class method to define the fields to be exported for QuestionAdvice records def self.export_fields(_options) + # Retrieves and maps the column names of the QuestionAdvice table QuestionAdvice.columns.map(&:name) end + # Class method to export QuestionAdvice records related to a specific questionnaire to CSV def self.export(csv, parent_id, _options) + # Finds the Questionnaire by the given parent_id questionnaire = Questionnaire.find(parent_id) + # Iterates over each question in the questionnaire questionnaire.questions.each do |question| + # Fetches and iterates over each piece of advice for the current question QuestionAdvice.where(question_id: question.id).each do |advice| + # Adds the advice attributes' values to the CSV csv << advice.attributes.values end end end + # Class method to convert advices related to a specific question into JSON format def self.to_json_by_question_id(question_id) + # Retrieves QuestionAdvice records for a specific question, ordered by id question_advices = QuestionAdvice.where(question_id: question_id).order(:id) + # Maps each advice to a hash with its score and advice content question_advices.map do |advice| { score: advice.score, advice: advice.advice } end end -end +end \ No newline at end of file diff --git a/app/models/quiz_question.rb b/app/models/quiz_question.rb index db55527d6..da6faeab9 100644 --- a/app/models/quiz_question.rb +++ b/app/models/quiz_question.rb @@ -1,30 +1,39 @@ require 'json' +# Define a base class for quiz questions, inheriting from Question class QuizQuestion < Question + # Establish a one-to-many relationship with quiz question choices + # Nullify the foreign key on dependent object destruction has_many :quiz_question_choices, class_name: 'QuizQuestionChoice', foreign_key: 'question_id', inverse_of: false, dependent: :nullify + # Method stub for editing a question def edit end + # Method to view question text along with choices and their correctness def view_question_text + # Map each choice to a hash with text and correctness choices = quiz_question_choices.map do |choice| { - text: choice.txt, - is_correct: choice.iscorrect? + text: choice.txt, # Text of the choice + is_correct: choice.iscorrect? # Correctness of the choice } end + # Construct a hash with question details and choices { - question_text: txt, - question_type: type, - question_weight: weight, - choices: choices - }.to_json + question_text: txt, # Text of the question + question_type: type, # Type of the question + question_weight: weight, # Weight/importance of the question + choices: choices # Choices for the question + }.to_json # Convert the hash to a JSON string end + # Method stub for completing a question def complete end + # Method stub for viewing a completed question with user answers def view_completed_question(user_answer = nil) end -end \ No newline at end of file +end diff --git a/app/models/quiz_question_choice.rb b/app/models/quiz_question_choice.rb index af6106cd9..34adc767e 100644 --- a/app/models/quiz_question_choice.rb +++ b/app/models/quiz_question_choice.rb @@ -1,3 +1,7 @@ +# Define a class for quiz question choices, inheriting from ApplicationRecord class QuizQuestionChoice < ApplicationRecord + # Establish a belongs-to relationship with a quiz question + # Specifies that the quiz question choice is dependent on the question, + # meaning if the question is destroyed, this choice will also be destroyed belongs_to :question, dependent: :destroy -end \ No newline at end of file +end diff --git a/app/models/scale.rb b/app/models/scale.rb index be6343769..334ea9ab2 100644 --- a/app/models/scale.rb +++ b/app/models/scale.rb @@ -1,34 +1,48 @@ +# Inherits from ScoredQuestion and includes QuestionHelper for shared functionalities class Scale < ScoredQuestion include QuestionHelper + # Define accessible attributes for instances of Scale attr_accessor :txt, :type, :weight, :min_label, :max_label, :answer, :min_question_score, :max_question_score + # Prepares and returns the JSON structure for editing a scale question def edit - edit_common('Question:', min_question_score, max_question_score , txt, weight, type).to_json + # Calls edit_common from QuestionHelper with question parameters and converts to JSON + edit_common('Question:', min_question_score, max_question_score, txt, weight, type).to_json end + # Generates the JSON structure for viewing the question text and details def view_question_text + # Calls view_question_text_common from QuestionHelper with question parameters, including the score range view_question_text_common(txt, type, weight, score_range).to_json end + # Prepares the scale options for question completion and marks the selected option def complete + # Generates a list of options from min to max score, marking the selected option options = (@min_question_score..@max_question_score).map do |option| { value: option, selected: (option == answer) } end + # Returns the scale options in JSON format { scale_options: options }.to_json end + # Displays the selected option for a completed question or indicates if unanswered def view_completed_question(options = {}) + # If sufficient data is provided in options, construct a response with count, answer, and max questionnaire score if options[:count] && options[:answer] && options[:questionnaire_max] { count: options[:count], answer: options[:answer], questionnaire_max: options[:questionnaire_max] }.to_json else + # If data is insufficient, return a message indicating the question was not answered { message: 'Question not answered.' }.to_json end end private + # Helper method to construct the score range string, incorporating labels if provided def score_range + # If min and max labels are not provided, use just the score numbers; otherwise, include the labels in the format min_label.nil? && max_label.nil? ? "#{@min_question_score} to #{@max_question_score}" : "#{min_label} #{@min_question_score} to #{@max_question_score} #{max_label}" end diff --git a/app/models/text_area.rb b/app/models/text_area.rb index 14dd7d286..50d0a5633 100644 --- a/app/models/text_area.rb +++ b/app/models/text_area.rb @@ -1,22 +1,32 @@ +# Inherits from Question to define specific behaviors for text area questions class TextArea < Question - def complete(count,answer = nil) + + # Method to construct the completion structure for a text area question + # @param count [Integer] The sequence number of the question + # @param answer [Answer, nil] The answer object associated with the question, may be nil if not answered + # @return [String] JSON representation of the completion structure + def complete(count, answer = nil) { - action: 'complete', - data: { - count: count, - comment: answer&.comments, - size: size || '70,1', # Assuming '70,1' is the default size + action: 'complete', # Indicates the action type + data: { # Data related to the text area question + count: count, # The sequence number of the question + comment: answer&.comments, # The comment from the answer, uses safe navigation operator to handle nil + size: size || '70,1', # The size of the text area, defaults to '70,1' if size is nil } - }.to_json + }.to_json # Converts the hash to JSON format end + # Method to construct the structure for viewing a completed text area question + # @param count [Integer] The sequence number of the question + # @param answer [Answer] The answer object associated with the question + # @return [String] JSON representation of the completed question view structure def view_completed_question(count, answer) { - action: 'view_completed_question', - data: { - count: count, - comment: answer.comments + action: 'view_completed_question', # Indicates the action type + data: { # Data related to the completed text area question + count: count, # The sequence number of the question + comment: answer.comments # The comment from the answer } - }.to_json + }.to_json # Converts the hash to JSON format end -end +end \ No newline at end of file diff --git a/app/models/text_field.rb b/app/models/text_field.rb index fac966d77..063f2d6ea 100644 --- a/app/models/text_field.rb +++ b/app/models/text_field.rb @@ -1,40 +1,30 @@ +# Inherits from Question to define specific behaviors for text field questions class TextField < Question + # Validates the presence of the size attribute validates :size, presence: true def complete(count, answer = nil) { - action: 'complete', - data: { - label: "Question ##{count}", - type: 'text', - name: "response[answers][#{id}]", - id: "responses_#{id}", - value: answer&.comments + action: 'complete', # Indicates the action type + data: { # Data related to the text field question + label: "Question ##{count}", # Label for the question, including its sequence number + type: 'text', # Type of input (text field) + name: "response[answers][#{id}]", # Name attribute for form submission + id: "responses_#{id}", # ID attribute for HTML element + value: answer&.comments # Value of the text field, uses safe navigation operator to handle nil } - }.to_json + }.to_json # Converts the hash to JSON format end def view_completed_question(count, files) - if question_type == 'TextField' && break_before - { - action: 'view_completed_question', - data: { - type: 'text', - label: "Completed Question ##{count}", - value: txt, - break_before: break_before - } - }.to_json - else - { - action: 'view_completed_question', - data: { - type: 'text', - label: "Completed Question ##{count}", - value: txt, - break_before: break_before - } - }.to_json - end + { + action: 'view_completed_question', # Indicates the action type + data: { # Data related to the completed text field question + type: 'text', # Type of input (text field) + label: "Completed Question ##{count}", # Label for the completed question, including its sequence number + value: txt, # Value of the completed text field + break_before: break_before # Flag indicating whether to break before this question in the view + } + }.to_json # Converts the hash to JSON format end -end +end \ No newline at end of file diff --git a/app/models/text_response.rb b/app/models/text_response.rb index a26912efd..3f7289480 100644 --- a/app/models/text_response.rb +++ b/app/models/text_response.rb @@ -1,64 +1,66 @@ +# Inherits from Question to define specific behaviors for text response questions class TextResponse < Question + # Validates the presence of the size attribute validates :size, presence: true def edit(_count) { - action: 'edit', - elements: [ + action: 'edit', # Indicates the action type + elements: [ # Array of elements comprising the edit form { - type: 'link', - text: 'Remove', - href: "/questions/#{id}", - method: 'delete' + type: 'link', # Type of element (link) + text: 'Remove', # Text for the link + href: "/questions/#{id}", # URL for the link + method: 'delete' # HTTP method for the link }, { - type: 'input', - input_type: 'text', + type: 'input', # Type of element (input) + input_type: 'text', # Type of input (text) size: 6, - name: "question[#{id}][seq]", - id: "question_#{id}_seq", - value: seq.to_s + name: "question[#{id}][seq]", # Name attribute for form submission + id: "question_#{id}_seq", # ID attribute for HTML element + value: seq.to_s # Value of the input field, converted to string }, { - type: 'textarea', - cols: 50, - rows: 1, - name: "question[#{id}][txt]", - id: "question_#{id}_txt", - placeholder: 'Edit question content here', - value: txt + type: 'textarea', # Type of element (textarea) + cols: 50, # Number of columns for the textarea + rows: 1, # Number of rows for the textarea + name: "question[#{id}][txt]", # Name attribute for form submission + id: "question_#{id}_txt", # ID attribute for HTML element + placeholder: 'Edit question content here', # Placeholder text for the textarea + value: txt # Value of the textarea }, { - type: 'input', - input_type: 'text', + type: 'input', # Type of element (input) + input_type: 'text', # Type of input (text) size: 10, - name: "question[#{id}][question_type]", - id: "question_#{id}_question_type", - value: question_type, - disabled: true + name: "question[#{id}][question_type]", # Name attribute for form submission + id: "question_#{id}_question_type", # ID attribute for HTML element + value: question_type, # Value of the input field + disabled: true # Indicates whether the input is disabled }, { - type: 'input', - input_type: 'text', + type: 'input', # Type of element (input) + input_type: 'text', # Type of input (text) size: 6, - name: "question[#{id}][size]", - id: "question_#{id}_size", - value: size, - label: 'Text area size' + name: "question[#{id}][size]", # Name attribute for form submission + id: "question_#{id}_size", # ID attribute for HTML element + value: size, # Value of the input field + label: 'Text area size' # Label for the input field } ] - }.to_json + }.to_json # Converts the hash to JSON format end def view_question_text { - action: 'view_question_text', - elements: [ - { type: 'text', value: txt }, - { type: 'text', value: question_type }, - { type: 'text', value: weight.to_s }, - { type: 'text', value: '—' } + action: 'view_question_text', # Indicates the action type + elements: [ # Array of elements comprising the question view + { type: 'text', value: txt }, # Text element displaying the question text + { type: 'text', value: question_type }, # Text element displaying the question type + { type: 'text', value: weight.to_s }, # Text element displaying the question weight (converted to string) + { type: 'text', value: '—' } # Placeholder text element (not specified in the requirement) ] - }.to_json + }.to_json # Converts the hash to JSON format end -end +end \ No newline at end of file diff --git a/app/models/unscored_question.rb b/app/models/unscored_question.rb index ce754d763..80d2bc45b 100644 --- a/app/models/unscored_question.rb +++ b/app/models/unscored_question.rb @@ -1,9 +1,17 @@ +# Inherits from ChoiceQuestion, representing a type of question that doesn't contribute to the overall score class UnscoredQuestion < ChoiceQuestion + # to provide the specific logic for editing an unscored question. def edit; end + + # to return the structure or content necessary to display the text of the unscored question. def view_question_text; end + + # to provide the specific logic required for a respondent to complete an unscored question. def complete; end + # to return the structure or content necessary to display a completed unscored question, including + # any selected answers or respondent inputs. def view_completed_question; end -end \ No newline at end of file +end diff --git a/app/models/upload_file.rb b/app/models/upload_file.rb index 061d34356..d7d9f57c1 100644 --- a/app/models/upload_file.rb +++ b/app/models/upload_file.rb @@ -2,71 +2,68 @@ class UploadFile < Question def edit(_count) { - action: 'edit', - elements: [ + action: 'edit', # Indicates the action type + elements: [ # Array of elements comprising the edit form { - type: 'link', - text: 'Remove', - href: "/questions/#{id}", - method: 'delete' + type: 'link', # Type of element (link) + text: 'Remove', # Text for the link + href: "/questions/#{id}", # URL for the link + method: 'delete' # HTTP method for the link }, { - type: 'input', - input_type: 'text', - name: "question[#{id}][seq]", - id: "question_#{id}_seq", - value: seq.to_s + type: 'input', # Type of element (input) + input_type: 'text', # Type of input (text) + name: "question[#{id}][seq]", # Name attribute for form submission + id: "question_#{id}_seq", # ID attribute for HTML element + value: seq.to_s # Value of the input field, converted to string }, { - type: 'input', - input_type: 'text', - name: "question[#{id}][id]", - id: "question_#{id}", - value: id.to_s + type: 'input', # Type of element (input) + input_type: 'text', # Type of input (text) + name: "question[#{id}][id]", # Name attribute for form submission + id: "question_#{id}", # ID attribute for HTML element + value: id.to_s # Value of the input field, converted to string }, { - type: 'textarea', - cols: 50, - rows: 1, - name: "question[#{id}][txt]", - id: "question_#{id}_txt", - placeholder: 'Edit question content here', - value: txt + type: 'textarea', # Type of element (textarea) + cols: 50, # Number of columns for the textarea + rows: 1, # Number of rows for the textarea + name: "question[#{id}][txt]", # Name attribute for form submission + id: "question_#{id}_txt", # ID attribute for HTML element + placeholder: 'Edit question content here', # Placeholder text for the textarea + value: txt # Value of the textarea }, { - type: 'input', - input_type: 'text', - size: 10, - name: "question[#{id}][question_type]", - id: "question_#{id}_question_type", - value: question_type, - disabled: true + type: 'input', # Type of element (input) + input_type: 'text', # Type of input (text) + size: 10, # Size of the input field + name: "question[#{id}][question_type]", # Name attribute for form submission + id: "question_#{id}_question_type", # ID attribute for HTML element + value: question_type, # Value of the input field + disabled: true # Indicates whether the input is disabled } ] - }.to_json + }.to_json # Converts the hash to JSON format end def view_question_text { - action: 'view_question_text', - elements: [ - { type: 'text', value: txt }, - { type: 'text', value: question_type }, - { type: 'text', value: weight.to_s }, - { type: 'text', value: id.to_s }, + action: 'view_question_text', # Indicates the action type + elements: [ # Array of elements comprising the question view + { type: 'text', value: txt }, # Text element displaying the question text + { type: 'text', value: question_type }, # Text element displaying the question type + { type: 'text', value: weight.to_s }, # Text element displaying the question weight (converted to string) + { type: 'text', value: id.to_s }, # Text element displaying the question ID (converted to string) { type: 'text', value: '—' } # Placeholder for non-applicable fields ] - }.to_json + }.to_json # Converts the hash to JSON format end - # Implement this method for completing a question def complete(count, answer = nil) - # Implement the logic for completing a question end - # Implement this method for viewing a completed question by a student + def view_completed_question(count, files) - # Implement the logic for viewing a completed question by a student end end diff --git a/db/migrate/20240315004809_create_question_advices.rb b/db/migrate/20240315004809_create_question_advices.rb index 4b2c128e7..56a542b39 100644 --- a/db/migrate/20240315004809_create_question_advices.rb +++ b/db/migrate/20240315004809_create_question_advices.rb @@ -1,10 +1,20 @@ +# Migration to create the question_advices table in the database class CreateQuestionAdvices < ActiveRecord::Migration[7.0] + # Method to define changes to be made to the database def change + # Creates a new table named question_advices create_table :question_advices do |t| + # Adds a reference to the questions table. Each question_advice is associated with a question. + # The `null: false` constraint ensures that every question_advice must have an associated question. t.references :question, null: false, foreign_key: true + + # Adds an integer column named score to store the score associated with the advice. t.integer :score + + # Adds a text column named advice to store the advice text. t.text :advice + # Adds created_at and updated_at columns automatically to track when question advices are created and updated. t.timestamps end end diff --git a/db/migrate/20240409000041_create_quiz_question_choices.rb b/db/migrate/20240409000041_create_quiz_question_choices.rb index c1379f585..a28e6a984 100644 --- a/db/migrate/20240409000041_create_quiz_question_choices.rb +++ b/db/migrate/20240409000041_create_quiz_question_choices.rb @@ -1,11 +1,14 @@ +# Define a migration for creating the quiz_question_choices table in the database class CreateQuizQuestionChoices < ActiveRecord::Migration[7.0] + # Method defining changes to be made to the database def change + # Create a new table named quiz_question_choices create_table :quiz_question_choices, id: :integer, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=latin1" do |t| - t.integer :question_id - t.text :txt - t.boolean :iscorrect, default: false + t.integer :question_id # Column for storing the associated question's ID + t.text :txt # Column for storing the text of the quiz question choice + t.boolean :iscorrect, default: false # Boolean column indicating whether the choice is correct, defaulting to false - t.timestamps + t.timestamps # Adds created_at and updated_at columns automatically end end end diff --git a/spec/models/checkbox_spec.rb b/spec/models/checkbox_spec.rb index 31140a7c3..a8af99d2d 100644 --- a/spec/models/checkbox_spec.rb +++ b/spec/models/checkbox_spec.rb @@ -1,12 +1,18 @@ require 'rails_helper' +# RSpec tests for the Checkbox class RSpec.describe Checkbox do + # Setup a Checkbox instance before each test case let!(:checkbox) { Checkbox.new(id: 10, question_type: 'Checkbox', seq: 1.0, txt: 'test txt', weight: 11) } + # Setup an Answer instance to be used in the tests let!(:answer) { Answer.new(answer: 1) } + # Test suite for the #edit method describe '#edit' do it 'returns the JSON' do + # Call the edit method and store the result json = checkbox.edit(0) + # Define the expected JSON structure expected_json = { remove_button: { type: 'remove_button', action: 'delete', href: "/questions/10", text: 'Remove' }, seq: { type: 'seq', input_size: 6, value: 1.0, name: "question[10][seq]", id: "question_10_seq" }, @@ -14,53 +20,65 @@ type: { type: 'text', input_size: 10, disabled: true, value: 'Checkbox', name: "question[10][type]", id: "question_10_type" }, weight: { type: 'weight', placeholder: 'UnscoredQuestion does not need weight' } } + # Assert that the actual JSON matches the expected structure expect(json).to eq(expected_json) end end + # Test suite for the #complete method describe '#complete' do - let(:checkbox) { Checkbox.new(id: 10, question_type: 'Checkbox', seq: 1.0, txt: 'test txt', weight: 11) } - let(:count) { 1 } - let(:answer) { OpenStruct.new(answer: 1) } # Mocking Answer object - + # Context when an answer is provided context 'when an answer is provided' do it 'returns the expected completion structure' do + # Call the complete method and store the result result = checkbox.complete(count, answer) - + # Check for the presence of the previous_question key and that inputs is an array expect(result[:previous_question]).to be_present expect(result[:inputs]).to be_an(Array) + # Check that the label text matches the question text and that the script includes the correct function expect(result[:label]).to include(text: checkbox.txt) expect(result[:script]).to include("checkbox#{count}Changed()") + # Check that the checkbox is marked as checked expect(result[:inputs].last[:checked]).to be true end end + # Context when no answer is provided context 'when no answer is provided' do - let(:answer) { OpenStruct.new(answer: nil) } + let(:answer) { OpenStruct.new(answer: nil) } # Mock an empty answer it 'returns a structure with the checkbox not checked' do + # Call the complete method and store the result result = checkbox.complete(count, answer) + # Check that the checkbox is not marked as checked expect(result[:inputs].last[:checked]).to be false end end end + # Test suite for the #view_question_text method describe '#view_question_text' do it 'returns the JSON' do + # Call the view_question_text method and store the result json = checkbox.view_question_text + # Define the expected JSON structure expected_json = { content: 'test txt', type: 'Checkbox', weight: '11', checked_state: 'Checked/Unchecked' } + # Assert that the actual JSON matches the expected structure expect(json).to eq(expected_json) end end + # Test suite for the #view_completed_question method describe '#view_completed_question' do it 'returns the JSON' do + # Call the view_completed_question method with parameters and store the result json = checkbox.view_completed_question(0, answer) + # Define the expected JSON structure expected_json = { previous_question: { type: 'other' }, answer: { @@ -71,7 +89,8 @@ }, if_column_header: 'continue' } + # Assert that the actual JSON matches the expected structure expect(json).to eq(expected_json) end end -end +end \ No newline at end of file diff --git a/spec/models/criterion_spec.rb b/spec/models/criterion_spec.rb index 8adc5045b..984ef6926 100644 --- a/spec/models/criterion_spec.rb +++ b/spec/models/criterion_spec.rb @@ -1,77 +1,90 @@ require 'rails_helper' +# RSpec tests for the Criterion class, a type of ScoredQuestion RSpec.describe Criterion, type: :model do + # Setup for tests: defining a questionnaire and a criterion question associated with it let(:questionnaire) { Questionnaire.new(min_question_score: 0, max_question_score: 5) } let(:criterion) { Criterion.new(id: 1, question_type: 'Criterion', seq: 1.0, txt: 'test txt', weight: 1, questionnaire: questionnaire) } + + # Dummy answers for testing let(:answer_no_comments) { Answer.new(answer: 8) } let(:answer_comments) { Answer.new(answer: 3, comments: 'text comments') } + # Test suite for the #view_question_text method describe '#view_question_text' do it 'returns the JSON' do + # Calling the method and checking the returned JSON structure json = criterion.view_question_text expected_json = { - text: 'test txt', - question_type: 'Criterion', - weight: 1, - score_range: '0 to 5' + text: 'test txt', # The text of the question + question_type: 'Criterion', # The type of question + weight: 1, # The weight of the question + score_range: '0 to 5' # The score range, derived from the associated questionnaire } - expect(json).to eq(expected_json) + expect(json).to eq(expected_json) # Asserting that the actual JSON matches the expected structure end end + # Test suite for the #complete method describe '#complete' do it 'returns JSON without answer and no dropdown or scale specified' do + # Testing completion without specifying dropdown or scale json = criterion.complete(0, nil, 0, 5) expected_json = { - label: 'test txt' + label: 'test txt' # The label should only contain the question text } - expect(json).to include(expected_json) + expect(json).to include(expected_json) # Asserting the inclusion of the expected JSON end it 'returns JSON with a dropdown, including answer options' do + # Testing completion with dropdown options json = criterion.complete(0, nil, 0, 5, 'dropdown') - expected_options = (0..5).map { |score| { value: score, label: score.to_s } } + expected_options = (0..5).map { |score| { value: score, label: score.to_s } } # Expected options for the dropdown expected_json = { - label: 'test txt', - response_options: { + label: 'test txt', # The question text + response_options: { # The options for responding to the question in a dropdown format type: 'dropdown', comments: nil, current_answer: nil, options: expected_options, } } - expect(json).to include(expected_json) + expect(json).to include(expected_json) # Asserting the inclusion of the expected JSON end end + # Test suite for the #dropdown_criterion_question method describe '#dropdown_criterion_question' do it 'returns JSON for a dropdown without an answer selected' do + # Testing dropdown options without a selected answer json = criterion.dropdown_criterion_question(0, nil, 0, 5) - expected_options = (0..5).map { |score| { value: score, label: score.to_s } } + expected_options = (0..5).map { |score| { value: score, label: score.to_s } } # Expected options for the dropdown expected_json = { - type: 'dropdown', - comments: nil, - current_answer: nil, - options: expected_options, + type: 'dropdown', # The type of response input + comments: nil, # No comments + current_answer: nil, # No current answer + options: expected_options, # The options for the dropdown } - expect(json).to eq(expected_json) + expect(json).to eq(expected_json) # Asserting that the actual JSON matches the expected structure end end + # Test suite for the #scale_criterion_question method describe '#scale_criterion_question' do it 'returns JSON for a scale question without an answer selected' do + # Testing scale question options without a selected answer json = criterion.scale_criterion_question(0, nil, 0, 5) expected_json = { - type: 'scale', - min: 0, - max: 5, - comments: nil, - current_answer: nil, - min_label: nil, - max_label: nil, - size: nil, + type: 'scale', # The type of response input + min: 0, # Minimum value for the scale + max: 5, # Maximum value for the scale + comments: nil, # No comments + current_answer: nil, # No current answer + min_label: nil, # No minimum label + max_label: nil, # No maximum label + size: nil, # No size specified } - expect(json).to eq(expected_json) + expect(json).to eq(expected_json) # Asserting that the actual JSON matches the expected structure end end end diff --git a/spec/models/dropdown_spec.rb b/spec/models/dropdown_spec.rb index 605980f31..1762f132b 100644 --- a/spec/models/dropdown_spec.rb +++ b/spec/models/dropdown_spec.rb @@ -1,12 +1,19 @@ require 'rails_helper' +# RSpec test suite for the Dropdown model RSpec.describe Dropdown, type: :model do + + # Test suite for the #edit method describe '#edit' do + # Context when a count is provided to the method context 'when given a count' do it 'returns a JSON object with the edit form for a question' do + # Initialize a Dropdown instance with specific attributes dropdown = Dropdown.new(txt: "Some Text", type: "dropdown", weight: 1) + # Call the edit method with a count argument and store the result json_result = dropdown.edit(5) + # Define the expected JSON structure expected_result = { form: true, label: "Question 5:", @@ -18,34 +25,43 @@ weight: 1, type: 'dropdown' }.to_json + # Assert that the actual result matches the expected result expect(json_result).to eq(expected_result) end end end + # Test suite for the #view_question_text method describe '#view_question_text' do + # Using let to define a reusable Dropdown instance for the tests in this block let(:dropdown) { Dropdown.new } context 'when given valid inputs' do it 'returns the JSON for displaying the question text, type, weight, and score range' do + # Stub methods to return specific values allow(dropdown).to receive(:txt).and_return("Question 1") allow(dropdown).to receive(:type).and_return("Multiple Choice") allow(dropdown).to receive(:weight).and_return(1) + + # Define the expected JSON structure expected_json = { text: "Question 1", type: "Multiple Choice", weight: 1, score_range: "N/A" }.to_json + # Assert that the view_question_text method returns the expected JSON expect(dropdown.view_question_text).to eq(expected_json) end end end + # Test suite for the #complete method describe '#complete' do - let(:dropdown) { Dropdown.new } + let(:dropdown) { Dropdown.new } # Reusable Dropdown instance for these tests context 'when count is provided' do it 'generates JSON for a select input with the given count' do - count = 3 + count = 3 # Define a count for the options + # Define the expected JSON structure for dropdown options expected_json = { dropdown_options: [ { value: 1, selected: false }, @@ -53,14 +69,16 @@ { value: 3, selected: false } ] }.to_json + # Assert that the complete method returns the expected JSON expect(dropdown.complete(count)).to eq(expected_json) end end context 'when answer is provided' do it 'generates JSON with the provided answer selected' do - count = 3 - answer = 2 + count = 3 # Define a count for the options + answer = 2 # Specify the selected answer + # Define the expected JSON structure with the selected answer expected_json = { dropdown_options: [ { value: 1, selected: false }, @@ -68,13 +86,15 @@ { value: 3, selected: false } ] }.to_json + # Assert that the complete method returns the expected JSON with the answer selected expect(dropdown.complete(count, answer)).to eq(expected_json) end end context 'when answer is not provided' do it 'generates JSON without any answer selected' do - count = 3 + count = 3 # Define a count for the options + # Define the expected JSON structure with no selected answers expected_json = { dropdown_options: [ { value: 1, selected: false }, @@ -82,17 +102,20 @@ { value: 3, selected: false } ] }.to_json + # Assert that the complete method returns the expected JSON with no answers selected expect(dropdown.complete(count)).to eq(expected_json) end end end + # Test suite for the #complete_for_alternatives method describe '#complete_for_alternatives' do - let(:dropdown) { Dropdown.new } + let(:dropdown) { Dropdown.new } # Reusable Dropdown instance for these tests context 'when given an array of alternatives and an answer' do it 'returns JSON options with the selected alternative marked' do - alternatives = [1, 2, 3] - answer = 2 + alternatives = [1, 2, 3] # Define an array of alternative options + answer = 2 # Specify the selected answer + # Define the expected JSON structure with the selected alternative expected_json = { dropdown_options: [ { value: 1, selected: false }, @@ -100,8 +123,9 @@ { value: 3, selected: false } ] }.to_json + # Assert that the complete_for_alternatives method returns the expected JSON with the selected alternative expect(dropdown.complete_for_alternatives(alternatives, answer)).to eq(expected_json) end end end -end +end \ No newline at end of file diff --git a/spec/models/multiple_choice_checkbox_spec.rb b/spec/models/multiple_choice_checkbox_spec.rb index 819dc05b6..3ce564926 100644 --- a/spec/models/multiple_choice_checkbox_spec.rb +++ b/spec/models/multiple_choice_checkbox_spec.rb @@ -1,13 +1,19 @@ require 'rails_helper' +# RSpec test suite for MultipleChoiceCheckbox model RSpec.describe MultipleChoiceCheckbox, type: :model do - let(:multiple_choice_checkbox) { MultipleChoiceCheckbox.new(id: 1, txt: 'Test question:', weight: 1) } # Adjust as needed + # Setup a sample instance of MultipleChoiceCheckbox for use in tests + let(:multiple_choice_checkbox) { MultipleChoiceCheckbox.new(id: 1, txt: 'Test question:', weight: 1) } + # Tests for the #edit method describe '#edit' do it 'returns the JSON structure for editing' do + # Setup a mock object for QuizQuestionChoice with predefined attributes qc = instance_double('QuizQuestionChoice', iscorrect: true, txt: 'question text', id: 1) + # Stub the .where method to return a predefined set of choices allow(QuizQuestionChoice).to receive(:where).with(question_id: 1).and_return([qc, qc, qc, qc]) + # Define the expected JSON structure expected_structure = { "id" => 1, "question_text" => "Test question:", @@ -20,50 +26,68 @@ ] }.to_json + # Verify that the edit method returns the expected JSON structure expect(multiple_choice_checkbox.edit).to eq(expected_structure) end end + # Tests for the #isvalid method describe '#isvalid' do context 'when the question itself does not have txt' do it 'returns a JSON with error message' do + # Stub the txt method to return an empty string to simulate a question without text allow(multiple_choice_checkbox).to receive_messages(txt: '', id: 1) + # Define a set of question choices questions = { '1' => { txt: 'question text', iscorrect: '1' }, '2' => { txt: 'question text', iscorrect: '1' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } + # Define the expected JSON response with an error message expected_response = { valid: false, error: 'Please make sure all questions have text' }.to_json + # Verify that the isvalid method returns the expected response expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) end end context 'when a choice does not have txt' do it 'returns a JSON with error message' do + # Define a set of question choices with empty text questions = { '1' => { txt: '', iscorrect: '1' }, '2' => { txt: '', iscorrect: '1' }, '3' => { txt: '', iscorrect: '0' }, '4' => { txt: '', iscorrect: '0' } } + # Define the expected JSON response indicating the choices are valid despite missing text expected_response = { valid: true, error: nil }.to_json + # Verify that the isvalid method returns the expected response expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) end end context 'when no choices are correct' do it 'returns a JSON with error message' do + # Define a set of question choices with no correct answers questions = { '1' => { txt: 'question text', iscorrect: '0' }, '2' => { txt: 'question text', iscorrect: '0' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } + # Define the expected JSON response with an error message about needing a correct answer expected_response = { valid: false, error: 'Please select a correct answer for all questions' }.to_json + # Verify that the isvalid method returns the expected response expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) end end context 'when only one choice is correct' do it 'returns a JSON with error message' do + # Define a set of question choices with only one correct answer questions = { '1' => { txt: 'question text', iscorrect: '1' }, '2' => { txt: 'question text', iscorrect: '0' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } + # Define the expected JSON response with an error message for having only one correct answer in a multiple-choice question expected_response = { valid: false, error: 'A multiple-choice checkbox question should have more than one correct answer.' }.to_json + # Verify that the isvalid method returns the expected response expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) end end context 'when 2 choices are correct' do it 'returns valid status' do + # Define a set of question choices with two correct answers questions = { '1' => { txt: 'question text', iscorrect: '1' }, '2' => { txt: 'question text', iscorrect: '1' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } + # Define the expected JSON response indicating the question is valid expected_response = { valid: true, error: nil}.to_json + # Verify that the isvalid method returns the expected response expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) end end end -end +end \ No newline at end of file diff --git a/spec/models/scale_spec.rb b/spec/models/scale_spec.rb index ce6bfcacb..524f9a5ad 100644 --- a/spec/models/scale_spec.rb +++ b/spec/models/scale_spec.rb @@ -1,10 +1,12 @@ - require 'rails_helper' +# RSpec tests for the Scale model RSpec.describe Scale, type: :model do + # Define the subject of the tests as a new instance of Scale subject { Scale.new } + # Set up initial attributes for the Scale instance before each test before do subject.txt = "Rate your experience" subject.type = "Scale" @@ -16,13 +18,17 @@ subject.answer = 3 end + # Test suite for the #edit method describe "#edit" do it 'returns a JSON object with question text, type, weight, and score range' do + # Create a new instance of Scale with specific attributes for testing scale = Scale.new(txt: 'Scale Question', type: 'scale', weight: 2, min_question_score: 0, max_question_score: 10) + # Call the edit method and store the result json_result = scale.edit + # Define the expected JSON structure expected_result = { form: true, label: "Question:", @@ -34,24 +40,30 @@ weight: 2, type: 'scale' }.to_json + # Assert that the actual result matches the expected result expect(json_result).to eq(expected_result) end end + # Test suite for the #view_question_text method describe "#view_question_text" do it "returns JSON containing the question text" do + # Define the expected JSON structure for the question text expected_json = { text: "Rate your experience", type: "Scale", weight: 1, score_range: "Poor 1 to 5 Excellent" }.to_json + # Assert that the view_question_text method returns the expected JSON expect(subject.view_question_text).to eq(expected_json) end end + # Test suite for the #complete method describe "#complete" do it "returns JSON with scale options" do + # Define the expected JSON structure for the scale options expected_json = { scale_options: [ { value: 1, selected: false }, { value: 2, selected: false }, @@ -59,24 +71,31 @@ { value: 4, selected: false }, { value: 5, selected: false } ] }.to_json + # Assert that the complete method returns the expected JSON expect(subject.complete).to eq(expected_json) end end + # Test suite for the #view_completed_question method describe "#view_completed_question" do context "when the question has been answered" do it "returns JSON with the count, answer, and questionnaire_max" do + # Define options to simulate the question being answered options = { count: 10, answer: 3, questionnaire_max: 50 } + # Define the expected JSON structure when the question is answered expected_json = options.to_json + # Assert that the view_completed_question method returns the expected JSON when answered expect(subject.view_completed_question(options)).to eq(expected_json) end end context "when the question has not been answered" do it "returns a message indicating the question was not answered" do + # Define the expected JSON structure when the question is not answered expected_json = { message: "Question not answered." }.to_json + # Assert that the view_completed_question method returns the expected JSON when not answered expect(subject.view_completed_question).to eq(expected_json) end end end -end +end \ No newline at end of file diff --git a/spec/models/text_area_spec.rb b/spec/models/text_area_spec.rb index 05e8f174f..dd7e09ce5 100644 --- a/spec/models/text_area_spec.rb +++ b/spec/models/text_area_spec.rb @@ -1,11 +1,14 @@ require 'rails_helper' RSpec.describe TextArea do + # Create a TextArea instance let(:text_area) { TextArea.create(size: '34,1') } + # Create an Answer instance let!(:answer) { Answer.create(comments: 'test comment') } describe '#complete' do context 'when count is provided' do + # Test case for generating JSON for a textarea input it 'generates JSON for a textarea input' do result = JSON.parse(text_area.complete(1)) expect(result['action']).to eq('complete') @@ -13,6 +16,7 @@ expect(result['data']['size']).to eq('34,1') end + # Test case for including existing comments in the textarea input it 'includes any existing comments in the textarea input' do result = JSON.parse(text_area.complete(1, answer)) expect(result['data']['comment']).to eq('test comment') @@ -20,6 +24,7 @@ end context 'when count is not provided' do + # Test case for generating JSON with default size for the textarea input it 'generates JSON with default size for the textarea input' do text_area = TextArea.create(size: nil) result = JSON.parse(text_area.complete(nil)) @@ -30,6 +35,7 @@ describe 'view_completed_question' do context 'when given a count and an answer' do + # Test case for returning the formatted JSON for the completed question it 'returns the formatted JSON for the completed question' do result = JSON.parse(text_area.view_completed_question(1, answer)) expect(result['action']).to eq('view_completed_question') diff --git a/spec/models/text_field_spec.rb b/spec/models/text_field_spec.rb index 66f52b830..83d6b562e 100644 --- a/spec/models/text_field_spec.rb +++ b/spec/models/text_field_spec.rb @@ -1,11 +1,14 @@ require 'rails_helper' RSpec.describe TextField, type: :model do + # Create a TextField instance let(:question) { TextField.create(txt: 'Sample Text Question', question_type: 'TextField', size: 'medium', break_before: true) } + # Create an Answer instance let(:answer) { Answer.new(comments: 'This is a sample answer.') } describe '#complete' do context 'when count is provided' do + # Test case for returning JSON data for a paragraph with a label and input fields it 'returns JSON data for a paragraph with a label and input fields' do result = JSON.parse(question.complete(1)) expect(result['action']).to eq('complete') @@ -15,6 +18,7 @@ end context 'when count and answer are provided' do + # Test case for returning JSON data with pre-filled comment value it 'returns JSON data with pre-filled comment value' do result = JSON.parse(question.complete(1, answer)) expect(result['data']['value']).to eq(answer.comments) @@ -24,6 +28,7 @@ describe '#view_completed_question' do context "when the question type is 'TextField' and break_before is true" do + # Test case for returning the formatted JSON for the completed question it 'returns the formatted JSON for the completed question' do result = JSON.parse(question.view_completed_question(1, [])) expect(result['data']['type']).to eq('text') @@ -32,12 +37,14 @@ end context "when the question type is not 'TextField' or break_before is false" do + # Create a TextField instance with different properties let(:non_text_field_question) { TextField.create(txt: 'Non-text question', question_type: 'NotTextField', size: 'small', break_before: false) } + # Test case for returning the formatted JSON for the completed question it 'returns the formatted JSON for the completed question' do result = JSON.parse(non_text_field_question.view_completed_question(1, [])) expect(result['data']['break_before']).to be false end end end -end +end \ No newline at end of file diff --git a/spec/models/text_response_spec.rb b/spec/models/text_response_spec.rb index f326e9653..a94161e6b 100644 --- a/spec/models/text_response_spec.rb +++ b/spec/models/text_response_spec.rb @@ -1,32 +1,38 @@ require 'rails_helper' RSpec.describe TextResponse, type: :model do + # Create a TextResponse instance let(:text_response) { TextResponse.create(seq: '001', txt: 'Sample question content', question_type: 'TextResponse', size: 'medium', weight: 10) } describe '#edit' do + # Parse the JSON result of the edit method let(:result) { JSON.parse(text_response.edit(1)) } + # Test case for verifying correct action in the JSON result it 'returns JSON for editing with correct action' do expect(result["action"]).to eq('edit') end + # Test case for verifying the presence of elements for editing question it 'includes elements for editing question' do expect(result["elements"].length).to be > 0 end end describe '#view_question_text' do + # Parse the JSON result of the view_question_text method let(:result) { JSON.parse(text_response.view_question_text) } + # Test case for verifying correct action in the JSON result it 'returns JSON for viewing question text with correct action' do expect(result["action"]).to eq('view_question_text') end + # Test case for verifying the presence of question text, question_type, and weight in elements it 'includes the question text, question_type, and weight in elements' do expect(result["elements"].any? { |e| e["value"] == 'Sample question content' }).to be true expect(result["elements"].any? { |e| e["value"] == 'TextResponse' }).to be true expect(result["elements"].any? { |e| e["value"].match?(/^\d+$/) }).to be true end end - -end +end \ No newline at end of file diff --git a/spec/models/upload_file_spec.rb b/spec/models/upload_file_spec.rb index 97897ada0..17e828e72 100644 --- a/spec/models/upload_file_spec.rb +++ b/spec/models/upload_file_spec.rb @@ -1,48 +1,56 @@ require 'rails_helper' RSpec.describe UploadFile do - # Adjust the let statement to not include 'type' and 'weight' if they are not recognized attributes. + # Create an UploadFile instance with basic attributes let(:upload_file) { UploadFile.new(id: 1, seq: '1', txt: 'Sample question content', question_type: 'UploadFile') } describe '#edit' do context 'when given a count' do + # Parse the JSON response from the edit method let(:json_response) { JSON.parse(upload_file.edit(1)) } + # Test case for verifying correct action and structure in the JSON response it 'returns JSON with a correct structure for editing' do expect(json_response["action"]).to eq('edit') expect(json_response["elements"]).to be_an(Array) end + # Test case for verifying the presence of "Remove" link in the elements it 'includes a "Remove" link in the elements' do link_element = json_response["elements"].find { |el| el["text"] == "Remove" } expect(link_element).not_to be_nil expect(link_element["href"]).to include("/questions/1") end + # Test case for verifying the presence of an input field for the sequence it 'includes an input field for the sequence' do seq_element = json_response["elements"].find { |el| el["name"]&.include?("seq") } expect(seq_element).not_to be_nil expect(seq_element["value"]).to eq('1') end + # Test case for verifying the presence of an input field for the id it 'includes an input field for the id' do id_element = json_response["elements"].find { |el| el["name"]&.include?("id") } expect(id_element).not_to be_nil expect(id_element["value"]).to eq('1') end + # Test case for verifying the presence of a textarea for editing the question content it 'includes a textarea for editing the question content' do textarea_element = json_response["elements"].find { |el| el["name"]&.include?("txt") } expect(textarea_element).not_to be_nil expect(textarea_element["value"]).to eq('Sample question content') end + # Test case for verifying the presence of an input field for the question type, disabled it 'includes an input field for the question type, disabled' do type_element = json_response["elements"].find { |el| el["name"]&.include?("question_type") } expect(type_element).not_to be_nil expect(type_element["disabled"]).to be true end + # Test case for verifying that weight element is not present in the JSON response it 'does not include an explicit cell for weight, as it is not applicable' do weight_element = json_response["elements"].none? { |el| el.has_key?("weight") } expect(weight_element).to be true @@ -52,8 +60,10 @@ describe '#view_question_text' do context 'when given valid input' do + # Parse the JSON response from the view_question_text method let(:json_response) { JSON.parse(upload_file.view_question_text) } + # Test case for verifying correct action and structure in the JSON response it 'returns JSON for displaying a question' do expect(json_response["action"]).to eq('view_question_text') expect(json_response["elements"].map { |el| el["value"] }).to include('Sample question content', 'UploadFile', "", '1','—') @@ -62,14 +72,16 @@ end describe '#complete' do + # Placeholder test case for the complete method logic it 'implements the logic for completing a question' do # Write your test for the complete method logic here. end end describe '#view_completed_question' do + # Placeholder test case for the view_completed_question method logic it 'implements the logic for viewing a completed question by a student' do # Write your test for the view_completed_question method logic here. end end -end +end \ No newline at end of file From 84d52569fe4f3b4d7a33cb19fdac7b2ee3cabfd9 Mon Sep 17 00:00:00 2001 From: jshah22 Date: Mon, 8 Apr 2024 22:08:32 -0400 Subject: [PATCH 12/12] Revert "comments and code updated" This reverts commit 88e6c22d7970224e540bab12c31487043ec8caef. --- app/helpers/question_helper.rb | 29 ++- app/models/checkbox.rb | 167 ++++++++---------- app/models/criterion.rb | 85 ++++----- app/models/dropdown.rb | 19 +- app/models/multiple_choice_checkbox.rb | 41 +---- app/models/question_advice.rb | 15 +- app/models/quiz_question.rb | 25 +-- app/models/quiz_question_choice.rb | 6 +- app/models/scale.rb | 16 +- app/models/text_area.rb | 36 ++-- app/models/text_field.rb | 50 +++--- app/models/text_response.rb | 80 ++++----- app/models/unscored_question.rb | 10 +- app/models/upload_file.rb | 81 +++++---- .../20240315004809_create_question_advices.rb | 10 -- ...0409000041_create_quiz_question_choices.rb | 11 +- spec/models/checkbox_spec.rb | 33 +--- spec/models/criterion_spec.rb | 65 +++---- spec/models/dropdown_spec.rb | 42 +---- spec/models/multiple_choice_checkbox_spec.rb | 28 +-- spec/models/scale_spec.rb | 23 +-- spec/models/text_area_spec.rb | 6 - spec/models/text_field_spec.rb | 9 +- spec/models/text_response_spec.rb | 10 +- spec/models/upload_file_spec.rb | 16 +- 25 files changed, 320 insertions(+), 593 deletions(-) diff --git a/app/helpers/question_helper.rb b/app/helpers/question_helper.rb index f957490d4..f26a9fd5c 100644 --- a/app/helpers/question_helper.rb +++ b/app/helpers/question_helper.rb @@ -1,26 +1,19 @@ -# Module QuestionHelper provides common functionalities for question-related operations module QuestionHelper - def edit_common(label, input_value, min_question_score, max_question_score, weight, type) + def edit_common(label = nil,min_question_score = nil, max_question_score = nil, txt ,weight, type) { - form: true, # Indicates the presence of a form - label: label, # Label for the question input field - input_type: 'text', # Type of the input field (text) - input_name: 'question', # Name attribute for the input field - input_value: input_value, # Current value for the input field - min_question_score: min_question_score, # Minimum score for the question - max_question_score: max_question_score, # Maximum score for the question - weight: weight, # Weight/importance of the question - type: type # Type of the question + form: true, + label: label, + input_type: 'text', + input_name: 'question', + input_value: txt, + min_question_score: min_question_score, + max_question_score: max_question_score, + weight: weight, + type: type } end def view_question_text_common(text, type, weight, score_range) - { - text: text, # Text of the question - type: type, # Type of the question - weight: weight, # Weight/importance of the question - score_range: score_range # Score range for the question - } + { text: text, type: type, weight: weight, score_range: score_range } end - end \ No newline at end of file diff --git a/app/models/checkbox.rb b/app/models/checkbox.rb index 1271bef95..50b7e3b74 100644 --- a/app/models/checkbox.rb +++ b/app/models/checkbox.rb @@ -1,189 +1,172 @@ -# Inherits from UnscoredQuestion, defining behaviors specific to checkbox questions class Checkbox < UnscoredQuestion - - # Builds the edit view structure for a checkbox question def edit(count) { - remove_button: edit_remove_button(count), # Structure for the remove button - seq: edit_seq(count), # Structure for question sequence input - question: edit_question(count), # Structure for editing the question content - type: edit_type(count), # Structure for displaying the question type - weight: edit_weight(count) # Structure for displaying the weight (not applicable for UnscoredQuestion) + remove_button: edit_remove_button(count), + seq: edit_seq(count), + question: edit_question(count), + type: edit_type(count), + weight: edit_weight(count) } end - # Defines the remove button component in the edit view def edit_remove_button(count) { - type: 'remove_button', # Component type - action: 'delete', # Action associated with the button - href: "/questions/#{id}", # URL for the delete action - text: 'Remove' # Button text + type: 'remove_button', + action: 'delete', + href: "/questions/#{id}", + text: 'Remove' } end - # Defines the sequence input component in the edit view def edit_seq(count) { - type: 'seq', # Component type - input_size: 6, # Size attribute of the input - value: seq, # Current sequence value - name: "question[#{id}][seq]", # Input name - id: "question_#{id}_seq" # Input id + type: 'seq', + input_size: 6, + value: seq, + name: "question[#{id}][seq]", + id: "question_#{id}_seq" } end - # Defines the question content editing component in the edit view def edit_question(count) { - type: 'textarea', # Component type - cols: 50, # Columns attribute of the textarea - rows: 1, # Rows attribute of the textarea - name: "question[#{id}][txt]", # Textarea name - id: "question_#{id}_txt", # Textarea id - placeholder: 'Edit question content here', # Placeholder text - content: txt # Current question content + type: 'textarea', + cols: 50, + rows: 1, + name: "question[#{id}][txt]", + id: "question_#{id}_txt", + placeholder: 'Edit question content here', + content: txt } end - # Defines the question type display component in the edit view def edit_type(count) { - type: 'text', # Component type - input_size: 10, # Size attribute of the input - disabled: true, # Disabled attribute to make it read-only - value: question_type, # Current question type - name: "question[#{id}][type]", # Input name - id: "question_#{id}_type" # Input id + type: 'text', + input_size: 10, + disabled: true, + value: question_type, + name: "question[#{id}][type]", + id: "question_#{id}_type" } end - # Defines the weight display component in the edit view (not applicable for UnscoredQuestion) def edit_weight(count) { - type: 'weight', # Component type - placeholder: 'UnscoredQuestion does not need weight' # Placeholder text indicating weight is not applicable + type: 'weight', + placeholder: 'UnscoredQuestion does not need weight' } end - # Builds the structure to view question text and related details + def view_question_text { - content: txt, # Question content - type: question_type, # Question type - weight: weight.to_s, # Question weight as a string - checked_state: 'Checked/Unchecked' # Placeholder for checked state description + content: txt, + type: question_type, + weight: weight.to_s, + checked_state: 'Checked/Unchecked' } end - # Constructs the completion structure for a checkbox question, including inputs and scripts def complete(count, answer = nil) { - previous_question: check_previous_question, # Checks and indicates if the previous question is a column header - inputs: [ # Array of input structures for the question - complete_first_second_input(count, answer), # Hidden inputs for comments and score - complete_third_input(count, answer) # Checkbox input for the actual question response + previous_question: check_previous_question, + inputs: [ + complete_first_second_input(count, answer), + complete_third_input(count, answer) ], - label: { # Label structure for the checkbox - for: "responses_#{count}", # Associated input id - text: txt # Question text as the label + label: { + for: "responses_#{count}", + text: txt }, - script: complete_script(count), # Script to handle checkbox change events - if_column_header: complete_if_column_header # Indicates if the next question is a column header or other special type + script: complete_script(count), + if_column_header: complete_if_column_header } end - # Checks if the previous question is a column header and returns the appropriate structure def check_previous_question - prev_question = Question.where('seq < ?', seq).order(:seq).last # Finds the last question before this one by sequence + prev_question = Question.where('seq < ?', seq).order(:seq).last { - type: prev_question&.type == 'ColumnHeader' ? 'ColumnHeader' : 'other' # Indicates if the previous question is a column header + type: prev_question&.type == 'ColumnHeader' ? 'ColumnHeader' : 'other' } end - # Hidden inputs for comments and score, used in the complete structure def complete_first_second_input(count, answer = nil) [ { - id: "responses_#{count}_comments", # Input id for comments - name: "responses[#{count}][comment]", # Input name for comments - type: 'hidden', # Input type hidden - value: '' # Default value + id: "responses_#{count}_comments", + name: "responses[#{count}][comment]", + type: 'hidden', + value: '' }, { - id: "responses_#{count}_score", # Input id for score - name: "responses[#{count}][score]", # Input name for score - type: 'hidden', # Input type hidden - value: answer&.answer == 1 ? '1' : '0' # Value based on answer + id: "responses_#{count}_score", + name: "responses[#{count}][score]", + type: 'hidden', + value: answer&.answer == 1 ? '1' : '0' } ] end - # Checkbox input for the actual question response in the complete structure def complete_third_input(count, answer = nil) { - id: "responses_#{count}_checkbox", # Checkbox id - type: 'checkbox', # Input type checkbox - onchange: "checkbox#{count}Changed()", # Onchange script function - checked: answer&.answer == 1 # Checked state based on answer + id: "responses_#{count}_checkbox", + type: 'checkbox', + onchange: "checkbox#{count}Changed()", + checked: answer&.answer == 1 } end - # JavaScript function to update the hidden score input based on checkbox state def complete_script(count) "function checkbox#{count}Changed() { var checkbox = jQuery('#responses_#{count}_checkbox'); var response_score = jQuery('#responses_#{count}_score'); if (checkbox.is(':checked')) { response_score.val('1'); } else { response_score.val('0'); }}" end - # Determines the flow after this question based on the type of the next question def complete_if_column_header - next_question = Question.where('seq > ?', seq).order(:seq).first # Finds the first question after this one by sequence + next_question = Question.where('seq > ?', seq).order(:seq).first if next_question case next_question.question_type - when 'ColumnHeader' # Next question is a column header + when 'ColumnHeader' 'end_of_column_header' - when 'SectionHeader', 'TableHeader' # Next question is a section or table header + when 'SectionHeader', 'TableHeader' 'end_of_section_or_table' - else # Continues with more questions of standard types + else 'continue' end - else # This is the last question + else 'end' end end - # Builds the structure for viewing a completed question, including previous question check and answer details def view_completed_question(count, answer) { - previous_question: check_previous_question, # Checks and indicates if the previous question is a column header - answer: view_completed_question_answer(count, answer), # Structure for displaying the given answer - if_column_header: view_completed_question_if_column_header # Indicates if the next question is a column header or other special type + previous_question: check_previous_question, + answer: view_completed_question_answer(count, answer), + if_column_header: view_completed_question_if_column_header } end - # Structure for displaying the given answer in the completed question view def view_completed_question_answer(count, answer) { - number: count, # Question number - image: answer.answer == 1 ? 'Check-icon.png' : 'delete_icon.png', # Icon based on answer - content: txt, # Question content - bold: true # Bold formatting for display + number: count, + image: answer.answer == 1 ? 'Check-icon.png' : 'delete_icon.png', + content: txt, + bold: true } end - # Determines the flow after this question in the completed question view based on the type of the next question def view_completed_question_if_column_header - next_question = Question.where('seq > ?', seq).order(:seq).first # Finds the first question after this one by sequence + next_question = Question.where('seq > ?', seq).order(:seq).first if next_question case next_question.question_type - when 'ColumnHeader' # Next question is a column header + when 'ColumnHeader' 'end_of_column_header' - when 'TableHeader' # Next question is a table header + when 'TableHeader' 'end_of_table_header' - else # Continues with more questions of standard types + else 'continue' end - else # This is the last question + else 'end' end end -end \ No newline at end of file +end diff --git a/app/models/criterion.rb b/app/models/criterion.rb index 8e9f7b7ac..eb22880fa 100644 --- a/app/models/criterion.rb +++ b/app/models/criterion.rb @@ -1,100 +1,81 @@ -# Inherits from ScoredQuestion, representing a question type with a scoring criterion class Criterion < ScoredQuestion - # Ensure the presence of the size attribute for a Criterion instance validates :size, presence: true - # Method to construct the structure needed for editing a criterion question def edit { - remove_link: "/questions/#{id}", # Link to remove the question - sequence_input: seq.to_s, # The sequence number of the question as a string - question_text: txt, # The text content of the question - question_type: question_type, # The type of the question, e.g., 'Criterion' - weight: weight.to_s, # The weight of the question as a string - size: size.to_s, # The size attribute of the criterion question as a string - max_label: max_label, # The label for the maximum score - min_label: min_label # The label for the minimum score + remove_link: "/questions/#{id}", + sequence_input: seq.to_s, + question_text: txt, + question_type: question_type, + weight: weight.to_s, + size: size.to_s, + max_label: max_label, + min_label: min_label } end - # Method to construct the structure needed for viewing the text of a criterion question def view_question_text question_data = { - text: txt, # The text content of the question - question_type: question_type, # The type of the question - weight: weight, # The weight of the question - score_range: "#{questionnaire.min_question_score} to #{questionnaire.max_question_score}" # The score range from the associated questionnaire + text: txt, + question_type: question_type, + weight: weight, + score_range: "#{questionnaire.min_question_score} to #{questionnaire.max_question_score}" } - # Append min and max labels to the score range if they are present - if max_label && min_label - question_data[:score_range] = "(#{min_label}) " + question_data[:score_range] + " (#{max_label})" - end - + question_data[:score_range] = "(#{min_label}) " + question_data[:score_range] + " (#{max_label})" if max_label && min_label question_data end - # Method to construct the response structure for a criterion question - def complete(count, answer = nil, questionnaire_min, questionnaire_max, dropdown_or_scale) - # Retrieve advices for the question and calculate their total length + def complete(count,answer = nil, questionnaire_min, questionnaire_max, dropdown_or_scale) question_advices = QuestionAdvice.to_json_by_question_id(id) advice_total_length = question_advices.sum { |advice| advice.advice.length unless advice.advice.blank? } - # Determine the response options based on whether it's a dropdown or scale type response_options = if dropdown_or_scale == 'dropdown' dropdown_criterion_question(count, answer, questionnaire_min, questionnaire_max) elsif dropdown_or_scale == 'scale' scale_criterion_question(count, answer, questionnaire_min, questionnaire_max) end - # Construct the advice section if there are any advices advice_section = question_advices.empty? || advice_total_length.zero? ? nil : advices_criterion_question(count, question_advices) - # Construct and return the final structure, removing any nil values with .compact { - label: txt, # The text of the question - advice: advice_section, # The advice section, if applicable - response_options: response_options # The response options for the question - }.compact + label: txt, + advice: advice_section, + response_options: response_options + }.compact # Use .compact to remove nil values end - # Method to generate dropdown options for a criterion question - def dropdown_criterion_question(count, answer, questionnaire_min, questionnaire_max) - # Generate a list of options based on the min and max scores from the questionnaire + # Assuming now these methods should be public based on the test cases + def dropdown_criterion_question(count,answer, questionnaire_min, questionnaire_max) options = (questionnaire_min..questionnaire_max).map do |score| - option = { value: score, label: score.to_s } # Each option has a value and label - option[:selected] = 'selected' if answer && score == answer.answer # Mark the option as selected if it matches the answer + option = { value: score, label: score.to_s } + option[:selected] = 'selected' if answer && score == answer.answer option end - # Return the structure for a dropdown criterion question { type: 'dropdown', options: options, current_answer: answer.try(:answer), comments: answer.try(:comments) } end - # Method to generate scale options for a criterion question - def scale_criterion_question(count, answer, questionnaire_min, questionnaire_max) - # Return the structure for a scale criterion question + def scale_criterion_question(count,answer, questionnaire_min, questionnaire_max) { type: 'scale', - min: questionnaire_min, # Minimum score - max: questionnaire_max, # Maximum score - current_answer: answer.try(:answer), # Current answer if available - comments: answer.try(:comments), # Any comments on the answer - min_label: min_label, # Label for the minimum score - max_label: max_label, # Label for the maximum score - size: size # Size of the scale + min: questionnaire_min, + max: questionnaire_max, + current_answer: answer.try(:answer), + comments: answer.try(:comments), + min_label: min_label, + max_label: max_label, + size: size } end private - # Method to structure advices for a criterion question def advices_criterion_question(question_advices) - # Map each advice to a structure containing its score and advice content question_advices.map do |advice| { - score: advice.score, # The score associated with the advice - advice: advice.advice # The advice content + score: advice.score, + advice: advice.advice } end end -end \ No newline at end of file +end diff --git a/app/models/dropdown.rb b/app/models/dropdown.rb index 4295b6e18..0fbb6195d 100644 --- a/app/models/dropdown.rb +++ b/app/models/dropdown.rb @@ -1,43 +1,26 @@ -# Inherits from UnscoredQuestion and includes QuestionHelper to utilize common functionality class Dropdown < UnscoredQuestion include QuestionHelper - # Define accessible attributes for instances of Dropdown attr_accessor :txt, :type, :count, :weight - - # Method to prepare and return the JSON structure for editing a dropdown question def edit(count) - # Utilizes edit_common method from QuestionHelper with specified parameters - # and converts the result to JSON format - edit_common("Question #{count}:", txt, weight, type).to_json + edit_common("Question #{count}:", txt , weight, type).to_json end - # Method to view the text and details of a dropdown question def view_question_text - # Utilizes view_question_text_common method from QuestionHelper with specified parameters - # and converts the result to JSON format. 'N/A' signifies that scoring is not applicable view_question_text_common(txt, type, weight, 'N/A').to_json end - # Method to prepare the dropdown options for completing a question, marking the selected option def complete(count, answer = nil) - # Generates a list of options based on the count, marking the selected option based on the answer options = (1..count).map { |option| { value: option, selected: (option == answer.to_i) } } - # Returns the dropdown options in JSON format { dropdown_options: options }.to_json end - # Similar to the complete method but uses predefined alternatives instead of a numeric range def complete_for_alternatives(alternatives, answer) - # Generates a list of options from the alternatives, marking the selected option based on the answer options = alternatives.map { |alt| { value: alt, selected: (alt == answer) } } - # Returns the dropdown options in JSON format { dropdown_options: options }.to_json end - # Method to display the selected option of a completed question def view_completed_question - # Constructs a response indicating the selected option or that the question was not answered { selected_option: (count && answer) ? "#{answer} (out of #{count})" : 'Question not answered.' }.to_json end end \ No newline at end of file diff --git a/app/models/multiple_choice_checkbox.rb b/app/models/multiple_choice_checkbox.rb index a3d73801e..4d49d8382 100644 --- a/app/models/multiple_choice_checkbox.rb +++ b/app/models/multiple_choice_checkbox.rb @@ -1,68 +1,51 @@ require 'json' -# Define a class for multiple-choice checkbox questions, inheriting from QuizQuestion class MultipleChoiceCheckbox < QuizQuestion - - # Method to prepare data for editing a question def edit - # Fetch choices associated with this question quiz_question_choices = QuizQuestionChoice.where(question_id: id) - # Construct a hash with question data and its choices data = { - id: id, # Question ID - question_text: txt, # Question text - weight: weight, # Question weight + id: id, + question_text: txt, + weight: weight, choices: quiz_question_choices.each_with_index.map do |choice, index| - # Map each choice to a hash with choice details { - id: choice.id, # Choice ID - text: choice.txt, # Choice text - is_correct: choice.iscorrect, # Indicates if the choice is correct - position: index + 1 # Position of the choice + id: choice.id, + text: choice.txt, + is_correct: choice.iscorrect, + position: index + 1 } end } - # Convert the hash to a JSON string data.to_json end - # Method to prepare data for completing a question def complete - # Fetch choices associated with this question quiz_question_choices = QuizQuestionChoice.where(question_id: id) - # Construct a hash with question data and its choices (without correct answers) data = { - id: id, # Question ID - question_text: txt, # Question text + id: id, + question_text: txt, choices: quiz_question_choices.map do |choice| - # Map each choice to a hash with only the choice text { text: choice.txt } end } - # Convert the hash to a JSON string data.to_json end - # Method to display a completed question along with user answers def view_completed_question(user_answer) - # Fetch choices associated with this question quiz_question_choices = QuizQuestionChoice.where(question_id: id) - # Construct a hash with the choices and user answers data = { question_choices: quiz_question_choices.map do |choice| - # Map each choice to a hash with choice details and correct status { text: choice.txt, is_correct: choice.iscorrect } end, user_answers: user_answer.map do |answer| - # Map each user answer to a hash with its correctness and comments { is_correct: answer.answer == 1, comments: answer.comments @@ -70,27 +53,21 @@ def view_completed_question(user_answer) end } - # Convert the hash to a JSON string data.to_json end - # Method to validate a choice selection for the question def isvalid(choice_info) error_message = nil - # Ensure the question text is not blank error_message = 'Please make sure all questions have text' if txt.blank? - # Count the number of correct answers correct_count = choice_info.count { |_idx, value| value[:iscorrect] == '1' } - # Set error messages based on the number of correct answers if correct_count.zero? error_message = 'Please select a correct answer for all questions' elsif correct_count == 1 error_message = 'A multiple-choice checkbox question should have more than one correct answer.' end - # Return a hash indicating whether the choices are valid and any error message { valid: error_message.nil?, error: error_message }.to_json end end diff --git a/app/models/question_advice.rb b/app/models/question_advice.rb index cb4b30a46..e4cc5bdd3 100644 --- a/app/models/question_advice.rb +++ b/app/models/question_advice.rb @@ -1,35 +1,22 @@ -# Inherits from ApplicationRecord, representing advices associated with questions in a questionnaire class QuestionAdvice < ApplicationRecord - # Establishes a belongs_to association with the Question model belongs_to :question - - # Class method to define the fields to be exported for QuestionAdvice records def self.export_fields(_options) - # Retrieves and maps the column names of the QuestionAdvice table QuestionAdvice.columns.map(&:name) end - # Class method to export QuestionAdvice records related to a specific questionnaire to CSV def self.export(csv, parent_id, _options) - # Finds the Questionnaire by the given parent_id questionnaire = Questionnaire.find(parent_id) - # Iterates over each question in the questionnaire questionnaire.questions.each do |question| - # Fetches and iterates over each piece of advice for the current question QuestionAdvice.where(question_id: question.id).each do |advice| - # Adds the advice attributes' values to the CSV csv << advice.attributes.values end end end - # Class method to convert advices related to a specific question into JSON format def self.to_json_by_question_id(question_id) - # Retrieves QuestionAdvice records for a specific question, ordered by id question_advices = QuestionAdvice.where(question_id: question_id).order(:id) - # Maps each advice to a hash with its score and advice content question_advices.map do |advice| { score: advice.score, advice: advice.advice } end end -end \ No newline at end of file +end diff --git a/app/models/quiz_question.rb b/app/models/quiz_question.rb index da6faeab9..db55527d6 100644 --- a/app/models/quiz_question.rb +++ b/app/models/quiz_question.rb @@ -1,39 +1,30 @@ require 'json' -# Define a base class for quiz questions, inheriting from Question class QuizQuestion < Question - # Establish a one-to-many relationship with quiz question choices - # Nullify the foreign key on dependent object destruction has_many :quiz_question_choices, class_name: 'QuizQuestionChoice', foreign_key: 'question_id', inverse_of: false, dependent: :nullify - # Method stub for editing a question def edit end - # Method to view question text along with choices and their correctness def view_question_text - # Map each choice to a hash with text and correctness choices = quiz_question_choices.map do |choice| { - text: choice.txt, # Text of the choice - is_correct: choice.iscorrect? # Correctness of the choice + text: choice.txt, + is_correct: choice.iscorrect? } end - # Construct a hash with question details and choices { - question_text: txt, # Text of the question - question_type: type, # Type of the question - question_weight: weight, # Weight/importance of the question - choices: choices # Choices for the question - }.to_json # Convert the hash to a JSON string + question_text: txt, + question_type: type, + question_weight: weight, + choices: choices + }.to_json end - # Method stub for completing a question def complete end - # Method stub for viewing a completed question with user answers def view_completed_question(user_answer = nil) end -end +end \ No newline at end of file diff --git a/app/models/quiz_question_choice.rb b/app/models/quiz_question_choice.rb index 34adc767e..af6106cd9 100644 --- a/app/models/quiz_question_choice.rb +++ b/app/models/quiz_question_choice.rb @@ -1,7 +1,3 @@ -# Define a class for quiz question choices, inheriting from ApplicationRecord class QuizQuestionChoice < ApplicationRecord - # Establish a belongs-to relationship with a quiz question - # Specifies that the quiz question choice is dependent on the question, - # meaning if the question is destroyed, this choice will also be destroyed belongs_to :question, dependent: :destroy -end +end \ No newline at end of file diff --git a/app/models/scale.rb b/app/models/scale.rb index 334ea9ab2..be6343769 100644 --- a/app/models/scale.rb +++ b/app/models/scale.rb @@ -1,48 +1,34 @@ -# Inherits from ScoredQuestion and includes QuestionHelper for shared functionalities class Scale < ScoredQuestion include QuestionHelper - # Define accessible attributes for instances of Scale attr_accessor :txt, :type, :weight, :min_label, :max_label, :answer, :min_question_score, :max_question_score - # Prepares and returns the JSON structure for editing a scale question def edit - # Calls edit_common from QuestionHelper with question parameters and converts to JSON - edit_common('Question:', min_question_score, max_question_score, txt, weight, type).to_json + edit_common('Question:', min_question_score, max_question_score , txt, weight, type).to_json end - # Generates the JSON structure for viewing the question text and details def view_question_text - # Calls view_question_text_common from QuestionHelper with question parameters, including the score range view_question_text_common(txt, type, weight, score_range).to_json end - # Prepares the scale options for question completion and marks the selected option def complete - # Generates a list of options from min to max score, marking the selected option options = (@min_question_score..@max_question_score).map do |option| { value: option, selected: (option == answer) } end - # Returns the scale options in JSON format { scale_options: options }.to_json end - # Displays the selected option for a completed question or indicates if unanswered def view_completed_question(options = {}) - # If sufficient data is provided in options, construct a response with count, answer, and max questionnaire score if options[:count] && options[:answer] && options[:questionnaire_max] { count: options[:count], answer: options[:answer], questionnaire_max: options[:questionnaire_max] }.to_json else - # If data is insufficient, return a message indicating the question was not answered { message: 'Question not answered.' }.to_json end end private - # Helper method to construct the score range string, incorporating labels if provided def score_range - # If min and max labels are not provided, use just the score numbers; otherwise, include the labels in the format min_label.nil? && max_label.nil? ? "#{@min_question_score} to #{@max_question_score}" : "#{min_label} #{@min_question_score} to #{@max_question_score} #{max_label}" end diff --git a/app/models/text_area.rb b/app/models/text_area.rb index 50d0a5633..14dd7d286 100644 --- a/app/models/text_area.rb +++ b/app/models/text_area.rb @@ -1,32 +1,22 @@ -# Inherits from Question to define specific behaviors for text area questions class TextArea < Question - - # Method to construct the completion structure for a text area question - # @param count [Integer] The sequence number of the question - # @param answer [Answer, nil] The answer object associated with the question, may be nil if not answered - # @return [String] JSON representation of the completion structure - def complete(count, answer = nil) + def complete(count,answer = nil) { - action: 'complete', # Indicates the action type - data: { # Data related to the text area question - count: count, # The sequence number of the question - comment: answer&.comments, # The comment from the answer, uses safe navigation operator to handle nil - size: size || '70,1', # The size of the text area, defaults to '70,1' if size is nil + action: 'complete', + data: { + count: count, + comment: answer&.comments, + size: size || '70,1', # Assuming '70,1' is the default size } - }.to_json # Converts the hash to JSON format + }.to_json end - # Method to construct the structure for viewing a completed text area question - # @param count [Integer] The sequence number of the question - # @param answer [Answer] The answer object associated with the question - # @return [String] JSON representation of the completed question view structure def view_completed_question(count, answer) { - action: 'view_completed_question', # Indicates the action type - data: { # Data related to the completed text area question - count: count, # The sequence number of the question - comment: answer.comments # The comment from the answer + action: 'view_completed_question', + data: { + count: count, + comment: answer.comments } - }.to_json # Converts the hash to JSON format + }.to_json end -end \ No newline at end of file +end diff --git a/app/models/text_field.rb b/app/models/text_field.rb index 063f2d6ea..fac966d77 100644 --- a/app/models/text_field.rb +++ b/app/models/text_field.rb @@ -1,30 +1,40 @@ -# Inherits from Question to define specific behaviors for text field questions class TextField < Question - # Validates the presence of the size attribute validates :size, presence: true def complete(count, answer = nil) { - action: 'complete', # Indicates the action type - data: { # Data related to the text field question - label: "Question ##{count}", # Label for the question, including its sequence number - type: 'text', # Type of input (text field) - name: "response[answers][#{id}]", # Name attribute for form submission - id: "responses_#{id}", # ID attribute for HTML element - value: answer&.comments # Value of the text field, uses safe navigation operator to handle nil + action: 'complete', + data: { + label: "Question ##{count}", + type: 'text', + name: "response[answers][#{id}]", + id: "responses_#{id}", + value: answer&.comments } - }.to_json # Converts the hash to JSON format + }.to_json end def view_completed_question(count, files) - { - action: 'view_completed_question', # Indicates the action type - data: { # Data related to the completed text field question - type: 'text', # Type of input (text field) - label: "Completed Question ##{count}", # Label for the completed question, including its sequence number - value: txt, # Value of the completed text field - break_before: break_before # Flag indicating whether to break before this question in the view - } - }.to_json # Converts the hash to JSON format + if question_type == 'TextField' && break_before + { + action: 'view_completed_question', + data: { + type: 'text', + label: "Completed Question ##{count}", + value: txt, + break_before: break_before + } + }.to_json + else + { + action: 'view_completed_question', + data: { + type: 'text', + label: "Completed Question ##{count}", + value: txt, + break_before: break_before + } + }.to_json + end end -end \ No newline at end of file +end diff --git a/app/models/text_response.rb b/app/models/text_response.rb index 3f7289480..a26912efd 100644 --- a/app/models/text_response.rb +++ b/app/models/text_response.rb @@ -1,66 +1,64 @@ -# Inherits from Question to define specific behaviors for text response questions class TextResponse < Question - # Validates the presence of the size attribute validates :size, presence: true def edit(_count) { - action: 'edit', # Indicates the action type - elements: [ # Array of elements comprising the edit form + action: 'edit', + elements: [ { - type: 'link', # Type of element (link) - text: 'Remove', # Text for the link - href: "/questions/#{id}", # URL for the link - method: 'delete' # HTTP method for the link + type: 'link', + text: 'Remove', + href: "/questions/#{id}", + method: 'delete' }, { - type: 'input', # Type of element (input) - input_type: 'text', # Type of input (text) + type: 'input', + input_type: 'text', size: 6, - name: "question[#{id}][seq]", # Name attribute for form submission - id: "question_#{id}_seq", # ID attribute for HTML element - value: seq.to_s # Value of the input field, converted to string + name: "question[#{id}][seq]", + id: "question_#{id}_seq", + value: seq.to_s }, { - type: 'textarea', # Type of element (textarea) - cols: 50, # Number of columns for the textarea - rows: 1, # Number of rows for the textarea - name: "question[#{id}][txt]", # Name attribute for form submission - id: "question_#{id}_txt", # ID attribute for HTML element - placeholder: 'Edit question content here', # Placeholder text for the textarea - value: txt # Value of the textarea + type: 'textarea', + cols: 50, + rows: 1, + name: "question[#{id}][txt]", + id: "question_#{id}_txt", + placeholder: 'Edit question content here', + value: txt }, { - type: 'input', # Type of element (input) - input_type: 'text', # Type of input (text) + type: 'input', + input_type: 'text', size: 10, - name: "question[#{id}][question_type]", # Name attribute for form submission - id: "question_#{id}_question_type", # ID attribute for HTML element - value: question_type, # Value of the input field - disabled: true # Indicates whether the input is disabled + name: "question[#{id}][question_type]", + id: "question_#{id}_question_type", + value: question_type, + disabled: true }, { - type: 'input', # Type of element (input) - input_type: 'text', # Type of input (text) + type: 'input', + input_type: 'text', size: 6, - name: "question[#{id}][size]", # Name attribute for form submission - id: "question_#{id}_size", # ID attribute for HTML element - value: size, # Value of the input field - label: 'Text area size' # Label for the input field + name: "question[#{id}][size]", + id: "question_#{id}_size", + value: size, + label: 'Text area size' } ] - }.to_json # Converts the hash to JSON format + }.to_json end def view_question_text { - action: 'view_question_text', # Indicates the action type - elements: [ # Array of elements comprising the question view - { type: 'text', value: txt }, # Text element displaying the question text - { type: 'text', value: question_type }, # Text element displaying the question type - { type: 'text', value: weight.to_s }, # Text element displaying the question weight (converted to string) - { type: 'text', value: '—' } # Placeholder text element (not specified in the requirement) + action: 'view_question_text', + elements: [ + { type: 'text', value: txt }, + { type: 'text', value: question_type }, + { type: 'text', value: weight.to_s }, + { type: 'text', value: '—' } ] - }.to_json # Converts the hash to JSON format + }.to_json end -end \ No newline at end of file +end diff --git a/app/models/unscored_question.rb b/app/models/unscored_question.rb index 80d2bc45b..ce754d763 100644 --- a/app/models/unscored_question.rb +++ b/app/models/unscored_question.rb @@ -1,17 +1,9 @@ -# Inherits from ChoiceQuestion, representing a type of question that doesn't contribute to the overall score class UnscoredQuestion < ChoiceQuestion - # to provide the specific logic for editing an unscored question. def edit; end - - # to return the structure or content necessary to display the text of the unscored question. def view_question_text; end - - # to provide the specific logic required for a respondent to complete an unscored question. def complete; end - # to return the structure or content necessary to display a completed unscored question, including - # any selected answers or respondent inputs. def view_completed_question; end -end +end \ No newline at end of file diff --git a/app/models/upload_file.rb b/app/models/upload_file.rb index d7d9f57c1..061d34356 100644 --- a/app/models/upload_file.rb +++ b/app/models/upload_file.rb @@ -2,68 +2,71 @@ class UploadFile < Question def edit(_count) { - action: 'edit', # Indicates the action type - elements: [ # Array of elements comprising the edit form + action: 'edit', + elements: [ { - type: 'link', # Type of element (link) - text: 'Remove', # Text for the link - href: "/questions/#{id}", # URL for the link - method: 'delete' # HTTP method for the link + type: 'link', + text: 'Remove', + href: "/questions/#{id}", + method: 'delete' }, { - type: 'input', # Type of element (input) - input_type: 'text', # Type of input (text) - name: "question[#{id}][seq]", # Name attribute for form submission - id: "question_#{id}_seq", # ID attribute for HTML element - value: seq.to_s # Value of the input field, converted to string + type: 'input', + input_type: 'text', + name: "question[#{id}][seq]", + id: "question_#{id}_seq", + value: seq.to_s }, { - type: 'input', # Type of element (input) - input_type: 'text', # Type of input (text) - name: "question[#{id}][id]", # Name attribute for form submission - id: "question_#{id}", # ID attribute for HTML element - value: id.to_s # Value of the input field, converted to string + type: 'input', + input_type: 'text', + name: "question[#{id}][id]", + id: "question_#{id}", + value: id.to_s }, { - type: 'textarea', # Type of element (textarea) - cols: 50, # Number of columns for the textarea - rows: 1, # Number of rows for the textarea - name: "question[#{id}][txt]", # Name attribute for form submission - id: "question_#{id}_txt", # ID attribute for HTML element - placeholder: 'Edit question content here', # Placeholder text for the textarea - value: txt # Value of the textarea + type: 'textarea', + cols: 50, + rows: 1, + name: "question[#{id}][txt]", + id: "question_#{id}_txt", + placeholder: 'Edit question content here', + value: txt }, { - type: 'input', # Type of element (input) - input_type: 'text', # Type of input (text) - size: 10, # Size of the input field - name: "question[#{id}][question_type]", # Name attribute for form submission - id: "question_#{id}_question_type", # ID attribute for HTML element - value: question_type, # Value of the input field - disabled: true # Indicates whether the input is disabled + type: 'input', + input_type: 'text', + size: 10, + name: "question[#{id}][question_type]", + id: "question_#{id}_question_type", + value: question_type, + disabled: true } ] - }.to_json # Converts the hash to JSON format + }.to_json end def view_question_text { - action: 'view_question_text', # Indicates the action type - elements: [ # Array of elements comprising the question view - { type: 'text', value: txt }, # Text element displaying the question text - { type: 'text', value: question_type }, # Text element displaying the question type - { type: 'text', value: weight.to_s }, # Text element displaying the question weight (converted to string) - { type: 'text', value: id.to_s }, # Text element displaying the question ID (converted to string) + action: 'view_question_text', + elements: [ + { type: 'text', value: txt }, + { type: 'text', value: question_type }, + { type: 'text', value: weight.to_s }, + { type: 'text', value: id.to_s }, { type: 'text', value: '—' } # Placeholder for non-applicable fields ] - }.to_json # Converts the hash to JSON format + }.to_json end + # Implement this method for completing a question def complete(count, answer = nil) + # Implement the logic for completing a question end - + # Implement this method for viewing a completed question by a student def view_completed_question(count, files) + # Implement the logic for viewing a completed question by a student end end diff --git a/db/migrate/20240315004809_create_question_advices.rb b/db/migrate/20240315004809_create_question_advices.rb index 56a542b39..4b2c128e7 100644 --- a/db/migrate/20240315004809_create_question_advices.rb +++ b/db/migrate/20240315004809_create_question_advices.rb @@ -1,20 +1,10 @@ -# Migration to create the question_advices table in the database class CreateQuestionAdvices < ActiveRecord::Migration[7.0] - # Method to define changes to be made to the database def change - # Creates a new table named question_advices create_table :question_advices do |t| - # Adds a reference to the questions table. Each question_advice is associated with a question. - # The `null: false` constraint ensures that every question_advice must have an associated question. t.references :question, null: false, foreign_key: true - - # Adds an integer column named score to store the score associated with the advice. t.integer :score - - # Adds a text column named advice to store the advice text. t.text :advice - # Adds created_at and updated_at columns automatically to track when question advices are created and updated. t.timestamps end end diff --git a/db/migrate/20240409000041_create_quiz_question_choices.rb b/db/migrate/20240409000041_create_quiz_question_choices.rb index a28e6a984..c1379f585 100644 --- a/db/migrate/20240409000041_create_quiz_question_choices.rb +++ b/db/migrate/20240409000041_create_quiz_question_choices.rb @@ -1,14 +1,11 @@ -# Define a migration for creating the quiz_question_choices table in the database class CreateQuizQuestionChoices < ActiveRecord::Migration[7.0] - # Method defining changes to be made to the database def change - # Create a new table named quiz_question_choices create_table :quiz_question_choices, id: :integer, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=latin1" do |t| - t.integer :question_id # Column for storing the associated question's ID - t.text :txt # Column for storing the text of the quiz question choice - t.boolean :iscorrect, default: false # Boolean column indicating whether the choice is correct, defaulting to false + t.integer :question_id + t.text :txt + t.boolean :iscorrect, default: false - t.timestamps # Adds created_at and updated_at columns automatically + t.timestamps end end end diff --git a/spec/models/checkbox_spec.rb b/spec/models/checkbox_spec.rb index a8af99d2d..31140a7c3 100644 --- a/spec/models/checkbox_spec.rb +++ b/spec/models/checkbox_spec.rb @@ -1,18 +1,12 @@ require 'rails_helper' -# RSpec tests for the Checkbox class RSpec.describe Checkbox do - # Setup a Checkbox instance before each test case let!(:checkbox) { Checkbox.new(id: 10, question_type: 'Checkbox', seq: 1.0, txt: 'test txt', weight: 11) } - # Setup an Answer instance to be used in the tests let!(:answer) { Answer.new(answer: 1) } - # Test suite for the #edit method describe '#edit' do it 'returns the JSON' do - # Call the edit method and store the result json = checkbox.edit(0) - # Define the expected JSON structure expected_json = { remove_button: { type: 'remove_button', action: 'delete', href: "/questions/10", text: 'Remove' }, seq: { type: 'seq', input_size: 6, value: 1.0, name: "question[10][seq]", id: "question_10_seq" }, @@ -20,65 +14,53 @@ type: { type: 'text', input_size: 10, disabled: true, value: 'Checkbox', name: "question[10][type]", id: "question_10_type" }, weight: { type: 'weight', placeholder: 'UnscoredQuestion does not need weight' } } - # Assert that the actual JSON matches the expected structure expect(json).to eq(expected_json) end end - # Test suite for the #complete method describe '#complete' do - # Context when an answer is provided + let(:checkbox) { Checkbox.new(id: 10, question_type: 'Checkbox', seq: 1.0, txt: 'test txt', weight: 11) } + let(:count) { 1 } + let(:answer) { OpenStruct.new(answer: 1) } # Mocking Answer object + context 'when an answer is provided' do it 'returns the expected completion structure' do - # Call the complete method and store the result result = checkbox.complete(count, answer) - # Check for the presence of the previous_question key and that inputs is an array + expect(result[:previous_question]).to be_present expect(result[:inputs]).to be_an(Array) - # Check that the label text matches the question text and that the script includes the correct function expect(result[:label]).to include(text: checkbox.txt) expect(result[:script]).to include("checkbox#{count}Changed()") - # Check that the checkbox is marked as checked expect(result[:inputs].last[:checked]).to be true end end - # Context when no answer is provided context 'when no answer is provided' do - let(:answer) { OpenStruct.new(answer: nil) } # Mock an empty answer + let(:answer) { OpenStruct.new(answer: nil) } it 'returns a structure with the checkbox not checked' do - # Call the complete method and store the result result = checkbox.complete(count, answer) - # Check that the checkbox is not marked as checked expect(result[:inputs].last[:checked]).to be false end end end - # Test suite for the #view_question_text method describe '#view_question_text' do it 'returns the JSON' do - # Call the view_question_text method and store the result json = checkbox.view_question_text - # Define the expected JSON structure expected_json = { content: 'test txt', type: 'Checkbox', weight: '11', checked_state: 'Checked/Unchecked' } - # Assert that the actual JSON matches the expected structure expect(json).to eq(expected_json) end end - # Test suite for the #view_completed_question method describe '#view_completed_question' do it 'returns the JSON' do - # Call the view_completed_question method with parameters and store the result json = checkbox.view_completed_question(0, answer) - # Define the expected JSON structure expected_json = { previous_question: { type: 'other' }, answer: { @@ -89,8 +71,7 @@ }, if_column_header: 'continue' } - # Assert that the actual JSON matches the expected structure expect(json).to eq(expected_json) end end -end \ No newline at end of file +end diff --git a/spec/models/criterion_spec.rb b/spec/models/criterion_spec.rb index 984ef6926..8adc5045b 100644 --- a/spec/models/criterion_spec.rb +++ b/spec/models/criterion_spec.rb @@ -1,90 +1,77 @@ require 'rails_helper' -# RSpec tests for the Criterion class, a type of ScoredQuestion RSpec.describe Criterion, type: :model do - # Setup for tests: defining a questionnaire and a criterion question associated with it let(:questionnaire) { Questionnaire.new(min_question_score: 0, max_question_score: 5) } let(:criterion) { Criterion.new(id: 1, question_type: 'Criterion', seq: 1.0, txt: 'test txt', weight: 1, questionnaire: questionnaire) } - - # Dummy answers for testing let(:answer_no_comments) { Answer.new(answer: 8) } let(:answer_comments) { Answer.new(answer: 3, comments: 'text comments') } - # Test suite for the #view_question_text method describe '#view_question_text' do it 'returns the JSON' do - # Calling the method and checking the returned JSON structure json = criterion.view_question_text expected_json = { - text: 'test txt', # The text of the question - question_type: 'Criterion', # The type of question - weight: 1, # The weight of the question - score_range: '0 to 5' # The score range, derived from the associated questionnaire + text: 'test txt', + question_type: 'Criterion', + weight: 1, + score_range: '0 to 5' } - expect(json).to eq(expected_json) # Asserting that the actual JSON matches the expected structure + expect(json).to eq(expected_json) end end - # Test suite for the #complete method describe '#complete' do it 'returns JSON without answer and no dropdown or scale specified' do - # Testing completion without specifying dropdown or scale json = criterion.complete(0, nil, 0, 5) expected_json = { - label: 'test txt' # The label should only contain the question text + label: 'test txt' } - expect(json).to include(expected_json) # Asserting the inclusion of the expected JSON + expect(json).to include(expected_json) end it 'returns JSON with a dropdown, including answer options' do - # Testing completion with dropdown options json = criterion.complete(0, nil, 0, 5, 'dropdown') - expected_options = (0..5).map { |score| { value: score, label: score.to_s } } # Expected options for the dropdown + expected_options = (0..5).map { |score| { value: score, label: score.to_s } } expected_json = { - label: 'test txt', # The question text - response_options: { # The options for responding to the question in a dropdown format + label: 'test txt', + response_options: { type: 'dropdown', comments: nil, current_answer: nil, options: expected_options, } } - expect(json).to include(expected_json) # Asserting the inclusion of the expected JSON + expect(json).to include(expected_json) end end - # Test suite for the #dropdown_criterion_question method describe '#dropdown_criterion_question' do it 'returns JSON for a dropdown without an answer selected' do - # Testing dropdown options without a selected answer json = criterion.dropdown_criterion_question(0, nil, 0, 5) - expected_options = (0..5).map { |score| { value: score, label: score.to_s } } # Expected options for the dropdown + expected_options = (0..5).map { |score| { value: score, label: score.to_s } } expected_json = { - type: 'dropdown', # The type of response input - comments: nil, # No comments - current_answer: nil, # No current answer - options: expected_options, # The options for the dropdown + type: 'dropdown', + comments: nil, + current_answer: nil, + options: expected_options, } - expect(json).to eq(expected_json) # Asserting that the actual JSON matches the expected structure + expect(json).to eq(expected_json) end end - # Test suite for the #scale_criterion_question method describe '#scale_criterion_question' do it 'returns JSON for a scale question without an answer selected' do - # Testing scale question options without a selected answer json = criterion.scale_criterion_question(0, nil, 0, 5) expected_json = { - type: 'scale', # The type of response input - min: 0, # Minimum value for the scale - max: 5, # Maximum value for the scale - comments: nil, # No comments - current_answer: nil, # No current answer - min_label: nil, # No minimum label - max_label: nil, # No maximum label - size: nil, # No size specified + type: 'scale', + min: 0, + max: 5, + comments: nil, + current_answer: nil, + min_label: nil, + max_label: nil, + size: nil, } - expect(json).to eq(expected_json) # Asserting that the actual JSON matches the expected structure + expect(json).to eq(expected_json) end end end diff --git a/spec/models/dropdown_spec.rb b/spec/models/dropdown_spec.rb index 1762f132b..605980f31 100644 --- a/spec/models/dropdown_spec.rb +++ b/spec/models/dropdown_spec.rb @@ -1,19 +1,12 @@ require 'rails_helper' -# RSpec test suite for the Dropdown model RSpec.describe Dropdown, type: :model do - - # Test suite for the #edit method describe '#edit' do - # Context when a count is provided to the method context 'when given a count' do it 'returns a JSON object with the edit form for a question' do - # Initialize a Dropdown instance with specific attributes dropdown = Dropdown.new(txt: "Some Text", type: "dropdown", weight: 1) - # Call the edit method with a count argument and store the result json_result = dropdown.edit(5) - # Define the expected JSON structure expected_result = { form: true, label: "Question 5:", @@ -25,43 +18,34 @@ weight: 1, type: 'dropdown' }.to_json - # Assert that the actual result matches the expected result expect(json_result).to eq(expected_result) end end end - # Test suite for the #view_question_text method describe '#view_question_text' do - # Using let to define a reusable Dropdown instance for the tests in this block let(:dropdown) { Dropdown.new } context 'when given valid inputs' do it 'returns the JSON for displaying the question text, type, weight, and score range' do - # Stub methods to return specific values allow(dropdown).to receive(:txt).and_return("Question 1") allow(dropdown).to receive(:type).and_return("Multiple Choice") allow(dropdown).to receive(:weight).and_return(1) - - # Define the expected JSON structure expected_json = { text: "Question 1", type: "Multiple Choice", weight: 1, score_range: "N/A" }.to_json - # Assert that the view_question_text method returns the expected JSON expect(dropdown.view_question_text).to eq(expected_json) end end end - # Test suite for the #complete method describe '#complete' do - let(:dropdown) { Dropdown.new } # Reusable Dropdown instance for these tests + let(:dropdown) { Dropdown.new } context 'when count is provided' do it 'generates JSON for a select input with the given count' do - count = 3 # Define a count for the options - # Define the expected JSON structure for dropdown options + count = 3 expected_json = { dropdown_options: [ { value: 1, selected: false }, @@ -69,16 +53,14 @@ { value: 3, selected: false } ] }.to_json - # Assert that the complete method returns the expected JSON expect(dropdown.complete(count)).to eq(expected_json) end end context 'when answer is provided' do it 'generates JSON with the provided answer selected' do - count = 3 # Define a count for the options - answer = 2 # Specify the selected answer - # Define the expected JSON structure with the selected answer + count = 3 + answer = 2 expected_json = { dropdown_options: [ { value: 1, selected: false }, @@ -86,15 +68,13 @@ { value: 3, selected: false } ] }.to_json - # Assert that the complete method returns the expected JSON with the answer selected expect(dropdown.complete(count, answer)).to eq(expected_json) end end context 'when answer is not provided' do it 'generates JSON without any answer selected' do - count = 3 # Define a count for the options - # Define the expected JSON structure with no selected answers + count = 3 expected_json = { dropdown_options: [ { value: 1, selected: false }, @@ -102,20 +82,17 @@ { value: 3, selected: false } ] }.to_json - # Assert that the complete method returns the expected JSON with no answers selected expect(dropdown.complete(count)).to eq(expected_json) end end end - # Test suite for the #complete_for_alternatives method describe '#complete_for_alternatives' do - let(:dropdown) { Dropdown.new } # Reusable Dropdown instance for these tests + let(:dropdown) { Dropdown.new } context 'when given an array of alternatives and an answer' do it 'returns JSON options with the selected alternative marked' do - alternatives = [1, 2, 3] # Define an array of alternative options - answer = 2 # Specify the selected answer - # Define the expected JSON structure with the selected alternative + alternatives = [1, 2, 3] + answer = 2 expected_json = { dropdown_options: [ { value: 1, selected: false }, @@ -123,9 +100,8 @@ { value: 3, selected: false } ] }.to_json - # Assert that the complete_for_alternatives method returns the expected JSON with the selected alternative expect(dropdown.complete_for_alternatives(alternatives, answer)).to eq(expected_json) end end end -end \ No newline at end of file +end diff --git a/spec/models/multiple_choice_checkbox_spec.rb b/spec/models/multiple_choice_checkbox_spec.rb index 3ce564926..819dc05b6 100644 --- a/spec/models/multiple_choice_checkbox_spec.rb +++ b/spec/models/multiple_choice_checkbox_spec.rb @@ -1,19 +1,13 @@ require 'rails_helper' -# RSpec test suite for MultipleChoiceCheckbox model RSpec.describe MultipleChoiceCheckbox, type: :model do - # Setup a sample instance of MultipleChoiceCheckbox for use in tests - let(:multiple_choice_checkbox) { MultipleChoiceCheckbox.new(id: 1, txt: 'Test question:', weight: 1) } + let(:multiple_choice_checkbox) { MultipleChoiceCheckbox.new(id: 1, txt: 'Test question:', weight: 1) } # Adjust as needed - # Tests for the #edit method describe '#edit' do it 'returns the JSON structure for editing' do - # Setup a mock object for QuizQuestionChoice with predefined attributes qc = instance_double('QuizQuestionChoice', iscorrect: true, txt: 'question text', id: 1) - # Stub the .where method to return a predefined set of choices allow(QuizQuestionChoice).to receive(:where).with(question_id: 1).and_return([qc, qc, qc, qc]) - # Define the expected JSON structure expected_structure = { "id" => 1, "question_text" => "Test question:", @@ -26,68 +20,50 @@ ] }.to_json - # Verify that the edit method returns the expected JSON structure expect(multiple_choice_checkbox.edit).to eq(expected_structure) end end - # Tests for the #isvalid method describe '#isvalid' do context 'when the question itself does not have txt' do it 'returns a JSON with error message' do - # Stub the txt method to return an empty string to simulate a question without text allow(multiple_choice_checkbox).to receive_messages(txt: '', id: 1) - # Define a set of question choices questions = { '1' => { txt: 'question text', iscorrect: '1' }, '2' => { txt: 'question text', iscorrect: '1' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } - # Define the expected JSON response with an error message expected_response = { valid: false, error: 'Please make sure all questions have text' }.to_json - # Verify that the isvalid method returns the expected response expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) end end context 'when a choice does not have txt' do it 'returns a JSON with error message' do - # Define a set of question choices with empty text questions = { '1' => { txt: '', iscorrect: '1' }, '2' => { txt: '', iscorrect: '1' }, '3' => { txt: '', iscorrect: '0' }, '4' => { txt: '', iscorrect: '0' } } - # Define the expected JSON response indicating the choices are valid despite missing text expected_response = { valid: true, error: nil }.to_json - # Verify that the isvalid method returns the expected response expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) end end context 'when no choices are correct' do it 'returns a JSON with error message' do - # Define a set of question choices with no correct answers questions = { '1' => { txt: 'question text', iscorrect: '0' }, '2' => { txt: 'question text', iscorrect: '0' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } - # Define the expected JSON response with an error message about needing a correct answer expected_response = { valid: false, error: 'Please select a correct answer for all questions' }.to_json - # Verify that the isvalid method returns the expected response expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) end end context 'when only one choice is correct' do it 'returns a JSON with error message' do - # Define a set of question choices with only one correct answer questions = { '1' => { txt: 'question text', iscorrect: '1' }, '2' => { txt: 'question text', iscorrect: '0' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } - # Define the expected JSON response with an error message for having only one correct answer in a multiple-choice question expected_response = { valid: false, error: 'A multiple-choice checkbox question should have more than one correct answer.' }.to_json - # Verify that the isvalid method returns the expected response expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) end end context 'when 2 choices are correct' do it 'returns valid status' do - # Define a set of question choices with two correct answers questions = { '1' => { txt: 'question text', iscorrect: '1' }, '2' => { txt: 'question text', iscorrect: '1' }, '3' => { txt: 'question text', iscorrect: '0' }, '4' => { txt: 'question text', iscorrect: '0' } } - # Define the expected JSON response indicating the question is valid expected_response = { valid: true, error: nil}.to_json - # Verify that the isvalid method returns the expected response expect(multiple_choice_checkbox.isvalid(questions)).to eq(expected_response) end end end -end \ No newline at end of file +end diff --git a/spec/models/scale_spec.rb b/spec/models/scale_spec.rb index 524f9a5ad..ce6bfcacb 100644 --- a/spec/models/scale_spec.rb +++ b/spec/models/scale_spec.rb @@ -1,12 +1,10 @@ + require 'rails_helper' -# RSpec tests for the Scale model RSpec.describe Scale, type: :model do - # Define the subject of the tests as a new instance of Scale subject { Scale.new } - # Set up initial attributes for the Scale instance before each test before do subject.txt = "Rate your experience" subject.type = "Scale" @@ -18,17 +16,13 @@ subject.answer = 3 end - # Test suite for the #edit method describe "#edit" do it 'returns a JSON object with question text, type, weight, and score range' do - # Create a new instance of Scale with specific attributes for testing scale = Scale.new(txt: 'Scale Question', type: 'scale', weight: 2, min_question_score: 0, max_question_score: 10) - # Call the edit method and store the result json_result = scale.edit - # Define the expected JSON structure expected_result = { form: true, label: "Question:", @@ -40,30 +34,24 @@ weight: 2, type: 'scale' }.to_json - # Assert that the actual result matches the expected result expect(json_result).to eq(expected_result) end end - # Test suite for the #view_question_text method describe "#view_question_text" do it "returns JSON containing the question text" do - # Define the expected JSON structure for the question text expected_json = { text: "Rate your experience", type: "Scale", weight: 1, score_range: "Poor 1 to 5 Excellent" }.to_json - # Assert that the view_question_text method returns the expected JSON expect(subject.view_question_text).to eq(expected_json) end end - # Test suite for the #complete method describe "#complete" do it "returns JSON with scale options" do - # Define the expected JSON structure for the scale options expected_json = { scale_options: [ { value: 1, selected: false }, { value: 2, selected: false }, @@ -71,31 +59,24 @@ { value: 4, selected: false }, { value: 5, selected: false } ] }.to_json - # Assert that the complete method returns the expected JSON expect(subject.complete).to eq(expected_json) end end - # Test suite for the #view_completed_question method describe "#view_completed_question" do context "when the question has been answered" do it "returns JSON with the count, answer, and questionnaire_max" do - # Define options to simulate the question being answered options = { count: 10, answer: 3, questionnaire_max: 50 } - # Define the expected JSON structure when the question is answered expected_json = options.to_json - # Assert that the view_completed_question method returns the expected JSON when answered expect(subject.view_completed_question(options)).to eq(expected_json) end end context "when the question has not been answered" do it "returns a message indicating the question was not answered" do - # Define the expected JSON structure when the question is not answered expected_json = { message: "Question not answered." }.to_json - # Assert that the view_completed_question method returns the expected JSON when not answered expect(subject.view_completed_question).to eq(expected_json) end end end -end \ No newline at end of file +end diff --git a/spec/models/text_area_spec.rb b/spec/models/text_area_spec.rb index dd7e09ce5..05e8f174f 100644 --- a/spec/models/text_area_spec.rb +++ b/spec/models/text_area_spec.rb @@ -1,14 +1,11 @@ require 'rails_helper' RSpec.describe TextArea do - # Create a TextArea instance let(:text_area) { TextArea.create(size: '34,1') } - # Create an Answer instance let!(:answer) { Answer.create(comments: 'test comment') } describe '#complete' do context 'when count is provided' do - # Test case for generating JSON for a textarea input it 'generates JSON for a textarea input' do result = JSON.parse(text_area.complete(1)) expect(result['action']).to eq('complete') @@ -16,7 +13,6 @@ expect(result['data']['size']).to eq('34,1') end - # Test case for including existing comments in the textarea input it 'includes any existing comments in the textarea input' do result = JSON.parse(text_area.complete(1, answer)) expect(result['data']['comment']).to eq('test comment') @@ -24,7 +20,6 @@ end context 'when count is not provided' do - # Test case for generating JSON with default size for the textarea input it 'generates JSON with default size for the textarea input' do text_area = TextArea.create(size: nil) result = JSON.parse(text_area.complete(nil)) @@ -35,7 +30,6 @@ describe 'view_completed_question' do context 'when given a count and an answer' do - # Test case for returning the formatted JSON for the completed question it 'returns the formatted JSON for the completed question' do result = JSON.parse(text_area.view_completed_question(1, answer)) expect(result['action']).to eq('view_completed_question') diff --git a/spec/models/text_field_spec.rb b/spec/models/text_field_spec.rb index 83d6b562e..66f52b830 100644 --- a/spec/models/text_field_spec.rb +++ b/spec/models/text_field_spec.rb @@ -1,14 +1,11 @@ require 'rails_helper' RSpec.describe TextField, type: :model do - # Create a TextField instance let(:question) { TextField.create(txt: 'Sample Text Question', question_type: 'TextField', size: 'medium', break_before: true) } - # Create an Answer instance let(:answer) { Answer.new(comments: 'This is a sample answer.') } describe '#complete' do context 'when count is provided' do - # Test case for returning JSON data for a paragraph with a label and input fields it 'returns JSON data for a paragraph with a label and input fields' do result = JSON.parse(question.complete(1)) expect(result['action']).to eq('complete') @@ -18,7 +15,6 @@ end context 'when count and answer are provided' do - # Test case for returning JSON data with pre-filled comment value it 'returns JSON data with pre-filled comment value' do result = JSON.parse(question.complete(1, answer)) expect(result['data']['value']).to eq(answer.comments) @@ -28,7 +24,6 @@ describe '#view_completed_question' do context "when the question type is 'TextField' and break_before is true" do - # Test case for returning the formatted JSON for the completed question it 'returns the formatted JSON for the completed question' do result = JSON.parse(question.view_completed_question(1, [])) expect(result['data']['type']).to eq('text') @@ -37,14 +32,12 @@ end context "when the question type is not 'TextField' or break_before is false" do - # Create a TextField instance with different properties let(:non_text_field_question) { TextField.create(txt: 'Non-text question', question_type: 'NotTextField', size: 'small', break_before: false) } - # Test case for returning the formatted JSON for the completed question it 'returns the formatted JSON for the completed question' do result = JSON.parse(non_text_field_question.view_completed_question(1, [])) expect(result['data']['break_before']).to be false end end end -end \ No newline at end of file +end diff --git a/spec/models/text_response_spec.rb b/spec/models/text_response_spec.rb index a94161e6b..f326e9653 100644 --- a/spec/models/text_response_spec.rb +++ b/spec/models/text_response_spec.rb @@ -1,38 +1,32 @@ require 'rails_helper' RSpec.describe TextResponse, type: :model do - # Create a TextResponse instance let(:text_response) { TextResponse.create(seq: '001', txt: 'Sample question content', question_type: 'TextResponse', size: 'medium', weight: 10) } describe '#edit' do - # Parse the JSON result of the edit method let(:result) { JSON.parse(text_response.edit(1)) } - # Test case for verifying correct action in the JSON result it 'returns JSON for editing with correct action' do expect(result["action"]).to eq('edit') end - # Test case for verifying the presence of elements for editing question it 'includes elements for editing question' do expect(result["elements"].length).to be > 0 end end describe '#view_question_text' do - # Parse the JSON result of the view_question_text method let(:result) { JSON.parse(text_response.view_question_text) } - # Test case for verifying correct action in the JSON result it 'returns JSON for viewing question text with correct action' do expect(result["action"]).to eq('view_question_text') end - # Test case for verifying the presence of question text, question_type, and weight in elements it 'includes the question text, question_type, and weight in elements' do expect(result["elements"].any? { |e| e["value"] == 'Sample question content' }).to be true expect(result["elements"].any? { |e| e["value"] == 'TextResponse' }).to be true expect(result["elements"].any? { |e| e["value"].match?(/^\d+$/) }).to be true end end -end \ No newline at end of file + +end diff --git a/spec/models/upload_file_spec.rb b/spec/models/upload_file_spec.rb index 17e828e72..97897ada0 100644 --- a/spec/models/upload_file_spec.rb +++ b/spec/models/upload_file_spec.rb @@ -1,56 +1,48 @@ require 'rails_helper' RSpec.describe UploadFile do - # Create an UploadFile instance with basic attributes + # Adjust the let statement to not include 'type' and 'weight' if they are not recognized attributes. let(:upload_file) { UploadFile.new(id: 1, seq: '1', txt: 'Sample question content', question_type: 'UploadFile') } describe '#edit' do context 'when given a count' do - # Parse the JSON response from the edit method let(:json_response) { JSON.parse(upload_file.edit(1)) } - # Test case for verifying correct action and structure in the JSON response it 'returns JSON with a correct structure for editing' do expect(json_response["action"]).to eq('edit') expect(json_response["elements"]).to be_an(Array) end - # Test case for verifying the presence of "Remove" link in the elements it 'includes a "Remove" link in the elements' do link_element = json_response["elements"].find { |el| el["text"] == "Remove" } expect(link_element).not_to be_nil expect(link_element["href"]).to include("/questions/1") end - # Test case for verifying the presence of an input field for the sequence it 'includes an input field for the sequence' do seq_element = json_response["elements"].find { |el| el["name"]&.include?("seq") } expect(seq_element).not_to be_nil expect(seq_element["value"]).to eq('1') end - # Test case for verifying the presence of an input field for the id it 'includes an input field for the id' do id_element = json_response["elements"].find { |el| el["name"]&.include?("id") } expect(id_element).not_to be_nil expect(id_element["value"]).to eq('1') end - # Test case for verifying the presence of a textarea for editing the question content it 'includes a textarea for editing the question content' do textarea_element = json_response["elements"].find { |el| el["name"]&.include?("txt") } expect(textarea_element).not_to be_nil expect(textarea_element["value"]).to eq('Sample question content') end - # Test case for verifying the presence of an input field for the question type, disabled it 'includes an input field for the question type, disabled' do type_element = json_response["elements"].find { |el| el["name"]&.include?("question_type") } expect(type_element).not_to be_nil expect(type_element["disabled"]).to be true end - # Test case for verifying that weight element is not present in the JSON response it 'does not include an explicit cell for weight, as it is not applicable' do weight_element = json_response["elements"].none? { |el| el.has_key?("weight") } expect(weight_element).to be true @@ -60,10 +52,8 @@ describe '#view_question_text' do context 'when given valid input' do - # Parse the JSON response from the view_question_text method let(:json_response) { JSON.parse(upload_file.view_question_text) } - # Test case for verifying correct action and structure in the JSON response it 'returns JSON for displaying a question' do expect(json_response["action"]).to eq('view_question_text') expect(json_response["elements"].map { |el| el["value"] }).to include('Sample question content', 'UploadFile', "", '1','—') @@ -72,16 +62,14 @@ end describe '#complete' do - # Placeholder test case for the complete method logic it 'implements the logic for completing a question' do # Write your test for the complete method logic here. end end describe '#view_completed_question' do - # Placeholder test case for the view_completed_question method logic it 'implements the logic for viewing a completed question by a student' do # Write your test for the view_completed_question method logic here. end end -end \ No newline at end of file +end