From 39df8eb371cc2168c39f1d29f823d29b032098ea Mon Sep 17 00:00:00 2001 From: smiti Date: Wed, 19 Mar 2025 07:31:00 +0530 Subject: [PATCH 01/13] Added migration file and renamed sign_up_topic to project_topic --- app/models/assignment.rb | 4 +- .../{sign_up_topic.rb => project_topic.rb} | 2 +- config/application.rb | 1 + ...rename_sign_up_topics_to_project_topics.rb | 4 ++ db/schema.rb | 38 +++++++++---------- 5 files changed, 27 insertions(+), 22 deletions(-) rename app/models/{sign_up_topic.rb => project_topic.rb} (91%) create mode 100644 db/migrate/20250319015434_rename_sign_up_topics_to_project_topics.rb diff --git a/app/models/assignment.rb b/app/models/assignment.rb index e4e3b44fe..ac804a70d 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -8,7 +8,7 @@ class Assignment < ApplicationRecord has_many :questionnaires, through: :assignment_questionnaires has_many :response_maps, foreign_key: 'reviewed_object_id', dependent: :destroy, inverse_of: :assignment has_many :review_mappings, class_name: 'ReviewResponseMap', foreign_key: 'reviewed_object_id', dependent: :destroy, inverse_of: :assignment - has_many :sign_up_topics , class_name: 'SignUpTopic', foreign_key: 'assignment_id', dependent: :destroy + has_many :sign_up_topics , class_name: 'ProjectTopic', foreign_key: 'assignment_id', dependent: :destroy has_many :due_dates, class_name: 'DueDate', foreign_key: 'due_date_id', dependent: :destroy, as: :parent belongs_to :course, optional: true belongs_to :instructor, class_name: 'User', inverse_of: :assignments @@ -138,7 +138,7 @@ def staggered_and_no_topic?(topic_id) #This method return the value of the has_topics field for the given assignment object. # has_topics is of boolean type and is set true if there is any topic associated with the assignment. def topics? - @has_topics ||= sign_up_topics.any? + @has_topics ||= project_topics.any? end #This method return if the given assignment is a team assignment. diff --git a/app/models/sign_up_topic.rb b/app/models/project_topic.rb similarity index 91% rename from app/models/sign_up_topic.rb rename to app/models/project_topic.rb index 641fdc9b1..36ae25b2f 100644 --- a/app/models/sign_up_topic.rb +++ b/app/models/project_topic.rb @@ -1,4 +1,4 @@ -class SignUpTopic < ApplicationRecord +class ProjectTopic < ApplicationRecord has_many :signed_up_teams, foreign_key: 'topic_id', dependent: :destroy has_many :teams, through: :signed_up_teams # list all teams choose this topic, no matter in waitlist or not has_many :assignment_questionnaires, class_name: 'AssignmentQuestionnaire', foreign_key: 'topic_id', dependent: :destroy diff --git a/config/application.rb b/config/application.rb index 4bd4ca23e..c66af389b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -16,6 +16,7 @@ module Reimplementation class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.0 + config.active_record.schema_format = :ruby # Configuration for the application, engines, and railties goes here. # diff --git a/db/migrate/20250319015434_rename_sign_up_topics_to_project_topics.rb b/db/migrate/20250319015434_rename_sign_up_topics_to_project_topics.rb new file mode 100644 index 000000000..018520189 --- /dev/null +++ b/db/migrate/20250319015434_rename_sign_up_topics_to_project_topics.rb @@ -0,0 +1,4 @@ +class RenameSignUpTopicsToProjectTopics < ActiveRecord::Migration[8.0] + def change + end +end diff --git a/db/schema.rb b/db/schema.rb index 7db16863e..6e611febb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_02_16_020117) do +ActiveRecord::Schema[8.0].define(version: 2025_03_19_015434) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -238,6 +238,22 @@ t.index ["user_id"], name: "index_participants_on_user_id" end + create_table "project_topics", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.text "topic_name", null: false + t.bigint "assignment_id", null: false + t.integer "max_choosers", default: 0, null: false + t.text "category" + t.string "topic_identifier", limit: 10 + t.integer "micropayment", default: 0 + t.integer "private_to" + t.text "description" + t.string "link" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["assignment_id"], name: "fk_sign_up_categories_sign_up_topics" + t.index ["assignment_id"], name: "index_project_topics_on_assignment_id" + end + create_table "question_advices", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "question_id", null: false t.integer "score" @@ -307,22 +323,6 @@ t.index ["parent_id"], name: "fk_rails_4404228d2f" end - create_table "sign_up_topics", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| - t.text "topic_name", null: false - t.bigint "assignment_id", null: false - t.integer "max_choosers", default: 0, null: false - t.text "category" - t.string "topic_identifier", limit: 10 - t.integer "micropayment", default: 0 - t.integer "private_to" - t.text "description" - t.string "link" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["assignment_id"], name: "fk_sign_up_categories_sign_up_topics" - t.index ["assignment_id"], name: "index_sign_up_topics_on_assignment_id" - end - create_table "signed_up_teams", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "sign_up_topic_id", null: false t.bigint "team_id", null: false @@ -398,10 +398,10 @@ add_foreign_key "participants", "join_team_requests" add_foreign_key "participants", "teams" add_foreign_key "participants", "users" + add_foreign_key "project_topics", "assignments" add_foreign_key "question_advices", "items", column: "question_id" add_foreign_key "roles", "roles", column: "parent_id", on_delete: :cascade - add_foreign_key "sign_up_topics", "assignments" - add_foreign_key "signed_up_teams", "sign_up_topics" + add_foreign_key "signed_up_teams", "project_topics", column: "sign_up_topic_id" add_foreign_key "signed_up_teams", "teams" add_foreign_key "ta_mappings", "courses" add_foreign_key "ta_mappings", "users" From c242e1698c462eb7436329245ead4d86fb68cfde Mon Sep 17 00:00:00 2001 From: smiti Date: Thu, 20 Mar 2025 05:05:37 +0530 Subject: [PATCH 02/13] refactored the project_topic model. --- app/models/project_topic.rb | 84 +++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/app/models/project_topic.rb b/app/models/project_topic.rb index 36ae25b2f..5ad9a5f92 100644 --- a/app/models/project_topic.rb +++ b/app/models/project_topic.rb @@ -1,7 +1,83 @@ class ProjectTopic < ApplicationRecord - has_many :signed_up_teams, foreign_key: 'topic_id', dependent: :destroy - has_many :teams, through: :signed_up_teams # list all teams choose this topic, no matter in waitlist or not - has_many :assignment_questionnaires, class_name: 'AssignmentQuestionnaire', foreign_key: 'topic_id', dependent: :destroy - has_many :due_dates, class_name: 'DueDate', foreign_key: 'due_date_id', dependent: :destroy, as: :parent + # Associations + has_many :signed_up_teams, dependent: :destroy + has_many :teams, through: :signed_up_teams belongs_to :assignment + + # Validations + validates :max_choosers, numericality: { + only_integer: true, + greater_than_or_equal_to: 0 + } + validates :topic_name, presence: true + + # Sign up a team to the topic with waitlist management + def signup_team(team) + return false if signed_up_teams.exists?(team: team) + + ActiveRecord::Base.transaction do + signed_up_team = signed_up_teams.create!( + team: team, + is_waitlisted: !slot_available? + ) + + # Remove from other waitlists if successfully registered + remove_from_other_waitlists(team) unless signed_up_team.is_waitlisted? + true + end + rescue ActiveRecord::RecordInvalid + false + end + + # Remove team from topic and handle waitlist promotion + def drop_team(team) + signed_up_team = signed_up_teams.find_by(team: team) + return unless signed_up_team + + was_confirmed = !signed_up_team.is_waitlisted? + signed_up_team.destroy! + + promote_waitlisted_team if was_confirmed + end + + # Get current number of available slots + def available_slots + max_choosers - confirmed_teams_count + end + + # Check if slot is available + def slot_available? + available_slots.positive? + end + + # Get confirmed teams + def confirmed_teams + teams.joins(:signed_up_teams) + .where(signed_up_teams: { is_waitlisted: false }) + end + + # Get waitlisted teams in order + def waitlisted_teams + teams.joins(:signed_up_teams) + .where(signed_up_teams: { is_waitlisted: true }) + .order('signed_up_teams.created_at ASC') + end + + private + + def confirmed_teams_count + signed_up_teams.where(is_waitlisted: false).count + end + + def promote_waitlisted_team + next_team = waitlisted_teams.first + return unless next_team + + signed_up_teams.find_by(team: next_team)&.update!(is_waitlisted: false) + remove_from_other_waitlists(next_team) + end + + def remove_from_other_waitlists(team) + team.signed_up_teams.waitlisted.destroy_all + end end From 6c0dbecb3ee0a74c2bfe6b52f3e489c6fa281e43 Mon Sep 17 00:00:00 2001 From: dmpatel3 Date: Thu, 20 Mar 2025 18:17:26 -0400 Subject: [PATCH 03/13] Refactored remaining SignUpTopic-related instances across the project to ProjectTopic-related instances --- Gemfile.lock | 1 + .../api/v1/assignments_controller.rb | 2 +- .../api/v1/project_topics_controller.rb | 84 ++++++++++++ .../api/v1/sign_up_topics_controller.rb | 84 ------------ .../api/v1/signed_up_teams_controller.rb | 4 +- app/models/assignment.rb | 2 +- app/models/bookmark.rb | 2 +- app/models/signed_up_team.rb | 2 +- config/routes.rb | 2 +- spec/models/due_date_spec.rb | 8 +- ...ec.rb => project_topic_controller_spec.rb} | 122 +++++++++--------- ...spec.rb => project_topics_routing_spec.rb} | 2 +- swagger/v1/swagger.yaml | 104 +++++++-------- 13 files changed, 210 insertions(+), 209 deletions(-) create mode 100644 app/controllers/api/v1/project_topics_controller.rb delete mode 100644 app/controllers/api/v1/sign_up_topics_controller.rb rename spec/requests/api/v1/{sign_up_topic_controller_spec.rb => project_topic_controller_spec.rb} (68%) rename spec/routing/{sign_up_topics_routing_spec.rb => project_topics_routing_spec.rb} (92%) diff --git a/Gemfile.lock b/Gemfile.lock index bd42d1751..1fee32183 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -308,6 +308,7 @@ PLATFORMS aarch64-linux arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x64-mingw-ucrt x86_64-linux diff --git a/app/controllers/api/v1/assignments_controller.rb b/app/controllers/api/v1/assignments_controller.rb index fdaf9bc75..410c8e4e1 100644 --- a/app/controllers/api/v1/assignments_controller.rb +++ b/app/controllers/api/v1/assignments_controller.rb @@ -151,7 +151,7 @@ def show_assignment_details end # check if assignment has topics - # has_topics is set to true if there is SignUpTopic corresponding to the input assignment id + # has_topics is set to true if there is ProjectTopic corresponding to the input assignment id def has_topics assignment = Assignment.find_by(id: params[:assignment_id]) if assignment.nil? diff --git a/app/controllers/api/v1/project_topics_controller.rb b/app/controllers/api/v1/project_topics_controller.rb new file mode 100644 index 000000000..24e1ef86d --- /dev/null +++ b/app/controllers/api/v1/project_topics_controller.rb @@ -0,0 +1,84 @@ +class Api::V1::ProjectTopicsController < ApplicationController + before_action :set_project_topic, only: %i[ show update ] + + # GET /api/v1/project_topics?assignment_id=&topic_ids[]= + # Retrieve ProjectTopics by two query parameters - assignment_id (compulsory) and an array of topic_ids (optional) + def index + if params[:assignment_id].nil? + render json: { message: 'Assignment ID is required!' }, status: :unprocessable_entity + elsif params[:topic_ids].nil? + @project_topics = ProjectTopic.where(assignment_id: params[:assignment_id]) + render json: @project_topics, status: :ok + else + @project_topics = ProjectTopic.where(assignment_id: params[:assignment_id], topic_identifier: params[:topic_ids]) + render json: @project_topics, status: :ok + end + # render json: {message: 'All selected topics have been loaded successfully.', project_topics: @stopics}, status: 200 + end + + # POST /project_topics + # The create method allows the instructor to create a new topic + # params[:project_topic][:topic_identifier] follows a json format + # The method takes inputs and outputs the if the topic creation was successful. + def create + @project_topic = ProjectTopic.new(project_topic_params) + @assignment = Assignment.find(params[:project_topic][:assignment_id]) + @project_topic.micropayment = params[:micropayment] if @assignment.microtask? + if @project_topic.save + # undo_link "The topic: \"#{@project_topic.topic_name}\" has been created successfully. " + render json: { message: "The topic: \"#{@project_topic.topic_name}\" has been created successfully. " }, status: :created + else + render json: { message: @project_topic.errors }, status: :unprocessable_entity + end + end + + # PATCH/PUT /project_topics/1 + # updates parameters present in project_topic_params. + def update + if @project_topic.update(project_topic_params) + render json: { message: "The topic: \"#{@project_topic.topic_name}\" has been updated successfully. " }, status: 200 + else + render json: @project_topic.errors, status: :unprocessable_entity + end + end + + # Show a ProjectTopic by ID + def show + render json: @project_topic, status: :ok + end + + # Similar to index method, this method destroys ProjectTopics by two query parameters + # assignment_id is compulsory. + # topic_ids[] is optional + def destroy + # render json: {message: @sign_up_topic} + # filters topics based on assignment id (required) and topic identifiers (optional) + if params[:assignment_id].nil? + render json: { message: 'Assignment ID is required!' }, status: :unprocessable_entity + elsif params[:topic_ids].nil? + @project_topics = ProjectTopic.where(assignment_id: params[:assignment_id]) + # render json: @project_topics, status: :ok + else + @project_topics = ProjectTopic.where(assignment_id: params[:assignment_id], topic_identifier: params[:topic_ids]) + # render json: @project_topics, status: :ok + end + + if @project_topics.each(&:delete) + render json: { message: "The topic has been deleted successfully. " }, status: :no_content + else + render json: @project_topic.errors, status: :unprocessable_entity + end + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_project_topic + @project_topic = ProjectTopic.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def project_topic_params + params.require(:project_topic).permit(:topic_identifier, :category, :topic_name, :max_choosers, :assignment_id) + end +end diff --git a/app/controllers/api/v1/sign_up_topics_controller.rb b/app/controllers/api/v1/sign_up_topics_controller.rb deleted file mode 100644 index a736f3eaa..000000000 --- a/app/controllers/api/v1/sign_up_topics_controller.rb +++ /dev/null @@ -1,84 +0,0 @@ -class Api::V1::SignUpTopicsController < ApplicationController - before_action :set_sign_up_topic, only: %i[ show update ] - - # GET /api/v1/sign_up_topics?assignment_id=&topic_ids[]= - # Retrieve SignUpTopics by two query parameters - assignment_id (compulsory) and an array of topic_ids (optional) - def index - if params[:assignment_id].nil? - render json: { message: 'Assignment ID is required!' }, status: :unprocessable_entity - elsif params[:topic_ids].nil? - @sign_up_topics = SignUpTopic.where(assignment_id: params[:assignment_id]) - render json: @sign_up_topics, status: :ok - else - @sign_up_topics = SignUpTopic.where(assignment_id: params[:assignment_id], topic_identifier: params[:topic_ids]) - render json: @sign_up_topics, status: :ok - end - # render json: {message: 'All selected topics have been loaded successfully.', sign_up_topics: @stopics}, status: 200 - end - - # POST /sign_up_topics - # The create method allows the instructor to create a new topic - # params[:sign_up_topic][:topic_identifier] follows a json format - # The method takes inputs and outputs the if the topic creation was successful. - def create - @sign_up_topic = SignUpTopic.new(sign_up_topic_params) - @assignment = Assignment.find(params[:sign_up_topic][:assignment_id]) - @sign_up_topic.micropayment = params[:micropayment] if @assignment.microtask? - if @sign_up_topic.save - # undo_link "The topic: \"#{@sign_up_topic.topic_name}\" has been created successfully. " - render json: { message: "The topic: \"#{@sign_up_topic.topic_name}\" has been created successfully. " }, status: :created - else - render json: { message: @sign_up_topic.errors }, status: :unprocessable_entity - end - end - - # PATCH/PUT /sign_up_topics/1 - # updates parameters present in sign_up_topic_params. - def update - if @sign_up_topic.update(sign_up_topic_params) - render json: { message: "The topic: \"#{@sign_up_topic.topic_name}\" has been updated successfully. " }, status: 200 - else - render json: @sign_up_topic.errors, status: :unprocessable_entity - end - end - - # Show a SignUpTopic by ID - def show - render json: @sign_up_topic, status: :ok - end - - # Similar to index method, this method destroys SignUpTopics by two query parameters - # assignment_id is compulsory. - # topic_ids[] is optional - def destroy - # render json: {message: @sign_up_topic} - # filters topics based on assignment id (required) and topic identifiers (optional) - if params[:assignment_id].nil? - render json: { message: 'Assignment ID is required!' }, status: :unprocessable_entity - elsif params[:topic_ids].nil? - @sign_up_topics = SignUpTopic.where(assignment_id: params[:assignment_id]) - # render json: @sign_up_topics, status: :ok - else - @sign_up_topics = SignUpTopic.where(assignment_id: params[:assignment_id], topic_identifier: params[:topic_ids]) - # render json: @sign_up_topics, status: :ok - end - - if @sign_up_topics.each(&:delete) - render json: { message: "The topic has been deleted successfully. " }, status: :no_content - else - render json: @sign_up_topic.errors, status: :unprocessable_entity - end - end - - private - - # Use callbacks to share common setup or constraints between actions. - def set_sign_up_topic - @sign_up_topic = SignUpTopic.find(params[:id]) - end - - # Only allow a list of trusted parameters through. - def sign_up_topic_params - params.require(:sign_up_topic).permit(:topic_identifier, :category, :topic_name, :max_choosers, :assignment_id) - end -end diff --git a/app/controllers/api/v1/signed_up_teams_controller.rb b/app/controllers/api/v1/signed_up_teams_controller.rb index 97ada5a24..3a35b39c6 100644 --- a/app/controllers/api/v1/signed_up_teams_controller.rb +++ b/app/controllers/api/v1/signed_up_teams_controller.rb @@ -4,8 +4,8 @@ class Api::V1::SignedUpTeamsController < ApplicationController # Retrieves sign_up_topic using topic_id as a parameter def index # puts params[:topic_id] - @sign_up_topic = SignUpTopic.find(params[:topic_id]) - @signed_up_team = SignedUpTeam.find_team_participants(@sign_up_topic.assignment_id) + @project_topic = ProjectTopic.find(params[:topic_id]) + @signed_up_team = SignedUpTeam.find_team_participants(@project_topic.assignment_id) render json: @signed_up_team end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index ac804a70d..8a177be77 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -8,7 +8,7 @@ class Assignment < ApplicationRecord has_many :questionnaires, through: :assignment_questionnaires has_many :response_maps, foreign_key: 'reviewed_object_id', dependent: :destroy, inverse_of: :assignment has_many :review_mappings, class_name: 'ReviewResponseMap', foreign_key: 'reviewed_object_id', dependent: :destroy, inverse_of: :assignment - has_many :sign_up_topics , class_name: 'ProjectTopic', foreign_key: 'assignment_id', dependent: :destroy + has_many :project_topics , class_name: 'ProjectTopic', foreign_key: 'assignment_id', dependent: :destroy has_many :due_dates, class_name: 'DueDate', foreign_key: 'due_date_id', dependent: :destroy, as: :parent belongs_to :course, optional: true belongs_to :instructor, class_name: 'User', inverse_of: :assignments diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb index 2f02eabb5..df2fddb39 100644 --- a/app/models/bookmark.rb +++ b/app/models/bookmark.rb @@ -1,6 +1,6 @@ class Bookmark < ApplicationRecord belongs_to :user - # belongs_to :topic, class_name: "SignUpTopic" + # belongs_to :topic, class_name: "ProjectTopic" has_many :bookmark_ratings validates :url, presence: true validates :title, presence: true diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index 39b1e2de4..2919b85ca 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -1,4 +1,4 @@ class SignedUpTeam < ApplicationRecord - belongs_to :sign_up_topic + belongs_to :project_topic belongs_to :team end diff --git a/config/routes.rb b/config/routes.rb index e5d805c4f..f4a7c0fa7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -92,7 +92,7 @@ - resources :sign_up_topics do + resources :project_topics do collection do get :filter delete '/', to: 'sign_up_topics#destroy' diff --git a/spec/models/due_date_spec.rb b/spec/models/due_date_spec.rb index 2df3035cf..b401c8b87 100644 --- a/spec/models/due_date_spec.rb +++ b/spec/models/due_date_spec.rb @@ -136,12 +136,12 @@ end end - context 'when parent_type is SignUpTopic' do + context 'when parent_type is ProjectTopic' do let!(:assignment) { Assignment.create!(id: 2, name: 'Test Assignment', instructor:) } let!(:assignment2) { Assignment.create(id: 6, name: 'Test Assignment2', instructor:) } - let!(:topic1) { SignUpTopic.create!(id: 2, topic_name: 'Test Topic', assignment:) } - let!(:topic2) { SignUpTopic.create(id: 4, topic_name: 'Test Topic2', assignment: assignment2) } - let!(:topic3) { SignUpTopic.create(id: 5, topic_name: 'Test Topic2', assignment: assignment2) } + let!(:topic1) { ProjectTopic.create!(id: 2, topic_name: 'Test Topic', assignment:) } + let!(:topic2) { ProjectTopic.create(id: 4, topic_name: 'Test Topic2', assignment: assignment2) } + let!(:topic3) { ProjectTopic.create(id: 5, topic_name: 'Test Topic2', assignment: assignment2) } let!(:topic_due_date1) do DueDate.create(parent: topic1, due_at: 2.days.from_now, submission_allowed_id: 3, review_allowed_id: 3, deadline_type_id: 3, type: 'TopicDueDate') diff --git a/spec/requests/api/v1/sign_up_topic_controller_spec.rb b/spec/requests/api/v1/project_topic_controller_spec.rb similarity index 68% rename from spec/requests/api/v1/sign_up_topic_controller_spec.rb rename to spec/requests/api/v1/project_topic_controller_spec.rb index dbbe78673..4b978d221 100644 --- a/spec/requests/api/v1/sign_up_topic_controller_spec.rb +++ b/spec/requests/api/v1/project_topic_controller_spec.rb @@ -1,14 +1,14 @@ require 'swagger_helper' -RSpec.describe 'SignUpTopicController API', type: :request do +RSpec.describe 'ProjectTopicController API', type: :request do - # GET /sign_up_topics - path '/api/v1/sign_up_topics' do - get('Get sign-up topics') do + # GET /project_topics + path '/api/v1/project_topics' do + get('Get project topics') do parameter name: 'assignment_id', in: :query, type: :integer, description: 'Assignment ID', required: true parameter name: 'topic_ids', in: :query, type: :string, description: 'Topic Identifier', required: false - tags 'SignUpTopic' + tags 'ProjectTopic' produces 'application/json' response(200, 'successful') do after do |example| @@ -19,9 +19,9 @@ } end # context 'when assignment_id parameter is missing' do - # let(:assignment) { create(:sign_up_topic, assignment_id: create(:assignment)) } + # let(:assignment) { create(:project_topic, assignment_id: create(:assignment)) } # - # before { get '/api/v1/sign_up_topics', params: { assignment_id: assignment_id } } + # before { get '/api/v1/project_topics', params: { assignment_id: assignment_id } } # it 'returns an error message with status 422' do # expect(response).to have_http_status(422) # expect(response_body).to eq({ message: 'Assignment ID is required!' }) @@ -29,30 +29,30 @@ # end context 'when assignment_id parameter is present' do - let!(:sign_up_topics) { create_list(:sign_up_topic, 3, assignment_id: 1) } + let!(:project_topics) { create_list(:project_topic, 3, assignment_id: 1) } let(:assignment_id) { 1 } context 'when topic_identifier parameter is missing' do - before { get "/api/v1/sign_up_topics?assignment_id=#{assignment_id}" } + before { get "/api/v1/project_topics?assignment_id=#{assignment_id}" } - it 'returns a list of all sign-up topics with the given assignment_id' do + it 'returns a list of all project topics with the given assignment_id' do expect(response).to have_http_status(200) expect(response_body[:message]).to eq('All selected topics have been loaded successfully.') - expect(response_body[:sign_up_topics].count).to eq(3) + expect(response_body[:project_topics].count).to eq(3) end end context 'when topic_identifier parameter is present' do - let!(:sign_up_topic) { create(:sign_up_topic, assignment_id: 1, topic_identifier: 'abc') } + let!(:project_topic) { create(:project_topic, assignment_id: 1, topic_identifier: 'abc') } let(:topic_identifier) { 'abc' } - before { get "/api/v1/sign_up_topics?assignment_id=#{assignment_id}&topic_identifier=#{topic_identifier}" } + before { get "/api/v1/project_topics?assignment_id=#{assignment_id}&topic_identifier=#{topic_identifier}" } - it 'returns a list of sign-up topics with the given assignment_id and topic_identifier' do + it 'returns a list of project topics with the given assignment_id and topic_identifier' do expect(response).to have_http_status(200) expect(response_body[:message]).to eq('All selected topics have been loaded successfully.') - expect(response_body[:sign_up_topics].count).to eq(1) - expect(response_body[:sign_up_topics].first[:topic_identifier]).to eq('abc') + expect(response_body[:project_topics].count).to eq(1) + expect(response_body[:project_topics].first[:topic_identifier]).to eq('abc') end end end @@ -63,13 +63,13 @@ end - # DELETE /sign_up_topics - path '/api/v1/sign_up_topics' do - delete('Delete sign-up topics') do + # DELETE /project_topics + path '/api/v1/project_topics' do + delete('Delete project topics') do parameter name: 'assignment_id', in: :query, type: :integer, description: 'Assignment ID', required: true parameter name: 'topic_ids', in: :query, type: :array, items: { type: :string }, description: 'Topic Identifiers to delete', required: false - tags 'SignUpTopic' + tags 'ProjectTopic' produces 'application/json' response(200, 'successful') do after do |example| @@ -83,7 +83,7 @@ context 'when assignment_id parameter is missing' do let(:assignment_id) { nil } - before { delete '/api/v1/sign_up_topics', params: { assignment_id: assignment_id } } + before { delete '/api/v1/project_topics', params: { assignment_id: assignment_id } } it 'returns an error message with status 422' do expect(response).to have_http_status(422) @@ -95,26 +95,26 @@ context 'when topic_ids parameter is missing' do let(:assignment_id) { 1 } - before { delete "/api/v1/sign_up_topics?assignment_id=#{assignment_id}" } + before { delete "/api/v1/project_topics?assignment_id=#{assignment_id}" } - it 'deletes all sign-up topics with the given assignment_id' do + it 'deletes all project topics with the given assignment_id' do expect(response).to have_http_status(200) - expect(response_body).to eq({ message: 'All sign-up topics have been deleted successfully.' }) - expect(SignUpTopic.where(assignment_id: assignment_id)).to be_empty + expect(response_body).to eq({ message: 'All project topics have been deleted successfully.' }) + expect(ProjectTopic.where(assignment_id: assignment_id)).to be_empty end end context 'when topic_ids parameter is present' do - let!(:sign_up_topic) { create(:sign_up_topic, assignment_id: 1, topic_identifier: 'abc') } + let!(:project_topic) { create(:project_topic, assignment_id: 1, topic_identifier: 'abc') } let(:topic_ids) { ['abc'] } let(:assignment_id) { 1 } - before { delete "/api/v1/sign_up_topics?assignment_id=#{assignment_id}&topic_ids=#{topic_ids.join(',')}" } + before { delete "/api/v1/project_topics?assignment_id=#{assignment_id}&topic_ids=#{topic_ids.join(',')}" } - it 'deletes sign-up topics with the given assignment_id and topic_identifier' do + it 'deletes project topics with the given assignment_id and topic_identifier' do expect(response).to have_http_status(200) expect(response_body).to eq({ message: 'All selected topics have been deleted successfully.' }) - expect(SignUpTopic.where(assignment_id: assignment_id, topic_identifier: topic_ids)).to be_empty + expect(ProjectTopic.where(assignment_id: assignment_id, topic_identifier: topic_ids)).to be_empty end end end @@ -122,14 +122,14 @@ end end - # CREATE /sign_up_topics - path '/api/v1/sign_up_topics' do + # CREATE /project_topics + path '/api/v1/project_topics' do post('create a new topic in the sheet') do - tags 'SignUpTopic' + tags 'ProjectTopic' consumes 'application/json' - #inputs are from the sign up topic table with properties as ID, name, choosers + #inputs are from the project topic table with properties as ID, name, choosers # assignment ID and micropayment - parameter name: :sign_up_topic, in: :body, schema: { + parameter name: :project_topic, in: :body, schema: { type: :object, properties: { topic_identifier: { type: :integer }, @@ -158,20 +158,20 @@ let!(:assignment) { create(:assignment) } context 'when the request is valid' do - let(:valid_attributes) { { sign_up_topic: attributes_for(:sign_up_topic, assignment_id: assignment.id), micropayment: 0.1 } } + let(:valid_attributes) { { project_topic: attributes_for(:project_topic, assignment_id: assignment.id), micropayment: 0.1 } } - before { post '/api/v1/sign_up_topics', params: valid_attributes } + before { post '/api/v1/project_topics', params: valid_attributes } - it 'creates a sign-up topic' do + it 'creates a project topic' do expect(response).to have_http_status(:created) - expect(response_body[:message]).to eq("The topic: \"#{SignUpTopic.last.topic_name}\" has been created successfully.") + expect(response_body[:message]).to eq("The topic: \"#{ProjectTopic.last.topic_name}\" has been created successfully.") end end context 'when the request is invalid' do - let(:invalid_attributes) { { sign_up_topic: { topic_name: '' }, micropayment: 0.1, assignment_id: assignment.id } } + let(:invalid_attributes) { { project_topic: { topic_name: '' }, micropayment: 0.1, assignment_id: assignment.id } } - before { post '/api/v1/sign_up_topics', params: invalid_attributes } + before { post '/api/v1/project_topics', params: invalid_attributes } it 'returns an error message' do expect(response).to have_http_status(:unprocessable_entity) @@ -180,9 +180,9 @@ end context 'when the assignment does not exist' do - let(:invalid_attributes) { { sign_up_topic: attributes_for(:sign_up_topic), micropayment: 0.1, assignment_id: 999 } } + let(:invalid_attributes) { { project_topic: attributes_for(:project_topic), micropayment: 0.1, assignment_id: 999 } } - before { post '/api/v1/sign_up_topics', params: invalid_attributes } + before { post '/api/v1/project_topics', params: invalid_attributes } it 'returns an error message' do expect(response).to have_http_status(:unprocessable_entity) @@ -191,42 +191,42 @@ end context 'when the assignment is a microtask' do - let(:valid_attributes) { { sign_up_topic: attributes_for(:sign_up_topic, assignment_id: assignment.id), micropayment: 0.1 } } + let(:valid_attributes) { { project_topic: attributes_for(:project_topic, assignment_id: assignment.id), micropayment: 0.1 } } before do assignment.update(microtask: true) - post '/api/v1/sign_up_topics', params: valid_attributes + post '/api/v1/project_topics', params: valid_attributes end it 'sets the micropayment' do expect(response).to have_http_status(:created) - expect(SignUpTopic.last.micropayment).to eq(0.1) + expect(ProjectTopic.last.micropayment).to eq(0.1) end end context 'when the assignment is not a microtask' do - let(:valid_attributes) { { sign_up_topic: attributes_for(:sign_up_topic, assignment_id: assignment.id), micropayment: 0.1 } } + let(:valid_attributes) { { project_topic: attributes_for(:project_topic, assignment_id: assignment.id), micropayment: 0.1 } } before do assignment.update(microtask: false) - post '/api/v1/sign_up_topics', params: valid_attributes + post '/api/v1/project_topics', params: valid_attributes end it 'does not set the micropayment' do expect(response).to have_http_status(:created) - expect(SignUpTopic.last.micropayment).to be_nil + expect(ProjectTopic.last.micropayment).to be_nil end end end - # UPDATE /sign_up_topics - path '/api/v1/sign_up_topics/{id}' do - parameter name: 'id', in: :path, type: :integer, description: 'id of the sign up topic' + # UPDATE /project_topics + path '/api/v1/project_topics/{id}' do + parameter name: 'id', in: :path, type: :integer, description: 'id of the project topic' put('update a new topic in the sheet') do - tags 'SignUpTopic' + tags 'ProjectTopic' consumes 'application/json' - parameter name: :sign_up_topic, in: :body, schema: { + parameter name: :project_topic, in: :body, schema: { type: :object, properties: { topic_identifier: { type: :integer }, @@ -250,12 +250,12 @@ run_test! end - let(:sign_up_topic) { create(:sign_up_topic) } - let(:url) { "/api/v1/sign_up_topics/#{sign_up_topic.id}" } + let(:project_topic) { create(:project_topic) } + let(:url) { "/api/v1/project_topics/#{project_topic.id}" } context "when valid params are provided" do let(:new_topic_name) { "New Topic Name" } - let(:params) { { sign_up_topic: { topic_name: new_topic_name } } } + let(:params) { { project_topic: { topic_name: new_topic_name } } } before { put url, params: params } @@ -264,8 +264,8 @@ end it "updates the sign-up topic" do - sign_up_topic.reload - expect(sign_up_topic.topic_name).to eq new_topic_name + project_topic.reload + expect(project_topic.topic_name).to eq new_topic_name end it "returns a success message" do @@ -274,7 +274,7 @@ end context "when invalid params are provided" do - let(:params) { { sign_up_topic: { topic_name: "" } } } + let(:params) { { project_topic: { topic_name: "" } } } before { put url, params: params } @@ -283,8 +283,8 @@ end it "does not update the sign-up topic" do - sign_up_topic.reload - expect(sign_up_topic.topic_name).not_to eq("") + project_topic.reload + expect(project_topic.topic_name).not_to eq("") end it "returns an error message" do diff --git a/spec/routing/sign_up_topics_routing_spec.rb b/spec/routing/project_topics_routing_spec.rb similarity index 92% rename from spec/routing/sign_up_topics_routing_spec.rb rename to spec/routing/project_topics_routing_spec.rb index bf7cfa236..58166e31a 100644 --- a/spec/routing/sign_up_topics_routing_spec.rb +++ b/spec/routing/project_topics_routing_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe Api::V1::SignUpTopicsController, type: :routing do +RSpec.describe Api::V1::ProjectTopicsController, type: :routing do describe "routing" do it "routes to #index" do expect(get: "/api/v1/sign_up_topics").to route_to("api/v1/sign_up_topics#index") diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index de8081625..4f15e3077 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -1038,54 +1038,54 @@ paths: responses: '204': description: successful - "/api/v1/sign_up_topics": + "/api/v1/project_topics": get: - summary: Get sign-up topics + summary: Get project topics parameters: - - name: assignment_id - in: query - description: Assignment ID - required: true - schema: - type: integer - - name: topic_ids - in: query - description: Topic Identifier - required: false - schema: - type: string + - name: assignment_id + in: query + description: Assignment ID + required: true + schema: + type: integer + - name: topic_ids + in: query + description: Topic Identifier + required: false + schema: + type: string tags: - - SignUpTopic + - ProjectTopic responses: '200': description: successful delete: - summary: Delete sign-up topics + summary: Delete project topics parameters: - - name: assignment_id - in: query - description: Assignment ID - required: true - schema: - type: integer - - name: topic_ids - in: query - items: - type: string - description: Topic Identifiers to delete - required: false - schema: - type: array + - name: assignment_id + in: query + description: Assignment ID + required: true + schema: + type: integer + - name: topic_ids + in: query + items: + type: string + description: Topic Identifiers to delete + required: false + schema: + type: array tags: - - SignUpTopic + - ProjectTopic responses: '200': description: successful post: summary: create a new topic in the sheet tags: - - SignUpTopic - parameters: [] + - ProjectTopic + parameters: [ ] responses: '201': description: Success @@ -1108,25 +1108,25 @@ paths: micropayment: type: integer required: - - topic_identifier - - topic_name - - max_choosers - - category - - assignment_id - - micropayment - "/api/v1/sign_up_topics/{id}": + - topic_identifier + - topic_name + - max_choosers + - category + - assignment_id + - micropayment + "/api/v1/project_topics/{id}": parameters: - - name: id - in: path - description: id of the sign up topic - required: true - schema: - type: integer + - name: id + in: path + description: id of the sign up topic + required: true + schema: + type: integer put: summary: update a new topic in the sheet tags: - - SignUpTopic - parameters: [] + - ProjectTopic + parameters: [ ] responses: '200': description: successful @@ -1149,10 +1149,10 @@ paths: micropayment: type: integer required: - - topic_identifier - - topic_name - - category - - assignment_id + - topic_identifier + - topic_name + - category + - assignment_id "/api/v1/signed_up_teams/sign_up": post: summary: Creates a signed up team From 8ed7a4aca0f187a9b1f28bc217996dd64125fdf7 Mon Sep 17 00:00:00 2001 From: smiti Date: Sat, 22 Mar 2025 05:20:37 +0530 Subject: [PATCH 04/13] add tests --- app/models/project_topic.rb | 23 +++++------------------ app/models/signed_up_team.rb | 19 ++++++++++++++++++- app/models/team.rb | 18 ++++++++++++------ db/schema.rb | 2 +- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/app/models/project_topic.rb b/app/models/project_topic.rb index 5ad9a5f92..ae4fd82c8 100644 --- a/app/models/project_topic.rb +++ b/app/models/project_topic.rb @@ -1,17 +1,14 @@ class ProjectTopic < ApplicationRecord - # Associations has_many :signed_up_teams, dependent: :destroy - has_many :teams, through: :signed_up_teams + has_many :teams, through: :signed_up_teams belongs_to :assignment - # Validations validates :max_choosers, numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :topic_name, presence: true - # Sign up a team to the topic with waitlist management def signup_team(team) return false if signed_up_teams.exists?(team: team) @@ -21,7 +18,6 @@ def signup_team(team) is_waitlisted: !slot_available? ) - # Remove from other waitlists if successfully registered remove_from_other_waitlists(team) unless signed_up_team.is_waitlisted? true end @@ -29,44 +25,35 @@ def signup_team(team) false end - # Remove team from topic and handle waitlist promotion def drop_team(team) signed_up_team = signed_up_teams.find_by(team: team) return unless signed_up_team was_confirmed = !signed_up_team.is_waitlisted? signed_up_team.destroy! - promote_waitlisted_team if was_confirmed end - # Get current number of available slots def available_slots max_choosers - confirmed_teams_count end - # Check if slot is available def slot_available? available_slots.positive? end - # Get confirmed teams def confirmed_teams - teams.joins(:signed_up_teams) - .where(signed_up_teams: { is_waitlisted: false }) + teams.confirmed end - # Get waitlisted teams in order def waitlisted_teams - teams.joins(:signed_up_teams) - .where(signed_up_teams: { is_waitlisted: true }) - .order('signed_up_teams.created_at ASC') + teams.waitlisted.order('signed_up_teams.created_at ASC') end private def confirmed_teams_count - signed_up_teams.where(is_waitlisted: false).count + signed_up_teams.confirmed.count end def promote_waitlisted_team @@ -80,4 +67,4 @@ def promote_waitlisted_team def remove_from_other_waitlists(team) team.signed_up_teams.waitlisted.destroy_all end -end +end \ No newline at end of file diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index 2919b85ca..b86405ed5 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -1,4 +1,21 @@ class SignedUpTeam < ApplicationRecord + scope :confirmed, -> { where(is_waitlisted: false) } + scope :waitlisted, -> { where(is_waitlisted: true) } + belongs_to :project_topic belongs_to :team -end + + validates :project_topic, presence: true + validates :team, presence: true, + uniqueness: { scope: :project_topic } + + def self.signup_for_topic(team, topic) + topic.signup_team(team) + end + + def self.remove_team_signups(team) + team.signed_up_teams.includes(:project_topic).each do |sut| + sut.project_topic.drop_team(team) + end + end +end \ No newline at end of file diff --git a/app/models/team.rb b/app/models/team.rb index afb8ac66f..a8e94bba0 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -1,20 +1,26 @@ class Team < ApplicationRecord has_many :signed_up_teams, dependent: :destroy + has_many :project_topics, through: :signed_up_teams has_many :teams_users, dependent: :destroy has_many :users, through: :teams_users has_many :participants belongs_to :assignment attr_accessor :max_participants + after_update :release_topics_if_empty + # TODO Team implementing Teams controller and model should implement this method better. # TODO partial implementation here just for the functionality needed for join_team_tequests controller def full? - max_participants ||= 3 - if participants.count >= max_participants - true - else - false - end + participants.count >= max_participants + end + + private + + def release_topics_if_empty + return unless saved_change_to_participants_count? && participants.empty? + + project_topics.each { |topic| topic.drop_team(self) } end end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 6e611febb..0c2839041 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_03_19_015434) do +ActiveRecord::Schema[8.0].define(version: 2025_03_21_222753) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" From 27936161d05b15caf4bfc7ebe813ea351cfdfbc9 Mon Sep 17 00:00:00 2001 From: smiti Date: Sat, 22 Mar 2025 05:20:54 +0530 Subject: [PATCH 05/13] add tests and code for model --- app/models/project_topic.rb | 7 +- ...pic_to_project_topic_in_signed_up_teams.rb | 8 ++ db/schema.rb | 14 +- spec/models/project_topic_spec.rb | 124 ++++++++++++++++++ spec/models/signed_up_team_spec.rb | 74 +++++++++++ 5 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 db/migrate/20250321222753_rename_sign_up_topic_to_project_topic_in_signed_up_teams.rb create mode 100644 spec/models/project_topic_spec.rb create mode 100644 spec/models/signed_up_team_spec.rb diff --git a/app/models/project_topic.rb b/app/models/project_topic.rb index ae4fd82c8..9e2018bbc 100644 --- a/app/models/project_topic.rb +++ b/app/models/project_topic.rb @@ -43,11 +43,14 @@ def slot_available? end def confirmed_teams - teams.confirmed + teams.joins(:signed_up_teams) + .where(signed_up_teams: { is_waitlisted: false }) end def waitlisted_teams - teams.waitlisted.order('signed_up_teams.created_at ASC') + teams.joins(:signed_up_teams) + .where(signed_up_teams: { is_waitlisted: true }) + .order('signed_up_teams.created_at ASC') end private diff --git a/db/migrate/20250321222753_rename_sign_up_topic_to_project_topic_in_signed_up_teams.rb b/db/migrate/20250321222753_rename_sign_up_topic_to_project_topic_in_signed_up_teams.rb new file mode 100644 index 000000000..1d9305315 --- /dev/null +++ b/db/migrate/20250321222753_rename_sign_up_topic_to_project_topic_in_signed_up_teams.rb @@ -0,0 +1,8 @@ +class RenameSignUpTopicToProjectTopicInSignedUpTeams < ActiveRecord::Migration[8.0] + def change + rename_column :signed_up_teams, :sign_up_topic_id, :project_topic_id + rename_index :signed_up_teams, + :index_signed_up_teams_on_sign_up_topic_id, + :index_signed_up_teams_on_project_topic_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 0c2839041..e18179f93 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -324,13 +324,13 @@ end create_table "signed_up_teams", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| - t.bigint "sign_up_topic_id", null: false + t.bigint "project_topic_id", null: false t.bigint "team_id", null: false t.boolean "is_waitlisted" t.integer "preference_priority_number" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["sign_up_topic_id"], name: "index_signed_up_teams_on_sign_up_topic_id" + t.index ["project_topic_id"], name: "index_signed_up_teams_on_project_topic_id" t.index ["team_id"], name: "index_signed_up_teams_on_team_id" end @@ -401,14 +401,4 @@ add_foreign_key "project_topics", "assignments" add_foreign_key "question_advices", "items", column: "question_id" add_foreign_key "roles", "roles", column: "parent_id", on_delete: :cascade - add_foreign_key "signed_up_teams", "project_topics", column: "sign_up_topic_id" - add_foreign_key "signed_up_teams", "teams" - add_foreign_key "ta_mappings", "courses" - add_foreign_key "ta_mappings", "users" - add_foreign_key "teams", "assignments" - add_foreign_key "teams_users", "teams" - add_foreign_key "teams_users", "users" - add_foreign_key "users", "institutions" - add_foreign_key "users", "roles" - add_foreign_key "users", "users", column: "parent_id" end diff --git a/spec/models/project_topic_spec.rb b/spec/models/project_topic_spec.rb new file mode 100644 index 000000000..90f181e47 --- /dev/null +++ b/spec/models/project_topic_spec.rb @@ -0,0 +1,124 @@ +# spec/models/project_topic_spec.rb +require 'rails_helper' + +RSpec.describe ProjectTopic, type: :model do + let!(:role) { Role.create!(name: "Instructor") } + let!(:instructor) do + Instructor.create!( + name: "test_instructor", + password: "password", + full_name: "Test Instructor", + email: "instructor@example.com", + role: role + ) + end + let!(:assignment) { Assignment.create!(name: "Test Assignment", instructor: instructor) } + let!(:project_topic) { ProjectTopic.create!(topic_name: "Test Topic", assignment: assignment, max_choosers: 2) } + let!(:team) { Team.create!(assignment: assignment) } + + describe '#signup_team' do + context 'when slots are available' do + it 'adds team as confirmed' do + expect(project_topic.signup_team(team)).to be true + expect(project_topic.confirmed_teams).to include(team) + end + + it 'removes team from other waitlists' do + other_topic = ProjectTopic.create!(topic_name: "Other Topic", assignment: assignment, max_choosers: 1) + other_topic.signup_team(team) + + project_topic.signup_team(team) + expect(other_topic.reload.waitlisted_teams).not_to include(team) + end + end + + context 'when slots are full' do + before do + 2.times do |n| + t = Team.create!(assignment: assignment) + project_topic.signup_team(t) + end + end + + it 'adds team to waitlist' do + new_team = Team.create!(assignment: assignment) + expect(project_topic.signup_team(new_team)).to be true + expect(project_topic.waitlisted_teams).to include(new_team) + end + end + + context 'when team already signed up' do + before { project_topic.signup_team(team) } + + it 'returns false' do + expect(project_topic.signup_team(team)).to be false + end + end + end + + describe '#drop_team' do + before do + project_topic.signup_team(team) + project_topic.signup_team(Team.create!(assignment: assignment)) + end + + context 'when dropping confirmed team' do + it 'promotes waitlisted team' do + waitlisted_team = Team.create!(assignment: assignment) + project_topic.signup_team(waitlisted_team) + + expect { + project_topic.drop_team(team) + }.to change { project_topic.confirmed_teams.count }.by(0) + + expect(waitlisted_team.reload.signed_up_teams.first.is_waitlisted).to be false + end + end + + context 'when dropping waitlisted team' do + it 'does not promote other teams' do + waitlisted_team = Team.create!(assignment: assignment) + project_topic.signup_team(waitlisted_team) + + expect { + project_topic.drop_team(waitlisted_team) + }.not_to change { project_topic.confirmed_teams.count } + end + end + end + + describe '#available_slots' do + it 'calculates correctly' do + expect(project_topic.available_slots).to eq(2) + project_topic.signup_team(team) + expect(project_topic.available_slots).to eq(1) + end + end + + describe '#slot_available?' do + it 'returns true when slots available' do + expect(project_topic.slot_available?).to be true + end + + it 'returns false when slots full' do + 2.times { |n| project_topic.signup_team(Team.create!(assignment: assignment)) } + expect(project_topic.slot_available?).to be false + end + end + + describe '#confirmed_teams' do + it 'returns non-waitlisted teams' do + project_topic.signup_team(team) + expect(project_topic.confirmed_teams).to include(team) + end + end + + describe '#waitlisted_teams' do + it 'returns waitlisted teams in order' do + teams = 3.times.map { Team.create!(assignment: assignment) } + teams.each { |t| project_topic.signup_team(t) } + + expect(project_topic.waitlisted_teams).to eq([teams[2]]) + end + end +end \ No newline at end of file diff --git a/spec/models/signed_up_team_spec.rb b/spec/models/signed_up_team_spec.rb new file mode 100644 index 000000000..6754096ea --- /dev/null +++ b/spec/models/signed_up_team_spec.rb @@ -0,0 +1,74 @@ +# spec/models/signed_up_team_spec.rb +require 'rails_helper' + +RSpec.describe SignedUpTeam, type: :model do + let!(:role) { Role.create!(name: "Instructor") } + let!(:instructor) do + Instructor.create!( + name: "test_instructor", + password: "password", + full_name: "Test Instructor", + email: "instructor@example.com", + role: role + ) + end + let!(:assignment) { Assignment.create!(name: "Test Assignment", instructor: instructor) } + let!(:project_topic) { ProjectTopic.create!(topic_name: "Test Topic", assignment: assignment) } + let!(:team) { Team.create!(assignment: assignment) } + + describe 'validations' do + it 'requires a project topic' do + sut = SignedUpTeam.new(team: team) + expect(sut).not_to be_valid + expect(sut.errors[:project_topic]).to include("must exist") + end + + it 'requires a team' do + sut = SignedUpTeam.new(project_topic: project_topic) + expect(sut).not_to be_valid + expect(sut.errors[:team]).to include("must exist") + end + + it 'enforces unique team per project topic' do + SignedUpTeam.create!(project_topic: project_topic, team: team) + duplicate = SignedUpTeam.new(project_topic: project_topic, team: team) + expect(duplicate).not_to be_valid + expect(duplicate.errors[:team]).to include("has already been taken") + end + end + + describe 'scopes' do + let!(:confirmed_signup) { SignedUpTeam.create!(project_topic: project_topic, team: team, is_waitlisted: false) } + let!(:waitlisted_signup) { SignedUpTeam.create!(project_topic: project_topic, team: Team.create!(assignment: assignment), is_waitlisted: true) } + + it 'returns confirmed signups' do + expect(SignedUpTeam.confirmed).to contain_exactly(confirmed_signup) + end + + it 'returns waitlisted signups' do + expect(SignedUpTeam.waitlisted).to contain_exactly(waitlisted_signup) + end + end + + describe '.signup_for_topic' do + it 'delegates to project topic signup' do + allow(project_topic).to receive(:signup_team).with(team).and_return(true) + result = SignedUpTeam.signup_for_topic(team, project_topic) + expect(result).to be true + expect(project_topic).to have_received(:signup_team).with(team) + end + end + + describe '.remove_team_signups' do + let!(:topic1) { ProjectTopic.create!(topic_name: "Topic 1", assignment: assignment) } + let!(:topic2) { ProjectTopic.create!(topic_name: "Topic 2", assignment: assignment) } + let!(:signup1) { SignedUpTeam.create!(project_topic: topic1, team: team) } + let!(:signup2) { SignedUpTeam.create!(project_topic: topic2, team: team) } + + it 'removes all team signups across topics' do + expect { + SignedUpTeam.remove_team_signups(team) + }.to change(SignedUpTeam, :count).by(-2) + end + end +end \ No newline at end of file From acecf6bfababe6d6cfe3a0e184a1a13bda55785a Mon Sep 17 00:00:00 2001 From: smiti Date: Mon, 24 Mar 2025 04:58:41 +0530 Subject: [PATCH 06/13] made necessary changes --- app/models/project_topic.rb | 8 ++++++++ app/models/signed_up_team.rb | 3 +++ config/routes.rb | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/models/project_topic.rb b/app/models/project_topic.rb index 9e2018bbc..97ec6c581 100644 --- a/app/models/project_topic.rb +++ b/app/models/project_topic.rb @@ -10,6 +10,7 @@ class ProjectTopic < ApplicationRecord validates :topic_name, presence: true def signup_team(team) + """Signs up a team, adding to waitlist if no slots available. Returns success status""" return false if signed_up_teams.exists?(team: team) ActiveRecord::Base.transaction do @@ -26,6 +27,7 @@ def signup_team(team) end def drop_team(team) + """Removes a team from the topic. Promotes next waitlisted team if slot opens""" signed_up_team = signed_up_teams.find_by(team: team) return unless signed_up_team @@ -35,19 +37,23 @@ def drop_team(team) end def available_slots + """Calculates remaining available slots for confirmed teams""" max_choosers - confirmed_teams_count end def slot_available? + """Checks if there are available slots for immediate confirmation""" available_slots.positive? end def confirmed_teams + """Returns confirmed (non-waitlisted) teams for this topic""" teams.joins(:signed_up_teams) .where(signed_up_teams: { is_waitlisted: false }) end def waitlisted_teams + """Returns waitlisted teams in first-come-first-served order""" teams.joins(:signed_up_teams) .where(signed_up_teams: { is_waitlisted: true }) .order('signed_up_teams.created_at ASC') @@ -60,6 +66,7 @@ def confirmed_teams_count end def promote_waitlisted_team + """Promotes the first waitlisted team to confirmed status""" next_team = waitlisted_teams.first return unless next_team @@ -68,6 +75,7 @@ def promote_waitlisted_team end def remove_from_other_waitlists(team) + """Removes team from all other topic waitlists after successful confirmation""" team.signed_up_teams.waitlisted.destroy_all end end \ No newline at end of file diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index b86405ed5..41b6f22ca 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -1,4 +1,5 @@ class SignedUpTeam < ApplicationRecord + """Scopes for filtering confirmed/waitlisted records""" scope :confirmed, -> { where(is_waitlisted: false) } scope :waitlisted, -> { where(is_waitlisted: true) } @@ -10,10 +11,12 @@ class SignedUpTeam < ApplicationRecord uniqueness: { scope: :project_topic } def self.signup_for_topic(team, topic) + """Wrapper method to initiate team signup for a specific topic.""" topic.signup_team(team) end def self.remove_team_signups(team) + """Removes all topic associations for a team.""" team.signed_up_teams.includes(:project_topic).each do |sut| sut.project_topic.drop_team(team) end diff --git a/config/routes.rb b/config/routes.rb index f4a7c0fa7..986274166 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -95,7 +95,7 @@ resources :project_topics do collection do get :filter - delete '/', to: 'sign_up_topics#destroy' + delete '/', to: 'project_topics#destroy' end end From f6a4774d345a39714c5e8a612590385d76098325 Mon Sep 17 00:00:00 2001 From: dmpatel3 Date: Sun, 23 Mar 2025 21:54:21 -0400 Subject: [PATCH 07/13] Added and modified tests for the ProjectTopic controller --- app/models/project_topic.rb | 27 +++++++------------- app/models/signed_up_team.rb | 4 +-- spec/models/project_topic_spec.rb | 41 ++++++++++++++++++++++-------- spec/models/signed_up_team_spec.rb | 5 ++-- 4 files changed, 43 insertions(+), 34 deletions(-) diff --git a/app/models/project_topic.rb b/app/models/project_topic.rb index 97ec6c581..89a3b79c6 100644 --- a/app/models/project_topic.rb +++ b/app/models/project_topic.rb @@ -2,7 +2,6 @@ class ProjectTopic < ApplicationRecord has_many :signed_up_teams, dependent: :destroy has_many :teams, through: :signed_up_teams belongs_to :assignment - validates :max_choosers, numericality: { only_integer: true, greater_than_or_equal_to: 0 @@ -10,16 +9,13 @@ class ProjectTopic < ApplicationRecord validates :topic_name, presence: true def signup_team(team) - """Signs up a team, adding to waitlist if no slots available. Returns success status""" return false if signed_up_teams.exists?(team: team) - ActiveRecord::Base.transaction do signed_up_team = signed_up_teams.create!( team: team, is_waitlisted: !slot_available? ) - - remove_from_other_waitlists(team) unless signed_up_team.is_waitlisted? + remove_from_waitlist(team) unless signed_up_team.is_waitlisted? true end rescue ActiveRecord::RecordInvalid @@ -27,33 +23,31 @@ def signup_team(team) end def drop_team(team) - """Removes a team from the topic. Promotes next waitlisted team if slot opens""" signed_up_team = signed_up_teams.find_by(team: team) return unless signed_up_team - - was_confirmed = !signed_up_team.is_waitlisted? + team_confirmed = !signed_up_team.is_waitlisted? signed_up_team.destroy! - promote_waitlisted_team if was_confirmed + promote_waitlisted_team if team_confirmed end def available_slots - """Calculates remaining available slots for confirmed teams""" max_choosers - confirmed_teams_count end def slot_available? - """Checks if there are available slots for immediate confirmation""" available_slots.positive? end + def get_signed_up_teams + signed_up_teams + end + def confirmed_teams - """Returns confirmed (non-waitlisted) teams for this topic""" teams.joins(:signed_up_teams) .where(signed_up_teams: { is_waitlisted: false }) end def waitlisted_teams - """Returns waitlisted teams in first-come-first-served order""" teams.joins(:signed_up_teams) .where(signed_up_teams: { is_waitlisted: true }) .order('signed_up_teams.created_at ASC') @@ -66,16 +60,13 @@ def confirmed_teams_count end def promote_waitlisted_team - """Promotes the first waitlisted team to confirmed status""" next_team = waitlisted_teams.first return unless next_team - signed_up_teams.find_by(team: next_team)&.update!(is_waitlisted: false) - remove_from_other_waitlists(next_team) + remove_from_waitlist(next_team) end - def remove_from_other_waitlists(team) - """Removes team from all other topic waitlists after successful confirmation""" + def remove_from_waitlist(team) team.signed_up_teams.waitlisted.destroy_all end end \ No newline at end of file diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index 41b6f22ca..d60a2a358 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -10,12 +10,12 @@ class SignedUpTeam < ApplicationRecord validates :team, presence: true, uniqueness: { scope: :project_topic } - def self.signup_for_topic(team, topic) + def signup_for_topic(team, topic) """Wrapper method to initiate team signup for a specific topic.""" topic.signup_team(team) end - def self.remove_team_signups(team) + def remove_team_signups(team) """Removes all topic associations for a team.""" team.signed_up_teams.includes(:project_topic).each do |sut| sut.project_topic.drop_team(team) diff --git a/spec/models/project_topic_spec.rb b/spec/models/project_topic_spec.rb index 90f181e47..bbc398936 100644 --- a/spec/models/project_topic_spec.rb +++ b/spec/models/project_topic_spec.rb @@ -23,10 +23,9 @@ expect(project_topic.confirmed_teams).to include(team) end - it 'removes team from other waitlists' do + it 'removes team from waitlist' do other_topic = ProjectTopic.create!(topic_name: "Other Topic", assignment: assignment, max_choosers: 1) other_topic.signup_team(team) - project_topic.signup_team(team) expect(other_topic.reload.waitlisted_teams).not_to include(team) end @@ -49,7 +48,6 @@ context 'when team already signed up' do before { project_topic.signup_team(team) } - it 'returns false' do expect(project_topic.signup_team(team)).to be false end @@ -65,36 +63,56 @@ context 'when dropping confirmed team' do it 'promotes waitlisted team' do waitlisted_team = Team.create!(assignment: assignment) + waitlisted_team2 = Team.create!(assignment: assignment) project_topic.signup_team(waitlisted_team) - + project_topic.signup_team(waitlisted_team2) expect { project_topic.drop_team(team) }.to change { project_topic.confirmed_teams.count }.by(0) - expect(waitlisted_team.reload.signed_up_teams.first.is_waitlisted).to be false + expect(project_topic.waitlisted_teams.first).to eq(waitlisted_team2) end end context 'when dropping waitlisted team' do it 'does not promote other teams' do waitlisted_team = Team.create!(assignment: assignment) + waitlisted_team2 = Team.create!(assignment: assignment) project_topic.signup_team(waitlisted_team) - + project_topic.signup_team(waitlisted_team2) expect { project_topic.drop_team(waitlisted_team) }.not_to change { project_topic.confirmed_teams.count } + expect(project_topic.waitlisted_teams.first).to eq(waitlisted_team2) end end end describe '#available_slots' do - it 'calculates correctly' do + it 'returns # of available slots correctly' do expect(project_topic.available_slots).to eq(2) project_topic.signup_team(team) expect(project_topic.available_slots).to eq(1) end end + describe '#get_signed_up_teams' do + it 'returns confirmed and waitlisted teams' do + team2 = Team.create!(assignment: assignment) + team3 = Team.create!(assignment: assignment) + project_topic.signup_team(team) + project_topic.signup_team(team2) + project_topic.signup_team(team3) + expect(project_topic.confirmed_teams).to include(team) + expect(project_topic.confirmed_teams).to include(team2) + expect(project_topic.waitlisted_teams).to include(team3) + topic_teams = project_topic.get_signed_up_teams + expect(topic_teams).to include(team) + expect(topic_teams).to include(team2) + expect(topic_teams).to include(team3) + end + end + describe '#slot_available?' do it 'returns true when slots available' do expect(project_topic.slot_available?).to be true @@ -107,7 +125,7 @@ end describe '#confirmed_teams' do - it 'returns non-waitlisted teams' do + it 'returns confirmed teams' do project_topic.signup_team(team) expect(project_topic.confirmed_teams).to include(team) end @@ -115,10 +133,11 @@ describe '#waitlisted_teams' do it 'returns waitlisted teams in order' do - teams = 3.times.map { Team.create!(assignment: assignment) } + teams = 5.times.map { Team.create!(assignment: assignment) } teams.each { |t| project_topic.signup_team(t) } - - expect(project_topic.waitlisted_teams).to eq([teams[2]]) + expect(project_topic.waitlisted_teams.first).to eq([teams[2]]) + expect(project_topic.waitlisted_teams.second).to eq([teams[3]]) + expect(project_topic.waitlisted_teams.third).to eq([teams[4]]) end end end \ No newline at end of file diff --git a/spec/models/signed_up_team_spec.rb b/spec/models/signed_up_team_spec.rb index 6754096ea..2bc32bf14 100644 --- a/spec/models/signed_up_team_spec.rb +++ b/spec/models/signed_up_team_spec.rb @@ -50,7 +50,7 @@ end end - describe '.signup_for_topic' do + describe 'signup_for_topic' do it 'delegates to project topic signup' do allow(project_topic).to receive(:signup_team).with(team).and_return(true) result = SignedUpTeam.signup_for_topic(team, project_topic) @@ -59,12 +59,11 @@ end end - describe '.remove_team_signups' do + describe 'remove_team_signups' do let!(:topic1) { ProjectTopic.create!(topic_name: "Topic 1", assignment: assignment) } let!(:topic2) { ProjectTopic.create!(topic_name: "Topic 2", assignment: assignment) } let!(:signup1) { SignedUpTeam.create!(project_topic: topic1, team: team) } let!(:signup2) { SignedUpTeam.create!(project_topic: topic2, team: team) } - it 'removes all team signups across topics' do expect { SignedUpTeam.remove_team_signups(team) From 0faeb334754c5ae12e1572d6ef53d93468b1a0db Mon Sep 17 00:00:00 2001 From: smiti Date: Mon, 24 Mar 2025 09:07:34 +0530 Subject: [PATCH 08/13] fixed the routing test file --- spec/routing/project_topics_routing_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/routing/project_topics_routing_spec.rb b/spec/routing/project_topics_routing_spec.rb index 58166e31a..6130c9984 100644 --- a/spec/routing/project_topics_routing_spec.rb +++ b/spec/routing/project_topics_routing_spec.rb @@ -3,27 +3,27 @@ RSpec.describe Api::V1::ProjectTopicsController, type: :routing do describe "routing" do it "routes to #index" do - expect(get: "/api/v1/sign_up_topics").to route_to("api/v1/sign_up_topics#index") + expect(get: "/api/v1/project_topics").to route_to("api/v1/project_topics#index") end it "routes to #show" do - expect(get: "/api/v1/sign_up_topics/1").to route_to("api/v1/sign_up_topics#show", id: "1") + expect(get: "/api/v1/project_topics/1").to route_to("api/v1/project_topics#show", id: "1") end it "routes to #create" do - expect(post: "/api/v1/sign_up_topics").to route_to("api/v1/sign_up_topics#create") + expect(post: "/api/v1/project_topics").to route_to("api/v1/project_topics#create") end it "routes to #update via PUT" do - expect(put: "/api/v1/sign_up_topics/1").to route_to("api/v1/sign_up_topics#update", id: "1") + expect(put: "/api/v1/project_topics/1").to route_to("api/v1/project_topics#update", id: "1") end it "routes to #update via PATCH" do - expect(patch: "/api/v1/sign_up_topics/1").to route_to("api/v1/sign_up_topics#update", id: "1") + expect(patch: "/api/v1/project_topics/1").to route_to("api/v1/project_topics#update", id: "1") end it "routes to #destroy" do - expect(delete: "/api/v1/sign_up_topics/1").to route_to("api/v1/sign_up_topics#destroy", id: "1") + expect(delete: "/api/v1/project_topics/1").to route_to("api/v1/project_topics#destroy", id: "1") end end end \ No newline at end of file From 0e067e0d66f4df9b34718dcd8136489559276b32 Mon Sep 17 00:00:00 2001 From: smiti Date: Tue, 25 Mar 2025 01:41:31 +0530 Subject: [PATCH 09/13] fix the code --- app/models/signed_up_team.rb | 4 ++-- spec/models/project_topic_spec.rb | 19 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index d60a2a358..41b6f22ca 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -10,12 +10,12 @@ class SignedUpTeam < ApplicationRecord validates :team, presence: true, uniqueness: { scope: :project_topic } - def signup_for_topic(team, topic) + def self.signup_for_topic(team, topic) """Wrapper method to initiate team signup for a specific topic.""" topic.signup_team(team) end - def remove_team_signups(team) + def self.remove_team_signups(team) """Removes all topic associations for a team.""" team.signed_up_teams.includes(:project_topic).each do |sut| sut.project_topic.drop_team(team) diff --git a/spec/models/project_topic_spec.rb b/spec/models/project_topic_spec.rb index bbc398936..91e7cf43e 100644 --- a/spec/models/project_topic_spec.rb +++ b/spec/models/project_topic_spec.rb @@ -103,13 +103,10 @@ project_topic.signup_team(team) project_topic.signup_team(team2) project_topic.signup_team(team3) - expect(project_topic.confirmed_teams).to include(team) - expect(project_topic.confirmed_teams).to include(team2) - expect(project_topic.waitlisted_teams).to include(team3) - topic_teams = project_topic.get_signed_up_teams - expect(topic_teams).to include(team) - expect(topic_teams).to include(team2) - expect(topic_teams).to include(team3) + + # Get SignedUpTeam records instead of Team objects + topic_signups = project_topic.get_signed_up_teams + expect(topic_signups.pluck(:team_id)).to include(team.id, team2.id, team3.id) end end @@ -135,9 +132,11 @@ it 'returns waitlisted teams in order' do teams = 5.times.map { Team.create!(assignment: assignment) } teams.each { |t| project_topic.signup_team(t) } - expect(project_topic.waitlisted_teams.first).to eq([teams[2]]) - expect(project_topic.waitlisted_teams.second).to eq([teams[3]]) - expect(project_topic.waitlisted_teams.third).to eq([teams[4]]) + + # Get the first team from the relation + expect(project_topic.waitlisted_teams.first).to eq(teams[2]) + expect(project_topic.waitlisted_teams.second).to eq(teams[3]) + expect(project_topic.waitlisted_teams.third).to eq(teams[4]) end end end \ No newline at end of file From a351623bbced1a02d868223205f12342d4c95567 Mon Sep 17 00:00:00 2001 From: Adithya Srinivasan Date: Mon, 24 Mar 2025 17:05:06 -0400 Subject: [PATCH 10/13] Added JSON parsing in project_topic_controller_spec.rb --- spec/factories/assignments.rb | 1 + spec/factories/project_topics.rb | 9 +++++++++ .../api/v1/project_topic_controller_spec.rb | 16 +++++++++++++--- 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 spec/factories/project_topics.rb diff --git a/spec/factories/assignments.rb b/spec/factories/assignments.rb index 11788ce8c..5108cb443 100644 --- a/spec/factories/assignments.rb +++ b/spec/factories/assignments.rb @@ -2,6 +2,7 @@ FactoryBot.define do factory :assignment do sequence(:name) { |n| "Assignment #{n}" } + microtask { false } directory_path { "assignment_#{name.downcase.gsub(/\s+/, '_')}" } # Required associations diff --git a/spec/factories/project_topics.rb b/spec/factories/project_topics.rb new file mode 100644 index 000000000..2891f001b --- /dev/null +++ b/spec/factories/project_topics.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :project_topic do + sequence(:topic_name) { |n| "Topic #{n}" } + sequence(:topic_identifier) { |n| "T#{n}" } + max_choosers { 2 } + category { "General" } + association :assignment + end +end \ No newline at end of file diff --git a/spec/requests/api/v1/project_topic_controller_spec.rb b/spec/requests/api/v1/project_topic_controller_spec.rb index 4b978d221..b0272a24c 100644 --- a/spec/requests/api/v1/project_topic_controller_spec.rb +++ b/spec/requests/api/v1/project_topic_controller_spec.rb @@ -2,6 +2,12 @@ RSpec.describe 'ProjectTopicController API', type: :request do + def response_body + JSON.parse(response.body, symbolize_names: true) + rescue JSON::ParserError + {} + end + # GET /project_topics path '/api/v1/project_topics' do get('Get project topics') do @@ -37,8 +43,10 @@ it 'returns a list of all project topics with the given assignment_id' do expect(response).to have_http_status(200) - expect(response_body[:message]).to eq('All selected topics have been loaded successfully.') - expect(response_body[:project_topics].count).to eq(3) + #expect(response_body[:message]).to eq('All selected topics have been loaded successfully.') + #expect(response_body[:project_topics].count).to eq(3) + expect(response).to have_http_status(200) + expect(JSON.parse(response.body).length).to eq(3) end end @@ -99,7 +107,9 @@ it 'deletes all project topics with the given assignment_id' do expect(response).to have_http_status(200) - expect(response_body).to eq({ message: 'All project topics have been deleted successfully.' }) + #expect(response_body).to eq({ message: 'All project topics have been deleted successfully.' }) + expect(response).to have_http_status(:no_content) + expect(response.body).to eq("") expect(ProjectTopic.where(assignment_id: assignment_id)).to be_empty end end From 54656ae4a8d50f2ce6d7190ed8fbc4c2a47f2fb5 Mon Sep 17 00:00:00 2001 From: smiti Date: Wed, 16 Apr 2025 10:41:57 +0530 Subject: [PATCH 11/13] add new function in signed up team and its tests. --- .ruby-version | 2 +- app/models/signed_up_team.rb | 35 +++++++-- safe.log | 3 + spec/models/signed_up_team_spec.rb | 113 ++++++++++++++++++++++++++++- 4 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 safe.log diff --git a/.ruby-version b/.ruby-version index aa6fd8a3d..406ebcbd9 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-3.2.7 \ No newline at end of file +3.2.7 diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index 41b6f22ca..3293b170e 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -1,5 +1,5 @@ class SignedUpTeam < ApplicationRecord - """Scopes for filtering confirmed/waitlisted records""" + # Scopes for filtering confirmed/waitlisted records scope :confirmed, -> { where(is_waitlisted: false) } scope :waitlisted, -> { where(is_waitlisted: true) } @@ -7,18 +7,41 @@ class SignedUpTeam < ApplicationRecord belongs_to :team validates :project_topic, presence: true - validates :team, presence: true, - uniqueness: { scope: :project_topic } + validates :team, presence: true, + uniqueness: { scope: :project_topic } def self.signup_for_topic(team, topic) - """Wrapper method to initiate team signup for a specific topic.""" + # Wrapper method to initiate team signup for a specific topic topic.signup_team(team) end def self.remove_team_signups(team) - """Removes all topic associations for a team.""" + # Removes all topic associations for a team team.signed_up_teams.includes(:project_topic).each do |sut| sut.project_topic.drop_team(team) end end -end \ No newline at end of file + + def self.find_team_participants(team_id) + team = Team.find_by(id: team_id) + return [] unless team + + team.users.to_a + end + + def self.find_team_users(team_id) + signed_up_team = SignedUpTeam.find_by(team_id: team_id) + return [] unless signed_up_team + + signed_up_team.team.try(:users).to_a + end + + def self.find_user_signup_topics(user_id) + user = User.find_by(id: user_id) + return [] unless user + + ProjectTopic.joins(:signed_up_teams) + .where(signed_up_teams: { team_id: user.teams.pluck(:id) }) + .distinct.to_a + end +end diff --git a/safe.log b/safe.log new file mode 100644 index 000000000..b9b045a3c --- /dev/null +++ b/safe.log @@ -0,0 +1,3 @@ +2025-04-16T04:36:19.538819Z mysqld_safe Logging to '/var/log/mysql/error.log'. +2025-04-16T04:36:19.556429Z mysqld_safe Starting mysqld daemon with databases from /var/lib/mysql +2025-04-16T04:36:56.271171Z mysqld_safe mysqld from pid file /var/lib/mysql/DESKTOP-JM8MLSR.pid ended diff --git a/spec/models/signed_up_team_spec.rb b/spec/models/signed_up_team_spec.rb index 2bc32bf14..8933d42af 100644 --- a/spec/models/signed_up_team_spec.rb +++ b/spec/models/signed_up_team_spec.rb @@ -3,15 +3,18 @@ RSpec.describe SignedUpTeam, type: :model do let!(:role) { Role.create!(name: "Instructor") } + let!(:student_role) { Role.create!(name: "Student") } + let!(:instructor) do - Instructor.create!( + User.create!( name: "test_instructor", - password: "password", full_name: "Test Instructor", + password: "password", email: "instructor@example.com", role: role ) end + let!(:assignment) { Assignment.create!(name: "Test Assignment", instructor: instructor) } let!(:project_topic) { ProjectTopic.create!(topic_name: "Test Topic", assignment: assignment) } let!(:team) { Team.create!(assignment: assignment) } @@ -57,6 +60,12 @@ expect(result).to be true expect(project_topic).to have_received(:signup_team).with(team) end + + it 'returns false if topic rejects signup' do + allow(project_topic).to receive(:signup_team).with(team).and_return(false) + result = SignedUpTeam.signup_for_topic(team, project_topic) + expect(result).to be false + end end describe 'remove_team_signups' do @@ -64,10 +73,108 @@ let!(:topic2) { ProjectTopic.create!(topic_name: "Topic 2", assignment: assignment) } let!(:signup1) { SignedUpTeam.create!(project_topic: topic1, team: team) } let!(:signup2) { SignedUpTeam.create!(project_topic: topic2, team: team) } + it 'removes all team signups across topics' do expect { SignedUpTeam.remove_team_signups(team) }.to change(SignedUpTeam, :count).by(-2) end + + it 'does not raise error if team has no signups' do + new_team = Team.create!(assignment: assignment) + expect { SignedUpTeam.remove_team_signups(new_team) }.not_to raise_error + end + end + + describe 'custom methods' do + let!(:user1) do + User.create!( + name: "Alice", + full_name: "Alice Wonderland", + password: "password", + email: "alice@example.com", + role: student_role + ) + end + + let!(:user2) do + User.create!( + name: "Bob", + full_name: "Bob Builder", + password: "password", + email: "bob@example.com", + role: student_role + ) + end + + before do + team.users << [user1, user2] + end + + describe '.find_team_participants' do + it 'returns all users in a given team' do + participants = SignedUpTeam.find_team_participants(team.id) + expect(participants).to contain_exactly(user1, user2) + end + + it 'returns empty array if team does not exist' do + expect(SignedUpTeam.find_team_participants(-1)).to eq([]) + end + + it 'returns empty array when team exists but has no users' do + new_team = Team.create!(assignment: assignment) + expect(SignedUpTeam.find_team_participants(new_team.id)).to eq([]) + end + end + + describe '.find_team_users' do + let!(:sut) { SignedUpTeam.create!(project_topic: project_topic, team: team) } + + it 'returns users in the team that signed up' do + users = SignedUpTeam.find_team_users(team.id) + expect(users).to contain_exactly(user1, user2) + end + + it 'returns empty array if no signed up team found' do + new_team = Team.create!(assignment: assignment) + expect(SignedUpTeam.find_team_users(new_team.id)).to eq([]) + end + + it 'handles nil team_id gracefully' do + expect(SignedUpTeam.find_team_users(nil)).to eq([]) + end + end + + describe '.find_user_signup_topics' do + let!(:sut) { SignedUpTeam.create!(project_topic: project_topic, team: team) } + + it 'returns topics signed up by user’s team' do + topics = SignedUpTeam.find_user_signup_topics(user1.id) + expect(topics).to include(project_topic) + end + + it 'returns empty array if user has no teams or no signups' do + unknown = User.create!( + name: "Ghost", + full_name: "Ghost User", + password: "password", + email: "ghost@example.com", + role: student_role + ) + expect(SignedUpTeam.find_user_signup_topics(unknown.id)).to eq([]) + end + + it 'handles nil user_id gracefully' do + expect(SignedUpTeam.find_user_signup_topics(nil)).to eq([]) + end + + it 'handles user with multiple teams' do + team2 = Team.create!(assignment: assignment) + team2.users << user1 + SignedUpTeam.create!(project_topic: project_topic, team: team2) + topics = SignedUpTeam.find_user_signup_topics(user1.id) + expect(topics).to include(project_topic) + end + end end -end \ No newline at end of file +end From 615c2b75e0aa9abe9c85890fd377093728f31eda Mon Sep 17 00:00:00 2001 From: smiti Date: Thu, 17 Apr 2025 06:37:54 +0530 Subject: [PATCH 12/13] add new tests and comments for project_topic and signed_up_team. --- app/models/project_topic.rb | 35 +++-- app/models/signed_up_team.rb | 16 +- spec/models/project_topic_spec.rb | 231 ++++++++++++++++++++++------- spec/models/signed_up_team_spec.rb | 68 ++++++++- 4 files changed, 277 insertions(+), 73 deletions(-) diff --git a/app/models/project_topic.rb b/app/models/project_topic.rb index 89a3b79c6..3627be58b 100644 --- a/app/models/project_topic.rb +++ b/app/models/project_topic.rb @@ -2,12 +2,19 @@ class ProjectTopic < ApplicationRecord has_many :signed_up_teams, dependent: :destroy has_many :teams, through: :signed_up_teams belongs_to :assignment - validates :max_choosers, numericality: { - only_integer: true, - greater_than_or_equal_to: 0 + + # Ensures the number of max choosers is non-negative + validates :max_choosers, numericality: { + only_integer: true, + greater_than_or_equal_to: 0 } + + # Ensures topic name is present validates :topic_name, presence: true + # Attempts to sign up a team for this topic. + # If slots are available, it's confirmed; otherwise, waitlisted. + # Also removes any previous waitlist entries for the same team on other topics. def signup_team(team) return false if signed_up_teams.exists?(team: team) ActiveRecord::Base.transaction do @@ -22,6 +29,7 @@ def signup_team(team) false end + # Drops a team from this topic and promotes a waitlisted team if necessary. def drop_team(team) signed_up_team = signed_up_teams.find_by(team: team) return unless signed_up_team @@ -30,23 +38,28 @@ def drop_team(team) promote_waitlisted_team if team_confirmed end + # Returns the number of available slots left for this topic. def available_slots max_choosers - confirmed_teams_count end + # Checks if there are any open slots for this topic. def slot_available? available_slots.positive? end + # Returns all SignedUpTeam entries (both confirmed and waitlisted). def get_signed_up_teams signed_up_teams end + # Returns only confirmed teams associated with this topic. def confirmed_teams teams.joins(:signed_up_teams) .where(signed_up_teams: { is_waitlisted: false }) end + # Returns only waitlisted teams ordered by signup time (FIFO). def waitlisted_teams teams.joins(:signed_up_teams) .where(signed_up_teams: { is_waitlisted: true }) @@ -55,18 +68,22 @@ def waitlisted_teams private + # Returns the count of teams confirmed for this topic. def confirmed_teams_count signed_up_teams.confirmed.count end + # Promotes the earliest waitlisted team to confirmed. def promote_waitlisted_team - next_team = waitlisted_teams.first - return unless next_team - signed_up_teams.find_by(team: next_team)&.update!(is_waitlisted: false) - remove_from_waitlist(next_team) + next_signup = SignedUpTeam.where(project_topic_id: id, is_waitlisted: true).order(:created_at).first + return unless next_signup + + next_signup.update_column(:is_waitlisted, false) + remove_from_waitlist(next_signup.team) end + # Removes waitlist entries for the given team from all other topics. def remove_from_waitlist(team) - team.signed_up_teams.waitlisted.destroy_all + team.signed_up_teams.waitlisted.where.not(project_topic_id: id).destroy_all end -end \ No newline at end of file +end diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index 3293b170e..a68d08565 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -1,34 +1,39 @@ class SignedUpTeam < ApplicationRecord - # Scopes for filtering confirmed/waitlisted records + # Scope to return confirmed signups scope :confirmed, -> { where(is_waitlisted: false) } + + # Scope to return waitlisted signups scope :waitlisted, -> { where(is_waitlisted: true) } belongs_to :project_topic belongs_to :team + # Validations for presence and uniqueness of team-topic pairing validates :project_topic, presence: true validates :team, presence: true, uniqueness: { scope: :project_topic } + # Calls ProjectTopic's signup_team method to initiate signup def self.signup_for_topic(team, topic) - # Wrapper method to initiate team signup for a specific topic topic.signup_team(team) end + # Removes all signups (confirmed and waitlisted) for the given team def self.remove_team_signups(team) - # Removes all topic associations for a team team.signed_up_teams.includes(:project_topic).each do |sut| sut.project_topic.drop_team(team) end end + # Returns all users in a given team def self.find_team_participants(team_id) team = Team.find_by(id: team_id) return [] unless team - team.users.to_a + team.users.to_a end + # Returns users only if the team has signed up for a topic def self.find_team_users(team_id) signed_up_team = SignedUpTeam.find_by(team_id: team_id) return [] unless signed_up_team @@ -36,11 +41,12 @@ def self.find_team_users(team_id) signed_up_team.team.try(:users).to_a end + # Returns all project topics that any of the user's teams have signed up for def self.find_user_signup_topics(user_id) user = User.find_by(id: user_id) return [] unless user - ProjectTopic.joins(:signed_up_teams) + ProjectTopic.joins(:signed_up_teams) .where(signed_up_teams: { team_id: user.teams.pluck(:id) }) .distinct.to_a end diff --git a/spec/models/project_topic_spec.rb b/spec/models/project_topic_spec.rb index 91e7cf43e..524f560dc 100644 --- a/spec/models/project_topic_spec.rb +++ b/spec/models/project_topic_spec.rb @@ -1,8 +1,7 @@ -# spec/models/project_topic_spec.rb require 'rails_helper' RSpec.describe ProjectTopic, type: :model do - let!(:role) { Role.create!(name: "Instructor") } + let!(:role) { Role.find_or_create_by!(name: "Instructor") } let!(:instructor) do Instructor.create!( name: "test_instructor", @@ -19,11 +18,13 @@ describe '#signup_team' do context 'when slots are available' do it 'adds team as confirmed' do + # This test verifies that a team is added as confirmed when slots are available. expect(project_topic.signup_team(team)).to be true expect(project_topic.confirmed_teams).to include(team) end - it 'removes team from waitlist' do + it 'removes team from waitlist of other topics' do + # This checks that a team signing up for one topic is removed from the waitlists of other topics. other_topic = ProjectTopic.create!(topic_name: "Other Topic", assignment: assignment, max_choosers: 1) other_topic.signup_team(team) project_topic.signup_team(team) @@ -33,13 +34,12 @@ context 'when slots are full' do before do - 2.times do |n| - t = Team.create!(assignment: assignment) - project_topic.signup_team(t) - end + # Fill all slots before each test in this context. + 2.times { project_topic.signup_team(Team.create!(assignment: assignment)) } end it 'adds team to waitlist' do + # When no slots are available, the team should be added to the waitlist. new_team = Team.create!(assignment: assignment) expect(project_topic.signup_team(new_team)).to be true expect(project_topic.waitlisted_teams).to include(new_team) @@ -49,94 +49,211 @@ context 'when team already signed up' do before { project_topic.signup_team(team) } it 'returns false' do + # A team cannot sign up more than once. The method returns false if already signed up. expect(project_topic.signup_team(team)).to be false end end + + it 'does not raise exception when transaction fails' do + # This test simulates an error in ActiveRecord and ensures the method handles it gracefully. + allow(project_topic).to receive(:signed_up_teams).and_raise(ActiveRecord::RecordInvalid) + expect(project_topic.signup_team(team)).to be false + end end describe '#drop_team' do - before do - project_topic.signup_team(team) - project_topic.signup_team(Team.create!(assignment: assignment)) - end - - context 'when dropping confirmed team' do - it 'promotes waitlisted team' do - waitlisted_team = Team.create!(assignment: assignment) - waitlisted_team2 = Team.create!(assignment: assignment) - project_topic.signup_team(waitlisted_team) - project_topic.signup_team(waitlisted_team2) - expect { - project_topic.drop_team(team) - }.to change { project_topic.confirmed_teams.count }.by(0) - expect(waitlisted_team.reload.signed_up_teams.first.is_waitlisted).to be false - expect(project_topic.waitlisted_teams.first).to eq(waitlisted_team2) - end + before { project_topic.signup_team(team) } + + it 'returns nil if team is not signed up' do + # Verifies that dropping a team not signed up to the topic returns nil. + new_team = Team.create!(assignment: assignment) + expect(project_topic.drop_team(new_team)).to be_nil end - context 'when dropping waitlisted team' do - it 'does not promote other teams' do - waitlisted_team = Team.create!(assignment: assignment) - waitlisted_team2 = Team.create!(assignment: assignment) - project_topic.signup_team(waitlisted_team) - project_topic.signup_team(waitlisted_team2) - expect { - project_topic.drop_team(waitlisted_team) - }.not_to change { project_topic.confirmed_teams.count } - expect(project_topic.waitlisted_teams.first).to eq(waitlisted_team2) - end + it 'does not raise error for non-existent team' do + # Ensures dropping a non-existent team doesn't raise an exception. + phantom_team = double("Team", id: -1) + expect { project_topic.drop_team(phantom_team) }.not_to raise_error end end describe '#available_slots' do - it 'returns # of available slots correctly' do + it 'returns correct number of slots' do + # Confirms available_slots returns the correct number after signups. expect(project_topic.available_slots).to eq(2) project_topic.signup_team(team) expect(project_topic.available_slots).to eq(1) end + + it 'returns 0 when full' do + # Ensures it returns 0 when max_choosers is reached. + 2.times { project_topic.signup_team(Team.create!(assignment: assignment)) } + expect(project_topic.available_slots).to eq(0) + end end describe '#get_signed_up_teams' do - it 'returns confirmed and waitlisted teams' do - team2 = Team.create!(assignment: assignment) - team3 = Team.create!(assignment: assignment) - project_topic.signup_team(team) - project_topic.signup_team(team2) - project_topic.signup_team(team3) + it 'returns all signed up teams' do + # Checks that all teams, both confirmed and waitlisted, are returned. + teams = 3.times.map { Team.create!(assignment: assignment) } + teams.each { |t| project_topic.signup_team(t) } + expect(project_topic.get_signed_up_teams.pluck(:team_id)).to include(*teams.map(&:id)) + end - # Get SignedUpTeam records instead of Team objects - topic_signups = project_topic.get_signed_up_teams - expect(topic_signups.pluck(:team_id)).to include(team.id, team2.id, team3.id) + it 'returns only SignedUpTeam records' do + # Verifies that returned records are of the SignedUpTeam model. + team1 = Team.create!(assignment: assignment) + project_topic.signup_team(team1) + expect(project_topic.get_signed_up_teams.first).to be_a(SignedUpTeam) end end describe '#slot_available?' do - it 'returns true when slots available' do + it 'returns true when slots are available' do + # Confirms slot_available? returns true before topic is full. expect(project_topic.slot_available?).to be true end - it 'returns false when slots full' do - 2.times { |n| project_topic.signup_team(Team.create!(assignment: assignment)) } + it 'returns false when no slots are left' do + # Confirms slot_available? returns false once topic is full. + 2.times { project_topic.signup_team(Team.create!(assignment: assignment)) } expect(project_topic.slot_available?).to be false end end describe '#confirmed_teams' do - it 'returns confirmed teams' do + it 'returns only confirmed teams' do + # Verifies that confirmed_teams returns only those not waitlisted. project_topic.signup_team(team) - expect(project_topic.confirmed_teams).to include(team) + expect(project_topic.confirmed_teams).to contain_exactly(team) + end + + it 'returns empty array if no confirmed teams' do + # Returns an empty array when no confirmed signups exist. + expect(project_topic.confirmed_teams).to be_empty end end describe '#waitlisted_teams' do it 'returns waitlisted teams in order' do - teams = 5.times.map { Team.create!(assignment: assignment) } - teams.each { |t| project_topic.signup_team(t) } + # Ensures waitlisted teams are returned in the order they were added. + 5.times { project_topic.signup_team(Team.create!(assignment: assignment)) } + waitlisted = project_topic.waitlisted_teams + expect(waitlisted.size).to eq(3) + expect(waitlisted).to eq(waitlisted.sort_by(&:created_at)) + end + + it 'returns empty array if no waitlisted teams' do + # Returns an empty array when no teams are waitlisted. + expect(project_topic.waitlisted_teams).to eq([]) + end + end + + describe 'validations' do + it 'requires topic_name' do + # Validates presence of topic_name field. + topic = ProjectTopic.new(assignment: assignment, max_choosers: 1) + expect(topic).not_to be_valid + expect(topic.errors[:topic_name]).to include("can't be blank") + end + + it 'requires non-negative integer for max_choosers' do + # Validates that max_choosers is a non-negative number. + topic = ProjectTopic.new(topic_name: "Invalid", assignment: assignment, max_choosers: -1) + expect(topic).not_to be_valid + expect(topic.errors[:max_choosers]).to include("must be greater than or equal to 0") + end + end + + describe 'functional checks' do + it 'increases confirmed team count on signup' do + # Ensures that the count of confirmed teams increases after signup. + expect { project_topic.signup_team(team) }.to change { project_topic.confirmed_teams.count }.by(1) + end + + it 'does not allow more than max_choosers confirmed teams' do + # Confirms that additional teams beyond limit go to waitlist. + t1 = Team.create!(assignment: assignment) + t2 = Team.create!(assignment: assignment) + t3 = Team.create!(assignment: assignment) + project_topic.signup_team(t1) + project_topic.signup_team(t2) + project_topic.signup_team(t3) + expect(project_topic.confirmed_teams.count).to eq(2) + expect(project_topic.waitlisted_teams.count).to eq(1) + end + + it 'removes team’s other waitlisted entries on confirmed signup' do + # Ensures a confirmed team is removed from other topic waitlists. + t = Team.create!(assignment: assignment) + t1 = ProjectTopic.create!(topic_name: "Alt Topic", assignment: assignment, max_choosers: 0) + t1.signup_team(t) + expect(t1.waitlisted_teams).to include(t) + project_topic.signup_team(t) + expect(t1.reload.waitlisted_teams).not_to include(t) + end + + it 'get_signed_up_teams includes waitlisted and confirmed teams' do + # Validates that all signed-up teams, regardless of status, are returned. + t1 = Team.create!(assignment: assignment) + t2 = Team.create!(assignment: assignment) + project_topic.signup_team(t1) + project_topic.signup_team(t2) + expect(project_topic.get_signed_up_teams.map(&:team_id)).to include(t1.id, t2.id) + end + + it 'slot_available? reflects accurate state after signup and drop' do + # Checks dynamic behavior of slot availability after signup and drop. + t1 = Team.create!(assignment: assignment) + t2 = Team.create!(assignment: assignment) + project_topic.signup_team(t1) + project_topic.signup_team(t2) + expect(project_topic.slot_available?).to be false + project_topic.drop_team(t1) + expect(project_topic.slot_available?).to be true + end + + it 'signed_up_team records are removed when team is dropped' do + # Confirms that dropping a team deletes the associated record. + project_topic.signup_team(team) + expect { project_topic.drop_team(team) }.to change { SignedUpTeam.count }.by(-1) + end + + it 'multiple topics maintain independent signups' do + # Ensures that signups in one topic do not affect another topic. + topic2 = ProjectTopic.create!(topic_name: "Topic 2", assignment: assignment, max_choosers: 1) + team2 = Team.create!(assignment: assignment) + project_topic.signup_team(team) + topic2.signup_team(team2) + expect(project_topic.confirmed_teams).to include(team) + expect(topic2.confirmed_teams).to include(team2) + end + + it 'promotes the earliest waitlisted team after dropping a confirmed one' do + # Ensures that when a confirmed team is dropped, the earliest waitlisted is promoted. + t1 = Team.create!(assignment: assignment) + t2 = Team.create!(assignment: assignment) + t3 = Team.create!(assignment: assignment) + project_topic.signup_team(t1) + project_topic.signup_team(t2) + project_topic.signup_team(t3) + expect(project_topic.waitlisted_teams.first).to eq(t3) + project_topic.drop_team(t1) + expect(project_topic.confirmed_teams).to include(t2, t3) + expect(project_topic.waitlisted_teams).to be_empty + end - # Get the first team from the relation - expect(project_topic.waitlisted_teams.first).to eq(teams[2]) - expect(project_topic.waitlisted_teams.second).to eq(teams[3]) - expect(project_topic.waitlisted_teams.third).to eq(teams[4]) + it 'does not increase available slots after promoting a waitlisted team' do + # Verifies that slot count remains constant when a waitlisted team is promoted. + t1 = Team.create!(assignment: assignment) + t2 = Team.create!(assignment: assignment) + t3 = Team.create!(assignment: assignment) + project_topic.signup_team(t1) + project_topic.signup_team(t2) + project_topic.signup_team(t3) + expect(project_topic.available_slots).to eq(0) + project_topic.drop_team(t1) + expect(project_topic.available_slots).to eq(0) end end -end \ No newline at end of file +end diff --git a/spec/models/signed_up_team_spec.rb b/spec/models/signed_up_team_spec.rb index 8933d42af..5e59e1808 100644 --- a/spec/models/signed_up_team_spec.rb +++ b/spec/models/signed_up_team_spec.rb @@ -2,8 +2,9 @@ require 'rails_helper' RSpec.describe SignedUpTeam, type: :model do - let!(:role) { Role.create!(name: "Instructor") } - let!(:student_role) { Role.create!(name: "Student") } + # Setup roles, users, assignment, topic and team to be reused across tests + let!(:role) { Role.find_or_create_by!(name: "Instructor") } + let!(:student_role) { Role.find_or_create_by!(name: "Student") } let!(:instructor) do User.create!( @@ -20,18 +21,21 @@ let!(:team) { Team.create!(assignment: assignment) } describe 'validations' do + # Ensure a project_topic is mandatory it 'requires a project topic' do sut = SignedUpTeam.new(team: team) expect(sut).not_to be_valid expect(sut.errors[:project_topic]).to include("must exist") end + # Ensure a team is mandatory it 'requires a team' do sut = SignedUpTeam.new(project_topic: project_topic) expect(sut).not_to be_valid expect(sut.errors[:team]).to include("must exist") end + # Ensure uniqueness of team per project_topic it 'enforces unique team per project topic' do SignedUpTeam.create!(project_topic: project_topic, team: team) duplicate = SignedUpTeam.new(project_topic: project_topic, team: team) @@ -41,19 +45,23 @@ end describe 'scopes' do + # Create one confirmed and one waitlisted signup for testing scopes let!(:confirmed_signup) { SignedUpTeam.create!(project_topic: project_topic, team: team, is_waitlisted: false) } let!(:waitlisted_signup) { SignedUpTeam.create!(project_topic: project_topic, team: Team.create!(assignment: assignment), is_waitlisted: true) } + # Scope should only return confirmed signups it 'returns confirmed signups' do expect(SignedUpTeam.confirmed).to contain_exactly(confirmed_signup) end + # Scope should only return waitlisted signups it 'returns waitlisted signups' do expect(SignedUpTeam.waitlisted).to contain_exactly(waitlisted_signup) end end describe 'signup_for_topic' do + # Should delegate logic to ProjectTopic's signup_team it 'delegates to project topic signup' do allow(project_topic).to receive(:signup_team).with(team).and_return(true) result = SignedUpTeam.signup_for_topic(team, project_topic) @@ -61,6 +69,7 @@ expect(project_topic).to have_received(:signup_team).with(team) end + # Should return false if ProjectTopic rejects the signup it 'returns false if topic rejects signup' do allow(project_topic).to receive(:signup_team).with(team).and_return(false) result = SignedUpTeam.signup_for_topic(team, project_topic) @@ -69,17 +78,20 @@ end describe 'remove_team_signups' do + # Setup two topics and signups for deletion tests let!(:topic1) { ProjectTopic.create!(topic_name: "Topic 1", assignment: assignment) } let!(:topic2) { ProjectTopic.create!(topic_name: "Topic 2", assignment: assignment) } let!(:signup1) { SignedUpTeam.create!(project_topic: topic1, team: team) } let!(:signup2) { SignedUpTeam.create!(project_topic: topic2, team: team) } + # Should remove all signups for the team across all topics it 'removes all team signups across topics' do expect { SignedUpTeam.remove_team_signups(team) }.to change(SignedUpTeam, :count).by(-2) end + # Should not error if team has no signups it 'does not raise error if team has no signups' do new_team = Team.create!(assignment: assignment) expect { SignedUpTeam.remove_team_signups(new_team) }.not_to raise_error @@ -87,6 +99,7 @@ end describe 'custom methods' do + # Create test users to populate team let!(:user1) do User.create!( name: "Alice", @@ -112,15 +125,18 @@ end describe '.find_team_participants' do + # Should return users of a valid team it 'returns all users in a given team' do participants = SignedUpTeam.find_team_participants(team.id) expect(participants).to contain_exactly(user1, user2) end + # Invalid team_id should return [] it 'returns empty array if team does not exist' do expect(SignedUpTeam.find_team_participants(-1)).to eq([]) end + # Team with no users should return [] it 'returns empty array when team exists but has no users' do new_team = Team.create!(assignment: assignment) expect(SignedUpTeam.find_team_participants(new_team.id)).to eq([]) @@ -130,16 +146,19 @@ describe '.find_team_users' do let!(:sut) { SignedUpTeam.create!(project_topic: project_topic, team: team) } + # Should return users if team is signed up it 'returns users in the team that signed up' do users = SignedUpTeam.find_team_users(team.id) expect(users).to contain_exactly(user1, user2) end + # Should return [] if team is not signed up it 'returns empty array if no signed up team found' do new_team = Team.create!(assignment: assignment) expect(SignedUpTeam.find_team_users(new_team.id)).to eq([]) end + # Gracefully handle nil it 'handles nil team_id gracefully' do expect(SignedUpTeam.find_team_users(nil)).to eq([]) end @@ -148,11 +167,13 @@ describe '.find_user_signup_topics' do let!(:sut) { SignedUpTeam.create!(project_topic: project_topic, team: team) } + # Returns topics signed up by any of the user’s teams it 'returns topics signed up by user’s team' do topics = SignedUpTeam.find_user_signup_topics(user1.id) expect(topics).to include(project_topic) end + # Should return empty array for unknown user it 'returns empty array if user has no teams or no signups' do unknown = User.create!( name: "Ghost", @@ -164,10 +185,12 @@ expect(SignedUpTeam.find_user_signup_topics(unknown.id)).to eq([]) end + # Gracefully handle nil user_id it 'handles nil user_id gracefully' do expect(SignedUpTeam.find_user_signup_topics(nil)).to eq([]) end + # Should work even if user is part of multiple teams it 'handles user with multiple teams' do team2 = Team.create!(assignment: assignment) team2.users << user1 @@ -177,4 +200,45 @@ end end end + + describe 'functional behavior' do + # Should create a record on successful signup + it 'creates a record when signup_for_topic succeeds' do + expect { + SignedUpTeam.signup_for_topic(team, project_topic) + }.to change { SignedUpTeam.count }.by(1) + end + + # Should prevent duplicate signups + it 'does not create a duplicate signup' do + SignedUpTeam.signup_for_topic(team, project_topic) + expect { + SignedUpTeam.signup_for_topic(team, project_topic) + }.not_to change { SignedUpTeam.count } + end + + # Ensure cleanup removes all records + it 'remove_team_signups deletes all signups for team' do + topic1 = ProjectTopic.create!(topic_name: "Another Topic", assignment: assignment) + topic2 = ProjectTopic.create!(topic_name: "Third Topic", assignment: assignment) + SignedUpTeam.signup_for_topic(team, topic1) + SignedUpTeam.signup_for_topic(team, topic2) + + expect { + SignedUpTeam.remove_team_signups(team) + }.to change { SignedUpTeam.count }.by(-2) + end + + # Scopes should work correctly with multiple signups + it 'confirmed and waitlisted scopes handle multiple entries' do + confirmed = SignedUpTeam.create!(project_topic: project_topic, team: team, is_waitlisted: false) + waitlisted = SignedUpTeam.create!( + project_topic: ProjectTopic.create!(topic_name: "Waitlist Topic", assignment: assignment), + team: Team.create!(assignment: assignment), + is_waitlisted: true + ) + expect(SignedUpTeam.confirmed).to include(confirmed) + expect(SignedUpTeam.waitlisted).to include(waitlisted) + end + end end From f608218a1f7df58eb31ab452e0b55d99c7b778b2 Mon Sep 17 00:00:00 2001 From: dmpatel3 Date: Wed, 16 Apr 2025 22:13:20 -0400 Subject: [PATCH 13/13] Renamed find_team_users() to find_project_topic_team_users() and adjusted SignedUpTeam tests accordingly --- app/models/signed_up_team.rb | 8 +++---- spec/models/signed_up_team_spec.rb | 35 +++++++++++------------------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index a68d08565..ebb5133d8 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -33,16 +33,16 @@ def self.find_team_participants(team_id) team.users.to_a end - # Returns users only if the team has signed up for a topic - def self.find_team_users(team_id) + # Returns all users in a given team that's signed up for a topic + def self.find_project_topic_team_users(team_id) signed_up_team = SignedUpTeam.find_by(team_id: team_id) return [] unless signed_up_team signed_up_team.team.try(:users).to_a end - # Returns all project topics that any of the user's teams have signed up for - def self.find_user_signup_topics(user_id) + # Returns project topic the given user signed up for + def self.find_user_project_topic(user_id) user = User.find_by(id: user_id) return [] unless user diff --git a/spec/models/signed_up_team_spec.rb b/spec/models/signed_up_team_spec.rb index 5e59e1808..53c02c813 100644 --- a/spec/models/signed_up_team_spec.rb +++ b/spec/models/signed_up_team_spec.rb @@ -143,33 +143,33 @@ end end - describe '.find_team_users' do + describe '.find_project_topic_team_users' do let!(:sut) { SignedUpTeam.create!(project_topic: project_topic, team: team) } - # Should return users if team is signed up - it 'returns users in the team that signed up' do - users = SignedUpTeam.find_team_users(team.id) + # Should return users of a team signed up for a topic + it 'returns all users in a team that signed up for a topic' do + users = SignedUpTeam.find_project_topic_team_users(team.id) expect(users).to contain_exactly(user1, user2) end - # Should return [] if team is not signed up + # Should return [] if team is not signed up to a topic it 'returns empty array if no signed up team found' do new_team = Team.create!(assignment: assignment) - expect(SignedUpTeam.find_team_users(new_team.id)).to eq([]) + expect(SignedUpTeam.find_project_topic_team_users(new_team.id)).to eq([]) end # Gracefully handle nil it 'handles nil team_id gracefully' do - expect(SignedUpTeam.find_team_users(nil)).to eq([]) + expect(SignedUpTeam.find_project_topic_team_users(nil)).to eq([]) end end - describe '.find_user_signup_topics' do + describe '.find_user_project_topic' do let!(:sut) { SignedUpTeam.create!(project_topic: project_topic, team: team) } - # Returns topics signed up by any of the user’s teams - it 'returns topics signed up by user’s team' do - topics = SignedUpTeam.find_user_signup_topics(user1.id) + # Returns project topic the user signed up for + it 'returns project topic signed up by user' do + topics = SignedUpTeam.find_user_project_topic(user1.id) expect(topics).to include(project_topic) end @@ -182,21 +182,12 @@ email: "ghost@example.com", role: student_role ) - expect(SignedUpTeam.find_user_signup_topics(unknown.id)).to eq([]) + expect(SignedUpTeam.find_user_project_topic(unknown.id)).to eq([]) end # Gracefully handle nil user_id it 'handles nil user_id gracefully' do - expect(SignedUpTeam.find_user_signup_topics(nil)).to eq([]) - end - - # Should work even if user is part of multiple teams - it 'handles user with multiple teams' do - team2 = Team.create!(assignment: assignment) - team2.users << user1 - SignedUpTeam.create!(project_topic: project_topic, team: team2) - topics = SignedUpTeam.find_user_signup_topics(user1.id) - expect(topics).to include(project_topic) + expect(SignedUpTeam.find_user_project_topic(nil)).to eq([]) end end end