diff --git a/app/controllers/api/v1/teams_participants_controller.rb b/app/controllers/api/v1/teams_participants_controller.rb new file mode 100644 index 000000000..5bf4439d6 --- /dev/null +++ b/app/controllers/api/v1/teams_participants_controller.rb @@ -0,0 +1,149 @@ +class Api::V1::TeamsParticipantsController < ApplicationController + include AuthorizationHelper + + # Determines if the current user is allowed to perform the requested action. + def action_allowed? + if %w[update_duties].include? params[:action] + current_user_has_student_privileges? + else + current_user_has_ta_privileges? + end + end + + # Fetches and renders an auto-complete list of possible team members based on a partial name input. + def auto_complete_for_participant_name + # Fetch the current team using the session-stored `team_id`. + current_team = Team.find(session[:team_id]) + + # Fetch potential members for the team based on the input name. + @potential_team_members = current_team.get_possible_team_members(params[:user][:name]) + + # Render the autocomplete suggestions. + render inline: "<%= auto_complete_result @potential_team_members, 'name' %>", layout: false + end + + # Updates the duty (role) assigned to a participant in a team. + def update_duties + # Find the team member relationship using the provided ID. + team_member_relationship = TeamsUser.find(params[:teams_user_id]) + + # Update the duty of the team member. + team_member_relationship.update_attribute(:duty_id, params[:teams_user]['duty_id']) + + # Redirect to the participant's team view page. + redirect_to controller: 'student_teams', action: 'view', student_id: params[:participant_id] + end + + # Displays a paginated list of all participants in a specific team. + def list_participants + # Fetch the team based on the provided ID. + current_team = Team.find(params[:id]) + + # Retrieve the associated assignment or course for the team. + associated_assignment_or_course = Assignment.find(current_team.parent_id) + + # Query and paginate participants of the current team. + @team_participants = TeamsUser.page(params[:page]).per_page(10).where(team_id: current_team.id) + + @team = current_team + @assignment = associated_assignment_or_course + end + + # Renders the form for adding a new participant to a team. + def add_new_participant + # Fetch the team for which a participant is to be added. + @team = Team.find(params[:id]) + end + #Deletes the selected participant + def delete_selected_participant + @teams_user = TeamsUser.find(params[:id]) + parent_id = Team.find(@teams_user.team_id).parent_id + @user = User.find(@teams_user.user_id) + @teams_user.destroy + undo_link("The team user \"#{@user.name}\" has been successfully removed. ") + redirect_to controller: 'teams', action: 'list', id: parent_id + end + + # Adds a new participant to a team after validation. + def create_participant + # Find the user by their name from the input. + find_participant = find_participant_by_name + # Fetch the team using the provided ID. + current_team = find_team_by_id + if validate_participant_and_team(participant, team) + if team.add_participants_with_validation(participant, team.parent_id) + undo_link("The participant \"#{participant.name}\" has been successfully added to \"#{team.name}\".") + else + flash[:error] = 'This team already has the maximum number of members.' + end + end + # Redirect to the list of teams for the parent assignment or course. + redirect_to controller: 'teams', action: 'list', id: current_team.parent_id + end + + private + + # Helper method to find a user by their name. + def find_participant_by_name + # Locate the user by their name. + find_participant = User.find_by(name: params[:user][:name].strip) + + # Display an error if the user is not found. + unless find_participant + flash[:error] = participant_not_found_error + redirect_back fallback_location: root_path + end + participant + end + + # Helper method to fetch a team by its ID. + def find_team_by_id + Team.find(params[:id]) + end + + # Validates whether a participant can be added to the given team. + def validate_participant_and_team(participant, team) + # Check if the participant is valid for the team type. + validation_result = if team.is_a?(AssignmentTeam) + Assignment.find(team.parent_id).valid_team_participant?(participant) + else + Course.find(team.parent_id).valid_team_participant?(participant) + end + + # Handle validation errors if any. + if validation_result[:success] + true + else + flash[:error] = validation_result[:error] + redirect_back fallback_location: root_path + false + end + end + + # Adds the participant to the team while handling constraints. + def add_participant_to_team(find_participant, team) + # Add the participant to the team and handle the outcome. + addition_result = find_team_by_id.add_participant(find_participant, team.parent_id) + process_participant_addition_result(find_participant, team, addition_result) + end + + # Handles the result of adding a participant to the team. + def process_participant_addition_result(find_participant, team, addition_result) + if addition_result == false + flash[:error] = 'This team already has the maximum number of members.' + else + undo_link("The team user \"#{find_participant.name}\" has been successfully added to \"#{team.name}\".") + end + end + + # Generates an error message when a user is not found. + def participant_not_found_error + new_participnat_url = url_for controller: 'users', action: 'new' + "\"#{params[:user][:name].strip}\" is not defined. Please create this user before continuing." + end + + def non_participant_error(find_participant, parent_id, model) + urlParticipantList = url_for controller: 'participants', action: 'list', id: parent_id, model: model, authorization: 'participant' + "\"#{find_participant.name}\" is not a participant of the current course/assignment. Please add this user before continuing." + end +end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 45e8d2acf..4a4401e88 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -11,6 +11,7 @@ class Assignment < ApplicationRecord has_many :sign_up_topics , class_name: 'SignUpTopic', foreign_key: 'assignment_id', dependent: :destroy belongs_to :course, optional: true belongs_to :instructor, class_name: 'User', inverse_of: :assignments + validates :max_team_size, numericality: { only_integer: true, greater_than_or_equal_to: 1 }, allow_nil: true #This method return the value of the has_badge field for the given assignment object. attr_accessor :title, :description, :has_badge, :enable_pair_programming, :is_calibrated, :staggered_deadline @@ -22,10 +23,11 @@ def review_questionnaire_id def teams? @has_teams ||= teams.any? end + def num_review_rounds rounds_of_reviews end - + # Add a participant to the assignment based on the provided user_id. # This method first finds the User with the given user_id. If the user does not exist, it raises an error. # It then checks if the user is already a participant in the assignment. If so, it raises an error. @@ -79,8 +81,6 @@ def remove_assignment_from_course self end - - # Assign a course to the assignment based on the provided course_id. # If the assignment already belongs to the specified course, an error is raised. # Returns the modified assignment object with the updated course assignment. @@ -98,7 +98,6 @@ def assign_course(course_id) assignment end - # Create a copy of the assignment, including its name, instructor, and course assignment. # The new assignment is named "Copy of [original assignment name]". # Returns the newly created assignment object, which is a copy of the original assignment. @@ -117,6 +116,7 @@ def copy copied_assignment end + def is_calibrated? is_calibrated end @@ -133,7 +133,6 @@ def staggered_and_no_topic?(topic_id) staggered_deadline? && topic_id.nil? end - #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? @@ -183,7 +182,6 @@ def valid_num_review(review_type) end end - #This method check if for the given assignment,different type of rubrics are used in different round. # Checks if for the given assignment any questionnaire is present with used_in_round field not nil. # Returns a bolean value whether such questionnaire is present. @@ -192,7 +190,40 @@ def varying_rubrics_by_round? # Check if any rubric has a specified round rubric_with_round.present? end - - + +#E2479 +#check if the user is on the team +def user_on_team?(user) + teams = self.teams + users = [] + teams.each do |team| + users << team.users + end + users.flatten.include? user +end +# Validates if a user is eligible to join a team for the current assignment. +# This method ensures that: +# - The user is not already part of another team for this assignment. +# - The user is a valid participant in the assignment. +# Params: +# - user: The user to validate for team membership. +# Returns: +# - A hash indicating the validation result: +# - { success: true } if the user can join the team. +# - { success: false, error: "Reason for failure" } if the user cannot join the team. +def valid_team_participant?(user) + # Check if the user is already part of a team for this assignment. + if user_on_team?(user) + { success: false, error: "This user is already assigned to a team for this assignment" } + + # Check if the user is a registered participant in the assignment. + elsif AssignmentParticipant.find_by(user_id: user.id, parent_id: assignment_id).nil? + { success: false, error: "#{user.name} is not a participant in this assignment" } + + # If both checks pass, the user is eligible to join the team. + else + { success: true } + end +end end \ No newline at end of file diff --git a/app/models/assignment_participant.rb b/app/models/assignment_participant.rb index 10ca53b5d..1114152ad 100644 --- a/app/models/assignment_participant.rb +++ b/app/models/assignment_participant.rb @@ -5,7 +5,6 @@ class AssignmentParticipant < Participant belongs_to :user validates :handle, presence: true - def set_handle self.handle = if user.handle.nil? || (user.handle == '') user.name @@ -16,5 +15,13 @@ def set_handle end self.save end + #E2479 + def team + AssignmentTeam.team(self) + end + + def team_user + TeamsUser.where(team_id: team.id, user_id: user_id).first if team + end end \ No newline at end of file diff --git a/app/models/course.rb b/app/models/course.rb index 9e70ccf7d..04992ddbf 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -11,7 +11,7 @@ def path raise 'Path can not be created as the course must be associated with an instructor.' if instructor_id.nil? Rails.root + '/' + Institution.find(institution_id).name.gsub(" ", "") + '/' + User.find(instructor_id).name.gsub(" ", "") + '/' + directory_path + '/' end - + # Add a Teaching Assistant to the course def add_ta(user) if user.nil? @@ -49,4 +49,39 @@ def copy_course new_course.name += '_copy' new_course.save end + #E2479 + #checks if the user is in the team + def user_on_team?(user) + teams = self.teams + users = [] + teams.each do |team| + users << team.users + end + users.flatten.include? user + end + # Checks if a user is eligible to join a specific team for a course. +# This method ensures that: +# - The user is not already a member of another team for the course. +# - The user is a valid participant in the course. +# Params: +# - user: The user to be validated for team membership. +# Returns: +# - A hash indicating success or failure: +# - { success: true } if the user can be added to the team. +# - { success: false, error: "Reason for failure" } if the user cannot be added. +def valid_team_participant?(user) + # Check if the user is already a member of another team for the same course. + if user_on_team?(user) + { success: false, error: "This user is already assigned to a team for this course" } + + # Check if the user is a participant in the course associated with this team. + elsif CourseParticipant.find_by(user_id: user.id, parent_id: course_id).nil? + { success: false, error: "#{user.name} is not a participant in this course" } + + # If both checks pass, the user is eligible to join the team. + else + { success: true } + end +end + end \ No newline at end of file diff --git a/app/models/course_participant.rb b/app/models/course_participant.rb new file mode 100644 index 000000000..6c284130a --- /dev/null +++ b/app/models/course_participant.rb @@ -0,0 +1,35 @@ +class CourseParticipant < Participant + belongs_to :course, class_name: 'Course', foreign_key: 'parent_id' + + # Copy this participant to an assignment + def copy_to_assignment(assignment_id) + part = AssignmentParticipant.find_or_create_by(user_id: user_id, parent_id: assignment_id) + part.set_handle if part.persisted? + part + end + + # Provide import functionality for Course Participants + def self.import(row_hash, session, course_id) + raise ArgumentError, 'No user ID has been specified.' if row_hash.empty? + + user = User.find_by(name: row_hash[:username]) + unless user + raise ArgumentError, "The record containing #{row_hash[:username]} does not have enough items." if row_hash.length < 4 + + attributes = ImportFileHelper.define_attributes(row_hash) + user = ImportFileHelper.create_new_user(attributes, session) + end + + course = Course.find(course_id) + raise ImportError, "The course with the ID #{course_id} was not found." unless course + + unless exists?(user_id: user.id, parent_id: course_id) + create(user_id: user.id, parent_id: course_id) + end + end + + # Generate a path for this participant + def path + course.path.join(directory_num.to_s) + end +end diff --git a/app/models/mentor_management.rb b/app/models/mentor_management.rb new file mode 100644 index 000000000..54a710964 --- /dev/null +++ b/app/models/mentor_management.rb @@ -0,0 +1,122 @@ +class MentorManagement + # Select a mentor using the following algorithm + # + # 1) Find all assignment participants for the + # assignment with id [assignment_id] whose + # duty is the same as [Particpant#DUTY_MENTOR]. + # 2) Count the number of teams those participants + # are a part of, acting as a proxy for the + # number of teams they mentor. + # 3) Return the mentor with the fewest number of + # teams they're currently mentoring. + # + # This method's runtime is O(n lg n) due to the call to + # Hash#sort_by. This assertion assumes that the + # database management system is capable of fetching the + # required rows at least as quickly. + # + # Implementation detail: Any tie between the top 2 + # mentors is decided by the Hash#sort_by algorithm. + # + # @return The id of the mentor with the fewest teams + # they are assigned to. Returns `nil` if there are + # no participants with mentor duty for [assignment_id]. + def self.select_mentor(assignment_id) + mentor_user_id, = zip_mentors_with_team_count(assignment_id).first + User.where(id: mentor_user_id).first + end + + # = Mentor Management + # E2115: Handles calls when an assignment has the auto_assign_mentor flag enabled and triggered by the event when a new member joins an assignment team. + # + # This event happens when: + # 1.) An invited student user accepts and successfully added to a team from + # app/models/invitation.rb + # 2.) A student user is successfully added to the team manually from + # app/controllers/teams_users_controller.rb. + # + # This method will determine if a mentor needs to be assigned, if so, + # selects one, and adds the mentor to the team if: + # 1.) The assignment does not have a topic. + # 2.) If the team has reached >50% full capacity. + # 3.) If the team does not have a mentor. + def self.assign_mentor(assignment_id, team_id) + assignment = Assignment.find(assignment_id) + team = Team.find(team_id) + + # RuboCop 'use guard clause instead of nested conditionals' + # return if assignments can't accept mentors + return unless assignment.auto_assign_mentor + + # RuboCop 'use guard clause instead of nested conditionals' + # return if the assignment or team already have a topic + return if assignment.topics? || !team.topic.nil? + + curr_team_size = Team.size(team_id) + max_team_members = Assignment.find(assignment_id).max_team_size + + # RuboCop 'use guard clause instead of nested conditionals' + # return if the team size hasn't reached > 50% of capacity + return if curr_team_size * 2 <= max_team_members + + # RuboCop 'use guard clause instead of nested conditionals' + # return if there's already a mentor in place + return if team.participants.any? { |participant| participant.can_mentor == true } + + mentor_user = select_mentor(assignment_id) + + # Add the mentor using team model class. + team_member_added = mentor_user.nil? ? false : team.add_member(mentor_user, assignment_id) + + return unless team_member_added + + notify_team_of_mentor_assignment(mentor_user, team) + end + + def self.notify_team_of_mentor_assignment(mentor, team) + members = team.users + emails = members.map(&:email) + members_info = members.map { |mem| "#{mem.fullname} - #{mem.email}" } + mentor_info = "#{mentor.fullname} (#{mentor.email})" + message = "#{mentor_info} has been assigned as your mentor for assignment #{Assignment.find(team.parent_id).name}
Current members:
#{members_info.join('
')}" + + Mailer.delayed_message(bcc: emails, + subject: '[Expertiza]: New Mentor Assignment', + body: message).deliver_now + end + + # Returns true if [user] is a mentor, and false if not. + # + # [user] must be a User object. + # + # Checks the Participant relation to see if a row exists with + # user_id == user.id that also has 'mentor' in the duty attribute. + def self.user_a_mentor?(user) + Participant.exists?(user_id: user.id, can_mentor: true) + end + + # Select all the participants who's duty in the participant + # table is [DUTY_MENTOR], and who are a participant of + # [assignment_id]. + # + # @see participant.rb for the value of DUTY_MENTOR + def self.mentors_for_assignment(assignment_id) + Participant.where(parent_id: assignment_id, can_mentor: true) + end + + # Produces a hash mapping mentor's user_ids to the aggregated + # number of teams they're part of, which acts as a proxy for + # the number of teams they're mentoring. + def self.zip_mentors_with_team_count(assignment_id) + mentor_ids = mentors_for_assignment(assignment_id).pluck(:user_id) + + return [] if mentor_ids.empty? + + team_counts = {} + mentor_ids.each { |id| team_counts[id] = 0 } + #E2351 removed (:team_id) after .count to fix balancing algorithm + team_counts.update(TeamsUser.where(user_id: mentor_ids).group(:user_id).count) + + team_counts.sort_by { |_, v| v } + end +end diff --git a/app/models/mentored_team.rb b/app/models/mentored_team.rb new file mode 100644 index 000000000..6b38ac48e --- /dev/null +++ b/app/models/mentored_team.rb @@ -0,0 +1,27 @@ +class MentoredTeam < AssignmentTeam + # Class created during refactoring of E2351 + # Overridden method to include the MentorManagement workflow + def add_member(user, _assignment_id = nil) + raise "The user #{user.name} is already a member of the team #{name}" if user?(user) + raise "A mentor already exists for team #{name}" if mentor_exists? && user.mentor_role? + can_add_member = false + unless full? || user.mentor_role? + can_add_member = true + t_user = TeamsUser.create(user_id: user.id, team_id: id) + parent = TeamNode.find_by(node_object_id: id) + TeamUserNode.create(parent_id: parent.id, node_object_id: t_user.id) + add_participant(parent_id, user) + ExpertizaLogger.info LoggerMessage.new('Model:Team', user.name, "Added member to the team #{id}") + end + if can_add_member + MentorManagement.assign_mentor(_assignment_id, id) if user.mentor_role? + end + can_add_member + end + #E2479 + private + + def mentor_exists? + teams_users.where(role: 'mentor').exists? + end +end diff --git a/app/models/team.rb b/app/models/team.rb index afb8ac66f..d0f6157c2 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -1,15 +1,20 @@ class Team < ApplicationRecord has_many :signed_up_teams, dependent: :destroy has_many :teams_users, dependent: :destroy + has_many :join_team_requests, dependent: :destroy + has_one :team_node, foreign_key: :node_object_id, dependent: :destroy has_many :users, through: :teams_users + has_many :bids, dependent: :destroy has_many :participants belongs_to :assignment + validates :name, presence: true attr_accessor :max_participants - + scope :find_team_for_assignment_and_user, lambda { |assignment_id, user_id| + joins(:teams_users).where('teams.parent_id = ? AND teams_users.user_id = ?', assignment_id, user_id) + } # 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 @@ -17,4 +22,32 @@ def full? false end end + + #E2479 + #Check if a user is already a member of the team if not it adds the user + def add_participant(user, _assignment_id = nil) + raise "The user #{user.name} is already a member of the team #{name}" if user?(user) + + can_add_member = false + unless full? + can_add_member = true + t_user = TeamsUser.create(user_id: user.id, team_id: id) + parent = TeamNode.find_by(node_object_id: id) + TeamUserNode.create(parent_id: parent.id, node_object_id: t_user.id) + add_participant(parent_id, user) + ExpertizaLogger.info LoggerMessage.new('Model:Team', user.name, "Added member to the team #{id}") + end + can_add_member + end + +def add_participants_with_handling(user, parent_id) + begin + # Attempt to add the user to the team. + addition_result = add_member(user, parent_id) + addition_result + rescue StandardError => e + # Return a failure message if an error occurs (e.g., user already in the team). + { success: false, error: "The user #{user.name} is already a member of the team #{name}" } + end +end end \ No newline at end of file diff --git a/app/models/teams_user.rb b/app/models/teams_user.rb deleted file mode 100644 index 9e1768b94..000000000 --- a/app/models/teams_user.rb +++ /dev/null @@ -1,23 +0,0 @@ -class TeamsUser < ApplicationRecord - belongs_to :user - belongs_to :team - - def name(ip_address = nil) - name = user.name(ip_address) - end - - def get_team_members(team_id) - team_members = TeamsUser.where('team_id = ?', team_id) - user_ids = team_members.pluck(:user_id) - users = User.where(id: user_ids) - - return users - end - - # Removes entry in the TeamUsers table for the given user and given team id - def self.remove_team(user_id, team_id) - team_user = TeamsUser.where('user_id = ? and team_id = ?', user_id, team_id).first - team_user&.destroy - end - -end diff --git a/app/models/teams_users.rb b/app/models/teams_users.rb new file mode 100644 index 000000000..0232dd8f2 --- /dev/null +++ b/app/models/teams_users.rb @@ -0,0 +1,31 @@ +class TeamsUser < ApplicationRecord + belongs_to :user + belongs_to :team + has_one :team_user_node, foreign_key: 'node_object_id', dependent: :destroy + + # Retrieves the team members for a specific team. + def get_team_members(team_id) + team_members = TeamsUser.where('team_id = ?', team_id) + user_ids = team_members.pluck(:user_id) + User.where(id: user_ids) + end + + # Removes entry in the TeamUsers table for the given user and team ID. + def self.remove_team(user_id, team_id) + team_user = TeamsUser.find_by(user_id: user_id, team_id: team_id) + team_user&.destroy + end + +#E2479 + # Deletes multiple team members in bulk. + def self.delete_multiple_participants(team_user_ids) + where(id: team_user_ids).destroy_all + end + + # Custom name display for mentors. + def name(ip_address = nil) + name = user.name(ip_address) + name += ' (Mentor)' if MentorManagement.user_a_mentor?(user) + name + end +end diff --git a/app/views/teams_participants/_form.html.erb b/app/views/teams_participants/_form.html.erb new file mode 100644 index 000000000..de299c570 --- /dev/null +++ b/app/views/teams_participants/_form.html.erb @@ -0,0 +1,5 @@ + +<%= hidden_field_tag 'id', @team.id %> +Enter user login: <%= text_field_with_auto_complete :user, :name, {:size => 41} %> + +<% end %> diff --git a/app/views/teams_participants/_members.html.erb b/app/views/teams_participants/_members.html.erb new file mode 100644 index 000000000..5483ecbc1 --- /dev/null +++ b/app/views/teams_participants/_members.html.erb @@ -0,0 +1,7 @@ + diff --git a/app/views/teams_participants/list.html.erb b/app/views/teams_participants/list.html.erb new file mode 100644 index 000000000..0808577c7 --- /dev/null +++ b/app/views/teams_participants/list.html.erb @@ -0,0 +1,17 @@ +

Team Members for <%= @assignment.name %>

+ + +

Team Name: <%= @team.name(session[:ip]) %>

+ +<% + column_definitions = [] +column_definitions << {:name => 'Team Member', :type => 'fetch', :model => 'User', :field => 'name', :column => 'user_id'} +column_definitions << {:name => 'Remove from Team', :action => 'delete_team_user', :controller => 'teams_users'} + +delete_options = {:enabled => true, :controller => 'teams_users'} +%> +<%= render :partial => 'shared_scripts/selectTable', :locals => {:column_definitions => column_definitions, :delete_options => delete_options, :elements => @teams_users} %> + +
+<%= link_to 'New Team Member', :action => 'new', :id => @team %> +| <%= link_to 'Back', :action => 'list', :controller => 'teams', :id => @team.parent_id %> diff --git a/app/views/teams_participants/new.html.erb b/app/views/teams_participants/new.html.erb new file mode 100644 index 000000000..68dba216e --- /dev/null +++ b/app/views/teams_participants/new.html.erb @@ -0,0 +1,12 @@ +

New Team Member

+<% session[:team_id] = @team.id %> + +<%= form_tag :action => 'create' do %> + <%= hidden_field_tag 'id', @team.id %> + Enter user login: <%= text_field_with_auto_complete :user, :name, {:size => 41} %> + +<% end %> + +
+ +<%= link_to 'Back', :controller => 'course_team', :action => 'list', :id => @team.parent_id %> diff --git a/spec/controllers/teams_participants_controller_spec.rb b/spec/controllers/teams_participants_controller_spec.rb new file mode 100644 index 000000000..c0b3f8257 --- /dev/null +++ b/spec/controllers/teams_participants_controller_spec.rb @@ -0,0 +1,129 @@ +require 'rails_helper' + +RSpec.describe Api::V1::TeamsParticipantsController, type: :controller do + let(:student_role) { create(:role, :student) } + let(:instructor_role) { create(:role, :instructor) } + let(:instructor) { create(:user, role: create(:role, :instructor)) } + let(:course) { create(:course, instructor_id: instructor.id) } + let(:user) { create(:user, role: student_role) } + let(:ta) { create(:teaching_assistant) } + let(:team) { create(:team, parent_id: create(:assignment).id) } + let(:participant) { create(:user, name: 'Test Participant') } + let(:team_participant) { create(:teams_user, user: participant, team: team) } + + describe '#action_allowed?' do + context 'when action is update_duties' do + it 'allows access for students' do + allow(controller).to receive(:current_user_has_student_privileges?).and_return(true) + allow(controller).to receive(:params).and_return({ action: 'update_duties' }) + expect(controller.action_allowed?).to be true + end + + it 'denies access for non-students' do + allow(controller).to receive(:current_user_has_student_privileges?).and_return(false) + allow(controller).to receive(:params).and_return({ action: 'update_duties' }) + expect(controller.action_allowed?).to be false + end + end + + context 'when action is not update_duties' do + it 'allows access for TAs' do + allow(controller).to receive(:current_user_has_ta_privileges?).and_return(true) + allow(controller).to receive(:params).and_return({ action: 'list_participants' }) + expect(controller.action_allowed?).to be true + end + end + end + + describe '#update_duties' do + it 'updates the duties for the participant' do + # Mock the TeamsUser object + allow(TeamsUser).to receive(:find).with('1').and_return(team_participant) + allow(team_participant).to receive(:update_attribute).with(:team_id, '2').and_return('OK') + + # Prepare request and session parameters + request_params = { + teams_user_id: '1', # ID of the TeamsUser to update + teams_user: { team_id: '2' }, # Attribute to update + participant_id: '1' # Participant ID for redirection + } + user_session = { user: stub_current_user(student, student.role.name, student.role) } + + # Perform the request + get :update_duties, params: request_params, session: user_session + + # Expectations + expect(response).to redirect_to('/student_teams/view?student_id=1') + expect(team_participant).to have_received(:update_attribute).with(:team_id, '2') + end +end + + + describe '#list_participants' do + it 'assigns participants and renders the view' do + assignment = create(:assignment, id: team.parent_id) + create_list(:teams_user, 5, team: team) + + get :list_participants, params: { id: team.id, page: 1 } + + expect(assigns(:team_participants).size).to eq(5) + expect(assigns(:team)).to eq(team) + expect(assigns(:assignment)).to eq(assignment) + end + end + + describe '#add_new_participant' do + it 'renders the form for adding a new participant' do + get :add_new_participant, params: { id: team.id } + + expect(response).to render_template(:add_new_participant) + end + end + + describe '#create_participant' do + context 'when participant is valid' do + it 'adds the participant and redirects' do + allow(controller).to receive(:find_participant_by_name).and_return(participant) + allow(controller).to receive(:find_team_by_id).and_return(team) + allow(controller).to receive(:validate_participant_and_team).and_return(true) + + post :create_participant, params: { user: { name: participant.name }, id: team.id } + + expect(response).to redirect_to(controller: 'teams', action: 'list', id: team.parent_id) + end + end + + context 'when participant is invalid' do + it 'flashes an error and redirects back' do + allow(controller).to receive(:find_participant_by_name).and_return(nil) + + post :create_participant, params: { user: { name: 'Invalid User' }, id: team.id } + + expect(flash[:error]).to be_present + expect(response).to redirect_to(root_path) + end + end + end + + describe '#delete_participant' do + it 'deletes the participant and redirects' do + Rails.logger.debug "Creating test data..." + assignment = create(:assignment) + parent_team = create(:team, parent_id: assignment.id) + team.update!(parent_id: parent_team.id) + participant = create(:user) + team_participant = create(:teams_user, team: team, user: participant) + + Rails.logger.debug "TeamsUser before deletion: #{TeamsUser.exists?(team_participant.id)}" + + delete :delete_participant, params: { id: team_participant.id } + + Rails.logger.debug "TeamsUser after deletion: #{TeamsUser.exists?(team_participant.id)}" + + expect(TeamsUser.exists?(team_participant.id)).to be_falsey # Verify deletion + expect(response).to redirect_to(controller: 'teams', action: 'list', id: parent_team.id) # Verify redirection + end +end + + +end diff --git a/spec/factories/factories.rb b/spec/factories/factories.rb index eb06d7682..eb7066da9 100644 --- a/spec/factories/factories.rb +++ b/spec/factories/factories.rb @@ -1,6 +1,5 @@ FactoryBot.define do + + end - -end -