CSC/ECE 517 Fall 2022 - E2281. Reimplement Waitlists
Topic Overview & Prior Work
Feature Overview
E2240 contains detailed information on the previous team's work with this feature.
A summary of the desired functionality is presented below:
Any instructor or TA can sign students up for a topic. Students are able to sign themselves up for a topic. If the topic is full then the team will be put on a queue or "waitlist" for the topic. If the current team holding that topic drops the topic, then the team at the top of the waitlist will be assigned the topic.
Our Concerns with Current Functionality
- What if a team is currently assigned a different topic and waitlisted for a topic that is now available? Currently assigned a topic, drop from all other waitlists
- How many topics can a student be waitlisted for? As many as needed
- How are students notified that they were assigned a new topic? Currently no functionality exists in expertiza
Comments on why E2240, was not merged
- The code for the waitlist is distributed all over the application, which is something could have been placed into a single controller and called through other files
- Although manual testing looked solid, there were not enough automated test cases to back their functionality.
- Too many class methods.
- The code does not follow ruby style design.
- Lack of descriptive code comments
Files Modified By Previous Team
- signed_up_team.rb
- waitlist.rb
- sign_up_sheet_controller.rb
- sign_up_sheet.rb
- sign_up_topic.rb
- suggestion_controller.rb
- invitation.rb
Current Instructor View
The current instructor view is clean and readable. We see no need to alter the view.
Current Student View
The current student view is clean and readable. We see no need to alter the view.
Planned Work
This Project is a total reimplementation of the Waitlists functionality because the previous implementation failed to be merged for reasons stated above. As such we have a number of things to complete in order for it to be operational. Our plan to accomplish this task is to create a new class for Waitlists that inherits from Ruby's Queue class. Within this class we will have a model, and controller that will house all of the functionality relating to Waitlists. The further implementation explanation will be below including diagrams.
Use Case Diagram
Flow Chart for Adding Team
This is our desired flow for adding a team to a topic
Flow Chart for Removing Team
This is our desired flow for removing a team to a topic
Functions to Implement
Functionalities for Team
- Adding Team to Waitlist
- Removing Team from Waitlist
- Removing Team from all Waitlists
- Checking if Team is on any Waitlists
- List all Waitlists a Team is on
Functionalities for Topic
- Checking if Topic has any teams on Waitlist
- Listing all Teams on Topics Waitlist
- Locate First Team on Waitlist
- Convert WaitListTeam into SignedUpTeam
Our Work
Schema Changes (db/migrate/20221129040129_create_waitlist_teams.rb)
When creating WaitlistTeams, they needed to be stored in the database in a particular way. The WaitlistTeams instance contains three columns:
- team_id: This stores the team_id of the team being waitlisted. This ID comes from their assignment team.
- topic_id: In order to keep track of teams for a certain topic, it must be link to the topic by storing it's topic_id.
- created_at: This is used to keep track of the order in which teams waitlist. While usually you can just look at the order in the database, this gives us an extra level of accuracy that the teams are ordered correctly.
class CreateWaitlistTeams < ActiveRecord::Migration[5.1] def self.up create_table 'waitlist_teams' do |t| t.column 'team_id', :integer t.column 'topic_id', :integer t.column 'created_at', :datetime end add_index 'waitlist_teams', ['team_id'], name: 'fk_waitlist_teams' execute "alter table waitlist_teams add constraint fk_waitlist_teams foreign key (team_id) references teams(id)" add_index 'waitlist_teams', ['topic_id'], name: 'fk_waitlist_teams_sign_up_topics' execute "alter table waitlist_teams add constraint fk_waitlist_teams_sign_up_topics foreign key (topic_id) references sign_up_topics(id)" add_index 'waitlist_teams', ["team_id", "topic_id"], unique: true end def self.down drop_table 'waitlist_teams' end end
Model Methods (app/models/waitlist_team.rb)
add_team_to_topic_waitlist
This method stores the incoming team_id and topic_id into the instance of WaitlistTeam
Then it ensures its valid before storing it in the database, if it is not, then it has an error
def self.add_team_to_topic_waitlist(team_id, topic_id, user_id) new_waitlist = WaitlistTeam.new new_waitlist.topic_id = topic_id new_waitlist.team_id = team_id if new_waitlist.valid? WaitlistTeam.create(topic_id: topic_id, team_id: team_id) else ExpertizaLogger.info LoggerMessage.new('WaitlistTeam', user_id, "Team #{team_id} cannot be added to waitlist for the topic #{topic_id}") ExpertizaLogger.info LoggerMessage.new('WaitlistTeam', user_id, new_waitlist.errors.full_messages.join(" ")) return false end return true end
remove_team_from_topic_waitlist
This method first searches the database for the instance of WaitlistTeam that matches the incoming team_id and topic_id
Then it calls the destroyer to remove the instance from the database
def self.remove_team_from_topic_waitlist(team_id, topic_id, user_id) waitlisted_team_for_topic = WaitlistTeam.find_by(topic_id: topic_id, team_id: team_id) unless waitlisted_team_for_topic.nil? waitlisted_team_for_topic.destroy else ExpertizaLogger.info LoggerMessage.new('WaitlistTeam', user_id, "Cannot find Team #{team_id} in waitlist for the topic #{topic_id} to be deleted.") end return true end
cancel_all_waitlists
When a team is added to a topic or removed from an assignment, this method is called
It first finds all Waitlists associated with that teams team_id for that assignment
Then one by one destroys the entry
def self.cancel_all_waitlists(team_id, assignment_id) waitlisted_topics = SignUpTopic.find_waitlisted_topics(assignment_id, team_id) unless waitlisted_topics.nil? waitlisted_topics.each do |waitlisted_topic| entry = SignedUpTeam.find_by(topic_id: waitlisted_topic.id) next if entry.nil? entry.destroy end end end
Controller Methods (app/controllers/waitlist_team_controller.rb)
first_team_on_waitlist
Determines first team on waitlist based on date/time of entry
def self.first_team_on_waitlist(topic_id) waitlisted_team_for_topic = WaitlistTeam.where(topic_id: topic_id).order("created_at ASC").first waitlisted_team_for_topic end
team_is_on_any_waitlists?
Searches database for waitlist entries for team
def self.team_is_on_any_waitlists?(team_id) WaitlistTeam.where(team_id: team_id).empty? end
topic_has_waitlist?
Searches database for waitlist entries for topic
def self.topic_has_waitlist?(topic_id) WaitlistTeam.where(topic_id: topic_id).empty? end
delete_all_waitlists_for_team
Finds the list of waitlist entries for a team
Then calls the destroyer to remove the entries
def self.delete_all_waitlists_for_team(team_id) waitlisted_topics_for_team = get_all_waitlists_for_team team_id if !waitlisted_topics_for_team.nil? waitlisted_topics_for_team.destroy_all else ExpertizaLogger.info LoggerMessage.new('WaitlistTeam', user_id, "Cannot find Team #{team_id} in waitlist.") end return true end
delete_all_waitlists_for_topic
Finds the list of waitlist entries for a topic
Then calls the destroyer to remove the entries
def self.delete_all_waitlists_for_topic(topic_id) waitlisted_teams_for_topic = get_all_waitlists_for_topic topic_id if !waitlisted_teams_for_topic.nil? waitlisted_teams_for_topic.destroy_all else ExpertizaLogger.info LoggerMessage.new('WaitlistTeam', user_id, "Cannot find Topic #{topic_id} in waitlist.") end return true end
get_all_waitlists_for_team
Returns list of all waitlist topics for a team
Order is dependent on the created_at attribute
def self.get_all_waitlists_for_team(team_id) WaitlistTeam.joins(:topic).select('waitlist_teams.id, topic_name, team_id, topic_id, created_at').where(team_id: team_id) end
get_all_waitlists_for_topic
Returns list of all waitlisted teams for a topic
def self.get_all_waitlists_for_topic(topic_id) WaitlistTeam.where(topic_id: topic_id) end
count_all_waitlists_pre_topic_per_assignment
Searches through all topics on an assignment to determine waitlist sizes
def self.count_all_waitlists_per_topic_per_assignment(assignment_id) list_of_topic_waitlist_counts = [] assignment_topics = Assignment.find(assignment_id).sign_up_topics assignment_topics.each do |topic| list_of_topic_waitlist_counts.append({ topic_id: topic.id, count: topic.waitlist_teams.size }) end list_of_topic_waitlist_counts end
find_waitlisted_teams_for_assignment
Searches database for all waitlisted teams for specific assignment
def self.find_waitlisted_teams_for_assignment(assignment_id, ip_address = nil) waitlisted_participants = WaitlistTeam.joins('INNER JOIN sign_up_topics ON waitlist_teams.topic_id = sign_up_topics.id') .select('waitlist_teams.id as id, sign_up_topics.id as topic_id, sign_up_topics.topic_name as name, sign_up_topics.topic_name as team_name_placeholder, sign_up_topics.topic_name as user_name_placeholder, waitlist_teams.team_id as team_id') .where('sign_up_topics.assignment_id = ?', assignment_id) SignedUpTeam.fill_participant_names waitlisted_participants, ip_address waitlisted_participants end
check_team_waitlisted_for_topic Simple database check to find if waitlisted team exists
def self.check_team_waitlisted_for_topic(team_id, topic_id) WaitlistTeam.exists?(team_id: team_id, topic_id: topic_id) end
sign_up_first_waitlisted_team
Find team at top of queue and converts to SignedUpTeam
def self.sign_up_first_waitlisted_team(topic_id) sign_up_waitlist_team = nil ApplicationRecord.transaction do first_waitlist_team = first_team_on_waitlist(topic_id) unless first_waitlist_team.blank? sign_up_waitlist_team = SignedUpTeam.new sign_up_waitlist_team.topic_id = first_waitlist_team.topic_id sign_up_waitlist_team.team_id = first_waitlist_team.team_id if sign_up_waitlist_team.valid? sign_up_waitlist_team.is_waitlisted = false sign_up_waitlist_team.save first_waitlist_team.destroy delete_all_waitlists_for_team sign_up_waitlist_team.team_id else ExpertizaLogger.info LoggerMessage.new('WaitlistTeam', session[:user].id, "Cannot find Topic #{topic_id} in waitlist.") raise ActiveRecord::Rollback end end end sign_up_waitlist_team end
Design Strategy
Ruby Style Guide Basics
Previous teams were not merged due to failure of following the Ruby Style Guide, so our intent is to ensure we follow all Ruby Style Guidelines. Below we have highlighted which we believe are the most important.
- Indentation: Each indentation level should be marked using two spaces.
- Line Length: Each line should contain a maximum of 80 characters.
- End of Line: Each line should have no trailing whitespace and should use UNIX Style line endings (CRLF).
- Expression: Each line should only contain one expression.
DRY Principle
The principle simply means "Don't Repeat Yourself". Which aims to reduce repetition and redundancy in favor of abstraction.
Current/Past Implementation of the WaitLists has failed to follow this principle by having similar versions of the same code within all classes that need the waitlist functionality. Our Intent is to take the functionalities of WaitLists and contain them in a centralized WaitLists class. From there all classes that need a waitlist can just call from the WaitLists controller.
Testing
Planned Test Cases
- Waitlist
Scenario: Student leaves a waitlisted team Given: Logged in as an Student When: Student leaves a team Then: Student should be removed from all waitlisted topics associated with that team Scenario: Instructor/TA Removes Student from Class Given: Logged in as an Instructor/Admin and Student is on a team by themselves When: Student is dropped from class Then: Team should be removed from all waitlists Scenario: Student accepts new team invitation Given: Logged in as an Student When: Student accepted invitation to join team on a waitlist Then: Student should join team on waitlist Scenario: Instructor/TA Removes a topic Given: Logged in as an Student When: Instructor/TA removes a topic containing a waitlist Then: The waitlist should be deleted Scenario: Team is Assigned a Topic Given: Team is enrolled in multiple waitlists When: Team is assigned topic Then: Team should be dropped from all other waitlists within that project Scenario: Instructor Views Waitlist for a Topic Given: A Topic has a waitlist filled with teams When: An Instructor attempted to view the waitlist Then: A List of the Waitlisted teams in order on queue should be returned Scenario: Instructor Views Topics Waitlist by a Team Given: A Team is Waitlisted for multiple topics for a project When: An Instructor attempted to view which topics Then: A List of the Waitlisted topics should be returned
RSPEC Test Cases
Model Tests
rspec spec/models/waitlist_team_spec.rb
describe 'CRUD WaitlistTeam' do before(:each) do allow(waitlist_team1).to receive(:topic_id).and_return(1) allow(waitlist_team1).to receive(:team_id).and_return(1) allow(waitlist_team2).to receive(:topic_id).and_return(2) allow(waitlist_team2).to receive(:team_id).and_return(2) end it 'adds and removes a team from the topic waitlist' do expect(WaitlistTeam.add_team_to_topic_waitlist(waitlist_team1.team_id, waitlist_team1.topic_id,1)).to be_truthy expect(WaitlistTeam.remove_team_from_topic_waitlist(waitlist_team1.team_id, waitlist_team1.topic_id,1)).to be_truthy end it 'cannot remove a team that does not exist from the topic waitlist' do WaitlistTeam.add_team_to_topic_waitlist(waitlist_team1.team_id, waitlist_team1.topic_id,1) expect(WaitlistTeam.remove_team_from_topic_waitlist(waitlist_team1.team_id, waitlist_team1.topic_id,1)).to be_truthy expect(WaitlistTeam.remove_team_from_topic_waitlist(waitlist_team1.team_id, waitlist_team2.topic_id,1)).to be_truthy end end
Controller Tests
rspec spec/controllers/waitlist_controller_spec.rb
describe 'CRUD WaitlistController' do before(:each) do allow(waitlist_team1).to receive(:topic_id).and_return(1) allow(waitlist_team1).to receive(:team_id).and_return(1) allow(waitlist_team2).to receive(:topic_id).and_return(2) allow(waitlist_team2).to receive(:team_id).and_return(2) end it 'adds first waitlist team in the topic waitlist' do WaitlistTeam.add_team_to_topic_waitlist(waitlist_team1.team_id, waitlist_team1.topic_id,1) signed_up_team = WaitlistController.sign_up_first_waitlisted_team(1) expect(signed_up_team).to be_instance_of(SignedUpTeam) or expect(signed_up_team).to be_nil expect(WaitlistController.team_has_any_waitlists?(waitlist_team1.team_id)).to be_truthy end it 'check if team does not has any waitlists' do WaitlistTeam.add_team_to_topic_waitlist(waitlist_team1.team_id, waitlist_team1.topic_id,1) expect(WaitlistController.team_is_on_any_waitlists?(waitlist_team1.team_id)).to be_falsey end it 'check if topic does not has any waitlists' do WaitlistTeam.add_team_to_topic_waitlist(waitlist_team1.team_id, waitlist_team1.topic_id,1) expect(WaitlistController.topic_has_waitlist?(waitlist_team1.topic_id)).to be_falsey end it 'check and delete all waitlists for a team' do WaitlistTeam.add_team_to_topic_waitlist(waitlist_team1.team_id, waitlist_team1.topic_id,1) expect(WaitlistController.delete_all_waitlists_for_team(waitlist_team1.team_id)).to be_truthy end it 'check and delete all waitlists for a topic' do WaitlistTeam.add_team_to_topic_waitlist(waitlist_team1.team_id, waitlist_team1.topic_id,1) expect(WaitlistController.delete_all_waitlists_for_topic(waitlist_team1.topic_id)).to be_truthy end it 'verify team waitlisted for topic' do WaitlistTeam.add_team_to_topic_waitlist(waitlist_team1.team_id, waitlist_team1.topic_id,1) expect(WaitlistController.check_team_waitlisted_for_topic(waitlist_team1.team_id, waitlist_team1.topic_id)).to be_truthy end end
Manual UI Testing
Testing as Student
Login: student11, student14, student15 Password: password
- Login as Student
- Click on Assignments
- Find an Assignment with a Signup Sheet (ex. Ensure Waitlists)
- Click Signup Sheet
- Click the Green Check Mark under the Actions Column to add a topic (for testing choose an available topic)
- Ensure when Student is added to Topic that the Available Slots decrements
- To ensure the Waitlist works, login as another student (Follow steps 1-5). For step 5 choose the same topic that you chose with the other student
- Ensure this student should now be added to the waitlist
- Now log back in as the original student and navigate back to the signup sheet
- Now drop your topic. This should cause the available slots to remain the same and only the waitlist to decrement, because the student on the waitlist should've been added to the topic
- Now log back in a the second student an ensure you are assigned the topic
Testing as Instructor
Login: instructor6 Password: password
- Login as Instructor
- Click on Assignments
- Find an Assignment with a Signup Sheet (ex. Final project (and design doc))
- Click Signup Sheet
- Begin by adding a Student (we will call them Student A) to an open topic
- Then add another Student (we will call them Student B) to the same topic. Student B should've been added to the waitlist
- Now remove Student A from the topic. Student B should now be assigned the topic and the waitlist should've decremented
Test Case 1
Scenario: Student leaves a waitlisted team Given: Logged in as an Student When: Student leaves a team Then: Student should be removed from all waitlisted topics associated with that team
- Login as Student
- Clicked on Ensure WaitList
- Showed Student Signed up for Topic1 and Topic2
- Went to Team View
- Left Team
- Went Back to Signup Sheet
Shows Student is no longer on any waitlists
Test Case 2
Scenario: Instructor/TA Removes a topic Given: Logged in as an Student When: Instructor/TA removes a topic containing a waitlist Then: The waitlist should be deleted
- As instructor notice that student10 is waitlisted for Topic 1 and Topic 3
- When Instructor Deletes Topic 3
- Login as student10
- Notice student10 is only waitlisted for Topic 1
Test Case 3
Scenario: Team is Assigned a Topic Given: Team is enrolled in multiple waitlists When: Team is assigned topic Then: Team should be dropped from all other waitlists within that project
- Logged in as student10, notice that you are waitlisted for Topic 1, Topic 2 and Topic 3
- Click the check next to Topic 4
- This will cancel waitlists for Topic 1, Topic 2 and Topic 3 and assign Topic 4
Conclusion
Our goal was to create a simple waitlist functionality that would allow teams to signup for topics that are currently full and remain waitlisted until the topic is made available or they signup for a different topic. We accomplished this using by creating a WaitlistTeam class that would take a team's team_id and the topic_id for the topic they wanted and would store them on the database. Then if a topic became available, if a waitlist existed it would assign the first team on the waitlist to the topic, then cancel all outstanding waitlists that the team_id had for that particular assignment. Our code was tested meticulously in the UI to confirm that no edge cases broke the system, and even automated a few tests using the RSPEC functionality.
Useful Links
Our Github Forked Repo
Our Pull Request
Contributors
Students
- Nick Aydt (naydt)
- Nagaraj Madamshetti (nmadams)
- Rohan Shiveshwarkar (rsshives)
- Gun Ju Im (gim)
Mentor
- Naman Shrimali (nshrima)