-
Notifications
You must be signed in to change notification settings - Fork 194
E2601. Reimplement student quizzes #309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4b6a39d
65d70f2
04f6686
24e23cd
79d405b
375cbbc
ec32bdd
083259b
1033a52
490f789
a564a62
6a3bb7c
af4664c
6d0871f
5bf44c9
b201645
360a3bf
177b930
d2ebc1e
753a026
cd97a31
d391d4c
20ca777
c717383
6e18e07
54cf1c5
4b2c1e2
cb36585
20f8677
19def28
464b1ea
ae03802
e8355c5
056da86
b941c84
676e2f2
b60e0ca
60fa961
c4fe2ca
072ae8d
0354ee8
04f87f3
b8dc59a
dc7d24c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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 | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,123 @@ | ||||||||||||||||||||||||
| # frozen_string_literal: true | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| class ResponsesController < ApplicationController | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not let clients choose the task round. The task-ordering layer only provisions 🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||
|
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not let
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
Suggested change
🧰 Tools🪛 RuboCop (1.86.1)[convention] 82-82: Expected 1 empty line between method definitions; found 2. (Layout/EmptyLineBetweenDefs) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't collapse multi-round reviews to the first questionnaire. This helper ignores 🤖 Prompt for AI Agents |
||
|
|
||
| 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 | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep
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 (Metrics/CyclomaticComplexity) 🤖 Prompt for AI Agents |
||
| end | ||
| end | ||
| 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 |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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