Skip to content

E2619 - Student Quiz Backend#345

Open
atmai0829 wants to merge 8 commits into
expertiza:mainfrom
atmai0829:main
Open

E2619 - Student Quiz Backend#345
atmai0829 wants to merge 8 commits into
expertiza:mainfrom
atmai0829:main

Conversation

@atmai0829

@atmai0829 atmai0829 commented Apr 28, 2026

Copy link
Copy Markdown

Implements the server-side quiz workflow for Expertiza. Submitting teams can now author quizzes with per-item correct answers. Reviewers must complete the team's quiz before starting their peer review. Quiz responses are automatically scored at submission time

Summary by CodeRabbit

Release Notes

  • New Features

    • Added quiz questionnaire support with quiz-specific item types and validation.
    • Introduced comprehensive Response API for creating, updating, submitting, and managing responses.
    • Added Response Map API for listing and managing peer-review mappings.
    • Launched Quiz Response Map endpoints for quiz assignment tracking.
    • Introduced Sign-Up Topics API for managing assignment topics.
    • Added new endpoints for retrieving questionnaire types and item types.
  • Tests

    • Added extensive test coverage for new Response, Response Map, and Quiz Response Map APIs.

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request introduces comprehensive quiz-response functionality and restructures questionnaire management. It adds new API controllers for responses, response maps, quiz responses, items, and sign-up topics; establishes new models for questionnaire types and item types; refactors response scoring to support quiz-specific logic; expands assignment and questionnaire associations with nested attributes; and migrates the schema to include quiz-related columns and table renames.

Changes

Cohort / File(s) Summary
Gem Dependencies
Gemfile
Added ffi gem for Windows REST client support.
New API Controllers
app/controllers/responses_controller.rb, app/controllers/response_maps_controller.rb, app/controllers/quiz_response_maps_controller.rb
Introduced three major controllers handling response CRUD operations, response map management with quiz state tracking, and quiz-response creation with participant/team resolution and validation.
Questionnaire & Item Controllers
app/controllers/items_controller.rb, app/controllers/item_types_controller.rb, app/controllers/questionnaire_types_controller.rb, app/controllers/questionnaires_controller.rb
Added new item and questionnaire-type index endpoints; expanded questionnaires controller with eager loading, nested item attributes, error handling, and destroy callback support.
Existing Controller Updates
app/controllers/assignments_controller.rb, app/controllers/questions_controller.rb, app/controllers/sign_up_topics_controller.rb, app/controllers/teams_controller.rb
Enhanced assignments show/update to include questionnaires and nested attributes; added quiz item type validation and correct-answer support to questions; introduced full CRUD for sign-up topics with microtask detection; added set_quiz_questionnaire endpoint to teams.
Controller Whitespace Normalization
app/controllers/invitations_controller.rb, app/controllers/student_teams_controller.rb, app/controllers/grades_controller.rb
Reformatted controller files with line-ending normalization; logic and signatures unchanged.
Model Core Logic
app/models/response.rb
Refactored scoring methods (aggregate_questionnaire_score, maximum_score, reportable_difference?) to support quiz logic with comment-based correct-answer matching; added rubric_label helper; enabled nested scores attributes.
Quiz & Association Models
app/models/item.rb, app/models/questionnaire.rb, app/models/quiz_response_map.rb, app/models/team.rb
Made questionnaire optional on items; added quiz-item validation and serialization; enabled nested item/questionnaire updates; added optional assignment on quiz-response-maps; linked teams to quiz questionnaires.
New Model Classes
app/models/item_type.rb, app/models/questionnaire_type.rb, app/models/sign_up_topic.rb, app/models/assignment.rb
Introduced item-types and questionnaire-types with name validation; added sign-up-topic model with associations and cascading deletes; enabled assignment nested attributes for questionnaires.
Response Map Enhancement
app/models/response_map.rb
Added UI-label helper, update-link checker, and submission/activity tracking methods to determine when review links should appear.
Student Task Refactor
app/models/student_task.rb
Expanded from simple aggregator to quiz-aware serializer with assignment tracking, quiz state detection via team lookups, and JSON serialization.
Model Whitespace & Concerns
app/models/concerns/response_map_subclass_titles.rb, app/models/concerns/review_aggregator.rb
Added SURVEY_RESPONSE_MAP_TITLE constant; normalized formatting in review-aggregator module.
Serializers
app/serializers/assignment_serializer.rb, app/serializers/questionnaire_serializer.rb, app/serializers/assignment_questionnaire_serializer.rb
Significantly expanded assignment serializer with 30+ attributes plus relationships; introduced questionnaire and assignment-questionnaire serializers with nested instructor data.
Mailers & Helpers
app/mailers/invitation_mailer.rb, app/mailers/invitation_sent_mailer.rb, app/helpers/security_helper.rb, app/validators/invitation_validator.rb
Reintroduced existing invitation mailer and normalized formatting; added new invitation-sent mailer with email dispatch; updated security-helper message punctuation; normalized invitation-validator formatting.
Mailer View
app/views/invitation_sent_mailer/send_invitation_email.html.erb
New mailer template rendering invitation message with recipient fullname.
API Routes & Configuration
config/routes.rb, config/database.yml
Expanded routes with new resource endpoints for responses, response maps, quiz responses, items, questionnaire types, and sign-up topics; added fallback MySQL URL in database config.
Docker & Setup
docker-compose.yml, setup.sh
Changed container startup from tail -f /dev/null to executing setup.sh; removed trailing ? from DATABASE_URL; normalized whitespace in setup script.
Database Migrations (Idempotency)
db/migrate/20250321222753_*, db/migrate/20250418004442_*, db/migrate/20250629185100_*, db/migrate/202506291854*_*, db/migrate/20250727170825_*, db/migrate/2025102*_*, db/migrate/20251125012619_*, db/migrate/20251126161701_*
Made migrations idempotent by adding column-existence checks before schema alterations to prevent rerun errors.
Database Migrations (New Schema)
db/migrate/20251230123456_rename_question_types_to_item_types.rb, db/migrate/20260423000001_add_correct_answer_to_items.rb, db/migrate/20260424000001_add_quiz_questionnaire_id_to_teams.rb, db/migrate/20251013123456_add_textarea_width_to_items.rb, db/migrate/20251013123457_add_cols_rows_to_items.rb, db/migrate/20260626161114_add_name_to_teams.rb
Added columns for quiz support (correct_answer, quiz_questionnaire_id), text formatting (textarea_*, textbox_width), matrix-item definitions (col_names, row_names), and team naming.
Database Schema & Seeds
db/schema.rb, db/seeds.rb, db/seeds2.rb
Updated schema to v2026_04_24_000001; removed old tables; added item-types and sign-up-topics tables; made seeds idempotent with find_or_create_by! and conditional random data; normalized seeds2 formatting.
Test Specs - Controllers
spec/controllers/responses_controller_spec.rb
Added comprehensive RSpec suite for ResponsesController covering create/update/submit/unsubmit/destroy with auth, validation, deadline, and state-transition assertions.
Test Specs - Factories
spec/factories/reponse.rb, spec/factories/response_maps.rb, spec/factories/review_response_maps.rb
Created response factory; added response-map and review-response-map factories; deleted separate review-response-map factory file.
Test Specs - Models
spec/models/item_spec.rb, spec/models/response_spec.rb, spec/models/student_task_spec.rb, spec/models/response_map_spec.rb, spec/models/response_map_label_spec.rb, spec/models/response_rubric_label_spec.rb
Updated item spec for optional questionnaire; added extensive quiz-scoring tests for response aggregation/maximum-score; enriched student-task spec with quiz state; added response-map update-link and label tests; added response rubric-label behavior tests.
Test Specs - Formatting
spec/factories/project_topics.rb, spec/models/project_topic_spec.rb, spec/models/signed_up_team_spec.rb, spec/requests/api/v1/grades_controller_spec.rb, spec/requests/api/v1/invitation_controller_spec.rb, spec/requests/api/v1/student_teams_controller_spec.rb, spec/validators/invitation_validator_spec.rb, spec/rails_helper.rb
Normalized line endings and formatting across multiple test files without logic changes.
Test Specs - Request APIs
spec/requests/api/v1/questions_spec.rb, spec/requests/api/v1/quiz_response_maps_controller_spec.rb, spec/requests/api/v1/response_maps_controller_spec.rb, spec/routing/sign_up_topics_routing_spec.rb
Added quiz-item creation and validation tests; introduced comprehensive quiz-response-maps and response-maps endpoint specs with JWT auth and schema validation; added sign-up-topics routing verification.
Test Support
spec/mailers/previews/invitation_sent_preview.rb
Added mailer preview class for invitation-sent emails.
API Documentation
swagger/v1/swagger.yaml
Added OpenAPI specifications for student-task and responses endpoints (create/update/delete/submit/unsubmit) with parameters and response schemas.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 New quizzes hop into the questionnaire pen,
With responses and maps, we're scoring again,
Items wear correct_answer crowns so bright,
Teams find their questions in quiz_questionnaire's light,
Nested attributes dance—validations all right! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main change: implementing student quiz backend functionality. It directly relates to the primary objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
4 Warnings
⚠️ Pull request is too big (more than 500 LoC).
⚠️ Pull request touches too many files (more than 30 files).
⚠️ Pull request modifies config or setup files: config/database.yml, setup.sh.
⚠️ RSpec tests seem shallow (single it blocks or no context). Consider improving test structure.

Generated by 🚫 Danger

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
spec/rails_helper.rb (1)

34-48: ⚠️ Potential issue | 🟡 Minor

Remove redundant before/after hooks; use only the around(:each) wrapper.

The before(:each), after(:each), and around(:each) hooks are overlapping. In DatabaseCleaner 2.0.1, the cleaning method internally handles both start and clean, so combining it with explicit before/after hooks causes:

  • DatabaseCleaner.start called twice (before hook + cleaning's internal start)
  • DatabaseCleaner.clean called twice (cleaning's internal clean + after hook)

Keep the around(:each) block alone and move the strategy configuration inside it:

Suggested fix
   config.before(:suite) do
     FactoryBot.factories.clear
     FactoryBot.find_definitions
     DatabaseCleaner.allow_remote_database_url = true

     if ENV['GITHUB_ACTIONS']
       DatabaseCleaner.clean_with(:deletion, except: %w[ar_internal_metadata])
     else
       DatabaseCleaner.clean_with(:truncation)
     end
   end

-  config.before(:each) do
-    DatabaseCleaner.strategy = :transaction
-    DatabaseCleaner.start
-  end
-
-  config.after(:each) do
-    DatabaseCleaner.clean
-  end
-
-  # Ensures proper cleanup
   config.around(:each) do |example|
+    DatabaseCleaner.strategy = :transaction
     DatabaseCleaner.cleaning { example.run }
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/rails_helper.rb` around lines 34 - 48, Remove the redundant
config.before(:each) and config.after(:each) hooks so DatabaseCleaner.start and
DatabaseCleaner.clean are not invoked twice; instead keep only the
config.around(:each) wrapper and set DatabaseCleaner.strategy inside that around
block before calling DatabaseCleaner.cleaning { example.run } (referencing
DatabaseCleaner.strategy, DatabaseCleaner.start, DatabaseCleaner.clean,
config.before(:each), config.after(:each), config.around(:each), and example.run
to locate the code).
db/migrate/20250621180851_rename_item_foreign_key_index_in_answers.rb (1)

1-7: ⚠️ Potential issue | 🟠 Major

Use up/down with defensive guards to ensure reversible migration.

The change method will hard-fail at runtime if the index "fk_score_questions" doesn't exist, and its automatic reversal won't work correctly. When rolling back, Rails will try to re-add the old index by name against a column that doesn't exist (since question_id was renamed to item_id in the prior migration). Use explicit up/down methods with index_exists? guards:

 class RenameItemForeignKeyIndexInAnswers < ActiveRecord::Migration[8.0]
-  def change
-    remove_index :answers, name: "fk_score_questions"
-    add_index :answers, :item_id, name: "fk_score_items"
-  end
+  def up
+    remove_index :answers, name: "fk_score_questions" if index_exists?(:answers, name: "fk_score_questions")
+    add_index :answers, :item_id, name: "fk_score_items" unless index_exists?(:answers, :item_id, name: "fk_score_items")
+  end
+
+  def down
+    remove_index :answers, name: "fk_score_items" if index_exists?(:answers, name: "fk_score_items")
+    add_index :answers, :item_id, name: "fk_score_questions" unless index_exists?(:answers, :item_id, name: "fk_score_questions")
+  end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/migrate/20250621180851_rename_item_foreign_key_index_in_answers.rb` around
lines 1 - 7, Replace the current change method in
RenameItemForeignKeyIndexInAnswers with explicit up and down methods that use
index_exists? guards: in up, only remove_index(:answers, name:
"fk_score_questions") if index_exists? with that name, and only
add_index(:answers, :item_id, name: "fk_score_items") if that index doesn't
already exist; in down, only remove_index(:answers, name: "fk_score_items") if
it exists and re-create the old-named index ("fk_score_questions") but create it
on :item_id (not :question_id) and only if an index with that name doesn't
already exist—this ensures safe, reversible migrations without referencing a
missing column.
🟠 Major comments (29)
db/seeds2.rb-44-53 (1)

44-53: ⚠️ Potential issue | 🟠 Major

Fix brittle/idempotency issues in seeds + remove redundant begin

Right now this seeds join records using assignment_id: i+1 and questionnaire_id: i+1 and then blindly calls create!. That will:

  • fail on a fresh DB if IDs 1..4 don’t exist yet
  • create duplicate AssignmentQuestionnaire rows on re-runs (unless the DB already enforces uniqueness)

Also, the surrounding begin block is redundant (no rescue/ensure), and the block has multiple RuboCop-style indentation/spacing issues.

🛠️ Proposed rewrite
-begin
-    count = 4
-    count.times do |i|
-        AssignmentQuestionnaire.create!(
-            assignment_id: i+1,
-            questionnaire_id: i+1,
-            used_in_round: [1,2].sample
-        )
-    end
-end
+count = 4
+
+count.times do |i|
+  assignment = Assignment.find_by(id: i + 1)
+  questionnaire = Questionnaire.find_by(id: i + 1)
+
+  raise "Missing Assignment with id=#{i + 1}" if assignment.nil?
+  raise "Missing Questionnaire with id=#{i + 1}" if questionnaire.nil?
+
+  AssignmentQuestionnaire.find_or_create_by!(
+    assignment_id: assignment.id,
+    questionnaire_id: questionnaire.id
+  ) do |aq|
+    aq.used_in_round = [1, 2].sample
+  end
+end

If you intend to rely on upstream seeds guaranteeing those IDs, consider still adding the explicit raise checks above so failures are actionable instead of “mysterious FK errors”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/seeds2.rb` around lines 44 - 53, Remove the redundant begin block and fix
indentation; make seeding idempotent by replacing the blind
AssignmentQuestionnaire.create! call with a lookup-or-create and explicit
existence checks: verify the referenced Assignment and Questionnaire exist
(raise a clear error if not) and then use
AssignmentQuestionnaire.find_or_create_by(assignment_id: i+1, questionnaire_id:
i+1) (or find_or_initialize_by + save!) to avoid duplicates, and ensure
used_in_round is set/updated on the found or created record; keep the loop
(count = 4 / count.times) but adjust spacing to match RuboCop style.
app/controllers/student_teams_controller.rb-65-83 (1)

65-83: ⚠️ Potential issue | 🟠 Major

Fix duplicate-team-name check in update to exclude the current team.

Right now matching_teams includes the team you’re updating, so setting the team name to its current value would be treated as “already in use” (query isn’t excluding team.id).

🔧 Proposed fix
 def update
   # Checks for duplicate team names within the same assignment (by parent_id).
-  matching_teams = AssignmentTeam.where(name: params[:team][:name], parent_id: team.parent_id)
+  desired_name = params.dig(:team, :name)
+  matching_teams = AssignmentTeam
+    .where(name: desired_name, parent_id: team.parent_id)
+    .where.not(id: team.id)

   # no team with that name found - goes ahead and saves the new name
   if matching_teams.empty?
-    if team.update(name: params[:team][:name])
+    if team.update(name: desired_name)
       serialized_team = ActiveModelSerializers::SerializableResource.new(team, serializer: TeamSerializer).as_json
       render json: serialized_team.merge({ message: "Team updated successfully", success: true }), status: :ok
     else
       render json: { error: team.errors.full_messages }, status: :unprocessable_entity
     end
   else
     # Returns an error if another team with the same name already exists.
-    render json: { error: "#{params[:team][:name]} is already in use." }, status: :unprocessable_entity
+    render json: { error: "#{desired_name} is already in use." }, status: :unprocessable_entity
   end
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/student_teams_controller.rb` around lines 65 - 83, The
duplicate-name check in update is including the record being edited
(matching_teams on AssignmentTeam), so updating to the same name triggers a
false positive; change the query that builds matching_teams in the update action
to exclude the current team (use a where.not(id: team.id) or equivalent) so the
check only considers other teams in the same parent/assignment before deciding
if the name is already in use.
db/migrate/20251013123456_add_textarea_width_to_items.rb-3-5 (1)

3-5: ⚠️ Potential issue | 🟠 Major

Guard these new columns with unless column_exists? to prevent duplicate-column migration failures.

The unconditional add_column calls on lines 3–5 will fail if the columns already exist in the database. This occurs during reruns, partial rollbacks, or when the migration is applied to environments that already have these columns (as indicated by their presence in db/schema.rb). Apply the proposed guards:

Fix
 class AddTextareaWidthToItems < ActiveRecord::Migration[8.0]
   def change
-    add_column :items, :textarea_width, :integer
-    add_column :items, :textarea_height, :integer
-    add_column :items, :textbox_width, :integer
+    add_column :items, :textarea_width, :integer unless column_exists?(:items, :textarea_width)
+    add_column :items, :textarea_height, :integer unless column_exists?(:items, :textarea_height)
+    add_column :items, :textbox_width, :integer unless column_exists?(:items, :textbox_width)
   end
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/migrate/20251013123456_add_textarea_width_to_items.rb` around lines 3 - 5,
The migration currently unconditionally calls add_column for :textarea_width,
:textarea_height, and :textbox_width on the :items table which will error if
those columns already exist; update the migration to guard each add_column with
a column_exists? check (e.g. only call add_column :items, :textarea_width,
:integer unless column_exists?(:items, :textarea_width)) for all three columns
so re-running or applying to schemas that already contain these fields is safe.
db/migrate/20251021165336_add_participant_ref_to_invitations.rb-3-5 (1)

3-5: ⚠️ Potential issue | 🟠 Major

FK constraint not created if participant_id column pre-exists.

The guard on line 3 only checks for the column's existence. If participant_id already exists but lacks a foreign key constraint, this migration silently succeeds without creating the FK. The current schema confirms this scenario: the column exists but the FK is missing.

The proposed fix has an issue: add_reference will fail with a duplicate column error if the column already exists, regardless of the foreign_key setting. The correct approach is to separate concerns:

Corrected fix
 class AddParticipantRefToInvitations < ActiveRecord::Migration[8.0]
   def change
     unless column_exists?(:invitations, :participant_id)
       add_column :invitations, :participant_id, :bigint, null: false
     end
+
+    unless foreign_key_exists?(:invitations, :participants, column: :participant_id)
+      add_foreign_key :invitations, :participants, column: :participant_id
+    end
   end
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/migrate/20251021165336_add_participant_ref_to_invitations.rb` around lines
3 - 5, The migration currently only checks column_exists?(:invitations,
:participant_id) and skips creating a foreign key if the column exists; instead,
change the logic to handle two cases: if the column does not exist, call
add_reference :invitations, :participant, null: false (without attempting a
duplicate foreign key), and if the column exists but the FK is missing, call
add_foreign_key :invitations, :participants, column: :participant_id; use
foreign_key_exists? (or equivalent) to detect if the FK already exists before
calling add_foreign_key so the migration is idempotent.
spec/factories/reponse.rb-3-6 (1)

3-6: ⚠️ Potential issue | 🟠 Major

Use an association-backed response map instead of hardcoding the foreign key.

map_id { 1 } is non-portable and can fail depending on DB state. The Response model uses belongs_to :response_map, so the association should be association :response_map, factory: :review_response_map. The timestamps can also be removed as FactoryBot handles these automatically.

Proposed fix
 FactoryBot.define do
   factory :response do
-    map_id { 1 }
+    association :response_map, factory: :review_response_map
     is_submitted { false }
-    created_at { Time.current }
-    updated_at { Time.current }
   end
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/factories/reponse.rb` around lines 3 - 6, Replace the hardcoded foreign
key and manual timestamps in the factory: remove map_id { 1 } and the
created_at/updated_at attributes and instead declare the association using
association :response_map, factory: :review_response_map so FactoryBot will
build a linked ResponseMap; keep is_submitted { false } as-is and let FactoryBot
handle timestamps automatically.
db/migrate/20251126161701_remove_participant_ref_from_invitations.rb-3-5 (1)

3-5: ⚠️ Potential issue | 🟠 Major

Remove the foreign key constraint explicitly before dropping the column.

The add_reference in the prior migration creates a foreign key with foreign_key: true, but remove_reference without foreign_key: true only removes the column, leaving the constraint behind. This causes the migration to fail in Rails 8.0.

Proposed fix
   def change
     if column_exists?(:invitations, :participant_id)
-      remove_reference :invitations, :participant
+      remove_foreign_key :invitations, :participants if foreign_key_exists?(:invitations, :participants)
+      remove_reference :invitations, :participant, foreign_key: false
     end
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/migrate/20251126161701_remove_participant_ref_from_invitations.rb` around
lines 3 - 5, The migration currently checks column_exists?(:invitations,
:participant_id) and calls remove_reference :invitations, :participant but does
not drop the foreign key first; update the migration to first remove the FK
using remove_foreign_key (for example remove_foreign_key :invitations,
:participants or remove_foreign_key :invitations, column: :participant_id) when
the column exists, then call remove_reference :invitations, :participant so the
constraint is explicitly removed before the column is dropped.
app/mailers/invitation_mailer.rb-31-33 (1)

31-33: ⚠️ Potential issue | 🟠 Major

Avoid find_by + immediate dereference in subject construction.

Line 31 can return nil, and Line 33 then calls .name, which can raise and drop the email.

Suggested fix
-    `@from_team` = AssignmentTeam.find_by(id: `@from_participant.team_id`)
+    `@from_team` = AssignmentTeam.find(`@from_participant.team_id`)
     `@assignment` = Assignment.find(`@invitation.assignment_id`)
-    mail(to: `@to_participant.user.email`, subject: 'You have a new invitation from Expertiza for assignment ' + `@assignment.name` + ' by team ' + `@from_team.name`)
+    mail(
+      to: `@to_participant.user.email`,
+      subject: "You have a new invitation from Expertiza for assignment #{`@assignment.name`} by team #{`@from_team.name`}"
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/mailers/invitation_mailer.rb` around lines 31 - 33, The subject
construction assumes `@from_team` is present and calls `@from_team.name` which can
be nil; change the code that builds the mail subject in invitation_mailer.rb to
safely handle a missing team by using `@from_team`&.name or a fallback (e.g.,
'Unknown team') when composing the subject string (keep references to
`@from_team`, `@from_participant.team_id`, `@assignment.name` and the mail(...) call).
app/mailers/invitation_mailer.rb-27-28 (1)

27-28: ⚠️ Potential issue | 🟠 Major

Remove invitation debug dump from the mailer path.

Line 27 logs @invitation.inspect before Line 28 assigns @invitation, and it can leak invitation payloads into logs.

Suggested fix
   def send_invitation_email
-    puts `@invitation.inspect`
     `@invitation` = params[:invitation]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/mailers/invitation_mailer.rb` around lines 27 - 28, Remove the debug dump
that potentially leaks sensitive invitation data: delete the stray puts
`@invitation.inspect` in InvitationMailer (the mailer method where `@invitation` is
set from params[:invitation]) and ensure any logging happens only after
`@invitation` is assigned and uses safe/filtered fields if necessary; in short,
remove the .inspect debug line or move and replace it with a safe log that
references permitted attributes after `@invitation` = params[:invitation].
db/migrate/20251022160053_change_invitation_from_id_foreign_key.rb-2-10 (1)

2-10: ⚠️ Potential issue | 🟠 Major

Use up/down or wrap change_column in a reversible block.

Line 9 uses change_column :invitations, :from_id, :bigint inside def change, which Rails cannot automatically reverse. Per Rails migration documentation, change_column is irreversible. Either switch to explicit up/down methods or wrap this operation in a reversible block to ensure rollbacks are predictable.

Suggested fix with up/down
 class ChangeInvitationFromIdForeignKey < ActiveRecord::Migration[7.0]
-  def change
+  def up
     # Remove old foreign key to participants if it exists
     if foreign_key_exists?(:invitations, column: :from_id)
       remove_foreign_key :invitations, column: :from_id
     end

     # Ensure from_id is bigint to match teams.id
     change_column :invitations, :from_id, :bigint

     # Add new foreign key to teams
     unless foreign_key_exists?(:invitations, :teams, column: :from_id)
       add_foreign_key :invitations, :teams, column: :from_id
     end
   end
+
+  def down
+    raise ActiveRecord::IrreversibleMigration, "Reverting invitations.from_id foreign key requires explicit legacy schema mapping"
+  end
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/migrate/20251022160053_change_invitation_from_id_foreign_key.rb` around
lines 2 - 10, The migration uses change_column :invitations, :from_id, :bigint
inside def change which Rails cannot reverse; update the migration to make the
schema change reversible by either converting def change to explicit def up /
def down methods (move the foreign_key_exists? check and remove_foreign_key +
change_column into up, and restore the original column type and foreign key in
down) or wrap the change_column call in a reversible do |dir| block and provide
dir.up { change_column :invitations, :from_id, :bigint } and dir.down {
change_column :invitations, :from_id, <original_type> } so rollbacks are
deterministic; refer to the migration method names change, up, down and the
change_column :invitations, :from_id, :bigint call to locate the code to modify.
app/controllers/items_controller.rb-6-13 (1)

6-13: ⚠️ Potential issue | 🟠 Major

Avoid logging full item payloads and attributes in production paths.

Line 8 and Line 12 can leak sensitive quiz data (e.g., correct_answer) to logs. Please remove these logs or strictly whitelist non-sensitive fields.

Suggested hardening
-    Rails.logger.info "Items for Questionnaire #{questionnaire.id}:"
-    questionnaire.items.each do |item|
-      Rails.logger.info item.attributes.inspect
-    end
-
-    items_json = questionnaire.items.as_json
-    Rails.logger.info "JSON being rendered: #{items_json.inspect}"
+    Rails.logger.info "Fetching items for Questionnaire #{questionnaire.id}"
+    items_json = questionnaire.items.as_json(only: %i[id seq txt question_type break_before weight questionnaire_id])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/items_controller.rb` around lines 6 - 13, Remove the calls
that log full item attributes and the full JSON payload (the Rails.logger.info
lines that inspect questionnaire.items and items_json) to avoid leaking
sensitive quiz data; instead, if logging is necessary use a whitelist of safe
fields (e.g., call questionnaire.items.as_json(only: [:id, :question_text,
:position]) or map/select on questionnaire.items to pick safe keys) and replace
the existing Rails.logger.info invocations so only those non-sensitive fields
are emitted (references: questionnaire.items, items_json).
swagger/v1/swagger.yaml-1932-1954 (1)

1932-1954: ⚠️ Potential issue | 🟠 Major

Duplicate student_tasks paths with conflicting definitions will shadow earlier operations.

/student_tasks/list and /student_tasks/view are already defined at lines 1474 and 1489 respectively, with complete parameter sets and error responses. The redeclarations at lines 1932 and 1940 have significant differences that conflict with the originals:

  • /student_tasks/list (line 1932): Missing Authorization header parameter, fewer response codes
  • /student_tasks/view (line 1940): Parameter type is string instead of Integer, missing Authorization header, missing 500/401 error responses

OpenAPI parsers use the last definition when encountering duplicate path keys, causing the newer incomplete definitions to override the original ones. This will break API documentation and client generation.

Remove the duplicate paths at lines 1932–1954 or consolidate all operations under the existing path definitions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@swagger/v1/swagger.yaml` around lines 1932 - 1954, There are duplicate
OpenAPI path definitions for /student_tasks/list and /student_tasks/view that
are shadowing the earlier, complete definitions; remove the duplicate path
objects or merge them so the original definitions remain authoritative: delete
the later /student_tasks/list and /student_tasks/view entries (or consolidate by
copying missing items into the original entries) and ensure the retained
definitions include the Authorization header parameter, correct parameter types
(Integer for view.id), and the full response set including 401/500 where
applicable so parsers/clients use the complete spec.
swagger/v1/swagger.yaml-2050-2058 (1)

2050-2058: ⚠️ Potential issue | 🟠 Major

Use OpenAPI path-template syntax for unsubmit route.

"/responses/:id/unsubmit" is not valid OpenAPI syntax. Path parameters must use curly braces: "/responses/{id}/unsubmit". This is inconsistent with other response endpoints in the file (/responses/{id} and /responses/{id}/submit) and violates OpenAPI 3.0 specification.

✅ Proposed fix
-  "/responses/:id/unsubmit":
+  "/responses/{id}/unsubmit":
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@swagger/v1/swagger.yaml` around lines 2050 - 2058, The OpenAPI path
"/responses/:id/unsubmit" uses Express-style colon syntax instead of OpenAPI
path-template syntax; update the path key to "/responses/{id}/unsubmit" so it
matches the other endpoints (e.g., "/responses/{id}" and
"/responses/{id}/submit") and keeps the existing parameters block (name: id, in:
path, required: true) valid under OpenAPI 3.0.
app/controllers/questions_controller.rb-46-47 (1)

46-47: ⚠️ Potential issue | 🟠 Major

Quiz type whitelist is too narrow for current item-type naming.

QUIZ_ITEM_TYPES only includes CamelCase variants, but quiz scoring/tests and seeded item types use spaced names too ("Text field", "Multiple choice", "Multiple choice checkbox"). Valid quiz inputs can be rejected with 422.

Suggested fix (normalize aliases)
-  QUIZ_ITEM_TYPES = %w[TextField MultipleChoiceRadio MultipleChoiceCheckbox Scale Checkbox].freeze
+  QUIZ_ITEM_TYPES = %w[TextField MultipleChoiceRadio MultipleChoiceCheckbox Scale Checkbox].freeze
+  QUIZ_ITEM_TYPE_ALIASES = {
+    'Text field' => 'TextField',
+    'Multiple choice' => 'MultipleChoiceRadio',
+    'Multiple choice checkbox' => 'MultipleChoiceCheckbox'
+  }.freeze
...
-    if is_quiz && !QUIZ_ITEM_TYPES.include?(params[:question_type])
+    normalized_type = QUIZ_ITEM_TYPE_ALIASES.fetch(params[:question_type], params[:question_type])
+    if is_quiz && !QUIZ_ITEM_TYPES.include?(normalized_type)
       return render json: { error: "Invalid quiz item type. Allowed types: #{QUIZ_ITEM_TYPES.join(', ')}" },
                     status: :unprocessable_entity
     end
...
-      question_type: params[:question_type],
+      question_type: normalized_type,

Also applies to: 69-74

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/questions_controller.rb` around lines 46 - 47,
QUIZ_ITEM_TYPES currently lists only CamelCase names (constant QUIZ_ITEM_TYPES)
causing valid inputs like "Text field" or "Multiple choice checkbox" to be
rejected; update the code to normalize incoming item-type strings and the
whitelist by mapping both the constant and inputs to a canonical form (e.g.,
downcase and strip spaces or replace spaces with no-space) before validation.
Specifically, add a normalization step used wherever QUIZ_ITEM_TYPES is checked
(referenced symbol: QUIZ_ITEM_TYPES) and in the related validation logic around
the code block referenced at lines ~69-74 so that variants like "TextField",
"Text field", "text field" and "textfield" all match the canonical whitelist
entries. Ensure existing tests/seed data that use spaced names are accepted
without changing seed data.
db/seeds.rb-17-23 (1)

17-23: ⚠️ Potential issue | 🟠 Major

Do not hard-code foreign keys in idempotent seeds.

This block still pins institution_id/role_id to numeric IDs. If IDs differ in an existing DB, seeds can link to wrong records or fail.

Suggested fix
 User.find_or_create_by!(name: 'admin') do |user|
   user.email = 'admin2@example.com'
   user.password = 'password123'
   user.full_name = 'admin admin'
-  user.institution_id = 1
-  user.role_id = 1
+  user.institution_id = inst_id
+  user.role_id = Role.find_by!(name: 'Super Administrator').id
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/seeds.rb` around lines 17 - 23, The seed is hard-coding foreign keys
(institution_id and role_id) on User.find_or_create_by! which breaks
idempotency; update the seed to resolve the associated records by name (e.g.,
find_or_create_by on Institution and Role) and assign their ids or the
associations to the User (use user.institution = found_institution or user.role
= found_role or set user.institution_id = found_institution.id) instead of fixed
numeric literals so the seed works regardless of existing ID values.
app/models/sign_up_topic.rb-4-4 (1)

4-4: ⚠️ Potential issue | 🟠 Major

Update foreign key in has_many :signed_up_teams association.

The signed_up_teams table uses project_topic_id as the foreign key column (as shown in schema line 359 and foreign key constraint on line 476), but the association specifies foreign_key: 'topic_id'. This mismatch will break association queries and the dependent: :destroy callback. Change to foreign_key: 'project_topic_id' to match the actual schema column after the rename migration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/sign_up_topic.rb` at line 4, The has_many association in
SignUpTopic currently uses the wrong foreign_key; update the association
declaration has_many :signed_up_teams, foreign_key: 'topic_id', dependent:
:destroy to use foreign_key: 'project_topic_id' so it matches the
signed_up_teams schema and preserves correct association queries and the
dependent: :destroy behavior.
app/controllers/teams_controller.rb-90-94 (1)

90-94: ⚠️ Potential issue | 🟠 Major

Validate quiz type and assignment ownership before linking.

The action currently accepts any existing Questionnaire. This allows linking non-quiz or cross-assignment questionnaires to a team, which breaks the quiz flow contract.

Suggested fix
-    unless Questionnaire.exists?(id: questionnaire_id)
+    questionnaire = Questionnaire.find_by(id: questionnaire_id)
+    unless questionnaire
       return render json: { error: 'Questionnaire not found' }, status: :not_found
     end
-    `@team.update`!(quiz_questionnaire_id: questionnaire_id)
+    unless %w[Quiz QuizQuestionnaire].include?(questionnaire.questionnaire_type.to_s)
+      return render json: { error: 'Questionnaire must be a quiz' }, status: :unprocessable_entity
+    end
+    unless AssignmentQuestionnaire.exists?(assignment_id: `@team.parent_id`, questionnaire_id: questionnaire.id)
+      return render json: { error: 'Questionnaire does not belong to this team assignment' }, status: :unprocessable_entity
+    end
+
+    `@team.update`!(quiz_questionnaire_id: questionnaire.id)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/teams_controller.rb` around lines 90 - 94, The code currently
links any existing Questionnaire to a team; restrict this by loading the
Questionnaire (e.g., via Questionnaire.find_by(id: questionnaire_id)), verify it
is a quiz (check the model discriminator or boolean like questionnaire.quiz? or
questionnaire.questionnaire_type == 'quiz') and ensure it belongs to the same
assignment as the team (compare questionnaire.assignment_id with
`@team.assignment_id`) before calling `@team.update`!(quiz_questionnaire_id:
questionnaire_id); if either check fails, return a 422 or 404 with an
explanatory error message instead of updating.
app/controllers/questionnaires_controller.rb-33-34 (1)

33-34: ⚠️ Potential issue | 🟠 Major

Do not return backtraces in API responses.

Lines 33-34 expose raw exception messages and stack frames to clients. That leaks implementation details and can surface sensitive data from lower layers. Log the exception server-side and return a generic 500 payload instead.

Safer error handling
-    rescue => e
-      render json: { error: e.message, backtrace: e.backtrace.take(5) }, status: :internal_server_error
+    rescue StandardError => e
+      Rails.logger.error("QuestionnairesController#create failed: #{e.class}: #{e.message}")
+      render json: { error: 'Failed to create questionnaire' }, status: :internal_server_error
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/questionnaires_controller.rb` around lines 33 - 34, The
rescue block currently renders the exception message and backtrace to clients;
instead, log the full exception server-side (e.g., Rails.logger.error or
logger.error with e.class, e.message and e.backtrace.join("\n") and/or send to
your error tracker) and change the render call in the rescue to return a generic
JSON 500 payload (e.g., render json: { error: "Internal server error" }, status:
:internal_server_error) so the line that currently renders e.message and
backtrace is not exposed to API clients.
app/models/questionnaire.rb-18-19 (1)

18-19: ⚠️ Potential issue | 🟠 Major

The instructor exemption misses QuizQuestionnaire.

Line 19 only skips instructor_id validation for the literal 'Quiz', but this model still recognizes 'QuizQuestionnaire' elsewhere. Student-authored quiz payloads using that existing type string will still fail validation.

Safer predicate
-  validates :instructor_id, presence: true, unless: -> { questionnaire_type == 'Quiz' }
+  validates :instructor_id, presence: true,
+            unless: -> { %w[Quiz QuizQuestionnaire].include?(questionnaire_type) }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/questionnaire.rb` around lines 18 - 19, The current validation in
the Questionnaire model requires instructor_id unless questionnaire_type ==
'Quiz', but other parts of the app use 'QuizQuestionnaire' so student-authored
quizzes still fail; update the validation predicate on the Questionnaire model
(the validates :instructor_id, presence: true ...) to treat both 'Quiz' and
'QuizQuestionnaire' as quiz types (e.g., check questionnaire_type against a set
or use a predicate like in? for %w[Quiz QuizQuestionnaire] or a method
quiz_type?) so instructor_id is skipped for either quiz type.
db/schema.rb-403-405 (1)

403-405: ⚠️ Potential issue | 🟠 Major

Add a foreign key for teams.quiz_questionnaire_id.

Line 403 introduces a new cross-table reference, but Lines 404-405 only index it. Without an FK, teams can keep dangling quiz questionnaire ids after deletes, which will show up later as hard-to-debug 404/500 paths in quiz creation and scoring.

Migration to add referential integrity
class AddQuizQuestionnaireForeignKeyToTeams < ActiveRecord::Migration[8.0]
  def change
    add_foreign_key :teams, :questionnaires, column: :quiz_questionnaire_id
  end
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/schema.rb` around lines 403 - 405, Add a foreign key constraint for
teams.quiz_questionnaire_id to enforce referential integrity: create a migration
(e.g. AddQuizQuestionnaireForeignKeyToTeams) that calls add_foreign_key on the
teams table referencing questionnaires with column: :quiz_questionnaire_id so
deletes/updates on questionnaires cannot leave dangling ids in teams; run the
migration and ensure the new FK name and index (existing index
index_teams_on_quiz_questionnaire_id) remain consistent.
config/routes.rb-234-236 (1)

234-236: ⚠️ Potential issue | 🟠 Major

Avoid declaring resources :assignments twice.

Line 234 opens a second top-level resources :assignments, which generates duplicate assignment CRUD routes and helper names. If you only need the nested duties route here, wrap this block with only: [] to prevent the duplicate routes while allowing the nested resource to function.

Suggested fix
-      resources :assignments do
-        resources :duties, controller: 'assignments_duties', only: [:index, :create, :destroy]
+      resources :assignments, only: [] do
+        resources :duties, controller: 'assignments_duties', only: %i[index create destroy]
       end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/routes.rb` around lines 234 - 236, The routes file declares a second
top-level resources :assignments block which duplicates CRUD routes; change that
block to only expose the nested resource by using resources :assignments, only:
[] do ... end and keep the nested resources :duties (controller:
'assignments_duties', only: [:index, :create, :destroy]) inside it so you do not
generate duplicate assignment routes or helper names.
app/controllers/questionnaires_controller.rb-40-46 (1)

40-46: ⚠️ Potential issue | 🟠 Major

destroy! now turns protected deletes into 500s.

Line 43 will raise ActiveRecord::DeleteRestrictionError when the check_for_question_associations callback (defined in app/models/questionnaire.rb line 78) blocks destruction, but this action only rescues ActiveRecord::RecordNotFound. The unhandled exception results in a 500 error instead of a controlled 409/422 response.

Suggested controller handling
   def destroy
     begin
       `@questionnaire` = Questionnaire.find(params[:id])
       `@questionnaire.destroy`! # ensures dependent items are removed
+      head :no_content
     rescue ActiveRecord::RecordNotFound
       render json: $ERROR_INFO.to_s, status: :not_found and return
+    rescue ActiveRecord::DeleteRestrictionError, ActiveRecord::RecordNotDestroyed => e
+      render json: { error: e.message }, status: :conflict and return
     end
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/questionnaires_controller.rb` around lines 40 - 46, The
destroy action calls `@questionnaire.destroy`! which can raise
ActiveRecord::DeleteRestrictionError from the model callback
check_for_question_associations; add a rescue for
ActiveRecord::DeleteRestrictionError (in addition to the existing
ActiveRecord::RecordNotFound rescue) around the `@questionnaire.destroy`! call and
render a controlled JSON error response (including the exception message) with
an appropriate 409/422 status (e.g., status: :conflict) so protected deletes
return a client error instead of a 500.
app/models/response_map.rb-115-123 (1)

115-123: ⚠️ Potential issue | 🟠 Major

Team activity is never considered for review maps.

For ReviewResponseMap, reviewee_id points to a team, but this helper starts by reading the reviewee association as a Participant. That makes reviewee nil for normal peer reviews, so needs_update_link? misses fresh team submissions and keeps returning the stale "Edit" state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/response_map.rb` around lines 115 - 123,
latest_submission_at_for_reviewee assumes reviewee is a Participant and ignores
team activity, causing ReviewResponseMap (where reviewee_id is a Team) to miss
fresh submissions; update latest_submission_at_for_reviewee to handle team
reviewees by checking if reviewee.respond_to?(:team) and, when reviewee is a
Team or has a team, include timestamps from the Team (e.g., team.updated_at,
team.submissions or team.participants' latest updated_at) in the candidates
array so needs_update_link? sees recent team activity; ensure you reference the
existing reviewee association and preserve legacy checks using
respond_to?(:updated_at) and .present? when adding team-derived timestamps.
app/models/student_task.rb-65-91 (1)

65-91: ⚠️ Potential issue | 🟠 Major

This collapses per-team quiz state into one arbitrary assignment-level quiz.

The PR now allows different reviewee teams on the same assignment to own different quizzes, but this code picks the first ReviewResponseMap and exposes a single quiz_questionnaire_id / quiz_taken on StudentTask. For reviewers with multiple maps, that state will be wrong for at least some rows. Prefer returning quiz state per response map only, or aggregate it explicitly instead of using .first.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/student_task.rb` around lines 65 - 91, The code collapses
team-specific quiz state by taking the first ReviewResponseMap; stop using
.first on ReviewResponseMap and instead compute quiz state per response map (or
explicitly aggregate) so reviewers with multiple maps get correct info. Replace
the ReviewResponseMap... .first/quiz_questionnaire_id/quiz_taken logic: iterate
over ReviewResponseMap.where(reviewer_id: participant.id) (or map each rm), for
each rm find the Team via Team.find_by(id: rm.reviewee_id), read
team&.quiz_questionnaire_id, and determine quiz_taken by querying
QuizResponseMap.where(reviewer_id: participant.id, reviewed_object_id:
quiz_questionnaire_id).joins(...).where(responses: { is_submitted: true
}).exists?; then attach the per-map results (e.g., an array of
{review_response_map_id/quiz_questionnaire_id/quiz_taken} or an explicit
aggregated value) to StudentTask instead of a single
quiz_questionnaire_id/quiz_taken derived from .first.
app/controllers/assignments_controller.rb-256-256 (1)

256-256: ⚠️ Potential issue | 🟠 Major

notification_limit is no longer writable.

Response#reportable_difference? still reads assignment_questionnaire.notification_limit, but that attribute is filtered out here. New or updated questionnaire links will silently drop the threshold instead of persisting it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/assignments_controller.rb` at line 256, The
assignment_questionnaires_attributes strong params are missing
:notification_limit which causes new/updated questionnaire links to drop that
value while Response#reportable_difference? still reads
assignment_questionnaire.notification_limit; update the permitted attributes
list (assignment_questionnaires_attributes) to include :notification_limit so
the controller permits and persists that field when creating/updating
assignment_questionnaire records.
app/controllers/quiz_response_maps_controller.rb-48-60 (1)

48-60: ⚠️ Potential issue | 🟠 Major

The fallback path can attach the wrong team's quiz.

This branch explicitly guesses from the reviewer's first ReviewResponseMap when reviewee_team_id is missing. In the exact multi-team case this PR adds support for, that can create/reuse a quiz map for the wrong team and score the wrong questionnaire. If the team id is required for correctness, fail fast instead of guessing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/quiz_response_maps_controller.rb` around lines 48 - 60, The
fallback branch that guesses a reviewee team when reviewee_team_id is missing
can attach the wrong team's quiz; instead, if reviewee_team_id is blank or zero,
fail fast rather than guessing: remove the code path that picks the first
ReviewResponseMap (review_map = ReviewResponseMap.find_by(...)) and replace it
with a guard that checks for reviewee_team_id presence and returns/raises an
error (e.g., render status 400 or raise
ArgumentError/ActiveRecord::RecordNotFound) when missing; if you must attempt
lookup, use ReviewResponseMap.where(...).to_a and if more than one match exists,
treat that as ambiguous and fail rather than selecting the first—refer to
reviewee_team_id, reviewer_participant_lookup, ReviewResponseMap (review_map)
and reviewee_team to locate the code to change.
app/controllers/response_maps_controller.rb-45-50 (1)

45-50: ⚠️ Potential issue | 🟠 Major

Skipping "quiz" maps by id equality will drop legitimate peer reviews.

Here reviewer_id and reviewee_id come from different tables for normal review maps, so equal integers are possible without the row being a quiz. That means some peer-review rows will disappear from the API by coincidence. Use the map's actual type/class discriminator instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/response_maps_controller.rb` around lines 45 - 50, The guard
that skips quiz maps by comparing map.reviewer_id and map.reviewee_id can
falsely drop legitimate peer reviews; replace that equality check with a
type/class discriminator check. Locate the code using map.reviewer_id and
map.reviewee_id and change the guard to detect quiz maps by their class/type
(for example: next if map.type == 'QuizResponseMap' or next if
map.is_a?(QuizResponseMap)) so you skip only actual quiz response maps rather
than rows with coincidentally equal IDs.
app/models/response.rb-122-125 (1)

122-125: ⚠️ Potential issue | 🟠 Major

Perfect quiz submissions are under-scored when max_question_score > 1.

The quiz branch awards 1 * weight for a correct answer, but maximum_score still uses weight * questionnaire.max_question_score. That means a fully correct quiz tops out below 100% unless max_question_score happens to be 1.

💡 Suggested fix
-        sum += (student_answer == correct && correct.present? ? 1 : 0) * (s.item.weight || 1)
+        per_question_max = questionnaire.max_question_score
+        sum += (student_answer == correct && correct.present? ? per_question_max : 0) * (s.item.weight || 1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/response.rb` around lines 122 - 125, The quiz scoring currently
adds a flat 1 * weight for correct answers, causing under-scoring when
questionnaire.max_question_score > 1; update the quiz branch (inside the is_quiz
&& comment_scored_types check) to award (weight * per-question max score) for a
correct answer instead of 1 * weight — use s.item.max_question_score if present
or fall back to questionnaire.max_question_score (and then weight via
s.item.weight || 1) so sum and maximum_score use the same per-question scale.
app/controllers/responses_controller.rb-26-26 (1)

26-26: ⚠️ Potential issue | 🟠 Major

Remove the unconditional true return or restructure to return boolean explicitly.

The method calls render json: { error: 'forbidden' }, status: :forbidden in lines 15 and 21 but then unconditionally returns true at line 26. This contradicts the authorization contract—rendering a 403 response signals denial, but returning true signals approval. In the authorization flow, all_actions_allowed? returns the result of action_allowed?, so a true return will pass the authorization check in the authorize before_action despite the forbidden response being sent.

All other controllers in the codebase either return boolean expressions directly (without rendering) or use explicit patterns like return false and render_forbidden. Restructure ResponsesController#action_allowed? to either:

  • Return false (let authorize filter render the 403), or
  • Use return after render to halt execution explicitly
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/responses_controller.rb` at line 26, The method
ResponsesController#action_allowed? currently ends with an unconditional `true`
which conflicts with earlier `render json: { error: 'forbidden' }, status:
:forbidden` calls; remove that unconditional `true` and instead ensure the
method returns a boolean consistent with the rendered response by either
returning false after rendering (or calling an existing helper like
render_forbidden and then `return false`) or adding an explicit `return` after
each `render` so execution halts; update all_actions_allowed? usage if needed to
rely on the boolean returned from action_allowed?.
app/controllers/responses_controller.rb-125-127 (1)

125-127: ⚠️ Potential issue | 🟠 Major

Check return values of update and destroy to prevent silent failures.

The unsubmit endpoint ignores the return value of update, and destroy only rescues exceptions. In Rails, both operations can fail silently (returning false) without raising exceptions—for example, if validations fail or destroy callbacks abort. While the Response model currently has no validations or callbacks, the code is not defensively written and would silently report success if either issue occurs in the future.

Suggested fix
   if `@response.is_submitted`?
-    `@response.update`(is_submitted: false)
-    render json: { message: "#{response_map_label} submission reopened for edits. The reviewer can now make changes.", response: `@response` }, status: :ok
+    if `@response.update`(is_submitted: false)
+      render json: { message: "#{response_map_label} submission reopened for edits. The reviewer can now make changes.", response: `@response` }, status: :ok
+    else
+      render json: { error: `@response.errors.full_messages.to_sentence` }, status: :unprocessable_entity
+    end
   else
     render json: { error: "This #{response_map_label.downcase} submission is not locked, so it cannot be reopened" }, status: :unprocessable_entity
   end
@@
-  `@response.destroy`
-  head :no_content
-rescue StandardError => e
-  render json: { error: e.message }, status: :unprocessable_entity
+  `@response.destroy`!
+  head :no_content
+rescue ActiveRecord::RecordNotDestroyed => e
+  render json: { error: e.record.errors.full_messages.to_sentence }, status: :unprocessable_entity

Also applies to: 138-141

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/responses_controller.rb` around lines 125 - 127, The unsubmit
action currently calls `@response.update`(is_submitted: false) and always renders
success; change it to check the boolean return of `@response.update` and render an
error (e.g., 422) with `@response.errors.full_messages` when update returns false.
Do the same for the destroy flow: call `@response.destroy` and if it returns false
(or frozen?) render an error response with the model errors or a generic failure
message instead of assuming success; ensure both branches use the existing
rendering pattern in ResponsesController so callers get proper HTTP status codes
and error details.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4668e577-e2e6-4b30-b0ff-115ccfa58d69

📥 Commits

Reviewing files that changed from the base of the PR and between cc03ecd and 4674e97.

⛔ Files ignored due to path filters (1)
  • Gemfile.lock is excluded by !**/*.lock
📒 Files selected for processing (92)
  • Gemfile
  • app/controllers/assignments_controller.rb
  • app/controllers/grades_controller.rb
  • app/controllers/invitations_controller.rb
  • app/controllers/item_types_controller.rb
  • app/controllers/items_controller.rb
  • app/controllers/questionnaire_types_controller.rb
  • app/controllers/questionnaires_controller.rb
  • app/controllers/questions_controller.rb
  • app/controllers/quiz_response_maps_controller.rb
  • app/controllers/response_maps_controller.rb
  • app/controllers/responses_controller.rb
  • app/controllers/sign_up_topics_controller.rb
  • app/controllers/student_teams_controller.rb
  • app/controllers/teams_controller.rb
  • app/helpers/security_helper.rb
  • app/mailers/invitation_mailer.rb
  • app/mailers/invitation_sent_mailer.rb
  • app/models/Item.rb
  • app/models/assignment.rb
  • app/models/concerns/response_map_subclass_titles.rb
  • app/models/concerns/review_aggregator.rb
  • app/models/item_type.rb
  • app/models/project_topic.rb
  • app/models/questionnaire.rb
  • app/models/questionnaire_type.rb
  • app/models/quiz_response_map.rb
  • app/models/response.rb
  • app/models/response_map.rb
  • app/models/sign_up_topic.rb
  • app/models/student_task.rb
  • app/models/team.rb
  • app/serializers/assignment_questionnaire_serializer.rb
  • app/serializers/assignment_serializer.rb
  • app/serializers/questionnaire_serializer.rb
  • app/validators/invitation_validator.rb
  • app/views/invitation_sent_mailer/send_invitation_email.html.erb
  • config/database.yml
  • config/routes.rb
  • db/migrate/20250321222753_rename_sign_up_topic_to_project_topic_in_signed_up_teams.rb
  • db/migrate/20250418004442_change_to_polymorphic_association_in_teams.rb
  • db/migrate/20250621151644_add_round_to_responses.rb
  • db/migrate/20250621152946_add_version_number_to_responses.rb
  • db/migrate/20250621180527_change_question_to_item_in_answers.rb
  • db/migrate/20250621180851_rename_item_foreign_key_index_in_answers.rb
  • db/migrate/20250626161114_add_name_to_teams.rb
  • db/migrate/20250629185100_add_grade_to_participant.rb
  • db/migrate/20250629185439_add_grade_for_submission_to_team.rb
  • db/migrate/20250629190818_add_comment_for_submission_to_team.rb
  • db/migrate/20250727170825_add_questionnaire_weight_to_assignment_questionnaire.rb
  • db/migrate/20250805174104_rename_status_to_reply_status_in_in_join_team_requests.rb
  • db/migrate/20251013123456_add_textarea_width_to_items.rb
  • db/migrate/20251013123457_add_cols_rows_to_items.rb
  • db/migrate/20251021165336_add_participant_ref_to_invitations.rb
  • db/migrate/20251022160053_change_invitation_from_id_foreign_key.rb
  • db/migrate/20251029071649_add_advertisement_fields_to_signed_up_teams.rb
  • db/migrate/20251125012619_add_vary_by_round_to_assignments.rb
  • db/migrate/20251126161701_remove_participant_ref_from_invitations.rb
  • db/migrate/20251230123456_rename_question_types_to_item_types.rb
  • db/migrate/20260227203258_add_max_members_for_duty_to_duties.rb
  • db/migrate/20260313064334_add_submission_fields_to_teams.rb
  • db/migrate/20260423000001_add_correct_answer_to_items.rb
  • db/migrate/20260424000001_add_quiz_questionnaire_id_to_teams.rb
  • db/schema.rb
  • db/seeds.rb
  • db/seeds2.rb
  • docker-compose.yml
  • setup.sh
  • spec/controllers/responses_controller_spec.rb
  • spec/factories/project_topics.rb
  • spec/factories/reponse.rb
  • spec/factories/response_maps.rb
  • spec/factories/review_response_maps.rb
  • spec/mailers/previews/invitation_sent_preview.rb
  • spec/models/item_spec.rb
  • spec/models/project_topic_spec.rb
  • spec/models/response_map_label_spec.rb
  • spec/models/response_map_spec.rb
  • spec/models/response_rubric_label_spec.rb
  • spec/models/response_spec.rb
  • spec/models/signed_up_team_spec.rb
  • spec/models/student_task_spec.rb
  • spec/rails_helper.rb
  • spec/requests/api/v1/grades_controller_spec.rb
  • spec/requests/api/v1/invitation_controller_spec.rb
  • spec/requests/api/v1/questions_spec.rb
  • spec/requests/api/v1/quiz_response_maps_controller_spec.rb
  • spec/requests/api/v1/response_maps_controller_spec.rb
  • spec/requests/api/v1/student_teams_controller_spec.rb
  • spec/routing/sign_up_topics_routing_spec.rb
  • spec/validators/invitation_validator_spec.rb
  • swagger/v1/swagger.yaml
💤 Files with no reviewable changes (1)
  • spec/factories/review_response_maps.rb

Comment on lines +30 to +36
# When the frontend sends assignment_questionnaires_attributes without ids,
# clear existing records first to avoid duplicates.
if params[:assignment]&.key?(:assignment_questionnaires_attributes)
incoming = params[:assignment][:assignment_questionnaires_attributes]
has_ids = incoming.is_a?(Array) && incoming.any? { |aq| aq[:id].present? }
assignment.assignment_questionnaires.destroy_all unless has_ids
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

This deletes existing questionnaire links before the update is known to be valid.

destroy_all commits immediately. If assignment.update(...) fails afterward, the assignment has already lost its current assignment_questionnaires. Wrap the replacement in a transaction or express removals through nested _destroy so the whole change rolls back atomically.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/assignments_controller.rb` around lines 30 - 36, The code
currently calls assignment.assignment_questionnaires.destroy_all before
attempting assignment.update, which commits deletions immediately; change this
to perform removals atomically by wrapping the replacement in a transaction or
by converting removals to nested attributes with _destroy so they occur as part
of assignment.update. Specifically, remove the direct call to
assignment.assignment_questionnaires.destroy_all and instead either (A) wrap the
update flow in Assignment.transaction { assignment.update!(params[:assignment])
} where you build the incoming attributes so missing ids result in explicit
nested attributes deletions, or (B) transform the incoming
params[:assignment][:assignment_questionnaires_attributes] into nested
attributes that set _destroy: true for existing records to be removed and then
call assignment.update(params[:assignment]) so changes roll back if update
fails; locate the logic around
params[:assignment][:assignment_questionnaires_attributes],
assignment.assignment_questionnaires.destroy_all, and the subsequent
assignment.update(...) to implement one of these approaches.

Comment on lines +4 to +6
def action_allowed?
true
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not let callers choose the reviewer identity.

This endpoint unconditionally authorizes and then trusts reviewer_user_id from the request. Any authenticated user can create quiz participants/maps for another student and learn the target team's quiz identifiers. Bind the reviewer to current_user unless the caller is staff acting on someone else's behalf.

Also applies to: 33-35, 67-75

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/quiz_response_maps_controller.rb` around lines 4 - 6, The
controller currently trusts params[:reviewer_user_id] from callers; change
create/update flows (the actions handling quiz response map creation referenced
around lines 33-35 and 67-75) so that reviewer_user_id is set to current_user.id
by default and only overridden when the caller is authorized staff (use your
app's staff-check like current_user.is_staff? or a role check) — update the code
paths that read params[:reviewer_user_id] to enforce this binding, and use
action_allowed? (or the app's existing permission helpers) to gate any branch
that permits specifying a different reviewer.

Comment on lines +4 to +6
def action_allowed?
true
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

These endpoints allow cross-user map reads and writes.

The controller unconditionally authorizes and then trusts reviewer_user_id/id from the request. That lets any authenticated user enumerate another student's review maps, create mappings on their behalf, or delete arbitrary maps. Derive the reviewer from current_user and gate deletes to staff/owners.

Also applies to: 23-29, 111-119, 155-161

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/response_maps_controller.rb` around lines 4 - 6, The
controller currently trusts incoming reviewer_user_id/id params and always
returns true in action_allowed?, allowing cross-user reads/writes; update
action_allowed? to require at least authenticated user and remove reliance on
reviewer_user_id by deriving the reviewer from current_user (use current_user.id
wherever reviewer_user_id or id from params is used in create/update/show
methods), and restrict destroy (and any delete paths referenced around methods
handling ids) so only staff or map owners can delete (check current_user.staff?
|| current_user.id == map.reviewer_id before deletion); similarly validate
ownership when returning or modifying maps in the methods referenced (ensure
methods that read or modify maps compare map.reviewer_id to current_user.id or
require staff role).

Comment on lines +9 to +10
when 'create', 'show', 'index'
true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

create, show, and index are missing authorization.

These branches allow every caller through. Since create can return an existing draft and show/index serialize answer and comments, a user who knows or guesses IDs can read another reviewer’s quiz/review data. Gate these actions with the same owner/teaching-staff checks used for the write paths.

🧰 Tools
🪛 RuboCop (1.86.1)

[warning] 10-10: Literal true used in void context.

(Lint/Void)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/responses_controller.rb` around lines 9 - 10, The branches
allowing 'create', 'show', and 'index' in ResponsesController currently return
true and skip authorization; change them to invoke the same owner/teaching-staff
authorization used for write paths (i.e., replace the unconditional true for the
'create', 'show', 'index' branches with a call to the existing
owner/teaching-staff check method used elsewhere in this controller, or add a
before_action that enforces that check for create/show/index) so only the
response owner or teaching staff can read or create/return drafts.

Comment on lines +151 to +155
params.require(:response).permit(
:map_id,
:is_submitted,
:submitted_at,
scores_attributes: [:id, :item_id, :answer, :comments]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't mass-assign workflow-controlled fields.

update uses these params directly, so a reviewer can PATCH is_submitted, submitted_at, or even map_id. That bypasses the deadline/scoring flow in submit and lets a response be rebound to a different map. Only editable score content should be writable here.

Suggested fix
 def response_params
   params.require(:response).permit(
-    :map_id,
-    :is_submitted,
-    :submitted_at,
     scores_attributes: [:id, :item_id, :answer, :comments]
   )
 end
🧰 Tools
🪛 RuboCop (1.86.1)

[convention] 155-155: Use %i or %I for an array of symbols.

(Style/SymbolArray)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/responses_controller.rb` around lines 151 - 155, The
controller is permitting workflow-controlled fields (map_id, is_submitted,
submitted_at) in params.require(:response).permit used by update; remove map_id,
is_submitted and submitted_at from the permitted list so update cannot
mass-assign them, leaving only safe editable attributes (e.g. scores_attributes:
[:id, :item_id, :answer, :comments] and any other truly user-editable fields),
and ensure the submission/deadline flow remains handled only via the submit
method or dedicated controller action (do not add back those fields to the
update permit list).

Comment thread app/models/Item.rb
Comment on lines +60 to +85
# Serialises the item to a JSON-safe hash for API responses.
#
# The +correct_answer+ field is included only when {#is_quiz_item?} is +true+
# so that it is never accidentally exposed on peer-review rubric items.
# The nested +questionnaire+ object includes +questionnaire_type+ so the
# frontend can distinguish quiz items from review items without a second
# request. A synthetic +is_quiz_item+ boolean is appended to the hash.
#
# @param options [Hash] standard ActiveModel::Serializers::JSON options
# @return [Hash] the serialised item
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
only_fields = %i[
id txt weight seq question_type size alternatives break_before
min_label max_label created_at updated_at textarea_width
textarea_height textbox_width col_names row_names
]
only_fields << :correct_answer if is_quiz_item?

super(options.merge({
only: only_fields,
include: {
questionnaire: { only: %i[name id questionnaire_type] }
}
})).tap do |hash|
hash['is_quiz_item'] = is_quiz_item?
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not serialize correct_answer in reviewer-facing quiz payloads.

Line 76 appends the answer key to standard item JSON whenever the questionnaire is a quiz. Any client that can fetch quiz items can read the correct answers before submitting, which defeats the gating/scoring flow this PR adds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/Item.rb` around lines 60 - 85, The as_json method currently
appends :correct_answer whenever is_quiz_item? is true; change it so
correct_answer is only added when a caller explicitly requests it (e.g. via
options[:include_correct_answer] or similar) to avoid exposing answer keys to
reviewers. Update the line that modifies only_fields (replace the unconditional
check) to require both is_quiz_item? and options[:include_correct_answer], and
ensure any legitimate callers that need the key (exporters/instructors) pass
that option when calling Item#as_json; keep the rest of as_json (including the
questionnaire include and is_quiz_item? flag) unchanged.

Comment thread app/models/response.rb
Comment on lines +107 to +122
# E2619: quiz maps are identified by reviewer_id == reviewee_id (the student reviews
# themselves). The response_maps table has no STI type column so is_a?(QuizResponseMap)
# always returns false; this is the only reliable discriminator.
is_quiz = map.reviewer_id == map.reviewee_id
# E2619: The frontend stores question types with spaces ("Text field", "Multiple choice",
# "Multiple choice checkbox"). Include both the spaced and CamelCase variants so scoring
# works regardless of which convention was used when the quiz was created.
comment_scored_types = %w[
TextField MultipleChoiceRadio MultipleChoiceCheckbox
Text\ field Multiple\ choice Multiple\ choice\ checkbox
].freeze
scores.each do |s|
# For quiz responses, the weights will be 1 or 0, depending on if correct
sum += s.answer * s.item.weight unless s.answer.nil? #|| !s.item.scorable?
# E2619: TextField, MultipleChoiceRadio, and MultipleChoiceCheckbox quiz items put
# the student's selected/typed answer into the comments column (answer is null).
# Score them by case-insensitive equality against item.correct_answer.
if is_quiz && comment_scored_types.include?(s.item.question_type)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

reviewer_id == reviewee_id is not a safe quiz discriminator.

Normal review maps compare a participant id to a team id, so equal integers can happen by coincidence. When that happens, this code will treat a peer review as a quiz in both aggregate_questionnaire_score and maximum_score. ResponseMap already exposes a real type signal elsewhere in the codebase, so key off that instead.

Also applies to: 151-160

🧰 Tools
🪛 RuboCop (1.86.1)

[convention] 114-117: Use an array literal [...] for an array of words.

(Style/WordArray)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/response.rb` around lines 107 - 122, Replace the unsafe quiz
discriminator (map.reviewer_id == map.reviewee_id) with the ResponseMap's real
type signal: use the same type-check used elsewhere in the codebase (for example
checking map.type == 'QuizResponseMap' or calling the existing predicate method
used for ResponseMap type discrimination) to set is_quiz; update both the spot
around is_quiz in this block and the analogous check in lines 151-160 (affecting
aggregate_questionnaire_score and maximum_score) so they both rely on the
ResponseMap type signal (e.g., QuizResponseMap) rather than comparing
reviewer_id and reviewee_id.

</head>

<body>
<h1>You have been invited to join the team by <%= @to_user.fullname %></h1>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix inviter identity in email body.

Line 7 says the invitation is by @to_user (the recipient), which is incorrect. It should display the inviter (@from_user).

Suggested fix
-  <h1>You have been invited to join the team by <%= `@to_user.fullname` %></h1>
+  <h1>You have been invited to join the team by <%= `@from_user.fullname` %></h1>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<h1>You have been invited to join the team by <%= @to_user.fullname %></h1>
<h1>You have been invited to join the team by <%= `@from_user.fullname` %></h1>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/invitation_sent_mailer/send_invitation_email.html.erb` at line 7,
The email body currently shows the recipient `@to_user` as the inviter; update the
template to use the inviter instance variable `@from_user` instead (replace
`@to_user.fullname` with `@from_user.fullname`). Also add a safe fallback in the
view (e.g., use `@from_user`&.fullname or a default string) to avoid nil errors
when `@from_user` is missing.

Comment thread config/database.yml
Comment on lines +10 to +18
url: <%= ENV.fetch('DATABASE_URL', 'mysql2://root:expertiza@127.0.0.1:3307/reimplementation').gsub('?', '_development?') %>

test:
<<: *default
url: <%= ENV['DATABASE_URL'].gsub('?', '_test?') %>
url: <%= ENV.fetch('DATABASE_URL', 'mysql2://root:expertiza@127.0.0.1:3307/reimplementation').gsub('?', '_test?') %>

production:
<<: *default
url: <%= ENV['DATABASE_URL'].gsub('?', '_production?') %> No newline at end of file
url: <%= ENV.fetch('DATABASE_URL', 'mysql2://root:expertiza@127.0.0.1:3307/reimplementation').gsub('?', '_production?') %> No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm same fallback base URL is reused across environments
rg -n "ENV\.fetch\('DATABASE_URL', 'mysql2://root:expertiza@127\.0\.0\.1:3307/reimplementation'\)\.gsub" config/database.yml

Repository: expertiza/reimplementation-back-end

Length of output: 458


🏁 Script executed:

# Search for DATABASE_URL references and deployment configuration
rg -i "DATABASE_URL" --type-list | head -20

Repository: expertiza/reimplementation-back-end

Length of output: 469


🏁 Script executed:

# Check for environment configuration files and documentation
fd -type f \( -name ".env*" -o -name "*.md" -o -name "docker-compose*" -o -name "Dockerfile*" -o -name ".github" \) | head -20

Repository: expertiza/reimplementation-back-end

Length of output: 250


🏁 Script executed:

# Search for DATABASE_URL in various configuration contexts
rg -n "DATABASE_URL" -A 2 -B 2

Repository: expertiza/reimplementation-back-end

Length of output: 2558


🏁 Script executed:

# Look for README and setup documentation
fd -type f -iname "readme*" -o -iname "setup*" -o -iname "contributing*" | head -10

Repository: expertiza/reimplementation-back-end

Length of output: 250


🏁 Script executed:

# Check GitHub Actions and CI configurations
fd -type f \( -path "*/.github/workflows/*" -o -name ".gitlab-ci.yml" -o -name ".travis.yml" \)

Repository: expertiza/reimplementation-back-end

Length of output: 250


🏁 Script executed:

# Search for comments/documentation about database setup or DATABASE_URL expectations
rg -n "setup\|development\|environment" config/database.yml config/database.yml.old 2>/dev/null | head -20

Repository: expertiza/reimplementation-back-end

Length of output: 61


🏁 Script executed:

# Check if there's a .env.example or similar that documents DATABASE_URL
fd -type f -iname ".env*"

Repository: expertiza/reimplementation-back-end

Length of output: 250


🏁 Script executed:

# Find README files
find . -maxdepth 2 -iname "readme*" -o -iname "setup*"

Repository: expertiza/reimplementation-back-end

Length of output: 114


🏁 Script executed:

# Look for GitHub Actions or CI config directories
find . -maxdepth 3 -type d -name ".github" -o -type f -name ".gitlab-ci.yml" -o -type f -name "*.yml" -path "*/.github/*"

Repository: expertiza/reimplementation-back-end

Length of output: 281


🏁 Script executed:

# Read the full config/database.yml.old to understand the intended setup
cat config/database.yml.old | head -60

Repository: expertiza/reimplementation-back-end

Length of output: 1982


🏁 Script executed:

# List root directory to see what's available
ls -la | head -20

Repository: expertiza/reimplementation-back-end

Length of output: 1251


🏁 Script executed:

# Read README to understand setup requirements
cat README.md

Repository: expertiza/reimplementation-back-end

Length of output: 1081


🏁 Script executed:

# Check GitHub Actions test workflow to see how DATABASE_URL is handled
cat .github/workflows/TestPR.yml

Repository: expertiza/reimplementation-back-end

Length of output: 3542


🏁 Script executed:

# Check setup scripts
cat bin/setup 2>/dev/null || cat setup.sh 2>/dev/null || echo "No setup found"

Repository: expertiza/reimplementation-back-end

Length of output: 1117


Environment DB isolation breaks in fallback scenarios when DATABASE_URL is unset.

The gsub('?', '_<env>?') pattern only works if a ? exists in the URL. The fallback 'mysql2://root:expertiza@127.0.0.1:3307/reimplementation' has no query string, so all three environments would use the same database name, breaking data isolation.

While the documented setup (docker-compose) sets DATABASE_URL explicitly, the fallback is reachable in local non-Docker environments or misconfigured deployments. This creates a critical data safety risk.

The proposed fix is sound: use environment-specific variable names (DATABASE_URL_DEVELOPMENT, DATABASE_URL_TEST, DATABASE_URL_PRODUCTION) with appropriate fallback databases to guarantee isolation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/database.yml` around lines 10 - 18, The current url fallback uses
gsub('?', '_<env>?') on ENV.fetch('DATABASE_URL',
'mysql2://.../reimplementation'), which fails when DATABASE_URL is unset (no '?'
present) and causes all environments to share the same DB; change each
environment's url to prefer a dedicated env var (e.g. DATABASE_URL_DEVELOPMENT,
DATABASE_URL_TEST, DATABASE_URL_PRODUCTION) with a sensible fallback that
includes the env-specific database name instead of relying on gsub, and update
the three url lines (the ENV.fetch calls around DATABASE_URL and the gsub usage)
so each reads its own ENV key and falls back to a distinct database name to
guarantee isolation.

Comment on lines +255 to +275
# DELETE /invitations/:id
delete("Delete invitation") do
tags "Invitations"
parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token'

response(403, "Student cannot delete invitations") do
let(:id) { invitation2.id }
run_test!
end
end

delete("Delete invitation") do
tags "Invitations"
parameter name: 'Authorization', in: :header, type: :string, required: true
let(:Authorization) { "Bearer #{ta_token}" }

response(200, "Delete successful") do
let(:id) { invitation.id }
run_test!
end
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In rswag-specs, if the same pathblock defines two operations with the same HTTP verb (e.g., twodelete blocks), are both preserved or can one override the other in generated OpenAPI metadata?

💡 Result:

In rswag-specs, if the same path block defines two operations with the same HTTP verb (e.g., two delete blocks), the second operation overrides the first in the generated OpenAPI metadata. Both are not preserved. The DSL defines each verb method (delete, get, etc.) to create a single describe block that sets metadata[:operation] = { verb: verb, ... }. RSpec example groups nest hierarchically, so a second delete block inside the same path creates a nested group. The SwaggerFormatter merges metadata via deep_merge! on groups with :response metadata (from response blocks), but since both operations share the same path and verb keys in the OpenAPI paths object, the later one overwrites the earlier one due to hash key uniqueness in OpenAPI. No sources explicitly document allowing or handling duplicate verbs in one path block; the structure implies one operation per verb per path. OpenAPI itself prohibits duplicate verbs per path. Attempting duplicates would likely result in only the last operation appearing in the spec.

Citations:


The first delete operation is silently overridden by the second in the OpenAPI spec.

Defining two delete blocks for /invitations/{id} causes the second operation to overwrite the first in the generated OpenAPI metadata. This results in the 403 response case (student cannot delete) being completely lost from the spec. OpenAPI prohibits duplicate verbs per path, so only the last operation survives. Consolidate both response scenarios into a single delete block to preserve all test cases.

Refactor to one delete operation
-    # DELETE /invitations/:id
-    delete("Delete invitation") do
-      tags "Invitations"
-      parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token'
-
-      response(403, "Student cannot delete invitations") do
-        let(:id) { invitation2.id }
-        run_test!
-      end
-    end
-
-    delete("Delete invitation") do
+    delete("Delete invitation") do
       tags "Invitations"
-      parameter name: 'Authorization', in: :header, type: :string, required: true
-      let(:Authorization) { "Bearer #{ta_token}" }
+      parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token'
+
+      response(403, "Student cannot delete invitations") do
+        let(:id) { invitation2.id }
+        run_test!
+      end
 
       response(200, "Delete successful") do
+        let(:Authorization) { "Bearer #{ta_token}" }
         let(:id) { invitation.id }
         run_test!
       end
     end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# DELETE /invitations/:id
delete("Delete invitation") do
tags "Invitations"
parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token'
response(403, "Student cannot delete invitations") do
let(:id) { invitation2.id }
run_test!
end
end
delete("Delete invitation") do
tags "Invitations"
parameter name: 'Authorization', in: :header, type: :string, required: true
let(:Authorization) { "Bearer #{ta_token}" }
response(200, "Delete successful") do
let(:id) { invitation.id }
run_test!
end
end
delete("Delete invitation") do
tags "Invitations"
parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token'
response(403, "Student cannot delete invitations") do
let(:id) { invitation2.id }
run_test!
end
response(200, "Delete successful") do
let(:Authorization) { "Bearer #{ta_token}" }
let(:id) { invitation.id }
run_test!
end
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/requests/api/v1/invitation_controller_spec.rb` around lines 255 - 275,
Two separate delete("Delete invitation") blocks for the same path overwrite each
other; consolidate into a single delete("Delete invitation") operation that
declares the Authorization header once and contains both response examples
(response(403, ...) using let(:id) { invitation2.id } and response(200, ...)
using let(:id) { invitation.id } with let(:Authorization) { "Bearer #{ta_token}"
} where appropriate) so both test cases are preserved in the OpenAPI metadata
and run_test! is called inside each response block.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant