CSC/ECE 517 Fall 2024 - E2473. Reimplement sign up topic.rb as project topic.rb

From Expertiza_Wiki
Jump to navigation Jump to search

Expertiza

Expertiza is a Ruby on Rails based open source project. Instructors have the ability to add new projects, assignments, etc., as well as edit existing ones. Later on, they can view student submissions and grade them. Students can also use Expertiza to organize into teams to work on different projects and assignments and submit their work. They can also review other students' submissions.

Introduction

This project is a reimplementation of sign up topic as project topic in the Expertiza backend. Expertiza backend is being reimplemented to leverage recent updates in Ruby on Rails ecosystem. This project is part of those efforts.

Problem Statement

The following tasks were required for the reimplementation :

  1. Redesigning SignUpTopic as ProjectTopic: This involves reimplementing SignUpTopic class from the original expertiza repository as ProjectTopic within reimplementation-backend repository. This reimplementation should follow Rails best practices, use Ruby naming conventions, and adhere to the DRY principle to improve code maintainability and readability.
  2. Implementing Team Signup and Topic Drop Functionality: Adding methods to sign a team up for a topic and handling cases when a team drops a topic.
  3. Assignment Team Retrieval: Implementing a method to retrieve AssignmentTeams that are currently signed up for a specific topic, allowing for efficient tracking and management of team assignments.
  4. Slot Availability Management: Including methods for managing and retrieving the current number of available slots for a topic, updating as teams sign up or drop topics to reflect accurate availability.
  5. Writing tests for project topic model: RSpec tests have to be written for ProjectTopic Model.
  6. Writing Swagger UI tests for API endpoints: Swagger UI tests have to be written to test ProjectTopic and SignedUpTeam functionalities.

Design Goal

While reimplementing the SignUpTopic as ProjectTopic, the following design rules must be ensured:

  • Maintain Functional Integrity: Validate the functionality of existing and new methods, incorporating any necessary improvements to meet updated requirements for managing topic signups, slot availability, and team assignments.
  • Promote Loose Coupling and Tight Cohesion: Ensure that ProjectTopic and related classes, such as SignedUpTeam, are designed for optimal organization and maintainability, with clear separation of responsibilities to improve code quality and reduce dependencies.
  • Apply the DRY Principle: Refactor any redundant code in the previous implementation, adhering to DRY principles by eliminating duplicated functionality from the Expertiza system and implementing reusable methods where possible.
  • Ensure Robust Testing and Documentation: Validate the accuracy of existing test cases, updating them to reflect changes in ProjectTopic. Add comprehensive RSpec tests for new functionality, and perform Swagger UI testing on API endpoints to ensure complete coverage and reliability.

Implementation

Below are the methods implemented for the `project_topic` and `signed_up_team` classes. These methods handle slot availability, team sign-ups, waitlist management, topic reassignment, and data consistency. They ensure efficient topic allocation and seamless team management.

app/models/project_topic.rb

The `ProjectTopic` class is a pivotal component for managing team-based project topics, ensuring fairness, data consistency, and efficient slot allocation. It handles real-time slot availability, allowing teams to sign up for topics directly if slots are open while maintaining a fair waitlist for overflow through chronological assignment. The class dynamically reassigns slots when teams drop out, ensuring optimal utilization of resources. Methods like `current_available_slots` provide accurate slot tracking, while administrative functionality such as `signed_up_teams_for_topic` offers comprehensive reporting of both active and waitlisted teams. By managing these operations seamlessly, the class upholds fairness and integrity across team-topic assignments. The following methods were reimplemented in this model:

slot_available?:

This method checks if there are any available slots for a team to sign up for the topic. This function is crucial for managing team assignments effectively without exceeding the maximum allowed capacity.

  # Checks if there are any slots currently available
  # Returns true if the number of available slots is greater than 0, otherwise false
  def slot_available?
    current_available_slots > 0
  end

---

sign_up_team(team_id):

This method handles the team sign-up process for the current topic. It first checks if the team is already signed up for the topic and ensures it is not waitlisted. If slots are available, the team is assigned to the topic directly. Otherwise, the team is added to the waitlist. This method is central to managing both immediate assignments and waitlisted entries.

  # Signs up a team for the current topic.
  # Checks if the team is already signed up, and if so, ensures they are not waitlisted.
  # If a slot is available, assigns the topic to the team; otherwise, adds the team to the waitlist.
  def signup_team(team_id)
    # Check if the team has already signed up for this topic
    team_signup_record = SignedUpTeam.find_by(sign_up_topic_id: self.id, team_id: team_id, is_waitlisted: false)

    # If the team is already signed up, return false
    if !team_signup_record.nil?
      return false
    end

    # Create a new sign-up entry for the team
    new_signup_record = SignedUpTeam.new(sign_up_topic_id: self.id, team_id: team_id)

     # If there are available slots, assign the topic to the team and remove the team from the waitlist
    if slot_available?
      new_signup_record.update(is_waitlisted: false, sign_up_topic_id: self.id)
      result = SignedUpTeam.drop_off_topic_waitlists(team_id)
    else
      # If no slots are available, add the team to the waitlist
      result = new_signup_record.update(is_waitlisted: true, sign_up_topic_id: self.id)
    end

    result
  end

---

longest_waiting_team:

This method retrieves the team that has been on the waitlist the longest for the current topic. It sorts the waitlisted entries by their creation time and returns the oldest entry. This is useful for assigning slots to teams in a fair and chronological manner as slots become available.

  # Retrieves the team with the earliest waitlisted record for a given topic.
  # The team is determined based on the creation time of the waitlisted record.
  def longest_waiting_team
    SignedUpTeam.where(sign_up_topic_id: self.id, is_waitlisted: true).order(:created_at).first
  end

---

drop_team_from_topic(team_id):

This method removes a team from the current topic. If the removed team was not waitlisted, it reassigns the topic to the next waitlisted team, ensuring efficient utilization of available slots. The method then deletes the team's sign-up record, maintaining data consistency.

  # Removes a team from the current topic.
  # If the team is not waitlisted, the next waitlisted team is reassigned to the topic.
  # The team is then destroyed (removed from the sign-up record).
  def drop_team_from_topic(team_id)
    # Find the sign-up record for the team for this topic
    signed_up_team = SignedUpTeam.find_by(team_id: team_id, sign_up_topic_id: self.id)
    return nil unless signed_up_team

    # If the team is not waitlisted, reassign the topic to the next waitlisted team
    unless signed_up_team.is_waitlisted
      next_waitlisted_team = longest_waiting_team
      next_waitlisted_team&.assign_topic_to_waitlisted_team(self.id)
    end

    # Destroy the sign-up record for the team
    signed_up_team.destroy
  end

---

signed_up_teams_for_topic:

This method retrieves all teams that have signed up for the current topic, including both waitlisted and non-waitlisted teams. It provides an overview of the teams associated with a particular topic, aiding in administrative and reporting tasks.

  # Retrieves all teams that are signed up for a given topic.
  def signed_up_teams_for_topic
    SignedUpTeam.where(sign_up_topic_id: self.id)
  end

---

current_available_slots:

This method calculates the number of available slots for the topic by subtracting the count of non-waitlisted teams from the maximum allowed capacity. It ensures the integrity of slot management by providing real-time availability data, which is critical for the sign-up and waitlisting processes.

  # Calculates the number of available slots for a topic.
  # It checks how many teams have already chosen the topic and subtracts that from the maximum allowed choosers.
  def current_available_slots
    # Find the number of teams who have already chosen the topic and are not waitlisted
    # This would give us the number of teams who have been assigned the topic
    num_teams_registered = SignedUpTeam.where(sign_up_topic_id: self.id, is_waitlisted: false).size

    # Compute the number of available slots and return
    self.max_choosers.to_i - num_teams_registered
  end

app/models/signed_up_team.rb

The `SignedUpTeam` class is essential for managing the lifecycle of team sign-ups and their associations with topics, ensuring efficient and consistent handling of team-topic relationships. This class facilitates creating sign-up records using methods like `signup_team_for_topic`, enabling teams to associate seamlessly with topics by leveraging the `signup_team` method of the `ProjectTopic` class. It also manages waitlist operations, allowing for the removal of waitlisted entries through `drop_off_team_waitlists` and assignment of waitlisted teams to their topics via `assign_topic_to_waitlisted_team`. For robust data management, it provides functionality to clear all topic associations for a team using `delete_team_signup_records`. The class also supports administrative operations, such as retrieving team IDs for specific users via `get_team_id_for_user`, ensuring streamlined integration with other system components. By maintaining data integrity and supporting dynamic updates to team-topic relationships, the `SignedUpTeam` class plays a crucial role in the overall project management system.

drop_off_team_waitlists(team_id):

This class method removes all waitlisted entries for a specific team. It deletes all sign-up records where the team is marked as waitlisted, ensuring the team is no longer on any waitlists.

  # Removes all waitlist entries for a given team.
  # This deletes all sign-up records where the team is waitlisted.
  def self.drop_off_topic_waitlists(team_id)
    SignedUpTeam.where(team_id: team_id, is_waitlisted: true).destroy_all
  end

---

signup_team_for_topic(topic_id, team_id):

This class method creates a sign-up record for a team by associating it with the specified topic. It retrieves the project topic by its ID and uses the `sign_up_team` method to handle the sign-up process.

  # Signs up a team for a specific project topic.
  # Finds the project topic by its ID and associates the given team with the topic by calling `signup_team`.
  def self.signup_team_for_topic(topic_id, team_id)
    # Find the project topic by its ID
    project_topic = ProjectTopic.find(topic_id)

    # Sign up the team for the topic
    project_topic.signup_team(team_id)
  end

---

get_team_id_for_user(user_id, assignment_id):

This class method retrieves the team ID associated with a specific user and assignment. It first identifies the teams the user belongs to and then filters the team linked to the given assignment, returning its ID.

  # Retrieves the team ID for a given user and assignment.
  # This first finds the team(s) the user is associated with and then retrieves the team for the specified assignment.
  # NOTE: This method is called in signed_up_teams_controller
  def self.get_team_id_for_user(user_id, assignment_id)
    # Get the team IDs associated with the given user
    team_ids = TeamsUser.select('team_id').where(user_id: user_id)

    if team_ids.empty?
      return nil
    end
    # Find the team that matches the assignment ID and retrieve its team_id
    team_id = Team.where(id: team_ids, assignment_id: assignment_id).first.id
    # Return the team ID
    team_id
  end

---

delete_team_signup_records(team_id):

This class method deletes all sign-up records for a given team. It removes every entry associated with the specified team, clearing all their topic associations.

  # Deletes all sign-up records for a given team.
  # This removes all sign-up entries associated with the specified team.
  def self.delete_team_signup_records(team_id)
    SignedUpTeam.where(team_id: team_id).destroy_all
  end

---

assign_topic_to_waitlisted_team(topic_id):

This instance method assigns a waitlisted team to a given topic. It first removes the team from its current topic if they are assigned, then updates the team's waitlist status to indicate they are now assigned to the given topic.

  # Reassigns the team to a new topic by removing them from their current topic
  # and marking them as no longer waitlisted for the new topic.
  # NOTE: This method gets called only on a waitlisted team (See project_topic.rb -> drop_team_from_topic)
  def assign_topic_to_waitlisted_team(topic_id)
    # Find the team's current sign-up record where they are not waitlisted
    # As this method gets called only on a waitlisted team, we need to check if the team has been assigned another topic
    assigned_team = SignedUpTeam.find_by(team_id: self.team_id, is_waitlisted: false)

    # If the team is already assigned to a topic, remove them from that topic
    if assigned_team
      project_topic = ProjectTopic.find(assigned_team.sign_up_topic_id)
      project_topic.drop_team_from_topic(team_id: self.team_id)
    end

    # Update the team's waitlist status to false (indicating they are no longer waitlisted)
    self.update(sign_up_topic_id: topic_id, is_waitlisted: false)
  end

Testing

spec/models/project_topic_spec.rb

These set of tests are written for Project Topic class.

1. slot_available?

This RSpec test suite validates the `#slot_available?` method of `project_topic`, ensuring it returns `true` when no teams have chosen the topic and `false` when all slots (`max_choosers`) are filled.

  describe '#slot_available?' do
    context 'when no teams have chosen the topic' do
      it 'returns true' do
        expect(project_topic.slot_available?).to be true
      end
    end

    context 'when no slots are available' do
      before do
        project_topic.max_choosers.times do
          SignedUpTeam.create!(sign_up_topic_id: project_topic.id, is_waitlisted: false, team: Team.create!(assignment_id: assignment.id))
        end
      end

      it 'returns false' do
        expect(project_topic.slot_available?).to be false
      end
    end
  end

2. sign_up_team

This RSpec test suite validates the `#signup_team` method of `project_topic`. It ensures the method assigns the topic to a team and clears team waitlists when slots are available, adds the team to the waitlist when slots are full, and returns `false` if the team is already signed up and not waitlisted.

 describe '#signup_team' do
   let(:new_team) { Team.create!(assignment_id: assignment.id) }
   context 'when slot is available' do
     it 'assigns the topic to the team and drops team waitlists' do
       allow(SignedUpTeam).to receive(:drop_off_topic_waitlists).with(new_team.id).and_return(true)
       expect(project_topic.signup_team(new_team.id)).to be true
       expect(SignedUpTeam).to have_received(:drop_off_topic_waitlists).with(new_team.id)
     end
   end
   context 'when no slots are available' do
     before do
       project_topic.max_choosers.times do
         SignedUpTeam.create!(sign_up_topic_id: project_topic.id, is_waitlisted: false, team: Team.create!(assignment_id: assignment.id))
       end
     end
     it 'adds the team to the waitlist' do
       expect { project_topic.signup_team(new_team.id) }.to change { SignedUpTeam.count }.by(1)
       expect(SignedUpTeam.last.is_waitlisted).to be true
     end
   end
   context 'when the team is already signed up and not waitlisted' do
     before do
       SignedUpTeam.create!(sign_up_topic_id: project_topic.id, team: team, is_waitlisted: false)
     end
     it 'returns false' do
       expect(project_topic.signup_team(team.id)).to be false
     end
   end
 end

3. longest_waiting_team

This RSpec test validates the `#longest_waiting_team` method of `project_topic`, ensuring it correctly returns the team that has been on the waitlist for the longest duration.

 describe '#longest_waiting_team' do
   let!(:waitlisted_team) { SignedUpTeam.create!(sign_up_topic_id: project_topic.id, is_waitlisted: true, created_at: 1.day.ago, team: Team.create!(assignment_id: assignment.id)) }
   it 'returns the team that has been waitlisted the longest' do
     expect(project_topic.longest_waiting_team).to eq(waitlisted_team)
   end
 end

4. drop_team_from_topic

This RSpec test suite verifies the `#drop_team_from_topic` method of `project_topic`. It ensures the method removes a team from the topic and, if applicable, assigns the topic to the next longest-waitlisted team by updating its waitlist status.

  describe '#drop_team_from_topic' do
    let!(:signed_up_team) { SignedUpTeam.create!(sign_up_topic_id: project_topic.id, team: team) }

    it 'removes the team from the topic' do
      expect { project_topic.drop_team_from_topic(team.id) }.to change { SignedUpTeam.count }.by(-1)
    end

    context 'when the team is not waitlisted' do
      let!(:waitlisted_team) { SignedUpTeam.create!(sign_up_topic_id: project_topic.id, is_waitlisted: true, created_at: 1.day.ago, team: Team.create!(assignment_id: assignment.id)) }

      it 'assigns the topic to the next waitlisted team' do
        project_topic.drop_team_from_topic(team.id)
        expect(waitlisted_team.reload.is_waitlisted).to be false
      end
    end
  end

5. current_available_slots

This RSpec test suite validates the `#current_available_slots` method of `project_topic`. It ensures the method returns the total `max_choosers` when no teams have signed up and accurately calculates the remaining slots when some teams have already signed up.

 describe '#current_available_slots' do
   context 'when no teams have signed up' do
     it 'returns max_choosers as available slots' do
       expect(project_topic.current_available_slots).to eq(project_topic.max_choosers)
     end
   end
   context 'when some teams have signed up' do
     before do
       2.times do
         SignedUpTeam.create!(sign_up_topic_id: project_topic.id, is_waitlisted: false, team: Team.create!(assignment_id: assignment.id))
       end
     end
     it 'returns the remaining available slots' do
       expect(project_topic.current_available_slots).to eq(1)
     end
   end
 end

spec/models/signed_up_team_spec.rb

These set of tests are written for Signed Up Team class

1. get_team_id_for_user

This RSpec test suite validates the `.get_team_id_for_user` class method of `SignedUpTeam`. It ensures the method returns the correct team ID for a user associated with a specific assignment and returns `nil` if the user is not associated with any team for that assignment.

   describe '.get_team_id_for_user' do
       let!(:user) { User.create!(name: "Name", password: "password", full_name: "Full Name", email: "email@example.com", mru_directory_path: "/dummy/path", role_id: role.id) }
       let!(:teams_user) { TeamsUser.create!(team_id: team.id, user_id: user.id) }
       it 'returns the correct team ID for the given user and assignment' do
         expect(SignedUpTeam.get_team_id_for_user(user.id, assignment.id)).to eq(team.id)
       end
       it 'returns nil if the user is not associated with any team for the assignment' do
         other_user = User.create!(name: "Name 2", password: "password", full_name: "Full Name", email: "email@example.com", mru_directory_path: "/dummy/path", role_id: role.id)
         expect(SignedUpTeam.get_team_id_for_user(other_user.id, assignment.id)).to be_nil
       end
   end

2. delete_team_signup_records

This RSpec test validates the `.delete_team_signup_records` class method of `SignedUpTeam`. It ensures that the method correctly removes all sign-up records associated with a given team, reducing the count of relevant records by the expected amount.

   describe '.delete_team_signup_records' do
       let!(:signup1) { SignedUpTeam.create!(sign_up_topic_id: project_topic.id, is_waitlisted: false, team_id: team.id) }
       let!(:project_topic_2) { ProjectTopic.create!(topic_name: "Dummy Topic 2", assignment_id: assignment.id, max_choosers: 3) }
       let!(:signup2) { SignedUpTeam.create!(sign_up_topic_id: project_topic_2.id, is_waitlisted: true, team_id: team.id) }
       it 'removes all sign-up records for the given team' do
         expect { SignedUpTeam.delete_team_signup_records(team.id) }
           .to change { SignedUpTeam.where(team_id: team.id).count }.by(-2)
       end
   end

3. assign_topic_to_waitlisted_team

This RSpec test validates the `#assign_topic_to_waitlisted_team` method. It ensures that the method successfully reassigns a waitlisted team to a new topic, marks them as not waitlisted, and triggers the `drop_team_from_topic` method on the previous topic.

   describe '#assign_topic_to_waitlisted_team' do
       let!(:project_topic_2) { ProjectTopic.create!(topic_name: "Dummy Topic 2", assignment_id: assignment.id, max_choosers: 3) }
       let!(:signed_up_team) { SignedUpTeam.create!(sign_up_topic_id: project_topic.id, is_waitlisted: true, team_id: team.id) }
       let!(:signed_up_team_2) { SignedUpTeam.create!(sign_up_topic_id: project_topic_2.id, is_waitlisted: false, team_id: team.id) }
       before do
           allow(ProjectTopic).to receive(:find).with(project_topic_2.id).and_return(project_topic_2)
           allow(project_topic_2).to receive(:drop_team_from_topic)
       end
       it 'reassigns the team to a new topic and marks them as not waitlisted' do
           signed_up_team.assign_topic_to_waitlisted_team(project_topic.id)
           expect(signed_up_team.is_waitlisted).to be_falsey
           expect(project_topic_2).to have_received(:drop_team_from_topic).with(team_id: team.id)
       end
   end

Team

Mentors
  • Dinesh Pasupuleti
  • Ankur Banerji
Members
  • Ajith Kanumuri <skanumu2@ncsu.edu>
  • Suhas Adidela <sadidel@ncsu.edu>
  • Ravi Chandu Bollepalli <rbollep@ncsu.edu>

Pull Request

Video

References