diff --git a/.gitignore b/.gitignore index b5973ad4c..1eadeda8c 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,8 @@ coverage/ rsa_keys.yml pg_data/ + +# Other +setup.sh +Gemfile.lock +Gemfile \ No newline at end of file diff --git a/Gemfile b/Gemfile index 540f19cb5..8d405f34c 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '3.4.5' gem 'mysql2', '~> 0.5.7' +gem 'redis', '~> 5.0' gem 'sqlite3', '~> 1.4' # Alternative for development gem 'puma', '~> 6.4' gem 'rails', '~> 8.0', '>= 8.0.1' @@ -57,13 +58,10 @@ gem 'find_with_order' # For handling zip file uploads and extraction gem 'rubyzip' +gem 'faker' group :development, :test do - gem 'debug', platforms: %i[mri mingw x64_mingw] - gem 'factory_bot_rails' - gem 'database_cleaner-active_record' - gem 'faker' gem 'rspec-rails' gem 'rswag-specs' gem 'rubocop' diff --git a/Gemfile.lock b/Gemfile.lock index aac9f8c1a..6ef9c0ac2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -121,14 +121,7 @@ GEM octokit (>= 4.0) pstore (~> 0.1) terminal-table (>= 1, < 5) - database_cleaner-active_record (2.2.2) - activerecord (>= 5.a) - database_cleaner-core (~> 2.0) - database_cleaner-core (2.0.1) date (3.4.1) - debug (1.11.0) - irb (~> 1.10) - reline (>= 0.3.8) delegate (0.4.0) diff-lcs (1.6.2) docile (1.4.1) @@ -136,11 +129,6 @@ GEM drb (2.2.3) erb (5.0.2) erubi (1.13.1) - factory_bot (6.5.5) - activesupport (>= 6.1.0) - factory_bot_rails (6.5.1) - factory_bot (~> 6.5) - railties (>= 6.1.0) faker (3.5.2) i18n (>= 1.8.11, < 2) faraday (2.14.0) @@ -225,6 +213,9 @@ GEM net-protocol netrc (0.11.0) nio4r (2.5.9) + nokogiri (1.18.10) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) nokogiri (1.18.10-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.18.10-arm64-darwin) @@ -303,6 +294,10 @@ GEM rdoc (6.14.2) erb psych (>= 4.0.0) + redis (5.4.1) + redis-client (>= 0.22.0) + redis-client (0.28.0) + connection_pool regexp_parser (2.11.3) reline (0.6.2) io-console (~> 0.5) @@ -407,6 +402,7 @@ PLATFORMS arm64-darwin-23 arm64-darwin-24 arm64-darwin-25 + ruby x64-mingw-ucrt x86_64-linux @@ -418,11 +414,8 @@ DEPENDENCIES coveralls csv danger - database_cleaner-active_record date - debug delegate - factory_bot_rails faker faraday-retry find_with_order @@ -440,6 +433,7 @@ DEPENDENCIES puma (~> 6.4) rack-cors rails (~> 8.0, >= 8.0.1) + redis (~> 5.0) rspec-rails rswag-api rswag-specs diff --git a/app/controllers/assignments_duties_controller.rb b/app/controllers/assignments_duties_controller.rb index e72a69f6f..4fe55cfb2 100644 --- a/app/controllers/assignments_duties_controller.rb +++ b/app/controllers/assignments_duties_controller.rb @@ -1,31 +1,78 @@ class AssignmentsDutiesController < ApplicationController include Authorization #before_action :authenticate_user! - before_action :action_allowed!, only: [:create, :destroy] + before_action :action_allowed!, only: [:create, :destroy, :update_limit] + before_action :set_assignment + before_action :set_assignment_duty, only: [:update_limit] + # GET /assignments/:assignment_id/duties def index - assignment = Assignment.find(params[:assignment_id]) - render json: assignment.duties + render json: serialized_assignment_duties end # POST /assignments/:assignment_id/duties def create - assignment = Assignment.find(params[:assignment_id]) duty = Duty.find(params[:duty_id]) - assignment.duties << duty unless assignment.duties.include?(duty) - render json: assignment.duties + assignment_duty = @assignment.assignments_duties.find_or_initialize_by(duty_id: duty.id) + assignment_duty.max_members_for_duty ||= 1 + + if assignment_duty.save + render json: serialized_assignment_duties, status: :ok + else + render json: assignment_duty.errors, status: :unprocessable_entity + end end # DELETE /assignments/:assignment_id/duties/:id def destroy - assignment = Assignment.find(params[:assignment_id]) duty = Duty.find(params[:id]) - assignment.duties.delete(duty) + @assignment.duties.delete(duty) head :no_content end + # PATCH /assignments/:assignment_id/duties/:id/limit + # Set or update the maximum number of team members allowed to have this duty (role) for a given assignment. + # Request body: { max_members_for_duty: integer } + # Response: The updated duty mapping with the new limit, or validation errors if invalid. + def update_limit + if @assignment_duty.update(limit_params) + render json: serialize_assignment_duty(@assignment_duty), status: :ok + else + render json: @assignment_duty.errors, status: :unprocessable_entity + end + end + private + def set_assignment + @assignment = Assignment.find(params[:assignment_id]) + end + + def set_assignment_duty + @assignment_duty = @assignment.assignments_duties.find_by(duty_id: params[:id]) + return if @assignment_duty + + render json: { error: 'Duty is not assigned to this assignment' }, status: :not_found + end + + def limit_params + params.permit(:max_members_for_duty) + end + + def serialized_assignment_duties + @assignment.assignments_duties.includes(:duty).map do |assignment_duty| + serialize_assignment_duty(assignment_duty) + end + end + + def serialize_assignment_duty(assignment_duty) + { + duty_id: assignment_duty.duty_id, + duty_name: assignment_duty.duty&.name, + max_members_for_duty: assignment_duty.max_members_for_duty + } + end + def action_allowed! unless current_user_has_instructor_privileges? render json: { error: 'Not authorized' }, status: :forbidden diff --git a/app/controllers/participants_controller.rb b/app/controllers/participants_controller.rb index 607faa015..5aad3547e 100644 --- a/app/controllers/participants_controller.rb +++ b/app/controllers/participants_controller.rb @@ -46,6 +46,8 @@ def show end end + + # Assign the specified authorization to the participant and add them to an assignment # POST /participants/:authorization def add @@ -98,6 +100,20 @@ def update_authorization end end + # Update the specified participant duty + # PATCH /participants/:id/duty + def update_duty + participant = find_participant + return unless participant + + participant.duty_id = params[:duty_id].presence + if participant.save + render json: participant, status: :created + else + render json: participant.errors, status: :unprocessable_entity + end + end + # Delete a participant # params - id # DELETE /participants/:id @@ -189,4 +205,4 @@ def validate_authorization authorization end -end \ No newline at end of file +end diff --git a/app/controllers/teams_participants_controller.rb b/app/controllers/teams_participants_controller.rb index c34e3f34d..37fe39acb 100644 --- a/app/controllers/teams_participants_controller.rb +++ b/app/controllers/teams_participants_controller.rb @@ -24,8 +24,30 @@ def update_duty end duty_id = params.dig(:teams_participant, :duty_id) || params.dig("teams_participant", "duty_id") - team_participant.update(duty_id: duty_id) - render json: { message: "Duty updated successfully" }, status: :ok + participant = team_participant.participant + + # For assignment teams, derive the assignment from the team, not from participant.parent_id. + # This controller also serves CourseTeam, so participant.parent_id is not always an assignment id. + # Gate this action to assignment teams and resolve the assignment from team_participant.team. + assignment = nil + if team_participant.team.type == "AssignmentTeam" + assignment = team_participant.team.assignment + else + render json: { error: 'Duty assignment is only supported for assignment teams' }, status: :unprocessable_entity and return + end + if duty_id.present? + assignment_duty = AssignmentsDuty.find_by(assignment_id: assignment.id, duty_id: duty_id) + unless assignment_duty + render json: { error: 'Duty is not assigned to this assignment' }, status: :unprocessable_entity and return + end + end + + participant.duty_id = duty_id + if participant.save + render json: { message: 'Duty updated successfully' }, status: :ok + else + render json: { error: participant.errors.full_messages.to_sentence }, status: :unprocessable_entity + end end # Displays a list of all participants in a specific team. diff --git a/app/models/assignment_questionnaire.rb b/app/models/assignment_questionnaire.rb index 87a55883d..6c3175dcc 100644 --- a/app/models/assignment_questionnaire.rb +++ b/app/models/assignment_questionnaire.rb @@ -3,4 +3,6 @@ class AssignmentQuestionnaire < ApplicationRecord belongs_to :assignment belongs_to :questionnaire + belongs_to :duty, optional: true end + diff --git a/app/models/assignments_duty.rb b/app/models/assignments_duty.rb index eeb3519b0..c8045d63a 100644 --- a/app/models/assignments_duty.rb +++ b/app/models/assignments_duty.rb @@ -1,4 +1,6 @@ class AssignmentsDuty < ApplicationRecord belongs_to :assignment belongs_to :duty + + validates :max_members_for_duty, numericality: { only_integer: true, greater_than_or_equal_to: 1 } end \ No newline at end of file diff --git a/app/models/participant.rb b/app/models/participant.rb index c1e0973b7..6eed34998 100644 --- a/app/models/participant.rb +++ b/app/models/participant.rb @@ -1,23 +1,20 @@ # frozen_string_literal: true class Participant < ApplicationRecord - # Associations belongs_to :user has_many :join_team_requests, dependent: :destroy - # belongs_to :team, optional: true belongs_to :assignment, class_name: 'Assignment', foreign_key: 'parent_id', optional: true, inverse_of: :participants belongs_to :course, class_name: 'Course', foreign_key: 'parent_id', optional: true, inverse_of: :participants + belongs_to :duty, optional: true - # Validations validates :user_id, presence: true validates :parent_id, presence: true validates :type, presence: true, inclusion: { in: %w[AssignmentParticipant CourseParticipant], message: "must be either 'AssignmentParticipant' or 'CourseParticipant'" } + validate :duty_limit_within_team, if: -> { duty_id.present? && team.present? } def retract_sent_invitations - # do nothing. Invitations are only sent by AssignmentParticipants only. end - # Methods def fullname user.full_name end @@ -26,4 +23,20 @@ def team TeamsParticipant.where(participant: self).first&.team end + private + + def duty_limit_within_team + assignment_duty = AssignmentsDuty.find_by(assignment_id: parent_id, duty_id: duty_id) + limit = assignment_duty&.max_members_for_duty || 1 + + count = TeamsParticipant.where(team_id: team.id) + .joins("INNER JOIN participants ON teams_participants.participant_id = participants.id") + .where(participants: { duty_id: duty_id }) + .where.not(participants: { id: id }) + .count + + if count >= limit + errors.add(:duty, "limit reached for this team. Only #{limit} member(s) can be a #{duty.name}.") + end + end end \ No newline at end of file diff --git a/app/serializers/assignment_serializer.rb b/app/serializers/assignment_serializer.rb index 7124e4bf5..84fe14bed 100644 --- a/app/serializers/assignment_serializer.rb +++ b/app/serializers/assignment_serializer.rb @@ -1,3 +1,17 @@ class AssignmentSerializer < ActiveModel::Serializer - attributes :id, :name, :max_team_size, :course_id + attributes :id, :name, :max_team_size, :course_id, :has_role_based_review, :assignment_duties + + def has_role_based_review + object.assignments_duties.exists? + end + + def assignment_duties + object.assignments_duties.includes(:duty).map do |assignment_duty| + { + duty_id: assignment_duty.duty_id, + duty_name: assignment_duty.duty&.name, + max_members_for_duty: assignment_duty.max_members_for_duty + } + end + end end diff --git a/app/serializers/participant_serializer.rb b/app/serializers/participant_serializer.rb index 611a59982..4d63c7f6b 100644 --- a/app/serializers/participant_serializer.rb +++ b/app/serializers/participant_serializer.rb @@ -1,5 +1,5 @@ class ParticipantSerializer < ActiveModel::Serializer - attributes :id, :user, :parent_id, :user_id, :authorization + attributes :id, :user, :parent_id, :user_id, :authorization, :duty_id, :duty_name def user { @@ -9,4 +9,8 @@ def user fullName: object.user.full_name } end + + def duty_name + object.duty&.name + end end diff --git a/config/application.rb b/config/application.rb index 798f8702b..5b9263961 100644 --- a/config/application.rb +++ b/config/application.rb @@ -17,7 +17,7 @@ def self.preview_path=(_) module Reimplementation class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.0 + config.load_defaults 8.0 config.active_record.schema_format = :ruby # Configuration for the application, engines, and railties goes here. @@ -32,6 +32,7 @@ class Application < Rails::Application # Middleware like session, flash, cookies can be added back manually. # Skip views, helpers and assets when generating a new resource. config.api_only = true - config.cache_store = :redis_store, ENV['CACHE_STORE'], { expires_in: 3.days, raise_errors: false } + config.cache_store = :redis_cache_store, { url: ENV['CACHE_STORE'], expires_in: 3.days } end end + diff --git a/config/routes.rb b/config/routes.rb index 57559d007..6d9f00f8b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -154,16 +154,21 @@ end end - resources :participants do - collection do - get '/user/:user_id', to: 'participants#list_user_participants' - get '/assignment/:assignment_id', to: 'participants#list_assignment_participants' - get '/:id', to: 'participants#show' - post '/:authorization', to: 'participants#add' - patch '/:id/:authorization', to: 'participants#update_authorization' - delete '/:id', to: 'participants#destroy' - end - end + resources :participants do + collection do + get '/user/:user_id', to: 'participants#list_user_participants' + get '/assignment/:assignment_id', to: 'participants#list_assignment_participants' + get '/:id', to: 'participants#show' + post '/:authorization', to: 'participants#add' + patch '/:id/:authorization', + to: 'participants#update_authorization', + constraints: { authorization: /reader|reviewer|submitter|mentor/i } + delete '/:id', to: 'participants#destroy' + end + member do + patch :duty, to: 'participants#update_duty' + end + end resources :student_teams, only: %i[create update] do collection do @@ -212,6 +217,10 @@ end end resources :assignments do - resources :duties, controller: 'assignments_duties', only: [:index, :create, :destroy] + resources :duties, controller: 'assignments_duties', only: [:index, :create, :destroy] do + member do + patch :limit, action: :update_limit + end + end end end diff --git a/db/migrate/20260423202332_adjust_duty_limits.rb b/db/migrate/20260423202332_adjust_duty_limits.rb new file mode 100644 index 000000000..9eda5a297 --- /dev/null +++ b/db/migrate/20260423202332_adjust_duty_limits.rb @@ -0,0 +1,6 @@ +class AdjustDutyLimits < ActiveRecord::Migration[8.0] + def change + remove_column :duties, :max_members_for_duty, :integer + add_column :assignments_duties, :max_members_for_duty, :integer, default: 1 + end +end diff --git a/db/migrate/20260423210000_add_duty_id_to_assignment_questionnaires.rb b/db/migrate/20260423210000_add_duty_id_to_assignment_questionnaires.rb new file mode 100644 index 000000000..4ff132f32 --- /dev/null +++ b/db/migrate/20260423210000_add_duty_id_to_assignment_questionnaires.rb @@ -0,0 +1,5 @@ +class AddDutyIdToAssignmentQuestionnaires < ActiveRecord::Migration[8.0] + def change + add_column :assignment_questionnaires, :duty_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index cddbe12c6..423e32594 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: 2026_03_13_064334) do +ActiveRecord::Schema[8.0].define(version: 2026_04_23_210000) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -44,6 +44,7 @@ t.datetime "updated_at", null: false t.integer "used_in_round" t.integer "questionnaire_weight" + t.integer "duty_id" t.index ["assignment_id"], name: "fk_aq_assignments_id" t.index ["questionnaire_id"], name: "fk_aq_questionnaire_id" end @@ -113,6 +114,7 @@ t.bigint "duty_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "max_members_for_duty", default: 1 t.index ["assignment_id"], name: "index_assignments_duties_on_assignment_id" t.index ["duty_id"], name: "index_assignments_duties_on_duty_id" end @@ -135,11 +137,6 @@ t.datetime "updated_at", null: false end - create_table "cakes", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - create_table "courses", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name" t.string "directory_path" @@ -184,7 +181,6 @@ t.bigint "instructor_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.integer "max_members_for_duty" t.index ["instructor_id"], name: "index_duties_on_instructor_id" end @@ -229,7 +225,7 @@ t.integer "participant_id" t.integer "team_id" t.text "comments" - t.string "reply_status" + t.string "status" end create_table "nodes", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -279,6 +275,7 @@ 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 @@ -330,7 +327,6 @@ t.integer "reviewee_id", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "type" t.index ["reviewer_id"], name: "fk_response_map_reviewer" end @@ -417,6 +413,7 @@ t.bigint "user_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["team_id", "user_id"], name: "index_teams_users_on_team_id_and_user_id", unique: true t.index ["team_id"], name: "index_teams_users_on_team_id" t.index ["user_id"], name: "index_teams_users_on_user_id" end @@ -456,8 +453,6 @@ add_foreign_key "assignments_duties", "duties" add_foreign_key "courses", "institutions" add_foreign_key "courses", "users", column: "instructor_id" - add_foreign_key "invitations", "participants", column: "from_id" - add_foreign_key "invitations", "participants", column: "to_id" add_foreign_key "duties", "users", column: "instructor_id" add_foreign_key "items", "questionnaires" add_foreign_key "participants", "duties" diff --git a/docker-compose.yml b/docker-compose.yml index f22dc27ef..2413a65fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: command: tail -f /dev/null environment: RAILS_ENV: development - DATABASE_URL: mysql2://root:expertiza@db:3306/reimplementation? + DATABASE_URL: mysql2://root:expertiza@db:3306/reimplementation CACHE_STORE: redis://redis:6380/0 ports: - "3002:3002" diff --git a/setup.sh b/setup.sh index c10d11f6f..c30600d8b 100755 --- a/setup.sh +++ b/setup.sh @@ -12,6 +12,9 @@ rm -f /app/tmp/pids/server.pid echo "Step 2: Bundling dependencies..." bundle install + +#echo "Debug Step: Drop schema and reset database" +#rake db:drop echo "Step 3: Creating the database..." rake db:create @@ -19,7 +22,7 @@ rake db:create echo "Step 4: Running database migrations..." rake db:migrate -echo "Step 5: Seeding the database..." +echo "Step 5: Seeding the database..." rake db:seed echo "Step 6: Starting the Rails server..." diff --git a/spec/requests/api/v1/assignments_duties_controller_spec.rb b/spec/requests/api/v1/assignments_duties_controller_spec.rb new file mode 100644 index 000000000..5e485f378 --- /dev/null +++ b/spec/requests/api/v1/assignments_duties_controller_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'json_web_token' + +RSpec.describe 'Assignments duties API', type: :request do + let!(:instructor_role) { Role.find_or_create_by!(name: 'Instructor') } + let!(:student_role) { Role.find_or_create_by!(name: 'Student') } + let!(:institution) { Institution.create!(name: 'NC State') } + + let!(:instructor) do + User.create!( + name: 'instructor_user', + email: 'instructor@example.com', + password: 'password123', + full_name: 'Instructor User', + role_id: instructor_role.id, + institution_id: institution.id + ) + end + + let!(:student) do + User.create!( + name: 'student_user', + email: 'student@example.com', + password: 'password123', + full_name: 'Student User', + role_id: student_role.id, + institution_id: institution.id + ) + end + + let!(:assignment) { Assignment.create!(name: 'Assignment A', instructor_id: instructor.id) } + let!(:duty) { Duty.create!(name: 'Reviewer Duty', instructor_id: instructor.id) } + let!(:assignment_duty) do + AssignmentsDuty.create!(assignment_id: assignment.id, duty_id: duty.id, max_members_for_duty: 2) + end + + let(:instructor_headers) do + { + 'Authorization' => "Bearer #{JsonWebToken.encode({ id: instructor.id })}", + 'Content-Type' => 'application/json' + } + end + + let(:student_headers) do + { + 'Authorization' => "Bearer #{JsonWebToken.encode({ id: student.id })}", + 'Content-Type' => 'application/json' + } + end + + describe 'GET /assignments/:id' do + it 'includes assignment duties and role-based review flag' do + get "/assignments/#{assignment.id}", headers: instructor_headers + + expect(response).to have_http_status(:ok) + body = JSON.parse(response.body) + expect(body['has_role_based_review']).to eq(true) + expect(body['assignment_duties']).to be_an(Array) + expect(body['assignment_duties'].first['duty_id']).to eq(duty.id) + expect(body['assignment_duties'].first['max_members_for_duty']).to eq(2) + end + end + + describe 'PATCH /assignments/:assignment_id/duties/:id/limit' do + it 'updates max members per duty for an assignment' do + patch "/assignments/#{assignment.id}/duties/#{duty.id}/limit", + params: { max_members_for_duty: 4 }.to_json, + headers: instructor_headers + + expect(response).to have_http_status(:ok) + body = JSON.parse(response.body) + expect(body['duty_id']).to eq(duty.id) + expect(body['max_members_for_duty']).to eq(4) + expect(assignment_duty.reload.max_members_for_duty).to eq(4) + end + + it 'returns validation errors for invalid max members' do + patch "/assignments/#{assignment.id}/duties/#{duty.id}/limit", + params: { max_members_for_duty: 0 }.to_json, + headers: instructor_headers + + expect(response).to have_http_status(:unprocessable_entity) + body = JSON.parse(response.body) + expect(body['max_members_for_duty']).to be_an(Array) + end + + it 'returns not found when duty is not assigned to assignment' do + other_duty = Duty.create!(name: 'Mentor Duty', instructor_id: instructor.id) + + patch "/assignments/#{assignment.id}/duties/#{other_duty.id}/limit", + params: { max_members_for_duty: 3 }.to_json, + headers: instructor_headers + + expect(response).to have_http_status(:not_found) + body = JSON.parse(response.body) + expect(body['error']).to eq('Duty is not assigned to this assignment') + end + + it 'forbids non-instructor users from changing limits' do + patch "/assignments/#{assignment.id}/duties/#{duty.id}/limit", + params: { max_members_for_duty: 3 }.to_json, + headers: student_headers + + expect(response).to have_http_status(:forbidden) + end + end +end diff --git a/spec/requests/api/v1/participants_controller_spec.rb b/spec/requests/api/v1/participants_controller_spec.rb index 835a444c6..499252627 100644 --- a/spec/requests/api/v1/participants_controller_spec.rb +++ b/spec/requests/api/v1/participants_controller_spec.rb @@ -341,4 +341,43 @@ def fetch_username(user_id) end end end + + path '/participants/{id}/duty' do + patch 'Update participant duty' do + tags 'Participants' + consumes 'application/json' + produces 'application/json' + + parameter name: :id, in: :path, type: :integer, description: 'ID of the participant' + parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token' + parameter name: :duty, in: :body, schema: { + type: :object, + properties: { + duty_id: { type: :integer, nullable: true, description: 'Duty ID for this participant' } + } + } + + let!(:role_duty) { Duty.create!(name: 'Developer') } + + response '201', 'Participant duty updated' do + let(:id) { participant1.id } + let(:duty) { { duty_id: role_duty.id } } + + run_test! do |response| + data = JSON.parse(response.body) + expect(data['duty_id']).to eq(role_duty.id) + expect(data['duty_name']).to eq('Developer') + end + end + + response '404', 'Participant not found' do + let(:id) { 99 } + let(:duty) { { duty_id: nil } } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eql('Participant not found') + end + end + end + end end diff --git a/spec/requests/api/v1/teams_participants_controller_spec.rb b/spec/requests/api/v1/teams_participants_controller_spec.rb index 2b973f3a3..1f5b242a2 100644 --- a/spec/requests/api/v1/teams_participants_controller_spec.rb +++ b/spec/requests/api/v1/teams_participants_controller_spec.rb @@ -179,7 +179,7 @@ run_test! do |response| json = JSON.parse(response.body) expect(json['message']).to eq("Duty updated successfully") - expect(teams_participant.reload.duty_id).to eq(2) + expect(participant_for_update.reload.duty_id).to eq(2) end end