Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4b6a39d
Initial commit
DevPatel1106 Mar 23, 2026
65d70f2
Merge branch 'expertiza:main' into Reimp-Quiz
DevPatel1106 Mar 23, 2026
04f6686
Task ordering queue logic
MaybeNotArnav Mar 23, 2026
24e23cd
Added controller methods
DevPatel1106 Mar 23, 2026
79d405b
Refactor Task_gen_flow & student_task endpoints
DevPatel1106 Mar 25, 2026
375cbbc
Refactored TaskQueue and finished controller logic
MaybeNotArnav Mar 28, 2026
ec32bdd
Written some test cases but some of them fail.
akhilkumar2004 Mar 29, 2026
083259b
finished writing all the necessary test cases and running them. all o…
akhilkumar2004 Mar 29, 2026
1033a52
tested the student tasks controller and duties task controller
akhilkumar2004 Mar 29, 2026
490f789
finished testing all the required files!
akhilkumar2004 Mar 29, 2026
a564a62
added a test file for quiz_task and improved the line coverage for re…
akhilkumar2004 Mar 30, 2026
6a3bb7c
Added comments
DevPatel1106 Mar 30, 2026
af4664c
Merge branch 'Reimp-Quiz' into feature/test-cases
DevPatel1106 Mar 30, 2026
6d0871f
Merge pull request #5 from DevPatel1106/feature/test-cases
DevPatel1106 Mar 30, 2026
5bf44c9
Revert "Merge pull request #5 from DevPatel1106/feature/test-cases"
DevPatel1106 Mar 30, 2026
b201645
Clean Routes changes
DevPatel1106 Mar 30, 2026
360a3bf
redid all the test cases as all my test cases were under the test fil…
akhilkumar2004 Mar 30, 2026
177b930
Merge branch 'Reimp-Quiz' into feature/test-cases
DevPatel1106 Mar 31, 2026
d2ebc1e
Merge pull request #6 from DevPatel1106/feature/test-cases
DevPatel1106 Mar 31, 2026
753a026
Fixed improper modification of application_controller.rb
MaybeNotArnav Apr 24, 2026
cd97a31
Add student task quiz and review item classes
MaybeNotArnav Apr 24, 2026
d391d4c
Merge branch 'expertiza:main' into refactor
MaybeNotArnav Apr 24, 2026
20ca777
refactor(student_tasks): move task sequencing and gating logic into c…
DevPatel1106 Apr 25, 2026
c717383
Removed legacy task ordering layer
MaybeNotArnav Apr 25, 2026
6e18e07
Moved methods from the controller
MaybeNotArnav Apr 27, 2026
54cf1c5
Added quiz and review items
MaybeNotArnav Apr 27, 2026
4b2c1e2
Merge pull request #11 from DevPatel1106/refactor
DevPatel1106 Apr 27, 2026
cb36585
Moved task items and added comments
MaybeNotArnav Apr 29, 2026
20f8677
fix minor Indent
DevPatel1106 Apr 29, 2026
19def28
Try to untouch application_controller
DevPatel1106 Apr 29, 2026
464b1ea
Merge pull request #13 from DevPatel1106/refactor
DevPatel1106 Apr 29, 2026
ae03802
Try to untouch application_controller again
DevPatel1106 Apr 29, 2026
e8355c5
Merge pull request #14 from DevPatel1106/refactor
DevPatel1106 Apr 29, 2026
056da86
Fix student_task-Bot Comments
DevPatel1106 Apr 29, 2026
b941c84
Merge pull request #15 from DevPatel1106/refactor
DevPatel1106 Apr 29, 2026
676e2f2
Fix Bot & Review Comments
DevPatel1106 Apr 29, 2026
b60e0ca
Merge pull request #16 from DevPatel1106/refactor
DevPatel1106 Apr 29, 2026
60fa961
Fix Gramatical Error
DevPatel1106 Apr 29, 2026
c4fe2ca
Merge pull request #17 from DevPatel1106/refactor
DevPatel1106 Apr 29, 2026
072ae8d
Created test cases for the Student Task, Responses Controller, and Ta…
akhilkumar2004 Apr 29, 2026
0354ee8
Added test cases to test student tasks flow, responses task ordering,…
akhilkumar2004 Apr 29, 2026
04f87f3
Merge pull request #18 from DevPatel1106/refactor
akhilkumar2004 Apr 29, 2026
b8dc59a
Update student_tasks_controller_tasks_item_spec.rb
akhilkumar2004 Apr 29, 2026
dc7d24c
Merge pull request #19 from DevPatel1106/refactor
akhilkumar2004 Apr 29, 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
12 changes: 6 additions & 6 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ Style/FrozenStringLiteralComment:
Metrics/BlockLength:
Max: 120
Exclude:
- 'db/**/*.rb'
- "db/**/*.rb"

Metrics/MethodLength:
Max: 20
Exclude:
- 'db/**/*.rb'
Max: 20
Exclude:
- "db/**/*.rb"

Metrics/AbcSize:
Max: 20
Exclude:
- 'db/**/*.rb'
- "db/**/*.rb"

@johnmweisz johnmweisz Apr 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: unnecessary diffs in this file

Style/StringLiterals:
Enabled: true
EnforcedStyle: single_quotes
Exclude:
- 'db/**/*.rb'
- "db/**/*.rb"
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ application up and running.

Things you may want to cover:

* Ruby version - 3.4.5
- Ruby version - 3.4.5

## Development Environment

### Prerequisites

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: unnecessary diffs in this file

- Verify that [Docker Desktop](https://www.docker.com/products/docker-desktop/) is installed and running.
- [Download](https://www.jetbrains.com/ruby/download/) RubyMine
- Make sure that the Docker plugin [is enabled](https://www.jetbrains.com/help/ruby/docker.html#enable_docker).


### Instructions

Tutorial: [Docker Compose as a remote interpreter](https://www.jetbrains.com/help/ruby/using-docker-compose-as-a-remote-interpreter.html)

### Video Tutorial
Expand All @@ -25,5 +26,6 @@ Tutorial: [Docker Compose as a remote interpreter](https://www.jetbrains.com/hel
alt="IMAGE ALT TEXT HERE" width="560" height="315" border="10" /></a>

### Database Credentials

- username: root
- password: expertiza
123 changes: 123 additions & 0 deletions app/controllers/responses_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# frozen_string_literal: true

class ResponsesController < ApplicationController

@johnmweisz johnmweisz Apr 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Responses is a very generic name, consider something like task_responses_controller instead

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Responses controller is supposed to have other endpoints and functionalities being worked on in another PR that will be merged at some point. For consistency we kept the same file name for now.

prepend_before_action :set_response, only: %i[show update]
before_action :find_and_authorize_map_for_create, only: %i[create]

# Checks whether the current user can use the requested response action.
def action_allowed?
case action_name
when "create"
true # auth already handled by before_action above
when "show", "update"
@response && @response.map.reviewer.user_id == current_user.id
else
true

@johnmweisz johnmweisz Apr 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: Defaulting to true for unknown actions is less secure than defaulting to false (consider this a nitpick ie can dismiss without explanation)

end
end

# Shows the response details for one task response.
def show
render json: {
response_id: @response.id,
map_id: @response.map_id,
task_type: @response.map.type,
submitted: @response.is_submitted,
additional_comment: @response.additional_comment
}
end

# Creates or reuses a response for the requested response map.
def create
return unless enforce_task_order!(@map)

round = (params[:round].presence || 1).to_i
response = Response.where(map_id: @map.id, round: round)
.order(:created_at)
.last || Response.new(map_id: @map.id, round: round)
Comment on lines +34 to +37

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 | 🟠 Major

Do not let clients choose the task round.

The task-ordering layer only provisions round: 1 responses (app/models/student_task.rb:173-182), but this action trusts params[:round] and even coerces invalid input to 0 via to_i. That lets callers create off-queue responses, and completed? will still count any submitted response on the map as task completion (app/models/student_task.rb:186-190). Derive the round server-side instead of accepting it here.

🤖 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 34 - 37, Ignore
params[:round] and derive the round on the server to prevent clients from
selecting off-queue rounds: stop using params[:round] (and the .to_i coercion)
in ResponsesController and instead compute the round from existing responses for
the map (e.g. inspect Response records for `@map` to get the current/highest round
or the next expected round) before doing Response.where(map_id: `@map.id`, round:
round)...last or building a new Response; update the logic in
ResponsesController that currently references Response.where(map_id: `@map.id`,
round: round) so it uses the server-derived round, preventing creation of
arbitrary rounds that StudentTask#completed? will count.


if params[:content].present? || params[:additional_comment].present?
response.additional_comment = params[:content].presence || params[:additional_comment]
end

if response.save
render json: { response_id: response.id, map_id: @map.id, round: response.round }, status: :created
else
render json: { errors: response.errors.full_messages }, status: :unprocessable_entity
end
end

# Updates the saved response with submission details or comments.
def update
return unless enforce_task_order!(@response.map)

if @response.update(response_update_params)
render json: {
response_id: @response.id,
map_id: @response.map_id,
submitted: @response.is_submitted,
additional_comment: @response.additional_comment
}, status: :ok
else
render json: { errors: @response.errors.full_messages }, status: :unprocessable_entity
end
end

private

# Finds the response used by show and update.
def set_response
@response = Response.find(params[:id])
end

# Finds the response map and checks that the current user owns it.
def find_and_authorize_map_for_create
@map = ResponseMap.find_by(id: params[:response_map_id])
unless @map
render json: { error: "ResponseMap not found" }, status: :not_found
return
end

unless @map.reviewer.user_id == current_user.id
render json: { error: "You are not authorized to create this response" }, status: :forbidden
end
Comment thread
coderabbitai[bot] marked this conversation as resolved.
end


# Allows only the response fields that can be changed by this controller.
def response_update_params
p = params.permit(:is_submitted, :additional_comment, :content, :round)
p[:additional_comment] = p[:content] if p[:content].present?
p.delete(:content)
p
Comment on lines +88 to +92

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 | 🟠 Major

Do not let update rewrite the response round.

update is acting on an already-selected Response, but these strong params still permit :round. That lets a client move an existing submission into another round after the queue/order checks have passed.

Proposed fix
   def response_update_params
-    p = params.permit(:is_submitted, :additional_comment, :content, :round)
+    p = params.permit(:is_submitted, :additional_comment, :content)
     p[:additional_comment] = p[:content] if p[:content].present?
     p.delete(:content)
     p
   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
def response_update_params
p = params.permit(:is_submitted, :additional_comment, :content, :round)
p[:additional_comment] = p[:content] if p[:content].present?
p.delete(:content)
p
def response_update_params
p = params.permit(:is_submitted, :additional_comment, :content)
p[:additional_comment] = p[:content] if p[:content].present?
p.delete(:content)
p
end
🧰 Tools
🪛 RuboCop (1.86.1)

[convention] 82-82: Expected 1 empty line between method definitions; found 2.

(Layout/EmptyLineBetweenDefs)

🤖 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 82 - 86, The
response_update_params method currently permits :round which allows clients to
change an existing Response's round; remove :round from the permitted list and
ensure update uses only the allowed fields (e.g., :is_submitted,
:additional_comment), keep the existing content-to-additional_comment mapping
(p[:additional_comment] = p[:content] if p[:content].present?) and delete
:content as before; update any reference in the update action to call
response_update_params unchanged so round cannot be mass-assigned or rewritten
via this endpoint.

end

# Makes sure earlier tasks are finished before this task can be changed.
def enforce_task_order!(map)
participant = map.reviewer
unless participant.user_id == current_user.id
render json: { error: "Unauthorized" }, status: :forbidden
return false
end

context = StudentTask.resolve_context_for_participant(participant)
unless context
render json: { error: "TeamsParticipant not found for reviewer" }, status: :forbidden
return false
end

tasks = StudentTask.build_tasks(context)
current_task = StudentTask.find_task_for_map(tasks, map.id)
unless current_task
render json: { error: "Response map is not a respondable task for this participant" }, status: :forbidden
return false
end

unless StudentTask.prior_tasks_complete?(tasks, current_task)
render json: { error: "Complete previous task first" }, status: :precondition_failed
return false
end

true
end
end
67 changes: 59 additions & 8 deletions app/controllers/student_tasks_controller.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,79 @@
class StudentTasksController < ApplicationController

# List retrieves all student tasks associated with the current logged-in user.
def action_allowed?
current_user_has_student_privileges?
end

# ---------------------------------------------------------------------------
# Actions
# ---------------------------------------------------------------------------

def list
# Retrieves all tasks that belong to the current user.
@student_tasks = StudentTask.from_user(current_user)
# Render the list of student tasks as JSON.
render json: @student_tasks, status: :ok
end

def show
render json: @student_task, status: :ok
end

# The view function retrieves a student task based on a participant's ID.
# It is meant to provide an endpoint where tasks can be queried based on participant ID.
def view
# Retrieves the student task where the participant's ID matches the provided parameter.
# This function will be used for clicking on a specific student task to "view" its details.
@student_task = StudentTask.from_participant_id(params[:id])
# Render the found student task as JSON.
render json: @student_task, status: :ok
end

# Returns the full ordered task queue for an assignment.
def queue
context = StudentTask.resolve_context_for_assignment(current_user, params[:assignment_id])
return render json: { error: "Not authorized or not found" }, status: :not_found unless context

tasks = StudentTask.build_tasks(context)
StudentTask.ensure_response_objects!(tasks)

render json: tasks.map(&:to_h), status: :ok
end

# Returns the next unfinished task in the assignment queue.
def next_task
context = StudentTask.resolve_context_for_assignment(current_user, params[:assignment_id])
return render json: { error: "Not authorized or not found" }, status: :not_found unless context

tasks = StudentTask.build_tasks(context)
StudentTask.ensure_response_objects!(tasks)
next_task = tasks.find { |task| !task.completed? }

if next_task
render json: next_task.to_h, status: :ok
else
render json: { message: "All tasks completed" }, status: :ok
end
end

# Starts a task after checking that earlier tasks are complete.
def start_task
map = ResponseMap.find_by(id: params[:response_map_id])
return render json: { error: "ResponseMap not found" }, status: :not_found unless map

participant = map.reviewer
return render json: { error: "Unauthorized" }, status: :forbidden if participant.user_id != current_user.id

context = StudentTask.resolve_context_for_participant(participant)
return render json: { error: "Task not in respondable queue" }, status: :not_found unless context

tasks = StudentTask.build_tasks(context)
current_task = StudentTask.find_task_for_map(tasks, map.id)
return render json: { error: "Task not in respondable queue" }, status: :not_found unless current_task

unless StudentTask.prior_tasks_complete?(tasks, current_task)
return render json: { error: "Complete previous task first" }, status: :forbidden
end

current_task.ensure_response!

render json: {
message: "Task started",
task: current_task.to_h
}, status: :ok
end
end
12 changes: 12 additions & 0 deletions app/models/assignment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,22 @@ class Assignment < ApplicationRecord
#This method return the value of the has_badge field for the given assignment object.
attr_accessor :title, :description, :has_badge, :enable_pair_programming, :is_calibrated, :staggered_deadline

# Returns the assignment-linked review questionnaire record.
# The assignment can be linked to many questionnaires via AssignmentQuestionnaire.
def review_questionnaire_for_review_flow
questionnaires.find_by(questionnaire_type: 'ReviewQuestionnaire')
end
Comment on lines +23 to +27

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 | 🟠 Major

Don't collapse multi-round reviews to the first questionnaire.

This helper ignores AssignmentQuestionnaire.used_in_round, even though this model already supports round-specific rubrics later via varying_rubrics_by_round? and review_rounds. On assignments with more than one review questionnaire, find_by can bind a review task to the wrong rubric/questions.

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

In `@app/models/assignment.rb` around lines 23 - 27, The helper
review_questionnaire_for_review_flow collapses multi-round questionnaires by
using questionnaires.find_by(...) and ignores
AssignmentQuestionnaire.used_in_round; update this method to select the
questionnaire whose join record AssignmentQuestionnaire.used_in_round matches
the current review round (or accept a round parameter), using the
AssignmentQuestionnaire association or join to filter by used_in_round when
varying_rubrics_by_round? is true and fall back to the original behavior when
not; reference the review_questionnaire_for_review_flow method, the
AssignmentQuestionnaire.used_in_round attribute, and the existing
varying_rubrics_by_round? / review_rounds helpers so the correct round-specific
questionnaire is returned.


def review_questionnaire_id
Questionnaire.find_by_assignment_id id
end

# Returns the quiz questionnaire used by the reviewer pre-check flow.
# If no quiz questionnaire is attached, the caller can skip quiz task creation.
def quiz_questionnaire_for_review_flow
questionnaires.find_by(questionnaire_type: 'QuizQuestionnaire')
end

def teams?
@has_teams ||= teams.any?
end
Expand Down
35 changes: 35 additions & 0 deletions app/models/quiz_task_item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

class QuizTaskItem < StudentTask::BaseTaskItem
# Labels this task item as a quiz task.
def task_type
:quiz
end

# Finds the quiz questionnaire attached to this assignment.
def questionnaire
assignment.quiz_questionnaire_for_review_flow
end

# Finds or creates the quiz response map used to answer this task.
def response_map
return @response_map if @response_map

existing_map = QuizResponseMap.find_by(
reviewer_id: team_participant.participant_id,
reviewee_id: review_map&.reviewee_id || 0
)
return @response_map = existing_map if existing_map

return nil if questionnaire.nil?

attributes = {
reviewer_id: team_participant.participant_id,
reviewee_id: review_map&.reviewee_id || 0,
reviewed_object_id: questionnaire.id,
type: "QuizResponseMap"
}

@response_map = QuizResponseMap.new(attributes).tap { |map| map.save!(validate: false) }
Comment on lines +15 to +33

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 | 🟠 Major

Keep response_map read-only.

StudentTask::BaseTaskItem#completed?, #to_h, and StudentTask.find_task_for_map all call response_map. Persisting a QuizResponseMap from this accessor means read paths can create rows just by checking completion or serializing tasks. Move the create/save path into ensure_response_map! and keep response_map query-only.

Suggested split
 def response_map
-  return `@response_map` if `@response_map`
-
-  existing_map = QuizResponseMap.find_by(
-    reviewer_id: team_participant.participant_id,
-    reviewee_id: review_map&.reviewee_id || 0
-  )
-  return `@response_map` = existing_map if existing_map
-
-  return nil if questionnaire.nil?
-
-  attributes = {
-    reviewer_id: team_participant.participant_id,
-    reviewee_id: review_map&.reviewee_id || 0,
-    reviewed_object_id: questionnaire.id,
-    type: "QuizResponseMap"
-  }
-
-  `@response_map` = QuizResponseMap.new(attributes).tap { |map| map.save!(validate: false) }
+  return `@response_map` if defined?(`@response_map`)
+
+  `@response_map` = QuizResponseMap.find_by(
+    reviewer_id: team_participant.participant_id,
+    reviewee_id: review_map&.reviewee_id || 0
+  )
+end
+
+def ensure_response_map!
+  return response_map if response_map
+  return nil if questionnaire.nil?
+
+  attributes = {
+    reviewer_id: team_participant.participant_id,
+    reviewee_id: review_map&.reviewee_id || 0,
+    reviewed_object_id: questionnaire.id,
+    type: "QuizResponseMap"
+  }
+
+  `@response_map` = QuizResponseMap.new(attributes).tap { |map| map.save!(validate: false) }
 end
🧰 Tools
🪛 RuboCop (1.86.1)

[convention] 12-31: Cyclomatic complexity for response_map is too high. [8/7]

(Metrics/CyclomaticComplexity)

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

In `@app/models/student_task/quiz_task_item.rb` around lines 12 - 30, The
response_map accessor currently creates and persists a QuizResponseMap; change
response_map to be read-only by returning an existing QuizResponseMap lookup
(using QuizResponseMap.find_by with reviewer_id/team_participant.participant_id
and reviewee_id/review_map&.reviewee_id || 0) or nil, and remove any
creation/saving logic from it. Add a new method ensure_response_map! that builds
and persists the QuizResponseMap (using the same attributes: reviewer_id,
reviewee_id, reviewed_object_id: questionnaire.id, type: "QuizResponseMap"),
assigns it to `@response_map`, and saves it (validate: false) so callers that need
creation can call ensure_response_map! instead; keep callers like
StudentTask::BaseTaskItem#completed?, `#to_h` and StudentTask.find_task_for_map
using response_map for read-only access only.

end
end
13 changes: 13 additions & 0 deletions app/models/review_task_item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

class ReviewTaskItem < StudentTask::BaseTaskItem
# Labels this task item as a review task.
def task_type
:review
end

# Uses the existing review response map for this task.
def response_map
review_map
end
end
Loading
Loading