From 9ce86f30531000a82a3c0b7e42672d4e0acdddf5 Mon Sep 17 00:00:00 2001 From: manideepika21 <145089468+manideepika21@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:45:34 -0500 Subject: [PATCH 01/18] Created teams_participants_controller.rb file --- app/controllers/teams_participants_controller | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 app/controllers/teams_participants_controller diff --git a/app/controllers/teams_participants_controller b/app/controllers/teams_participants_controller new file mode 100644 index 000000000..b0b716106 --- /dev/null +++ b/app/controllers/teams_participants_controller @@ -0,0 +1,112 @@ +class TeamsUsersController < ApplicationController + include AuthorizationHelper + + def action_allowed? + # Allow duty updation for a team if current user is student, else require TA or above Privileges. + if %w[update_duties].include? params[:action] + current_user_has_student_privileges? + else + current_user_has_ta_privileges? + end + end + + def auto_complete_for_user_name + team = Team.find(session[:team_id]) + @users = team.get_possible_team_members(params[:user][:name]) + render inline: "<%= auto_complete_result @users, 'name' %>", layout: false + end + + # Example of duties: manager, designer, programmer, tester. Finds TeamsUser and save preferred Duty + def update_duties + team_user = TeamsUser.find(params[:teams_user_id]) + team_user.update_attribute(:duty_id, params[:teams_user]['duty_id']) + redirect_to controller: 'student_teams', action: 'view', student_id: params[:participant_id] + end + + def list + @team = Team.find(params[:id]) + @assignment = Assignment.find(@team.parent_id) + @teams_users = TeamsUser.page(params[:page]).per_page(10).where(['team_id = ?', params[:id]]) + end + + def new + @team = Team.find(params[:id]) + end + + def create + user = User.find_by(name: params[:user][:name].strip) + unless user + urlCreate = url_for controller: 'users', action: 'new' + flash[:error] = "\"#{params[:user][:name].strip}\" is not defined. Please create this user before continuing." + end + + team = Team.find(params[:id]) + unless user.nil? + if team.is_a?(AssignmentTeam) + assignment = Assignment.find(team.parent_id) + if assignment.user_on_team?(user) + flash[:error] = "This user is already assigned to a team for this assignment" + redirect_back fallback_location: root_path + return + end + if AssignmentParticipant.find_by(user_id: user.id, parent_id: assignment.id).nil? + urlAssignmentParticipantList = url_for controller: 'participants', action: 'list', id: assignment.id, model: 'Assignment', authorization: 'participant' + flash[:error] = "\"#{user.name}\" is not a participant of the current assignment. Please add this user before continuing." + else + begin + add_member_return = team.add_member(user, team.parent_id) + rescue + flash[:error] = "The user #{user.name} is already a member of the team #{team.name}" + redirect_back fallback_location: root_path + return + end + flash[:error] = 'This team already has the maximum number of members.' if add_member_return == false + end + else # CourseTeam + course = Course.find(team.parent_id) + if course.user_on_team?(user) + flash[:error] = "This user is already assigned to a team for this course" + redirect_back fallback_location: root_path + return + end + if CourseParticipant.find_by(user_id: user.id, parent_id: course.id).nil? + urlCourseParticipantList = url_for controller: 'participants', action: 'list', id: course.id, model: 'Course', authorization: 'participant' + flash[:error] = "\"#{user.name}\" is not a participant of the current course. Please add this user before continuing." + else + begin + add_member_return = team.add_member(user, team.parent_id) + rescue + flash[:error] = "The user #{user.name} is already a member of the team #{team.name}" + redirect_back fallback_location: root_path + return + end + flash[:error] = 'This team already has the maximum number of members.' if add_member_return == false + if add_member_return + @teams_user = TeamsUser.last + undo_link("The team user \"#{user.name}\" has been successfully added to \"#{team.name}\".") + end + end + end + end + + redirect_to controller: 'teams', action: 'list', id: team.parent_id + end + + def delete + @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 + + def delete_selected + params[:item].each do |item_id| + team_user = TeamsUser.find(item_id).first + team_user.destroy + end + + redirect_to action: 'list', id: params[:id] + end +end From a397851c47cbbef1c156a813f5032c5ff5278553 Mon Sep 17 00:00:00 2001 From: manideepika21 <145089468+manideepika21@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:48:31 -0500 Subject: [PATCH 02/18] Created teams_participants view folder and added _form.html.erb --- app/views/layouts/teams_participants/_form.html.erb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/views/layouts/teams_participants/_form.html.erb diff --git a/app/views/layouts/teams_participants/_form.html.erb b/app/views/layouts/teams_participants/_form.html.erb new file mode 100644 index 000000000..de299c570 --- /dev/null +++ b/app/views/layouts/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 %> From 178579c0840beff5ce6b8b924fc07d76bd0c55f1 Mon Sep 17 00:00:00 2001 From: manideepika21 Date: Mon, 25 Nov 2024 10:53:52 -0500 Subject: [PATCH 03/18] Added html files to teams_participants views folder --- .../teams_participants/_form.html.erb | 0 app/views/teams_participants/_members.html.erb | 7 +++++++ app/views/teams_participants/list.html.erb | 17 +++++++++++++++++ app/views/teams_participants/new.html.erb | 12 ++++++++++++ 4 files changed, 36 insertions(+) rename app/views/{layouts => }/teams_participants/_form.html.erb (100%) create mode 100644 app/views/teams_participants/_members.html.erb create mode 100644 app/views/teams_participants/list.html.erb create mode 100644 app/views/teams_participants/new.html.erb diff --git a/app/views/layouts/teams_participants/_form.html.erb b/app/views/teams_participants/_form.html.erb similarity index 100% rename from app/views/layouts/teams_participants/_form.html.erb rename to app/views/teams_participants/_form.html.erb 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 %> From 80963e70ef3a295c816c292e1746fca7aefcedbb Mon Sep 17 00:00:00 2001 From: manideepika21 Date: Mon, 25 Nov 2024 10:57:23 -0500 Subject: [PATCH 04/18] Modified teams_users model name to teams_participants --- ...s_participants_controller => teams_participants_controller.rb} | 0 app/models/{teams_user.rb => teams_participants.rb} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/controllers/{teams_participants_controller => teams_participants_controller.rb} (100%) rename app/models/{teams_user.rb => teams_participants.rb} (100%) diff --git a/app/controllers/teams_participants_controller b/app/controllers/teams_participants_controller.rb similarity index 100% rename from app/controllers/teams_participants_controller rename to app/controllers/teams_participants_controller.rb diff --git a/app/models/teams_user.rb b/app/models/teams_participants.rb similarity index 100% rename from app/models/teams_user.rb rename to app/models/teams_participants.rb From 4dbe2443af22bc1a5ad82ce7e6a11e3aaafa68f9 Mon Sep 17 00:00:00 2001 From: bhuvan chandra kurra Date: Fri, 29 Nov 2024 13:10:31 -0500 Subject: [PATCH 05/18] Updated teams_participants_controller - Refactored the logic for adding/removing users to Team and TeamUser models - Cleaned up create action - Refgactored delete method - Improved list method - Enhance delete_selected method --- .../teams_participants_controller.rb | 150 ++++++++---------- 1 file changed, 66 insertions(+), 84 deletions(-) diff --git a/app/controllers/teams_participants_controller.rb b/app/controllers/teams_participants_controller.rb index b0b716106..5e265df51 100644 --- a/app/controllers/teams_participants_controller.rb +++ b/app/controllers/teams_participants_controller.rb @@ -1,112 +1,94 @@ class TeamsUsersController < ApplicationController include AuthorizationHelper + # Check permissions for actions def action_allowed? - # Allow duty updation for a team if current user is student, else require TA or above Privileges. - if %w[update_duties].include? params[:action] + if %w[update_duties].include?(params[:action]) current_user_has_student_privileges? else current_user_has_ta_privileges? end end + # Autocomplete user names for adding members to a team def auto_complete_for_user_name - team = Team.find(session[:team_id]) - @users = team.get_possible_team_members(params[:user][:name]) - render inline: "<%= auto_complete_result @users, 'name' %>", layout: false + team = Team.find_by(id: session[:team_id]) + if team + @users = team.get_members.where("name LIKE ?", "%#{params[:user][:name]}%") + render inline: "<%= auto_complete_result @users, 'name' %>", layout: false + else + render plain: "Team not found", status: :not_found + end end - # Example of duties: manager, designer, programmer, tester. Finds TeamsUser and save preferred Duty - def update_duties - team_user = TeamsUser.find(params[:teams_user_id]) - team_user.update_attribute(:duty_id, params[:teams_user]['duty_id']) - redirect_to controller: 'student_teams', action: 'view', student_id: params[:participant_id] - end + # Add a user to a team + def create + user = User.find_by(name: params[:user][:name].strip) + return redirect_with_error("User not found. Please create the user.") unless user - def list - @team = Team.find(params[:id]) - @assignment = Assignment.find(@team.parent_id) - @teams_users = TeamsUser.page(params[:page]).per_page(10).where(['team_id = ?', params[:id]]) - end + team = Team.find(params[:team_id]) + begin + team.add_member(user) + flash[:success] = "User #{user.name} successfully added to the team." + rescue StandardError => e + flash[:error] = e.message + end - def new - @team = Team.find(params[:id]) + redirect_to action: 'list', id: team.assignment_id end - def create - user = User.find_by(name: params[:user][:name].strip) - unless user - urlCreate = url_for controller: 'users', action: 'new' - flash[:error] = "\"#{params[:user][:name].strip}\" is not defined. Please create this user before continuing." - end + # Remove a user from a team + def delete + team = Team.find(params[:team_id]) + user = User.find(params[:user_id]) - team = Team.find(params[:id]) - unless user.nil? - if team.is_a?(AssignmentTeam) - assignment = Assignment.find(team.parent_id) - if assignment.user_on_team?(user) - flash[:error] = "This user is already assigned to a team for this assignment" - redirect_back fallback_location: root_path - return - end - if AssignmentParticipant.find_by(user_id: user.id, parent_id: assignment.id).nil? - urlAssignmentParticipantList = url_for controller: 'participants', action: 'list', id: assignment.id, model: 'Assignment', authorization: 'participant' - flash[:error] = "\"#{user.name}\" is not a participant of the current assignment. Please add this user before continuing." - else - begin - add_member_return = team.add_member(user, team.parent_id) - rescue - flash[:error] = "The user #{user.name} is already a member of the team #{team.name}" - redirect_back fallback_location: root_path - return - end - flash[:error] = 'This team already has the maximum number of members.' if add_member_return == false - end - else # CourseTeam - course = Course.find(team.parent_id) - if course.user_on_team?(user) - flash[:error] = "This user is already assigned to a team for this course" - redirect_back fallback_location: root_path - return - end - if CourseParticipant.find_by(user_id: user.id, parent_id: course.id).nil? - urlCourseParticipantList = url_for controller: 'participants', action: 'list', id: course.id, model: 'Course', authorization: 'participant' - flash[:error] = "\"#{user.name}\" is not a participant of the current course. Please add this user before continuing." - else - begin - add_member_return = team.add_member(user, team.parent_id) - rescue - flash[:error] = "The user #{user.name} is already a member of the team #{team.name}" - redirect_back fallback_location: root_path - return - end - flash[:error] = 'This team already has the maximum number of members.' if add_member_return == false - if add_member_return - @teams_user = TeamsUser.last - undo_link("The team user \"#{user.name}\" has been successfully added to \"#{team.name}\".") - end - end - end + begin + team.remove_member(user) + flash[:success] = "User #{user.name} successfully removed from the team." + rescue StandardError => e + flash[:error] = e.message end - redirect_to controller: 'teams', action: 'list', id: team.parent_id + redirect_to action: 'list', id: team.assignment_id end - def delete - @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 + # List all members of a team + def list + @team = Team.find(params[:id]) + @assignment = @team.assignment + @teams_users = @team.teams_users.page(params[:page]).per_page(10) end + # Update a team user's duties + def update_duties + team_user = TeamsUser.find(params[:teams_user_id]) + if team_user + team_user.update!(duty_id: params[:teams_user][:duty_id]) + redirect_to controller: 'student_teams', action: 'view', student_id: params[:participant_id] + else + flash[:error] = "Team member not found." + redirect_back fallback_location: root_path + end + rescue StandardError => e + flash[:error] = e.message + redirect_back fallback_location: root_path + end + + # Delete selected users from a team def delete_selected - params[:item].each do |item_id| - team_user = TeamsUser.find(item_id).first - team_user.destroy + team = Team.find(params[:id]) + user_ids = params[:item] + + begin + user_ids.each do |user_id| + user = User.find(user_id) + team.remove_member(user) + end + flash[:success] = "Selected users successfully removed from the team." + rescue StandardError => e + flash[:error] = e.message end - redirect_to action: 'list', id: params[:id] + redirect_to action: 'list', id: team.assignment_id end -end +end \ No newline at end of file From 88af789948a051d0ad9ddb4443fc4636c1d37af0 Mon Sep 17 00:00:00 2001 From: bhuvan chandra kurra Date: Fri, 29 Nov 2024 13:13:09 -0500 Subject: [PATCH 06/18] Moved logic of adding and removing team members to teams model --- app/models/team.rb | 82 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/app/models/team.rb b/app/models/team.rb index afb8ac66f..073eac951 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -6,15 +6,85 @@ class Team < ApplicationRecord belongs_to :assignment attr_accessor :max_participants - # 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 + # Check if the team is full def full? + max_participants ||= 3 # Default maximum participants + participants.count >= max_participants + end + + # Add a user to the team + def add_member(user) + return false if full? + + TeamsUser.create!(team_id: id, user_id: user.id) + rescue ActiveRecord::RecordInvalid => e + raise "Failed to add member to the team: #{e.message}" + end + + # Remove a user from the team + def remove_member(user) + team_user = teams_users.find_by(user_id: user.id) + if team_user + team_user.destroy + else + raise "The user #{user.name} is not a member of the team." + end + rescue StandardError => e + raise "Failed to remove member from the team: #{e.message}" + end + + # Check if a user belongs to this team + def has_member?(user) + users.exists?(id: user.id) + end + + # Get all team members + def get_members + users + end - max_participants ||= 3 - if participants.count >= max_participants - true + # Assign a leader to the team + def assign_leader(user) + team_user = teams_users.find_by(user_id: user.id) + if team_user + team_user.update!(role: 'leader') else - false + raise "The user #{user.name} is not a member of the team." + end + rescue StandardError => e + raise "Failed to assign team leader: #{e.message}" + end + + # Get the team leader + def team_leader + teams_users.find_by(role: 'leader')&.user + end + + # Check if the team is empty + def empty? + users.empty? + end + + # Transfer all members to another team + def transfer_members_to(other_team) + raise "Cannot transfer members to a full team." if other_team.full? + + users.each do |user| + other_team.add_member(user) + remove_member(user) + end + rescue StandardError => e + raise "Failed to transfer members: #{e.message}" + end + + # Auto-assign users to the team + def auto_assign_users(users_to_add) + users_to_add.each do |user| + break if full? + + add_member(user) end + rescue StandardError => e + raise "Failed to auto-assign users: #{e.message}" end end \ No newline at end of file From bcd48029ea76cb4b28d394a9f4e3cbcd459f3d76 Mon Sep 17 00:00:00 2001 From: bhuvan chandra kurra Date: Fri, 29 Nov 2024 13:15:31 -0500 Subject: [PATCH 07/18] Moved logic of adding/removing user to/from a team to teams_participants model --- app/models/team.rb | 17 --------- app/models/teams_participants.rb | 60 ++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/app/models/team.rb b/app/models/team.rb index 073eac951..7291daf60 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -43,23 +43,6 @@ def get_members users end - # Assign a leader to the team - def assign_leader(user) - team_user = teams_users.find_by(user_id: user.id) - if team_user - team_user.update!(role: 'leader') - else - raise "The user #{user.name} is not a member of the team." - end - rescue StandardError => e - raise "Failed to assign team leader: #{e.message}" - end - - # Get the team leader - def team_leader - teams_users.find_by(role: 'leader')&.user - end - # Check if the team is empty def empty? users.empty? diff --git a/app/models/teams_participants.rb b/app/models/teams_participants.rb index 9e1768b94..a1e4b53e3 100644 --- a/app/models/teams_participants.rb +++ b/app/models/teams_participants.rb @@ -2,22 +2,62 @@ class TeamsUser < ApplicationRecord belongs_to :user belongs_to :team + # Returns the user's name. If an IP address is provided, it may influence the name retrieval logic. def name(ip_address = nil) - name = user.name(ip_address) + 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) + # Retrieves all team members for a given team ID as a collection of User objects. + # Allows optional exclusion of certain roles. + def self.get_team_members(team_id, excluded_roles: []) + users = where(team_id: team_id).includes(:user).map(&:user) + return users if excluded_roles.empty? - return users + # Exclude users with specific roles, if any + users.reject { |user| excluded_roles.include?(user.role) } 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 + # Adds a user to a team. Raises an error if the user is already on the team. + # Returns the created TeamsUser object if successful. + def self.add_to_team(user_id, team_id) + # Check if the user is already a team member + if where(user_id: user_id, team_id: team_id).exists? + raise "The user is already a member of the team." + end + + # Create the association + create!(user_id: user_id, team_id: team_id) + rescue ActiveRecord::RecordInvalid => e + raise "Failed to add user to team: #{e.message}" end + # Removes a user's association with a team. Raises an error if the association does not exist. + def self.remove_from_team(user_id, team_id) + team_user = find_by(user_id: user_id, team_id: team_id) + raise "The user is not a member of this team." if team_user.nil? + + team_user.destroy + rescue StandardError => e + raise "Failed to remove user from team: #{e.message}" + end + + # Transfers a user from one team to another within the same context. + # Ensures that the user is removed from the previous team before adding to the new one. + def self.transfer_user_to_team(user_id, old_team_id, new_team_id) + remove_from_team(user_id, old_team_id) + add_to_team(user_id, new_team_id) + rescue StandardError => e + raise "Failed to transfer user between teams: #{e.message}" + end + + # Checks if a user is already on a team. + def self.user_on_team?(user_id, team_id) + where(user_id: user_id, team_id: team_id).exists? + end + + # Retrieves all teams for a given user as a collection of Team objects. + def self.get_teams_for_user(user_id) + team_ids = where(user_id: user_id).pluck(:team_id) + Team.where(id: team_ids) + end end From 22cdee93339307ce9a2536299e7100976d1f5f05 Mon Sep 17 00:00:00 2001 From: manideepika21 Date: Sat, 30 Nov 2024 12:27:52 -0500 Subject: [PATCH 08/18] Moved teams model from expertiza to reimplementation-back-end repository --- .../teams_participants_controller.rb | 4 +- app/models/assignment.rb | 27 ++ app/models/course.rb | 123 +++++-- app/models/course_participant.rb | 35 ++ app/models/team.rb | 324 +++++++++++++++++- app/models/teams_participants.rb | 60 +++- 6 files changed, 514 insertions(+), 59 deletions(-) create mode 100644 app/models/course_participant.rb diff --git a/app/controllers/teams_participants_controller.rb b/app/controllers/teams_participants_controller.rb index b0b716106..e445a0345 100644 --- a/app/controllers/teams_participants_controller.rb +++ b/app/controllers/teams_participants_controller.rb @@ -1,4 +1,4 @@ -class TeamsUsersController < ApplicationController +class TeamsParticipantsController < ApplicationController include AuthorizationHelper def action_allowed? @@ -109,4 +109,4 @@ def delete_selected redirect_to action: 'list', id: params[:id] end -end +end \ No newline at end of file diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 45e8d2acf..e4b6ed13b 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -26,6 +26,33 @@ def num_review_rounds rounds_of_reviews end + # Check if a user is on a team in this assignment + def user_on_team?(user) + teams.joins(:users).exists?(users: { id: user.id }) + end + + # Add a user to a team for this assignment + def add_user_to_team(user, team_id) + team = teams.find_by(id: team_id) + raise "Team not found in this assignment." unless team + + if user_on_team?(user) + raise "The user #{user.name} is already assigned to a team for this assignment." + end + + team.add_member(user, id) + rescue StandardError => e + raise "Failed to add user to team: #{e.message}" + end + + # Validate if a user is a participant of this assignment + def validate_participant(user) + participant = participants.find_by(user_id: user.id) + raise "User #{user.name} is not a participant of this assignment." unless participant + + participant + 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. diff --git a/app/models/course.rb b/app/models/course.rb index 9e70ccf7d..c7ed5b506 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -1,52 +1,101 @@ class Course < ApplicationRecord + enum locale: Locale.code_name_to_db_encoding + + # Associations + has_many :ta_mappings, dependent: :destroy + has_many :tas, through: :ta_mappings + has_many :assignments, dependent: :destroy belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id' - belongs_to :institution, foreign_key: 'institution_id' + belongs_to :institution, foreign_key: 'institutions_id' + has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy + has_many :course_teams, foreign_key: 'parent_id', dependent: :destroy + has_one :course_node, foreign_key: 'node_object_id', dependent: :destroy + has_many :notifications, dependent: :destroy + has_paper_trail + + # Validations validates :name, presence: true validates :directory_path, presence: true - has_many :ta_mappings, dependent: :destroy - has_many :tas, through: :ta_mappings - # Returns the submission directory for the course - 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 + '/' + # Return teams associated with this course + def get_teams + course_teams end - # Add a Teaching Assistant to the course - def add_ta(user) - if user.nil? - return { success: false, message: "The user with id #{user.id} does not exist" } - elsif TaMapping.exists?(ta_id: user.id, course_id: id) - return { success: false, message: "The user with id #{user.id} is already a TA for this course." } + # Get all participants in this course + def get_participants + participants + end + + # Get a specific participant by user ID + def get_participant(user_id) + participants.find_by(user_id: user_id) + end + + # Check if a user is on any team in the course + def user_on_team?(user) + course_teams.joins(:users).exists?(users: { id: user.id }) + end + + # Add a user as a participant to this course + def add_participant(user_name) + user = User.find_by(name: user_name) + raise "No user account exists with the name #{user_name}. Please create the user first." unless user + + participant = participants.find_by(user_id: user.id) + if participant + raise "The user #{user.name} is already a participant." else - ta_mapping = TaMapping.create(ta_id: user.id, course_id: id) - user.update(role: Role::TEACHING_ASSISTANT) - if ta_mapping.save - return { success: true, data: ta_mapping.slice(:course_id, :ta_id) } - else - return { success: false, message: ta_mapping.errors } - end + participants.create(user_id: user.id, permission_granted: user.master_permission_granted) end end - # Removes Teaching Assistant from the course - def remove_ta(ta_id) - ta_mapping = ta_mappings.find_by(ta_id: ta_id, course_id: :id) - return { success: false, message: "No TA mapping found for the specified course and TA" } if ta_mapping.nil? - ta = User.find(ta_mapping.ta_id) - ta_count = TaMapping.where(ta_id: ta_id).size - 1 - if ta_count.zero? - ta.update(role: Role::STUDENT) + def remove_participants(user_ids) + user_ids.each do |user_id| + participant = participants.find_by(user_id: user_id) + raise "User with ID #{user_id} is not a participant." if participant.nil? + + participant.destroy end - ta_mapping.destroy - { success: true, ta_name: ta.name } end - # Creates a copy of the course - def copy_course - new_course = dup - new_course.directory_path += '_copy' - new_course.name += '_copy' - new_course.save + # Add a user to a team + def add_user_to_team(user, team_id) + team = course_teams.find_by(id: team_id) + raise "Team not found in this course." unless team + + if user_on_team?(user) + raise "The user #{user.name} is already assigned to a team for this course." + end + + team.add_member(user, id) end -end \ No newline at end of file + + # Copy participants from an assignment to this course + def copy_participants_from_assignment(assignment_id) + participants = AssignmentParticipant.where(parent_id: assignment_id) + errors = [] + + participants.each do |participant| + user = User.find(participant.user_id) + begin + add_participant(user.name) + rescue StandardError => e + errors << e.message + end + end + + raise errors.join('
') unless errors.empty? + end + + # Returns the path for this course + def path + raise 'Path cannot be created. The course must be associated with an instructor.' if instructor_id.nil? + + Rails.root.join('pg_data', FileHelper.clean_path(instructor.name), FileHelper.clean_path(directory_path)) + end + + # Analytics + require 'analytic/course_analytic' + include CourseAnalytic +end 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/team.rb b/app/models/team.rb index afb8ac66f..94cc9a90a 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -1,20 +1,324 @@ class Team < ApplicationRecord - has_many :signed_up_teams, dependent: :destroy has_many :teams_users, dependent: :destroy has_many :users, through: :teams_users - has_many :participants - belongs_to :assignment - attr_accessor :max_participants + has_many :join_team_requests, dependent: :destroy + has_one :team_node, foreign_key: :node_object_id, dependent: :destroy + has_many :signed_up_teams, dependent: :destroy + has_many :bids, dependent: :destroy + has_paper_trail + + 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) + } + + # Allowed types of teams -- ASSIGNMENT teams or COURSE teams + def self.allowed_types + # non-interpolated array of single-quoted strings + %w[Assignment Course] + end + + # Get the participants of the given team + def participants + users.where(parent_id: parent_id || current_user_id).flat_map(&:participants) + end + alias get_participants participants + + # copies content of one object to the another + def self.copy_content(source, destination) + source.each do |each_element| + each_element.copy(destination.id) + end + end + + # enum method for team clone operations + def self.team_operation + { inherit: 'inherit', bequeath: 'bequeath' }.freeze + end + + # Get the response review map + def responses + participants.flat_map(&:responses) + end + + # Delete the given team + def delete + TeamsUser.where(team_id: id).find_each(&:destroy) + node = TeamNode.find_by(node_object_id: id) + node.destroy if node + destroy + end + + # Get the node type of the tree structure + def node_type + 'TeamNode' + end + + # Get the names of the users + def author_names + names = [] + users.each do |user| + names << user.fullname + end + names + end - # 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 + # Check if the user exist + def user?(user) + users.include? user + end + + # Check if the current team is full? def full? + return false if parent_id.nil? # course team, does not max_team_size + + max_team_members = Assignment.find(parent_id).max_team_size + curr_team_size = Team.size(id) + curr_team_size >= max_team_members + end + + # Add member to the team, changed to hash by E1776 + def add_member(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 + + # Define the size of the team + def self.size(team_id) + #TeamsUser.where(team_id: team_id).count + count = 0 + members = TeamsUser.where(team_id: team_id) + members.each do |member| + member_name = member.name + unless member_name.include?(' (Mentor)') + count = count + 1 + end + end + count + end + + # Copy method to copy this team + def copy_members(new_team) + members = TeamsUser.where(team_id: id) + members.each do |member| + t_user = TeamsUser.create(team_id: new_team.id, user_id: member.user_id) + parent = Object.const_get(parent_model).find(parent_id) + TeamUserNode.create(parent_id: parent.id, node_object_id: t_user.id) + end + end + + # Check if the team exists + def self.check_for_existing(parent, name, team_type) + list = Object.const_get(team_type + 'Team').where(parent_id: parent.id, name: name) + raise TeamExistsError, "The team name #{name} is already in use." unless list.empty? + end + + # Algorithm + # Start by adding single members to teams that are one member too small. + # Add two-member teams to teams that two members too small. etc. + def self.randomize_all_by_parent(parent, team_type, min_team_size) + participants = Participant.where(parent_id: parent.id, type: parent.class.to_s + 'Participant', can_mentor: [false, nil]) + participants = participants.sort { rand(-1..1) } + users = participants.map { |p| User.find(p.user_id) }.to_a + # find teams still need team members and users who are not in any team + teams = Team.where(parent_id: parent.id, type: parent.class.to_s + 'Team').to_a + teams.each do |team| + TeamsUser.where(team_id: team.id).each do |teams_user| + users.delete(User.find(teams_user.user_id)) + end + end + teams.reject! { |team| Team.size(team.id) >= min_team_size } + # sort teams that still need members by decreasing team size + teams.sort_by { |team| Team.size(team.id) }.reverse! + # insert users who are not in any team to teams still need team members + assign_single_users_to_teams(min_team_size, parent, teams, users) if !users.empty? && !teams.empty? + # If all the existing teams are fill to the min_team_size and we still have more users, create teams for them. + create_team_from_single_users(min_team_size, parent, team_type, users) unless users.empty? + end + + # Creates teams from a list of users based on minimum team size + # Then assigns the created team to the parent object + def self.create_team_from_single_users(min_team_size, parent, team_type, users) + num_of_teams = users.length.fdiv(min_team_size).ceil + next_team_member_index = 0 + (1..num_of_teams).to_a.each do |i| + team = Object.const_get(team_type + 'Team').create(name: 'Team_' + i.to_s, parent_id: parent.id) + TeamNode.create(parent_id: parent.id, node_object_id: team.id) + min_team_size.times do + break if next_team_member_index >= users.length - max_participants ||= 3 - if participants.count >= max_participants - true + user = users[next_team_member_index] + team.add_member(user, parent.id) + next_team_member_index += 1 + end + end + end + + # Assigns list of users to list of teams based on minimum team size + def self.assign_single_users_to_teams(min_team_size, parent, teams, users) + teams.each do |team| + curr_team_size = Team.size(team.id) + member_num_difference = min_team_size - curr_team_size + while member_num_difference > 0 + team.add_member(users.first, parent.id) + users.delete(users.first) + member_num_difference -= 1 + break if users.empty? + end + break if users.empty? + end + end + + # Generate the team name + def self.generate_team_name(_team_name_prefix = '') + last_team = Team.where('name LIKE ?', "#{_team_name_prefix} Team_%") + .order("CAST(SUBSTRING(name, LENGTH('#{_team_name_prefix} Team_') + 1) AS UNSIGNED) DESC") + .first + counter = last_team ? last_team.name.scan(/\d+/).first.to_i + 1 : 1 + team_name = "#{_team_name_prefix} Team_#{counter}" + team_name + end + + # Extract team members from the csv and push to DB, changed to hash by E1776 + def import_team_members(row_hash) + row_hash[:teammembers].each_with_index do |teammate, _index| + user = User.find_by(name: teammate.to_s) + if user.nil? + raise ImportError, "The user '#{teammate}' was not found. Create this user?" + else + add_member(user) if TeamsUser.find_by(team_id: id, user_id: user.id).nil? + end + end + end + + # changed to hash by E1776 + def self.import(row_hash, id, options, teamtype) + raise ArgumentError, 'Not enough fields on this line.' if row_hash.empty? || (row_hash[:teammembers].empty? && (options[:has_teamname] == 'true_first' || options[:has_teamname] == 'true_last')) || (row_hash[:teammembers].empty? && (options[:has_teamname] == 'true_first' || options[:has_teamname] == 'true_last')) + + if options[:has_teamname] == 'true_first' || options[:has_teamname] == 'true_last' + name = row_hash[:teamname].to_s + team = where(['name =? && parent_id =?', name, id]).first + team_exists = !team.nil? + name = handle_duplicate(team, name, id, options[:handle_dups], teamtype) else - false + if teamtype.is_a?(CourseTeam) + name = generate_team_name(Course.find(id).name) + elsif teamtype.is_a?(AssignmentTeam) + name = generate_team_name(Assignment.find(id).name) + end + end + if name + team = Object.const_get(teamtype.to_s).create_team_and_node(id) + team.name = name + team.save + end + + # insert team members into team unless team was pre-existing & we ignore duplicate teams + + team.import_team_members(row_hash) unless team_exists && options[:handle_dups] == 'ignore' + end + + # Handle existence of the duplicate team + def self.handle_duplicate(team, name, id, handle_dups, teamtype) + return name if team.nil? # no duplicate + return nil if handle_dups == 'ignore' # ignore: do not create the new team + + if handle_dups == 'rename' # rename: rename new team + if teamtype.is_a?(CourseTeam) + return generate_team_name(Course.find(id).name) + elsif teamtype.is_a?(AssignmentTeam) + return generate_team_name(Assignment.find(id).name) + end end + if handle_dups == 'replace' # replace: delete old team + team.delete + return name + else # handle_dups = "insert" + return nil + end + end + + # Export the teams to csv + def self.export(csv, parent_id, options, teamtype) + if teamtype.is_a?(CourseTeam) + teams = CourseTeam.where(parent_id: parent_id) + elsif teamtype.is_a?(AssignmentTeam) + teams = AssignmentTeam.where(parent_id: parent_id) + end + teams.each do |team| + output = [] + output.push(team.name) + if options[:team_name] == 'false' + team_members = TeamsUser.where(team_id: team.id) + team_members.each do |user| + output.push(user.name) + end + end + csv << output + end + csv + end + + # Create the team with corresponding tree node + def self.create_team_and_node(id) + parent = parent_model id # current_task will be either a course object or an assignment object. + team_name = Team.generate_team_name(parent.name) + team = create(name: team_name, parent_id: id) + # new teamnode will have current_task.id as parent_id and team_id as node_object_id. + TeamNode.create(parent_id: id, node_object_id: team.id) + ExpertizaLogger.info LoggerMessage.new('Model:Team', '', "New TeamNode created with teamname #{team_name}") + team + end + + # E1991 : This method allows us to generate + # team names based on whether anonymized view + # is set or not. The logic is similar to + # existing logic of User model. + def name(ip_address = nil) + if User.anonymized_view?(ip_address) + return "Anonymized_Team_#{self[:id]}" + else + return self[:name] + end + end + + # REFACTOR END:: class methods import export moved from course_team & assignment_team to here + + # Create the team with corresponding tree node and given users + def self.create_team_with_users(parent_id, user_ids) + team = create_team_and_node(parent_id) + + user_ids.each do |user_id| + remove_user_from_previous_team(parent_id, user_id) + + # Create new team_user and team_user node + team.add_member(User.find(user_id)) + end + team + end + + # Removes the specified user from any team of the specified assignment + def self.remove_user_from_previous_team(parent_id, user_id) + team_user = TeamsUser.where(user_id: user_id).find { |team_user_obj| team_user_obj.team.parent_id == parent_id } + begin + team_user.destroy + rescue StandardError + nil + end + end + + def self.find_team_users(assignment_id, user_id) + TeamsUser.joins('INNER JOIN teams ON teams_users.team_id = teams.id') + .select('teams.id as t_id') + .where('teams.parent_id = ? and teams_users.user_id = ?', assignment_id, user_id) end end \ No newline at end of file diff --git a/app/models/teams_participants.rb b/app/models/teams_participants.rb index 9e1768b94..a1e4b53e3 100644 --- a/app/models/teams_participants.rb +++ b/app/models/teams_participants.rb @@ -2,22 +2,62 @@ class TeamsUser < ApplicationRecord belongs_to :user belongs_to :team + # Returns the user's name. If an IP address is provided, it may influence the name retrieval logic. def name(ip_address = nil) - name = user.name(ip_address) + 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) + # Retrieves all team members for a given team ID as a collection of User objects. + # Allows optional exclusion of certain roles. + def self.get_team_members(team_id, excluded_roles: []) + users = where(team_id: team_id).includes(:user).map(&:user) + return users if excluded_roles.empty? - return users + # Exclude users with specific roles, if any + users.reject { |user| excluded_roles.include?(user.role) } 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 + # Adds a user to a team. Raises an error if the user is already on the team. + # Returns the created TeamsUser object if successful. + def self.add_to_team(user_id, team_id) + # Check if the user is already a team member + if where(user_id: user_id, team_id: team_id).exists? + raise "The user is already a member of the team." + end + + # Create the association + create!(user_id: user_id, team_id: team_id) + rescue ActiveRecord::RecordInvalid => e + raise "Failed to add user to team: #{e.message}" end + # Removes a user's association with a team. Raises an error if the association does not exist. + def self.remove_from_team(user_id, team_id) + team_user = find_by(user_id: user_id, team_id: team_id) + raise "The user is not a member of this team." if team_user.nil? + + team_user.destroy + rescue StandardError => e + raise "Failed to remove user from team: #{e.message}" + end + + # Transfers a user from one team to another within the same context. + # Ensures that the user is removed from the previous team before adding to the new one. + def self.transfer_user_to_team(user_id, old_team_id, new_team_id) + remove_from_team(user_id, old_team_id) + add_to_team(user_id, new_team_id) + rescue StandardError => e + raise "Failed to transfer user between teams: #{e.message}" + end + + # Checks if a user is already on a team. + def self.user_on_team?(user_id, team_id) + where(user_id: user_id, team_id: team_id).exists? + end + + # Retrieves all teams for a given user as a collection of Team objects. + def self.get_teams_for_user(user_id) + team_ids = where(user_id: user_id).pluck(:team_id) + Team.where(id: team_ids) + end end From a5534356539f1c776691b786f785a866c39a7356 Mon Sep 17 00:00:00 2001 From: manideepika21 Date: Sat, 30 Nov 2024 12:31:08 -0500 Subject: [PATCH 09/18] Moved teams_users model from expertiza to reimplementation-back-end repository --- app/models/teams_participants.rb | 101 +++++++++++++++++-------------- 1 file changed, 57 insertions(+), 44 deletions(-) diff --git a/app/models/teams_participants.rb b/app/models/teams_participants.rb index a1e4b53e3..d2086e969 100644 --- a/app/models/teams_participants.rb +++ b/app/models/teams_participants.rb @@ -1,63 +1,76 @@ class TeamsUser < ApplicationRecord belongs_to :user belongs_to :team + has_one :team_user_node, foreign_key: 'node_object_id', dependent: :destroy + has_paper_trail + # attr_accessible :user_id, :team_id # unnecessary protected attributes - # Returns the user's name. If an IP address is provided, it may influence the name retrieval logic. def name(ip_address = nil) - user.name(ip_address) - end + name = user.name(ip_address) - # Retrieves all team members for a given team ID as a collection of User objects. - # Allows optional exclusion of certain roles. - def self.get_team_members(team_id, excluded_roles: []) - users = where(team_id: team_id).includes(:user).map(&:user) - return users if excluded_roles.empty? + # E2115 Mentor Management + # Indicate that someone is a Mentor in the UI. The view code is + # often hard to follow, and this is the best place we could find + # for this to go. + name += ' (Mentor)' if MentorManagement.user_a_mentor?(user) + name + end - # Exclude users with specific roles, if any - users.reject { |user| excluded_roles.include?(user.role) } + def delete + TeamUserNode.find_by(node_object_id: id).destroy + team = self.team + destroy + team.delete if team.teams_users.empty? end - # Adds a user to a team. Raises an error if the user is already on the team. - # Returns the created TeamsUser object if successful. - def self.add_to_team(user_id, team_id) - # Check if the user is already a team member - if where(user_id: user_id, team_id: team_id).exists? - raise "The user is already a member of the team." - end + def get_team_members(team_id); end - # Create the association - create!(user_id: user_id, team_id: team_id) - rescue ActiveRecord::RecordInvalid => e - raise "Failed to add user to team: #{e.message}" + # 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 - # Removes a user's association with a team. Raises an error if the association does not exist. - def self.remove_from_team(user_id, team_id) - team_user = find_by(user_id: user_id, team_id: team_id) - raise "The user is not a member of this team." if team_user.nil? - - team_user.destroy - rescue StandardError => e - raise "Failed to remove user from team: #{e.message}" + # Returns the first entry in the TeamUsers table for a given team id + def self.first_by_team_id(team_id) + TeamsUser.where('team_id = ?', team_id).first end - # Transfers a user from one team to another within the same context. - # Ensures that the user is removed from the previous team before adding to the new one. - def self.transfer_user_to_team(user_id, old_team_id, new_team_id) - remove_from_team(user_id, old_team_id) - add_to_team(user_id, new_team_id) - rescue StandardError => e - raise "Failed to transfer user between teams: #{e.message}" + # Determines whether a team is empty of not + def self.team_empty?(team_id) + team_members = TeamsUser.where('team_id = ?', team_id) + team_members.blank? end - # Checks if a user is already on a team. - def self.user_on_team?(user_id, team_id) - where(user_id: user_id, team_id: team_id).exists? + # Add member to the team they were invited to and accepted the invite for + def self.add_member_to_invited_team(invitee_user_id, invited_user_id, assignment_id) + can_add_member = false + users_teams = TeamsUser.where(['user_id = ?', invitee_user_id]) + users_teams.each do |team| + new_team = AssignmentTeam.where(['id = ? and parent_id = ?', team.team_id, assignment_id]).first + unless new_team.nil? + can_add_member = new_team.add_member(User.find(invited_user_id), assignment_id) + end + end + can_add_member end - # Retrieves all teams for a given user as a collection of Team objects. - def self.get_teams_for_user(user_id) - team_ids = where(user_id: user_id).pluck(:team_id) - Team.where(id: team_ids) + # 2015-5-27 [zhewei]: + # We just remove the topic_id field from the participants table. + def self.team_id(assignment_id, user_id) + # team_id variable represents the team_id for this user in this assignment + team_id = nil + teams_users = TeamsUser.where(user_id: user_id) + teams_users.each do |teams_user| + if teams_user.team_id == nil + next + end + team = Team.find(teams_user.team_id) + if team.parent_id == assignment_id + team_id = teams_user.team_id + break + end + end + team_id end -end +end \ No newline at end of file From 638ed3135e39d617a348c8faaf1b16c73ff036d1 Mon Sep 17 00:00:00 2001 From: bhuvan chandra kurra Date: Mon, 2 Dec 2024 21:19:47 -0500 Subject: [PATCH 10/18] modified assignments model to move the assignment specific methods to this model --- app/models/assignment.rb | 60 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 45e8d2acf..148ffed46 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. @@ -193,6 +191,56 @@ def varying_rubrics_by_round? rubric_with_round.present? end + +#E2479 +# Checks if a specific user is already part of any team for this assignment. +# This method queries the teams associated with this assignment and checks if the user is a member. +# Params: +# - user: The user to check for team membership. +# Returns: +# - true if the user is part of any team for this assignment. +# - false otherwise. +def is_participant_on_team?(user) + # Join the teams and team users tables to check if a record exists for the user. + teams.joins(:teams_users).exists?(teams_users: { user_id: user.id }) +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 validate_team_participant_for_assignment?(user) + # Check if the user is already part of a team for this assignment. + if is_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 + +# Retrieves all courses assigned to a specific instructor. +# This method fetches courses for which the given user is listed as the instructor. +# The courses are ordered alphabetically by their names for consistent display. +# Params: +# - user: The instructor user whose courses are to be fetched. +# Returns: +# - An ActiveRecord relation containing the courses assigned to the instructor. +def self.fetch_courses_for_instructor(user) + # Query for courses where the instructor ID matches the user's ID, ordered by course name. + Course.where(instructor_id: user.id).order(:name) +end end \ No newline at end of file From 2d4b9804d40880841d4ee94d9db71f96c63e6e87 Mon Sep 17 00:00:00 2001 From: bhuvan chandra kurra Date: Mon, 2 Dec 2024 21:22:58 -0500 Subject: [PATCH 11/18] modified course model to move the course specific methods to this model --- app/models/course.rb | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/models/course.rb b/app/models/course.rb index 9e70ccf7d..f2874d43d 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,30 @@ def copy_course new_course.name += '_copy' new_course.save end + #E2479 + # 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 already_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 From f97c630072c606b25b1317e435f0b97733fd10a8 Mon Sep 17 00:00:00 2001 From: bhuvan chandra kurra Date: Mon, 2 Dec 2024 21:28:45 -0500 Subject: [PATCH 12/18] modified teams participants model to move the teams participants methods to this model --- app/models/teams_participants.rb | 109 +++++++++++++++++-------------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/app/models/teams_participants.rb b/app/models/teams_participants.rb index a1e4b53e3..9d4b49fb7 100644 --- a/app/models/teams_participants.rb +++ b/app/models/teams_participants.rb @@ -1,63 +1,72 @@ -class TeamsUser < ApplicationRecord +class TeamsParticipant < ApplicationRecord belongs_to :user belongs_to :team + has_one :team_user_node, foreign_key: 'node_object_id', dependent: :destroy + has_paper_trail - # Returns the user's name. If an IP address is provided, it may influence the name retrieval logic. - def name(ip_address = nil) - user.name(ip_address) - end - - # Retrieves all team members for a given team ID as a collection of User objects. - # Allows optional exclusion of certain roles. - def self.get_team_members(team_id, excluded_roles: []) - users = where(team_id: team_id).includes(:user).map(&:user) - return users if excluded_roles.empty? + 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) - # Exclude users with specific roles, if any - users.reject { |user| excluded_roles.include?(user.role) } + return users end - # Adds a user to a team. Raises an error if the user is already on the team. - # Returns the created TeamsUser object if successful. - def self.add_to_team(user_id, team_id) - # Check if the user is already a team member - if where(user_id: user_id, team_id: team_id).exists? - raise "The user is already a member of the team." - end - - # Create the association - create!(user_id: user_id, team_id: team_id) - rescue ActiveRecord::RecordInvalid => e - raise "Failed to add user to team: #{e.message}" + # 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 - # Removes a user's association with a team. Raises an error if the association does not exist. - def self.remove_from_team(user_id, team_id) - team_user = find_by(user_id: user_id, team_id: team_id) - raise "The user is not a member of this team." if team_user.nil? +#E2479 +# Retrieves the name of the user associated with the team member. +# Optionally appends ' (Mentor)' if the user is a mentor. +# Params: +# - ip_address (optional): The IP address of the user (used for logging or contextual name resolution). +# Returns: +# - The name of the user, with '(Mentor)' appended if the user is a mentor. +def display_name(ip_address = nil) + participant_name = user.name(ip_address) + participant_name += ' (Mentor)' if MentorManagement.user_a_mentor?(user) + participant_name +end - team_user.destroy - rescue StandardError => e - raise "Failed to remove user from team: #{e.message}" - end +# Deletes multiple team members (identified by their IDs) in bulk. +# This method is used for efficient removal of multiple TeamsUser records. +# Params: +# - team_user_ids: An array of IDs of the TeamsUser records to be deleted. +# Returns: +# - The number of records deleted (implicit return from destroy_all). +def self.bulk_delete(team_user_ids) + # Delete all TeamsUser records matching the provided IDs. + where(id: team_user_ids).destroy_all +end - # Transfers a user from one team to another within the same context. - # Ensures that the user is removed from the previous team before adding to the new one. - def self.transfer_user_to_team(user_id, old_team_id, new_team_id) - remove_from_team(user_id, old_team_id) - add_to_team(user_id, new_team_id) - rescue StandardError => e - raise "Failed to transfer user between teams: #{e.message}" - end +# Checks whether a specific user is a member of a given team. +# Params: +# - user_id: The ID of the user to check. +# - team_id: The ID of the team to check for membership. +# Returns: +# - true if the user is a member of the team. +# - false otherwise. +def self.participant_part_of_team?(user_id, team_id) + # Check if a TeamsUser record exists with the specified user and team IDs. + exists?(user_id: user_id, team_id: team_id) +end + +# Checks whether a team is empty (i.e., has no members). +# Params: +# - team_id: The ID of the team to check. +# Returns: +# - true if the team has no members. +# - false otherwise. +def self.is_team_empty?(team_id) + # Retrieve all members of the team. + team_members = TeamsUser.where('team_id = ?', team_id) + + # Return true if the team has no members; false otherwise. + team_members.blank? +end - # Checks if a user is already on a team. - def self.user_on_team?(user_id, team_id) - where(user_id: user_id, team_id: team_id).exists? - end - # Retrieves all teams for a given user as a collection of Team objects. - def self.get_teams_for_user(user_id) - team_ids = where(user_id: user_id).pluck(:team_id) - Team.where(id: team_ids) - end end From c3893ef54e283504ba060c52422e268d2ddcb726 Mon Sep 17 00:00:00 2001 From: manideepika21 Date: Mon, 2 Dec 2024 21:30:27 -0500 Subject: [PATCH 13/18] Modified teams_users_controller to adhere to DRY principles and refactores the code with detailed comments and the variable names --- .../teams_participants_controller.rb | 176 ++++--- app/models/assignment.rb | 85 ++-- app/models/assignment_participant.rb | 9 +- app/models/course.rb | 147 +++--- app/models/mentor_management.rb | 122 +++++ app/models/mentored_team.rb | 27 ++ app/models/team.rb | 447 ++++++------------ app/models/teams_participants.rb | 110 +++-- 8 files changed, 559 insertions(+), 564 deletions(-) create mode 100644 app/models/mentor_management.rb create mode 100644 app/models/mentored_team.rb diff --git a/app/controllers/teams_participants_controller.rb b/app/controllers/teams_participants_controller.rb index e445a0345..c6796c102 100644 --- a/app/controllers/teams_participants_controller.rb +++ b/app/controllers/teams_participants_controller.rb @@ -1,8 +1,8 @@ class TeamsParticipantsController < ApplicationController include AuthorizationHelper + # Determines if the current user is allowed to perform the requested action. def action_allowed? - # Allow duty updation for a team if current user is student, else require TA or above Privileges. if %w[update_duties].include? params[:action] current_user_has_student_privileges? else @@ -10,103 +10,127 @@ def action_allowed? end end + # Fetches and renders an auto-complete list of possible team members based on a partial name input. def auto_complete_for_user_name - team = Team.find(session[:team_id]) - @users = team.get_possible_team_members(params[:user][:name]) - render inline: "<%= auto_complete_result @users, 'name' %>", layout: false + # 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 - # Example of duties: manager, designer, programmer, tester. Finds TeamsUser and save preferred Duty + # Updates the duty (role) assigned to a participant in a team. def update_duties - team_user = TeamsUser.find(params[:teams_user_id]) - team_user.update_attribute(:duty_id, params[:teams_user]['duty_id']) + # 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 - @team = Team.find(params[:id]) - @assignment = Assignment.find(@team.parent_id) - @teams_users = TeamsUser.page(params[:page]).per_page(10).where(['team_id = ?', params[:id]]) + # 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 new + # Fetch the team for which a participant is to be added. @team = Team.find(params[:id]) end + # Adds a new participant to a team after validation. def create - user = User.find_by(name: params[:user][:name].strip) - unless user - urlCreate = url_for controller: 'users', action: 'new' - flash[:error] = "\"#{params[:user][:name].strip}\" is not defined. Please create this user before continuing." - end + # Find the user by their name from the input. + participant = find_user_by_name + + # Fetch the team using the provided ID. + current_team = find_team_by_id + + # Return early if validation fails. + return unless validate_participant_and_team(participant, current_team) + + # Add the participant to the team. + add_participant_to_team(participant, current_team) - team = Team.find(params[:id]) - unless user.nil? - if team.is_a?(AssignmentTeam) - assignment = Assignment.find(team.parent_id) - if assignment.user_on_team?(user) - flash[:error] = "This user is already assigned to a team for this assignment" - redirect_back fallback_location: root_path - return - end - if AssignmentParticipant.find_by(user_id: user.id, parent_id: assignment.id).nil? - urlAssignmentParticipantList = url_for controller: 'participants', action: 'list', id: assignment.id, model: 'Assignment', authorization: 'participant' - flash[:error] = "\"#{user.name}\" is not a participant of the current assignment. Please add this user before continuing." - else - begin - add_member_return = team.add_member(user, team.parent_id) - rescue - flash[:error] = "The user #{user.name} is already a member of the team #{team.name}" - redirect_back fallback_location: root_path - return - end - flash[:error] = 'This team already has the maximum number of members.' if add_member_return == false - end - else # CourseTeam - course = Course.find(team.parent_id) - if course.user_on_team?(user) - flash[:error] = "This user is already assigned to a team for this course" - redirect_back fallback_location: root_path - return - end - if CourseParticipant.find_by(user_id: user.id, parent_id: course.id).nil? - urlCourseParticipantList = url_for controller: 'participants', action: 'list', id: course.id, model: 'Course', authorization: 'participant' - flash[:error] = "\"#{user.name}\" is not a participant of the current course. Please add this user before continuing." - else - begin - add_member_return = team.add_member(user, team.parent_id) - rescue - flash[:error] = "The user #{user.name} is already a member of the team #{team.name}" - redirect_back fallback_location: root_path - return - end - flash[:error] = 'This team already has the maximum number of members.' if add_member_return == false - if add_member_return - @teams_user = TeamsUser.last - undo_link("The team user \"#{user.name}\" has been successfully added to \"#{team.name}\".") - end - 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_user_by_name + # Locate the user by their name. + participant = User.find_by(name: params[:user][:name].strip) + + # Display an error if the user is not found. + unless participant + flash[:error] = user_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_member?(participant) + else + Course.find(team.parent_id).valid_team_member?(participant) + end - redirect_to controller: 'teams', action: 'list', id: team.parent_id + # 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 - def delete - @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 + # Adds the participant to the team while handling constraints. + def add_participant_to_team(participant, team) + # Add the participant to the team and handle the outcome. + addition_result = team.add_member_with_handling(participant, team.parent_id) + handle_addition_result(participant, team, addition_result) end - def delete_selected - params[:item].each do |item_id| - team_user = TeamsUser.find(item_id).first - team_user.destroy + # Handles the result of adding a participant to the team. + def handle_addition_result(participant, team, addition_result) + if addition_result == false + flash[:error] = 'This team already has the maximum number of members.' + else + undo_link("The participant \"#{participant.name}\" has been successfully added to \"#{team.name}\".") end + end - redirect_to action: 'list', id: params[:id] + # Generates an error message when a user is not found. + def user_not_found_error + new_user_url = url_for controller: 'users', action: 'new' + "\"#{params[:user][:name].strip}\" is not defined. Please create this user before continuing." end -end \ No newline at end of file +end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index e4b6ed13b..9b3167c03 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,36 +23,10 @@ def review_questionnaire_id def teams? @has_teams ||= teams.any? end + def num_review_rounds rounds_of_reviews end - - # Check if a user is on a team in this assignment - def user_on_team?(user) - teams.joins(:users).exists?(users: { id: user.id }) - end - - # Add a user to a team for this assignment - def add_user_to_team(user, team_id) - team = teams.find_by(id: team_id) - raise "Team not found in this assignment." unless team - - if user_on_team?(user) - raise "The user #{user.name} is already assigned to a team for this assignment." - end - - team.add_member(user, id) - rescue StandardError => e - raise "Failed to add user to team: #{e.message}" - end - - # Validate if a user is a participant of this assignment - def validate_participant(user) - participant = participants.find_by(user_id: user.id) - raise "User #{user.name} is not a participant of this assignment." unless participant - - participant - 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. @@ -106,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. @@ -125,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. @@ -144,6 +116,7 @@ def copy copied_assignment end + def is_calibrated? is_calibrated end @@ -160,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? @@ -210,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. @@ -219,7 +190,55 @@ def varying_rubrics_by_round? # Check if any rubric has a specified round rubric_with_round.present? end - + #E2479 + # Checks if a specific user is already part of any team for this assignment. +# This method queries the teams associated with this assignment and checks if the user is a member. +# Params: +# - user: The user to check for team membership. +# Returns: +# - true if the user is part of any team for this assignment. +# - false otherwise. +def is_participant_on_team?(user) + # Join the teams and team users tables to check if a record exists for the user. + teams.joins(:teams_users).exists?(teams_users: { user_id: user.id }) +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 validate_team_participant_for_assignment?(user) + # Check if the user is already part of a team for this assignment. + if is_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 + +# Retrieves all courses assigned to a specific instructor. +# This method fetches courses for which the given user is listed as the instructor. +# The courses are ordered alphabetically by their names for consistent display. +# Params: +# - user: The instructor user whose courses are to be fetched. +# Returns: +# - An ActiveRecord relation containing the courses assigned to the instructor. +def self.fetch_courses_for_instructor(user) + # Query for courses where the instructor ID matches the user's ID, ordered by course name. + Course.where(instructor_id: user.id).order(:name) +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 c7ed5b506..f2874d43d 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -1,101 +1,78 @@ class Course < ApplicationRecord - enum locale: Locale.code_name_to_db_encoding - - # Associations - has_many :ta_mappings, dependent: :destroy - has_many :tas, through: :ta_mappings - has_many :assignments, dependent: :destroy belongs_to :instructor, class_name: 'User', foreign_key: 'instructor_id' - belongs_to :institution, foreign_key: 'institutions_id' - has_many :participants, class_name: 'CourseParticipant', foreign_key: 'parent_id', dependent: :destroy - has_many :course_teams, foreign_key: 'parent_id', dependent: :destroy - has_one :course_node, foreign_key: 'node_object_id', dependent: :destroy - has_many :notifications, dependent: :destroy - has_paper_trail - - # Validations + belongs_to :institution, foreign_key: 'institution_id' validates :name, presence: true validates :directory_path, presence: true + has_many :ta_mappings, dependent: :destroy + has_many :tas, through: :ta_mappings - # Return teams associated with this course - def get_teams - course_teams - end - - # Get all participants in this course - def get_participants - participants - end - - # Get a specific participant by user ID - def get_participant(user_id) - participants.find_by(user_id: user_id) - end - - # Check if a user is on any team in the course - def user_on_team?(user) - course_teams.joins(:users).exists?(users: { id: user.id }) + # Returns the submission directory for the course + 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 user as a participant to this course - def add_participant(user_name) - user = User.find_by(name: user_name) - raise "No user account exists with the name #{user_name}. Please create the user first." unless user - - participant = participants.find_by(user_id: user.id) - if participant - raise "The user #{user.name} is already a participant." + + # Add a Teaching Assistant to the course + def add_ta(user) + if user.nil? + return { success: false, message: "The user with id #{user.id} does not exist" } + elsif TaMapping.exists?(ta_id: user.id, course_id: id) + return { success: false, message: "The user with id #{user.id} is already a TA for this course." } else - participants.create(user_id: user.id, permission_granted: user.master_permission_granted) - end - end - - def remove_participants(user_ids) - user_ids.each do |user_id| - participant = participants.find_by(user_id: user_id) - raise "User with ID #{user_id} is not a participant." if participant.nil? - - participant.destroy + ta_mapping = TaMapping.create(ta_id: user.id, course_id: id) + user.update(role: Role::TEACHING_ASSISTANT) + if ta_mapping.save + return { success: true, data: ta_mapping.slice(:course_id, :ta_id) } + else + return { success: false, message: ta_mapping.errors } + end end end - # Add a user to a team - def add_user_to_team(user, team_id) - team = course_teams.find_by(id: team_id) - raise "Team not found in this course." unless team - - if user_on_team?(user) - raise "The user #{user.name} is already assigned to a team for this course." + # Removes Teaching Assistant from the course + def remove_ta(ta_id) + ta_mapping = ta_mappings.find_by(ta_id: ta_id, course_id: :id) + return { success: false, message: "No TA mapping found for the specified course and TA" } if ta_mapping.nil? + ta = User.find(ta_mapping.ta_id) + ta_count = TaMapping.where(ta_id: ta_id).size - 1 + if ta_count.zero? + ta.update(role: Role::STUDENT) end - - team.add_member(user, id) + ta_mapping.destroy + { success: true, ta_name: ta.name } end - # Copy participants from an assignment to this course - def copy_participants_from_assignment(assignment_id) - participants = AssignmentParticipant.where(parent_id: assignment_id) - errors = [] - - participants.each do |participant| - user = User.find(participant.user_id) - begin - add_participant(user.name) - rescue StandardError => e - errors << e.message - end - end - - raise errors.join('
') unless errors.empty? + # Creates a copy of the course + def copy_course + new_course = dup + new_course.directory_path += '_copy' + new_course.name += '_copy' + new_course.save end - - # Returns the path for this course - def path - raise 'Path cannot be created. The course must be associated with an instructor.' if instructor_id.nil? - - Rails.root.join('pg_data', FileHelper.clean_path(instructor.name), FileHelper.clean_path(directory_path)) + #E2479 + # 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 already_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 - - # Analytics - require 'analytic/course_analytic' - include CourseAnalytic end + +end \ No newline at end of file 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 94cc9a90a..1a7c2973f 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -1,324 +1,147 @@ class Team < ApplicationRecord + has_many :signed_up_teams, dependent: :destroy has_many :teams_users, dependent: :destroy - has_many :users, through: :teams_users has_many :join_team_requests, dependent: :destroy has_one :team_node, foreign_key: :node_object_id, dependent: :destroy - has_many :signed_up_teams, dependent: :destroy + has_many :users, through: :teams_users has_many :bids, dependent: :destroy - has_paper_trail - + has_many :participants + belongs_to :assignment + 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) } - - # Allowed types of teams -- ASSIGNMENT teams or COURSE teams - def self.allowed_types - # non-interpolated array of single-quoted strings - %w[Assignment Course] - end - - # Get the participants of the given team - def participants - users.where(parent_id: parent_id || current_user_id).flat_map(&:participants) - end - alias get_participants participants - - # copies content of one object to the another - def self.copy_content(source, destination) - source.each do |each_element| - each_element.copy(destination.id) - end - end - - # enum method for team clone operations - def self.team_operation - { inherit: 'inherit', bequeath: 'bequeath' }.freeze - end - - # Get the response review map - def responses - participants.flat_map(&:responses) - end - - # Delete the given team - def delete - TeamsUser.where(team_id: id).find_each(&:destroy) - node = TeamNode.find_by(node_object_id: id) - node.destroy if node - destroy - end - - # Get the node type of the tree structure - def node_type - 'TeamNode' - end - - # Get the names of the users - def author_names - names = [] - users.each do |user| - names << user.fullname - end - names - end - - # Check if the user exist - def user?(user) - users.include? user - end - - # Check if the current team is full? + # 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? - return false if parent_id.nil? # course team, does not max_team_size - - max_team_members = Assignment.find(parent_id).max_team_size - curr_team_size = Team.size(id) - curr_team_size >= max_team_members - end - - # Add member to the team, changed to hash by E1776 - def add_member(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 - - # Define the size of the team - def self.size(team_id) - #TeamsUser.where(team_id: team_id).count - count = 0 - members = TeamsUser.where(team_id: team_id) - members.each do |member| - member_name = member.name - unless member_name.include?(' (Mentor)') - count = count + 1 - end - end - count - end - - # Copy method to copy this team - def copy_members(new_team) - members = TeamsUser.where(team_id: id) - members.each do |member| - t_user = TeamsUser.create(team_id: new_team.id, user_id: member.user_id) - parent = Object.const_get(parent_model).find(parent_id) - TeamUserNode.create(parent_id: parent.id, node_object_id: t_user.id) - end - end - - # Check if the team exists - def self.check_for_existing(parent, name, team_type) - list = Object.const_get(team_type + 'Team').where(parent_id: parent.id, name: name) - raise TeamExistsError, "The team name #{name} is already in use." unless list.empty? - end - - # Algorithm - # Start by adding single members to teams that are one member too small. - # Add two-member teams to teams that two members too small. etc. - def self.randomize_all_by_parent(parent, team_type, min_team_size) - participants = Participant.where(parent_id: parent.id, type: parent.class.to_s + 'Participant', can_mentor: [false, nil]) - participants = participants.sort { rand(-1..1) } - users = participants.map { |p| User.find(p.user_id) }.to_a - # find teams still need team members and users who are not in any team - teams = Team.where(parent_id: parent.id, type: parent.class.to_s + 'Team').to_a - teams.each do |team| - TeamsUser.where(team_id: team.id).each do |teams_user| - users.delete(User.find(teams_user.user_id)) - end - end - teams.reject! { |team| Team.size(team.id) >= min_team_size } - # sort teams that still need members by decreasing team size - teams.sort_by { |team| Team.size(team.id) }.reverse! - # insert users who are not in any team to teams still need team members - assign_single_users_to_teams(min_team_size, parent, teams, users) if !users.empty? && !teams.empty? - # If all the existing teams are fill to the min_team_size and we still have more users, create teams for them. - create_team_from_single_users(min_team_size, parent, team_type, users) unless users.empty? - end - - # Creates teams from a list of users based on minimum team size - # Then assigns the created team to the parent object - def self.create_team_from_single_users(min_team_size, parent, team_type, users) - num_of_teams = users.length.fdiv(min_team_size).ceil - next_team_member_index = 0 - (1..num_of_teams).to_a.each do |i| - team = Object.const_get(team_type + 'Team').create(name: 'Team_' + i.to_s, parent_id: parent.id) - TeamNode.create(parent_id: parent.id, node_object_id: team.id) - min_team_size.times do - break if next_team_member_index >= users.length - - user = users[next_team_member_index] - team.add_member(user, parent.id) - next_team_member_index += 1 - end - end - end - - # Assigns list of users to list of teams based on minimum team size - def self.assign_single_users_to_teams(min_team_size, parent, teams, users) - teams.each do |team| - curr_team_size = Team.size(team.id) - member_num_difference = min_team_size - curr_team_size - while member_num_difference > 0 - team.add_member(users.first, parent.id) - users.delete(users.first) - member_num_difference -= 1 - break if users.empty? - end - break if users.empty? - end - end - - # Generate the team name - def self.generate_team_name(_team_name_prefix = '') - last_team = Team.where('name LIKE ?', "#{_team_name_prefix} Team_%") - .order("CAST(SUBSTRING(name, LENGTH('#{_team_name_prefix} Team_') + 1) AS UNSIGNED) DESC") - .first - counter = last_team ? last_team.name.scan(/\d+/).first.to_i + 1 : 1 - team_name = "#{_team_name_prefix} Team_#{counter}" - team_name - end - - # Extract team members from the csv and push to DB, changed to hash by E1776 - def import_team_members(row_hash) - row_hash[:teammembers].each_with_index do |teammate, _index| - user = User.find_by(name: teammate.to_s) - if user.nil? - raise ImportError, "The user '#{teammate}' was not found. Create this user?" - else - add_member(user) if TeamsUser.find_by(team_id: id, user_id: user.id).nil? - end - end - end - - # changed to hash by E1776 - def self.import(row_hash, id, options, teamtype) - raise ArgumentError, 'Not enough fields on this line.' if row_hash.empty? || (row_hash[:teammembers].empty? && (options[:has_teamname] == 'true_first' || options[:has_teamname] == 'true_last')) || (row_hash[:teammembers].empty? && (options[:has_teamname] == 'true_first' || options[:has_teamname] == 'true_last')) - - if options[:has_teamname] == 'true_first' || options[:has_teamname] == 'true_last' - name = row_hash[:teamname].to_s - team = where(['name =? && parent_id =?', name, id]).first - team_exists = !team.nil? - name = handle_duplicate(team, name, id, options[:handle_dups], teamtype) + max_participants ||= 3 + if participants.count >= max_participants + true else - if teamtype.is_a?(CourseTeam) - name = generate_team_name(Course.find(id).name) - elsif teamtype.is_a?(AssignmentTeam) - name = generate_team_name(Assignment.find(id).name) - end - end - if name - team = Object.const_get(teamtype.to_s).create_team_and_node(id) - team.name = name - team.save - end - - # insert team members into team unless team was pre-existing & we ignore duplicate teams + false + end + end + + # Adds a user to the team while handling potential errors such as duplicate membership. +# This is a wrapper around the `add_member` method with error handling. +# Params: +# - user: The user to be added to the team. +# - parent_id: The ID of the parent entity (assignment or course). +# Returns: +# - true if the user was successfully added. +# - A hash with an error message if the addition fails. +def add_member_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 + +# Adds a user to the team if they are not already a member and the team is not full. +# Also creates the necessary team user and team node entries. +# Params: +# - user: The user to be added. +# - _assignment_id (optional): The ID of the assignment (not used directly in this implementation). +# Returns: +# - true if the user was successfully added to the team. +# - false if the team is full or if the user cannot be added. +def add_member(user, _assignment_id = nil) + # Raise an error if the user is already a member of the team. + raise "The user #{user.name} is already a member of the team #{name}" if member?(user) + + # Check if the team is not full before adding the user. + if !full? + # Create a relationship between the user and the team. + team_user_relationship = TeamsUser.create(user_id: user.id, team_id: id) + + # Create the corresponding team user node in the hierarchy. + team_node = TeamNode.find_by(node_object_id: id) + TeamUserNode.create(parent_id: team_node.id, node_object_id: team_user_relationship.id) + + # Add the user as a participant for the parent assignment or course. + add_participant_to_parent_entity(parent_id, user) + + # Log the successful addition of the user to the team. + ExpertizaLogger.info LoggerMessage.new('Model:Team', user.name, "Added member to the team #{id}") + true + else + false + end +end + +# Checks if the given user is already a member of the team. +# Params: +# - user: The user to check for membership. +# Returns: +# - true if the user is already a member of the team. +# - false otherwise. +def participant_present?(user) + users.include?(user) +end + +# Retrieves all participants associated with the team. +# Participants are derived from the users belonging to the team. +# Returns: +# - An array of participant objects. +def participants + users.where(parent_id: parent_id || current_user_id).flat_map(&:participants) +end +alias get_participants participants + +# Retrieves the full names of all users in the team. +# Returns: +# - An array of strings representing the full names of the team members. +def participant_full_names + users.map(&:fullname) +end +alias author_names participant_full_names + +# Determines the current size of the team, excluding mentors. +# Params: +# - team_id: The ID of the team whose size is being checked. +# Returns: +# - The number of non-mentor members in the team. +def self.size(team_id) + count = 0 + team_participants = TeamsUser.where(team_id: team_id) + + # Exclude mentors from the count. + team_participants.each do |team_participants| + team_participants_name = team_participants.name + count += 1 unless team_participants_name.include?(' (Mentor)') + end + count +end + +# Checks if a team with the given name already exists for a parent entity (assignment or course). +# Raises an error if a team with the same name already exists. +# Params: +# - parent: The parent entity (assignment or course) associated with the team. +# - name: The name of the team to check for uniqueness. +# - team_type: The type of team (e.g., 'Assignment', 'Course'). +def self.check_for_existing(parent, name, team_type) + existing_teams = Object.const_get("#{team_type}Team").where(parent_id: parent.id, name: name) + + # Raise an error if any team with the same name is found. + unless existing_teams.empty? + raise TeamExistsError, "The team name #{name} is already in use." + end +end + +# Retrieves all teams that a specific user belongs to for a given assignment. +# Params: +# - assignment_id: The ID of the assignment. +# - user_id: The ID of the user. +# Returns: +# - A list of team IDs for the user within the specified assignment. +def self.find_team_participants(assignment_id, user_id) + TeamsUser.joins('INNER JOIN teams ON teams_users.team_id = teams.id') + .select('teams.id as team_id') + .where('teams.parent_id = ? AND teams_users.user_id = ?', assignment_id, user_id) +end - team.import_team_members(row_hash) unless team_exists && options[:handle_dups] == 'ignore' - end - - # Handle existence of the duplicate team - def self.handle_duplicate(team, name, id, handle_dups, teamtype) - return name if team.nil? # no duplicate - return nil if handle_dups == 'ignore' # ignore: do not create the new team - - if handle_dups == 'rename' # rename: rename new team - if teamtype.is_a?(CourseTeam) - return generate_team_name(Course.find(id).name) - elsif teamtype.is_a?(AssignmentTeam) - return generate_team_name(Assignment.find(id).name) - end - end - if handle_dups == 'replace' # replace: delete old team - team.delete - return name - else # handle_dups = "insert" - return nil - end - end - - # Export the teams to csv - def self.export(csv, parent_id, options, teamtype) - if teamtype.is_a?(CourseTeam) - teams = CourseTeam.where(parent_id: parent_id) - elsif teamtype.is_a?(AssignmentTeam) - teams = AssignmentTeam.where(parent_id: parent_id) - end - teams.each do |team| - output = [] - output.push(team.name) - if options[:team_name] == 'false' - team_members = TeamsUser.where(team_id: team.id) - team_members.each do |user| - output.push(user.name) - end - end - csv << output - end - csv - end - - # Create the team with corresponding tree node - def self.create_team_and_node(id) - parent = parent_model id # current_task will be either a course object or an assignment object. - team_name = Team.generate_team_name(parent.name) - team = create(name: team_name, parent_id: id) - # new teamnode will have current_task.id as parent_id and team_id as node_object_id. - TeamNode.create(parent_id: id, node_object_id: team.id) - ExpertizaLogger.info LoggerMessage.new('Model:Team', '', "New TeamNode created with teamname #{team_name}") - team - end - - # E1991 : This method allows us to generate - # team names based on whether anonymized view - # is set or not. The logic is similar to - # existing logic of User model. - def name(ip_address = nil) - if User.anonymized_view?(ip_address) - return "Anonymized_Team_#{self[:id]}" - else - return self[:name] - end - end - - # REFACTOR END:: class methods import export moved from course_team & assignment_team to here - - # Create the team with corresponding tree node and given users - def self.create_team_with_users(parent_id, user_ids) - team = create_team_and_node(parent_id) - - user_ids.each do |user_id| - remove_user_from_previous_team(parent_id, user_id) - - # Create new team_user and team_user node - team.add_member(User.find(user_id)) - end - team - end - - # Removes the specified user from any team of the specified assignment - def self.remove_user_from_previous_team(parent_id, user_id) - team_user = TeamsUser.where(user_id: user_id).find { |team_user_obj| team_user_obj.team.parent_id == parent_id } - begin - team_user.destroy - rescue StandardError - nil - end - end - - def self.find_team_users(assignment_id, user_id) - TeamsUser.joins('INNER JOIN teams ON teams_users.team_id = teams.id') - .select('teams.id as t_id') - .where('teams.parent_id = ? and teams_users.user_id = ?', assignment_id, user_id) - end end \ No newline at end of file diff --git a/app/models/teams_participants.rb b/app/models/teams_participants.rb index d2086e969..a12c7608e 100644 --- a/app/models/teams_participants.rb +++ b/app/models/teams_participants.rb @@ -3,74 +3,70 @@ class TeamsUser < ApplicationRecord belongs_to :team has_one :team_user_node, foreign_key: 'node_object_id', dependent: :destroy has_paper_trail - # attr_accessible :user_id, :team_id # unnecessary protected attributes - def name(ip_address = nil) - name = user.name(ip_address) - - # E2115 Mentor Management - # Indicate that someone is a Mentor in the UI. The view code is - # often hard to follow, and this is the best place we could find - # for this to go. - name += ' (Mentor)' if MentorManagement.user_a_mentor?(user) - name - 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) - def delete - TeamUserNode.find_by(node_object_id: id).destroy - team = self.team - destroy - team.delete if team.teams_users.empty? + return users end - def get_team_members(team_id); 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 - # Returns the first entry in the TeamUsers table for a given team id - def self.first_by_team_id(team_id) - TeamsUser.where('team_id = ?', team_id).first - end +#E2479 +# Retrieves the name of the user associated with the team member. +# Optionally appends ' (Mentor)' if the user is a mentor. +# Params: +# - ip_address (optional): The IP address of the user (used for logging or contextual name resolution). +# Returns: +# - The name of the user, with '(Mentor)' appended if the user is a mentor. +def display_name(ip_address = nil) + participant_name = user.name(ip_address) + participant_name += ' (Mentor)' if MentorManagement.user_a_mentor?(user) + participant_name +end - # Determines whether a team is empty of not - def self.team_empty?(team_id) - team_members = TeamsUser.where('team_id = ?', team_id) - team_members.blank? - end +# Deletes multiple team members (identified by their IDs) in bulk. +# This method is used for efficient removal of multiple `TeamsUser` records. +# Params: +# - team_user_ids: An array of IDs of the `TeamsUser` records to be deleted. +# Returns: +# - The number of records deleted (implicit return from `destroy_all`). +def self.bulk_delete(team_user_ids) + # Delete all `TeamsUser` records matching the provided IDs. + where(id: team_user_ids).destroy_all +end + +# Checks whether a specific user is a member of a given team. +# Params: +# - user_id: The ID of the user to check. +# - team_id: The ID of the team to check for membership. +# Returns: +# - true if the user is a member of the team. +# - false otherwise. +def self.participant_part_of_team?(user_id, team_id) + # Check if a `TeamsUser` record exists with the specified user and team IDs. + exists?(user_id: user_id, team_id: team_id) +end + +# Checks whether a team is empty (i.e., has no members). +# Params: +# - team_id: The ID of the team to check. +# Returns: +# - true if the team has no members. +# - false otherwise. +def self.is_team_empty?(team_id) + # Retrieve all members of the team. + team_members = TeamsUser.where('team_id = ?', team_id) + + # Return true if the team has no members; false otherwise. + team_members.blank? +end - # Add member to the team they were invited to and accepted the invite for - def self.add_member_to_invited_team(invitee_user_id, invited_user_id, assignment_id) - can_add_member = false - users_teams = TeamsUser.where(['user_id = ?', invitee_user_id]) - users_teams.each do |team| - new_team = AssignmentTeam.where(['id = ? and parent_id = ?', team.team_id, assignment_id]).first - unless new_team.nil? - can_add_member = new_team.add_member(User.find(invited_user_id), assignment_id) - end - end - can_add_member - end - # 2015-5-27 [zhewei]: - # We just remove the topic_id field from the participants table. - def self.team_id(assignment_id, user_id) - # team_id variable represents the team_id for this user in this assignment - team_id = nil - teams_users = TeamsUser.where(user_id: user_id) - teams_users.each do |teams_user| - if teams_user.team_id == nil - next - end - team = Team.find(teams_user.team_id) - if team.parent_id == assignment_id - team_id = teams_user.team_id - break - end - end - team_id - end end \ No newline at end of file From 52fdca59f44e19437ecabb15d49cfa5fc87d2081 Mon Sep 17 00:00:00 2001 From: manideepika21 Date: Tue, 3 Dec 2024 23:06:09 -0500 Subject: [PATCH 14/18] Changed the variable names and added comments --- Gemfile | 2 +- .../v1}/teams_participants_controller.rb | 71 ++++++----- app/models/assignment.rb | 26 +--- app/models/team.rb | 43 +++++-- app/models/teams_participants.rb | 114 +++--------------- .../teams_participants_controller_spec.rb | 89 ++++++++++++++ spec/factories/factories.rb | 22 +++- 7 files changed, 202 insertions(+), 165 deletions(-) rename app/controllers/{ => api/v1}/teams_participants_controller.rb (60%) create mode 100644 spec/controllers/teams_participants_controller_spec.rb diff --git a/Gemfile b/Gemfile index 1786836d0..53202af81 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '3.2.1' +ruby '3.2.5' gem 'mysql2', '~> 0.5.5' gem 'puma', '~> 5.0' diff --git a/app/controllers/teams_participants_controller.rb b/app/controllers/api/v1/teams_participants_controller.rb similarity index 60% rename from app/controllers/teams_participants_controller.rb rename to app/controllers/api/v1/teams_participants_controller.rb index 1606f92a7..c500ee01a 100644 --- a/app/controllers/teams_participants_controller.rb +++ b/app/controllers/api/v1/teams_participants_controller.rb @@ -1,4 +1,4 @@ -class TeamsParticipantsController < ApplicationController +class Api::V1::TeamsParticipantsController < ApplicationController include AuthorizationHelper # Determines if the current user is allowed to perform the requested action. @@ -11,7 +11,7 @@ def action_allowed? end # Fetches and renders an auto-complete list of possible team members based on a partial name input. - def auto_complete_for_participtant_name + def auto_complete_for_participant_name # Fetch the current team using the session-stored `team_id`. current_team = Team.find(session[:team_id]) @@ -25,7 +25,7 @@ def auto_complete_for_participtant_name # 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 = TeamsParticipant.find(params[:teams_user_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']) @@ -35,7 +35,7 @@ def update_duties end # Displays a paginated list of all participants in a specific team. - def list + def list_participants # Fetch the team based on the provided ID. current_team = Team.find(params[:id]) @@ -43,46 +43,58 @@ def list associated_assignment_or_course = Assignment.find(current_team.parent_id) # Query and paginate participants of the current team. - @team_participants = TeamsParticipant.page(params[:page]).per_page(10).where(team_id: current_team.id) + @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 new + def add_new_participant # Fetch the team for which a participant is to be added. @team = Team.find(params[:id]) end # Adds a new participant to a team after validation. - def create + def create_participant # Find the user by their name from the input. - participant = find_user_by_name + find_participant = find_participant_by_name # Fetch the team using the provided ID. current_team = find_team_by_id - # Return early if validation fails. - return unless validate_participant_and_team(participant, current_team) - - # Add the participant to the team. - add_participant_to_team(participant, current_team) + if validate_participant_and_team(participant, team) + if team.add_participants_with_handling(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 + #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 + private # Helper method to find a user by their name. - def find_user_by_name + def find_participant_by_name # Locate the user by their name. - participant = User.find_by(name: params[:user][:name].strip) + find_participant = User.find_by(name: params[:user][:name].strip) # Display an error if the user is not found. - unless participant - flash[:error] = user_not_found_error + unless find_participant + flash[:error] = participant_not_found_error redirect_back fallback_location: root_path end participant @@ -97,9 +109,9 @@ def find_team_by_id 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_member?(participant) + Assignment.find(team.parent_id).valid_team_participant?(participant) else - Course.find(team.parent_id).valid_team_member?(participant) + Course.find(team.parent_id).valid_team_participant?(participant) end # Handle validation errors if any. @@ -113,24 +125,29 @@ def validate_participant_and_team(participant, team) end # Adds the participant to the team while handling constraints. - def add_participant_to_team(participant, team) + def add_participant_to_team(find_participant, team) # Add the participant to the team and handle the outcome. - addition_result = team.add_member_with_handling(participant, team.parent_id) - handle_addition_result(participant, team, addition_result) + addition_result = find_team_by_id.add_participant(find_participant, team.parent_id) + handle_addition_result(find_participant, team, addition_result) end # Handles the result of adding a participant to the team. - def handle_addition_result(participant, team, addition_result) + def handle_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 participant \"#{participant.name}\" has been successfully added to \"#{team.name}\".") + 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 user_not_found_error - new_user_url = url_for controller: 'users', action: 'new' - "\"#{params[:user][:name].strip}\" is not defined. Please create this user before continuing." + 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 f6b943d45..c1d114f7b 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -192,17 +192,6 @@ def varying_rubrics_by_round? end #E2479 -# Checks if a specific user is already part of any team for this assignment. -# This method queries the teams associated with this assignment and checks if the user is a member. -# Params: -# - user: The user to check for team membership. -# Returns: -# - true if the user is part of any team for this assignment. -# - false otherwise. -def is_participant_on_team?(user) - # Join the teams and team users tables to check if a record exists for the user. - teams.joins(:teams_users).exists?(teams_users: { user_id: user.id }) -end # Validates if a user is eligible to join a team for the current assignment. # This method ensures that: @@ -214,7 +203,7 @@ def is_participant_on_team?(user) # - 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 validate_team_participant_for_assignment?(user) +def valid_team_participant_for_assignment?(user) # Check if the user is already part of a team for this assignment. if is_user_on_team?(user) { success: false, error: "This user is already assigned to a team for this assignment" } @@ -229,17 +218,4 @@ def validate_team_participant_for_assignment?(user) end end -# Retrieves all courses assigned to a specific instructor. -# This method fetches courses for which the given user is listed as the instructor. -# The courses are ordered alphabetically by their names for consistent display. -# Params: -# - user: The instructor user whose courses are to be fetched. -# Returns: -# - An ActiveRecord relation containing the courses assigned to the instructor. -def self.fetch_courses_for_instructor(user) - # Query for courses where the instructor ID matches the user's ID, ordered by course name. - Course.where(instructor_id: user.id).order(:name) -end - - end \ No newline at end of file diff --git a/app/models/team.rb b/app/models/team.rb index b387fe906..563532596 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -1,15 +1,16 @@ class Team < ApplicationRecord has_many :signed_up_teams, dependent: :destroy - has_many :teams_participants, 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_participants + has_many :users, through: :teams_users has_many :bids, dependent: :destroy has_many :participants - belongs_to :assignment + belongs_to :assignment, optional: true, foreign_key: 'parent_id' + validates :name, presence: true attr_accessor :max_participants scope :find_team_for_assignment_and_user, lambda { |assignment_id, user_id| - joins(:teams_participants).where('teams.parent_id = ? AND teams_participants.user_id = ?', 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 @@ -21,7 +22,29 @@ def full? false end end + def add_member(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 +# Generate the team name +def self.generate_team_name(_team_name_prefix = '') + last_team = Team.where('name LIKE ?', "#{_team_name_prefix} Team_%") + .order("CAST(SUBSTRING(name, LENGTH('#{_team_name_prefix} Team_') + 1) AS UNSIGNED) DESC") + .first + counter = last_team ? last_team.name.scan(/\d+/).first.to_i + 1 : 1 + team_name = "#{_team_name_prefix} Team_#{counter}" + team_name +end # Adds a user to the team while handling potential errors such as duplicate membership. # This is a wrapper around the `add_member` method with error handling. # Params: @@ -87,10 +110,10 @@ def participant_present?(user) # Participants are derived from the users belonging to the team. # Returns: # - An array of participant objects. -def participants +def participants_in_team users.where(parent_id: parent_id || current_user_id).flat_map(&:participants) end -alias get_participants participants +alias get_participants participants_in_team # Retrieves the full names of all users in the team. # Returns: @@ -107,7 +130,7 @@ def participant_full_names # - The number of non-mentor members in the team. def self.size(team_id) count = 0 - team_participants = TeamsParticipant.where(team_id: team_id) + team_participants = TeamsUser.where(team_id: team_id) # Exclude mentors from the count. team_participants.each do |team_participants| @@ -123,7 +146,7 @@ def self.size(team_id) # - parent: The parent entity (assignment or course) associated with the team. # - name: The name of the team to check for uniqueness. # - team_type: The type of team (e.g., 'Assignment', 'Course'). -def self.check_for_existing(parent, name, team_type) +def self.check_for_existing_team(parent, name, team_type) existing_teams = Object.const_get("#{team_type}Team").where(parent_id: parent.id, name: name) # Raise an error if any team with the same name is found. @@ -139,9 +162,9 @@ def self.check_for_existing(parent, name, team_type) # Returns: # - A list of team IDs for the user within the specified assignment. def self.find_team_participants(assignment_id, user_id) - TeamsParticipant.joins('INNER JOIN teams ON teams_participants.team_id = teams.id') + TeamsUser.joins('INNER JOIN teams ON teams_users.team_id = teams.id') .select('teams.id as team_id') - .where('teams.parent_id = ? AND teams_participants.user_id = ?', assignment_id, user_id) + .where('teams.parent_id = ? AND teams_users.user_id = ?', assignment_id, user_id) end end \ No newline at end of file diff --git a/app/models/teams_participants.rb b/app/models/teams_participants.rb index 5774bf4d2..ba9197f02 100644 --- a/app/models/teams_participants.rb +++ b/app/models/teams_participants.rb @@ -1,115 +1,31 @@ -class TeamsParticipant < ApplicationRecord +class TeamsUser < ApplicationRecord belongs_to :user belongs_to :team has_one :team_user_node, foreign_key: 'node_object_id', dependent: :destroy - has_paper_trail + # 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) - users = User.where(id: user_ids) - - return users + User.where(id: user_ids) end - # Removes entry in the TeamUsers table for the given user and given team id + # Removes entry in the TeamUsers table for the given user and 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 = TeamsUser.find_by(user_id: user_id, team_id: team_id) team_user&.destroy end #E2479 -# Retrieves the name of the user associated with the team member. -# Optionally appends ' (Mentor)' if the user is a mentor. -# Params: -# - ip_address (optional): The IP address of the user (used for logging or contextual name resolution). -# Returns: -# - The name of the user, with '(Mentor)' appended if the user is a mentor. -def display_name(ip_address = nil) - participant_name = user.name(ip_address) - participant_name += ' (Mentor)' if MentorManagement.user_a_mentor?(user) - participant_name -<<<<<<< HEAD -======= -end - -# Deletes multiple team members (identified by their IDs) in bulk. -# This method is used for efficient removal of multiple TeamsUser records. -# Params: -# - team_user_ids: An array of IDs of the TeamsUser records to be deleted. -# Returns: -# - The number of records deleted (implicit return from destroy_all). -def self.bulk_delete(team_user_ids) - # Delete all TeamsUser records matching the provided IDs. - where(id: team_user_ids).destroy_all -end - -# Checks whether a specific user is a member of a given team. -# Params: -# - user_id: The ID of the user to check. -# - team_id: The ID of the team to check for membership. -# Returns: -# - true if the user is a member of the team. -# - false otherwise. -def self.participant_part_of_team?(user_id, team_id) - # Check if a TeamsUser record exists with the specified user and team IDs. - exists?(user_id: user_id, team_id: team_id) -end - -# Checks whether a team is empty (i.e., has no members). -# Params: -# - team_id: The ID of the team to check. -# Returns: -# - true if the team has no members. -# - false otherwise. -def self.is_team_empty?(team_id) - # Retrieve all members of the team. - team_members = TeamsUser.where('team_id = ?', team_id) - - # Return true if the team has no members; false otherwise. - team_members.blank? -end - - ->>>>>>> f97c630072c606b25b1317e435f0b97733fd10a8 -end - -# Deletes multiple team members (identified by their IDs) in bulk. -# This method is used for efficient removal of multiple `TeamsUser` records. -# Params: -# - team_user_ids: An array of IDs of the `TeamsUser` records to be deleted. -# Returns: -# - The number of records deleted (implicit return from `destroy_all`). -def self.bulk_delete(team_user_ids) - # Delete all `TeamsUser` records matching the provided IDs. - where(id: team_user_ids).destroy_all -end - -# Checks whether a specific user is a member of a given team. -# Params: -# - user_id: The ID of the user to check. -# - team_id: The ID of the team to check for membership. -# Returns: -# - true if the user is a member of the team. -# - false otherwise. -def self.participant_part_of_team?(user_id, team_id) - # Check if a `TeamsUser` record exists with the specified user and team IDs. - exists?(user_id: user_id, team_id: team_id) -end - -# Checks whether a team is empty (i.e., has no members). -# Params: -# - team_id: The ID of the team to check. -# Returns: -# - true if the team has no members. -# - false otherwise. -def self.is_team_empty?(team_id) - # Retrieve all members of the team. - team_members = TeamsUser.where('team_id = ?', team_id) + # Deletes multiple team members in bulk. + def self.bulk_delete_participants(team_user_ids) + where(id: team_user_ids).destroy_all + end - # Return true if the team has no members; false otherwise. - team_members.blank? + # 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 - - -end \ No newline at end of file diff --git a/spec/controllers/teams_participants_controller_spec.rb b/spec/controllers/teams_participants_controller_spec.rb new file mode 100644 index 000000000..96530960f --- /dev/null +++ b/spec/controllers/teams_participants_controller_spec.rb @@ -0,0 +1,89 @@ +describe 'GET #auto_complete_for_participant_name' do + let(:team) { create(:team) } + let(:participant) { create(:user, name: 'John Doe') } + + before do + allow(Team).to receive(:find).and_return(team) + allow(team).to receive(:get_possible_team_members).and_return([participant]) + end + + it 'fetches potential team members based on input' do + session[:team_id] = team.id + get :auto_complete_for_participant_name, params: { user: { name: 'John' } } + expect(response).to be_successful + expect(assigns(:potential_team_members)).to eq([participant]) + end + + it 'renders the auto-complete suggestions' do + session[:team_id] = team.id + get :auto_complete_for_participant_name, params: { user: { name: 'John' } } + expect(response.body).to include(participant.name) + end +end +describe 'POST #update_duties' do + let(:team_participant) { create(:teams_participant) } + + it 'updates the duty of a team member' do + post :update_duties, params: { + teams_user_id: team_participant.id, + teams_user: { duty_id: 2 }, + participant_id: team_participant.user_id + } + expect(team_participant.reload.duty_id).to eq(2) + expect(response).to redirect_to(controller: 'student_teams', action: 'view', student_id: team_participant.user_id) + end +end +describe 'GET #list_participants' do + let(:team) { create(:team) } + let(:assignment) { create(:assignment, id: team.parent_id) } + let(:participant) { create(:teams_participant, team: team) } + + it 'lists participants of a team' do + get :list_participants, params: { id: team.id } + expect(assigns(:team)).to eq(team) + expect(assigns(:assignment)).to eq(assignment) + expect(assigns(:team_participants)).to include(participant) + end +end +describe 'GET #add_new_participant' do + let(:team) { create(:team) } + + 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) + expect(assigns(:team)).to eq(team) + end +end +describe 'POST #create_participant' do + let(:team) { create(:team) } + let(:participant) { create(:user) } + + before do + allow(Team).to receive(:find).and_return(team) + allow(User).to receive(:find_by).and_return(participant) + allow(team).to receive(:add_member_with_handling).and_return(true) + end + + it 'adds a participant to a team successfully' do + post :create_participant, params: { user: { name: participant.name }, id: team.id } + expect(flash[:notice]).to include(participant.name) + expect(response).to redirect_to(controller: 'teams', action: 'list', id: team.parent_id) + end + + it 'shows an error when the participant is invalid' do + allow(team).to receive(:add_member_with_handling).and_return(false) + post :create_participant, params: { user: { name: participant.name }, id: team.id } + expect(flash[:error]).to include('maximum number of members') + end +end +describe 'DELETE #delete_participant' do + let(:team) { create(:team) } + let(:participant) { create(:user) } + let(:team_participant) { create(:teams_participant, team: team, user: participant) } + + it 'deletes the participant from the team' do + delete :delete_participant, params: { id: team_participant.id } + expect(flash[:notice]).to include(participant.name) + expect(response).to redirect_to(controller: 'teams', action: 'list', id: team.parent_id) + end +end diff --git a/spec/factories/factories.rb b/spec/factories/factories.rb index eb06d7682..a2820c536 100644 --- a/spec/factories/factories.rb +++ b/spec/factories/factories.rb @@ -1,6 +1,22 @@ FactoryBot.define do + factory :user do + name { "John Doe" } + end + + factory :team do + name { "Team A" } + parent_id { 1 } + end + + factory :teams_participant do + association :user + association :team + duty_id { 1 } + end + + factory :assignment do + name { "Assignment 1" } + end + end - - -end From 4f3b30869b3ca4a30c46369145b3deaa96d4aba5 Mon Sep 17 00:00:00 2001 From: manideepika21 Date: Tue, 3 Dec 2024 23:09:54 -0500 Subject: [PATCH 15/18] Added spec file to test the teams_participants_controller --- Gemfile | 2 +- app/models/team.rb | 129 +----------- .../teams_participants_controller_spec.rb | 184 +++++++++++------- spec/factories/factories.rb | 19 +- 4 files changed, 120 insertions(+), 214 deletions(-) diff --git a/Gemfile b/Gemfile index 53202af81..1786836d0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '3.2.5' +ruby '3.2.1' gem 'mysql2', '~> 0.5.5' gem 'puma', '~> 5.0' diff --git a/app/models/team.rb b/app/models/team.rb index 563532596..d0f6157c2 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -6,7 +6,7 @@ class Team < ApplicationRecord has_many :users, through: :teams_users has_many :bids, dependent: :destroy has_many :participants - belongs_to :assignment, optional: true, foreign_key: 'parent_id' + belongs_to :assignment validates :name, presence: true attr_accessor :max_participants scope :find_team_for_assignment_and_user, lambda { |assignment_id, user_id| @@ -22,7 +22,10 @@ def full? false end end - def add_member(user, _assignment_id = nil) + + #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 @@ -36,23 +39,7 @@ def add_member(user, _assignment_id = nil) end can_add_member end -# Generate the team name -def self.generate_team_name(_team_name_prefix = '') - last_team = Team.where('name LIKE ?', "#{_team_name_prefix} Team_%") - .order("CAST(SUBSTRING(name, LENGTH('#{_team_name_prefix} Team_') + 1) AS UNSIGNED) DESC") - .first - counter = last_team ? last_team.name.scan(/\d+/).first.to_i + 1 : 1 - team_name = "#{_team_name_prefix} Team_#{counter}" - team_name -end - # Adds a user to the team while handling potential errors such as duplicate membership. -# This is a wrapper around the `add_member` method with error handling. -# Params: -# - user: The user to be added to the team. -# - parent_id: The ID of the parent entity (assignment or course). -# Returns: -# - true if the user was successfully added. -# - A hash with an error message if the addition fails. + def add_participants_with_handling(user, parent_id) begin # Attempt to add the user to the team. @@ -63,108 +50,4 @@ def add_participants_with_handling(user, parent_id) { success: false, error: "The user #{user.name} is already a member of the team #{name}" } end end - -# Adds a user to the team if they are not already a member and the team is not full. -# Also creates the necessary team user and team node entries. -# Params: -# - user: The user to be added. -# - _assignment_id (optional): The ID of the assignment (not used directly in this implementation). -# Returns: -# - true if the user was successfully added to the team. -# - false if the team is full or if the user cannot be added. -def add_participant(user, _assignment_id = nil) - # Raise an error if the user is already a member of the team. - raise "The user #{user.name} is already a member of the team #{name}" if member?(user) - - # Check if the team is not full before adding the user. - if !full? - # Create a relationship between the user and the team. - team_user_relationship = TeamsParticipant.create(user_id: user.id, team_id: id) - - # Create the corresponding team user node in the hierarchy. - team_node = TeamNode.find_by(node_object_id: id) - TeamUserNode.create(parent_id: team_node.id, node_object_id: team_user_relationship.id) - - # Add the user as a participant for the parent assignment or course. - add_participant_to_parent_entity(parent_id, user) - - # Log the successful addition of the user to the team. - ExpertizaLogger.info LoggerMessage.new('Model:Team', user.name, "Added member to the team #{id}") - true - else - false - end -end - -# Checks if the given user is already a member of the team. -# Params: -# - user: The user to check for membership. -# Returns: -# - true if the user is already a member of the team. -# - false otherwise. -def participant_present?(user) - users.include?(user) -end - -# Retrieves all participants associated with the team. -# Participants are derived from the users belonging to the team. -# Returns: -# - An array of participant objects. -def participants_in_team - users.where(parent_id: parent_id || current_user_id).flat_map(&:participants) -end -alias get_participants participants_in_team - -# Retrieves the full names of all users in the team. -# Returns: -# - An array of strings representing the full names of the team members. -def participant_full_names - users.map(&:fullname) -end -alias author_names participant_full_names - -# Determines the current size of the team, excluding mentors. -# Params: -# - team_id: The ID of the team whose size is being checked. -# Returns: -# - The number of non-mentor members in the team. -def self.size(team_id) - count = 0 - team_participants = TeamsUser.where(team_id: team_id) - - # Exclude mentors from the count. - team_participants.each do |team_participants| - team_participants_name = team_participants.name - count += 1 unless team_participants_name.include?(' (Mentor)') - end - count -end - -# Checks if a team with the given name already exists for a parent entity (assignment or course). -# Raises an error if a team with the same name already exists. -# Params: -# - parent: The parent entity (assignment or course) associated with the team. -# - name: The name of the team to check for uniqueness. -# - team_type: The type of team (e.g., 'Assignment', 'Course'). -def self.check_for_existing_team(parent, name, team_type) - existing_teams = Object.const_get("#{team_type}Team").where(parent_id: parent.id, name: name) - - # Raise an error if any team with the same name is found. - unless existing_teams.empty? - raise TeamExistsError, "The team name #{name} is already in use." - end -end - -# Retrieves all teams that a specific user belongs to for a given assignment. -# Params: -# - assignment_id: The ID of the assignment. -# - user_id: The ID of the user. -# Returns: -# - A list of team IDs for the user within the specified assignment. -def self.find_team_participants(assignment_id, user_id) - TeamsUser.joins('INNER JOIN teams ON teams_users.team_id = teams.id') - .select('teams.id as team_id') - .where('teams.parent_id = ? AND teams_users.user_id = ?', assignment_id, user_id) -end - end \ No newline at end of file diff --git a/spec/controllers/teams_participants_controller_spec.rb b/spec/controllers/teams_participants_controller_spec.rb index 96530960f..c0b3f8257 100644 --- a/spec/controllers/teams_participants_controller_spec.rb +++ b/spec/controllers/teams_participants_controller_spec.rb @@ -1,89 +1,129 @@ -describe 'GET #auto_complete_for_participant_name' do - let(:team) { create(:team) } - let(:participant) { create(:user, name: 'John Doe') } +require 'rails_helper' - before do - allow(Team).to receive(:find).and_return(team) - allow(team).to receive(:get_possible_team_members).and_return([participant]) - end +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) } - it 'fetches potential team members based on input' do - session[:team_id] = team.id - get :auto_complete_for_participant_name, params: { user: { name: 'John' } } - expect(response).to be_successful - expect(assigns(:potential_team_members)).to eq([participant]) - end + 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 'renders the auto-complete suggestions' do - session[:team_id] = team.id - get :auto_complete_for_participant_name, params: { user: { name: 'John' } } - expect(response.body).to include(participant.name) + 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 -end -describe 'POST #update_duties' do - let(:team_participant) { create(:teams_participant) } - - it 'updates the duty of a team member' do - post :update_duties, params: { - teams_user_id: team_participant.id, - teams_user: { duty_id: 2 }, - participant_id: team_participant.user_id + + 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 } - expect(team_participant.reload.duty_id).to eq(2) - expect(response).to redirect_to(controller: 'student_teams', action: 'view', student_id: team_participant.user_id) - end -end -describe 'GET #list_participants' do - let(:team) { create(:team) } - let(:assignment) { create(:assignment, id: team.parent_id) } - let(:participant) { create(:teams_participant, team: team) } - - it 'lists participants of a team' do - get :list_participants, params: { id: team.id } - expect(assigns(:team)).to eq(team) - expect(assigns(:assignment)).to eq(assignment) - expect(assigns(:team_participants)).to include(participant) - end -end -describe 'GET #add_new_participant' do - let(:team) { create(:team) } + user_session = { user: stub_current_user(student, student.role.name, student.role) } - 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) - expect(assigns(:team)).to eq(team) + # 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 'POST #create_participant' do - let(:team) { create(:team) } - let(:participant) { create(:user) } - - before do - allow(Team).to receive(:find).and_return(team) - allow(User).to receive(:find_by).and_return(participant) - allow(team).to receive(:add_member_with_handling).and_return(true) + + + 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 - it 'adds a participant to a team successfully' do - post :create_participant, params: { user: { name: participant.name }, id: team.id } - expect(flash[:notice]).to include(participant.name) - expect(response).to redirect_to(controller: 'teams', action: 'list', id: team.parent_id) + 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 - it 'shows an error when the participant is invalid' do - allow(team).to receive(:add_member_with_handling).and_return(false) - post :create_participant, params: { user: { name: participant.name }, id: team.id } - expect(flash[:error]).to include('maximum number of members') + 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 -end -describe 'DELETE #delete_participant' do - let(:team) { create(:team) } - let(:participant) { create(:user) } - let(:team_participant) { create(:teams_participant, team: team, user: participant) } - it 'deletes the participant from the team' do + 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 } - expect(flash[:notice]).to include(participant.name) - expect(response).to redirect_to(controller: 'teams', action: 'list', id: team.parent_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 a2820c536..eb7066da9 100644 --- a/spec/factories/factories.rb +++ b/spec/factories/factories.rb @@ -1,22 +1,5 @@ FactoryBot.define do - factory :user do - name { "John Doe" } - end - - factory :team do - name { "Team A" } - parent_id { 1 } - end - - factory :teams_participant do - association :user - association :team - duty_id { 1 } - end - - factory :assignment do - name { "Assignment 1" } - end + end From e6b39e79b653f8ecd7b7d56e90a813641a9e4249 Mon Sep 17 00:00:00 2001 From: manideepika21 Date: Wed, 4 Dec 2024 01:56:16 -0500 Subject: [PATCH 16/18] Changed the function name in assignment model --- app/models/assignment.rb | 2 +- app/models/{teams_participants.rb => teams_users.rb} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/models/{teams_participants.rb => teams_users.rb} (100%) diff --git a/app/models/assignment.rb b/app/models/assignment.rb index c1d114f7b..dc6888ca1 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -203,7 +203,7 @@ def varying_rubrics_by_round? # - 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_for_assignment?(user) +def valid_team_participant?(user) # Check if the user is already part of a team for this assignment. if is_user_on_team?(user) { success: false, error: "This user is already assigned to a team for this assignment" } diff --git a/app/models/teams_participants.rb b/app/models/teams_users.rb similarity index 100% rename from app/models/teams_participants.rb rename to app/models/teams_users.rb From 98eeee32022778ab58e1e254a4ef568c8af17fbd Mon Sep 17 00:00:00 2001 From: manideepika21 Date: Wed, 4 Dec 2024 01:59:10 -0500 Subject: [PATCH 17/18] Added logic to check if the user is already in the team --- app/models/assignment.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/models/assignment.rb b/app/models/assignment.rb index dc6888ca1..4a4401e88 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -192,7 +192,15 @@ def varying_rubrics_by_round? 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. @@ -205,7 +213,7 @@ def varying_rubrics_by_round? # - { 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 is_user_on_team?(user) + 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. From 51af1ccbd701222ed2c70577dfca37fc0a55622e Mon Sep 17 00:00:00 2001 From: manideepika21 Date: Wed, 4 Dec 2024 02:37:46 -0500 Subject: [PATCH 18/18] Modified variable names in teams_participants_controller --- .../api/v1/teams_participants_controller.rb | 30 ++++++++----------- app/models/course.rb | 11 ++++++- app/models/teams_users.rb | 2 +- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/app/controllers/api/v1/teams_participants_controller.rb b/app/controllers/api/v1/teams_participants_controller.rb index c500ee01a..5bf4439d6 100644 --- a/app/controllers/api/v1/teams_participants_controller.rb +++ b/app/controllers/api/v1/teams_participants_controller.rb @@ -54,37 +54,33 @@ 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_handling(participant, team.parent_id) + 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 - - #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 - + private # Helper method to find a user by their name. @@ -128,11 +124,11 @@ def validate_participant_and_team(participant, team) 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) - handle_addition_result(find_participant, team, addition_result) + process_participant_addition_result(find_participant, team, addition_result) end # Handles the result of adding a participant to the team. - def handle_addition_result(find_participant, team, addition_result) + 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 diff --git a/app/models/course.rb b/app/models/course.rb index f2874d43d..04992ddbf 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -50,6 +50,15 @@ def copy_course 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. @@ -62,7 +71,7 @@ def copy_course # - { 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 already_on_team?(user) + 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. diff --git a/app/models/teams_users.rb b/app/models/teams_users.rb index ba9197f02..0232dd8f2 100644 --- a/app/models/teams_users.rb +++ b/app/models/teams_users.rb @@ -18,7 +18,7 @@ def self.remove_team(user_id, team_id) #E2479 # Deletes multiple team members in bulk. - def self.bulk_delete_participants(team_user_ids) + def self.delete_multiple_participants(team_user_ids) where(id: team_user_ids).destroy_all end