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/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 e4e3b44fe..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: 'SignUpTopic', 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 @@ -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/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/project_topic.rb b/app/models/project_topic.rb new file mode 100644 index 000000000..3627be58b --- /dev/null +++ b/app/models/project_topic.rb @@ -0,0 +1,89 @@ +class ProjectTopic < ApplicationRecord + has_many :signed_up_teams, dependent: :destroy + has_many :teams, through: :signed_up_teams + belongs_to :assignment + + # 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 + signed_up_team = signed_up_teams.create!( + team: team, + is_waitlisted: !slot_available? + ) + remove_from_waitlist(team) unless signed_up_team.is_waitlisted? + true + end + rescue ActiveRecord::RecordInvalid + 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 + team_confirmed = !signed_up_team.is_waitlisted? + signed_up_team.destroy! + 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 }) + .order('signed_up_teams.created_at ASC') + end + + 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_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.where.not(project_topic_id: id).destroy_all + end +end diff --git a/app/models/sign_up_topic.rb b/app/models/sign_up_topic.rb deleted file mode 100644 index 641fdc9b1..000000000 --- a/app/models/sign_up_topic.rb +++ /dev/null @@ -1,7 +0,0 @@ -class SignUpTopic < 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 - belongs_to :assignment -end diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index 39b1e2de4..ebb5133d8 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -1,4 +1,53 @@ class SignedUpTeam < ApplicationRecord - belongs_to :sign_up_topic + # 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) + topic.signup_team(team) + end + + # Removes all signups (confirmed and waitlisted) for the given team + def self.remove_team_signups(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 + end + + # 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 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 + + ProjectTopic.joins(:signed_up_teams) + .where(signed_up_teams: { team_id: user.teams.pluck(:id) }) + .distinct.to_a + end end 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/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/config/routes.rb b/config/routes.rb index e5d805c4f..986274166 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -92,10 +92,10 @@ - resources :sign_up_topics do + resources :project_topics do collection do get :filter - delete '/', to: 'sign_up_topics#destroy' + delete '/', to: 'project_topics#destroy' end end 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/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 7db16863e..e18179f93 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_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" @@ -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,30 +323,14 @@ 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 "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 @@ -398,17 +398,7 @@ 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", "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/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/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/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/models/project_topic_spec.rb b/spec/models/project_topic_spec.rb new file mode 100644 index 000000000..524f560dc --- /dev/null +++ b/spec/models/project_topic_spec.rb @@ -0,0 +1,259 @@ +require 'rails_helper' + +RSpec.describe ProjectTopic, type: :model do + let!(:role) { Role.find_or_create_by!(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 + # 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 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) + expect(other_topic.reload.waitlisted_teams).not_to include(team) + end + end + + context 'when slots are full' do + before do + # 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) + end + end + + 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 { 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 + + 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 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 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 + + 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 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 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 only confirmed teams' do + # Verifies that confirmed_teams returns only those not waitlisted. + project_topic.signup_team(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 + # 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 + + 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 diff --git a/spec/models/signed_up_team_spec.rb b/spec/models/signed_up_team_spec.rb new file mode 100644 index 000000000..53c02c813 --- /dev/null +++ b/spec/models/signed_up_team_spec.rb @@ -0,0 +1,235 @@ +# spec/models/signed_up_team_spec.rb +require 'rails_helper' + +RSpec.describe SignedUpTeam, type: :model do + # 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!( + name: "test_instructor", + 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) } + + 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) + expect(duplicate).not_to be_valid + expect(duplicate.errors[:team]).to include("has already been taken") + end + 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) + expect(result).to be true + 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) + expect(result).to be false + end + 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 + end + end + + describe 'custom methods' do + # Create test users to populate team + 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 + # 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([]) + end + end + + describe '.find_project_topic_team_users' do + let!(:sut) { SignedUpTeam.create!(project_topic: project_topic, team: team) } + + # 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 to a topic + it 'returns empty array if no signed up team found' do + new_team = Team.create!(assignment: assignment) + 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_project_topic_team_users(nil)).to eq([]) + end + end + + describe '.find_user_project_topic' do + let!(:sut) { SignedUpTeam.create!(project_topic: project_topic, team: team) } + + # 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 + + # 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", + full_name: "Ghost User", + password: "password", + email: "ghost@example.com", + role: student_role + ) + 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_project_topic(nil)).to eq([]) + 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 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 65% 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..b0272a24c 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,20 @@ 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 + 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 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 +25,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 +35,32 @@ # 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[: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 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 +71,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 +91,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 +103,28 @@ 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(response).to have_http_status(:no_content) + expect(response.body).to eq("") + 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 +132,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 +168,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 +190,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 +201,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 +260,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 +274,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 +284,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 +293,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/project_topics_routing_spec.rb b/spec/routing/project_topics_routing_spec.rb new file mode 100644 index 000000000..6130c9984 --- /dev/null +++ b/spec/routing/project_topics_routing_spec.rb @@ -0,0 +1,29 @@ +require "rails_helper" + +RSpec.describe Api::V1::ProjectTopicsController, type: :routing do + describe "routing" do + it "routes to #index" do + expect(get: "/api/v1/project_topics").to route_to("api/v1/project_topics#index") + end + + it "routes to #show" do + 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/project_topics").to route_to("api/v1/project_topics#create") + end + + it "routes to #update via PUT" do + 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/project_topics/1").to route_to("api/v1/project_topics#update", id: "1") + end + + it "routes to #destroy" do + 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 diff --git a/spec/routing/sign_up_topics_routing_spec.rb b/spec/routing/sign_up_topics_routing_spec.rb deleted file mode 100644 index bf7cfa236..000000000 --- a/spec/routing/sign_up_topics_routing_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require "rails_helper" - -RSpec.describe Api::V1::SignUpTopicsController, 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") - 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") - end - - it "routes to #create" do - expect(post: "/api/v1/sign_up_topics").to route_to("api/v1/sign_up_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") - 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") - 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") - end - end -end \ No newline at end of file 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