-
Notifications
You must be signed in to change notification settings - Fork 194
E2612 - course based reports #333
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
93f473f
9180b5c
7e69cb0
fbb0be4
2e57ecb
63da377
9fe1600
3e93628
9be6da4
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 |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class CourseReportsController < ApplicationController | ||
| class FinalDueDateNotReviewDeadlineError < StandardError; end | ||
|
|
||
| # only restruct for course staff (TA + instr) | ||
| def action_allowed? | ||
| case params[:action] | ||
| when 'index' | ||
| course = Course.find_by(id: params[:course_id]) | ||
| return current_user_has_instructor_privileges? || current_user_has_ta_privileges? if course.nil? | ||
|
|
||
| current_user_teaching_staff_of_course?(course) | ||
| else | ||
| false | ||
| end | ||
| end | ||
|
|
||
| # GET /course_reports | ||
| # Returns a table for all assignments in the given course, | ||
| # with students as rows and assignments as horizontal columns. | ||
| def index | ||
| course = Course.find_by(id: params[:course_id]) | ||
| return render json: { error: 'Course not found' }, status: :not_found unless course | ||
|
|
||
| assignments = assignments_ordered_by(course, :final_review_due_date) | ||
| assignment_ids = assignments.map(&:id) | ||
| participants = AssignmentParticipant | ||
| .includes(:user, :assignment) | ||
| .where(parent_id: assignment_ids) | ||
|
|
||
| student_rows = participants | ||
| .group_by(&:user_id) | ||
| .values | ||
| .map { |student_participants| build_student_row(assignments, student_participants) } | ||
| .sort_by { |row| row[:user_name].downcase } | ||
|
|
||
| render json: course_report_response(course, assignments, student_rows), status: :ok | ||
| rescue FinalDueDateNotReviewDeadlineError => e | ||
| render json: { error: e.message }, status: :internal_server_error | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def current_user_teaching_staff_of_course?(course) | ||
| user_logged_in? && ( | ||
| course.instructor_id == current_user.id || | ||
| TaMapping.exists?(user_id: current_user.id, course_id: course.id) | ||
| ) | ||
| end | ||
|
|
||
| # metadata col | ||
| # indicates whether an assignment has optional columns eg topics. | ||
| def assignment_column(assignment) | ||
| { | ||
| assignment_id: assignment.id, | ||
| assignment_name: assignment.name, | ||
| has_topics: !!assignment.has_topics | ||
| } | ||
| end | ||
|
|
||
| def assignments_ordered_by(course, field) | ||
| course.assignments.includes(:due_dates).sort_by do |assignment| | ||
| [assignment_sort_value(assignment, field), assignment.id] | ||
| end | ||
| end | ||
|
|
||
| def assignment_sort_value(assignment, field) | ||
| case field | ||
| when :final_review_due_date | ||
| final_review_due_date_for(assignment) | ||
| else | ||
| assignment.public_send(field) | ||
| end | ||
| end | ||
|
|
||
| # function to retrieve final review due date from the db: | ||
| # Raises FinalDueDateNotReviewDeadline as error | ||
| def final_review_due_date_for(assignment) | ||
| final_due_date = assignment.due_dates.max_by(&:due_at) | ||
| return final_due_date.due_at if final_due_date&.deadline_type_id == DueDate::REVIEW_DEADLINE_TYPE_ID | ||
|
|
||
| # At this point of the project, all assignments are peer review assignments, | ||
| # so the final deadline is bound to be a review deadline, hence this guard | ||
| # | ||
| #Replace this with code in the incident that non peer review assignments are introduced | ||
| raise FinalDueDateNotReviewDeadlineError, | ||
| "Final due date for assignment #{assignment.id} is not a review deadline" | ||
| end | ||
|
|
||
| # function to build full response, along with the metadata + student rows. | ||
| def course_report_response(course, assignments, student_rows) | ||
| { | ||
| course_id: course.id, | ||
| course_name: course.name, | ||
| assignments: assignments.map { |assignment| assignment_column(assignment) }, | ||
| students: student_rows | ||
| } | ||
| end | ||
|
|
||
| # function to build each row of students (students x assignment matrix) | ||
| def build_student_row(assignments, student_participants) | ||
| first_participant = student_participants.first | ||
| participant_by_assignment = student_participants.index_by(&:parent_id) | ||
|
|
||
| { | ||
| user_id: first_participant.user_id, | ||
| user_name: first_participant.user_name, | ||
| assignments: assignment_cells_for_student(assignments, participant_by_assignment) | ||
| } | ||
| end | ||
|
|
||
| # per user assignment stats, combines assignment cells (next call )with student row | ||
| def assignment_cells_for_student(assignments, participant_by_assignment) | ||
| assignments.to_h do |assignment| | ||
| participant = participant_by_assignment[assignment.id] | ||
| [assignment.id.to_s, participant ? build_assignment_cell(assignment, participant) : nil] | ||
| end | ||
| end | ||
|
|
||
| # building per-assignment cell. each cell corresponds to each assignment in a single student row. | ||
| def build_assignment_cell(assignment, participant) | ||
| team = participant.team | ||
|
|
||
| { | ||
| participant_id: participant.id, | ||
| peer_grade: team&.aggregate_review_grade, | ||
| instructor_grade: team&.grade_for_submission, | ||
| avg_teammate_score: participant.aggregate_teammate_review_grade(teammate_review_maps_for(assignment, participant)), | ||
| avg_author_feedback_score: participant.aggregate_teammate_review_grade(author_feedback_maps_for(assignment, participant)) | ||
| }.tap do |cell| # optional topic col | ||
| cell[:topic] = topic_name_for(assignment, participant) if assignment.has_topics | ||
| end | ||
| end | ||
|
|
||
| # get topic name if exists | ||
| def topic_name_for(assignment, participant) | ||
| return unless assignment.has_topics | ||
|
|
||
| team_id = TeamsParticipant.find_by(participant_id: participant.id)&.team_id | ||
| return unless team_id | ||
|
|
||
| SignedUpTeam.find_by(team_id: team_id)&.project_topic&.topic_name | ||
| end | ||
|
|
||
| # response maps for teammate review. | ||
|
|
||
| def teammate_review_maps_for(assignment, participant) | ||
| TeammateReviewResponseMap.where(reviewed_object_id: assignment.id, reviewee_id: participant.id) | ||
| end | ||
|
|
||
| # response maps for auth feedback | ||
| def author_feedback_maps_for(assignment, participant) | ||
| review_maps = ReviewResponseMap.where(reviewed_object_id: assignment.id, reviewer_id: participant.id) | ||
|
|
||
| FeedbackResponseMap.where(reviewed_object_id: review_maps.select(:id), reviewee_id: participant.id) | ||
| end | ||
|
Comment on lines
+122
to
+157
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. This report builder currently has an N+1 query cascade. At Line 100-Line 131, each assignment cell triggers additional DB calls ( Refactor direction (preload once, then lookup in-memory) def index
course = Course.find_by(id: params[:course_id])
return render json: { error: 'Course not found' }, status: :not_found unless course
assignments = assignments_ordered_by_final_review_due_date(course)
assignment_ids = assignments.map(&:id)
- participants = AssignmentParticipant
- .includes(:user, :assignment)
+ participants = AssignmentParticipant
+ .includes(:user, :assignment)
.where(parent_id: assignment_ids)
+
+ participant_ids = participants.map(&:id)
+ teammate_maps = TeammateReviewResponseMap.where(reviewed_object_id: assignment_ids, reviewee_id: participant_ids)
+ .group_by { |m| [m.reviewed_object_id, m.reviewee_id] }
+ review_maps = ReviewResponseMap.where(reviewed_object_id: assignment_ids, reviewer_id: participant_ids)
+ feedback_maps = FeedbackResponseMap.where(reviewed_object_id: review_maps.select(:id), reviewee_id: participant_ids)
+ .group_by(&:reviewee_id)
student_rows = participants
.group_by(&:user_id)
.values
- .map { |student_participants| build_student_row(assignments, student_participants) }
+ .map { |student_participants| build_student_row(assignments, student_participants, teammate_maps, feedback_maps) }
.sort_by { |row| row[:user_name].downcase }🧰 Tools🪛 RuboCop (1.86.1)[convention] 107-107: Line is too long. [121/120] (Layout/LineLength) [convention] 108-108: Line is too long. [127/120] (Layout/LineLength) 🤖 Prompt for AI Agents |
||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,21 +154,23 @@ | |
| end | ||
| end | ||
|
|
||
| resources :participants do | ||
| collection do | ||
| get '/user/:user_id', to: 'participants#list_user_participants' | ||
| get '/assignment/:assignment_id', to: 'participants#list_assignment_participants' | ||
| get '/:id', to: 'participants#show' | ||
| resources :participants do | ||
| collection do | ||
| get '/user/:user_id', to: 'participants#list_user_participants' | ||
| get '/assignment/:assignment_id', to: 'participants#list_assignment_participants' | ||
| get '/:id', to: 'participants#show' | ||
| post '/:authorization', to: 'participants#add' | ||
| patch '/:id/:authorization', to: 'participants#update_authorization' | ||
| delete '/:id', to: 'participants#destroy' | ||
| end | ||
| end | ||
|
|
||
| resources :student_teams, only: %i[create update] do | ||
| collection do | ||
| get :view | ||
| get :mentor | ||
| delete '/:id', to: 'participants#destroy' | ||
| end | ||
| end | ||
|
Comment on lines
+157
to
+166
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. Fix indentation/trailing whitespace in the new route blocks. Line 157-Line 166, Line 168, and Line 172 currently trigger RuboCop layout cops; this can fail lint gates. Suggested cleanup- resources :participants do
- collection do
- get '/user/:user_id', to: 'participants#list_user_participants'
- get '/assignment/:assignment_id', to: 'participants#list_assignment_participants'
- get '/:id', to: 'participants#show'
+ resources :participants do
+ collection do
+ get '/user/:user_id', to: 'participants#list_user_participants'
+ get '/assignment/:assignment_id', to: 'participants#list_assignment_participants'
+ get '/:id', to: 'participants#show'
post '/:authorization', to: 'participants#add'
patch '/:id/:authorization', to: 'participants#update_authorization'
- delete '/:id', to: 'participants#destroy'
- end
- end
+ delete '/:id', to: 'participants#destroy'
+ end
+ end
- resources :course_reports, only: [:index]
+ resources :course_reports, only: [:index]
- resources :student_teams, only: %i[create update] do
- collection do
- get :view
- get :mentor
+ resources :student_teams, only: %i[create update] do
+ collection do
+ get :view
+ get :mentorAlso applies to: 170-173 🧰 Tools🪛 RuboCop (1.86.1)[convention] 157-166: Inconsistent indentation detected. (Layout/IndentationConsistency) 🤖 Prompt for AI Agents |
||
|
|
||
| resources :course_reports, only: [:index] | ||
|
|
||
| resources :student_teams, only: %i[create update] do | ||
| collection do | ||
| get :view | ||
| get :mentor | ||
| get :remove_participant | ||
| put '/leave', to: 'student_teams#leave_team' | ||
| end | ||
|
|
||
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.
Filter by review-deadline type before picking the “final” review deadline.
At Line 72,
max_by(&:due_at)runs across all due dates. If a later non-review deadline exists, Line 73 fails the type check and the endpoint returns 500 even when valid review deadlines are present.Proposed fix
🧰 Tools
🪛 RuboCop (1.86.1)
[convention] 77-77: Trailing whitespace detected.
(Layout/TrailingWhitespace)
[convention] 78-78: Missing space after
#.(Layout/LeadingCommentSpace)
🤖 Prompt for AI Agents