CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing
About This Project
| Project Info | |
|---|---|
| Course | CSC/ECE 517 Spring 2026 |
| Project | E2610 — Teams Hierarchy Testing |
| Mentor | Vihar Manojkumar Shah |
| Platform | Expertiza (Ruby on Rails) |
| Test Framework | RSpec |
| Root Class | Team (STI superclass)
|
| Subclasses | CourseTeam, AssignmentTeam, MentoredTeam
|
| Membership Join Model | TeamsParticipant
|
| Collaborators | Atharva, Krisha, Saladin
|
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the unit used to reserve topics, collaborate on coursework, and submit work. This project focuses on validating the correctness of the Team hierarchy and providing thorough RSpec test coverage for team membership rules, conversions between team types, capacity enforcement, and MentoredTeam mentor logic (duty-based rather than role-based).
Background
The Team Hierarchy
The hierarchy consists of exactly four classes: Team (superclass), CourseTeam, AssignmentTeam, and MentoredTeam. Teams are stored using Rails STI (single table inheritance) via the type column.
Two broad kinds of teams exist:
- Course teams (
CourseTeam) — persist for the duration of an entire course and can be reused across multiple assignments. - Assignment teams (
AssignmentTeam) — formed for a single assignment. When mentor assignment is enabled, teams are instantiated asMentoredTeam, a subclass ofAssignmentTeam.
Class Hierarchy Diagram
┌─────────────────┐
│ Team │
│ (STI parent) │
└────────┬────────┘
│
┌─────────────────┴──────────────────┐
│ │
┌────────┴────────┐ ┌──────────┴──────────┐
│ CourseTeam │ │ AssignmentTeam │
│ (course-scoped) │ │ (assignment-scoped) │
└─────────────────┘ └──────────┬──────────┘
│
┌──────────┴──────────┐
│ MentoredTeam │
│ (mentor via duty) │
└─────────────────────┘
Subclass Responsibilities
| Class | Scope | Key Constraint | Notes |
|---|---|---|---|
Team |
— | STI superclass, shared associations/logic | Holds membership helpers and core add/remove methods |
CourseTeam |
Course | Members must be course participants | Supports conversion to/from assignment teams |
AssignmentTeam |
Assignment | Members must be assignment participants | Capacity comes from Assignment#max_team_size
|
MentoredTeam |
Assignment | Mentor identified by participant duty | Uses Duty + participant duty_id
|
Duty vs. Role Distinction (Key Fix)
A critical design point is the separation between a user's role and their duty within a team.
| Concept | Definition | Examples |
|---|---|---|
| Role | System-level permission level for a user account | Instructor, Teaching Assistant, Student
|
| Duty | Team-level function of a participant on a specific team | Submitter, Reviewer, Mentor
|
In Expertiza, mentors are not special system accounts. A mentor is a normal user whose participant record has been assigned the Mentor duty for a particular team. This project enforces and tests mentor identification by duty (not user role).
Project Goals Implemented
Membership grounded in valid participation
Membership is mediated through Participant records:
- CourseTeam: must be a
CourseParticipantfor the same course (parent_id). - AssignmentTeam / MentoredTeam: must be an
AssignmentParticipantfor the same assignment (parent_id).
This is enforced in Team#add_member by resolving the correct participant type based on the team’s subclass and the team’s parent_id.
# app/models/team.rb
def add_member(participant_or_user)
participant =
if participant_or_user.is_a?(AssignmentParticipant) || participant_or_user.is_a?(CourseParticipant)
participant_or_user
elsif participant_or_user.is_a?(User)
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)
else
nil
end
return { success: false, error: "#{participant_or_user.name} is not a participant in this #{is_a?(AssignmentTeam) ? 'assignment' : 'course'}" } if participant.nil?
return { success: false, error: "Participant already on the team" } if participants.exists?(id: participant.id)
return { success: false, error: "Unable to add participant: team is at full capacity." } if full?
team_participant = TeamsParticipant.create(
participant_id: participant.id,
team_id: id,
user_id: participant.user_id
)
team_participant.persisted? ? { success: true } : { success: false, error: team_participant.errors.full_messages.join(', ') }
end
No user should appear on multiple teams in the same scope
The membership join model includes a uniqueness rule that prevents the same participant from being on multiple teams.
# app/models/teams_participant.rb
validates :participant_id, uniqueness: true
A corresponding database migration adds a unique index on participant_id to make this constraint durable at the DB level.
Team size limits respected (capacity enforcement)
Capacity is enforced in two layers:
1) Domain-level check: Team#add_member rejects when full? is true (returns {success:false, error: ...}).
2) Join-model backstop: direct creation of TeamsParticipant is prevented if the team is already full.
# app/models/teams_participant.rb
validate :team_not_full, on: :create
def team_not_full
return unless team
max = team.max_size
return if max.blank?
if team.participants.count >= max
errors.add(:base, "Team is at full capacity (max #{max}).")
end
end
This prevents bypassing capacity by calling TeamsParticipant.create! directly.
Join request acceptance is race-safe
The join request acceptance flow is guarded using a transaction and a lock, so that capacity is checked at the time of insertion.
# app/controllers/join_team_requests_controller.rb
ActiveRecord::Base.transaction do
team.with_lock do
raise ActiveRecord::RecordInvalid, tp if team.full?
result = team.add_member(participant)
raise ActiveRecord::RecordInvalid, tp unless result[:success]
end
@join_team_request.update!(reply_status: ACCEPTED)
render json: { message: 'Join team request accepted successfully', ... }, status: :ok
end
MentoredTeam (Duty-Based Mentor)
MentoredTeam mentor identification and assignment are based on participant duty:
- A mentor duty is represented by a
Dutyrecord with name'Mentor'. - The mentor is the participant on the team whose
duty_idequals that duty.
# app/models/mentored_team.rb
def assign_mentor(user)
mentor_duty = Duty.find_by(name: 'Mentor')
return false unless mentor_duty
participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id)
return false unless participant
participant.update(duty_id: mentor_duty.id)
end
# app/models/mentored_team.rb (mentor lookup)
AssignmentParticipant
.joins('INNER JOIN teams_participants ON teams_participants.participant_id = participants.id')
.where('teams_participants.team_id = ? AND participants.duty_id = ?', id, mentor_duty.id)
.first&.user
RSpec Test Coverage (Actual Specs in This Project)
Model Specs
| Area | Spec File(s) | What is validated |
|---|---|---|
| Team validations + membership rules | spec/models/team_spec.rb |
Presence/type validation, full?, add_member, eligibility rules
|
| Capacity behavior | spec/models/team_capacity_spec.rb |
Assignment capacity behavior; course teams expected to be uncapped by default |
| Association integrity | spec/models/team_association_spec.rb |
Team-parent linkage and membership scoping |
| Conversion behavior | spec/models/team_conversion_spec.rb |
CourseTeam ⇄ AssignmentTeam conversions and member copying |
| MentoredTeam duty behavior | spec/models/mentored_team_spec.rb |
Mentor duty assignment/removal, mentor lookup by duty, capacity inheritance from AssignmentTeam |
| Join model constraints | spec/models/teams_participant_spec.rb |
Uniqueness/presence validations; enrollment rules via Team#add_member
|
Request Specs (API/Controllers)
| Endpoint Area | Spec File(s) | What is validated |
|---|---|---|
| Teams API | spec/requests/api/v1/teams_controller_spec.rb |
Index/show/members/add/remove behavior (HTTP codes + JSON shape) |
| JoinTeamRequests API | spec/requests/api/v1/join_team_requests_controller_spec.rb |
Authorization rules, creating requests, accepting/declining, “team full” responses |
| TeamsParticipants API | spec/requests/api/v1/teams_participants_controller_spec.rb |
Update duty authorization, list/add/delete participant endpoints |
Notes / Known Gaps
- CourseTeam capacity is not configured in the current schema (courses do not have
max_team_size), so capacity enforcement is defined for assignment-based teams viaAssignment#max_team_size. - TeamsController role-based restrictions require an explicit policy in
TeamsController#action_allowed?to satisfy “student blocked / instructor unrestricted” criteria; the Authorization concern enforces403only when controllers define action rules.
Summary
This project strengthens the teams hierarchy by enforcing enrollment-based membership, preventing duplicate participation across teams, enforcing assignment team capacity with a join-model backstop, and implementing/testing MentoredTeam mentor behavior using participant duty rather than user role. Comprehensive RSpec coverage spans models, conversions, and API request behaviors.
References
- Expertiza project specification — Teams hierarchy testing
- Rails Guides — Active Record Associations
- RSpec Documentation