Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ab69594
Fix add, see edit questionnaires
ruju4a Sep 24, 2026
990938a
fixed failing tests
ruju4a Sep 28, 2026
6f9bf45
fixed failing tests
ruju4a Sep 28, 2026
b2f8c54
Revert "fixed failing tests"
ruju4a Sep 28, 2026
53e1e9a
Revert schema.rb and docker-compose.yml to upstream/main
ruju4a Oct 6, 2026
8a31aa5
Address review: move question types to Item::QUESTION_TYPES constant
ruju4a Oct 6, 2026
d6b9d97
Refactor questions to items and fix tests
ruju4a Oct 7, 2026
11e6ef9
Fix CI tests and address PR comments
ruju4a Oct 7, 2026
70037cd
Rename Strategies to strategies to fix Zeitwerk eager loading
ruju4a Oct 7, 2026
24ce6f5
Fix syntax error truncating file_upload.rb which caused Zeitwerk to c…
ruju4a Oct 7, 2026
9ad5acc
Fix file_upload.rb prompt reference and join_team_request_spec.rb syn…
ruju4a Oct 8, 2026
4f4bbe0
Fix failing specs that referenced prompt instead of txt
ruju4a Oct 8, 2026
c911b7d
Fix prompt to txt in multiple_choice_checkbox model
ruju4a Oct 8, 2026
d65a2ea
Auto-correct rubocop issues in items_spec.rb
ruju4a Oct 8, 2026
4d17289
Disable RuboCop BlockLength and LineLength in items_spec.rb
ruju4a Oct 8, 2026
661f10e
Fix mailer spec, scale model prompt typo, and Itemnaire typo in items…
ruju4a Oct 8, 2026
5b95d62
Fix item_score back to question_score in items_spec.rb
ruju4a Oct 8, 2026
ad8d8c6
Fix RuboCop offenses flagged in CI
ruju4a Oct 8, 2026
0017fef
Fix failing items/questions specs: permit txt in item params, restore…
ruju4a Oct 8, 2026
7cd0c05
Remove duplicate QuestionsController, routes and spec; keep /items only
ruju4a Oct 8, 2026
746fcf2
Point item links at /items instead of /questions
ruju4a Oct 8, 2026
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
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
class QuestionsController < ApplicationController
before_action :set_question, only: [:show, :update]
class ItemsController < ApplicationController # rubocop:disable Metrics/ClassLength
before_action :set_item, only: %i[show update]

# GET /questions
# GET /items (instructors only)
def action_allowed?
current_user_has_role?('Instructor')
end
# Index method returns the list of questions JSON object
# GET on /questions

# Index method returns the list of items as a JSON object
# GET /items
def index
@questions = Item.order(:id)
render json: @questions, status: :ok
@items = Item.order(:id)
render json: @items, status: :ok
end

# GET /questions/:id
# GET /items/:id
def show
begin
@item = Item.find(params[:id])
@item = Item.find(params[:id])

# Choose the correct strategy based on item type
strategy = get_strategy_for_item(@item)
# Choose the correct strategy based on item type
strategy = get_strategy_for_item(@item)

# Render the item using the strategy
@rendered_item = strategy.render(@item)
# Render the item using the strategy
@rendered_item = strategy.render(@item)

render json: { item: @item, rendered_item: @rendered_item }, status: :ok
rescue ActiveRecord::RecordNotFound
render json: { error: "Question not found" }, status: :not_found
end
render json: { item: @item, rendered_item: @rendered_item }, status: :ok
rescue ActiveRecord::RecordNotFound
render json: { error: 'Item not found' }, status: :not_found
end

# GET /questions/show_all/questionnaire/:id
# GET /items/show_all/questionnaire/:id
def show_all
questionnaire = Questionnaire.find(params[:id])
items = questionnaire.items.order(:id)
Expand All @@ -38,33 +37,20 @@ def show_all
render json: { error: "Couldn't find Questionnaire" }, status: :not_found
end

# POST /questions
# POST /items
def create
questionnaire_id = params[:questionnaire_id]
questionnaire = Questionnaire.find(questionnaire_id)

# Create the new Item (item)
# Build the new Item using the frontend-facing param names
item = questionnaire.items.build(
txt: params[:txt],
txt: params[:prompt], # prompt maps to the txt DB column
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
apply_type_defaults(item)

if item.save
render json: item, status: :created
Expand All @@ -73,9 +59,9 @@ def create
end
end

# PUT /questions/:id
# PUT /items/:id
def update
if @item.update(question_params)
if @item.update(item_params)
render json: @item, status: :ok
else
render json: { error: @item.errors.full_messages.to_sentence }, status: :unprocessable_entity
Expand All @@ -90,32 +76,59 @@ def destroy
render json: { error: "Couldn't find Item" }, status: :not_found
end

# DELETE /questions/delete_all/questionnaire/:id
# DELETE /items/delete_all/questionnaire/:id
def delete_all
questionnaire = Questionnaire.find(params[:id])
if questionnaire.items.delete_all
render json: { message: "All questions deleted" }, status: :ok
render json: { message: 'All questions deleted' }, status: :ok
else
render json: { error: "Deletion failed" }, status: :unprocessable_entity
render json: { error: 'Deletion failed' }, status: :unprocessable_entity
end
rescue ActiveRecord::RecordNotFound
render json: { error: "Couldn't find Questionnaire" }, status: :not_found
end

def types
types = Item.pluck(:question_type).uniq
render json: types, status: :ok
render json: Item::QUESTION_TYPES, status: :ok
end


private

def set_question
# Set size and alternatives from structured params
def apply_type_defaults(item)
case item.question_type
when 'Scale'
item.weight = params[:weight]
item.max_label = 'Strongly agree'
item.min_label = 'Strongly disagree'
when 'Dropdown'
item.alternatives = '0|1|2|3|4|5'
else
apply_size(item)
end
end

def apply_size(item)
case item.question_type
when 'TextArea'
# rows and columns stored as "columns,rows" in the size field
item.size = "#{params[:columns] || 60},#{params[:rows] || 5}"
when 'TextField'
item.size = (params[:columns] || 30).to_s
end
end

def set_item
@item = Item.find(params[:id])
end

def question_params
params.require(:question).permit(:txt, :question_type, :seq, :weight, :max_value, :size, :alternatives)
def item_params
# The frontend sends :prompt, which maps to the txt column.
permitted = params.require(:item).permit(:prompt, :txt, :question_type, :seq, :weight, :alternatives,
:size, :break_before, :min_label, :max_label)
prompt = permitted.delete(:prompt)
permitted[:txt] = prompt if prompt
permitted
end

def get_strategy_for_item(item)
Expand All @@ -126,7 +139,7 @@ def get_strategy_for_item(item)
Strategies::ScaleStrategy.new
# You can add more strategies as needed
else
raise "Strategy for this item type not defined"
raise 'Strategy for this item type not defined'
end
end
end
109 changes: 59 additions & 50 deletions app/controllers/questionnaires_controller.rb
Original file line number Diff line number Diff line change
@@ -1,46 +1,55 @@
class QuestionnairesController < ApplicationController

# Index method returns the list of JSON objects of the questionnaire
# Index method returns the list of JSON objects of the questionnaire.
# Supports an optional ?type= query parameter to filter by questionnaire_type.
# GET on /questionnaires
# GET on /questionnaires?type=ReviewQuestionnaire
def index
@questionnaires = Questionnaire.order(:id)
if params[:type].present?
unless Questionnaire::QUESTIONNAIRE_TYPES.include?(params[:type])
render json: "Invalid questionnaire type: #{params[:type]}", status: :unprocessable_entity and return
end

@questionnaires = Questionnaire.by_type(params[:type]).order(:id)
else
@questionnaires = Questionnaire.order(:id)
end
render json: @questionnaires, status: :ok and return
end

# Show method returns the JSON object of questionnaire with id = {:id}
# GET on /questionnaires/:id
def show
begin
@questionnaire = Questionnaire.find(params[:id])
render json: @questionnaire, status: :ok and return
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found and return
end
@questionnaire = Questionnaire.find(params[:id])
render json: @questionnaire, status: :ok and return
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found and return
end

# Create method creates a questionnaire and returns the JSON object of the created questionnaire

# Create method creates a questionnaire and returns the JSON object of the created questionnaire.
# Instantiates the correct subclass (e.g. ReviewQuestionnaire) so that subclass callbacks
# such as after_initialize run and set display_type correctly.
# POST on /questionnaires
# Instructor Id statically defined since implementation of Instructor model is out of scope of E2345.
def create
begin
@questionnaire = Questionnaire.new(questionnaire_params)
@questionnaire.display_type = sanitize_display_type(@questionnaire.questionnaire_type)
@questionnaire.save!
render json: @questionnaire, status: :created and return
rescue ActiveRecord::RecordInvalid
render json: $ERROR_INFO.to_s, status: :unprocessable_entity
type = params.dig(:questionnaire, :questionnaire_type)
unless Questionnaire::QUESTIONNAIRE_TYPES.include?(type)
render json: "Invalid questionnaire type: #{type}", status: :unprocessable_entity and return
end

klass = type.constantize
@questionnaire = klass.new(questionnaire_params)
@questionnaire.save!
render json: @questionnaire, status: :created and return
rescue ActiveRecord::RecordInvalid
render json: $ERROR_INFO.to_s, status: :unprocessable_entity
end

# Destroy method deletes the questionnaire object with id- {:id}
# DELETE on /questionnaires/:id
def destroy
begin
@questionnaire = Questionnaire.find(params[:id])
@questionnaire.delete
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found and return
end
@questionnaire = Questionnaire.find(params[:id])
@questionnaire.delete
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found and return
end

# Update method updates the questionnaire object with id - {:id} and returns the updated questionnaire JSON object
Expand All @@ -54,48 +63,48 @@ def update
render json: @questionnaire.errors.full_messages, status: :unprocessable_entity
end
end

# Copy method creates a copy of questionnaire with id - {:id} and return its JSON object
# POST on /questionnaires/copy/:id
def copy
begin
@questionnaire = Questionnaire.copy(params)
render json: @questionnaire, status: :ok and return
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found and return
rescue ActiveRecord::RecordInvalid
render json: $ERROR_INFO.to_s, status: :unprocessable_entity
end
@questionnaire = Questionnaire.copy(params)
render json: @questionnaire, status: :ok and return
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found and return
rescue ActiveRecord::RecordInvalid
render json: $ERROR_INFO.to_s, status: :unprocessable_entity
end

# Toggle access method toggles the private variable of the questionnaire with id - {:id} and return its JSON object
# GET on /questionnaires/toggle_access/:id

def toggle_access
begin
@questionnaire = Questionnaire.find(params[:id])
@questionnaire.toggle!(:private)
@access = @questionnaire.private ? 'private' : 'public'
render json: "The questionnaire \"#{@questionnaire.name}\" has been successfully made #{@access}. ",
status: :ok
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found
rescue ActiveRecord::RecordInvalid
render json: $ERROR_INFO.to_s, status: :unprocessable_entity
end
@questionnaire = Questionnaire.find(params[:id])
@questionnaire.toggle!(:private)
@access = @questionnaire.private ? 'private' : 'public'
render json: "The questionnaire \"#{@questionnaire.name}\" has been successfully made #{@access}. ",
status: :ok
rescue ActiveRecord::RecordNotFound
render json: $ERROR_INFO.to_s, status: :not_found
rescue ActiveRecord::RecordInvalid
render json: $ERROR_INFO.to_s, status: :unprocessable_entity
end

private

def questionnaire_params
params.require(:questionnaire).permit(:name, :questionnaire_type, :private, :min_question_score, :max_question_score, :instructor_id)
params.require(:questionnaire).permit(:name, :questionnaire_type, :private, :min_question_score,
:max_question_score, :instructor_id,
items_attributes: %i[id txt question_type weight seq min_label max_label
alternatives size break_before _destroy])
end

def sanitize_display_type(type)
display_type = type.split('Questionnaire')[0]
if %w[AuthorFeedback CourseSurvey TeammateReview GlobalSurvey AssignmentSurvey BookmarkRating].include?(display_type)
display_type = (display_type.split(/(?=[A-Z])/)).join('%')
if %w[AuthorFeedback CourseSurvey TeammateReview GlobalSurvey AssignmentSurvey
BookmarkRating].include?(display_type)
display_type = display_type.split(/(?=[A-Z])/).join('%')
end
display_type
end

end
end
14 changes: 0 additions & 14 deletions app/models/Strategies/choice_strategy.rb

This file was deleted.

2 changes: 1 addition & 1 deletion app/models/checkbox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def edit_remove_button(count)
{
type: 'remove_button',
action: 'delete',
href: "/questions/#{id}",
href: "/items/#{id}",
text: 'Remove'
}
end
Expand Down
2 changes: 1 addition & 1 deletion app/models/criterion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def max_score

def edit
{
remove_link: "/questions/#{id}",
remove_link: "/items/#{id}",
sequence_input: seq.to_s,
question_text: txt,
question_type: question_type,
Expand Down
Loading
Loading