Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ GEM
PLATFORMS
aarch64-linux
arm64-darwin-22
arm64-darwin-23
x64-mingw-ucrt
x86_64-linux

Expand Down
Binary file added app/.DS_Store
Binary file not shown.
Binary file added app/controllers/.DS_Store
Binary file not shown.
Binary file added app/controllers/api/.DS_Store
Binary file not shown.
162 changes: 64 additions & 98 deletions app/controllers/api/v1/questions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,131 +1,97 @@
class Api::V1::QuestionsController < ApplicationController
before_action :set_question, only: [:show, :update]

# GET /questions
def action_allowed?
has_role?('Instructor')
end
# Index method returns the list of questions JSON object
# GET on /questions
def index
@questions = Question.order(:id)
@questions = Item.order(:id)
render json: @questions, status: :ok
end

# Show method returns the question object with id - {:id}
# GET on /questions/:id
# GET /questions/:id
def show
begin
@question = Question.find(params[:id])
render json: @question, status: :ok
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found
end
end
@item = Item.find(params[:id])

# Create method returns the JSON object of created question
# POST on /questions
def create
questionnaire_id = params[:questionnaire_id]
questionnaire = Questionnaire.find(questionnaire_id)
question = questionnaire.questions.build(
txt: params[:txt],
question_type: params[:question_type],
break_before: true
)
# Choose the correct strategy based on item type
strategy = get_strategy_for_item(@item)

case question.question_type
when 'Scale'
question.weight = params[:weight]
question.max_label = 'Strongly agree'
question.min_label = 'Strongly disagree'
when 'Cake', 'Criterion'
question.weight = params[:weight]
question.max_label = 'Strongly agree'
question.min_label = 'Strongly disagree'
question.size = '50, 3'
when 'Dropdown'
question.alternatives = '0|1|2|3|4|5'
when 'TextArea'
question.size = '60, 5'
when 'TextField'
question.size = '30'
end
# Render the item using the strategy
@rendered_item = strategy.render(@item)

if question.save
render json: question, status: :created
else
render json: question.errors.full_messages.to_sentence, status: :unprocessable_entity
end
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found and return
end


# Destroy method deletes the question with id - {:id}
# DELETE on /questions/:id
def destroy
begin
@question = Question.find(params[:id])
@question.destroy
render json: { item: @item, rendered_item: @rendered_item }, status: :ok
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found and return
render json: { error: "Question not found" }, status: :not_found
end
end

# show_all method returns all questions associated to a questionnaire with id - {:id}
# GET on /questions/show_all/questionnaire/:id
def show_all
begin
@questionnaire = Questionnaire.find(params[:id])
@questions = @questionnaire.questions
render json: @questions, status: :ok
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found
# POST /questions
def create
questionnaire_id = params[:questionnaire_id]
questionnaire = Questionnaire.find(questionnaire_id)

# Create the new Item (item)
item = questionnaire.items.build(
txt: params[:txt],
question_type: params[:question_type],
seq: params[:seq],
break_before: true
)

# Add attributes based on the item type
case item.question_type
when 'Scale'
item.weight = params[:weight]
item.max_label = 'Strongly agree'
item.min_label = 'Strongly disagree'
item.max_value = params[:max_value] || 5
when 'Dropdown'
item.alternatives = '0|1|2|3|4|5'
when 'TextArea'
item.size = '60, 5'
when 'TextField'
item.size = '30'
end
end

# Delete_all method deletes all the questions and returns the status code
# DELETE on /questions/delete_all/questionnaire/<questionnaire_id>
# Endpoint to delete all questions associated to a particular questionnaire.

def delete_all
begin
questionnaire = Questionnaire.find(params[:id])
if questionnaire.questions.empty?
render json: "No questions associated with questionnaire ID #{params[:id]}.", status: :unprocessable_entity and return
end

questionnaire.questions.destroy_all
render json: "All questions for questionnaire ID #{params[:id]} have been deleted.", status: :ok
rescue ActiveRecord::RecordNotFound
render json: "Questionnaire ID #{params[:id]} not found.", status: :not_found and return
if item.save
render json: item, status: :created
else
render json: { error: item.errors.full_messages.to_sentence }, status: :unprocessable_entity
end
end

# Update method updates the question with id - {:id} and returns its JSON object
# PUT on /questions/:id
# PUT /questions/:id
def update
begin
@question = Question.find(params[:id])
if @question.update(question_params)
render json: @question, status: :ok and return
else
render json: "Failed to update the question.", status: :unprocessable_entity and return
end
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found and return
if @item.update(question_params)
render json: @item, status: :ok
else
render json: { error: @item.errors.full_messages.to_sentence }, status: :unprocessable_entity
end
end

private

# GET on /questions/types
def types
types = Question.distinct.pluck(:question_type)
render json: types.to_a, status: :ok
def set_question
@item = Item.find(params[:id])
end

private

# Only allow a list of trusted parameters through.
def question_params
params.permit(:txt, :weight, :seq, :questionnaire_id, :question_type, :size,
:alternatives, :break_before, :max_label, :min_label)
params.require(:question).permit(:txt, :question_type, :seq, :weight, :max_value, :size, :alternatives)
end

def get_strategy_for_item(item)
case item.question_type
when 'Dropdown'
Strategies::DropdownStrategy.new
when 'Scale'
Strategies::ScaleStrategy.new
# You can add more strategies as needed
else
raise "Strategy for this item type not defined"
end
end
end
19 changes: 19 additions & 0 deletions app/helpers/question_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module QuestionHelper
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: 'item',
input_value: txt,
min_question_score: min_question_score,
max_question_score: max_question_score,
weight: weight,
type: type
}
end

def view_item_text_common(text, type, weight, score_range)
{ text: text, type: type, weight: weight, score_range: score_range }
end
end
12 changes: 6 additions & 6 deletions app/helpers/scorable_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ def calculate_total_score
question_ids = scores.map(&:question_id)

# We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids
questions = Question.find_with_order(question_ids)
questions = Item.find_with_order(question_ids)

scores.each_with_index do |score, idx|
question = questions[idx]
sum += score.answer * questions[idx].weight if !score.answer.nil? && question.scorable?
item = questions[idx]
sum += score.answer * questions[idx].weight if !score.answer.nil? && item.scorable?
end

sum
Expand All @@ -38,7 +38,7 @@ def maximum_score
question_ids = scores.map(&:question_id)

# We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids
questions = Question.find_with_order(question_ids)
questions = Item.find_with_order(question_ids)

scores.each_with_index do |score, idx|
total_weight += questions[idx].weight unless score.answer.nil? || !questions[idx].scorable?
Expand All @@ -54,13 +54,13 @@ def maximum_score

def questionnaire_by_answer(answer)
if answer.nil?
# Answers can be nil in cases such as "Upload File" being the only question.
# Answers can be nil in cases such as "Upload File" being the only item.
map = ResponseMap.find(map_id)
# E-1973 either get the assignment from the participant or the map itself
assignment = map.response_assignment
questionnaire = Questionnaire.find(assignment.review_questionnaire_id)
else
questionnaire = Question.find(answer.question_id).questionnaire
questionnaire = Item.find(answer.question_id).questionnaire
end
questionnaire
end
Expand Down
53 changes: 53 additions & 0 deletions app/models/Item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
class Item < ApplicationRecord
before_create :set_seq
belongs_to :questionnaire # each item belongs to a specific questionnaire
has_many :answers, dependent: :destroy
has_many :choices, dependent: :destroy
attr_accessor :choice_strategy

validates :seq, presence: true, numericality: true # sequence must be numeric
validates :txt, length: { minimum: 0, allow_nil: false, message: "can't be nil" } # text content must be provided
validates :question_type, presence: true # user must define the item type
validates :break_before, presence: true

def scorable?
false
end

def set_seq
self.seq = questionnaire.items.size + 1
end

def as_json(options = {})
super(options.merge({
only: %i[txt weight seq question_type size alternatives break_before min_label max_label created_at updated_at],
include: {
questionnaire: { only: %i[name id] }
}
})).tap do |hash|
end
end

def strategy
case question_type
when 'dropdown'
self.choice_strategy = Strategies::DropdownStrategy.new
when 'multiple_choice'
self.choice_strategy = Strategies::MultipleChoiceStrategy.new
when 'scale'
self.choice_strategy = Strategies::ScaleStrategy.new
else
raise "Unknown item type: #{question_type}"
end
end

# Use strategy to render the item
def render
strategy.render(self)
end

# Use strategy to validate the item
def validate_item
strategy.validate(self)
end
end
12 changes: 12 additions & 0 deletions app/models/Strategies/choice_strategy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Strategies
class ChoiceStrategy
def render(item)
raise NotImplementedError, "You must implement the render method"
end

def validate(item)
raise NotImplementedError, "You must implement the validate method"
end
end
end

15 changes: 15 additions & 0 deletions app/models/Strategies/dropdown_strategy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Strategies
class DropdownStrategy < ChoiceStrategy
def render(item)
# render the dropdown options as HTML
item.alternatives.map { |alt| "<option value='#{alt}'>#{alt}</option>" }.join
end

def validate(item)
# Validate that alternatives are non-empty
if item.alternatives.empty?
item.errors.add(:alternatives, "can't be empty for a dropdown")
end
end
end
end
15 changes: 15 additions & 0 deletions app/models/Strategies/multiple_choice_strategy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Strategies
class MultipleChoiceStrategy < ChoiceStrategy
def render(item)
# Render radio buttons for multiple choice
item.alternatives.map { |alt| "<input type='radio' name='item_#{item.id}' value='#{alt}'> #{alt}</input>" }.join
end

def validate(item)
# Validate that alternatives are non-empty
if item.alternatives.empty?
item.errors.add(:alternatives, "can't be empty for multiple choice")
end
end
end
end
15 changes: 15 additions & 0 deletions app/models/Strategies/scale_strategy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Strategies
class ScaleStrategy < ChoiceStrategy
def render(item)
# Render scale (numeric sequence of options)
item.alternatives.map { |alt| "<option value='#{alt}'>#{alt}</option>" }.join
end

def validate(item)
# Validate that alternatives are numeric
unless item.alternatives.all? { |alt| alt.match?(/^\d+$/) }
item.errors.add(:alternatives, "must be numeric for scale items")
end
end
end
end
2 changes: 1 addition & 1 deletion app/models/answer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class Answer < ApplicationRecord
belongs_to :response
belongs_to :question
belongs_to :item
end
Loading