Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@
coverage/
rsa_keys.yml
pg_data/

# Other
setup.sh
Gemfile.lock
Gemfile
6 changes: 2 additions & 4 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '3.4.5'

gem 'mysql2', '~> 0.5.7'
gem 'redis', '~> 5.0'
gem 'sqlite3', '~> 1.4' # Alternative for development
gem 'puma', '~> 6.4'
gem 'rails', '~> 8.0', '>= 8.0.1'
Expand Down Expand Up @@ -57,13 +58,10 @@ gem 'find_with_order'

# For handling zip file uploads and extraction
gem 'rubyzip'
gem 'faker'


group :development, :test do
gem 'debug', platforms: %i[mri mingw x64_mingw]
gem 'factory_bot_rails'
gem 'database_cleaner-active_record'
gem 'faker'
gem 'rspec-rails'
gem 'rswag-specs'
gem 'rubocop'
Expand Down
24 changes: 9 additions & 15 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -121,26 +121,14 @@ GEM
octokit (>= 4.0)
pstore (~> 0.1)
terminal-table (>= 1, < 5)
database_cleaner-active_record (2.2.2)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0)
database_cleaner-core (2.0.1)
date (3.4.1)
debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
delegate (0.4.0)
diff-lcs (1.6.2)
docile (1.4.1)
domain_name (0.6.20240107)
drb (2.2.3)
erb (5.0.2)
erubi (1.13.1)
factory_bot (6.5.5)
activesupport (>= 6.1.0)
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
faker (3.5.2)
i18n (>= 1.8.11, < 2)
faraday (2.14.0)
Expand Down Expand Up @@ -225,6 +213,9 @@ GEM
net-protocol
netrc (0.11.0)
nio4r (2.5.9)
nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
Expand Down Expand Up @@ -303,6 +294,10 @@ GEM
rdoc (6.14.2)
erb
psych (>= 4.0.0)
redis (5.4.1)
redis-client (>= 0.22.0)
redis-client (0.28.0)
connection_pool
regexp_parser (2.11.3)
reline (0.6.2)
io-console (~> 0.5)
Expand Down Expand Up @@ -407,6 +402,7 @@ PLATFORMS
arm64-darwin-23
arm64-darwin-24
arm64-darwin-25
ruby
x64-mingw-ucrt
x86_64-linux

Expand All @@ -418,11 +414,8 @@ DEPENDENCIES
coveralls
csv
danger
database_cleaner-active_record
date
debug
delegate
factory_bot_rails
faker
faraday-retry
find_with_order
Expand All @@ -440,6 +433,7 @@ DEPENDENCIES
puma (~> 6.4)
rack-cors
rails (~> 8.0, >= 8.0.1)
redis (~> 5.0)
rspec-rails
rswag-api
rswag-specs
Expand Down
63 changes: 55 additions & 8 deletions app/controllers/assignments_duties_controller.rb
Original file line number Diff line number Diff line change
@@ -1,31 +1,78 @@
class AssignmentsDutiesController < ApplicationController
include Authorization
#before_action :authenticate_user!
before_action :action_allowed!, only: [:create, :destroy]
before_action :action_allowed!, only: [:create, :destroy, :update_limit]
before_action :set_assignment
before_action :set_assignment_duty, only: [:update_limit]

# GET /assignments/:assignment_id/duties
def index
assignment = Assignment.find(params[:assignment_id])
render json: assignment.duties
render json: serialized_assignment_duties
end

# POST /assignments/:assignment_id/duties
def create
assignment = Assignment.find(params[:assignment_id])
duty = Duty.find(params[:duty_id])
assignment.duties << duty unless assignment.duties.include?(duty)
render json: assignment.duties
assignment_duty = @assignment.assignments_duties.find_or_initialize_by(duty_id: duty.id)
assignment_duty.max_members_for_duty ||= 1

if assignment_duty.save
render json: serialized_assignment_duties, status: :ok
else
render json: assignment_duty.errors, status: :unprocessable_entity
end
end

# DELETE /assignments/:assignment_id/duties/:id
def destroy
assignment = Assignment.find(params[:assignment_id])
duty = Duty.find(params[:id])
assignment.duties.delete(duty)
@assignment.duties.delete(duty)
head :no_content
end

# PATCH /assignments/:assignment_id/duties/:id/limit
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add a comment saying that the limit is the max # of team members who can have this duty.

# Set or update the maximum number of team members allowed to have this duty (role) for a given assignment.
# Request body: { max_members_for_duty: integer }
# Response: The updated duty mapping with the new limit, or validation errors if invalid.
def update_limit
if @assignment_duty.update(limit_params)
render json: serialize_assignment_duty(@assignment_duty), status: :ok
else
render json: @assignment_duty.errors, status: :unprocessable_entity
end
end

private

def set_assignment
@assignment = Assignment.find(params[:assignment_id])
end

def set_assignment_duty
@assignment_duty = @assignment.assignments_duties.find_by(duty_id: params[:id])
return if @assignment_duty

render json: { error: 'Duty is not assigned to this assignment' }, status: :not_found
end

def limit_params
params.permit(:max_members_for_duty)
end

def serialized_assignment_duties
@assignment.assignments_duties.includes(:duty).map do |assignment_duty|
serialize_assignment_duty(assignment_duty)
end
end

def serialize_assignment_duty(assignment_duty)
{
duty_id: assignment_duty.duty_id,
duty_name: assignment_duty.duty&.name,
max_members_for_duty: assignment_duty.max_members_for_duty
}
end

def action_allowed!
unless current_user_has_instructor_privileges?
render json: { error: 'Not authorized' }, status: :forbidden
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

action_allowed! renders 403 but does not return, so the action may continue after unauthorized access.

Expand Down
18 changes: 17 additions & 1 deletion app/controllers/participants_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def show
end
end



# Assign the specified authorization to the participant and add them to an assignment
# POST /participants/:authorization
def add
Expand Down Expand Up @@ -98,6 +100,20 @@ def update_authorization
end
end

# Update the specified participant duty
# PATCH /participants/:id/duty
def update_duty
participant = find_participant
return unless participant

participant.duty_id = params[:duty_id].presence
if participant.save
render json: participant, status: :created
else
render json: participant.errors, status: :unprocessable_entity
end
end
Comment on lines +103 to +115
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 | 🟡 Minor | ⚡ Quick win

Use :ok instead of :created for update operations.

The update_duty action returns HTTP 201 (:created) on line 111, but this endpoint updates an existing participant resource. Per REST conventions and codebase patterns (see UsersController#update, RolesController#update, DutiesController#update which all return :ok), successful updates should return HTTP 200.

Proposed fix
     if participant.save
-      render json: participant, status: :created
+      render json: participant, status: :ok
     else
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/participants_controller.rb` around lines 103 - 115, The
update_duty action incorrectly returns HTTP 201 (:created) for a successful
update; change the render in update_duty (method update_duty in
ParticipantsController) to use status: :ok to match REST conventions and other
controllers (UsersController#update, RolesController#update,
DutiesController#update); update the success render call from render json:
participant, status: :created to render json: participant, status: :ok and run
related controller specs to ensure expected status assertions are updated.


# Delete a participant
# params - id
# DELETE /participants/:id
Expand Down Expand Up @@ -189,4 +205,4 @@ def validate_authorization

authorization
end
end
end
26 changes: 24 additions & 2 deletions app/controllers/teams_participants_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,30 @@ def update_duty
end

duty_id = params.dig(:teams_participant, :duty_id) || params.dig("teams_participant", "duty_id")
team_participant.update(duty_id: duty_id)
render json: { message: "Duty updated successfully" }, status: :ok
participant = team_participant.participant

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Needs a clearer comment.

# For assignment teams, derive the assignment from the team, not from participant.parent_id.
# This controller also serves CourseTeam, so participant.parent_id is not always an assignment id.
# Gate this action to assignment teams and resolve the assignment from team_participant.team.
assignment = nil
if team_participant.team.type == "AssignmentTeam"
assignment = team_participant.team.assignment
else
render json: { error: 'Duty assignment is only supported for assignment teams' }, status: :unprocessable_entity and return
end
if duty_id.present?
assignment_duty = AssignmentsDuty.find_by(assignment_id: assignment.id, duty_id: duty_id)
unless assignment_duty
render json: { error: 'Duty is not assigned to this assignment' }, status: :unprocessable_entity and return
end
end

participant.duty_id = duty_id
if participant.save
render json: { message: 'Duty updated successfully' }, status: :ok
else
render json: { error: participant.errors.full_messages.to_sentence }, status: :unprocessable_entity
end
end

# Displays a list of all participants in a specific team.
Expand Down
2 changes: 2 additions & 0 deletions app/models/assignment_questionnaire.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
class AssignmentQuestionnaire < ApplicationRecord
belongs_to :assignment
belongs_to :questionnaire
belongs_to :duty, optional: true
end

2 changes: 2 additions & 0 deletions app/models/assignments_duty.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class AssignmentsDuty < ApplicationRecord
belongs_to :assignment
belongs_to :duty

validates :max_members_for_duty, numericality: { only_integer: true, greater_than_or_equal_to: 1 }
end
23 changes: 18 additions & 5 deletions app/models/participant.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
# frozen_string_literal: true

class Participant < ApplicationRecord
# Associations
belongs_to :user
has_many :join_team_requests, dependent: :destroy
# belongs_to :team, optional: true
belongs_to :assignment, class_name: 'Assignment', foreign_key: 'parent_id', optional: true, inverse_of: :participants
belongs_to :course, class_name: 'Course', foreign_key: 'parent_id', optional: true, inverse_of: :participants
belongs_to :duty, optional: true

# Validations
validates :user_id, presence: true
validates :parent_id, presence: true
validates :type, presence: true, inclusion: { in: %w[AssignmentParticipant CourseParticipant], message: "must be either 'AssignmentParticipant' or 'CourseParticipant'" }
validate :duty_limit_within_team, if: -> { duty_id.present? && team.present? }

def retract_sent_invitations
# do nothing. Invitations are only sent by AssignmentParticipants only.
end

# Methods
def fullname
user.full_name
end
Expand All @@ -26,4 +23,20 @@ def team
TeamsParticipant.where(participant: self).first&.team
end

private

def duty_limit_within_team
assignment_duty = AssignmentsDuty.find_by(assignment_id: parent_id, duty_id: duty_id)
limit = assignment_duty&.max_members_for_duty || 1

count = TeamsParticipant.where(team_id: team.id)
.joins("INNER JOIN participants ON teams_participants.participant_id = participants.id")
.where(participants: { duty_id: duty_id })
.where.not(participants: { id: id })
.count

if count >= limit
errors.add(:duty, "limit reached for this team. Only #{limit} member(s) can be a #{duty.name}.")
end
end
end
16 changes: 15 additions & 1 deletion app/serializers/assignment_serializer.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
class AssignmentSerializer < ActiveModel::Serializer
attributes :id, :name, :max_team_size, :course_id
attributes :id, :name, :max_team_size, :course_id, :has_role_based_review, :assignment_duties

def has_role_based_review
object.assignments_duties.exists?
end

def assignment_duties
object.assignments_duties.includes(:duty).map do |assignment_duty|
{
duty_id: assignment_duty.duty_id,
duty_name: assignment_duty.duty&.name,
max_members_for_duty: assignment_duty.max_members_for_duty
}
end
Comment on lines +4 to +15
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect where AssignmentSerializer is used and whether those paths preload duties.
# Expected result: collection render paths should preload assignments_duties: :duty.

echo "== Assignment render sites =="
rg -nP --type ruby 'AssignmentSerializer|each_serializer:\s*AssignmentSerializer|serializer:\s*AssignmentSerializer|render\s+json:\s+@assignments|render\s+json:\s+@assignment' app/controllers app/serializers

echo
echo "== Duty preloading in controllers =="
rg -nP --type ruby 'includes\([^)]*assignments_duties[^)]*duty|preload\([^)]*assignments_duties[^)]*duty|eager_load\([^)]*assignments_duties[^)]*duty' app/controllers

Repository: expertiza/reimplementation-back-end

Length of output: 756


🏁 Script executed:

#!/bin/bash
# Check for other potential uses of AssignmentSerializer or assignment rendering
echo "== All AssignmentSerializer references =="
rg -n 'AssignmentSerializer' --type ruby

echo
echo "== Render json with assignment variable (broader search) =="
rg -nB2 -A2 'render.*json.*assignment' --type ruby app/controllers

echo
echo "== Check for respond_with patterns =="
rg -n 'respond_with.*assignment' --type ruby app/controllers

echo
echo "== Check what student.assignment returns =="
rg -n 'def assignment' --type ruby app/models/user.rb app/models/student.rb

Repository: expertiza/reimplementation-back-end

Length of output: 13995


🏁 Script executed:

#!/bin/bash
# Check if Student model exists or if it's part of User
echo "== Student model or relationship =="
fd -e rb . app/models | xargs grep -l "class.*Student" | head -5

echo
echo "== Check AssignmentSerializer attributes =="
cat -n app/serializers/assignment_serializer.rb | head -30

echo
echo "== Check if assignment_duties is accessed in controllers =="
rg -n 'assignment_duties|assignments_duties' app/controllers/student_teams_controller.rb

Repository: expertiza/reimplementation-back-end

Length of output: 962


🏁 Script executed:

#!/bin/bash
# Verify the Assignment model associations
echo "== Assignment model class definition and associations =="
cat -n app/models/assignment.rb | head -50

echo
echo "== Check for has_many :assignments_duties in Assignment =="
rg -A5 'has_many.*assignments_duties|has_many.*:assignments_duties' app/models/assignment.rb

Repository: expertiza/reimplementation-back-end

Length of output: 3536


These serializer methods trigger unnecessary database queries per assignment render.

Both has_role_based_review and assignment_duties issue separate database queries. Since AssignmentSerializer is currently used in student_teams_controller.rb to serialize single assignments, each render causes 2 extra queries that could be avoided by preloading assignments_duties and assignments_duties.duty in the controller and passing the data directly to the serializer.

🧰 Tools
🪛 RuboCop (1.86.1)

[convention] 4-4: Use 2 (not 5) spaces for indentation.

(Layout/IndentationWidth)


[convention] 5-5: Use 2 (not 5) spaces for indentation.

(Layout/IndentationWidth)


[convention] 8-8: Use 2 (not 5) spaces for indentation.

(Layout/IndentationWidth)


[convention] 9-9: Use 2 (not 5) spaces for indentation.

(Layout/IndentationWidth)


[convention] 10-10: Use 2 (not 5) spaces for indentation.

(Layout/IndentationWidth)


[convention] 11-11: Use 2 spaces for indentation in a hash, relative to the start of the line where the left curly brace is.

(Layout/FirstHashElementIndentation)

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

In `@app/serializers/assignment_serializer.rb` around lines 4 - 15, The serializer
methods has_role_based_review and assignment_duties are causing extra DB queries
by calling object.assignments_duties and accessing assignment_duty.duty for each
serialized Assignment; fix it by preloading assignments_duties and their duty
association in the controller (e.g. include :assignments_duties and
assignments_duties: :duty) and change AssignmentSerializer to accept the
already-loaded association data (use
object.association(:assignments_duties).loaded? or accept a precomputed payload)
so has_role_based_review checks the in-memory collection and assignment_duties
iterates the preloaded objects without triggering queries.

end
end
6 changes: 5 additions & 1 deletion app/serializers/participant_serializer.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class ParticipantSerializer < ActiveModel::Serializer
attributes :id, :user, :parent_id, :user_id, :authorization
attributes :id, :user, :parent_id, :user_id, :authorization, :duty_id, :duty_name

def user
{
Expand All @@ -9,4 +9,8 @@ def user
fullName: object.user.full_name
}
end

def duty_name
object.duty&.name
end
end
5 changes: 3 additions & 2 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def self.preview_path=(_)
module Reimplementation
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
config.load_defaults 8.0
config.active_record.schema_format = :ruby

# Configuration for the application, engines, and railties goes here.
Expand All @@ -32,6 +32,7 @@ class Application < Rails::Application
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
config.cache_store = :redis_store, ENV['CACHE_STORE'], { expires_in: 3.days, raise_errors: false }
config.cache_store = :redis_cache_store, { url: ENV['CACHE_STORE'], expires_in: 3.days }
end
end

Loading
Loading