Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
32 changes: 24 additions & 8 deletions app/controllers/questionnaires_controller.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
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

Expand All @@ -18,13 +28,19 @@ def show
end
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)
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
Expand Down Expand Up @@ -87,7 +103,7 @@ def toggle_access
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: [:id, :txt, :question_type, :weight, :seq, :min_label, :max_label, :alternatives, :size, :break_before, :_destroy])
end

def sanitize_display_type(type)
Expand Down
3 changes: 1 addition & 2 deletions app/controllers/questions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ def create
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'
Expand Down Expand Up @@ -103,7 +102,7 @@ def delete_all
end

def types
types = Item.pluck(:question_type).uniq
types = ["Criterion", "Scale", "Dropdown", "Multiple choice", "Text area", "Text field", "Grid"]
Comment thread
efg marked this conversation as resolved.
Outdated
render json: types, status: :ok
end

Expand Down
6 changes: 3 additions & 3 deletions app/models/Item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

class Item < ApplicationRecord
before_create :set_seq
belongs_to :questionnaire # each item belongs to a specific questionnaire
belongs_to :questionnaire, inverse_of: :items # each item belongs to a specific questionnaire
has_many :answers, dependent: :destroy, foreign_key: 'item_id'
attr_accessor :choice_strategy

Expand All @@ -20,12 +20,12 @@ def scored?
end

def set_seq
self.seq = questionnaire.items.size + 1
self.seq = questionnaire.items.size
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],
only: %i[id txt weight seq question_type size alternatives break_before min_label max_label created_at updated_at],
include: {
questionnaire: { only: %i[name id] }
}
Expand Down
10 changes: 8 additions & 2 deletions app/models/questionnaire.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
class Questionnaire < ApplicationRecord
belongs_to :instructor
# the collection of items associated with this Questionnaire
has_many :items, class_name: 'Item', foreign_key: 'questionnaire_id', dependent: :destroy
has_many :items, class_name: 'Item', foreign_key: 'questionnaire_id', dependent: :destroy, inverse_of: :questionnaire
accepts_nested_attributes_for :items, allow_destroy: true
before_destroy :any_item_associations?

# Subclasses declare @print_name = '...' and inherit this reader automatically.
Expand Down Expand Up @@ -57,16 +58,21 @@ def as_json(options = {})
super(options.merge({
only: %i[id name private min_question_score max_question_score created_at updated_at questionnaire_type instructor_id],
include: {
instructor: { only: %i[name email fullname role] }
instructor: { only: %i[name email fullname role] },
items: {}
}
})).tap do |hash|
hash['instructor'] ||= { id: nil, name: nil }
hash['items'] ||= []
end
end

DEFAULT_MIN_ITEM_SCORE = 0 # The lowest score that a reviewer can assign to any questionnaire item
DEFAULT_MAX_ITEM_SCORE = 5 # The highest score that a reviewer can assign to any questionnaire item

# Filters questionnaires by questionnaire_type string.
scope :by_type, ->(type) { where(questionnaire_type: type) }

QUESTIONNAIRE_TYPES = [
'ReviewQuestionnaire',
'MetareviewQuestionnaire',
Expand Down
2 changes: 1 addition & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,47 @@
end
end

puts "creating questionnaires and items"
questionnaire = AuthorFeedbackQuestionnaire.create!(
name: "Sample Review Questionnaire",
instructor_id: 1,
private: false,
min_question_score: 0,
max_question_score: 5
)

Item.create!([
{
questionnaire: questionnaire,
txt: "How clearly did the author explain the problem statement?",
question_type: "Scale",
seq: 1,
weight: 1,
break_before: true,
min_label: "Strongly Disagree",
max_label: "Strongly Agree"
},
{
questionnaire: questionnaire,
txt: "Provide detailed feedback on the author's solution approach.",
question_type: "Textarea",
seq: 2,
weight: 1,
break_before: true,
size: "60,5"
},
{
questionnaire: questionnaire,
txt: "Rate the overall quality of the submission.",
question_type: "Scale",
seq: 3,
weight: 2,
break_before: false,
min_label: "Poor",
max_label: "Excellent"
}
])

rescue ActiveRecord::RecordInvalid => e
puts e, 'The db has already been seeded'
end
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
command: tail -f /dev/null
environment:
RAILS_ENV: development
DATABASE_URL: mysql2://root:expertiza@db:3306/reimplementation?
DATABASE_URL: mysql2://root:expertiza@db:3306/reimplementation
CACHE_STORE: redis://redis:6380/0
ports:
- "3002:3002"
Expand Down
78 changes: 74 additions & 4 deletions spec/requests/api/v1/questionnaires_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,55 @@
end
end

# get request on /questionnaires?type=ReviewQuestionnaire returns only matching questionnaires
get('list questionnaires by type') do
tags 'Questionnaires'
produces 'application/json'
parameter name: :type, in: :query, type: :string, required: false,
description: 'Filter by questionnaire type (e.g. ReviewQuestionnaire)'

response(200, 'successful - filters by valid type') do
before do
prof
ReviewQuestionnaire.create!(
name: 'Review Q',
private: false,
min_question_score: 0,
max_question_score: 5,
instructor_id: prof.id
)
Questionnaire.create!(
name: 'Survey Q',
questionnaire_type: 'SurveyQuestionnaire',
private: false,
min_question_score: 0,
max_question_score: 5,
instructor_id: prof.id
)
end

let(:type) { 'ReviewQuestionnaire' }

run_test! do
json = JSON.parse(response.body)
expect(json).to be_an(Array)
expect(json.all? { |q| q['questionnaire_type'] == 'ReviewQuestionnaire' }).to be true
end
end

response(422, 'unprocessable entity - invalid type') do
let(:type) { 'BogusQuestionnaire' }
run_test! do
expect(response.body).to include('Invalid questionnaire type')
end
end
end

post('create questionnaire') do
let(:valid_questionnaire_params) do
{
name: 'Test Questionnaire',
questionnaire_type: 'AuthorFeedbackReview',
questionnaire_type: 'ReviewQuestionnaire',
private: false,
min_question_score: 0,
max_question_score: 5,
Expand All @@ -75,7 +119,18 @@
let(:invalid_questionnaire_params) do
{
name: nil, # invalid name
questionnaire_type: 'AuthorFeedbackReview',
questionnaire_type: 'ReviewQuestionnaire',
private: false,
min_question_score: 0,
max_question_score: 5,
instructor_id: prof.id
}
end

let(:invalid_type_params) do
{
name: 'Bad Type Q',
questionnaire_type: 'BogusQuestionnaire',
private: false,
min_question_score: 0,
max_question_score: 5,
Expand All @@ -102,21 +157,36 @@
response(201, 'created') do
let(:questionnaire) do
prof
Questionnaire.create(valid_questionnaire_params)
valid_questionnaire_params
end
run_test! do
expect(response.body).to include('"name":"Test Questionnaire"')
# Verify the correct subclass was instantiated and display_type set via after_initialize
json = JSON.parse(response.body)
created = Questionnaire.find(json['id'])
expect(created.display_type).to eq('Review')
end
end

# post request on /questionnaires returns 422 response - unprocessable entity when wrong params is passed to create questionnaire
response(422, 'unprocessable entity') do
let(:questionnaire) do
prof
Questionnaire.create(invalid_questionnaire_params)
invalid_questionnaire_params
end
run_test!
end

# post request on /questionnaires returns 422 when an unrecognized questionnaire_type is provided
response(422, 'unprocessable entity - invalid type') do
let(:questionnaire) do
prof
invalid_type_params
end
run_test! do
expect(response.body).to include('Invalid questionnaire type')
end
end
end

end
Expand Down
10 changes: 7 additions & 3 deletions spec/requests/api/v1/questions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,17 @@
break_before: { type: :boolean },
txt: { type: :string },
question_type: { type: :string },
seq: { type: :number }
},
required: %w[weight questionnaire_id break_before txt question_type]
required: %w[weight questionnaire_id break_before txt question_type seq]
}

# post request on /questions returns 201 created response and creates a item with given valid parameters
response(201, 'created') do
let(:item) { valid_question_params }
run_test! do
expect(response.body).to include('"seq":1')
parsed_response = JSON.parse(response.body)
expect(parsed_response['seq'].to_i).to eq(1)
end
end

Expand Down Expand Up @@ -536,7 +538,9 @@
# get request on /questions/types returns types of questions present in the database
response(200, 'successful') do
run_test! do
expect(response.body.size).to eq(2)
parsed_response = JSON.parse(response.body)
expect(parsed_response.size).to eq(7)
expect(parsed_response).to include("Multiple choice")
end
end
end
Expand Down
Loading