CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing

From Expertiza_Wiki
Revision as of 00:15, 30 March 2026 by Awainga (talk | contribs)
Jump to navigation Jump to search

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 as MentoredTeam, a subclass of AssignmentTeam.

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 CourseParticipant for the same course (parent_id).
  • AssignmentTeam / MentoredTeam: must be an AssignmentParticipant for 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 Duty record with name 'Mentor'.
  • The mentor is the participant on the team whose duty_id equals 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 via Assignment#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 enforces 403 only 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