diff --git a/Dockerfile b/Dockerfile index ff57d5dd5..687c70771 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,4 +21,4 @@ RUN bundle install EXPOSE 3002 # Set the entry point -ENTRYPOINT ["/app/setup.sh"] +ENTRYPOINT ["/app/setup.sh"] \ No newline at end of file diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb index 0d769f2d7..59fa3efda 100644 --- a/app/controllers/bookmarks_controller.rb +++ b/app/controllers/bookmarks_controller.rb @@ -2,7 +2,7 @@ class BookmarksController < ApplicationController rescue_from ActiveRecord::RecordNotFound, with: :not_found def action_allowed? - has_privileges_of?('Student') + current_user_has_student_privileges? end # Index method returns the list of JSON objects of the bookmark # GET on /bookmarks diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb index e4b7c333a..626d1d3a4 100644 --- a/app/controllers/concerns/authorization.rb +++ b/app/controllers/concerns/authorization.rb @@ -17,7 +17,7 @@ def authorize # Check if all actions are allowed def all_actions_allowed? - return true if has_privileges_of?('Super Administrator') + return true if current_user_has_super_admin_privileges? action_allowed? end @@ -27,25 +27,13 @@ def action_allowed? true end - # Checks if current user has the required role or higher privileges - # @param required_role [Role, String] The minimum role required (can be Role object or role name) - # @return [Boolean] true if user has required role or higher privileges - # @example - # has_privileges_of?('Administrator') # checks if user is an admin or higher - # has_privileges_of?(Role::INSTRUCTOR) # checks if user is an instructor or higher - def has_privileges_of?(required_role) - required_role = Role.find_by_name(required_role) if required_role.is_a?(String) - current_user&.role&.all_privileges_of?(required_role) || false - end - - # Unlike has_privileges_of? which checks for role hierarchy and privilege levels, # this method checks if the user has exactly the specified role # @param role_name [String, Role] The exact role to check for # @return [Boolean] true if user has exactly this role, false otherwise # @example - # has_role?('Student') # true only if user is exactly a student - # has_role?(Role::INSTRUCTOR) # true only if user is exactly an instructor - def has_role?(required_role) + # current_user_has_role?('Student') # true only if user is exactly a student + # current_user_has_role?(Role::INSTRUCTOR) # true only if user is exactly an instructor + def current_user_has_role?(required_role) required_role = required_role.name if required_role.is_a?(Role) current_user&.role&.name == required_role end @@ -240,9 +228,6 @@ def current_user_has_all_heatgrid_data_privileges?(assignment) # Let the Role model define this logic for the sake of DRY # If there is no currently logged-in user simply return false def current_user_has_privileges_of?(role_name) - # puts current_user_and_role_exist? - # puts current_user - # puts current_user.role.all_privileges_of?(Role.find_by(name: role_name)) current_user_and_role_exist? && current_user.role.all_privileges_of?(Role.find_by(name: role_name)) end diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index f8d4ad65e..b8da8f874 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -4,7 +4,7 @@ class CoursesController < ApplicationController rescue_from ActionController::ParameterMissing, with: :parameter_missing def action_allowed? - has_privileges_of?('Instructor') + current_user_has_instructor_privileges? end # GET /courses diff --git a/app/controllers/institutions_controller.rb b/app/controllers/institutions_controller.rb index 1eaa1f431..57a90d973 100644 --- a/app/controllers/institutions_controller.rb +++ b/app/controllers/institutions_controller.rb @@ -1,7 +1,7 @@ class InstitutionsController < ApplicationController rescue_from ActiveRecord::RecordNotFound, with: :institution_not_found def action_allowed? - has_role?('Instructor') + current_user_has_role?('Instructor') end # GET /institutions def index diff --git a/app/controllers/invitations_controller.rb b/app/controllers/invitations_controller.rb index 6d3cb3738..dba7316e9 100644 --- a/app/controllers/invitations_controller.rb +++ b/app/controllers/invitations_controller.rb @@ -1,87 +1,157 @@ -class InvitationsController < ApplicationController - rescue_from ActiveRecord::RecordNotFound, with: :invite_not_found - - # GET /invitations - def index - @invitations = Invitation.all - render json: @invitations, status: :ok - end - - # POST /invitations/ - def create - params[:invitation][:reply_status] ||= InvitationValidator::WAITING_STATUS - @invitation = Invitation.invitation_factory(invite_params) - if @invitation.save - @invitation.send_invite_email - render json: @invitation, status: :created - else - render json: { error: @invitation.errors }, status: :unprocessable_entity - end - end - - # GET /invitations/:id - def show - @invitation = Invitation.find(params[:id]) - render json: @invitation, status: :ok - end - - # PATCH /invitations/:id - def update - @invitation = Invitation.find(params[:id]) - case params[:reply_status] - when InvitationValidator::ACCEPT_STATUS - @invitation.accept_invitation(nil) - render json: @invitation, status: :ok - when InvitationValidator::REJECT_STATUS - @invitation.decline_invitation(nil) - render json: @invitation, status: :ok - else - render json: @invitation.errors, status: :unprocessable_entity - end - - end - - # DELETE /invitations/:id - def destroy - @invitation = Invitation.find(params[:id]) - @invitation.retract_invitation(nil) - render nothing: true, status: :no_content - end - - # GET /invitations/:user_id/:assignment_id - def invitations_for_user_assignment - begin - @user = User.find(params[:user_id]) - rescue ActiveRecord::RecordNotFound => e - render json: { error: e.message }, status: :not_found - return - end - - begin - @assignment = Assignment.find(params[:assignment_id]) - rescue ActiveRecord::RecordNotFound => e - render json: { error: e.message }, status: :not_found - return - end - - @invitations = Invitation.where(to_id: @user.id).where(assignment_id: @assignment.id) - render json: @invitations, status: :ok - end - - private - - # This method will check if the invited user is a participant in the assignment. - # Currently there is no association between assignment and users therefore this method is not implemented yet. - def check_participant_before_invitation; end - - # only allow a list of valid invite params - def invite_params - params.require(:invitation).permit(:id, :assignment_id, :from_id, :to_id, :reply_status) - end - - # helper method used when invite is not found - def invite_not_found - render json: { error: "Invitation with id #{params[:id]} not found" }, status: :not_found - end - -end +class InvitationsController < ApplicationController + rescue_from ActiveRecord::RecordNotFound, with: :invite_not_found + before_action :set_invitation, only: %i[show update destroy] + before_action :invitee_participant, only: %i[create] + + def action_allowed? + case params[:action] + when 'invitations_sent_to_participant' + @participant = AssignmentParticipant.find(params[:participant_id]) + unless current_user_has_id?(@participant.user_id) + render json: { error: "You do not have permission to perform this action." }, status: :forbidden + end + return true + else + return true + end + end + + # GET /invitations + def index + @invitations = Invitation.all + render json: @invitations, status: :ok + end + + # POST /invitations/ + def create + params[:invitation][:reply_status] ||= InvitationValidator::WAITING_STATUS + @invitation = Invitation.invitation_factory(invite_params) + if @invitation.save + @invitation.send_invite_email + render json: { success: true, message: "Invitation successfully sent to #{params[:username]}", invitation: @invitation}, status: :created + else + render json: { error: @invitation.errors[:base].first}, status: :unprocessable_entity + end + end + + # GET /invitations/:id + def show + render json: @invitation, status: :ok + end + + # PATCH /invitations/:id + def update + case params[:reply_status] + when InvitationValidator::ACCEPT_STATUS + # if the current invitation status is either accepted/rejected/retracted then the invitation is no longer valid. + unless @invitation.reply_status.eql?(InvitationValidator::WAITING_STATUS) + render json: { error: "Sorry, the invitation is no longer valid" }, status: :unprocessable_entity + return + end + result = @invitation.accept_invitation + if result[:success] + render json: { success: true, message: result[:message], invitation: @invitation}, status: :ok + else + render json: { error: result[:error] }, status: :unprocessable_entity + end + when InvitationValidator::DECLINED_STATUS + @invitation.decline_invitation + render json: { success: true, message: "Invitation rejected successfully", invitation: @invitation}, status: :ok + when InvitationValidator::RETRACT_STATUS + @invitation.retract_invitation + render json: { success: true, message: "Invitation retracted successfully", invitation: @invitation}, status: :ok + else + render json: @invitation.errors, status: :unprocessable_entity + end + end + + # DELETE /invitations/:id + def destroy + @invitation.destroy! + render json: { success:true, message: "Invitation deleted successfully." }, status: :ok + + rescue ActiveRecord::RecordNotFound + render json: { error: "Invitation not found." }, status: :not_found + rescue ActiveRecord::RecordNotDestroyed => e + render json: { error: "Failed to retract invitation: #{e.record.errors.full_messages.to_sentence}" }, + status: :unprocessable_entity + rescue => e + render json: { error: "Unexpected error: #{e.message}" }, status: :internal_server_error + end + + def invitations_sent_to_participant + begin + rescue ActiveRecord::RecordNotFound => e + render json: { message: e.message, success:false }, status: :not_found + return + end + + @invitations = Invitation.where(to_id: @participant.id, assignment_id: @participant.parent_id) + render json: @invitations, status: :ok + end + + def invitations_sent_by_team + begin + team = AssignmentTeam.find(params[:team_id]) + rescue ActiveRecord::RecordNotFound => e + render json: { message: e.message, success: false }, status: :not_found + return + end + + @invitations = Invitation.where(from_id: team.id, assignment_id: team.parent_id) + render json: @invitations, status: :ok + end + + def invitations_sent_by_participant + begin + participant = AssignmentParticipant.find(params[:participant_id]) + rescue ActiveRecord::RecordNotFound => e + render json: { message: e.message, success: false }, status: :not_found + return + end + + @invitations = Invitation.where(participant_id: participant.id, assignment_id: participant.parent_id) + render json: @invitations, status: :ok + end + + + private + + # only allow a list of valid invite params + def invite_params + params.require(:invitation).permit(:id, :assignment_id, :reply_status).merge(from_team: inviter_team, to_participant: invitee_participant, from_participant: inviter_participant) + end + + # helper method used when invite is not found + def invite_not_found + render json: { error: "Invitation not found" }, status: :not_found + end + + # helper method used to fetch invitation from its id + def set_invitation + @invitation = Invitation.find(params[:id]) + end + + def inviter_participant + AssignmentParticipant.find_by(user: current_user) + end + + # the team of the inviter at the time of sending invitation + def inviter_team + inviter_participant.team + end + + def invitee_participant + invitee_user = User.find_by(name: params[:username]) + unless invitee_user + render json: { error: "Participant with username #{params[:username]} not found" }, status: :not_found + return + end + invitee = AssignmentParticipant.find_by(parent_id: params[:assignment_id], user: invitee_user) + unless invitee + render json: { error: "Participant with username #{params[:username]} not found" }, status: :not_found + return + end + invitee + end +end diff --git a/app/controllers/join_team_requests_controller.rb b/app/controllers/join_team_requests_controller.rb index 2179e6b2c..b4ee50cdc 100644 --- a/app/controllers/join_team_requests_controller.rb +++ b/app/controllers/join_team_requests_controller.rb @@ -8,58 +8,134 @@ class JoinTeamRequestsController < ApplicationController before_action :check_team_status, only: [:create] # This filter runs before the specified actions, finding the join team request - before_action :find_request, only: %i[show update destroy decline] + before_action :find_request, only: %i[show update destroy decline accept] - #checks if the current user is a student - def action_allowed? - @current_user.student? - end + # This filter ensures the request is still pending before processing + before_action :ensure_request_pending, only: %i[accept decline] - # GET api/v1/join_team_requests - # gets a list of all the join team requests - def index - unless @current_user.administrator? - return render json: { errors: 'Unauthorized' }, status: :unauthorized + # Centralized authorization method + def action_allowed? + case params[:action] + when 'create' + # Any student can create a join team request + current_user_has_student_privileges? + + when 'show' + # The participant who made the request OR any team member can view it + return false unless current_user_has_student_privileges? + load_request_for_authorization + return false unless @join_team_request + current_user_is_request_creator? || current_user_is_team_member? + + when 'update', 'destroy' + # Only the participant who created the request can update or delete it + return false unless current_user_has_student_privileges? + load_request_for_authorization + return false unless @join_team_request + current_user_is_request_creator? + + when 'decline', 'accept' + # Only team members of the target team can accept/decline a request + return false unless current_user_has_student_privileges? + load_request_for_authorization + return false unless @join_team_request + current_user_is_team_member? + + when 'for_team', 'by_user', 'pending' + # Students can view filtered lists + current_user_has_student_privileges? + + else + # Default: deny access + false end - join_team_requests = JoinTeamRequest.all - render json: join_team_requests, status: :ok end - # GET api/v1join_team_requests/1 + # GET api/v1/join_team_requests/1 # show the join team request that is passed into the route def show - render json: @join_team_request, status: :ok + render json: @join_team_request, serializer: JoinTeamRequestSerializer, status: :ok + end + + # GET api/v1/join_team_requests/for_team/:team_id + # Get all join team requests for a specific team + def for_team + team = Team.find(params[:team_id]) + join_team_requests = team.join_team_requests.includes(:participant, :team) + render json: join_team_requests, each_serializer: JoinTeamRequestSerializer, status: :ok + rescue ActiveRecord::RecordNotFound + render json: { error: 'Team not found' }, status: :not_found + end + + # GET api/v1/join_team_requests/by_user/:user_id + # Get all join team requests created by a specific user + def by_user + participant_ids = Participant.where(user_id: params[:user_id]).pluck(:id) + join_team_requests = JoinTeamRequest.where(participant_id: participant_ids).includes(:participant, :team) + render json: join_team_requests, each_serializer: JoinTeamRequestSerializer, status: :ok + end + + # GET api/v1/join_team_requests/pending + # Get all pending join team requests + def pending + join_team_requests = JoinTeamRequest.where(reply_status: PENDING).includes(:participant, :team) + render json: join_team_requests, each_serializer: JoinTeamRequestSerializer, status: :ok end # POST api/v1/join_team_requests # Creates a new join team request def create - join_team_request = JoinTeamRequest.new - join_team_request.comments = params[:comments] - join_team_request.status = PENDING - join_team_request.team_id = params[:team_id] - participant = Participant.where(user_id: @current_user.id, assignment_id: params[:assignment_id]).first - team = Team.find(params[:team_id]) + # Find participant object for the user who is requesting to join the team + participant = AssignmentParticipant.find_by(user_id: @current_user.id, parent_id: params[:assignment_id]) + + unless participant + return render json: { error: 'You are not a participant in this assignment' }, status: :unprocessable_entity + end + team = Team.find_by(id: params[:team_id]) + unless team + return render json: { error: 'Team not found' }, status: :not_found + end + + # Check if user already belongs to the team if team.participants.include?(participant) - render json: { error: 'You already belong to the team' }, status: :unprocessable_entity - elsif participant - join_team_request.participant_id = participant.id - if join_team_request.save - render json: join_team_request, status: :created - else - render json: { errors: join_team_request.errors.full_messages }, status: :unprocessable_entity - end + return render json: { error: 'You already belong to this team' }, status: :unprocessable_entity + end + + # Check for duplicate pending requests + existing_request = JoinTeamRequest.find_by( + participant_id: participant.id, + team_id: team.id, + reply_status: PENDING + ) + + if existing_request + return render json: { error: 'You already have a pending request to join this team' }, status: :unprocessable_entity + end + + # Create the request + join_team_request = JoinTeamRequest.new( + participant_id: participant.id, + team_id: team.id, + comments: params[:comments], + reply_status: PENDING + ) + + if join_team_request.save + render json: join_team_request, serializer: JoinTeamRequestSerializer, status: :created else - render json: { errors: 'Participant not found' }, status: :unprocessable_entity + render json: { errors: join_team_request.errors.full_messages }, status: :unprocessable_entity end + rescue ActiveRecord::RecordNotFound => e + render json: { error: e.message }, status: :not_found end # PATCH/PUT api/v1/join_team_requests/1 - # Updates a join team request + # Updates a join team request (comments only, not status) def update - if @join_team_request.update(join_team_request_params) - render json: { message: 'JoinTeamRequest was successfully updated' }, status: :ok + # Only allow updating comments + if @join_team_request.update(comments: params[:comments]) + render json: @join_team_request, serializer: JoinTeamRequestSerializer, status: :ok else render json: { errors: @join_team_request.errors.full_messages }, status: :unprocessable_entity end @@ -69,23 +145,70 @@ def update # delete a join team request def destroy if @join_team_request.destroy - render json: { message: 'JoinTeamRequest was successfully deleted' }, status: :ok + render json: { message: 'Join team request was successfully deleted' }, status: :ok else - render json: { errors: 'Failed to delete JoinTeamRequest' }, status: :unprocessable_entity + render json: { error: 'Failed to delete join team request' }, status: :unprocessable_entity end end - # decline a join team request + # PATCH api/v1/join_team_requests/1/accept + # Accept a join team request and add the participant to the team + def accept + team = @join_team_request.team + if team.full? + return render json: { error: 'Team is full' }, status: :unprocessable_entity + end + + participant = @join_team_request.participant + + # Use a transaction to ensure both removal and addition happen atomically + ActiveRecord::Base.transaction do + # Find and remove participant from their old team (if any) + old_team_participant = TeamsParticipant.find_by(participant_id: participant.id) + if old_team_participant + old_team = old_team_participant.team + old_team_participant.destroy! + + # If the old team is now empty, optionally clean up (but keep the team for now) + Rails.logger.info "Removed participant #{participant.id} from old team #{old_team&.id}" + end + + # Add participant to the new team + TeamsParticipant.create!( + participant_id: participant.id, + team_id: team.id, + user_id: participant.user_id + ) + + # Update the request status + @join_team_request.update!(reply_status: ACCEPTED) + + render json: { + message: 'Join team request accepted successfully', + join_team_request: JoinTeamRequestSerializer.new(@join_team_request).as_json + }, status: :ok + end + rescue ActiveRecord::RecordInvalid => e + render json: { error: e.message }, status: :unprocessable_entity + rescue StandardError => e + render json: { error: e.message }, status: :unprocessable_entity + end + + # PATCH api/v1/join_team_requests/1/decline + # Decline a join team request def decline - @join_team_request.status = DECLINED - if @join_team_request.save - render json: { message: 'JoinTeamRequest declined successfully' }, status: :ok + if @join_team_request.update(reply_status: DECLINED) + render json: { + message: 'Join team request declined successfully', + join_team_request: JoinTeamRequestSerializer.new(@join_team_request).as_json + }, status: :ok else render json: { errors: @join_team_request.errors.full_messages }, status: :unprocessable_entity end end private + # checks if the team is full already def check_team_status team = Team.find(params[:team_id]) @@ -94,13 +217,47 @@ def check_team_status end end + # Ensures the request is still pending before processing accept/decline + def ensure_request_pending + unless @join_team_request.reply_status == PENDING + render json: { error: 'This request has already been processed' }, status: :unprocessable_entity + end + end + # Finds the join team request by ID def find_request @join_team_request = JoinTeamRequest.find(params[:id]) end + # Loads the request for authorization check (avoids duplicate queries) + def load_request_for_authorization + @join_team_request ||= JoinTeamRequest.find_by(id: params[:id]) + end + # Permits specified parameters for join team requests def join_team_request_params - params.require(:join_team_request).permit(:comments, :status) + params.require(:join_team_request).permit(:comments, :reply_status) + end + + # Helper method to check if current user is the creator of the request + def current_user_is_request_creator? + return false unless @join_team_request && @current_user + + @join_team_request.participant&.user_id == @current_user.id + end + + # Helper method to check if current user is a member of the target team + def current_user_is_team_member? + return false unless @join_team_request && @current_user + + team = @join_team_request.team + return false unless team.is_a?(AssignmentTeam) + + participant = AssignmentParticipant.find_by( + user_id: @current_user.id, + parent_id: team.parent_id + ) + + participant && team.participants.include?(participant) end end diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 8baec1f7c..f49157861 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -3,7 +3,7 @@ class QuestionsController < ApplicationController # GET /questions def action_allowed? - has_role?('Instructor') + current_user_has_role?('Instructor') end # Index method returns the list of questions JSON object # GET on /questions diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 6e1ab69c3..843fec9c1 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -3,7 +3,7 @@ class RolesController < ApplicationController rescue_from ActionController::ParameterMissing, with: :parameter_missing def action_allowed? - has_privileges_of?('Administrator') + current_user_has_admin_privileges? end # GET /roles diff --git a/app/controllers/signed_up_teams_controller.rb b/app/controllers/signed_up_teams_controller.rb index 75dc05d67..aeb841293 100644 --- a/app/controllers/signed_up_teams_controller.rb +++ b/app/controllers/signed_up_teams_controller.rb @@ -1,11 +1,31 @@ class SignedUpTeamsController < ApplicationController - # Returns signed up topics using sign_up_topic assignment id - # Retrieves sign_up_topic using topic_id as a parameter + # Returns signed up teams + # Can query by topic_id or assignment_id def index - # puts params[:topic_id] - @sign_up_topic = SignUpTopic.find(params[:topic_id]) - @signed_up_team = SignedUpTeam.find_team_participants(@sign_up_topic.assignment_id) + if params[:assignment_id].present? + # Get all signed up teams for an assignment (across all topics) + topic_ids = SignUpTopic.where(assignment_id: params[:assignment_id]).pluck(:id) + @signed_up_teams = SignedUpTeam.where(sign_up_topic_id: topic_ids) + .includes(team: :users, sign_up_topic: :assignment) + render json: @signed_up_teams, include: { team: { methods: [:team_size, :max_size] }, sign_up_topic: {} } + elsif params[:topic_id].present? + # Get signed up teams for a specific topic with their participants + @signed_up_teams = SignedUpTeam.where(sign_up_topic_id: params[:topic_id]) + .includes(team: :users, sign_up_topic: :assignment) + render json: @signed_up_teams, include: { team: { include: :users, methods: [:team_size, :max_size] }, sign_up_topic: {} } + else + render json: { error: 'Either assignment_id or topic_id parameter is required' }, status: :bad_request + end + end + + def show + @signed_up_team = SignedUpTeam.find_by(id:params[:id]) + render json: @signed_up_team + end + + def show + @signed_up_team = SignedUpTeam.find_by(id:params[:id]) render json: @signed_up_team end @@ -64,10 +84,33 @@ def destroy end end + def create_advertisement + params[:signed_up_team] = { advertise_for_partner: true, comments_for_advertisement: params[:comments_for_advertisement] } + update_custom_message("Advertisement created successfully.") + end + + def update_advertisement + params[:signed_up_team] = { comments_for_advertisement: params[:comments_for_advertisement] } + update_custom_message("Advertisement updated successfully.") + end + + def remove_advertisement + params[:signed_up_team] = { advertise_for_partner: false, comments_for_advertisement: nil } + update_custom_message("Advertisement removed successfully.") + end + private - def signed_up_teams_params - params.require(:signed_up_team).permit(:topic_id, :team_id, :is_waitlisted, :preference_priority_number) + def update_custom_message(message) + @signed_up_team = SignedUpTeam.find(params[:id]) + if @signed_up_team.update(signed_up_teams_params) + render json: { success: true, message: message }, status: :ok + else + render json: {message: @signed_up_team.errors.first, success:false}, status: :unprocessable_entity + end end -end + def signed_up_teams_params + params.require(:signed_up_team).permit(:topic_id, :team_id, :is_waitlisted, :preference_priority_number, :comments_for_advertisement, :advertise_for_partner) + end +end \ No newline at end of file diff --git a/app/controllers/student_tasks_controller.rb b/app/controllers/student_tasks_controller.rb index 859dc32bd..ffb6097a5 100644 --- a/app/controllers/student_tasks_controller.rb +++ b/app/controllers/student_tasks_controller.rb @@ -2,7 +2,7 @@ class StudentTasksController < ApplicationController # List retrieves all student tasks associated with the current logged-in user. def action_allowed? - has_privileges_of?('Student') + current_user_has_student_privileges? end def list # Retrieves all tasks that belong to the current user. diff --git a/app/controllers/student_teams_controller.rb b/app/controllers/student_teams_controller.rb new file mode 100644 index 000000000..5aeb31bd9 --- /dev/null +++ b/app/controllers/student_teams_controller.rb @@ -0,0 +1,100 @@ +class StudentTeamsController < ApplicationController + + # team is gaining or losing a member + def team + @team ||= if params[:student_id].present? && student.present? + TeamsParticipant.find_by(participant_id:student.id)&.team + elsif params[:team_id].present? + AssignmentTeam.find_by(id:params[:team_id]) + end + end + + attr_writer :team + + # student is someone who is joining or leaving a team + def student + @student ||= AssignmentParticipant.find_by(id: params[:student_id]) + end + + attr_writer :student + + before_action :team, :student, only: %i[view update leave_team] + + def action_allowed? + # this can be accessed only by the student and so someone with atleast TA priviliges wont be able to access this controller + # also the current logged in user can view only its relevant team and not other student teams. + if current_user_has_ta_privileges? || student.nil? || !current_user_has_id?(student.user_id) + render json: { error: "You do not have permission to perform this action." }, status: :forbidden + end + return true + end + + # GET /student_teams/view?student_id=${studentId}` + # Returns details of the team that the current student belongs to. + def view + if @team.nil? + render json: { assignment: AssignmentSerializer.new(student.assignment), team: nil, message: "You are not part of any team currently."}, status: :ok + else + render json: {assignment: AssignmentSerializer.new(student.assignment), team: TeamSerializer.new(@team)}, status: :ok + end + end + + # POST /student_teams/` + def create + # Checks for duplicate team names within the same assignment (by parent_id). + matching_teams = AssignmentTeam.where(name: params[:team][:name], parent_id: params[:assignment_id]) + + # no team with that name found - goes ahead and creates the team + if matching_teams.empty? + team = AssignmentTeam.new({ name: params[:team][:name], parent_id: params[:assignment_id]}) + if team.save + # adding the student as the participant for the student_team just created + team.add_participant(student) + serialized_team = ActiveModelSerializers::SerializableResource.new(team, serializer: TeamSerializer).as_json + render json: serialized_team.merge({ message: "Team created successfully", success: true }), status: :ok + else + render json: { error: team.errors.full_messages }, status: :unprocessable_entity + end + + else + # Returns an error if another team with the same name already exists. + render json: { error: "#{params[:team][:name]} is already in use." }, status: :unprocessable_entity + end + end + + # Updates the name of the student's team. + def update + # Checks for duplicate team names within the same assignment (by parent_id). + matching_teams = AssignmentTeam.where(name: params[:team][:name], parent_id: team.parent_id) + + # no team with that name found - goes ahead and saves the new name + if matching_teams.empty? + if team.update(name: params[:team][:name]) + serialized_team = ActiveModelSerializers::SerializableResource.new(team, serializer: TeamSerializer).as_json + render json: serialized_team.merge({ message: "Team updated successfully", success: true }), status: :ok + else + render json: { error: team.errors.full_messages }, status: :unprocessable_entity + end + + else + # Returns an error if another team with the same name already exists. + render json: { error: "#{params[:team][:name]} is already in use." }, status: :unprocessable_entity + end + end + + # method to remove the student from the current team. + # PUT /student_teams/leave?student_id=${studentId} + def leave_team + @team.remove_participant(@student) + render json: { message: "Left the team successfully", success: true }, status: :ok + end + + # used to check student team requirements + def student_team_requirements_met? + # checks if the student has a team + return false if @student.team.nil? + # checks that the student's team has a topic + return false if @student.team.topic.nil? + true + end +end \ No newline at end of file diff --git a/app/controllers/teams_participants_controller.rb b/app/controllers/teams_participants_controller.rb index 4479562ab..c34e3f34d 100644 --- a/app/controllers/teams_participants_controller.rb +++ b/app/controllers/teams_participants_controller.rb @@ -3,9 +3,9 @@ class TeamsParticipantsController < ApplicationController def action_allowed? case params[:action] when 'update_duty' - has_privileges_of?('Student') + current_user_has_student_privileges? else - has_privileges_of?('Teaching Assistant') + current_user_has_ta_privileges? end end diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb index 45d2a784f..49fc576c0 100644 --- a/app/helpers/grades_helper.rb +++ b/app/helpers/grades_helper.rb @@ -154,19 +154,19 @@ def find_assignment(assignment_id) # Checks if the student has the necessary permissions and authorizations to proceed. def student_with_permissions? - has_role?('Student') && + current_user_has_role?('Student') && self_review_finished?(current_user.id) && are_needed_authorizations_present?(current_user.id, 'reader', 'reviewer') end # Checks if the user is either a student viewing their own team or has Teaching Assistant privileges. def student_or_ta? - student_viewing_own_team? || has_privileges_of?('Teaching Assistant') + student_viewing_own_team? || current_user_has_ta_privileges? end # This method checks if the current user, who must have the 'Student' role, is viewing their own team. def student_viewing_own_team? - return false unless has_role?('Student') + return false unless current_user_has_role?('Student') participant = AssignmentParticipant.find_by(id: params[:id]) participant && current_user_is_assignment_participant?(participant.assignment.id) diff --git a/app/mailers/invitation_mailer.rb b/app/mailers/invitation_mailer.rb new file mode 100644 index 000000000..eb762878d --- /dev/null +++ b/app/mailers/invitation_mailer.rb @@ -0,0 +1,32 @@ +class InvitationMailer < ApplicationMailer + default from: 'from@example.com' + + def send_invitation_email + @invitation = params[:invitation] + @to_participant = Participant.find(@invitation.to_id) + @from_team = AssignmentTeam.find(@invitation.from_id) + mail(to: @to_participant.user.email, subject: 'You have a new invitation from Expertiza') + end + + # Send acceptance email to the invitee + def send_acceptance_email + @invitation = params[:invitation] + @invitee = Participant.find(@invitation.to_id) + @inviter_team = AssignmentTeam.find(@invitation.from_id) + @assignment = Assignment.find(@invitation.assignment_id) + mail(to: @invitee.user.email, subject: 'Your invitation has been accepted') + end + + # Send acceptance notification to the entire inviting team + def send_team_acceptance_notification + @invitation = params[:invitation] + @invitee = Participant.find(@invitation.to_id) + @inviter_team = AssignmentTeam.find(@invitation.from_id) + @assignment = Assignment.find(@invitation.assignment_id) + + # Get all team members' emails + team_member_emails = @inviter_team.participants.map { |p| p.user.email } + + mail(to: team_member_emails, subject: "#{@invitee.user.full_name} has accepted the team invitation") + end +end diff --git a/app/mailers/invitation_sent_mailer.rb b/app/mailers/invitation_sent_mailer.rb deleted file mode 100644 index 0044c1aad..000000000 --- a/app/mailers/invitation_sent_mailer.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -class InvitationSentMailer < ApplicationMailer - default from: 'from@example.com' - def send_invitation_email - @invitation = params[:invitation] - @to_user = User.find(@invitation.to_id) - @from_user = User.find(@invitation.from_id) - mail(to: @to_user.email, subject: 'You have a new invitation from Expertiza') - end -end diff --git a/app/mailers/join_team_request_mailer.rb b/app/mailers/join_team_request_mailer.rb new file mode 100644 index 000000000..6ceb29506 --- /dev/null +++ b/app/mailers/join_team_request_mailer.rb @@ -0,0 +1,12 @@ +class JoinTeamRequestMailer < ApplicationMailer + default from: 'from@example.com' + + # Send acceptance email to the person whose join request was accepted + def send_acceptance_email + @join_team_request = params[:join_team_request] + @participant = @join_team_request.participant + @team = @join_team_request.team + @assignment = @team.assignment + mail(to: @participant.user.email, subject: 'Your join team request has been accepted') + end +end diff --git a/app/models/Item.rb b/app/models/Item.rb index 7d8cf2ed5..a7969776f 100644 --- a/app/models/Item.rb +++ b/app/models/Item.rb @@ -15,6 +15,10 @@ def scorable? false end + def scored? + question_type.in?(%w[ScaleItem CriterionItem]) + end + def scored? question_type.in?(%w[ScaleItem CriterionItem]) end @@ -73,4 +77,22 @@ def self.for(record) # Cast the existing record to the desired subclass klass.new(record.attributes) end + + def max_score + weight + end + + def self.for(record) + klass = case record.question_type + when 'Criterion' + Criterion + when 'Scale' + Scale + else + Item + end + + # Cast the existing record to the desired subclass + klass.new(record.attributes) + end end \ No newline at end of file diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 45ac849fb..fc3ade2e0 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -12,7 +12,6 @@ class Assignment < ApplicationRecord has_many :review_mappings, class_name: 'ReviewResponseMap', foreign_key: 'reviewed_object_id', dependent: :destroy, inverse_of: :assignment has_many :sign_up_topics , class_name: 'SignUpTopic', foreign_key: 'assignment_id', dependent: :destroy has_many :due_dates,as: :parent, class_name: 'DueDate', dependent: :destroy - has_many :due_dates,as: :parent, class_name: 'DueDate', dependent: :destroy belongs_to :course, optional: true belongs_to :instructor, class_name: 'User', inverse_of: :assignments @@ -208,4 +207,4 @@ def review_rounds(questionnaireType) review_rounds end -end +end \ No newline at end of file diff --git a/app/models/assignment_participant.rb b/app/models/assignment_participant.rb index d232413b1..412e2bf87 100644 --- a/app/models/assignment_participant.rb +++ b/app/models/assignment_participant.rb @@ -1,22 +1,27 @@ -# frozen_string_literal: true - -class AssignmentParticipant < Participant - include ReviewAggregator - belongs_to :user - validates :handle, presence: true - - def set_handle - self.handle = if user.handle.nil? || (user.handle == '') - user.name - elsif Participant.exists?(assignment_id: assignment.id, handle: user.handle) - user.name - else - user.handle - end - self.save - end - - def aggregate_teammate_review_grade(teammate_review_mappings) - compute_average_review_score(teammate_review_mappings) - end +# frozen_string_literal: true + +class AssignmentParticipant < Participant + include ReviewAggregator + has_many :sent_invitations, class_name: 'Invitation', foreign_key: 'participant_id' + belongs_to :user + validates :handle, presence: true + + def retract_sent_invitations + sent_invitations.each(&:retract_invitation) + end + + def set_handle + self.handle = if user.handle.nil? || (user.handle == '') + user.name + elsif Participant.exists?(assignment_id: assignment.id, handle: user.handle) + user.name + else + user.handle + end + self.save + end + + def aggregate_teammate_review_grade(teammate_review_mappings) + compute_average_review_score(teammate_review_mappings) + end end \ No newline at end of file diff --git a/app/models/assignment_team.rb b/app/models/assignment_team.rb index 95ad1fe14..4787e294d 100644 --- a/app/models/assignment_team.rb +++ b/app/models/assignment_team.rb @@ -25,6 +25,38 @@ def copy_to_course_team(course) end end course_team # Returns the newly created course team object + end + # Adds a participant to this team. + # - Update the participant's team_id (so their direct reference is consistent) + # - Ensure there is a TeamsParticipant join record connecting the participant and this team + def add_participant(participant) + # need to have a check if the team is full then it can not add participant to the team + raise TeamFullError, "Team is full." if full? + + # Update the participant's team_id column - will remove the team reference inside participants table later. keeping it for now + # participant.update!(team_id: id) + + # Create or reuse the join record to maintain the association + TeamsParticipant.find_or_create_by!(participant_id: participant.id, team_id: id, user_id: participant.user_id) + end + + # Removes a participant from this team. + # - Delete the TeamsParticipant join record + # - if the participant sent any invitations while being on the team, they all need to be retracted + # - If the team has no remaining members, destroy the team itself + def remove_participant(participant) + # retract all the invitations the participant sent (if any) while being on the this team + participant.retract_sent_invitations + + # Remove the join record if it exists + tp = TeamsParticipant.find_by(team_id: id, participant_id: participant.id) + tp&.destroy + + # Update the participant's team_id column - will remove the team reference inside participants table later. keeping it for now + # participant.update!(team_id: nil) + + # If no participants remain after removal, delete the team + destroy if participants.empty? end # Get the review response map @@ -49,6 +81,92 @@ def has_submissions? submitted_files.any? || submitted_hyperlinks.present? end + # Computes the average review grade for an assignment team. + # This method aggregates scores from all ReviewResponseMaps (i.e., all reviewers of the team). + def aggregate_review_grade + compute_average_review_score(review_mappings) + end + + # Adds a participant to this team. + # - Update the participant's team_id (so their direct reference is consistent) + # - Ensure there is a TeamsParticipant join record connecting the participant and this team + def add_participant(participant) + # need to have a check if the team is full then it can not add participant to the team + raise TeamFullError, "Team is full." if full? + + # Update the participant's team_id column - will remove the team reference inside participants table later. keeping it for now + # participant.update!(team_id: id) + + # Create or reuse the join record to maintain the association + TeamsParticipant.find_or_create_by!(participant_id: participant.id, team_id: id, user_id: participant.user_id) + end + + # Removes a participant from this team. + # - Delete the TeamsParticipant join record + # - if the participant sent any invitations while being on the team, they all need to be retracted + # - If the team has no remaining members, destroy the team itself + def remove_participant(participant) + # retract all the invitations the participant sent (if any) while being on the this team + participant.retract_sent_invitations + + # Remove the join record if it exists + tp = TeamsParticipant.find_by(team_id: id, participant_id: participant.id) + tp&.destroy + + # Update the participant's team_id column - will remove the team reference inside participants table later. keeping it for now + # participant.update!(team_id: nil) + + # If no participants remain after removal, delete the team + destroy if participants.empty? + end + + # Get the review response map + def review_map_type + 'ReviewResponseMap' + end + + # Adds a participant to this team. + # - Update the participant's team_id (so their direct reference is consistent) + # - Ensure there is a TeamsParticipant join record connecting the participant and this team + def add_participant(participant) + # need to have a check if the team is full then it can not add participant to the team + raise TeamFullError, "Team is full." if full? + + # Update the participant's team_id column - will remove the team reference inside participants table later. keeping it for now + # participant.update!(team_id: id) + + # Create or reuse the join record to maintain the association + TeamsParticipant.find_or_create_by!(participant_id: participant.id, team_id: id, user_id: participant.user_id) + end + + # Removes a participant from this team. + # - Delete the TeamsParticipant join record + # - if the participant sent any invitations while being on the team, they all need to be retracted + # - If the team has no remaining members, destroy the team itself + def remove_participant(participant) + # retract all the invitations the participant sent (if any) while being on the this team + participant.retract_sent_invitations + + # Remove the join record if it exists + tp = TeamsParticipant.find_by(team_id: id, participant_id: participant.id) + tp&.destroy + + # Update the participant's team_id column - will remove the team reference inside participants table later. keeping it for now + # participant.update!(team_id: nil) + + # Use current object (AssignmentTeam) as reviewee and create the ReviewResponseMap record + def assign_reviewer(reviewer) + assignment = Assignment.find(parent_id) + raise 'The assignment cannot be found.' if assignment.nil? + + ReviewResponseMap.create(reviewee_id: id, reviewer_id: reviewer.get_reviewer.id, reviewed_object_id: assignment.id, team_reviewing_enabled: assignment.team_reviewing_enabled) + end + + # Whether the team has submitted work or not + def has_submissions? + submitted_files.any? || submitted_hyperlinks.present? + end + # Computes the average review grade for an assignment team. # This method aggregates scores from all ReviewResponseMaps (i.e., all reviewers of the team). def aggregate_review_grade @@ -57,7 +175,7 @@ def aggregate_review_grade protected - # Validates if a user is eligible to join the team + # Validates if a user is eligible to join the team # - Checks whether the user is a participant of the associated assignment def validate_membership(user) # Ensure user is enrolled in the assignment by checking AssignmentParticipant @@ -71,6 +189,7 @@ def validate_assignment_team_type unless self.kind_of?(AssignmentTeam) errors.add(:type, 'must be an AssignmentTeam or its subclass') end - end + end +end -end \ No newline at end of file +class TeamFullError < StandardError; end diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 8c86d9cfd..71f2a2692 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -1,76 +1,111 @@ -# frozen_string_literal: true - -class Invitation < ApplicationRecord - after_initialize :set_defaults - - belongs_to :to_user, class_name: 'User', foreign_key: 'to_id', inverse_of: false - belongs_to :from_user, class_name: 'User', foreign_key: 'from_id', inverse_of: false - belongs_to :assignment, class_name: 'Assignment', foreign_key: 'assignment_id' - - validates_with InvitationValidator - - # Return a new invitation - # params = :assignment_id, :to_id, :from_id, :reply_status - def self.invitation_factory(params) - Invitation.new(params) - end - - # check if the user is invited - def self.invited?(from_id, to_id, assignment_id) - conditions = { - to_id:, - from_id:, - assignment_id:, - reply_status: InvitationValidator::WAITING_STATUS - } - @invitations_exist = Invitation.where(conditions).exists? - end - - # send invite email - def send_invite_email - InvitationSentMailer.with(invitation: self) - .send_invitation_email - .deliver_later - end - - # After a users accepts an invite, the teams_users table needs to be updated. - # NOTE: Depends on TeamUser model, which is not implemented yet. - def update_users_topic_after_invite_accept(_inviter_user_id, _invited_user_id, _assignment_id); end - - # This method handles all that needs to be done upon a user accepting an invitation. - # Expected functionality: First the users previous team is deleted if they were the only member of that - # team and topics that the old team signed up for will be deleted. - # Then invites the user that accepted the invite sent will be removed. - # Lastly the users team entry will be added to the TeamsUser table and their assigned topic is updated. - # NOTE: For now this method simply updates the invitation's reply_status. - def accept_invitation(_logged_in_user) - update(reply_status: InvitationValidator::ACCEPT_STATUS) - end - - # This method handles all that needs to be done upon an user declining an invitation. - def decline_invitation(_logged_in_user) - update(reply_status: InvitationValidator::REJECT_STATUS) - end - - # This method handles all that need to be done upon an invitation retraction. - def retract_invitation(_logged_in_user) - destroy - end - - # This will override the default as_json method in the ApplicationRecord class and specify - def as_json(options = {}) - super(options.merge({ - only: %i[id reply_status created_at updated_at], - include: { - assignment: { only: %i[id name] }, - from_user: { only: %i[id name fullname email] }, - to_user: { only: %i[id name fullname email] } - } - })).tap do |hash| - end - end - - def set_defaults - self.reply_status ||= InvitationValidator::WAITING_STATUS - end -end +# frozen_string_literal: true + +class Invitation < ApplicationRecord + after_initialize :set_defaults + + belongs_to :to_participant, class_name: 'Participant', foreign_key: 'to_id', inverse_of: false + belongs_to :from_participant, class_name: 'Participant', foreign_key: 'from_id', inverse_of: false + belongs_to :assignment, class_name: 'Assignment', foreign_key: 'assignment_id' + belongs_to :from_participant, class_name: 'AssignmentParticipant', foreign_key: 'participant_id' + + validates_with InvitationValidator + + # Return a new invitation + # params = :assignment_id, :to_id, :from_id, :reply_status + def self.invitation_factory(params) + Invitation.new(params) + end + + # check if the participant is invited + def self.invited?(from_id, to_id, assignment_id) + conditions = { + to_id:, + from_id:, + assignment_id:, + reply_status: InvitationValidator::WAITING_STATUS + } + @invitations_exist = Invitation.where(conditions).exists? + end + + # send invite email + def send_invite_email + InvitationMailer.with(invitation: self) + .send_invitation_email + .deliver_later + end + + # This method handles all that needs to be done upon a user accepting an invitation. + def accept_invitation + inviter_team = from_team + invitee_team = to_participant.team + + # Wrap in transaction to prevent partial updates and concurrency + ActiveRecord::Base.transaction do + # 1. Add the invitee to the inviter's team + inviter_team.add_participant(to_participant) + + # if participant is member of an existing team then only step 2 and 3 makes sense. otherwise just need to add the participant to the inviter team + if invitee_team.present? + # 2. Update the participant’s and team's assigned topic + inviter_signed_up_team = SignedUpTeam.find_by(team_id: inviter_team.id) + invitee_signed_up_team = SignedUpTeam.find_by(team_id: invitee_team.id) + + SignedUpTeam.update_topic_after_invite_accept(inviter_signed_up_team,invitee_signed_up_team) + + # 3. Remove participant from their old team + invitee_team.remove_participant(to_participant) + end + + # 4. Mark this invitation as accepted + update!(reply_status: InvitationValidator::ACCEPT_STATUS) + + # 5. Send acceptance emails + InvitationMailer.with(invitation: self) + .send_acceptance_email + .deliver_now + + InvitationMailer.with(invitation: self) + .send_team_acceptance_notification + .deliver_now + end + + { success: true, message: "Invitation accepted successfully." } + + rescue TeamFullError => e + { success: false, error: e.message } + rescue => e + { success: false, error: "Unexpected error: #{e.message}" } + end + + + # This method handles all that needs to be done upon an user declining an invitation. + def decline_invitation + update(reply_status: InvitationValidator::DECLINED_STATUS) + end + + # This method handles all that need to be done upon an invitation retraction. + def retract_invitation + update(reply_status: InvitationValidator::RETRACT_STATUS) + end + + # This will override the default as_json method in the ApplicationRecord class and specify + def as_json(options = {}) + super(options.merge({ + only: %i[id reply_status created_at updated_at], + include: { + assignment: { only: %i[id name] }, + from_team: { only: %i[id name] }, + to_participant: { + only: [:id], + include: { + user: { only: %i[id name full_name email] } + }} + } + })).tap do |hash| + end + end + + def set_defaults + self.reply_status ||= InvitationValidator::WAITING_STATUS + end +end \ No newline at end of file diff --git a/app/models/join_team_request.rb b/app/models/join_team_request.rb index b61341538..a8f763f3c 100644 --- a/app/models/join_team_request.rb +++ b/app/models/join_team_request.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class JoinTeamRequest < ApplicationRecord - # TODO Uncomment the following line when Team and Team Controller is thoroughly implemented - # belongs_to :team - has_one :participant, dependent: :nullify + belongs_to :team + belongs_to :participant + ACCEPTED_STATUSES = %w[ACCEPTED DECLINED PENDING] - validates :status, inclusion: { in: ACCEPTED_STATUSES } + validates :reply_status, inclusion: { in: ACCEPTED_STATUSES } end diff --git a/app/models/questionnaire.rb b/app/models/questionnaire.rb index 82c950ca2..709803fb6 100644 --- a/app/models/questionnaire.rb +++ b/app/models/questionnaire.rb @@ -1,85 +1,85 @@ -# frozen_string_literal: true - -class Questionnaire < ApplicationRecord - belongs_to :instructor - has_many :items, class_name: "Item", foreign_key: "questionnaire_id", dependent: :destroy # the collection of questions associated with this Questionnaire - before_destroy :check_for_question_associations - - validate :validate_questionnaire - validates :name, presence: true - validates :max_question_score, :min_question_score, numericality: true - - - # after_initialize :post_initialization - # @print_name = 'Review Rubric' - - # class << self - # attr_reader :print_name - # end - - # def post_initialization - # self.display_type = 'Review' - # end - - def symbol - 'review'.to_sym - end - - def get_assessments_for(participant) - participant.reviews - end - - # validate the entries for this questionnaire - def validate_questionnaire - errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score < 1 - errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score < 0 - errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score >= max_question_score - results = Questionnaire.where('id <> ? and name = ? and instructor_id = ?', id, name, instructor_id) - errors.add(:name, 'Questionnaire names must be unique.') if results.present? - end - - # clones the contents of a questionnaire, including the questions and associated advice - def self.copy_questionnaire_details(params) - orig_questionnaire = Questionnaire.find(params[:id]) - questions = Item.where(questionnaire_id: params[:id]) - questionnaire = orig_questionnaire.dup - questionnaire.instructor_id = params[:instructor_id] - questionnaire.name = 'Copy of ' + orig_questionnaire.name - questionnaire.created_at = Time.zone.now - questionnaire.save! - questions.each do |question| - new_question = question.dup - new_question.questionnaire_id = questionnaire.id - new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) && new_question.size.nil? - new_question.save! - advices = QuestionAdvice.where(question_id: question.id) - next if advices.empty? - - advices.each do |advice| - new_advice = advice.dup - new_advice.question_id = new_question.id - new_advice.save! - end - end - questionnaire - end - - # Check_for_question_associations checks if questionnaire has associated questions or not - def check_for_question_associations - if questions.any? - raise ActiveRecord::DeleteRestrictionError.new( "Cannot delete record because dependent questions exist") - end - end - - def as_json(options = {}) - super(options.merge({ - only: %i[id name private min_question_score max_question_score created_at updated_at questionnaire_type instructor_id], - include: { - instructor: { only: %i[name email fullname password role] - } - } - })).tap do |hash| - hash['instructor'] ||= { id: nil, name: nil } - end - end -end \ No newline at end of file +# frozen_string_literal: true + +class Questionnaire < ApplicationRecord + belongs_to :instructor + has_many :items, class_name: "Item", foreign_key: "questionnaire_id", dependent: :destroy # the collection of questions associated with this Questionnaire + before_destroy :check_for_question_associations + + validate :validate_questionnaire + validates :name, presence: true + validates :max_question_score, :min_question_score, numericality: true + + + # after_initialize :post_initialization + # @print_name = 'Review Rubric' + + # class << self + # attr_reader :print_name + # end + + # def post_initialization + # self.display_type = 'Review' + # end + + def symbol + 'review'.to_sym + end + + def get_assessments_for(participant) + participant.reviews + end + + # validate the entries for this questionnaire + def validate_questionnaire + errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score < 1 + errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score < 0 + errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score >= max_question_score + results = Questionnaire.where('id <> ? and name = ? and instructor_id = ?', id, name, instructor_id) + errors.add(:name, 'Questionnaire names must be unique.') if results.present? + end + + # clones the contents of a questionnaire, including the questions and associated advice + def self.copy_questionnaire_details(params) + orig_questionnaire = Questionnaire.find(params[:id]) + questions = Item.where(questionnaire_id: params[:id]) + questionnaire = orig_questionnaire.dup + questionnaire.instructor_id = params[:instructor_id] + questionnaire.name = 'Copy of ' + orig_questionnaire.name + questionnaire.created_at = Time.zone.now + questionnaire.save! + questions.each do |question| + new_question = question.dup + new_question.questionnaire_id = questionnaire.id + new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) && new_question.size.nil? + new_question.save! + advices = QuestionAdvice.where(question_id: question.id) + next if advices.empty? + + advices.each do |advice| + new_advice = advice.dup + new_advice.question_id = new_question.id + new_advice.save! + end + end + questionnaire + end + + # Check_for_question_associations checks if questionnaire has associated questions or not + def check_for_question_associations + if questions.any? + raise ActiveRecord::DeleteRestrictionError.new( "Cannot delete record because dependent questions exist") + end + end + + def as_json(options = {}) + super(options.merge({ + only: %i[id name private min_question_score max_question_score created_at updated_at questionnaire_type instructor_id], + include: { + instructor: { only: %i[name email fullname password role] + } + } + })).tap do |hash| + hash['instructor'] ||= { id: nil, name: nil } + end + end +end diff --git a/app/models/response.rb b/app/models/response.rb index 1dd9ba045..f99db770f 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -70,7 +70,6 @@ def maximum_score scores.each do |s| total_weight += s.item.weight unless s.answer.nil? #|| !s.item.is_a(ScoredItem)? end - # puts "total: #{total_weight * questionnaire.max_question_score} " total_weight * questionnaire.max_question_score end end \ No newline at end of file diff --git a/app/models/response_map.rb b/app/models/response_map.rb index 5657741cf..d891da390 100644 --- a/app/models/response_map.rb +++ b/app/models/response_map.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ResponseMap < ApplicationRecord + include ResponseMapSubclassTitles + has_many :responses, foreign_key: 'map_id', dependent: :destroy, inverse_of: false belongs_to :reviewer, class_name: 'Participant', foreign_key: 'reviewer_id', inverse_of: false belongs_to :reviewee, class_name: 'Participant', foreign_key: 'reviewee_id', inverse_of: false @@ -79,4 +81,32 @@ def aggregate_reviewers_score # Return the normalized score (as a float), or 0 if no valid total score total_score > 0 ? (response_score.to_f / total_score) : 0 end -end + + # Computes the average score (as a fraction between 0 and 1) across the latest submitted responses + # from each round for this ReviewResponseMap. + def review_grade + # Return 0 if there are no responses for this map + return 0 if responses.empty? + + # Group all responses by round, then select the latest one per round based on the most recent created one (i.e., most recent revision in that round) + latest_responses_by_round = responses + .group_by(&:round) + .transform_values { |resps| resps.max_by(&:created_at) } + + response_score = 0.0 # Sum of actual scores obtained + total_score = 0.0 # Sum of maximum possible scores + + # Iterate through the latest responses from each round + latest_responses_by_round.each_value do |response| + # Only consider responses that were submitted + next unless response.is_submitted + + # Accumulate the obtained and maximum scores + response_score += response.aggregate_questionnaire_score + total_score += response.maximum_score + end + + # Return the normalized score (as a float), or 0 if no valid total score + total_score > 0 ? (response_score.to_f / total_score) : 0 + end +end \ No newline at end of file diff --git a/app/models/review_response_map.rb b/app/models/review_response_map.rb index c00955bcc..17d2a329c 100644 --- a/app/models/review_response_map.rb +++ b/app/models/review_response_map.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true - class ReviewResponseMap < ResponseMap include ResponseMapSubclassTitles belongs_to :reviewee, class_name: 'Team', foreign_key: 'reviewee_id', inverse_of: false + belongs_to :assignment, class_name: 'Assignment', foreign_key: 'parent_id' + has_many :review_mappings, class_name: 'ReviewResponseMap', foreign_key: 'reviewee_id' + has_many :review_response_maps, foreign_key: 'reviewee_id' + has_many :responses, through: :review_response_maps, foreign_key: 'map_id' # returns the assignment related to the response map def response_assignment @@ -16,4 +19,27 @@ def questionnaire_type def get_title REVIEW_RESPONSE_MAP_TITLE end + + # Get the review response map + def review_map_type + 'ReviewResponseMap' + end + + # Computes the average review grade for an assignment team. + # This method aggregates scores from all ReviewResponseMaps (i.e., all reviewers of the team). + def aggregate_review_grade + obtained_score = 0 + + # Total number of reviewers for this team + total_reviewers = review_mappings.size + + # Loop through each ReviewResponseMap (i.e., each reviewer) + review_mappings.each do |map| + # Add the review grade (normalized score between 0 and 1) to the total + obtained_score += map.review_grade + end + + # Compute the average score across reviewers and convert it to a percentage + ((obtained_score / total_reviewers) * 100).round(2) + end end diff --git a/app/models/score_view.rb b/app/models/score_view.rb index fc3d9344e..7aadb04ad 100644 --- a/app/models/score_view.rb +++ b/app/models/score_view.rb @@ -1,13 +1,12 @@ class ScoreView < ApplicationRecord - # setting this to false so that factories can be created - # to test the grading of weighted quiz questionnaires - def readonly? - false - end - - def self.questionnaire_data(questionnaire_id, response_id) - questionnaire_data = ScoreView.find_by_sql ["SELECT q1_max_question_score ,SUM(question_weight) as sum_of_weights,SUM(question_weight * s_score) as weighted_score FROM score_views WHERE type in('Criterion', 'Scale') AND q1_id = ? AND s_response_id = ? GROUP BY q1_max_question_score", questionnaire_id, response_id] - questionnaire_data[0] - end + # setting this to false so that factories can be created + # to test the grading of weighted quiz questionnaires + def readonly? + false end - \ No newline at end of file + + def self.questionnaire_data(questionnaire_id, response_id) + questionnaire_data = ScoreView.find_by_sql ["SELECT q1_max_question_score ,SUM(question_weight) as sum_of_weights,SUM(question_weight * s_score) as weighted_score FROM score_views WHERE type in('Criterion', 'Scale') AND q1_id = ? AND s_response_id = ? GROUP BY q1_max_question_score", questionnaire_id, response_id] + questionnaire_data[0] + end +end \ No newline at end of file diff --git a/app/models/scored_item.rb b/app/models/scored_item.rb index e398e62d4..4910afe19 100644 --- a/app/models/scored_item.rb +++ b/app/models/scored_item.rb @@ -12,4 +12,4 @@ def self.compute_item_score(response_id) answer = Answer.find_by(item_id: id, response_id: response_id) weight * answer.answer end -end +end \ No newline at end of file diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index 5d1a47f19..c23a41df4 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -1,6 +1,23 @@ -# frozen_string_literal: true - -class SignedUpTeam < ApplicationRecord - belongs_to :sign_up_topic - belongs_to :team -end +class SignedUpTeam < ApplicationRecord + belongs_to :sign_up_topic + belongs_to :team + + # Case 1: If participant joins a team without a topic and participant has a topic, the team gets participant's topic. + # Case 2: If participant joins a team with a topic and participant doesn’t have a topic, participant get the team’s topic. + # Case 3: If participant joins a team with a topic and participant has a topic, participant is warned & participant lose its topic and get the team’s topic. + def self.update_topic_after_invite_accept(inviter_signed_up_team, invitee_signed_up_team) + return unless inviter_signed_up_team && invitee_signed_up_team + + ActiveRecord::Base.transaction do + inviter_topic = inviter_signed_up_team.sign_up_topic + invitee_topic = invitee_signed_up_team.sign_up_topic + + # Case 1: inviter team has no topic, take invitee participant's topic + if inviter_topic.nil? && invitee_topic.present? + inviter_signed_up_team.update!(sign_up_topic_id: invitee_topic.id) + end + # For all cases, the invitee signed up team record need to be removed + invitee_signed_up_team.destroy + end + end +end \ No newline at end of file diff --git a/app/models/ta.rb b/app/models/ta.rb index 9968b000c..3855d9de6 100644 --- a/app/models/ta.rb +++ b/app/models/ta.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Ta < User - # Get all users whose parent is the TA # @return [Array] all users that belongs to courses that is mapped to the TA def managed_users @@ -16,4 +15,9 @@ def courses_assisted_with courses = TaMapping.where(ta_id: id) courses.map { |c| Course.find(c.course_id) } end + + def courses_assisted_with + courses = TaMapping.where(ta_id: id) + courses.map { |c| Course.find(c.course_id) } + end end \ No newline at end of file diff --git a/app/models/team.rb b/app/models/team.rb index 9c8813c08..120a16d2d 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -8,6 +8,7 @@ class Team < ApplicationRecord has_many :teams_participants, dependent: :destroy has_many :users, through: :teams_participants has_many :participants, through: :teams_participants + has_many :join_team_requests, dependent: :destroy # The team is either an AssignmentTeam or a CourseTeam belongs_to :assignment, class_name: 'Assignment', foreign_key: 'parent_id', optional: true @@ -21,12 +22,29 @@ class Team < ApplicationRecord def has_member?(user) participants.exists?(user_id: user.id) end - + + # Returns the current number of team members + def team_size + users.count + end + + # Returns the maximum allowed team size + def max_size + if is_a?(AssignmentTeam) && assignment&.max_team_size + assignment.max_team_size + elsif is_a?(CourseTeam) && course&.max_team_size + course.max_team_size + else + nil + end + end + def full? current_size = participants.count # assignment teams use the column max_team_size if is_a?(AssignmentTeam) && assignment&.max_team_size + print current_size return current_size >= assignment.max_team_size end diff --git a/app/serializers/assignment_serializer.rb b/app/serializers/assignment_serializer.rb new file mode 100644 index 000000000..3705f7c86 --- /dev/null +++ b/app/serializers/assignment_serializer.rb @@ -0,0 +1,3 @@ +class AssignmentSerializer < ActiveModel::Serializer + attributes :id, :name, :max_team_size +end \ No newline at end of file diff --git a/app/serializers/join_team_request_serializer.rb b/app/serializers/join_team_request_serializer.rb new file mode 100644 index 000000000..f2586f55d --- /dev/null +++ b/app/serializers/join_team_request_serializer.rb @@ -0,0 +1,26 @@ +class JoinTeamRequestSerializer < ActiveModel::Serializer + attributes :id, :comments, :reply_status, :created_at, :updated_at + + # Include participant information + attribute :participant do + { + id: object.participant.id, + user_id: object.participant.user_id, + user_name: object.participant.user.name, + user_email: object.participant.user.email, + handle: object.participant.handle + } + end + + # Include team information + attribute :team do + { + id: object.team.id, + name: object.team.name, + type: object.team.type, + team_size: object.team.participants.count, + max_size: object.team.is_a?(AssignmentTeam) ? object.team.assignment&.max_team_size : nil, + is_full: object.team.full? + } + end +end diff --git a/app/serializers/participant_serializer.rb b/app/serializers/participant_serializer.rb new file mode 100644 index 000000000..a768e8b49 --- /dev/null +++ b/app/serializers/participant_serializer.rb @@ -0,0 +1,12 @@ +class ParticipantSerializer < ActiveModel::Serializer + attributes :id, :user + + def user + { + id: object.user.id, + username: object.user.name, + email: object.user.email, + fullName: object.user.full_name + } + end +end \ No newline at end of file diff --git a/app/serializers/team_serializer.rb b/app/serializers/team_serializer.rb index eece9d3bc..66e1c6f08 100644 --- a/app/serializers/team_serializer.rb +++ b/app/serializers/team_serializer.rb @@ -1,20 +1,24 @@ # frozen_string_literal: true class TeamSerializer < ActiveModel::Serializer - attributes :id, :name, :type, :team_size, :assignment_id - has_many :users, serializer: UserSerializer + attributes :id, :name, :type, :team_size, :signed_up_team, :sign_up_topic + has_many :members, serializer: ParticipantSerializer - def users - # Use teams_participants association to get users - object.teams_participants.includes(:user).map(&:user) + def members + # Use teams_participants association to get participants + object.teams_participants.includes(:participant).map(&:participant) end def team_size object.teams_participants.count end - def assignment_id - # Return parent_id for AssignmentTeam, nil for CourseTeam - object.is_a?(AssignmentTeam) ? object.parent_id : nil + def sign_up_topic + signed_up_team&.sign_up_topic end -end + + def signed_up_team + SignedUpTeam.find_by(team_id: object.id) + end + +end \ No newline at end of file diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 287456b04..a1a7e28ee 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -1,5 +1,13 @@ # frozen_string_literal: true class UserSerializer < ActiveModel::Serializer - attributes :id, :name, :email, :full_name + attributes :id, :username, :email, :fullName + + def username + object.name + end + + def fullName + object.full_name + end end diff --git a/app/validators/invitation_validator.rb b/app/validators/invitation_validator.rb index fa5aa6b95..be0fdc24f 100644 --- a/app/validators/invitation_validator.rb +++ b/app/validators/invitation_validator.rb @@ -1,52 +1,64 @@ -# frozen_string_literal: true - -# app/validators/invitation_validator.rb -class InvitationValidator < ActiveModel::Validator - ACCEPT_STATUS = 'A'.freeze - REJECT_STATUS = 'R'.freeze - WAITING_STATUS = 'W'.freeze - - DUPLICATE_INVITATION_ERROR_MSG = 'You cannot have duplicate invitations'.freeze - TO_FROM_SAME_ERROR_MSG = 'to and from users should be different'.freeze - REPLY_STATUS_ERROR_MSG = 'must be present and have a maximum length of 1'.freeze - REPLY_STATUS_INCLUSION_ERROR_MSG = "must be one of #{[ACCEPT_STATUS, REJECT_STATUS, WAITING_STATUS].to_sentence}".freeze - - def validate(record) - validate_reply_status(record) - validate_reply_status_inclusion(record) - validate_duplicate_invitation(record) - validate_to_from_different(record) - end - - private - - def validate_reply_status(record) - unless record.reply_status.present? && record.reply_status.length <= 1 - record.errors.add(:reply_status, REPLY_STATUS_ERROR_MSG) - end - end - - def validate_reply_status_inclusion(record) - unless [ACCEPT_STATUS, REJECT_STATUS, WAITING_STATUS].include?(record.reply_status) - record.errors.add(:reply_status, REPLY_STATUS_INCLUSION_ERROR_MSG) - end - end - - def validate_duplicate_invitation(record) - conditions = { - to_id: record.to_id, - from_id: record.from_id, - assignment_id: record.assignment_id, - reply_status: record.reply_status - } - if Invitation.where(conditions).exists? - record.errors[:base] << DUPLICATE_INVITATION_ERROR_MSG - end - end - - def validate_to_from_different(record) - if record.from_id == record.to_id - record.errors.add(:from_id, TO_FROM_SAME_ERROR_MSG) - end - end +# frozen_string_literal: true + +# app/validators/invitation_validator.rb +class InvitationValidator < ActiveModel::Validator + ACCEPT_STATUS = 'A'.freeze + DECLINED_STATUS = 'D'.freeze + WAITING_STATUS = 'W'.freeze + RETRACT_STATUS = 'R'.freeze + + DUPLICATE_INVITATION_ERROR_MSG = 'You cannot have duplicate invitations'.freeze + TO_FROM_SAME_ERROR_MSG = 'to and from participants should be different'.freeze + REPLY_STATUS_ERROR_MSG = 'must be present and have a maximum length of 1'.freeze + DIFFERENT_ASSIGNMENT_PARTICIPANT_MSG = "the participant is not part of this assignment".freeze + REPLY_STATUS_INCLUSION_ERROR_MSG = "must be one of #{[ACCEPT_STATUS, DECLINED_STATUS, WAITING_STATUS, RETRACT_STATUS].to_sentence}".freeze + + def validate(record) + validate_invitee(record) + validate_reply_status(record) + validate_reply_status_inclusion(record) + validate_duplicate_invitation(record) + validate_to_from_different(record) + end + + private + + # validates if the invitee is participant of the assignment or not + def validate_invitee(record) + participant = AssignmentParticipant.find_by(id: record.to_id, parent_id: record.assignment_id) + unless participant.present? + record.errors.add(:base, DIFFERENT_ASSIGNMENT_PARTICIPANT_MSG) + end + end + + def validate_reply_status(record) + unless record.reply_status.present? && record.reply_status.length <= 1 + record.errors.add(:base, REPLY_STATUS_ERROR_MSG) + end + end + + def validate_reply_status_inclusion(record) + unless [ACCEPT_STATUS, DECLINED_STATUS, WAITING_STATUS, RETRACT_STATUS].include?(record.reply_status) + record.errors.add(:base, REPLY_STATUS_INCLUSION_ERROR_MSG) + end + end + + def validate_duplicate_invitation(record) + conditions = { + id: record&.id, + to_id: record.to_id, + from_id: record.from_id, + assignment_id: record.assignment_id, + reply_status: record.reply_status + } + if Invitation.where(conditions).exists? + record.errors.add(:base, DUPLICATE_INVITATION_ERROR_MSG) + end + end + + def validate_to_from_different(record) + if record.from_participant.id == record.to_id + record.errors.add(:base, TO_FROM_SAME_ERROR_MSG) + end + end end \ No newline at end of file diff --git a/app/views/invitation_mailer/send_acceptance_email.html.erb b/app/views/invitation_mailer/send_acceptance_email.html.erb new file mode 100644 index 000000000..b5948df71 --- /dev/null +++ b/app/views/invitation_mailer/send_acceptance_email.html.erb @@ -0,0 +1,52 @@ + + + Invitation Accepted + + + + +
+
+

Invitation Accepted!

+
+ +
+

Dear <%= @invitee.user.full_name %>,

+ +

Your invitation to join team <%= @inviter_team.name %> for assignment <%= @assignment.name %> has been accepted!

+ +

You have been successfully added to the team. You can now collaborate with your teammates on this assignment.

+ +
+ Team Name: <%= @inviter_team.name %> +
+ +
+ Assignment: <%= @assignment.name %> +
+ +
+ Date Accepted: <%= I18n.l(@invitation.updated_at, format: :long) %> +
+ +

You can now start working with your team members on the assignment. Good luck!

+ +

Best regards,
+ Expertiza Team

+
+ + +
+ + diff --git a/app/views/invitation_mailer/send_invitation_acceptance_email.html.erb b/app/views/invitation_mailer/send_invitation_acceptance_email.html.erb new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/invitation_mailer/send_invitation_acceptance_to_team.html.erb b/app/views/invitation_mailer/send_invitation_acceptance_to_team.html.erb new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/invitation_sent_mailer/send_invitation_email.html.erb b/app/views/invitation_mailer/send_invitation_email.html.erb similarity index 52% rename from app/views/invitation_sent_mailer/send_invitation_email.html.erb rename to app/views/invitation_mailer/send_invitation_email.html.erb index bfe05d5f8..d98bfe7e9 100644 --- a/app/views/invitation_sent_mailer/send_invitation_email.html.erb +++ b/app/views/invitation_mailer/send_invitation_email.html.erb @@ -4,7 +4,7 @@ -

You have been invited to join the team by <%= @to_user.fullname %>

+

You have been invited to join <%= @from_team %>

diff --git a/app/views/invitation_mailer/send_team_acceptance_notification.html.erb b/app/views/invitation_mailer/send_team_acceptance_notification.html.erb new file mode 100644 index 000000000..02680ccaa --- /dev/null +++ b/app/views/invitation_mailer/send_team_acceptance_notification.html.erb @@ -0,0 +1,53 @@ + + + Team Member Joined + + + + +
+
+

Team Member Joined!

+
+ +
+

Hello Team Members,

+ +

<%= @invitee.user.full_name %> has accepted the team invitation and has been successfully added to your team!

+ +
+ New Team Member: <%= @invitee.user.full_name %> (<%= @invitee.user.email %>) +
+ +
+ Team Name: <%= @inviter_team.name %> +
+ +
+ Assignment: <%= @assignment.name %> +
+ +
+ Date Joined: <%= I18n.l(@invitation.updated_at, format: :long) %> +
+ +

You can now start collaborating with your new team member. Looking forward to great work from your team!

+ +

Best regards,
+ Expertiza Team

+
+ + +
+ + diff --git a/app/views/join_team_request_mailer/send_acceptance_email.html.erb b/app/views/join_team_request_mailer/send_acceptance_email.html.erb new file mode 100644 index 000000000..25770188e --- /dev/null +++ b/app/views/join_team_request_mailer/send_acceptance_email.html.erb @@ -0,0 +1,51 @@ + + + Join Request Accepted + + + + +
+
+

Join Request Accepted!

+
+ +
+

Dear <%= @participant.user.full_name %>,

+ +

Good news! Your request to join team <%= @team.name %> for assignment <%= @assignment.name %> has been accepted!

+ +

You have been successfully added to the team. You can now collaborate with your teammates on this assignment.

+ +
+ Team Name: <%= @team.name %> +
+ +
+ Assignment: <%= @assignment.name %> +
+ +
+ Date Accepted: <%= I18n.l(@join_team_request.updated_at, format: :long) %> +
+ +

You can now start working with your team members on the assignment. Good luck!

+ +

Best regards,
+ Expertiza Team

+
+ + +
+ + diff --git a/config/environments/development.rb b/config/environments/development.rb index 996589b2b..9dfc904f4 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -65,4 +65,5 @@ # config.action_cable.disable_request_forgery_protection = true config.hosts << 'localhost' config.hosts << "www.example.com" + config.hosts << "152.7.176.23" end diff --git a/config/routes.rb b/config/routes.rb index 25642363c..62422d353 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,152 +1,180 @@ -# frozen_string_literal: true - -Rails.application.routes.draw do - - mount Rswag::Api::Engine => 'api-docs' - mount Rswag::Ui::Engine => 'api-docs' - # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html - - # Defines the root path route ("/") - # root "articles#index" - post '/login', to: 'authentication#login' - resources :institutions - resources :roles do - collection do - # Get all roles that are subordinate to a role of a logged in user - get 'subordinate_roles', action: :subordinate_roles - end - end - resources :users do - collection do - get 'institution/:id', action: :institution_users - get ':id/managed', action: :managed_users - get 'role/:name', action: :role_users - end - end - resources :assignments do - collection do - post '/:assignment_id/add_participant/:user_id',action: :add_participant - delete '/:assignment_id/remove_participant/:user_id',action: :remove_participant - patch '/:assignment_id/remove_assignment_from_course',action: :remove_assignment_from_course - patch '/:assignment_id/assign_course/:course_id',action: :assign_course - post '/:assignment_id/copy_assignment', action: :copy_assignment - get '/:assignment_id/has_topics',action: :has_topics - get '/:assignment_id/show_assignment_details',action: :show_assignment_details - get '/:assignment_id/team_assignment', action: :team_assignment - get '/:assignment_id/has_teams', action: :has_teams - get '/:assignment_id/valid_num_review/:review_type', action: :valid_num_review - get '/:assignment_id/varying_rubrics_by_round', action: :varying_rubrics_by_round? - post '/:assignment_id/create_node',action: :create_node - end - end - - resources :bookmarks, except: [:new, :edit] do - member do - get 'bookmarkratings', to: 'bookmarks#get_bookmark_rating_score' - post 'bookmarkratings', to: 'bookmarks#save_bookmark_rating_score' - end - end - resources :student_tasks do - collection do - get :list, action: :list - get :view - end - end - - resources :courses do - collection do - get ':id/add_ta/:ta_id', action: :add_ta - get ':id/tas', action: :view_tas - get ':id/remove_ta/:ta_id', action: :remove_ta - get ':id/copy', action: :copy - end - end - - resources :questionnaires do - collection do - post 'copy/:id', to: 'questionnaires#copy', as: 'copy' - get 'toggle_access/:id', to: 'questionnaires#toggle_access', as: 'toggle_access' - end - end - - resources :questions do - collection do - get :types - get 'show_all/questionnaire/:id', to:'questions#show_all#questionnaire', as: 'show_all' - delete 'delete_all/questionnaire/:id', to:'questions#delete_all#questionnaire', as: 'delete_all' - end - end - - resources :signed_up_teams do - collection do - post '/sign_up', to: 'signed_up_teams#sign_up' - post '/sign_up_student', to: 'signed_up_teams#sign_up_student' - end - end - - resources :join_team_requests do - collection do - post 'decline/:id', to:'join_team_requests#decline' - end - end - - resources :sign_up_topics do - collection do - get :filter - delete '/', to: 'sign_up_topics#destroy' - end - end - - resources :invitations do - get 'user/:user_id/assignment/:assignment_id/', on: :collection, action: :invitations_for_user_assignment - end - - resources :account_requests do - collection do - get :pending, action: :pending_requests - get :processed, action: :processed_requests - end - end - - resources :participants do - collection do - get '/user/:user_id', to: 'participants#list_user_participants' - get '/assignment/:assignment_id', to: 'participants#list_assignment_participants' - get '/:id', to: 'participants#show' - post '/:authorization', to: 'participants#add' - patch '/:id/:authorization', to: 'participants#update_authorization' - delete '/:id', to: 'participants#destroy' - end - end - resources :teams do - member do - get 'members' - post 'members', to: 'teams#add_member' - delete 'members/:user_id', to: 'teams#remove_member' - - get 'join_requests' - post 'join_requests', to: 'teams#create_join_request' - put 'join_requests/:join_request_id', to: 'teams#update_join_request' - end - end - resources :teams_participants, only: [] do - collection do - put :update_duty - end - member do - get :list_participants - post :add_participant - delete :delete_participants - end - end - resources :grades do - collection do - get '/:assignment_id/view_all_scores', to: 'grades#view_all_scores' - patch '/:participant_id/assign_grade', to: 'grades#assign_grade' - get '/:participant_id/edit', to: 'grades#edit' - get '/:assignment_id/view_our_scores', to: 'grades#view_our_scores' - get '/:assignment_id/view_my_scores', to: 'grades#view_my_scores' - get '/:participant_id/instructor_review', to: 'grades#instructor_review' - end - end -end \ No newline at end of file +# frozen_string_literal: true + +Rails.application.routes.draw do + + mount Rswag::Api::Engine => 'api-docs' + mount Rswag::Ui::Engine => 'api-docs' + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + + # Defines the root path route ("/") + # root "articles#index" + post '/login', to: 'authentication#login' + resources :institutions + resources :roles do + collection do + # Get all roles that are subordinate to a role of a logged in user + get 'subordinate_roles', action: :subordinate_roles + end + end + resources :users do + collection do + get 'institution/:id', action: :institution_users + get ':id/managed', action: :managed_users + get 'role/:name', action: :role_users + end + end + resources :assignments do + collection do + post '/:assignment_id/add_participant/:user_id',action: :add_participant + delete '/:assignment_id/remove_participant/:user_id',action: :remove_participant + patch '/:assignment_id/remove_assignment_from_course',action: :remove_assignment_from_course + patch '/:assignment_id/assign_course/:course_id',action: :assign_course + post '/:assignment_id/copy_assignment', action: :copy_assignment + get '/:assignment_id/has_topics',action: :has_topics + get '/:assignment_id/show_assignment_details',action: :show_assignment_details + get '/:assignment_id/team_assignment', action: :team_assignment + get '/:assignment_id/has_teams', action: :has_teams + get '/:assignment_id/valid_num_review/:review_type', action: :valid_num_review + get '/:assignment_id/varying_rubrics_by_round', action: :varying_rubrics_by_round? + post '/:assignment_id/create_node',action: :create_node + end + end + + resources :bookmarks, except: [:new, :edit] do + member do + get 'bookmarkratings', to: 'bookmarks#get_bookmark_rating_score' + post 'bookmarkratings', to: 'bookmarks#save_bookmark_rating_score' + end + end + resources :student_tasks do + collection do + get :list, action: :list + get :view + end + end + + resources :courses do + collection do + get ':id/add_ta/:ta_id', action: :add_ta + get ':id/tas', action: :view_tas + get ':id/remove_ta/:ta_id', action: :remove_ta + get ':id/copy', action: :copy + end + end + + resources :questionnaires do + collection do + post 'copy/:id', to: 'questionnaires#copy', as: 'copy' + get 'toggle_access/:id', to: 'questionnaires#toggle_access', as: 'toggle_access' + end + end + + resources :questions do + collection do + get :types + get 'show_all/questionnaire/:id', to:'questions#show_all#questionnaire', as: 'show_all' + delete 'delete_all/questionnaire/:id', to:'questions#delete_all#questionnaire', as: 'delete_all' + end + end + + resources :signed_up_teams do + collection do + post '/sign_up', to: 'signed_up_teams#sign_up' + post '/sign_up_student', to: 'signed_up_teams#sign_up_student' + end + end + + resources :join_team_requests do + member do + patch 'accept', to: 'join_team_requests#accept' + patch 'decline', to: 'join_team_requests#decline' + end + collection do + get 'for_team/:team_id', to: 'join_team_requests#for_team' + get 'by_user/:user_id', to: 'join_team_requests#by_user' + get 'pending', to: 'join_team_requests#pending' + end + end + + resources :sign_up_topics do + collection do + get :filter + delete '/', to: 'sign_up_topics#destroy' + end + end + + resources :invitations do + collection do + get '/sent_by/team/:team_id', to: 'invitations_sent_by_team' + get '/sent_by/participant/:participant_id', to: 'invitations_sent_by_participant' + get '/sent_to/:participant_id', to: 'invitations_sent_to_participant' + end + end + + resources :account_requests do + collection do + get :pending, action: :pending_requests + get :processed, action: :processed_requests + end + end + + resources :participants do + collection do + get '/user/:user_id', to: 'participants#list_user_participants' + get '/assignment/:assignment_id', to: 'participants#list_assignment_participants' + get '/:id', to: 'participants#show' + post '/:authorization', to: 'participants#add' + patch '/:id/:authorization', to: 'participants#update_authorization' + delete '/:id', to: 'participants#destroy' + end + end + + resources :signed_up_teams do + member do + post :create_advertisement + patch :update_advertisement + delete :remove_advertisement + end + end + + resources :student_teams, only: %i[create update] do + collection do + get :view + get :mentor + get :remove_participant + put '/leave', to: 'student_teams#leave_team' + end + end + + resources :teams do + member do + get 'members' + post 'members', to: 'teams#add_member' + delete 'members/:user_id', to: 'teams#remove_member' + + get 'join_requests' + post 'join_requests', to: 'teams#create_join_request' + put 'join_requests/:join_request_id', to: 'teams#update_join_request' + end + end + resources :teams_participants, only: [] do + collection do + put :update_duty + end + member do + get :list_participants + post :add_participant + delete :delete_participants + end + end + resources :grades do + collection do + get '/:assignment_id/view_all_scores', to: 'grades#view_all_scores' + patch '/:participant_id/assign_grade', to: 'grades#assign_grade' + get '/:participant_id/edit', to: 'grades#edit' + get '/:assignment_id/view_our_scores', to: 'grades#view_our_scores' + get '/:assignment_id/view_my_scores', to: 'grades#view_my_scores' + get '/:participant_id/instructor_review', to: 'grades#instructor_review' + end + end +end diff --git a/database_dump.sql b/database_dump.sql new file mode 100644 index 000000000..bf29f1b50 --- /dev/null +++ b/database_dump.sql @@ -0,0 +1,1091 @@ +-- MySQL dump 10.13 Distrib 8.0.44, for Linux (x86_64) +-- +-- Host: localhost Database: reimplementation_development +-- ------------------------------------------------------ +-- Server version 8.0.44 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `account_requests` +-- + +DROP TABLE IF EXISTS `account_requests`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `account_requests` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `username` varchar(255) DEFAULT NULL, + `full_name` varchar(255) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `status` varchar(255) DEFAULT NULL, + `introduction` text, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `role_id` bigint NOT NULL, + `institution_id` bigint NOT NULL, + PRIMARY KEY (`id`), + KEY `index_account_requests_on_institution_id` (`institution_id`), + KEY `index_account_requests_on_role_id` (`role_id`), + CONSTRAINT `fk_rails_39cb3df9b0` FOREIGN KEY (`institution_id`) REFERENCES `institutions` (`id`), + CONSTRAINT `fk_rails_ea08ff5293` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `account_requests` +-- + +LOCK TABLES `account_requests` WRITE; +/*!40000 ALTER TABLE `account_requests` DISABLE KEYS */; +/*!40000 ALTER TABLE `account_requests` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `answers` +-- + +DROP TABLE IF EXISTS `answers`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `answers` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `item_id` int NOT NULL DEFAULT '0', + `response_id` int DEFAULT NULL, + `answer` int DEFAULT NULL, + `comments` text, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_score_items` (`item_id`), + KEY `fk_score_response` (`response_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `answers` +-- + +LOCK TABLES `answers` WRITE; +/*!40000 ALTER TABLE `answers` DISABLE KEYS */; +/*!40000 ALTER TABLE `answers` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ar_internal_metadata` +-- + +DROP TABLE IF EXISTS `ar_internal_metadata`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `ar_internal_metadata` ( + `key` varchar(255) NOT NULL, + `value` varchar(255) DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ar_internal_metadata` +-- + +LOCK TABLES `ar_internal_metadata` WRITE; +/*!40000 ALTER TABLE `ar_internal_metadata` DISABLE KEYS */; +INSERT INTO `ar_internal_metadata` VALUES ('environment','development','2025-11-15 22:48:42.225557','2025-11-15 22:48:42.225561'),('schema_sha1','d6d04ca9ec9015eb33402a2bd419dfa8c6a07af4','2025-11-15 22:48:42.235203','2025-11-15 22:48:42.235205'); +/*!40000 ALTER TABLE `ar_internal_metadata` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `assignment_questionnaires` +-- + +DROP TABLE IF EXISTS `assignment_questionnaires`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `assignment_questionnaires` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `assignment_id` int DEFAULT NULL, + `questionnaire_id` int DEFAULT NULL, + `notification_limit` int NOT NULL DEFAULT '15', + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `used_in_round` int DEFAULT NULL, + `questionnaire_weight` int DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `fk_aq_assignments_id` (`assignment_id`), + KEY `fk_aq_questionnaire_id` (`questionnaire_id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `assignment_questionnaires` +-- + +LOCK TABLES `assignment_questionnaires` WRITE; +/*!40000 ALTER TABLE `assignment_questionnaires` DISABLE KEYS */; +INSERT INTO `assignment_questionnaires` VALUES (1,1,1,15,'2025-11-15 22:49:22.118717','2025-11-15 22:49:22.118717',1,NULL),(2,1,2,15,'2025-11-15 22:49:22.133606','2025-11-15 22:49:22.133606',2,NULL),(3,2,3,15,'2025-11-15 22:49:22.152366','2025-11-15 22:49:22.152366',1,NULL),(4,2,4,15,'2025-11-15 22:49:22.169158','2025-11-15 22:49:22.169158',2,NULL); +/*!40000 ALTER TABLE `assignment_questionnaires` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `assignments` +-- + +DROP TABLE IF EXISTS `assignments`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `assignments` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `directory_path` varchar(255) DEFAULT NULL, + `submitter_count` int DEFAULT NULL, + `private` tinyint(1) DEFAULT NULL, + `num_reviews` int DEFAULT NULL, + `num_review_of_reviews` int DEFAULT NULL, + `num_review_of_reviewers` int DEFAULT NULL, + `reviews_visible_to_all` tinyint(1) DEFAULT NULL, + `num_reviewers` int DEFAULT NULL, + `spec_location` text, + `max_team_size` int DEFAULT NULL, + `staggered_deadline` tinyint(1) DEFAULT NULL, + `allow_suggestions` tinyint(1) DEFAULT NULL, + `days_between_submissions` int DEFAULT NULL, + `review_assignment_strategy` varchar(255) DEFAULT NULL, + `max_reviews_per_submission` int DEFAULT NULL, + `review_topic_threshold` int DEFAULT NULL, + `copy_flag` tinyint(1) DEFAULT NULL, + `rounds_of_reviews` int DEFAULT NULL, + `microtask` tinyint(1) DEFAULT NULL, + `require_quiz` tinyint(1) DEFAULT NULL, + `num_quiz_questions` int DEFAULT NULL, + `is_coding_assignment` tinyint(1) DEFAULT NULL, + `is_intelligent` tinyint(1) DEFAULT NULL, + `calculate_penalty` tinyint(1) DEFAULT NULL, + `late_policy_id` int DEFAULT NULL, + `is_penalty_calculated` tinyint(1) DEFAULT NULL, + `max_bids` int DEFAULT NULL, + `show_teammate_reviews` tinyint(1) DEFAULT NULL, + `availability_flag` tinyint(1) DEFAULT NULL, + `use_bookmark` tinyint(1) DEFAULT NULL, + `can_review_same_topic` tinyint(1) DEFAULT NULL, + `can_choose_topic_to_review` tinyint(1) DEFAULT NULL, + `is_calibrated` tinyint(1) DEFAULT NULL, + `is_selfreview_enabled` tinyint(1) DEFAULT NULL, + `reputation_algorithm` varchar(255) DEFAULT NULL, + `is_anonymous` tinyint(1) DEFAULT NULL, + `num_reviews_required` int DEFAULT NULL, + `num_metareviews_required` int DEFAULT NULL, + `num_metareviews_allowed` int DEFAULT NULL, + `num_reviews_allowed` int DEFAULT NULL, + `simicheck` int DEFAULT NULL, + `simicheck_threshold` int DEFAULT NULL, + `is_answer_tagging_allowed` tinyint(1) DEFAULT NULL, + `has_badge` tinyint(1) DEFAULT NULL, + `allow_selecting_additional_reviews_after_1st_round` tinyint(1) DEFAULT NULL, + `sample_assignment_id` int DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `instructor_id` bigint NOT NULL, + `course_id` bigint DEFAULT NULL, + `enable_pair_programming` tinyint(1) DEFAULT '0', + `has_teams` tinyint(1) DEFAULT '0', + `has_topics` tinyint(1) DEFAULT '0', + PRIMARY KEY (`id`), + KEY `index_assignments_on_course_id` (`course_id`), + KEY `index_assignments_on_instructor_id` (`instructor_id`), + CONSTRAINT `fk_rails_2194c084a6` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`), + CONSTRAINT `fk_rails_e22e619567` FOREIGN KEY (`instructor_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `assignments` +-- + +LOCK TABLES `assignments` WRITE; +/*!40000 ALTER TABLE `assignments` DISABLE KEYS */; +INSERT INTO `assignments` VALUES (1,'audit',NULL,NULL,0,NULL,NULL,NULL,NULL,NULL,NULL,4,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'2025-11-15 22:49:09.751586','2025-11-15 22:49:22.200267',8,2,0,1,1),(2,'encourage',NULL,NULL,0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'2025-11-15 22:49:09.769950','2025-11-15 22:49:09.769950',9,2,0,1,0),(3,'compel',NULL,NULL,0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'2025-11-15 22:49:09.782365','2025-11-15 22:49:09.782365',8,2,0,1,0),(4,'spray',NULL,NULL,0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'2025-11-15 22:49:09.801042','2025-11-15 22:49:09.801042',9,2,0,1,0),(5,'inspect',NULL,NULL,0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'2025-11-15 22:49:09.825501','2025-11-15 22:49:09.825501',8,2,0,1,0),(6,'impair',NULL,NULL,0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'2025-11-15 22:49:09.840605','2025-11-15 22:49:09.840605',9,2,0,1,0),(7,'wish',NULL,NULL,0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'2025-11-15 22:49:09.862102','2025-11-15 22:49:09.862102',8,2,0,1,0),(8,'bite',NULL,NULL,0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'2025-11-15 22:49:09.877849','2025-11-15 22:49:09.877849',9,2,0,1,0); +/*!40000 ALTER TABLE `assignments` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bookmark_ratings` +-- + +DROP TABLE IF EXISTS `bookmark_ratings`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `bookmark_ratings` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `bookmark_id` int DEFAULT NULL, + `user_id` int DEFAULT NULL, + `rating` int DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bookmark_ratings` +-- + +LOCK TABLES `bookmark_ratings` WRITE; +/*!40000 ALTER TABLE `bookmark_ratings` DISABLE KEYS */; +/*!40000 ALTER TABLE `bookmark_ratings` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bookmarks` +-- + +DROP TABLE IF EXISTS `bookmarks`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `bookmarks` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `url` text, + `title` text, + `description` text, + `user_id` int DEFAULT NULL, + `topic_id` int DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bookmarks` +-- + +LOCK TABLES `bookmarks` WRITE; +/*!40000 ALTER TABLE `bookmarks` DISABLE KEYS */; +/*!40000 ALTER TABLE `bookmarks` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `cakes` +-- + +DROP TABLE IF EXISTS `cakes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `cakes` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `cakes` +-- + +LOCK TABLES `cakes` WRITE; +/*!40000 ALTER TABLE `cakes` DISABLE KEYS */; +/*!40000 ALTER TABLE `cakes` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `courses` +-- + +DROP TABLE IF EXISTS `courses`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `courses` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `directory_path` varchar(255) DEFAULT NULL, + `info` text, + `private` tinyint(1) DEFAULT '0', + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `instructor_id` bigint NOT NULL, + `institution_id` bigint NOT NULL, + PRIMARY KEY (`id`), + KEY `index_courses_on_institution_id` (`institution_id`), + KEY `fk_course_users` (`instructor_id`), + KEY `index_courses_on_instructor_id` (`instructor_id`), + CONSTRAINT `fk_rails_2ab3132eb0` FOREIGN KEY (`instructor_id`) REFERENCES `users` (`id`), + CONSTRAINT `fk_rails_d012129e83` FOREIGN KEY (`institution_id`) REFERENCES `institutions` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `courses` +-- + +LOCK TABLES `courses` WRITE; +/*!40000 ALTER TABLE `courses` DISABLE KEYS */; +INSERT INTO `courses` VALUES (1,'Military','ample_relationship/cruelty-angel','A fake class',0,'2025-11-15 22:49:09.610380','2025-11-15 22:49:09.610380',8,1),(2,'Oil & Energy','midnight-shock/ambiguous-corn','A fake class',0,'2025-11-15 22:49:09.628681','2025-11-15 22:49:09.628681',9,1); +/*!40000 ALTER TABLE `courses` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `due_dates` +-- + +DROP TABLE IF EXISTS `due_dates`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `due_dates` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `due_at` datetime(6) NOT NULL, + `deadline_type_id` int NOT NULL, + `parent_type` varchar(255) NOT NULL, + `parent_id` bigint NOT NULL, + `submission_allowed_id` int NOT NULL, + `review_allowed_id` int NOT NULL, + `round` int DEFAULT NULL, + `flag` tinyint(1) DEFAULT '0', + `threshold` int DEFAULT '1', + `delayed_job_id` varchar(255) DEFAULT NULL, + `deadline_name` varchar(255) DEFAULT NULL, + `description_url` varchar(255) DEFAULT NULL, + `quiz_allowed_id` int DEFAULT '1', + `teammate_review_allowed_id` int DEFAULT '3', + `type` varchar(255) DEFAULT 'AssignmentDueDate', + `resubmission_allowed_id` int DEFAULT NULL, + `rereview_allowed_id` int DEFAULT NULL, + `review_of_review_allowed_id` int DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`), + KEY `index_due_dates_on_parent` (`parent_type`,`parent_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `due_dates` +-- + +LOCK TABLES `due_dates` WRITE; +/*!40000 ALTER TABLE `due_dates` DISABLE KEYS */; +/*!40000 ALTER TABLE `due_dates` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `institutions` +-- + +DROP TABLE IF EXISTS `institutions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `institutions` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `institutions` +-- + +LOCK TABLES `institutions` WRITE; +/*!40000 ALTER TABLE `institutions` DISABLE KEYS */; +INSERT INTO `institutions` VALUES (1,'North Carolina State University','2025-11-15 22:49:03.488893','2025-11-15 22:49:03.488893'); +/*!40000 ALTER TABLE `institutions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `invitations` +-- + +DROP TABLE IF EXISTS `invitations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `invitations` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `assignment_id` int DEFAULT NULL, + `reply_status` varchar(1) DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `from_id` bigint NOT NULL, + `to_id` bigint NOT NULL, + `participant_id` bigint NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_invitation_assignments` (`assignment_id`), + KEY `index_invitations_on_from_id` (`from_id`), + KEY `index_invitations_on_participant_id` (`participant_id`), + KEY `index_invitations_on_to_id` (`to_id`), + CONSTRAINT `fk_rails_5c28345ebb` FOREIGN KEY (`from_id`) REFERENCES `participants` (`id`), + CONSTRAINT `fk_rails_9ac855df28` FOREIGN KEY (`to_id`) REFERENCES `participants` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `invitations` +-- + +LOCK TABLES `invitations` WRITE; +/*!40000 ALTER TABLE `invitations` DISABLE KEYS */; +/*!40000 ALTER TABLE `invitations` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `items` +-- + +DROP TABLE IF EXISTS `items`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `items` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `txt` text, + `weight` int DEFAULT NULL, + `seq` decimal(10,0) DEFAULT NULL, + `question_type` varchar(255) DEFAULT NULL, + `size` varchar(255) DEFAULT NULL, + `alternatives` varchar(255) DEFAULT NULL, + `break_before` tinyint(1) DEFAULT NULL, + `max_label` varchar(255) DEFAULT NULL, + `min_label` varchar(255) DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `questionnaire_id` bigint NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_question_questionnaires` (`questionnaire_id`), + KEY `index_items_on_questionnaire_id` (`questionnaire_id`), + CONSTRAINT `fk_rails_c59f3245d3` FOREIGN KEY (`questionnaire_id`) REFERENCES `questionnaires` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `items` +-- + +LOCK TABLES `items` WRITE; +/*!40000 ALTER TABLE `items` DISABLE KEYS */; +INSERT INTO `items` VALUES (1,'Animi sequi voluptatem est ipsum asperiores quia architecto.',2,1,'Scale','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Assumenda','A','2025-11-15 22:49:21.438745','2025-11-15 22:49:21.438746',1),(2,'Earum a labore modi repellat eveniet consequuntur quasi.',2,2,'Criterion','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Recusandae','Eius','2025-11-15 22:49:21.475030','2025-11-15 22:49:21.475031',1),(3,'Ut natus perferendis quo id error et autem.',2,3,'Scale','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Est','Quidem','2025-11-15 22:49:21.488093','2025-11-15 22:49:21.488094',1),(4,'Eveniet eligendi consequuntur veritatis nam facilis explicabo commodi.',2,4,'TextArea','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Et','Quo','2025-11-15 22:49:21.502363','2025-11-15 22:49:21.502364',1),(5,'Eos impedit voluptate qui ullam ea ipsum atque.',1,5,'Dropdown','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Nam','Nesciunt','2025-11-15 22:49:21.519869','2025-11-15 22:49:21.519870',1),(6,'Laudantium voluptas voluptatem non ut officiis quis deleniti.',2,6,'Criterion','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Et','Iure','2025-11-15 22:49:21.535449','2025-11-15 22:49:21.535449',1),(7,'Ut error et labore repellendus qui ex sapiente.',1,7,'TextArea','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Nulla','Dolorem','2025-11-15 22:49:21.547132','2025-11-15 22:49:21.547133',1),(8,'Tenetur adipisci illo voluptatem aut distinctio et velit.',2,8,'Dropdown','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Vitae','Hic','2025-11-15 22:49:21.564389','2025-11-15 22:49:21.564390',1),(9,'Et ut sit vel accusantium suscipit ipsum fugit.',1,9,'Dropdown','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Aliquam','Eveniet','2025-11-15 22:49:21.578295','2025-11-15 22:49:21.578295',1),(10,'Sit ipsum autem facere dolorum fugiat vel est.',2,10,'TextArea','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Et','Enim','2025-11-15 22:49:21.591428','2025-11-15 22:49:21.591429',1),(11,'Dolore quisquam pariatur quis perferendis ullam praesentium quas.',1,1,'Dropdown','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Dolores','Repellendus','2025-11-15 22:49:21.603968','2025-11-15 22:49:21.603969',2),(12,'Unde ad fugit tenetur autem ex aut provident.',2,2,'Dropdown','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'At','Ipsum','2025-11-15 22:49:21.619502','2025-11-15 22:49:21.619503',2),(13,'Molestiae molestiae amet sunt aliquam quia laboriosam deleniti.',1,3,'Dropdown','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Voluptas','Mollitia','2025-11-15 22:49:21.635878','2025-11-15 22:49:21.635879',2),(14,'Mollitia sed quae fuga doloremque cum sunt eveniet.',2,4,'Criterion','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Rerum','Cumque','2025-11-15 22:49:21.648978','2025-11-15 22:49:21.648978',2),(15,'Et iusto veniam quis eius rerum voluptas perferendis.',2,5,'Dropdown','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Rem','Quaerat','2025-11-15 22:49:21.663995','2025-11-15 22:49:21.663995',2),(16,'Voluptatem exercitationem dolores quia id aut quas voluptatibus.',2,6,'Scale','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Perspiciatis','Eos','2025-11-15 22:49:21.682033','2025-11-15 22:49:21.682033',2),(17,'Alias quos neque officia tempore dolorum expedita a.',2,7,'Criterion','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Facilis','Consequatur','2025-11-15 22:49:21.695772','2025-11-15 22:49:21.695772',2),(18,'Iure in quo fugit eos laudantium non est.',2,8,'Criterion','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Cum','Sint','2025-11-15 22:49:21.711042','2025-11-15 22:49:21.711042',2),(19,'Reprehenderit quis ut omnis corrupti est inventore sint.',1,9,'TextArea','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Sit','Accusantium','2025-11-15 22:49:21.727462','2025-11-15 22:49:21.727462',2),(20,'Et veniam nostrum quia quaerat provident officia ex.',2,10,'Dropdown','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Dolores','Laborum','2025-11-15 22:49:21.742467','2025-11-15 22:49:21.742467',2),(21,'Dolorem modi rerum esse eos voluptas placeat omnis.',1,1,'Scale','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Dolores','Nam','2025-11-15 22:49:21.759247','2025-11-15 22:49:21.759248',3),(22,'Eveniet quia sit aut doloribus sapiente sequi illo.',1,2,'TextArea','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Amet','Minima','2025-11-15 22:49:21.777205','2025-11-15 22:49:21.777206',3),(23,'Veritatis doloribus cumque atque inventore molestiae facere qui.',1,3,'Criterion','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Qui','Possimus','2025-11-15 22:49:21.793967','2025-11-15 22:49:21.793968',3),(24,'Ipsam dolor ea id ducimus doloremque aliquid necessitatibus.',2,4,'Criterion','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Autem','Laudantium','2025-11-15 22:49:21.812252','2025-11-15 22:49:21.812253',3),(25,'Odio atque consequatur dolorem officiis quia autem nemo.',2,5,'Dropdown','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Occaecati','Neque','2025-11-15 22:49:21.826663','2025-11-15 22:49:21.826664',3),(26,'Pariatur et numquam est omnis dolores qui qui.',1,6,'TextArea','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Qui','Rerum','2025-11-15 22:49:21.842886','2025-11-15 22:49:21.842887',3),(27,'Qui sint nobis aut ab voluptas perspiciatis accusantium.',2,7,'Criterion','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Temporibus','Sint','2025-11-15 22:49:21.857543','2025-11-15 22:49:21.857544',3),(28,'Enim explicabo tenetur veniam temporibus sequi reprehenderit et.',2,8,'Dropdown','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Repellat','Sed','2025-11-15 22:49:21.871617','2025-11-15 22:49:21.871617',3),(29,'Dolores et cum debitis quibusdam est modi facere.',1,9,'Dropdown','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Cupiditate','Aut','2025-11-15 22:49:21.887063','2025-11-15 22:49:21.887063',3),(30,'Velit odit nemo hic quia numquam omnis nobis.',2,10,'Scale','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Quia','Consequatur','2025-11-15 22:49:21.903116','2025-11-15 22:49:21.903117',3),(31,'Nisi quo voluptatum velit eveniet aut rerum dolores.',2,1,'TextArea','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Beatae','Voluptates','2025-11-15 22:49:21.918620','2025-11-15 22:49:21.918621',4),(32,'Sed ut quos nulla magnam quas commodi ut.',2,2,'Criterion','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Aspernatur','Quas','2025-11-15 22:49:21.937170','2025-11-15 22:49:21.937171',4),(33,'Laboriosam earum veritatis voluptatibus quis quos quia nisi.',1,3,'TextArea','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Et','Quaerat','2025-11-15 22:49:21.954679','2025-11-15 22:49:21.954680',4),(34,'Ipsam exercitationem dolore dolores consequatur in nulla sit.',2,4,'Scale','60x4','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Recusandae','Amet','2025-11-15 22:49:21.972031','2025-11-15 22:49:21.972031',4),(35,'Rerum velit possimus nobis ipsam officiis ut eveniet.',2,5,'Scale','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Sit','Ut','2025-11-15 22:49:21.986587','2025-11-15 22:49:21.986588',4),(36,'Dolor commodi sit quidem a exercitationem cumque quia.',2,6,'Criterion','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Iure','Ratione','2025-11-15 22:49:22.001151','2025-11-15 22:49:22.001151',4),(37,'Ut a aliquam quod et quia aperiam praesentium.',2,7,'Dropdown','50x3','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Perspiciatis','Id','2025-11-15 22:49:22.017038','2025-11-15 22:49:22.017039',4),(38,'Recusandae error quas vitae aut harum velit corporis.',2,8,'Dropdown','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Eos','Quasi','2025-11-15 22:49:22.034568','2025-11-15 22:49:22.034568',4),(39,'Maxime sed quis sit corporis et molestias quas.',2,9,'Scale','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Saepe','Quis','2025-11-15 22:49:22.048509','2025-11-15 22:49:22.048510',4),(40,'Aut corporis debitis quia perspiciatis dolores optio accusantium.',2,10,'Dropdown','40x2','[\"Yes|No\", \"Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree\"]',1,'Et','Odio','2025-11-15 22:49:22.064913','2025-11-15 22:49:22.064913',4); +/*!40000 ALTER TABLE `items` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `join_team_requests` +-- + +DROP TABLE IF EXISTS `join_team_requests`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `join_team_requests` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `participant_id` int DEFAULT NULL, + `team_id` int DEFAULT NULL, + `comments` text, + `reply_status` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `join_team_requests` +-- + +LOCK TABLES `join_team_requests` WRITE; +/*!40000 ALTER TABLE `join_team_requests` DISABLE KEYS */; +INSERT INTO `join_team_requests` VALUES (1,'2025-11-15 22:49:22.704123','2025-11-15 22:51:39.643432',51,17,'I have experience with Python and machine learning. Would love to contribute to the AI project!','ACCEPTED'),(2,'2025-11-15 22:49:22.726732','2025-11-15 22:54:20.540402',52,18,'I am proficient in React and Node.js. Can help with both frontend and backend!','ACCEPTED'),(3,'2025-11-15 22:49:22.745286','2025-11-15 22:54:00.155401',51,18,'Also interested in web development. Have full-stack experience.','DECLINED'),(4,'2025-11-15 22:49:22.765134','2025-11-15 22:49:22.765134',52,19,'Interested in mobile development!','DECLINED'),(5,'2025-11-15 22:56:24.018459','2025-11-15 22:59:53.082017',50,18,'Responding to advertisement for Web Development','ACCEPTED'),(6,'2025-11-15 22:58:09.167184','2025-11-15 22:59:59.909223',48,18,'Responding to advertisement for Web Development','ACCEPTED'),(7,'2025-11-15 22:59:05.290277','2025-11-15 22:59:05.290277',47,18,'Responding to advertisement for Web Development','PENDING'),(8,'2025-11-15 23:21:12.514923','2025-11-15 23:23:30.147394',49,17,'Responding to advertisement for AI and Machine Learning','ACCEPTED'); +/*!40000 ALTER TABLE `join_team_requests` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `nodes` +-- + +DROP TABLE IF EXISTS `nodes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `nodes` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `parent_id` int DEFAULT NULL, + `node_object_id` int DEFAULT NULL, + `type` varchar(255) DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `nodes` +-- + +LOCK TABLES `nodes` WRITE; +/*!40000 ALTER TABLE `nodes` DISABLE KEYS */; +/*!40000 ALTER TABLE `nodes` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `participants` +-- + +DROP TABLE IF EXISTS `participants`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `participants` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `can_submit` tinyint(1) DEFAULT '1', + `can_review` tinyint(1) DEFAULT '1', + `handle` varchar(255) DEFAULT NULL, + `permission_granted` tinyint(1) DEFAULT '0', + `join_team_request_id` bigint DEFAULT NULL, + `team_id` bigint DEFAULT NULL, + `topic` varchar(255) DEFAULT NULL, + `current_stage` varchar(255) DEFAULT NULL, + `stage_deadline` datetime(6) DEFAULT NULL, + `can_take_quiz` tinyint(1) DEFAULT NULL, + `can_mentor` tinyint(1) DEFAULT NULL, + `authorization` varchar(255) DEFAULT NULL, + `parent_id` int NOT NULL, + `type` varchar(255) NOT NULL, + `grade` float DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `index_participants_on_join_team_request_id` (`join_team_request_id`), + KEY `index_participants_on_team_id` (`team_id`), + KEY `fk_participant_users` (`user_id`), + KEY `index_participants_on_user_id` (`user_id`), + CONSTRAINT `fk_rails_8cf3035ef1` FOREIGN KEY (`join_team_request_id`) REFERENCES `join_team_requests` (`id`), + CONSTRAINT `fk_rails_990c37f108` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`), + CONSTRAINT `fk_rails_b9a3c50f15` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=53 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `participants` +-- + +LOCK TABLES `participants` WRITE; +/*!40000 ALTER TABLE `participants` DISABLE KEYS */; +INSERT INTO `participants` VALUES (1,10,'2025-11-15 22:49:20.067974','2025-11-15 22:49:20.067974',1,1,'simon-rohan',0,NULL,1,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(2,11,'2025-11-15 22:49:20.131044','2025-11-15 22:49:20.131044',1,1,'martine',0,NULL,2,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(3,12,'2025-11-15 22:49:20.164957','2025-11-15 22:49:20.164957',1,1,'joanne-reichert',0,NULL,3,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(4,13,'2025-11-15 22:49:20.201889','2025-11-15 22:49:20.201889',1,1,'antonetta',0,NULL,4,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(5,14,'2025-11-15 22:49:20.249710','2025-11-15 22:49:20.249710',1,1,'glenda',0,NULL,5,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(6,15,'2025-11-15 22:49:20.290664','2025-11-15 22:49:20.290664',1,1,'kenda',0,NULL,6,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(7,16,'2025-11-15 22:49:20.323462','2025-11-15 22:49:20.323462',1,1,'ji',0,NULL,7,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(8,17,'2025-11-15 22:49:20.356540','2025-11-15 22:49:20.356540',1,1,'nelson',0,NULL,8,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(9,18,'2025-11-15 22:49:20.389857','2025-11-15 22:49:20.389857',1,1,'xavier',0,NULL,9,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(10,19,'2025-11-15 22:49:20.421129','2025-11-15 22:49:20.421129',1,1,'rose',0,NULL,10,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(11,20,'2025-11-15 22:49:20.456256','2025-11-15 22:49:20.456256',1,1,'lissa-keeling',0,NULL,11,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(12,21,'2025-11-15 22:49:20.496587','2025-11-15 22:49:20.496587',1,1,'jonathan-gulgowski',0,NULL,12,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(13,22,'2025-11-15 22:49:20.533512','2025-11-15 22:49:20.533512',1,1,'livia',0,NULL,13,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(14,23,'2025-11-15 22:49:20.564724','2025-11-15 22:49:20.564724',1,1,'janiece',0,NULL,14,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(15,24,'2025-11-15 22:49:20.598150','2025-11-15 22:49:20.598150',1,1,'jason',0,NULL,15,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(16,25,'2025-11-15 22:49:20.634535','2025-11-15 22:49:20.634535',1,1,'michel-mohr',0,NULL,16,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(17,26,'2025-11-15 22:49:20.670171','2025-11-15 22:49:20.670171',1,1,'wesley',0,NULL,1,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(18,27,'2025-11-15 22:49:20.698796','2025-11-15 22:49:20.698796',1,1,'eileen',0,NULL,2,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(19,28,'2025-11-15 22:49:20.731918','2025-11-15 22:49:20.731918',1,1,'lynwood-medhurst',0,NULL,3,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(20,29,'2025-11-15 22:49:20.764580','2025-11-15 22:49:20.764580',1,1,'ira',0,NULL,4,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(21,30,'2025-11-15 22:49:20.799624','2025-11-15 22:49:20.799624',1,1,'gavin',0,NULL,5,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(22,31,'2025-11-15 22:49:20.835038','2025-11-15 22:49:20.835038',1,1,'anthony',0,NULL,6,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(23,32,'2025-11-15 22:49:20.872685','2025-11-15 22:49:20.872685',1,1,'anthony-schuppe',0,NULL,7,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(24,33,'2025-11-15 22:49:20.931834','2025-11-15 22:49:20.931834',1,1,'julieann',0,NULL,8,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(25,34,'2025-11-15 22:49:20.948191','2025-11-15 22:49:20.948191',1,1,'milton-sanford',0,NULL,9,NULL,NULL,NULL,NULL,NULL,NULL,1,'CourseParticipant',NULL),(26,35,'2025-11-15 22:49:20.963813','2025-11-15 22:49:20.963813',1,1,'colin-stracke',0,NULL,10,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(27,36,'2025-11-15 22:49:20.980149','2025-11-15 22:49:20.980149',1,1,'rigoberto-oberbrunner',0,NULL,11,NULL,NULL,NULL,NULL,NULL,NULL,1,'CourseParticipant',NULL),(28,37,'2025-11-15 22:49:20.994813','2025-11-15 22:49:20.994813',1,1,'vern-blick',0,NULL,12,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(29,38,'2025-11-15 22:49:21.010647','2025-11-15 22:49:21.010647',1,1,'janyce',0,NULL,13,NULL,NULL,NULL,NULL,NULL,NULL,1,'CourseParticipant',NULL),(30,39,'2025-11-15 22:49:21.023602','2025-11-15 22:49:21.023602',1,1,'kerrie-hane',0,NULL,14,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(31,40,'2025-11-15 22:49:21.043091','2025-11-15 22:49:21.043091',1,1,'jerald',0,NULL,15,NULL,NULL,NULL,NULL,NULL,NULL,1,'CourseParticipant',NULL),(32,41,'2025-11-15 22:49:21.068544','2025-11-15 22:49:21.068544',1,1,'kylie-beier',0,NULL,16,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(33,42,'2025-11-15 22:49:21.095117','2025-11-15 22:49:21.095117',1,1,'willian-hirthe',0,NULL,1,NULL,NULL,NULL,NULL,NULL,NULL,1,'CourseParticipant',NULL),(34,43,'2025-11-15 22:49:21.110682','2025-11-15 22:49:21.110682',1,1,'vincent',0,NULL,2,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(35,44,'2025-11-15 22:49:21.125655','2025-11-15 22:49:21.125655',1,1,'kayla',0,NULL,3,NULL,NULL,NULL,NULL,NULL,NULL,1,'CourseParticipant',NULL),(36,45,'2025-11-15 22:49:21.143264','2025-11-15 22:49:21.143264',1,1,'hung',0,NULL,4,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(37,46,'2025-11-15 22:49:21.160411','2025-11-15 22:49:21.160411',1,1,'ermelinda-mills',0,NULL,5,NULL,NULL,NULL,NULL,NULL,NULL,1,'CourseParticipant',NULL),(38,47,'2025-11-15 22:49:21.172396','2025-11-15 22:49:21.172396',1,1,'whitney',0,NULL,6,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(39,48,'2025-11-15 22:49:21.185994','2025-11-15 22:49:21.185994',1,1,'sergio',0,NULL,7,NULL,NULL,NULL,NULL,NULL,NULL,1,'CourseParticipant',NULL),(40,49,'2025-11-15 22:49:21.202478','2025-11-15 22:49:21.202478',1,1,'stanford',0,NULL,8,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(41,50,'2025-11-15 22:49:21.217308','2025-11-15 22:49:21.217308',1,1,'margene',0,NULL,9,NULL,NULL,NULL,NULL,NULL,NULL,1,'CourseParticipant',NULL),(42,51,'2025-11-15 22:49:21.233449','2025-11-15 22:49:21.233449',1,1,'wanda-oreilly',0,NULL,10,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(43,52,'2025-11-15 22:49:21.254452','2025-11-15 22:49:21.254452',1,1,'james',0,NULL,11,NULL,NULL,NULL,NULL,NULL,NULL,1,'CourseParticipant',NULL),(44,53,'2025-11-15 22:49:21.269216','2025-11-15 22:49:21.269216',1,1,'gregory',0,NULL,12,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(45,54,'2025-11-15 22:49:21.283245','2025-11-15 22:49:21.283245',1,1,'johnie-zemlak',0,NULL,13,NULL,NULL,NULL,NULL,NULL,NULL,1,'CourseParticipant',NULL),(46,55,'2025-11-15 22:49:21.298326','2025-11-15 22:49:21.298326',1,1,'erline',0,NULL,14,NULL,NULL,NULL,NULL,NULL,NULL,2,'CourseParticipant',NULL),(47,2,'2025-11-15 22:49:22.304114','2025-11-15 22:49:22.456587',1,1,'alice',0,NULL,17,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(48,3,'2025-11-15 22:49:22.319883','2025-11-15 22:49:22.465496',1,1,'bob',0,NULL,17,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(49,4,'2025-11-15 22:49:22.334399','2025-11-15 22:49:22.558760',1,1,'charlie',0,NULL,18,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(50,5,'2025-11-15 22:49:22.349995','2025-11-15 22:49:22.631453',1,1,'diana',0,NULL,19,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(51,6,'2025-11-15 22:49:22.367524','2025-11-15 22:49:22.367524',1,1,'ethan',0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL),(52,7,'2025-11-15 22:49:22.384185','2025-11-15 22:49:22.384185',1,1,'fiona',0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,'AssignmentParticipant',NULL); +/*!40000 ALTER TABLE `participants` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `question_advices` +-- + +DROP TABLE IF EXISTS `question_advices`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `question_advices` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `question_id` bigint NOT NULL, + `score` int DEFAULT NULL, + `advice` text, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`), + KEY `index_question_advices_on_question_id` (`question_id`), + CONSTRAINT `fk_rails_e2f223545a` FOREIGN KEY (`question_id`) REFERENCES `items` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `question_advices` +-- + +LOCK TABLES `question_advices` WRITE; +/*!40000 ALTER TABLE `question_advices` DISABLE KEYS */; +/*!40000 ALTER TABLE `question_advices` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `question_types` +-- + +DROP TABLE IF EXISTS `question_types`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `question_types` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `question_types` +-- + +LOCK TABLES `question_types` WRITE; +/*!40000 ALTER TABLE `question_types` DISABLE KEYS */; +/*!40000 ALTER TABLE `question_types` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `questionnaire_types` +-- + +DROP TABLE IF EXISTS `questionnaire_types`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `questionnaire_types` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `questionnaire_types` +-- + +LOCK TABLES `questionnaire_types` WRITE; +/*!40000 ALTER TABLE `questionnaire_types` DISABLE KEYS */; +/*!40000 ALTER TABLE `questionnaire_types` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `questionnaires` +-- + +DROP TABLE IF EXISTS `questionnaires`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `questionnaires` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `instructor_id` int DEFAULT NULL, + `private` tinyint(1) DEFAULT NULL, + `min_question_score` int DEFAULT NULL, + `max_question_score` int DEFAULT NULL, + `questionnaire_type` varchar(255) DEFAULT NULL, + `display_type` varchar(255) DEFAULT NULL, + `instruction_loc` text, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `questionnaires` +-- + +LOCK TABLES `questionnaires` WRITE; +/*!40000 ALTER TABLE `questionnaires` DISABLE KEYS */; +INSERT INTO `questionnaires` VALUES (1,'Ab Et Nesciunt Corrupti Dolore',4,0,0,5,'ReviewQuestionnaire','Review',NULL,'2025-11-15 22:49:21.326436','2025-11-15 22:49:21.326436'),(2,'Adipisci Qui Voluptatem Quidem Ipsum',3,0,0,5,'ReviewQuestionnaire','Review',NULL,'2025-11-15 22:49:21.375613','2025-11-15 22:49:21.375614'),(3,'Vel Minus Vero Eum Dolores',1,0,0,5,'ReviewQuestionnaire','Review',NULL,'2025-11-15 22:49:21.387979','2025-11-15 22:49:21.387979'),(4,'Eaque Aut A Blanditiis Similique',3,0,0,5,'ReviewQuestionnaire','Review',NULL,'2025-11-15 22:49:21.404540','2025-11-15 22:49:21.404541'); +/*!40000 ALTER TABLE `questionnaires` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `quiz_question_choices` +-- + +DROP TABLE IF EXISTS `quiz_question_choices`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `quiz_question_choices` ( + `id` int NOT NULL AUTO_INCREMENT, + `question_id` int DEFAULT NULL, + `txt` text, + `iscorrect` tinyint(1) DEFAULT '0', + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `quiz_question_choices` +-- + +LOCK TABLES `quiz_question_choices` WRITE; +/*!40000 ALTER TABLE `quiz_question_choices` DISABLE KEYS */; +/*!40000 ALTER TABLE `quiz_question_choices` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `response_maps` +-- + +DROP TABLE IF EXISTS `response_maps`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `response_maps` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `reviewed_object_id` int NOT NULL DEFAULT '0', + `reviewer_id` int NOT NULL DEFAULT '0', + `reviewee_id` int NOT NULL DEFAULT '0', + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `type` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `fk_response_map_reviewer` (`reviewer_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `response_maps` +-- + +LOCK TABLES `response_maps` WRITE; +/*!40000 ALTER TABLE `response_maps` DISABLE KEYS */; +/*!40000 ALTER TABLE `response_maps` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `responses` +-- + +DROP TABLE IF EXISTS `responses`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `responses` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `map_id` int NOT NULL DEFAULT '0', + `additional_comment` text, + `is_submitted` tinyint(1) DEFAULT '0', + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `round` int DEFAULT NULL, + `version_num` int DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `fk_response_response_map` (`map_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `responses` +-- + +LOCK TABLES `responses` WRITE; +/*!40000 ALTER TABLE `responses` DISABLE KEYS */; +/*!40000 ALTER TABLE `responses` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `roles` +-- + +DROP TABLE IF EXISTS `roles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `roles` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `parent_id` bigint DEFAULT NULL, + `default_page_id` int DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_rails_4404228d2f` (`parent_id`), + CONSTRAINT `fk_rails_4404228d2f` FOREIGN KEY (`parent_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `roles` +-- + +LOCK TABLES `roles` WRITE; +/*!40000 ALTER TABLE `roles` DISABLE KEYS */; +INSERT INTO `roles` VALUES (1,'Super Administrator',NULL,NULL,'2025-11-15 22:49:03.561397','2025-11-15 22:49:03.561397'),(2,'Administrator',NULL,NULL,'2025-11-15 22:49:03.580891','2025-11-15 22:49:03.580891'),(3,'Instructor',NULL,NULL,'2025-11-15 22:49:03.603641','2025-11-15 22:49:03.603641'),(4,'Teaching Assistant',NULL,NULL,'2025-11-15 22:49:03.621059','2025-11-15 22:49:03.621059'),(5,'Student',NULL,NULL,'2025-11-15 22:49:03.643916','2025-11-15 22:49:03.643916'); +/*!40000 ALTER TABLE `roles` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `schema_migrations` +-- + +DROP TABLE IF EXISTS `schema_migrations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `schema_migrations` ( + `version` varchar(255) NOT NULL, + PRIMARY KEY (`version`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `schema_migrations` +-- + +LOCK TABLES `schema_migrations` WRITE; +/*!40000 ALTER TABLE `schema_migrations` DISABLE KEYS */; +INSERT INTO `schema_migrations` VALUES ('20230305064753'),('20230305185139'),('20230306022503'),('20230306035806'),('20230401213353'),('20230401213404'),('20230412013301'),('20230412013310'),('20230412020156'),('20230415003243'),('20230415011209'),('20230424172126'),('20230424172612'),('20230424173506'),('20230424174001'),('20230424174153'),('20230427171632'),('20231019170608'),('20231019195109'),('20231026002451'),('20231026002543'),('20231027211715'),('20231028012101'),('20231030174450'),('20231102173153'),('20231104070639'),('20231104071922'),('20231105193016'),('20231105193219'),('20231129021640'),('20231129023417'),('20231129024913'),('20231129050431'),('20231129051018'),('20231130030500'),('20231130030611'),('20231130030646'),('20231130033226'),('20231130033325'),('20231130033332'),('20231201012040'),('20231201024204'),('20240318205124'),('20240324000112'),('20240415155554'),('20240415163413'),('20240415192048'),('20240420000000'),('20240420070000'),('20241015223136'),('20241201224112'),('20241201224137'),('20241202165201'),('20250214224716'),('20250216020117'),('20250324193058'),('20250401020016'),('20250414002952'),('20250414005152'),('20250418004442'),('20250418013852'),('20250418014519'),('20250427014225'),('20250621151644'),('20250621152946'),('20250621180527'),('20250621180851'),('20250629185100'),('20250629185439'),('20250629190818'),('20250727170825'),('20250805174104'),('20251021165336'),('20251022160053'),('20251029071649'); +/*!40000 ALTER TABLE `schema_migrations` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `sign_up_topics` +-- + +DROP TABLE IF EXISTS `sign_up_topics`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sign_up_topics` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `topic_name` text NOT NULL, + `assignment_id` bigint NOT NULL, + `max_choosers` int NOT NULL DEFAULT '0', + `category` text, + `topic_identifier` varchar(10) DEFAULT NULL, + `micropayment` int DEFAULT '0', + `private_to` int DEFAULT NULL, + `description` text, + `link` varchar(255) DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_sign_up_categories_sign_up_topics` (`assignment_id`), + KEY `index_sign_up_topics_on_assignment_id` (`assignment_id`), + CONSTRAINT `fk_rails_c15a869a32` FOREIGN KEY (`assignment_id`) REFERENCES `assignments` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sign_up_topics` +-- + +LOCK TABLES `sign_up_topics` WRITE; +/*!40000 ALTER TABLE `sign_up_topics` DISABLE KEYS */; +INSERT INTO `sign_up_topics` VALUES (1,'AI and Machine Learning',1,2,NULL,NULL,0,NULL,'Research on artificial intelligence applications',NULL,'2025-11-15 22:49:22.252164','2025-11-15 22:49:22.252164'),(2,'Web Development',1,2,NULL,NULL,0,NULL,'Modern web development frameworks and tools',NULL,'2025-11-15 22:49:22.266382','2025-11-15 22:49:22.266382'),(3,'Mobile Applications',1,2,NULL,NULL,0,NULL,'iOS and Android app development',NULL,'2025-11-15 22:49:22.286811','2025-11-15 22:49:22.286811'); +/*!40000 ALTER TABLE `sign_up_topics` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `signed_up_teams` +-- + +DROP TABLE IF EXISTS `signed_up_teams`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `signed_up_teams` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `sign_up_topic_id` bigint NOT NULL, + `team_id` bigint NOT NULL, + `is_waitlisted` tinyint(1) DEFAULT NULL, + `preference_priority_number` int DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `comments_for_advertisement` text, + `advertise_for_partner` tinyint(1) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `index_signed_up_teams_on_sign_up_topic_id` (`sign_up_topic_id`), + KEY `index_signed_up_teams_on_team_id` (`team_id`), + CONSTRAINT `fk_rails_b3a6d3624c` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`), + CONSTRAINT `fk_rails_f886024d81` FOREIGN KEY (`sign_up_topic_id`) REFERENCES `sign_up_topics` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `signed_up_teams` +-- + +LOCK TABLES `signed_up_teams` WRITE; +/*!40000 ALTER TABLE `signed_up_teams` DISABLE KEYS */; +INSERT INTO `signed_up_teams` VALUES (1,1,17,0,NULL,'2025-11-15 22:49:22.516383','2025-11-15 22:49:22.516383','Python &AND& TensorFlow &AND& Data Science',1),(2,2,18,0,NULL,'2025-11-15 22:49:22.579260','2025-11-15 22:49:22.579260','React &AND& Node.js &AND& TypeScript',1),(3,3,19,0,NULL,'2025-11-15 22:49:22.649225','2025-11-15 22:49:22.649225',NULL,0); +/*!40000 ALTER TABLE `signed_up_teams` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ta_mappings` +-- + +DROP TABLE IF EXISTS `ta_mappings`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `ta_mappings` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `course_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`), + KEY `index_ta_mappings_on_course_id` (`course_id`), + KEY `fk_ta_mapping_users` (`user_id`), + KEY `index_ta_mappings_on_user_id` (`user_id`), + CONSTRAINT `fk_rails_3db3e2b248` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`), + CONSTRAINT `fk_rails_f98655c908` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ta_mappings` +-- + +LOCK TABLES `ta_mappings` WRITE; +/*!40000 ALTER TABLE `ta_mappings` DISABLE KEYS */; +/*!40000 ALTER TABLE `ta_mappings` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `teams` +-- + +DROP TABLE IF EXISTS `teams`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `teams` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `name` varchar(255) NOT NULL, + `type` varchar(255) NOT NULL, + `parent_id` int NOT NULL, + `grade_for_submission` int DEFAULT NULL, + `comment_for_submission` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `teams` +-- + +LOCK TABLES `teams` WRITE; +/*!40000 ALTER TABLE `teams` DISABLE KEYS */; +INSERT INTO `teams` VALUES (1,'2025-11-15 22:49:09.978170','2025-11-15 22:49:09.978170','denice','AssignmentTeam',1,NULL,NULL),(2,'2025-11-15 22:49:09.993937','2025-11-15 22:49:09.993937','adan stoltenberg','AssignmentTeam',1,NULL,NULL),(3,'2025-11-15 22:49:10.009747','2025-11-15 22:49:10.009747','sang','AssignmentTeam',1,NULL,NULL),(4,'2025-11-15 22:49:10.026409','2025-11-15 22:49:10.026409','herb','AssignmentTeam',1,NULL,NULL),(5,'2025-11-15 22:49:10.044312','2025-11-15 22:49:10.044312','giuseppe wiegand','AssignmentTeam',1,NULL,NULL),(6,'2025-11-15 22:49:10.077570','2025-11-15 22:49:10.077570','delia rogahn','AssignmentTeam',1,NULL,NULL),(7,'2025-11-15 22:49:10.176154','2025-11-15 22:49:10.176154','jordon spencer','AssignmentTeam',1,NULL,NULL),(8,'2025-11-15 22:49:10.190295','2025-11-15 22:49:10.190295','cruz','AssignmentTeam',1,NULL,NULL),(9,'2025-11-15 22:49:10.224191','2025-11-15 22:49:10.224191','lakeshia borer','CourseTeam',2,NULL,NULL),(10,'2025-11-15 22:49:10.240950','2025-11-15 22:49:10.240950','roni grimes','CourseTeam',2,NULL,NULL),(11,'2025-11-15 22:49:10.254785','2025-11-15 22:49:10.254785','gary predovic','CourseTeam',2,NULL,NULL),(12,'2025-11-15 22:49:10.279512','2025-11-15 22:49:10.279512','moses jakubowski','CourseTeam',2,NULL,NULL),(13,'2025-11-15 22:49:10.309244','2025-11-15 22:49:10.309244','gita','CourseTeam',2,NULL,NULL),(14,'2025-11-15 22:49:10.333895','2025-11-15 22:49:10.333895','machelle','CourseTeam',2,NULL,NULL),(15,'2025-11-15 22:49:10.349560','2025-11-15 22:49:10.349560','daine corkery','CourseTeam',2,NULL,NULL),(16,'2025-11-15 22:49:10.370800','2025-11-15 22:49:10.370800','verline','CourseTeam',2,NULL,NULL),(17,'2025-11-15 22:49:22.399739','2025-11-15 22:49:22.399739','AI Innovators','AssignmentTeam',1,NULL,NULL),(18,'2025-11-15 22:49:22.531058','2025-11-15 22:49:22.531058','Web Warriors','AssignmentTeam',1,NULL,NULL),(19,'2025-11-15 22:49:22.594653','2025-11-15 22:49:22.594653','Mobile Masters','AssignmentTeam',1,NULL,NULL); +/*!40000 ALTER TABLE `teams` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `teams_participants` +-- + +DROP TABLE IF EXISTS `teams_participants`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `teams_participants` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `team_id` bigint NOT NULL, + `duty_id` int DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `participant_id` bigint NOT NULL, + `user_id` int NOT NULL, + PRIMARY KEY (`id`), + KEY `index_teams_participants_on_participant_id` (`participant_id`), + KEY `index_teams_participants_on_team_id` (`team_id`), + KEY `index_teams_participants_on_user_id` (`user_id`), + CONSTRAINT `fk_rails_f4d20198de` FOREIGN KEY (`participant_id`) REFERENCES `participants` (`id`), + CONSTRAINT `fk_rails_fc217eb52e` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `teams_participants` +-- + +LOCK TABLES `teams_participants` WRITE; +/*!40000 ALTER TABLE `teams_participants` DISABLE KEYS */; +INSERT INTO `teams_participants` VALUES (1,1,NULL,'2025-11-15 22:49:20.120273','2025-11-15 22:49:20.120273',1,10),(2,2,NULL,'2025-11-15 22:49:20.151957','2025-11-15 22:49:20.151957',2,11),(3,3,NULL,'2025-11-15 22:49:20.187710','2025-11-15 22:49:20.187710',3,12),(4,4,NULL,'2025-11-15 22:49:20.230627','2025-11-15 22:49:20.230627',4,13),(5,5,NULL,'2025-11-15 22:49:20.273339','2025-11-15 22:49:20.273339',5,14),(6,6,NULL,'2025-11-15 22:49:20.312217','2025-11-15 22:49:20.312217',6,15),(7,7,NULL,'2025-11-15 22:49:20.343751','2025-11-15 22:49:20.343751',7,16),(8,8,NULL,'2025-11-15 22:49:20.376074','2025-11-15 22:49:20.376074',8,17),(9,9,NULL,'2025-11-15 22:49:20.409146','2025-11-15 22:49:20.409146',9,18),(10,10,NULL,'2025-11-15 22:49:20.440938','2025-11-15 22:49:20.440938',10,19),(11,11,NULL,'2025-11-15 22:49:20.482101','2025-11-15 22:49:20.482101',11,20),(12,12,NULL,'2025-11-15 22:49:20.519310','2025-11-15 22:49:20.519310',12,21),(13,13,NULL,'2025-11-15 22:49:20.552850','2025-11-15 22:49:20.552850',13,22),(14,14,NULL,'2025-11-15 22:49:20.586153','2025-11-15 22:49:20.586153',14,23),(15,15,NULL,'2025-11-15 22:49:20.621640','2025-11-15 22:49:20.621640',15,24),(16,16,NULL,'2025-11-15 22:49:20.657731','2025-11-15 22:49:20.657731',16,25),(17,1,NULL,'2025-11-15 22:49:20.687672','2025-11-15 22:49:20.687672',17,26),(18,2,NULL,'2025-11-15 22:49:20.719120','2025-11-15 22:49:20.719120',18,27),(19,3,NULL,'2025-11-15 22:49:20.752424','2025-11-15 22:49:20.752424',19,28),(20,4,NULL,'2025-11-15 22:49:20.785478','2025-11-15 22:49:20.785478',20,29),(21,5,NULL,'2025-11-15 22:49:20.821631','2025-11-15 22:49:20.821631',21,30),(22,6,NULL,'2025-11-15 22:49:20.857609','2025-11-15 22:49:20.857609',22,31),(23,7,NULL,'2025-11-15 22:49:20.891295','2025-11-15 22:49:20.891295',23,32),(24,17,NULL,'2025-11-15 22:49:22.426925','2025-11-15 22:49:22.426925',47,2),(25,17,NULL,'2025-11-15 22:49:22.447826','2025-11-15 22:49:22.447826',48,3),(26,18,NULL,'2025-11-15 22:49:22.550799','2025-11-15 22:49:22.550799',49,4),(27,19,NULL,'2025-11-15 22:49:22.619734','2025-11-15 22:49:22.619734',50,5),(28,17,NULL,'2025-11-15 22:51:39.631775','2025-11-15 22:51:39.631775',51,6),(29,18,NULL,'2025-11-15 22:54:20.533274','2025-11-15 22:54:20.533274',52,7),(30,18,NULL,'2025-11-15 22:59:53.075085','2025-11-15 22:59:53.075085',50,5),(31,18,NULL,'2025-11-15 22:59:59.901036','2025-11-15 22:59:59.901036',48,3),(32,17,NULL,'2025-11-15 23:23:30.137080','2025-11-15 23:23:30.137080',49,4); +/*!40000 ALTER TABLE `teams_participants` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `teams_users` +-- + +DROP TABLE IF EXISTS `teams_users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `teams_users` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `team_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + PRIMARY KEY (`id`), + KEY `index_teams_users_on_team_id` (`team_id`), + KEY `index_teams_users_on_user_id` (`user_id`), + CONSTRAINT `fk_rails_74983f37ec` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`), + CONSTRAINT `fk_rails_7caef73a94` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `teams_users` +-- + +LOCK TABLES `teams_users` WRITE; +/*!40000 ALTER TABLE `teams_users` DISABLE KEYS */; +/*!40000 ALTER TABLE `teams_users` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `users` +-- + +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `users` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `password_digest` varchar(255) DEFAULT NULL, + `full_name` varchar(255) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `mru_directory_path` varchar(255) DEFAULT NULL, + `email_on_review` tinyint(1) DEFAULT '0', + `email_on_submission` tinyint(1) DEFAULT '0', + `email_on_review_of_review` tinyint(1) DEFAULT '0', + `is_new_user` tinyint(1) DEFAULT '1', + `master_permission_granted` tinyint(1) DEFAULT '0', + `handle` varchar(255) DEFAULT NULL, + `persistence_token` varchar(255) DEFAULT NULL, + `timeZonePref` varchar(255) DEFAULT NULL, + `copy_of_emails` tinyint(1) DEFAULT '0', + `etc_icons_on_homepage` tinyint(1) DEFAULT '0', + `locale` int DEFAULT NULL, + `created_at` datetime(6) NOT NULL, + `updated_at` datetime(6) NOT NULL, + `institution_id` bigint DEFAULT NULL, + `role_id` bigint NOT NULL, + `parent_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `index_users_on_institution_id` (`institution_id`), + KEY `index_users_on_parent_id` (`parent_id`), + KEY `index_users_on_role_id` (`role_id`), + CONSTRAINT `fk_rails_642f17018b` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`), + CONSTRAINT `fk_rails_684a13307d` FOREIGN KEY (`parent_id`) REFERENCES `users` (`id`), + CONSTRAINT `fk_rails_7fcf39ca13` FOREIGN KEY (`institution_id`) REFERENCES `institutions` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=56 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `users` +-- + +LOCK TABLES `users` WRITE; +/*!40000 ALTER TABLE `users` DISABLE KEYS */; +INSERT INTO `users` VALUES (1,'admin','$2a$12$Vx8Y.xeROXxiS/L0y/V1lOAPNDRU61X698xqy23ZTOFFEQxdydnwS','admin admin','admin2@example.com',NULL,0,0,0,1,0,NULL,NULL,NULL,0,1,NULL,'2025-11-15 22:49:04.137704','2025-11-15 22:49:04.137704',1,1,NULL),(2,'alice','$2a$12$MY4nYh9fMeKYLJgZq/VmTuD3k2txq0wFhQnpDO4hcLy0TsPHMdGWC','Alice Johnson','alice@example.com',NULL,0,0,0,1,0,'alice',NULL,NULL,0,1,NULL,'2025-11-15 22:49:05.271823','2025-11-15 22:49:05.271823',1,5,NULL),(3,'bob','$2a$12$QBigs3zR7OX0O9tvjC0XROMA938IBxN9xYf082No4ANivlTPK88Na','Bob Smith','bob@example.com',NULL,0,0,0,1,0,'bob',NULL,NULL,0,1,NULL,'2025-11-15 22:49:06.475531','2025-11-15 22:49:06.475531',1,5,NULL),(4,'charlie','$2a$12$Q343KEH76jtMUXbpHjgd8.UsjsQffk.T6KGeXEnKtcRWjLt19Yh0C','Charlie Davis','charlie@example.com',NULL,0,0,0,1,0,'charlie',NULL,NULL,0,1,NULL,'2025-11-15 22:49:06.700163','2025-11-15 22:49:06.700163',1,5,NULL),(5,'diana','$2a$12$VNyl5.UMIv/IbgvyMHF4W.8hRzog60gZLP9ex5Nrg.dJzkiRt1fn6','Diana Martinez','diana@example.com',NULL,0,0,0,1,0,'diana',NULL,NULL,0,1,NULL,'2025-11-15 22:49:06.918754','2025-11-15 22:49:06.918754',1,5,NULL),(6,'ethan','$2a$12$9K7/6ME2hRezvI1hg16.r..NPOQZKx46NM9Mt6cskYDfTPORKbERa','Ethan Brown','ethan@example.com',NULL,0,0,0,1,0,'ethan',NULL,NULL,0,1,NULL,'2025-11-15 22:49:07.130909','2025-11-15 22:49:07.130909',1,5,NULL),(7,'fiona','$2a$12$VbG2NF21lktaVQE3TdprbeF6y7hxBZMKeyGkZZSKxeQDvWIy/5I/u','Fiona Wilson','fiona@example.com',NULL,0,0,0,1,0,'fiona',NULL,NULL,0,1,NULL,'2025-11-15 22:49:07.352882','2025-11-15 22:49:07.352882',1,5,NULL),(8,'heike','$2a$12$YjZNcOlcQHP/fZZNy29P4OQiC1IX8q34ISe95q/uDgQWtsvR77Y2G','Blaine Graham','troy@moore.example',NULL,0,0,0,1,0,NULL,NULL,NULL,0,1,NULL,'2025-11-15 22:49:09.361012','2025-11-15 22:49:09.361012',1,3,NULL),(9,'jimmy_hartmann','$2a$12$ctzF5S5Oa7ln7.zqaQOE3uppVAukgqbZzr27z6/JnVpW37H8dYYRy','Rep. Laurena Ledner','mariel@howe.example',NULL,0,0,0,1,0,NULL,NULL,NULL,0,1,NULL,'2025-11-15 22:49:09.558354','2025-11-15 22:49:09.558354',1,3,NULL),(10,'breann_ohara','$2a$12$lQE59ews5RzQ8G/eGulW7uiYUXxf8V3Vd7c0AETVyLFSSr9D79.12','Agustin Feil','phung@hettinger-schultz.example',NULL,0,0,0,1,0,'simon-rohan',NULL,NULL,0,1,NULL,'2025-11-15 22:49:10.589466','2025-11-15 22:49:10.589466',1,5,NULL),(11,'nan','$2a$12$EzVoweoIZDrhIcqt3j9vp.lYK8NceLtYEvbqlP/zdPwlt1roGhgIe','Ricky Okuneva','joseph@lemke.example',NULL,0,0,0,1,0,'martine',NULL,NULL,0,1,NULL,'2025-11-15 22:49:10.790924','2025-11-15 22:49:10.790924',1,5,NULL),(12,'raymond_dibbert','$2a$12$MPj82l1s1ODjSq7bxoyODOVDteqOYNwBCymcnbSf5Uu4jyedIXAf2','Bret Moore','lakeesha_donnelly@armstrong-bogisich.example',NULL,0,0,0,1,0,'joanne-reichert',NULL,NULL,0,1,NULL,'2025-11-15 22:49:10.983761','2025-11-15 22:49:10.983761',1,5,NULL),(13,'marshall_block','$2a$12$jYJxb9V4I0qNY6hyNlfJ4e.G4cNYs1.4QzP.avwsSK3Tfq10lgSv6','Arthur Mertz','zack@lehner-nikolaus.test',NULL,0,0,0,1,0,'antonetta',NULL,NULL,0,1,NULL,'2025-11-15 22:49:11.196071','2025-11-15 22:49:11.196071',1,5,NULL),(14,'kirby_kihn','$2a$12$.KDaZzjPFA6t4htIBshql.iUqw4cBbRuvuJHcqk/XGKcw9W.fMbKu','Raymond Nolan','tobias.treutel@schuster-gutkowski.test',NULL,0,0,0,1,0,'glenda',NULL,NULL,0,1,NULL,'2025-11-15 22:49:11.402705','2025-11-15 22:49:11.402705',1,5,NULL),(15,'pablo_medhurst','$2a$12$Kw/yW8tdqM3tWd8mplQAZu.2ZO4dRBx2XrKX87qFcxEfuP3rmQ3qu','Logan Yundt','chang@funk-bergstrom.test',NULL,0,0,0,1,0,'kenda',NULL,NULL,0,1,NULL,'2025-11-15 22:49:11.605050','2025-11-15 22:49:11.605050',1,5,NULL),(16,'heather','$2a$12$Jsu8iL1Io1IxZgdvb9HqfeMpqzdqF56WewRPIBSSFs23oOSFlauh6','Sam Leannon','robin@white.example',NULL,0,0,0,1,0,'ji',NULL,NULL,0,1,NULL,'2025-11-15 22:49:11.805567','2025-11-15 22:49:11.805567',1,5,NULL),(17,'edie_hane','$2a$12$fsCbIXuTfWGoHygg2mR7Xu4mdk3TNHXLvcc/5MptYxXRpoAl2Us0q','Jeffry Kris','lawerence@bednar.example',NULL,0,0,0,1,0,'nelson',NULL,NULL,0,1,NULL,'2025-11-15 22:49:12.007642','2025-11-15 22:49:12.007642',1,5,NULL),(18,'rosenda_morar','$2a$12$hXK32BUXSHNmz1Co1A0jSe7V9harj5ZXMmC.ZCTbzCBKwI9J4wQma','Jackson Kautzer','reyes.stiedemann@lang.test',NULL,0,0,0,1,0,'xavier',NULL,NULL,0,1,NULL,'2025-11-15 22:49:12.214776','2025-11-15 22:49:12.214776',1,5,NULL),(19,'edison','$2a$12$2gBlIWdChpu5CRClWwrXKemFlGhKRh.9TEHyuF394cb7vVrMAaQ7C','Rep. Sheldon Miller','abe@okeefe-jacobs.test',NULL,0,0,0,1,0,'rose',NULL,NULL,0,1,NULL,'2025-11-15 22:49:12.443180','2025-11-15 22:49:12.443180',1,5,NULL),(20,'rebeca_metz','$2a$12$NIUY5o2NpuDCsbvLKLSaQ.WmGd4uoNGVvbvSkK3ljxnqDRaqluPsq','Mariel Hettinger','damien@kunde-corwin.test',NULL,0,0,0,1,0,'lissa-keeling',NULL,NULL,0,1,NULL,'2025-11-15 22:49:12.677076','2025-11-15 22:49:12.677076',1,5,NULL),(21,'francesco','$2a$12$Bt52GRFtVAitdE4UQnuug.Ui8zhujseb87V8V5eWlShGTq.FoOYPe','Thomasena Prohaska','danae.bosco@lakin.test',NULL,0,0,0,1,0,'jonathan-gulgowski',NULL,NULL,0,1,NULL,'2025-11-15 22:49:12.880843','2025-11-15 22:49:12.880843',1,5,NULL),(22,'jacob','$2a$12$ApRpl.N48d17smJW6cexJeIZEQ0SDAnfw6mtxyvt90mmsQcNCc6Ai','Benjamin Romaguera','marty@okon.test',NULL,0,0,0,1,0,'livia',NULL,NULL,0,1,NULL,'2025-11-15 22:49:13.091180','2025-11-15 22:49:13.091180',1,5,NULL),(23,'ivory_pouros','$2a$12$Fv1F3x70gpNij.JCU1M/q.T3ja5wQRe1hLclz4I3bwAOJ9VOd5uRW','Germaine Altenwerth','marlena.predovic@koelpin.test',NULL,0,0,0,1,0,'janiece',NULL,NULL,0,1,NULL,'2025-11-15 22:49:13.327186','2025-11-15 22:49:13.327186',1,5,NULL),(24,'seymour','$2a$12$L7gn/H7kcY7vVnjgL4gjxOJ3E7tgx/bFzImFIpP.MHYzDZwjY3eA.','Hugh Shields','hassan.labadie@cartwright-witting.example',NULL,0,0,0,1,0,'jason',NULL,NULL,0,1,NULL,'2025-11-15 22:49:13.538869','2025-11-15 22:49:13.538869',1,5,NULL),(25,'catherina','$2a$12$BxEJQFfBN2623ngTs4LplOBib12i1RqZ7xXeMzpNFQtGkPSfN.H3y','Jeremy Wisoky','andreas_schoen@harris.example',NULL,0,0,0,1,0,'michel-mohr',NULL,NULL,0,1,NULL,'2025-11-15 22:49:13.734944','2025-11-15 22:49:13.734944',1,5,NULL),(26,'damian','$2a$12$.K7krwTb7fjySi2vcXu6He8xG4ziRmLkl542qm9PxAX6IqvFJN9um','Norman Rodriguez','russel@lockman.test',NULL,0,0,0,1,0,'wesley',NULL,NULL,0,1,NULL,'2025-11-15 22:49:13.947616','2025-11-15 22:49:13.947616',1,5,NULL),(27,'lakita','$2a$12$OBaHESnqSiuqvbwNpB70ueRuS5k39QVHFgVlPZQVFX0Ao1v2NUfHK','Almeda Wuckert Sr.','miranda@cole-sauer.example',NULL,0,0,0,1,0,'eileen',NULL,NULL,0,1,NULL,'2025-11-15 22:49:14.148864','2025-11-15 22:49:14.148864',1,5,NULL),(28,'betsey_breitenberg','$2a$12$k.tYvUNLiZ/WhKT/8kirgeW9ZuVHagWmEG9HUZJ7v0AvBrMgNuy8q','Marc Wuckert','johnie@bahringer-quigley.example',NULL,0,0,0,1,0,'lynwood-medhurst',NULL,NULL,0,1,NULL,'2025-11-15 22:49:14.347485','2025-11-15 22:49:14.347485',1,5,NULL),(29,'aldo','$2a$12$UbpJC6KFTiusZbkp.m3rzecCdVeXlRQemiGETvEX..F.3GaA2xQkq','Connie Moen','timothy_little@kovacek.test',NULL,0,0,0,1,0,'ira',NULL,NULL,0,1,NULL,'2025-11-15 22:49:14.554583','2025-11-15 22:49:14.554583',1,5,NULL),(30,'fidela','$2a$12$GSxQRlm0/PZTTAeIq0ZHguDkzQgWO/9RYTLKU6wYFheO74HqWJi4e','Msgr. Tommie Daugherty','dion@abshire.test',NULL,0,0,0,1,0,'gavin',NULL,NULL,0,1,NULL,'2025-11-15 22:49:14.753713','2025-11-15 22:49:14.753713',1,5,NULL),(31,'stacy','$2a$12$C4G.3cw6bgJEoTBAhdCWvOMmO3W7cw7r05P.ZbbODMQe5ewo13aA2','Darby Zemlak','brett.feil@thompson-hintz.example',NULL,0,0,0,1,0,'anthony',NULL,NULL,0,1,NULL,'2025-11-15 22:49:14.969435','2025-11-15 22:49:14.969435',1,5,NULL),(32,'renna_mccullough','$2a$12$62D151j/MivNsCWO9A9Rf.C2o/Vtoz1cUoaNXlwNJ3EcVddKGnFx2','Cherri Casper','jamey.leffler@gleichner.example',NULL,0,0,0,1,0,'anthony-schuppe',NULL,NULL,0,1,NULL,'2025-11-15 22:49:15.166643','2025-11-15 22:49:15.166643',1,5,NULL),(33,'adaline_runte','$2a$12$otC42sAsD1BlQnLMqLuO7ep0kr2MELuZha2K.dlwrYnUWdLKx7So2','Clinton Wiegand','kiana_gleichner@stoltenberg.example',NULL,0,0,0,1,0,'julieann',NULL,NULL,0,1,NULL,'2025-11-15 22:49:15.375901','2025-11-15 22:49:15.375901',1,5,NULL),(34,'david','$2a$12$6wlBCKrtP6R9zOgVxyB71etafith2YOO0mUTNkmEw4hDSdJZgkG62','Wyatt Mueller II','shirl@mayer.test',NULL,0,0,0,1,0,'milton-sanford',NULL,NULL,0,1,NULL,'2025-11-15 22:49:15.583437','2025-11-15 22:49:15.583437',1,5,NULL),(35,'donald','$2a$12$JZv328TZMDJmn4zgzrEzdOFSC965H5xWBcpqirFIdvDK8F4l/RNCy','Nita Macejkovic','nathanial.hessel@anderson.test',NULL,0,0,0,1,0,'colin-stracke',NULL,NULL,0,1,NULL,'2025-11-15 22:49:15.795134','2025-11-15 22:49:15.795134',1,5,NULL),(36,'thad_schaden','$2a$12$j6G1ulQ8hbCziSvLkrGJ5.9B7pW7zVztromG5GPWkj3I4HOwlxvdy','Dr. Eldon Buckridge','nestor@turner-tremblay.example',NULL,0,0,0,1,0,'rigoberto-oberbrunner',NULL,NULL,0,1,NULL,'2025-11-15 22:49:15.995918','2025-11-15 22:49:15.995918',1,5,NULL),(37,'casey','$2a$12$wQDmK3GbvmzPLBdQXR3AqOHhPSrMpHwEMw3Nwl4rORMNdfbjn/XhS','Marketta Kirlin','bettie_smitham@stokes-baumbach.example',NULL,0,0,0,1,0,'vern-blick',NULL,NULL,0,1,NULL,'2025-11-15 22:49:16.198008','2025-11-15 22:49:16.198008',1,5,NULL),(38,'quinn','$2a$12$jsATc51esaAoyUCojko60O9QKWiv7ShhR2cxO6zU1b/FGpnNMZOlm','Muriel Koss','charley_bailey@rath.test',NULL,0,0,0,1,0,'janyce',NULL,NULL,0,1,NULL,'2025-11-15 22:49:16.406961','2025-11-15 22:49:16.406961',1,5,NULL),(39,'mackenzie','$2a$12$At9c0GCZyzF5eJ2ICHnWP.Y5wVNoIx4C7HroSDxeGX7m5cvaspLom','Tamatha Gorczany','shea_hyatt@ward.test',NULL,0,0,0,1,0,'kerrie-hane',NULL,NULL,0,1,NULL,'2025-11-15 22:49:16.616785','2025-11-15 22:49:16.616785',1,5,NULL),(40,'shirley','$2a$12$t/l9CAjNlTcxIA96/sQIjeneMNTEaXjvivkx9Puyg87nAI74sv8dq','Selma Dooley','clarine@medhurst-ritchie.example',NULL,0,0,0,1,0,'jerald',NULL,NULL,0,1,NULL,'2025-11-15 22:49:16.821363','2025-11-15 22:49:16.821363',1,5,NULL),(41,'terisa','$2a$12$5zaSPlcP3554kNNzwZQoKezBq0ydUKUfthjZMfTLOmPxVSbG9q6qS','Season Abshire','burton.farrell@quitzon.test',NULL,0,0,0,1,0,'kylie-beier',NULL,NULL,0,1,NULL,'2025-11-15 22:49:17.025014','2025-11-15 22:49:17.025014',1,5,NULL),(42,'suzan','$2a$12$5BOJwKQL0zjo9QGZXXQwse.qzgz4l0vNhaWC8bmRw7GYvdXrZO5pO','Rhett Collier','palmer.gibson@stiedemann.example',NULL,0,0,0,1,0,'willian-hirthe',NULL,NULL,0,1,NULL,'2025-11-15 22:49:17.230537','2025-11-15 22:49:17.230537',1,5,NULL),(43,'tony_schuppe','$2a$12$kIJNqs7mT8IO.jlhMjB6HOOOxlRh89luvUjSHRExHSANerEBd5TvS','Evalyn Runte','claudette.homenick@renner.example',NULL,0,0,0,1,0,'vincent',NULL,NULL,0,1,NULL,'2025-11-15 22:49:17.432953','2025-11-15 22:49:17.432953',1,5,NULL),(44,'terrell','$2a$12$YzXdI5kEFDJ1mOOJonN9ze9UjRvOXOnd6CEfNNp2upsPf2KXR8Bhu','Brock O\'Keefe III','marilee@hilpert.test',NULL,0,0,0,1,0,'kayla',NULL,NULL,0,1,NULL,'2025-11-15 22:49:17.641109','2025-11-15 22:49:17.641109',1,5,NULL),(45,'dwana_macgyver','$2a$12$9FyYFTTEsukbX0iW/HFEEe9P5eVJHJmc6sew6m.O6VvGcK6CdYov.','Hyon Thiel','morris.herman@block-stehr.test',NULL,0,0,0,1,0,'hung',NULL,NULL,0,1,NULL,'2025-11-15 22:49:17.835641','2025-11-15 22:49:17.835641',1,5,NULL),(46,'mckinley','$2a$12$240kAcLb/X5/uSFgf6Lol.LsB/dmc8eGoaOOwQoEIZ1htxovP0PAe','Ms. Aurelio Deckow','cassaundra@gislason.test',NULL,0,0,0,1,0,'ermelinda-mills',NULL,NULL,0,1,NULL,'2025-11-15 22:49:18.030227','2025-11-15 22:49:18.030227',1,5,NULL),(47,'keeley','$2a$12$UPRIALtJ.wUIKqVyOOxWw.OO9.OjmvyoVAHeeDG8mNrW/h9chy16.','Liane Kohler Esq.','marybeth_stanton@sawayn.test',NULL,0,0,0,1,0,'whitney',NULL,NULL,0,1,NULL,'2025-11-15 22:49:18.243797','2025-11-15 22:49:18.243797',1,5,NULL),(48,'milford_brekke','$2a$12$Nf1BBymrusDIAEJD4WAtaue0obrcYRM.3n6SOb4Jisr0YEIbVdeLu','Vern Stehr','twyla.prohaska@leuschke-stehr.test',NULL,0,0,0,1,0,'sergio',NULL,NULL,0,1,NULL,'2025-11-15 22:49:18.464760','2025-11-15 22:49:18.464760',1,5,NULL),(49,'lupe_west','$2a$12$S.eRInToSH7Bee9YZkIkueR108wIk6u9WnbNzBbCtOMaqN7Tknmhq','Alfred White','marcelino@cruickshank.test',NULL,0,0,0,1,0,'stanford',NULL,NULL,0,1,NULL,'2025-11-15 22:49:18.674721','2025-11-15 22:49:18.674721',1,5,NULL),(50,'audria','$2a$12$CaymNcMEvNRlIrh5i3fXkOoUa/A2k8IJEpfya2G4BavKYBQERXRHq','Rosario Raynor','lashunda@adams-runolfsson.test',NULL,0,0,0,1,0,'margene',NULL,NULL,0,1,NULL,'2025-11-15 22:49:18.878725','2025-11-15 22:49:18.878725',1,5,NULL),(51,'mallie','$2a$12$Yhd7UMfiQByNd8befS3lL.WvIzbKJUJf31i3SKDpGF.vSp2mhvXGC','Dean Carroll','tamatha.macejkovic@williamson-schuster.test',NULL,0,0,0,1,0,'wanda-oreilly',NULL,NULL,0,1,NULL,'2025-11-15 22:49:19.089954','2025-11-15 22:49:19.089954',1,5,NULL),(52,'sharda','$2a$12$QREH39TpuZ.vX11HlskHXOA02wyWKLuLGNzV1MiwRFXjHfbaAHd2G','Hang Cormier DC','elvera@brown-wilderman.test',NULL,0,0,0,1,0,'james',NULL,NULL,0,1,NULL,'2025-11-15 22:49:19.293444','2025-11-15 22:49:19.293444',1,5,NULL),(53,'francisco_mills','$2a$12$NmvdYgWqHxVc/KiU1b260eRDT4HJc9MJ4vx5AQRA/BsCqbPMOi9Nq','Kai Jerde DC','karl.ziemann@mitchell.example',NULL,0,0,0,1,0,'gregory',NULL,NULL,0,1,NULL,'2025-11-15 22:49:19.491197','2025-11-15 22:49:19.491197',1,5,NULL),(54,'jeromy_schaden','$2a$12$J/Vr1ei/AQZdXRVL68ED.uqZqM7yHeIQIVtDkzg9S3Yxpob1fycJu','Wilson Lehner','sam@collier.test',NULL,0,0,0,1,0,'johnie-zemlak',NULL,NULL,0,1,NULL,'2025-11-15 22:49:19.694554','2025-11-15 22:49:19.694554',1,5,NULL),(55,'kizzy','$2a$12$acIqqMmYyUPnMJd0OU6Ck.ItrwH5Q9I2ZkCMhASxZ8.neA.kD.L/S','Malcolm Doyle','douglass.kshlerin@reilly-breitenberg.test',NULL,0,0,0,1,0,'erline',NULL,NULL,0,1,NULL,'2025-11-15 22:49:19.895422','2025-11-15 22:49:19.895422',1,5,NULL); +/*!40000 ALTER TABLE `users` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-11-15 23:30:17 diff --git a/db/add_signup_test_data.rb b/db/add_signup_test_data.rb new file mode 100644 index 000000000..7f1d50c5e --- /dev/null +++ b/db/add_signup_test_data.rb @@ -0,0 +1,258 @@ +# Script to add test data for signup sheet with advertisements + +puts "Starting to add test data for signup sheet..." + +# Find or create an assignment +assignment = Assignment.first +unless assignment + puts "Creating test assignment..." + assignment = Assignment.create!( + name: "Test Assignment - Final Project", + directory_path: "/test", + submitter_count: 0, + course_id: 1, + instructor_id: 2, + private: false, + num_reviews: 3, + num_review_of_reviews: 1, + num_review_of_reviewers: 1, + reviews_visible_to_all: true, + num_reviewers: 3, + spec_link: "https://example.com/spec", + max_team_size: 4, + staggered_deadline: false, + allow_suggestions: false, + days_between_submissions: 7, + review_assignment_strategy: "Auto-Selected", + max_reviews_per_submission: 3, + review_topic_threshold: 0, + copy_flag: false, + rounds_of_reviews: 1, + microtask: false, + require_quiz: false, + num_quiz_questions: 0, + is_calibrated: false, + availability_flag: true, + use_bookmark: true, + can_review_same_topic: true, + can_choose_topic_to_review: true, + is_intelligent: false, + calculate_penalty: false, + late_policy_id: nil, + is_penalty_calculated: false, + max_choosing_teams: 1, + is_anonymous: true, + num_reviews_required: 3, + num_metareviews_required: 1, + num_reviews_allowed: 3, + num_metareviews_allowed: 3, + simicheck: -1, + simicheck_threshold: 100 + ) + puts "Created assignment: #{assignment.name} (ID: #{assignment.id})" +end + +puts "Using assignment: #{assignment.name} (ID: #{assignment.id})" + +# Create sign up topics if they don't exist +topics_data = [ + { name: "Ruby on Rails Best Practices", max_choosers: 3, description: "Study and present best practices in Rails development" }, + { name: "React Frontend Development", max_choosers: 2, description: "Build a modern React application" }, + { name: "API Design and Documentation", max_choosers: 3, description: "Design and document RESTful APIs" }, + { name: "Database Optimization", max_choosers: 2, description: "Performance tuning for MySQL databases" } +] + +topics = [] +topics_data.each_with_index do |topic_data, index| + topic = SignUpTopic.find_or_create_by!( + topic_name: topic_data[:name], + assignment_id: assignment.id + ) do |t| + t.topic_identifier = (index + 1).to_s + t.max_choosers = topic_data[:max_choosers] + t.category = "Project Topics" + t.description = topic_data[:description] + end + topics << topic + puts "Created/found topic: #{topic.topic_name} (ID: #{topic.id})" +end + +# Find the student users +user1 = User.find_by(name: "quinn_johns") +user2 = User.find_by(name: "gaston_blick") || User.find_by(role_id: 3).second +user3 = User.where(role_id: 3).where.not(id: [user1&.id, user2&.id]).first + +unless user1 + puts "ERROR: User quinn_johns not found!" + exit 1 +end + +puts "Found users: #{user1.name}, #{user2&.name || 'N/A'}, #{user3&.name || 'N/A'}" + +# Create assignment participants if they don't exist +participant1 = AssignmentParticipant.find_or_create_by!( + user_id: user1.id, + parent_id: assignment.id +) do |p| + p.can_submit = true + p.can_review = true + p.can_take_quiz = true + p.handle = user1.handle || user1.name +end +puts "Created/found participant for #{user1.name} (ID: #{participant1.id})" + +participant2 = nil +if user2 + participant2 = AssignmentParticipant.find_or_create_by!( + user_id: user2.id, + parent_id: assignment.id + ) do |p| + p.can_submit = true + p.can_review = true + p.can_take_quiz = true + p.handle = user2.handle || user2.name + end + puts "Created/found participant for #{user2.name} (ID: #{participant2.id})" +end + +participant3 = nil +if user3 + participant3 = AssignmentParticipant.find_or_create_by!( + user_id: user3.id, + parent_id: assignment.id + ) do |p| + p.can_submit = true + p.can_review = true + p.can_take_quiz = true + p.handle = user3.handle || user3.name + end + puts "Created/found participant for #{user3.name} (ID: #{participant3.id})" +end + +# Create teams +team1 = AssignmentTeam.find_or_create_by!( + name: "Team Alpha", + parent_id: assignment.id +) do |t| + t.type = "AssignmentTeam" +end +puts "Created/found team: #{team1.name} (ID: #{team1.id})" + +team2 = nil +if user2 + team2 = AssignmentTeam.find_or_create_by!( + name: "Team Beta", + parent_id: assignment.id + ) do |t| + t.type = "AssignmentTeam" + end + puts "Created/found team: #{team2.name} (ID: #{team2.id})" +end + +team3 = nil +if user3 + team3 = AssignmentTeam.find_or_create_by!( + name: "Team Gamma", + parent_id: assignment.id + ) do |t| + t.type = "AssignmentTeam" + end + puts "Created/found team: #{team3.name} (ID: #{team3.id})" +end + +# Add users to teams +TeamsUser.find_or_create_by!(team_id: team1.id, user_id: user1.id) +puts "Added #{user1.name} to #{team1.name}" + +if user2 && team2 + TeamsUser.find_or_create_by!(team_id: team2.id, user_id: user2.id) + puts "Added #{user2.name} to #{team2.name}" +end + +if user3 && team3 + TeamsUser.find_or_create_by!(team_id: team3.id, user_id: user3.id) + puts "Added #{user3.name} to #{team3.name}" +end + +# Sign up teams for topics with advertisements +# Team 1 signs up for Topic 1 and advertises for partners +signed_up_team1 = SignedUpTeam.find_or_create_by!( + team_id: team1.id, + sign_up_topic_id: topics[0].id +) do |sut| + sut.is_waitlisted = false + sut.preference_priority_number = 1 + sut.advertise_for_partner = true + sut.comments_for_advertisement = "Looking for experienced Ruby developers! We have strong frontend skills and need backend expertise. Great team dynamic!" +end +puts "Team #{team1.name} signed up for topic '#{topics[0].topic_name}' WITH advertisement" + +# Team 2 signs up for Topic 2 and advertises +if team2 + signed_up_team2 = SignedUpTeam.find_or_create_by!( + team_id: team2.id, + sign_up_topic_id: topics[1].id + ) do |sut| + sut.is_waitlisted = false + sut.preference_priority_number = 1 + sut.advertise_for_partner = true + sut.comments_for_advertisement = "Seeking creative frontend developer for React project. We have backend covered and need someone passionate about UX/UI design!" + end + puts "Team #{team2.name} signed up for topic '#{topics[1].topic_name}' WITH advertisement" +end + +# Team 3 signs up for Topic 3 WITHOUT advertising +if team3 + signed_up_team3 = SignedUpTeam.find_or_create_by!( + team_id: team3.id, + sign_up_topic_id: topics[2].id + ) do |sut| + sut.is_waitlisted = false + sut.preference_priority_number = 1 + sut.advertise_for_partner = false + end + puts "Team #{team3.name} signed up for topic '#{topics[2].topic_name}' WITHOUT advertisement" +end + +# Add one more team on waitlist for Topic 1 WITH advertisement +if user2 && team2 + # Create another team for demonstration + team4 = AssignmentTeam.find_or_create_by!( + name: "Team Delta", + parent_id: assignment.id + ) do |t| + t.type = "AssignmentTeam" + end + + # Sign up on waitlist with advertisement + SignedUpTeam.find_or_create_by!( + team_id: team4.id, + sign_up_topic_id: topics[0].id + ) do |sut| + sut.is_waitlisted = true + sut.preference_priority_number = 2 + sut.advertise_for_partner = true + sut.comments_for_advertisement = "Waitlisted but ready to go! Full-stack team looking to collaborate. We're organized and committed!" + end + puts "Team #{team4.name} signed up for topic '#{topics[0].topic_name}' on WAITLIST WITH advertisement" +end + +puts "\n" + "="*80 +puts "TEST DATA SUMMARY" +puts "="*80 +puts "Assignment ID: #{assignment.id}" +puts "Assignment Name: #{assignment.name}" +puts "\nTopics created:" +topics.each do |topic| + signed_teams = SignedUpTeam.where(sign_up_topic_id: topic.id) + advertising_teams = signed_teams.where(advertise_for_partner: true) + puts " - #{topic.topic_name} (ID: #{topic.id})" + puts " Max choosers: #{topic.max_choosers}" + puts " Signed up teams: #{signed_teams.count}" + puts " Advertising teams: #{advertising_teams.count}" +end + +puts "\nTo test the signup sheet, navigate to:" +puts " http://localhost:3000/assignments/#{assignment.id}/signup_sheet" +puts "\nLogin as: #{user1.name}" +puts "="*80 diff --git a/db/migrate/20250621151644_add_round_to_responses.rb b/db/migrate/20250621151644_add_round_to_responses.rb index 79d5354fe..48a08e0f4 100644 --- a/db/migrate/20250621151644_add_round_to_responses.rb +++ b/db/migrate/20250621151644_add_round_to_responses.rb @@ -1,5 +1,5 @@ -class AddRoundToResponses < ActiveRecord::Migration[8.0] - def change - add_column :responses, :round, :integer - end -end +class AddRoundToResponses < ActiveRecord::Migration[8.0] + def change + add_column :responses, :round, :integer + end +end diff --git a/db/migrate/20250621152946_add_version_number_to_responses.rb b/db/migrate/20250621152946_add_version_number_to_responses.rb index a187f3c11..a11349338 100644 --- a/db/migrate/20250621152946_add_version_number_to_responses.rb +++ b/db/migrate/20250621152946_add_version_number_to_responses.rb @@ -1,5 +1,5 @@ -class AddVersionNumberToResponses < ActiveRecord::Migration[8.0] - def change - add_column :responses, :version_num, :integer - end -end +class AddVersionNumberToResponses < ActiveRecord::Migration[8.0] + def change + add_column :responses, :version_num, :integer + end +end diff --git a/db/migrate/20250621180527_change_question_to_item_in_answers.rb b/db/migrate/20250621180527_change_question_to_item_in_answers.rb index fdb3a5d3a..a6bed0d2c 100644 --- a/db/migrate/20250621180527_change_question_to_item_in_answers.rb +++ b/db/migrate/20250621180527_change_question_to_item_in_answers.rb @@ -1,5 +1,5 @@ -class ChangeQuestionToItemInAnswers < ActiveRecord::Migration[8.0] - def change - rename_column :answers, :question_id, :item_id - end -end +class ChangeQuestionToItemInAnswers < ActiveRecord::Migration[8.0] + def change + rename_column :answers, :question_id, :item_id + end +end diff --git a/db/migrate/20250621180851_rename_item_foreign_key_index_in_answers.rb b/db/migrate/20250621180851_rename_item_foreign_key_index_in_answers.rb index b9bdfabf9..6dd9e2bb7 100644 --- a/db/migrate/20250621180851_rename_item_foreign_key_index_in_answers.rb +++ b/db/migrate/20250621180851_rename_item_foreign_key_index_in_answers.rb @@ -1,6 +1,6 @@ -class RenameItemForeignKeyIndexInAnswers < ActiveRecord::Migration[8.0] - def change - remove_index :answers, name: "fk_score_questions" - add_index :answers, :item_id, name: "fk_score_items" - end -end +class RenameItemForeignKeyIndexInAnswers < ActiveRecord::Migration[8.0] + def change + remove_index :answers, name: "fk_score_questions" + add_index :answers, :item_id, name: "fk_score_items" + end +end diff --git a/db/migrate/20250626161114_add_name_to_teams.rb b/db/migrate/20250626161114_add_name_to_teams.rb deleted file mode 100644 index d18f94bcd..000000000 --- a/db/migrate/20250626161114_add_name_to_teams.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddNameToTeams < ActiveRecord::Migration[8.0] - def change - add_column :teams, :name, :string - end -end diff --git a/db/migrate/20250629185100_add_grade_to_participant.rb b/db/migrate/20250629185100_add_grade_to_participant.rb index 4ef9bdc96..8816e9ffe 100644 --- a/db/migrate/20250629185100_add_grade_to_participant.rb +++ b/db/migrate/20250629185100_add_grade_to_participant.rb @@ -1,5 +1,5 @@ -class AddGradeToParticipant < ActiveRecord::Migration[8.0] - def change - add_column :participants, :grade, :float - end -end +class AddGradeToParticipant < ActiveRecord::Migration[8.0] + def change + add_column :participants, :grade, :float + end +end diff --git a/db/migrate/20250629185439_add_grade_for_submission_to_team.rb b/db/migrate/20250629185439_add_grade_for_submission_to_team.rb index 83919e338..6c1cc62b3 100644 --- a/db/migrate/20250629185439_add_grade_for_submission_to_team.rb +++ b/db/migrate/20250629185439_add_grade_for_submission_to_team.rb @@ -1,5 +1,5 @@ -class AddGradeForSubmissionToTeam < ActiveRecord::Migration[8.0] - def change - add_column :teams, :grade_for_submission, :integer - end -end +class AddGradeForSubmissionToTeam < ActiveRecord::Migration[8.0] + def change + add_column :teams, :grade_for_submission, :integer + end +end diff --git a/db/migrate/20250629190818_add_comment_for_submission_to_team.rb b/db/migrate/20250629190818_add_comment_for_submission_to_team.rb index b2c6c5eb4..ac7e5dd44 100644 --- a/db/migrate/20250629190818_add_comment_for_submission_to_team.rb +++ b/db/migrate/20250629190818_add_comment_for_submission_to_team.rb @@ -1,5 +1,5 @@ -class AddCommentForSubmissionToTeam < ActiveRecord::Migration[8.0] - def change - add_column :teams, :comment_for_submission, :string - end -end +class AddCommentForSubmissionToTeam < ActiveRecord::Migration[8.0] + def change + add_column :teams, :comment_for_submission, :string + end +end diff --git a/db/migrate/20250727170825_add_questionnaire_weight_to_assignment_questionnaire.rb b/db/migrate/20250727170825_add_questionnaire_weight_to_assignment_questionnaire.rb index e5a6b7ca0..8b0bd9acb 100644 --- a/db/migrate/20250727170825_add_questionnaire_weight_to_assignment_questionnaire.rb +++ b/db/migrate/20250727170825_add_questionnaire_weight_to_assignment_questionnaire.rb @@ -1,5 +1,5 @@ -class AddQuestionnaireWeightToAssignmentQuestionnaire < ActiveRecord::Migration[8.0] - def change - add_column :assignment_questionnaires, :questionnaire_weight, :integer - end -end +class AddQuestionnaireWeightToAssignmentQuestionnaire < ActiveRecord::Migration[8.0] + def change + add_column :assignment_questionnaires, :questionnaire_weight, :integer + end +end diff --git a/db/migrate/20250805174104_rename_status_to_reply_status_in_in_join_team_requests.rb b/db/migrate/20250805174104_rename_status_to_reply_status_in_in_join_team_requests.rb new file mode 100644 index 000000000..fecb0ae22 --- /dev/null +++ b/db/migrate/20250805174104_rename_status_to_reply_status_in_in_join_team_requests.rb @@ -0,0 +1,5 @@ +class RenameStatusToReplyStatusInInJoinTeamRequests < ActiveRecord::Migration[8.0] + def change + rename_column :join_team_requests, :status, :reply_status + end +end diff --git a/db/migrate/20251021165336_add_participant_ref_to_invitations.rb b/db/migrate/20251021165336_add_participant_ref_to_invitations.rb new file mode 100644 index 000000000..470d22b40 --- /dev/null +++ b/db/migrate/20251021165336_add_participant_ref_to_invitations.rb @@ -0,0 +1,5 @@ +class AddParticipantRefToInvitations < ActiveRecord::Migration[8.0] + def change + add_reference :invitations, :participant, null: false, foreign_key: true + end +end \ No newline at end of file diff --git a/db/migrate/20251022160053_change_invitation_from_id_foreign_key.rb b/db/migrate/20251022160053_change_invitation_from_id_foreign_key.rb new file mode 100644 index 000000000..9f5ab7f09 --- /dev/null +++ b/db/migrate/20251022160053_change_invitation_from_id_foreign_key.rb @@ -0,0 +1,9 @@ +class ChangeInvitationFromIdForeignKey < ActiveRecord::Migration[7.0] + def change + # Remove old foreign key to participants + remove_foreign_key :invitations, column: :from_id + + # Add new foreign key to teams + add_foreign_key :invitations, :teams, column: :from_id + end +end \ No newline at end of file diff --git a/db/migrate/20251029071649_add_advertisement_fields_to_signed_up_teams.rb b/db/migrate/20251029071649_add_advertisement_fields_to_signed_up_teams.rb new file mode 100644 index 000000000..8cee078dc --- /dev/null +++ b/db/migrate/20251029071649_add_advertisement_fields_to_signed_up_teams.rb @@ -0,0 +1,6 @@ +class AddAdvertisementFieldsToSignedUpTeams < ActiveRecord::Migration[8.0] + def change + add_column :signed_up_teams, :comments_for_advertisement, :text + add_column :signed_up_teams, :advertise_for_partner, :boolean + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 8c11894f0..db4f8d374 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,128 +1,128 @@ -# frozen_string_literal: true - -begin - # Create an instritution - inst_id = Institution.create!( - name: 'North Carolina State University' - ).id - - Role.create!(id: 1, name: 'Super Administrator') - Role.create!(id: 2, name: 'Administrator') - Role.create!(id: 3, name: 'Instructor') - Role.create!(id: 4, name: 'Teaching Assistant') - Role.create!(id: 5, name: 'Student') - - # Create an admin user - User.create!( - name: 'admin', - email: 'admin2@example.com', - password: 'password123', - full_name: 'admin admin', - institution_id: 1, - role_id: 1 - ) - - # Generate Random Users - num_students = 48 - num_assignments = 8 - num_teams = 16 - num_courses = 2 - num_instructors = 2 - - puts "creating instructors" - instructor_user_ids = [] - num_instructors.times do - instructor_user_ids << User.create( - name: Faker::Internet.unique.username, - email: Faker::Internet.unique.email, - password: "password", - full_name: Faker::Name.name, - institution_id: 1, - role_id: 3 - ).id - end - - puts "creating courses" - course_ids = [] - num_courses.times do |i| - course_ids << Course.create( - instructor_id: instructor_user_ids[i], - institution_id: inst_id, - directory_path: Faker::File.dir(segment_count: 2), - name: Faker::Company.industry, - info: "A fake class", - private: false - ).id - end - - puts "creating assignments" - assignment_ids = [] - num_assignments.times do |i| - assignment_ids << Assignment.create( - name: Faker::Verb.base, - instructor_id: instructor_user_ids[i % num_instructors], - course_id: course_ids[i % num_courses], - has_teams: true, - private: false - ).id - end - - puts "creating teams" - team_ids = [] - num_teams.times do |i| - team_ids << AssignmentTeam.create( - name: "Team #{i + 1}", - parent_id: assignment_ids[i % num_assignments] - ).id - end - - puts "creating students" - student_user_ids = [] - num_students.times do - student_user_ids << User.create( - name: Faker::Internet.unique.username, - email: Faker::Internet.unique.email, - password: "password", - full_name: Faker::Name.name, - institution_id: 1, - role_id: 5, - parent_id: [nil, *instructor_user_ids].sample - ).id - end - - puts "assigning students to teams" - teams_users_ids = [] - # num_students.times do |i| - # teams_users_ids << TeamsUser.create( - # team_id: team_ids[i%num_teams], - # user_id: student_user_ids[i] - # ).id - # end - - num_students.times do |i| - puts "Creating TeamsUser with team_id: #{team_ids[i % num_teams]}, user_id: #{student_user_ids[i]}" - teams_user = TeamsUser.create( - team_id: team_ids[i % num_teams], - user_id: student_user_ids[i] - ) - if teams_user.persisted? - teams_users_ids << teams_user.id - puts "Created TeamsUser with ID: #{teams_user.id}" - else - puts "Failed to create TeamsUser: #{teams_user.errors.full_messages.join(', ')}" - end - end - - puts "assigning participant to students, teams, courses, and assignments" - participant_ids = [] - num_students.times do |i| - participant_ids << AssignmentParticipant.create( - user_id: student_user_ids[i], - parent_id: assignment_ids[i%num_assignments], - team_id: team_ids[i%num_teams], - ).id - end - -rescue ActiveRecord::RecordInvalid => e - puts e, 'The db has already been seeded' -end +# frozen_string_literal: true + +begin + # Create an instritution + inst_id = Institution.create!( + name: 'North Carolina State University' + ).id + + Role.create!(id: 1, name: 'Super Administrator') + Role.create!(id: 2, name: 'Administrator') + Role.create!(id: 3, name: 'Instructor') + Role.create!(id: 4, name: 'Teaching Assistant') + Role.create!(id: 5, name: 'Student') + + # Create an admin user + User.create!( + name: 'admin', + email: 'admin2@example.com', + password: 'password123', + full_name: 'admin admin', + institution_id: 1, + role_id: 1 + ) + + # Generate Random Users + num_students = 48 + num_assignments = 8 + num_teams = 16 + num_courses = 2 + num_instructors = 2 + + puts "creating instructors" + instructor_user_ids = [] + num_instructors.times do + instructor_user_ids << User.create( + name: Faker::Internet.unique.username, + email: Faker::Internet.unique.email, + password: "password", + full_name: Faker::Name.name, + institution_id: 1, + role_id: 3 + ).id + end + + puts "creating courses" + course_ids = [] + num_courses.times do |i| + course_ids << Course.create( + instructor_id: instructor_user_ids[i], + institution_id: inst_id, + directory_path: Faker::File.dir(segment_count: 2), + name: Faker::Company.industry, + info: "A fake class", + private: false + ).id + end + + puts "creating assignments" + assignment_ids = [] + num_assignments.times do |i| + assignment_ids << Assignment.create( + name: Faker::Verb.base, + instructor_id: instructor_user_ids[i % num_instructors], + course_id: course_ids[i % num_courses], + has_teams: true, + private: false + ).id + end + + puts "creating teams" + team_ids = [] + num_teams.times do |i| + team_ids << AssignmentTeam.create( + name: "Team #{i + 1}", + parent_id: assignment_ids[i % num_assignments] + ).id + end + + puts "creating students" + student_user_ids = [] + num_students.times do + student_user_ids << User.create( + name: Faker::Internet.unique.username, + email: Faker::Internet.unique.email, + password: "password", + full_name: Faker::Name.name, + institution_id: 1, + role_id: 5, + parent_id: [nil, *instructor_user_ids].sample + ).id + end + + puts "assigning students to teams" + teams_users_ids = [] + # num_students.times do |i| + # teams_users_ids << TeamsUser.create( + # team_id: team_ids[i%num_teams], + # user_id: student_user_ids[i] + # ).id + # end + + num_students.times do |i| + puts "Creating TeamsUser with team_id: #{team_ids[i % num_teams]}, user_id: #{student_user_ids[i]}" + teams_user = TeamsUser.create( + team_id: team_ids[i % num_teams], + user_id: student_user_ids[i] + ) + if teams_user.persisted? + teams_users_ids << teams_user.id + puts "Created TeamsUser with ID: #{teams_user.id}" + else + puts "Failed to create TeamsUser: #{teams_user.errors.full_messages.join(', ')}" + end + end + + puts "assigning participant to students, teams, courses, and assignments" + participant_ids = [] + num_students.times do |i| + participant_ids << AssignmentParticipant.create( + user_id: student_user_ids[i], + parent_id: assignment_ids[i%num_assignments], + team_id: team_ids[i%num_teams], + ).id + end + +rescue ActiveRecord::RecordInvalid => e + puts e, 'The db has already been seeded' +end diff --git a/db/seeds2.rb b/db/seeds2.rb index 908901b92..8ecf5ba20 100644 --- a/db/seeds2.rb +++ b/db/seeds2.rb @@ -1,53 +1,53 @@ -# begin -# questionnaire_count = 2 -# items_per_questionnaire = 10 -# questionnaire_ids = [] -# questionnaire_count.times do -# questionnaire_ids << Questionnaire.create!( -# name: "#{Faker::Lorem.words(number: 5).join(' ').titleize}", -# instructor_id: rand(1..5), # assuming some instructor IDs exist in range 1–5 -# private: false, -# min_question_score: 0, -# max_question_score: 5, -# questionnaire_type: "ReviewQuestionnaire", -# display_type: "Review", -# created_at: Time.now, -# updated_at: Time.now -# ).id - -# end -# puts questionnaire_ids - -# questionnaires = Questionnaire.all - -# questionnaires.each do |questionnaire| -# items_per_questionnaire.times do |i| -# Item.create!( -# txt: Faker::Lorem.sentence(word_count: 8), -# weight: rand(1..5), -# seq: i + 1, -# question_type: ['Criterion', 'Scale', 'TextArea', 'Dropdown'].sample, -# size: ['50x3', '60x4', '40x2'].sample, -# alternatives: ['Yes|No', 'Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree'], -# break_before: true, -# max_label: Faker::Lorem.word.capitalize, -# min_label: Faker::Lorem.word.capitalize, -# questionnaire_id: questionnaire.id, -# created_at: Time.now, -# updated_at: Time.now -# ) -# end -# end - -# end - -begin - count = 4 - count.times do |i| - AssignmentQuestionnaire.create!( - assignment_id: i+1, - questionnaire_id: i+1, - used_in_round: [1,2].sample - ) - end -end +# begin +# questionnaire_count = 2 +# items_per_questionnaire = 10 +# questionnaire_ids = [] +# questionnaire_count.times do +# questionnaire_ids << Questionnaire.create!( +# name: "#{Faker::Lorem.words(number: 5).join(' ').titleize}", +# instructor_id: rand(1..5), # assuming some instructor IDs exist in range 1–5 +# private: false, +# min_question_score: 0, +# max_question_score: 5, +# questionnaire_type: "ReviewQuestionnaire", +# display_type: "Review", +# created_at: Time.now, +# updated_at: Time.now +# ).id + +# end +# puts questionnaire_ids + +# questionnaires = Questionnaire.all + +# questionnaires.each do |questionnaire| +# items_per_questionnaire.times do |i| +# Item.create!( +# txt: Faker::Lorem.sentence(word_count: 8), +# weight: rand(1..5), +# seq: i + 1, +# question_type: ['Criterion', 'Scale', 'TextArea', 'Dropdown'].sample, +# size: ['50x3', '60x4', '40x2'].sample, +# alternatives: ['Yes|No', 'Strongly Agree|Agree|Neutral|Disagree|Strongly Disagree'], +# break_before: true, +# max_label: Faker::Lorem.word.capitalize, +# min_label: Faker::Lorem.word.capitalize, +# questionnaire_id: questionnaire.id, +# created_at: Time.now, +# updated_at: Time.now +# ) +# end +# end + +# end + +begin + count = 4 + count.times do |i| + AssignmentQuestionnaire.create!( + assignment_id: i+1, + questionnaire_id: i+1, + used_in_round: [1,2].sample + ) + end +end diff --git a/dump.sql b/dump.sql new file mode 100644 index 000000000..60ffec93f --- /dev/null +++ b/dump.sql @@ -0,0 +1,19 @@ +-- Common Users Seed Data for Development Environment +-- This ensures all team members have the same test users +-- Usage: docker exec -i reimplementation-back-end-db-1 mysql -u root -pexpertiza reimplementation_development < users_seed_data.sql + +-- Insert common user data (only if they don't already exist) +INSERT IGNORE INTO users (id, full_name, email, password_digest, role_id, created_at, updated_at) VALUES +(1, 'Admin User', 'admin@example.com', '$2a$12$PfN/CKp7R6aQp9bXt6SKIOElvh2DuXkopyN4xjRI3J.F8uAKsI1jW', 1, NOW(), NOW()), +(2, 'Alice Johnson', 'alice@example.com', '$2a$12$PfN/CKp7R6aQp9bXt6SKIOElvh2DuXkopyN4xjRI3J.F8uAKsI1jW', 4, NOW(), NOW()), +(3, 'Bob Smith', 'bob@example.com', '$2a$12$PfN/CKp7R6aQp9bXt6SKIOElvh2DuXkopyN4xjRI3J.F8uAKsI1jW', 4, NOW(), NOW()), +(4, 'Charlie Davis', 'charlie@example.com', '$2a$12$PfN/CKp7R6aQp9bXt6SKIOElvh2DuXkopyN4xjRI3J.F8uAKsI1jW', 4, NOW(), NOW()), +(5, 'Diana Martinez', 'diana@example.com', '$2a$12$PfN/CKp7R6aQp9bXt6SKIOElvh2DuXkopyN4xjRI3J.F8uAKsI1jW', 4, NOW(), NOW()), +(6, 'Ethan Brown', 'ethan@example.com', '$2a$12$PfN/CKp7R6aQp9bXt6SKIOElvh2DuXkopyN4xjRI3J.F8uAKsI1jW', 4, NOW(), NOW()), +(7, 'Fiona Wilson', 'fiona@example.com', '$2a$12$PfN/CKp7R6aQp9bXt6SKIOElvh2DuXkopyN4xjRI3J.F8uAKsI1jW', 4, NOW(), NOW()); + +-- Note: All users have the password "password123" +-- Password hash: $2a$12$PfN/CKp7R6aQp9bXt6SKIOElvh2DuXkopyN4xjRI3J.F8uAKsI1jW + +SELECT 'Users seed data loaded successfully!' AS message; +SELECT id, full_name, email FROM users WHERE id IN (1, 2, 3, 4, 5, 6, 7); \ No newline at end of file diff --git a/generate_usernames.rb b/generate_usernames.rb new file mode 100644 index 000000000..66738aa54 --- /dev/null +++ b/generate_usernames.rb @@ -0,0 +1,7 @@ +require 'faker' + +puts "Three example student usernames (generated by Faker):" +3.times do + username = Faker::Internet.unique.username(separators: ['_']) + puts "- #{username}" +end diff --git a/generate_users_with_emails.rb b/generate_users_with_emails.rb new file mode 100644 index 000000000..4c1ae7101 --- /dev/null +++ b/generate_users_with_emails.rb @@ -0,0 +1,11 @@ +require 'faker' + +puts "Three example students with username and email:" +3.times do + username = Faker::Internet.unique.username(separators: ['_']) + email = Faker::Internet.unique.email + puts "Username: #{username}" + puts "Email: #{email}" + puts "Password: password" + puts "---" +end diff --git a/get_token.sh b/get_token.sh new file mode 100755 index 000000000..b2bcb5c5a --- /dev/null +++ b/get_token.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Script to get a JWT token for testing + +echo "Getting JWT token for quinn_johns..." +echo "" + +# Make login request +response=$(curl -s -X POST http://152.7.176.23:3002/login \ + -H "Content-Type: application/json" \ + -d '{ + "user_name": "quinn_johns", + "password": "password123" + }') + +# Extract token +token=$(echo $response | grep -o '"token":"[^"]*' | cut -d'"' -f4) + +if [ -z "$token" ]; then + echo "❌ Failed to get token. Response:" + echo $response + echo "" + echo "Trying with default password 'password'..." + + response=$(curl -s -X POST http://152.7.176.23:3002/login \ + -H "Content-Type: application/json" \ + -d '{ + "user_name": "quinn_johns", + "password": "password" + }') + + token=$(echo $response | grep -o '"token":"[^"]*' | cut -d'"' -f4) + + if [ -z "$token" ]; then + echo "❌ Still failed. Response:" + echo $response + exit 1 + fi +fi + +echo "✅ Successfully obtained JWT token!" +echo "" +echo "Token: $token" +echo "" +echo "To use this token in your browser:" +echo "1. Open Developer Tools (F12)" +echo "2. Go to Console tab" +echo "3. Run: localStorage.setItem('jwt', '$token')" +echo "4. Refresh the page" +echo "" +echo "Or test the API directly:" +echo "curl -H 'Authorization: Bearer $token' http://152.7.176.23:3002/api/v1/sign_up_topics?assignment_id=1" diff --git a/get_usernames.rb b/get_usernames.rb new file mode 100644 index 000000000..bf0db8643 --- /dev/null +++ b/get_usernames.rb @@ -0,0 +1,75 @@ +require 'mysql2' +require 'dotenv/load' + +# Parse DATABASE_URL from environment +db_url = ENV['DATABASE_URL'] + +if db_url + # Extract database name - assuming format: mysql2://user:pass@host:port/dbname + db_name = db_url.split('/').last.split('?').first + db_name = db_name.sub('expertiza', 'expertiza_development') + + # Extract credentials + uri = URI.parse(db_url.sub('mysql2://', 'http://')) + + begin + client = Mysql2::Client.new( + host: uri.host || 'localhost', + port: uri.port || 3306, + username: uri.user || 'root', + password: uri.password || '', + database: db_name + ) + + # Get Student role ID + role_result = client.query("SELECT id FROM roles WHERE name = 'Student' LIMIT 1") + student_role_id = role_result.first['id'] if role_result.first + + if student_role_id + # Get 3 student usernames + results = client.query("SELECT name FROM users WHERE role_id = #{student_role_id} LIMIT 3") + + puts "Three student usernames:" + results.each do |row| + puts "- #{row['name']}" + end + else + puts "No Student role found in database" + end + + client.close + rescue => e + puts "Error: #{e.message}" + puts "\nTrying with default credentials..." + + # Try default connection + begin + client = Mysql2::Client.new( + host: 'localhost', + username: 'root', + password: '', + database: 'expertiza_development' + ) + + role_result = client.query("SELECT id FROM roles WHERE name = 'Student' LIMIT 1") + student_role_id = role_result.first['id'] if role_result.first + + if student_role_id + results = client.query("SELECT name FROM users WHERE role_id = #{student_role_id} LIMIT 3") + + puts "Three student usernames:" + results.each do |row| + puts "- #{row['name']}" + end + else + puts "No Student role found" + end + + client.close + rescue => e2 + puts "Failed: #{e2.message}" + end + end +else + puts "DATABASE_URL not set" +end diff --git a/spec/controllers/concerns/authorization_spec.rb b/spec/controllers/concerns/authorization_spec.rb index fd199e56a..09bdc9da3 100644 --- a/spec/controllers/concerns/authorization_spec.rb +++ b/spec/controllers/concerns/authorization_spec.rb @@ -17,31 +17,22 @@ end ########################################## - # Tests for has_privileges_of? method + # Tests for current_user_has_privileges_of? method ########################################## - describe '#has_privileges_of?' do + describe '#current_user_has_privileges_of?' do describe 'role validation' do context 'when required_role is a string' do let(:admin_role) { instance_double('Role') } before do - allow(Role).to receive(:find_by_name).with('Administrator').and_return(admin_role) + allow(Role).to receive(:find_by).with(name:'Administrator').and_return(admin_role) end it 'finds the role and checks privileges' do expect(role).to receive(:all_privileges_of?).with(admin_role).and_return(true) - expect(controller.has_privileges_of?('Administrator')).to be true + expect(controller.current_user_has_admin_privileges?).to be true end - end - - context 'when required_role is a Role object' do - let(:instructor_role) { instance_double('Role') } - - it 'directly checks privileges' do - expect(role).to receive(:all_privileges_of?).with(instructor_role).and_return(false) - expect(controller.has_privileges_of?(instructor_role)).to be false - end - end + end end describe 'edge cases' do @@ -51,7 +42,7 @@ end it 'returns false' do - expect(controller.has_privileges_of?('Administrator')).to be false + expect(controller.current_user_has_admin_privileges?).to be false end end @@ -61,16 +52,16 @@ end it 'returns false' do - expect(controller.has_privileges_of?('Administrator')).to be false + expect(controller.current_user_has_admin_privileges?).to be false end end end end ########################################## - # Tests for has_role? method + # Tests for current_user_has_role? method ########################################## - describe '#has_role?' do + describe '#current_user_has_role?' do describe 'role matching' do context 'when role_name is a string' do before do @@ -78,11 +69,11 @@ end it 'returns true when roles match' do - expect(controller.has_role?('Student')).to be true + expect(controller.current_user_has_role?('Student')).to be true end it 'returns false when roles do not match' do - expect(controller.has_role?('Instructor')).to be false + expect(controller.current_user_has_role?('Instructor')).to be false end end @@ -96,7 +87,7 @@ end it 'compares using the role name' do - expect(controller.has_role?(role_object)).to be true + expect(controller.current_user_has_role?(role_object)).to be true end end end @@ -108,7 +99,7 @@ end it 'returns false' do - expect(controller.has_role?('Student')).to be false + expect(controller.current_user_has_role?('Student')).to be false end end @@ -118,7 +109,7 @@ end it 'returns false' do - expect(controller.has_role?('Student')).to be false + expect(controller.current_user_has_role?('Student')).to be false end end end @@ -130,7 +121,7 @@ describe '#all_actions_allowed?' do context 'when the user has the Super Administrator role' do before do - allow(controller).to receive(:has_privileges_of?).with('Super Administrator').and_return(true) + allow(controller).to receive(:current_user_has_privileges_of?).with('Super Administrator').and_return(true) end it 'returns true' do @@ -140,7 +131,7 @@ context 'when the user does not have the Super Administrator role' do before do - allow(controller).to receive(:has_privileges_of?).with('Super Administrator').and_return(false) + allow(controller).to receive(:current_user_has_privileges_of?).with('Super Administrator').and_return(false) allow(controller).to receive(:action_allowed?).and_return(false) end @@ -151,7 +142,7 @@ context 'when action_allowed? returns true' do before do - allow(controller).to receive(:has_privileges_of?).with('Super Administrator').and_return(false) + allow(controller).to receive(:current_user_has_privileges_of?).with('Super Administrator').and_return(false) allow(controller).to receive(:action_allowed?).and_return(true) end diff --git a/spec/mailers/invitation_sent_spec.rb b/spec/mailers/invitation_sent_spec.rb index a95054448..256dfb887 100644 --- a/spec/mailers/invitation_sent_spec.rb +++ b/spec/mailers/invitation_sent_spec.rb @@ -2,6 +2,119 @@ require "rails_helper" -RSpec.describe InvitationSentMailer, type: :mailer do - pending "add some examples to (or delete) #{__FILE__}" +RSpec.describe InvitationMailer, type: :mailer do + include ActiveJob::TestHelper + + let(:role) { Role.create(name: 'Instructor', parent_id: nil, id: 3, default_page_id: nil) } + let(:student_role) { Role.create(name: 'Student', parent_id: nil, id: 5, default_page_id: nil) } + let(:instructor) { Instructor.create(name: 'testinstructor', email: 'instructor@test.com', full_name: 'Test Instructor', password: '123456', role: role) } + let(:user1) { create :user, name: 'invitee_user', role: student_role, email: 'invitee@test.com' } + let(:user2) { create :user, name: 'inviter_user', role: student_role, email: 'inviter@test.com' } + let(:assignment) { create(:assignment, instructor: instructor) } + + before(:each) do + ActiveJob::Base.queue_adapter = :test + end + + after(:each) do + clear_enqueued_jobs + end + + describe '#send_invitation_email' do + it 'sends invitation email to invitee' do + invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + + email = InvitationMailer.with(invitation: invitation).send_invitation_email + + expect(email.to).to eq([user1.email]) + expect(email.subject).to include('invitation') + end + end + + describe '#send_acceptance_email' do + it 'sends acceptance email to invitee' do + invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + + email = InvitationMailer.with(invitation: invitation).send_acceptance_email + + expect(email.to).to eq([user1.email]) + expect(email.subject).to include('accepted') + end + + it 'includes invitee name in email body' do + invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + + email = InvitationMailer.with(invitation: invitation).send_acceptance_email + + expect(email.body.encoded).to include(user1.full_name) + end + + it 'includes team name in email body' do + team = AssignmentTeam.create(name: 'Test Team', parent_id: assignment.id, type: 'AssignmentTeam') + invitation = Invitation.create(to_id: user1.id, from_id: team.id, assignment_id: assignment.id) + + email = InvitationMailer.with(invitation: invitation).send_acceptance_email + + expect(email.body.encoded).to include(team.name) + end + + it 'includes assignment name in email body' do + team = AssignmentTeam.create(name: 'Test Team', parent_id: assignment.id, type: 'AssignmentTeam') + invitation = Invitation.create(to_id: user1.id, from_id: team.id, assignment_id: assignment.id) + + email = InvitationMailer.with(invitation: invitation).send_acceptance_email + + expect(email.body.encoded).to include(assignment.name) + end + end + + describe '#send_team_acceptance_notification' do + it 'sends notification email to all team members' do + team = AssignmentTeam.create(name: 'Test Team', parent_id: assignment.id, type: 'AssignmentTeam') + inviter_participant = AssignmentParticipant.create(user_id: user2.id, parent_id: assignment.id, type: 'AssignmentParticipant', handle: 'inviter_handle') + TeamsParticipant.create(team_id: team.id, participant_id: inviter_participant.id, user_id: user2.id) + + invitation = Invitation.create(to_id: user1.id, from_id: team.id, assignment_id: assignment.id) + + email = InvitationMailer.with(invitation: invitation).send_team_acceptance_notification + + expect(email.to).to include(user2.email) + end + + it 'includes invitee name in team notification body' do + team = AssignmentTeam.create(name: 'Test Team', parent_id: assignment.id, type: 'AssignmentTeam') + inviter_participant = AssignmentParticipant.create(user_id: user2.id, parent_id: assignment.id, type: 'AssignmentParticipant', handle: 'inviter_handle') + TeamsParticipant.create(team_id: team.id, participant_id: inviter_participant.id, user_id: user2.id) + + invitation = Invitation.create(to_id: user1.id, from_id: team.id, assignment_id: assignment.id) + + email = InvitationMailer.with(invitation: invitation).send_team_acceptance_notification + + expect(email.body.encoded).to include(user1.full_name) + end + + it 'includes team name in team notification body' do + team = AssignmentTeam.create(name: 'Test Team', parent_id: assignment.id, type: 'AssignmentTeam') + inviter_participant = AssignmentParticipant.create(user_id: user2.id, parent_id: assignment.id, type: 'AssignmentParticipant', handle: 'inviter_handle') + TeamsParticipant.create(team_id: team.id, participant_id: inviter_participant.id, user_id: user2.id) + + invitation = Invitation.create(to_id: user1.id, from_id: team.id, assignment_id: assignment.id) + + email = InvitationMailer.with(invitation: invitation).send_team_acceptance_notification + + expect(email.body.encoded).to include(team.name) + end + + it 'includes assignment name in team notification body' do + team = AssignmentTeam.create(name: 'Test Team', parent_id: assignment.id, type: 'AssignmentTeam') + inviter_participant = AssignmentParticipant.create(user_id: user2.id, parent_id: assignment.id, type: 'AssignmentParticipant', handle: 'inviter_handle') + TeamsParticipant.create(team_id: team.id, participant_id: inviter_participant.id, user_id: user2.id) + + invitation = Invitation.create(to_id: user1.id, from_id: team.id, assignment_id: assignment.id) + + email = InvitationMailer.with(invitation: invitation).send_team_acceptance_notification + + expect(email.body.encoded).to include(assignment.name) + end + end end diff --git a/spec/mailers/join_team_request_mailer_spec.rb b/spec/mailers/join_team_request_mailer_spec.rb new file mode 100644 index 000000000..bba838d04 --- /dev/null +++ b/spec/mailers/join_team_request_mailer_spec.rb @@ -0,0 +1,69 @@ +require "rails_helper" + +RSpec.describe JoinTeamRequestMailer, type: :mailer do + include ActiveJob::TestHelper + + let(:role) { Role.create(name: 'Instructor', parent_id: nil, id: 3, default_page_id: nil) } + let(:student_role) { Role.create(name: 'Student', parent_id: nil, id: 5, default_page_id: nil) } + let(:instructor) { Instructor.create(name: 'testinstructor', email: 'instructor@test.com', full_name: 'Test Instructor', password: '123456', role: role) } + let(:requester) { create :user, name: 'requester_user', role: student_role, email: 'requester@test.com' } + let(:team_member) { create :user, name: 'team_member_user', role: student_role, email: 'team_member@test.com' } + let(:assignment) { create(:assignment, instructor: instructor) } + let(:team) { AssignmentTeam.create(name: 'Test Team', parent_id: assignment.id, type: 'AssignmentTeam') } + let(:requester_participant) { AssignmentParticipant.create(user_id: requester.id, parent_id: assignment.id, type: 'AssignmentParticipant', handle: 'requester_handle') } + let(:team_member_participant) { AssignmentParticipant.create(user_id: team_member.id, parent_id: assignment.id, type: 'AssignmentParticipant', handle: 'team_member_handle') } + let(:join_team_request) { JoinTeamRequest.create(participant_id: requester_participant.id, team_id: team.id, comments: 'Please let me join', reply_status: 'PENDING') } + + before(:each) do + ActiveJob::Base.queue_adapter = :test + TeamsParticipant.create(team_id: team.id, participant_id: team_member_participant.id, user_id: team_member.id) + end + + after(:each) do + clear_enqueued_jobs + end + + describe '#send_acceptance_email' do + it 'sends acceptance email to requester' do + email = JoinTeamRequestMailer.with(join_team_request: join_team_request).send_acceptance_email + + expect(email.to).to eq([requester.email]) + end + + it 'has correct subject line' do + email = JoinTeamRequestMailer.with(join_team_request: join_team_request).send_acceptance_email + + expect(email.subject).to include('accepted') + end + + it 'includes requester name in email body' do + email = JoinTeamRequestMailer.with(join_team_request: join_team_request).send_acceptance_email + + expect(email.body.encoded).to include(requester.full_name) + end + + it 'includes team name in email body' do + email = JoinTeamRequestMailer.with(join_team_request: join_team_request).send_acceptance_email + + expect(email.body.encoded).to include(team.name) + end + + it 'includes assignment name in email body' do + email = JoinTeamRequestMailer.with(join_team_request: join_team_request).send_acceptance_email + + expect(email.body.encoded).to include(assignment.name) + end + + it 'includes congratulatory message in email body' do + email = JoinTeamRequestMailer.with(join_team_request: join_team_request).send_acceptance_email + + expect(email.body.encoded).to include('Good news') + end + + it 'includes collaboration message in email body' do + email = JoinTeamRequestMailer.with(join_team_request: join_team_request).send_acceptance_email + + expect(email.body.encoded).to include('collaborate') + end + end +end diff --git a/spec/models/join_team_request_spec.rb b/spec/models/join_team_request_spec.rb index 21b04ee7b..21b8ac417 100644 --- a/spec/models/join_team_request_spec.rb +++ b/spec/models/join_team_request_spec.rb @@ -3,5 +3,382 @@ require 'rails_helper' RSpec.describe JoinTeamRequest, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + include ActiveJob::TestHelper + + let(:role) { Role.create(name: 'Instructor', parent_id: nil, id: 3, default_page_id: nil) } + let(:student_role) { Role.create(name: 'Student', parent_id: nil, id: 5, default_page_id: nil) } + let(:instructor) { Instructor.create(name: 'testinstructor', email: 'instructor@test.com', full_name: 'Test Instructor', password: '123456', role: role) } + let(:requester) { create :user, name: 'requester_user', role: student_role, email: 'requester@test.com' } + let(:team_member) { create :user, name: 'team_member_user', role: student_role, email: 'team_member@test.com' } + let(:another_user) { create :user, name: 'another_user', role: student_role, email: 'another@test.com' } + let(:assignment) { create(:assignment, instructor: instructor, max_team_size: 3) } + let(:team) { AssignmentTeam.create(name: 'Test Team', parent_id: assignment.id, type: 'AssignmentTeam') } + let(:another_team) { AssignmentTeam.create(name: 'Another Team', parent_id: assignment.id, type: 'AssignmentTeam') } + let(:requester_participant) { AssignmentParticipant.create(user_id: requester.id, parent_id: assignment.id, type: 'AssignmentParticipant', handle: 'requester_handle') } + let(:team_member_participant) { AssignmentParticipant.create(user_id: team_member.id, parent_id: assignment.id, type: 'AssignmentParticipant', handle: 'team_member_handle') } + let(:another_participant) { AssignmentParticipant.create(user_id: another_user.id, parent_id: assignment.id, type: 'AssignmentParticipant', handle: 'another_handle') } + + before(:each) do + ActiveJob::Base.queue_adapter = :test + TeamsParticipant.create(team_id: team.id, participant_id: team_member_participant.id, user_id: team_member.id) + end + + after(:each) do + clear_enqueued_jobs + end + + # -------------------------------------------------------------------------- + # Association Tests + # -------------------------------------------------------------------------- + describe 'associations' do + it 'belongs to participant' do + join_request = JoinTeamRequest.new(participant_id: requester_participant.id, team_id: team.id) + expect(join_request).to belong_to(:participant) + end + + it 'belongs to team' do + join_request = JoinTeamRequest.new(participant_id: requester_participant.id, team_id: team.id) + expect(join_request).to belong_to(:team) + end + + it 'can access participant user through association' do + join_request = JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + expect(join_request.participant.user).to eq(requester) + end + + it 'can access team assignment through association' do + join_request = JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + expect(join_request.team.assignment).to eq(assignment) + end + end + + # -------------------------------------------------------------------------- + # Validation Tests + # -------------------------------------------------------------------------- + describe 'validations' do + it 'is valid with valid attributes' do + join_request = JoinTeamRequest.new( + participant_id: requester_participant.id, + team_id: team.id, + comments: 'Please let me join', + reply_status: 'PENDING' + ) + expect(join_request).to be_valid + end + + it 'requires participant_id' do + join_request = JoinTeamRequest.new(team_id: team.id, comments: 'Join please', reply_status: 'PENDING') + expect(join_request).not_to be_valid + expect(join_request.errors[:participant]).to include("must exist") + end + + it 'requires team_id' do + join_request = JoinTeamRequest.new(participant_id: requester_participant.id, comments: 'Join please', reply_status: 'PENDING') + expect(join_request).not_to be_valid + expect(join_request.errors[:team]).to include("must exist") + end + + it 'validates reply_status inclusion' do + join_request = JoinTeamRequest.new( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'INVALID_STATUS' + ) + expect(join_request).not_to be_valid + expect(join_request.errors[:reply_status]).to include("is not included in the list") + end + + it 'accepts PENDING as valid reply_status' do + join_request = JoinTeamRequest.new( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + expect(join_request).to be_valid + end + + it 'accepts ACCEPTED as valid reply_status' do + join_request = JoinTeamRequest.new( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'ACCEPTED' + ) + expect(join_request).to be_valid + end + + it 'accepts DECLINED as valid reply_status' do + join_request = JoinTeamRequest.new( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'DECLINED' + ) + expect(join_request).to be_valid + end + end + + # -------------------------------------------------------------------------- + # Creation and Attributes Tests + # -------------------------------------------------------------------------- + describe 'creation and attributes' do + it 'creates a join request with correct attributes' do + join_request = JoinTeamRequest.create( + participant_id: requester_participant.id, + team_id: team.id, + comments: 'I want to join your team', + reply_status: 'PENDING' + ) + + expect(join_request.participant_id).to eq(requester_participant.id) + expect(join_request.team_id).to eq(team.id) + expect(join_request.comments).to eq('I want to join your team') + expect(join_request.reply_status).to eq('PENDING') + end + + it 'allows creating without explicit reply_status' do + join_request = JoinTeamRequest.create( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + + expect(join_request).to be_persisted + expect(join_request.reply_status).to eq('PENDING') + end + + it 'allows empty comments' do + join_request = JoinTeamRequest.create( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + expect(join_request).to be_valid + expect(join_request.comments).to be_nil + end + + it 'allows updating comments' do + join_request = JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + comments: 'Original comment', + reply_status: 'PENDING' + ) + + join_request.update!(comments: 'Updated comment') + expect(join_request.reload.comments).to eq('Updated comment') + end + end + + # -------------------------------------------------------------------------- + # Relationship Tests + # -------------------------------------------------------------------------- + describe 'relationships' do + it 'returns correct participant' do + join_request = JoinTeamRequest.create( + participant_id: requester_participant.id, + team_id: team.id + ) + + expect(join_request.participant).to eq(requester_participant) + end + + it 'returns correct team' do + join_request = JoinTeamRequest.create( + participant_id: requester_participant.id, + team_id: team.id + ) + + expect(join_request.team).to eq(team) + end + + it 'is destroyed when the team is destroyed' do + join_request = JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + + expect { team.destroy }.to change(JoinTeamRequest, :count).by(-1) + end + end + + # -------------------------------------------------------------------------- + # Status Transition Tests + # -------------------------------------------------------------------------- + describe 'status transitions' do + let(:join_request) do + JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + end + + it 'can transition from PENDING to ACCEPTED' do + join_request.update!(reply_status: 'ACCEPTED') + expect(join_request.reload.reply_status).to eq('ACCEPTED') + end + + it 'can transition from PENDING to DECLINED' do + join_request.update!(reply_status: 'DECLINED') + expect(join_request.reload.reply_status).to eq('DECLINED') + end + + it 'persists status changes' do + join_request.update!(reply_status: 'ACCEPTED') + reloaded = JoinTeamRequest.find(join_request.id) + expect(reloaded.reply_status).to eq('ACCEPTED') + end + end + + # -------------------------------------------------------------------------- + # Query Tests + # -------------------------------------------------------------------------- + describe 'queries' do + before do + @pending_request = JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + @accepted_request = JoinTeamRequest.create!( + participant_id: another_participant.id, + team_id: team.id, + reply_status: 'ACCEPTED' + ) + end + + it 'can filter by PENDING status' do + pending_requests = JoinTeamRequest.where(reply_status: 'PENDING') + expect(pending_requests).to include(@pending_request) + expect(pending_requests).not_to include(@accepted_request) + end + + it 'can filter by ACCEPTED status' do + accepted_requests = JoinTeamRequest.where(reply_status: 'ACCEPTED') + expect(accepted_requests).to include(@accepted_request) + expect(accepted_requests).not_to include(@pending_request) + end + + it 'can find requests by team_id' do + team_requests = JoinTeamRequest.where(team_id: team.id) + expect(team_requests.count).to eq(2) + end + + it 'can find requests by participant_id' do + participant_requests = JoinTeamRequest.where(participant_id: requester_participant.id) + expect(participant_requests).to include(@pending_request) + expect(participant_requests.count).to eq(1) + end + + it 'can check for existing pending request' do + existing = JoinTeamRequest.find_by( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + expect(existing).to eq(@pending_request) + end + end + + # -------------------------------------------------------------------------- + # Multiple Requests Tests + # -------------------------------------------------------------------------- + describe 'multiple requests' do + it 'allows same participant to request different teams' do + request1 = JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + + request2 = JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: another_team.id, + reply_status: 'PENDING' + ) + + expect(request1).to be_persisted + expect(request2).to be_persisted + end + + it 'allows different participants to request same team' do + request1 = JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + + request2 = JoinTeamRequest.create!( + participant_id: another_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + + expect(request1).to be_persisted + expect(request2).to be_persisted + expect(team.join_team_requests.count).to eq(2) + end + + it 'retrieves all requests for a team through association' do + JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + + JoinTeamRequest.create!( + participant_id: another_participant.id, + team_id: team.id, + reply_status: 'PENDING' + ) + + expect(team.join_team_requests.count).to eq(2) + end + end + + # -------------------------------------------------------------------------- + # Edge Cases Tests + # -------------------------------------------------------------------------- + describe 'edge cases' do + it 'handles long comments' do + long_comment = 'A' * 1000 + join_request = JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + comments: long_comment, + reply_status: 'PENDING' + ) + expect(join_request.comments).to eq(long_comment) + end + + it 'handles special characters in comments' do + special_comment = "Hello! I'd like to join. " + join_request = JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + comments: special_comment, + reply_status: 'PENDING' + ) + expect(join_request.comments).to eq(special_comment) + end + + it 'handles unicode in comments' do + unicode_comment = "I'd like to join! 🚀 こんにちは" + join_request = JoinTeamRequest.create!( + participant_id: requester_participant.id, + team_id: team.id, + comments: unicode_comment, + reply_status: 'PENDING' + ) + expect(join_request.comments).to eq(unicode_comment) + end + end end diff --git a/spec/models/teams_participant_spec.rb b/spec/models/teams_participant_spec.rb new file mode 100644 index 000000000..c9a711e4d --- /dev/null +++ b/spec/models/teams_participant_spec.rb @@ -0,0 +1,298 @@ +require 'rails_helper' + +RSpec.describe TeamsParticipant, type: :model do + include RolesHelper + + # -------------------------------------------------------------------------- + # Global Setup + # -------------------------------------------------------------------------- + before(:all) do + @roles = create_roles_hierarchy + end + + # ------------------------------------------------------------------------ + # Helper: DRY-up creation of student users + # ------------------------------------------------------------------------ + def create_student(suffix) + User.create!( + name: suffix, + email: "#{suffix}@example.com", + full_name: suffix.split('_').map(&:capitalize).join(' '), + password_digest: "password", + role_id: @roles[:student].id, + institution_id: institution.id + ) + end + + # ------------------------------------------------------------------------ + # Shared Data Setup + # ------------------------------------------------------------------------ + let(:institution) do + Institution.create!(name: "NC State") + end + + let(:instructor) do + User.create!( + name: "instructor", + full_name: "Instructor User", + email: "instructor@example.com", + password_digest: "password", + role_id: @roles[:instructor].id, + institution_id: institution.id + ) + end + + let(:student_user) { create_student("student1") } + let(:another_student) { create_student("student2") } + + let(:assignment) { Assignment.create!(name: "Test Assignment", instructor_id: instructor.id, max_team_size: 3) } + + let(:team) do + AssignmentTeam.create!( + parent_id: assignment.id, + name: 'Test Team' + ) + end + + let(:another_team) do + AssignmentTeam.create!( + parent_id: assignment.id, + name: 'Another Team' + ) + end + + let(:participant) do + AssignmentParticipant.create!( + user_id: student_user.id, + parent_id: assignment.id, + handle: 'student1_handle' + ) + end + + let(:another_participant) do + AssignmentParticipant.create!( + user_id: another_student.id, + parent_id: assignment.id, + handle: 'student2_handle' + ) + end + + # -------------------------------------------------------------------------- + # Association Tests + # -------------------------------------------------------------------------- + describe 'associations' do + it { should belong_to(:participant) } + it { should belong_to(:team) } + it { should belong_to(:user) } + end + + # -------------------------------------------------------------------------- + # Validation Tests + # -------------------------------------------------------------------------- + describe 'validations' do + it 'is valid with valid attributes' do + teams_participant = TeamsParticipant.new( + participant_id: participant.id, + team_id: team.id, + user_id: student_user.id + ) + expect(teams_participant).to be_valid + end + + it 'requires user_id' do + teams_participant = TeamsParticipant.new( + participant_id: participant.id, + team_id: team.id + ) + expect(teams_participant).not_to be_valid + expect(teams_participant.errors[:user_id]).to include("can't be blank") + end + + it 'enforces uniqueness of participant_id scoped to team_id' do + TeamsParticipant.create!( + participant_id: participant.id, + team_id: team.id, + user_id: student_user.id + ) + + duplicate = TeamsParticipant.new( + participant_id: participant.id, + team_id: team.id, + user_id: student_user.id + ) + + expect(duplicate).not_to be_valid + expect(duplicate.errors[:participant_id]).to include("has already been taken") + end + + it 'allows same participant in different teams' do + # Note: This tests the model validation only - business logic may prevent this + TeamsParticipant.create!( + participant_id: participant.id, + team_id: team.id, + user_id: student_user.id + ) + + different_team_membership = TeamsParticipant.new( + participant_id: participant.id, + team_id: another_team.id, + user_id: student_user.id + ) + + # The model allows this, but business logic in controllers should prevent it + expect(different_team_membership).to be_valid + end + end + + # -------------------------------------------------------------------------- + # Creation and Destruction Tests + # -------------------------------------------------------------------------- + describe 'creation' do + it 'creates a teams_participant record successfully' do + expect { + TeamsParticipant.create!( + participant_id: participant.id, + team_id: team.id, + user_id: student_user.id + ) + }.to change(TeamsParticipant, :count).by(1) + end + + it 'associates participant with the correct team' do + teams_participant = TeamsParticipant.create!( + participant_id: participant.id, + team_id: team.id, + user_id: student_user.id + ) + + expect(teams_participant.team).to eq(team) + expect(teams_participant.participant).to eq(participant) + expect(teams_participant.user).to eq(student_user) + end + end + + describe 'destruction' do + it 'removes the teams_participant record' do + teams_participant = TeamsParticipant.create!( + participant_id: participant.id, + team_id: team.id, + user_id: student_user.id + ) + + expect { + teams_participant.destroy + }.to change(TeamsParticipant, :count).by(-1) + end + + it 'does not destroy the associated team or participant' do + teams_participant = TeamsParticipant.create!( + participant_id: participant.id, + team_id: team.id, + user_id: student_user.id + ) + + team_id = team.id + participant_id = participant.id + + teams_participant.destroy + + expect(Team.find_by(id: team_id)).not_to be_nil + expect(Participant.find_by(id: participant_id)).not_to be_nil + end + end + + # -------------------------------------------------------------------------- + # Team Membership Transfer Tests (for join team requests) + # -------------------------------------------------------------------------- + describe 'team membership transfer' do + it 'allows removing participant from old team and adding to new team' do + # Create initial membership + old_membership = TeamsParticipant.create!( + participant_id: participant.id, + team_id: team.id, + user_id: student_user.id + ) + + # Transfer to new team (simulating accept join team request) + old_membership.destroy + + new_membership = TeamsParticipant.create!( + participant_id: participant.id, + team_id: another_team.id, + user_id: student_user.id + ) + + expect(new_membership).to be_persisted + expect(TeamsParticipant.find_by(participant_id: participant.id, team_id: team.id)).to be_nil + expect(TeamsParticipant.find_by(participant_id: participant.id, team_id: another_team.id)).not_to be_nil + end + + it 'updates team participant count correctly after transfer' do + # Add participant to first team + TeamsParticipant.create!( + participant_id: participant.id, + team_id: team.id, + user_id: student_user.id + ) + + # Add another participant to second team + TeamsParticipant.create!( + participant_id: another_participant.id, + team_id: another_team.id, + user_id: another_student.id + ) + + expect(team.participants.count).to eq(1) + expect(another_team.participants.count).to eq(1) + + # Transfer first participant to second team + TeamsParticipant.find_by(participant_id: participant.id, team_id: team.id).destroy + TeamsParticipant.create!( + participant_id: participant.id, + team_id: another_team.id, + user_id: student_user.id + ) + + team.reload + another_team.reload + + expect(team.participants.count).to eq(0) + expect(another_team.participants.count).to eq(2) + end + end + + # -------------------------------------------------------------------------- + # Query Tests + # -------------------------------------------------------------------------- + describe 'querying' do + before do + TeamsParticipant.create!( + participant_id: participant.id, + team_id: team.id, + user_id: student_user.id + ) + TeamsParticipant.create!( + participant_id: another_participant.id, + team_id: another_team.id, + user_id: another_student.id + ) + end + + it 'finds teams_participant by participant_id' do + result = TeamsParticipant.find_by(participant_id: participant.id) + expect(result).not_to be_nil + expect(result.team_id).to eq(team.id) + end + + it 'finds teams_participant by team_id' do + result = TeamsParticipant.where(team_id: team.id) + expect(result.count).to eq(1) + expect(result.first.participant_id).to eq(participant.id) + end + + it 'finds all participants for a team through association' do + expect(team.participants).to include(participant) + expect(another_team.participants).to include(another_participant) + end + end +end diff --git a/spec/requests/api/v1/acceptance_email_integration_spec.rb b/spec/requests/api/v1/acceptance_email_integration_spec.rb new file mode 100644 index 000000000..e533ad135 --- /dev/null +++ b/spec/requests/api/v1/acceptance_email_integration_spec.rb @@ -0,0 +1,394 @@ +require 'rails_helper' +require 'json_web_token' + +RSpec.describe 'Join Team Request and Invitation Acceptance Email Integration', type: :request do + include ActiveJob::TestHelper + + before(:all) do + @roles = create_roles_hierarchy + end + + let(:instructor) { + User.create!( + name: "instructor_user", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:instructor].id, + full_name: "Instructor User", + email: "instructor@example.com" + ) + } + + let(:student1) { + User.create!( + name: "student1", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:student].id, + full_name: "Student One", + email: "student1@example.com" + ) + } + + let(:student2) { + User.create!( + name: "student2", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:student].id, + full_name: "Student Two", + email: "student2@example.com" + ) + } + + let(:student3) { + User.create!( + name: "student3", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:student].id, + full_name: "Student Three", + email: "student3@example.com" + ) + } + + let(:assignment) { + Assignment.create!( + name: 'Integration Test Assignment', + instructor_id: instructor.id, + has_teams: true, + max_team_size: 4 + ) + } + + let(:team1) { + AssignmentTeam.create!( + name: 'Integration Test Team', + parent_id: assignment.id, + type: 'AssignmentTeam' + ) + } + + let(:participant1) { + AssignmentParticipant.create!( + user_id: student1.id, + parent_id: assignment.id, + type: 'AssignmentParticipant', + handle: 'student1_handle' + ) + } + + let(:participant2) { + AssignmentParticipant.create!( + user_id: student2.id, + parent_id: assignment.id, + type: 'AssignmentParticipant', + handle: 'student2_handle' + ) + } + + let(:participant3) { + AssignmentParticipant.create!( + user_id: student3.id, + parent_id: assignment.id, + type: 'AssignmentParticipant', + handle: 'student3_handle' + ) + } + + before(:each) do + ActiveJob::Base.queue_adapter = :test + # Add student1 to team1 + TeamsParticipant.create!( + team_id: team1.id, + participant_id: participant1.id, + user_id: student1.id + ) + end + + after(:each) do + clear_enqueued_jobs + end + + describe 'Complete Join Team Request Acceptance Workflow' do + let(:team_member_token) { JsonWebToken.encode({id: student1.id}) } + let(:team_member_headers) { { 'Authorization' => "Bearer #{team_member_token}" } } + + it 'completes full workflow: request creation -> acceptance -> email notification' do + participant2 # Ensure participant exists + + # Step 1: Create join team request + post '/api/v1/join_team_requests', + params: { + team_id: team1.id, + assignment_id: assignment.id, + comments: 'I want to join your team' + }, + headers: { 'Authorization' => "Bearer #{JsonWebToken.encode({id: student2.id})}" } + + expect(response).to have_http_status(:created) + created_request = JSON.parse(response.body) + request_id = created_request['join_team_request']['id'] + + # Step 2: Accept the request + expect { + patch "/api/v1/join_team_requests/#{request_id}/accept", headers: team_member_headers + }.to have_enqueued_job(ActionMailer::MailDeliveryJob) + + expect(response).to have_http_status(:ok) + + # Step 3: Verify participant was added + expect(team1.participants.reload).to include(participant2) + + # Step 4: Verify request status changed + updated_request = JoinTeamRequest.find(request_id) + expect(updated_request.reply_status).to eq('ACCEPTED') + end + + it 'sends email with correct content when join request is accepted' do + participant2 # Ensure participant exists + join_request = JoinTeamRequest.create!( + participant_id: participant2.id, + team_id: team1.id, + comments: 'Please let me join' + ) + + patch "/api/v1/join_team_requests/#{join_request.id}/accept", headers: team_member_headers + + # Email job should be enqueued + expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.on_queue('default') + end + end + + describe 'Complete Invitation Acceptance Workflow' do + it 'completes full workflow: invitation creation -> acceptance -> email notifications' do + participant2 # Ensure participant exists + participant3 # Ensure participant exists + + # Step 1: Create invitation + invitation = Invitation.create!( + to_id: participant2.id, + from_id: team1.id, + assignment_id: assignment.id + ) + + expect(invitation).to be_valid + + # Step 2: Accept invitation + result = nil + expect { + result = invitation.accept_invitation + }.to have_enqueued_job(ActionMailer::MailDeliveryJob).at_least(:twice) + + expect(result[:success]).to be true + + # Step 3: Verify participant was added to team + expect(team1.participants.reload).to include(participant2) + + # Step 4: Verify invitation status changed + invitation.reload + expect(invitation.reply_status).to eq(InvitationValidator::ACCEPT_STATUS) + end + + it 'sends acceptance email to invitee when invitation is accepted' do + participant2 # Ensure participant exists + + invitation = Invitation.create!( + to_id: participant2.id, + from_id: team1.id, + assignment_id: assignment.id + ) + + expect { + invitation.accept_invitation + }.to have_enqueued_job(ActionMailer::MailDeliveryJob) + .with('InvitationMailer', 'send_acceptance_email', anything) + end + + it 'sends team notification email when invitation is accepted' do + participant2 # Ensure participant exists + participant3 # Ensure participant exists + + invitation = Invitation.create!( + to_id: participant2.id, + from_id: team1.id, + assignment_id: assignment.id + ) + + expect { + invitation.accept_invitation + }.to have_enqueued_job(ActionMailer::MailDeliveryJob) + .with('InvitationMailer', 'send_team_acceptance_notification', anything) + end + + it 'sends two emails (to invitee and team) on acceptance' do + participant2 # Ensure participant exists + + invitation = Invitation.create!( + to_id: participant2.id, + from_id: team1.id, + assignment_id: assignment.id + ) + + expect { + invitation.accept_invitation + }.to have_enqueued_job(ActionMailer::MailDeliveryJob).exactly(2).times + end + end + + describe 'Email Content Validation' do + it 'join request acceptance email includes all required information' do + participant2 # Ensure participant exists + join_request = JoinTeamRequest.create!( + participant_id: participant2.id, + team_id: team1.id, + comments: 'Please let me join' + ) + + email = JoinTeamRequestMailer.with(join_team_request: join_request).send_acceptance_email + + # Verify recipient + expect(email.to).to include(student2.email) + + # Verify content + body = email.body.encoded + expect(body).to include(student2.full_name) + expect(body).to include(team1.name) + expect(body).to include(assignment.name) + expect(body).to include('Good news') + expect(body).to include('accepted') + end + + it 'invitation acceptance email includes all required information' do + participant2 # Ensure participant exists + invitation = Invitation.create!( + to_id: participant2.id, + from_id: team1.id, + assignment_id: assignment.id + ) + + email = InvitationMailer.with(invitation: invitation).send_acceptance_email + + # Verify recipient + expect(email.to).to include(student2.email) + + # Verify content + body = email.body.encoded + expect(body).to include(student2.full_name) + expect(body).to include(team1.name) + expect(body).to include(assignment.name) + expect(body).to include('accepted') + end + + it 'team notification email includes all required information' do + participant2 # Ensure participant exists + invitation = Invitation.create!( + to_id: participant2.id, + from_id: team1.id, + assignment_id: assignment.id + ) + + email = InvitationMailer.with(invitation: invitation).send_team_acceptance_notification + + # Verify recipient(s) + expect(email.to).to include(student1.email) + + # Verify content + body = email.body.encoded + expect(body).to include(student2.full_name) + expect(body).to include(team1.name) + expect(body).to include(assignment.name) + expect(body).to include('joined') + end + end + + describe 'Error Handling' do + it 'does not send email if team is full' do + participant2 # Ensure participant exists + assignment.update!(max_team_size: 1) + join_request = JoinTeamRequest.create!( + participant_id: participant2.id, + team_id: team1.id, + comments: 'Please let me join' + ) + + team_member_token = JsonWebToken.encode({id: student1.id}) + team_member_headers = { 'Authorization' => "Bearer #{team_member_token}" } + + expect { + patch "/api/v1/join_team_requests/#{join_request.id}/accept", headers: team_member_headers + }.not_to have_enqueued_job(ActionMailer::MailDeliveryJob) + end + + it 'handles invitation acceptance when team already has multiple members' do + participant2 # Ensure participant exists + participant3 # Ensure participant exists + + # Add another member to the team + TeamsParticipant.create!( + team_id: team1.id, + participant_id: participant3.id, + user_id: student3.id + ) + + invitation = Invitation.create!( + to_id: participant2.id, + from_id: team1.id, + assignment_id: assignment.id + ) + + expect { + result = invitation.accept_invitation + expect(result[:success]).to be true + }.to have_enqueued_job(ActionMailer::MailDeliveryJob).at_least(:twice) + end + end + + describe 'OODD Principles Compliance' do + it 'encapsulates email logic within mailer classes' do + # Email logic should not be in controller or model + # Instead, mailer classes should handle email composition + + participant2 # Ensure participant exists + join_request = JoinTeamRequest.create!( + participant_id: participant2.id, + team_id: team1.id, + comments: 'Please let me join' + ) + + # Mailer should be responsible for email composition + expect(JoinTeamRequestMailer).to respond_to(:send_acceptance_email) + end + + it 'separates concerns: controller handles requests, mailer handles emails' do + participant2 # Ensure participant exists + join_request = JoinTeamRequest.create!( + participant_id: participant2.id, + team_id: team1.id, + comments: 'Please let me join' + ) + + team_member_token = JsonWebToken.encode({id: student1.id}) + team_member_headers = { 'Authorization' => "Bearer #{team_member_token}" } + + # Controller should call mailer, not send email directly + expect(JoinTeamRequestMailer).to receive(:with).and_call_original + + patch "/api/v1/join_team_requests/#{join_request.id}/accept", headers: team_member_headers + end + + it 'maintains single responsibility: model accepts, mailer notifies' do + participant2 # Ensure participant exists + invitation = Invitation.create!( + to_id: participant2.id, + from_id: team1.id, + assignment_id: assignment.id + ) + + # Model's accept_invitation should focus on acceptance logic and delegation + result = invitation.accept_invitation + + # Check that it returns success + expect(result[:success]).to be true + + # Emails should be queued (responsibility of mailer) + expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.at_least(:twice) + end + end +end diff --git a/spec/requests/api/v1/advertisements_spec.rb b/spec/requests/api/v1/advertisements_spec.rb new file mode 100644 index 000000000..1c3cff681 --- /dev/null +++ b/spec/requests/api/v1/advertisements_spec.rb @@ -0,0 +1,361 @@ +require 'swagger_helper' +require 'rails_helper' +require 'json_web_token' + +RSpec.describe 'Advertisements API', type: :request do + before(:all) do + @roles = create_roles_hierarchy + end + + let(:instructor) { + User.create!( + name: "instructor_user", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:instructor].id, + full_name: "Instructor User", + email: "instructor@example.com" + ) + } + + let(:student1) { + User.create!( + name: "student1", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:student].id, + full_name: "Student One", + email: "student1@example.com" + ) + } + + let(:student2) { + User.create!( + name: "student2", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:student].id, + full_name: "Student Two", + email: "student2@example.com" + ) + } + + let(:student3) { + User.create!( + name: "student3", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:student].id, + full_name: "Student Three", + email: "student3@example.com" + ) + } + + let(:assignment) { + Assignment.create!( + name: 'Test Assignment', + instructor_id: instructor.id, + has_teams: true, + has_topics: true, + max_team_size: 3 + ) + } + + let(:sign_up_topic) { + SignUpTopic.create!( + topic_name: 'Test Topic', + assignment_id: assignment.id, + max_choosers: 2 + ) + } + + let(:team) { + AssignmentTeam.create!( + name: 'Team Alpha', + parent_id: assignment.id, + type: 'AssignmentTeam' + ) + } + + let(:participant1) { + AssignmentParticipant.create!( + user_id: student1.id, + parent_id: assignment.id, + type: 'AssignmentParticipant', + handle: 'student1_handle' + ) + } + + let(:participant2) { + AssignmentParticipant.create!( + user_id: student2.id, + parent_id: assignment.id, + type: 'AssignmentParticipant', + handle: 'student2_handle' + ) + } + + let(:participant3) { + AssignmentParticipant.create!( + user_id: student3.id, + parent_id: assignment.id, + type: 'AssignmentParticipant', + handle: 'student3_handle' + ) + } + + let(:signed_up_team) { + SignedUpTeam.create!( + sign_up_topic_id: sign_up_topic.id, + team_id: team.id, + is_waitlisted: false + ) + } + + before(:each) do + # Add student1 to team + TeamsParticipant.create!( + team_id: team.id, + participant_id: participant1.id, + user_id: student1.id + ) + end + + describe 'Advertisement Display' do + let(:student2_token) { JsonWebToken.encode({ id: student2.id }) } + let(:student2_headers) { { 'Authorization' => "Bearer #{student2_token}" } } + + context 'when viewing assignments with advertisements' do + it 'returns advertisement data when team has created advertisement' do + # Create advertisement + signed_up_team # Create signed up team + signed_up_team.update(advertise_for_partner: true, comments_for_advertisement: 'Looking for strong members!') + + # Simulate endpoint: GET /api/v1/assignments/:id/sign_up_topics + get "/api/v1/sign_up_topics?assignment_id=#{assignment.id}", headers: student2_headers + + expect(response).to have_http_status(:ok) + body = JSON.parse(response.body) + + # Verify we can see the topic and its signed up teams (which contain advertisement data) + expect(body).to be_a(Array) + end + + it 'includes trumpet icon indicator when team is advertising' do + signed_up_team.update(advertise_for_partner: true, comments_for_advertisement: 'We need members!') + participant2 # ensure exists + + get "/api/v1/sign_up_topics?assignment_id=#{assignment.id}", headers: student2_headers + + expect(response).to have_http_status(:ok) + # The frontend will check signed_up_teams[].advertise_for_partner to render trumpet icon + end + + it 'returns advertisement comments' do + ad_text = 'Experienced team looking for dedicated members to complete project' + signed_up_team.update(advertise_for_partner: true, comments_for_advertisement: ad_text) + participant2 # ensure exists + + get "/api/v1/sign_up_topics?assignment_id=#{assignment.id}", headers: student2_headers + + expect(response).to have_http_status(:ok) + # Frontend will display comments_for_advertisement text + end + end + end + + describe 'Advertisement Creation' do + context 'when team wants to create an advertisement' do + let(:student1_token) { JsonWebToken.encode({ id: student1.id }) } + let(:student1_headers) { { 'Authorization' => "Bearer #{student1_token}" } } + + it 'allows team member to enable advertisement' do + signed_up_team # Create signed up team + participant1 # ensure team member exists + + # Update signed up team to create advertisement + patch "/api/v1/signed_up_teams/#{signed_up_team.id}", + params: { + signed_up_team: { + advertise_for_partner: true, + comments_for_advertisement: 'Looking for passionate developers!' + } + }, + headers: student1_headers + + # Verify the team member can enable advertisement + expect(response).to have_http_status(:ok) + signed_up_team.reload + expect(signed_up_team.advertise_for_partner).to be true + expect(signed_up_team.comments_for_advertisement).to eq('Looking for passionate developers!') + end + + it 'stores advertisement text correctly' do + ad_text = 'We have one spot left. Please join our amazing team!' + signed_up_team.update( + advertise_for_partner: true, + comments_for_advertisement: ad_text + ) + + expect(signed_up_team.advertise_for_partner).to be true + expect(signed_up_team.comments_for_advertisement).to eq(ad_text) + end + + it 'allows team to disable advertisement' do + signed_up_team.update( + advertise_for_partner: true, + comments_for_advertisement: 'Looking for members' + ) + + signed_up_team.update(advertise_for_partner: false) + + expect(signed_up_team.advertise_for_partner).to be false + end + end + end + + describe 'Join Request from Advertisement' do + let(:student2_token) { JsonWebToken.encode({ id: student2.id }) } + let(:student2_headers) { { 'Authorization' => "Bearer #{student2_token}" } } + + context 'when student sees advertisement and wants to join' do + before do + signed_up_team.update( + advertise_for_partner: true, + comments_for_advertisement: 'We need one more member!' + ) + end + + it 'allows student to submit join request for advertising team' do + participant2 # ensure exists + + post '/api/v1/join_team_requests', + params: { + team_id: team.id, + assignment_id: assignment.id, + comments: 'I would like to join your team based on your advertisement' + }, + headers: student2_headers + + expect(response).to have_http_status(:created) + body = JSON.parse(response.body) + expect(body['reply_status']).to eq('PENDING') + end + + it 'tracks which team the request is for' do + participant2 # ensure exists + + post '/api/v1/join_team_requests', + params: { + team_id: team.id, + assignment_id: assignment.id, + comments: 'Interested in joining' + }, + headers: student2_headers + + join_request = JoinTeamRequest.last + expect(join_request.team_id).to eq(team.id) + end + end + end + + describe 'Team Full Scenario with Advertisements' do + let(:student1_token) { JsonWebToken.encode({ id: student1.id }) } + let(:student1_headers) { { 'Authorization' => "Bearer #{student1_token}" } } + + let(:student2_token) { JsonWebToken.encode({ id: student2.id }) } + let(:student2_headers) { { 'Authorization' => "Bearer #{student2_token}" } } + + let(:student3_token) { JsonWebToken.encode({ id: student3.id }) } + let(:student3_headers) { { 'Authorization' => "Bearer #{student3_token}" } } + + context 'when team reaches maximum capacity' do + before do + signed_up_team.update(advertise_for_partner: true, comments_for_advertisement: 'Last spot available!') + participant2 # ensure exists + participant3 # ensure exists + + # Add student2 to team (now 2 members) + TeamsParticipant.create!( + team_id: team.id, + participant_id: participant2.id, + user_id: student2.id + ) + end + + it 'prevents new join requests when team is full' do + assignment.update(max_team_size: 2) + + post '/api/v1/join_team_requests', + params: { + team_id: team.id, + assignment_id: assignment.id, + comments: 'I want to join' + }, + headers: student3_headers + + expect(response).to have_http_status(:unprocessable_entity) + body = JSON.parse(response.body) + expect(body['message']).to include('full') + end + + it 'prevents team owner from accepting request when team is full' do + join_request = JoinTeamRequest.create!( + participant_id: participant3.id, + team_id: team.id, + comments: 'Please let me join', + reply_status: 'PENDING' + ) + + assignment.update(max_team_size: 2) + + patch "/api/v1/join_team_requests/#{join_request.id}/accept", + headers: student1_headers + + expect(response).to have_http_status(:unprocessable_entity) + body = JSON.parse(response.body) + expect(body['error']).to include('full') + end + end + end + + describe 'Integration: Advertisement Lifecycle' do + let(:student1_token) { JsonWebToken.encode({ id: student1.id }) } + let(:student1_headers) { { 'Authorization' => "Bearer #{student1_token}" } } + + let(:student2_token) { JsonWebToken.encode({ id: student2.id }) } + let(:student2_headers) { { 'Authorization' => "Bearer #{student2_token}" } } + + it 'completes full workflow: create team -> advertise -> receive request -> accept' do + signed_up_team # Create signed up team with student1 + participant2 # ensure student2 exists + + # Step 1: Team creates advertisement + signed_up_team.update( + advertise_for_partner: true, + comments_for_advertisement: 'Expert team seeking final member' + ) + expect(signed_up_team.advertise_for_partner).to be true + + # Step 2: Another student sees advertisement and submits join request + post '/api/v1/join_team_requests', + params: { + team_id: team.id, + assignment_id: assignment.id, + comments: 'Saw your advertisement, interested in joining' + }, + headers: student2_headers + + expect(response).to have_http_status(:created) + join_request = JoinTeamRequest.last + expect(join_request.team_id).to eq(team.id) + + # Step 3: Team member sees the request and accepts it + patch "/api/v1/join_team_requests/#{join_request.id}/accept", + headers: student1_headers + + expect(response).to have_http_status(:ok) + + # Step 4: Verify student2 is now on the team + team.reload + expect(team.participants.count).to eq(2) + expect(team.participants.pluck(:user_id)).to include(student2.id) + end + end +end diff --git a/spec/requests/api/v1/join_team_requests_controller_spec.rb b/spec/requests/api/v1/join_team_requests_controller_spec.rb new file mode 100644 index 000000000..58713fad1 --- /dev/null +++ b/spec/requests/api/v1/join_team_requests_controller_spec.rb @@ -0,0 +1,424 @@ +require 'swagger_helper' +require 'rails_helper' +require 'json_web_token' + +RSpec.describe 'JoinTeamRequests API', type: :request do + before(:all) do + @roles = create_roles_hierarchy + end + + let(:admin) { + User.create!( + name: "admin_user", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:admin].id, + full_name: "Admin User", + email: "admin@example.com" + ) + } + + let(:instructor) { + User.create!( + name: "instructor_user", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:instructor].id, + full_name: "Instructor User", + email: "instructor@example.com" + ) + } + + let(:student1) { + User.create!( + name: "student1", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:student].id, + full_name: "Student One", + email: "student1@example.com" + ) + } + + let(:student2) { + User.create!( + name: "student2", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:student].id, + full_name: "Student Two", + email: "student2@example.com" + ) + } + + let(:student3) { + User.create!( + name: "student3", + password_digest: BCrypt::Password.create("password"), + role_id: @roles[:student].id, + full_name: "Student Three", + email: "student3@example.com" + ) + } + + let(:assignment) { + Assignment.create!( + name: 'Test Assignment', + instructor_id: instructor.id, + has_teams: true, + max_team_size: 3 + ) + } + + let(:team1) { + AssignmentTeam.create!( + name: 'Team 1', + parent_id: assignment.id, + type: 'AssignmentTeam' + ) + } + + let(:participant1) { + AssignmentParticipant.create!( + user_id: student1.id, + parent_id: assignment.id, + type: 'AssignmentParticipant', + handle: 'student1_handle' + ) + } + + let(:participant2) { + AssignmentParticipant.create!( + user_id: student2.id, + parent_id: assignment.id, + type: 'AssignmentParticipant', + handle: 'student2_handle' + ) + } + + let(:participant3) { + AssignmentParticipant.create!( + user_id: student3.id, + parent_id: assignment.id, + type: 'AssignmentParticipant', + handle: 'student3_handle' + ) + } + + let(:join_team_request) { + JoinTeamRequest.create!( + participant_id: participant2.id, + team_id: team1.id, + comments: 'Please let me join your team', + reply_status: 'PENDING' + ) + } + + before(:each) do + # Add student1 to team1 + TeamsParticipant.create!( + team_id: team1.id, + participant_id: participant1.id, + user_id: student1.id + ) + end + + describe 'Authorization Tests' do + context 'when user is admin' do + let(:admin_token) { JsonWebToken.encode({id: admin.id}) } + let(:admin_headers) { { 'Authorization' => "Bearer #{admin_token}" } } + + it 'allows admin to view all join team requests' do + join_team_request # Create the request + get '/api/v1/join_team_requests', headers: admin_headers + expect(response).to have_http_status(:ok) + end + end + + context 'when user is student trying to access index' do + let(:student_token) { JsonWebToken.encode({id: student1.id}) } + let(:student_headers) { { 'Authorization' => "Bearer #{student_token}" } } + + it 'denies student access to index action' do + get '/api/v1/join_team_requests', headers: student_headers + expect(response).to have_http_status(:forbidden) + end + end + + context 'when student creates a join team request' do + let(:student2_token) { JsonWebToken.encode({id: student2.id}) } + let(:student2_headers) { { 'Authorization' => "Bearer #{student2_token}" } } + + it 'allows student to create a request' do + participant2 # Ensure participant exists + post '/api/v1/join_team_requests', + params: { + team_id: team1.id, + assignment_id: assignment.id, + comments: 'I want to join' + }, + headers: student2_headers + expect(response).to have_http_status(:created) + end + + it 'prevents student from joining a full team' do + # Fill the team to max capacity + assignment.update!(max_team_size: 1) + participant2 # Ensure participant exists + + post '/api/v1/join_team_requests', + params: { + team_id: team1.id, + assignment_id: assignment.id, + comments: 'I want to join' + }, + headers: student2_headers + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['message']).to eq('This team is full.') + end + end + + context 'when viewing a join team request' do + let(:creator_token) { JsonWebToken.encode({id: student2.id}) } + let(:creator_headers) { { 'Authorization' => "Bearer #{creator_token}" } } + + let(:team_member_token) { JsonWebToken.encode({id: student1.id}) } + let(:team_member_headers) { { 'Authorization' => "Bearer #{team_member_token}" } } + + let(:outsider_token) { JsonWebToken.encode({id: student3.id}) } + let(:outsider_headers) { { 'Authorization' => "Bearer #{outsider_token}" } } + + it 'allows the request creator to view their own request' do + participant2 # Ensure participant exists + get "/api/v1/join_team_requests/#{join_team_request.id}", headers: creator_headers + expect(response).to have_http_status(:ok) + end + + it 'allows team members to view requests to their team' do + participant1 # Ensure participant exists + get "/api/v1/join_team_requests/#{join_team_request.id}", headers: team_member_headers + expect(response).to have_http_status(:ok) + end + + it 'denies access to students not involved in the request' do + participant3 # Ensure participant exists + get "/api/v1/join_team_requests/#{join_team_request.id}", headers: outsider_headers + expect(response).to have_http_status(:forbidden) + end + end + + context 'when updating a join team request' do + let(:creator_token) { JsonWebToken.encode({id: student2.id}) } + let(:creator_headers) { { 'Authorization' => "Bearer #{creator_token}" } } + + let(:team_member_token) { JsonWebToken.encode({id: student1.id}) } + let(:team_member_headers) { { 'Authorization' => "Bearer #{team_member_token}" } } + + it 'allows the request creator to update their own request' do + participant2 # Ensure participant exists + patch "/api/v1/join_team_requests/#{join_team_request.id}", + params: { join_team_request: { comments: 'Updated comment' } }, + headers: creator_headers + expect(response).to have_http_status(:ok) + end + + it 'denies team members from updating the request' do + participant1 # Ensure participant exists + patch "/api/v1/join_team_requests/#{join_team_request.id}", + params: { join_team_request: { comments: 'Updated comment' } }, + headers: team_member_headers + expect(response).to have_http_status(:forbidden) + end + end + + context 'when deleting a join team request' do + let(:creator_token) { JsonWebToken.encode({id: student2.id}) } + let(:creator_headers) { { 'Authorization' => "Bearer #{creator_token}" } } + + let(:team_member_token) { JsonWebToken.encode({id: student1.id}) } + let(:team_member_headers) { { 'Authorization' => "Bearer #{team_member_token}" } } + + it 'allows the request creator to delete their own request' do + participant2 # Ensure participant exists + delete "/api/v1/join_team_requests/#{join_team_request.id}", headers: creator_headers + expect(response).to have_http_status(:ok) + end + + it 'denies team members from deleting the request' do + participant1 # Ensure participant exists + delete "/api/v1/join_team_requests/#{join_team_request.id}", headers: team_member_headers + expect(response).to have_http_status(:forbidden) + end + end + + context 'when declining a join team request' do + let(:creator_token) { JsonWebToken.encode({id: student2.id}) } + let(:creator_headers) { { 'Authorization' => "Bearer #{creator_token}" } } + + let(:team_member_token) { JsonWebToken.encode({id: student1.id}) } + let(:team_member_headers) { { 'Authorization' => "Bearer #{team_member_token}" } } + + let(:outsider_token) { JsonWebToken.encode({id: student3.id}) } + let(:outsider_headers) { { 'Authorization' => "Bearer #{outsider_token}" } } + + it 'allows team members to decline a request' do + participant1 # Ensure participant exists + patch "/api/v1/join_team_requests/#{join_team_request.id}/decline", headers: team_member_headers + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['message']).to eq('Join team request declined successfully') + end + + it 'denies the request creator from declining their own request' do + participant2 # Ensure participant exists + patch "/api/v1/join_team_requests/#{join_team_request.id}/decline", headers: creator_headers + expect(response).to have_http_status(:forbidden) + end + + it 'denies outsiders from declining the request' do + participant3 # Ensure participant exists + patch "/api/v1/join_team_requests/#{join_team_request.id}/decline", headers: outsider_headers + expect(response).to have_http_status(:forbidden) + end + end + + context 'when accepting a join team request' do + let(:team_member_token) { JsonWebToken.encode({id: student1.id}) } + let(:team_member_headers) { { 'Authorization' => "Bearer #{team_member_token}" } } + + let(:creator_token) { JsonWebToken.encode({id: student2.id}) } + let(:creator_headers) { { 'Authorization' => "Bearer #{creator_token}" } } + + it 'allows team members to accept a request' do + participant1 # Ensure participant exists + patch "/api/v1/join_team_requests/#{join_team_request.id}/accept", headers: team_member_headers + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['message']).to eq('Join team request accepted successfully') + + # Verify participant was added to team + expect(team1.participants.reload).to include(participant2) + end + + it 'denies the request creator from accepting their own request' do + participant2 # Ensure participant exists + patch "/api/v1/join_team_requests/#{join_team_request.id}/accept", headers: creator_headers + expect(response).to have_http_status(:forbidden) + end + + it 'prevents accepting when team is full' do + # Fill the team to max capacity + assignment.update!(max_team_size: 1) + participant1 # Ensure participant exists + + patch "/api/v1/join_team_requests/#{join_team_request.id}/accept", headers: team_member_headers + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Team is full') + end + end + + context 'when filtering join team requests' do + let(:student_token) { JsonWebToken.encode({id: student1.id}) } + let(:student_headers) { { 'Authorization' => "Bearer #{student_token}" } } + + it 'gets requests for a specific team' do + participant2 # Ensure participant exists + join_team_request # Create the request + + get "/api/v1/join_team_requests/for_team/#{team1.id}", headers: student_headers + expect(response).to have_http_status(:ok) + + data = JSON.parse(response.body) + expect(data).to be_an(Array) + expect(data.length).to be >= 1 + end + + it 'gets requests by a specific user' do + participant2 # Ensure participant exists + join_team_request # Create the request + + get "/api/v1/join_team_requests/by_user/#{student2.id}", headers: student_headers + expect(response).to have_http_status(:ok) + + data = JSON.parse(response.body) + expect(data).to be_an(Array) + end + + it 'gets only pending requests' do + participant2 # Ensure participant exists + join_team_request # Create the request + + get "/api/v1/join_team_requests/pending", headers: student_headers + expect(response).to have_http_status(:ok) + + data = JSON.parse(response.body) + expect(data).to be_an(Array) + expect(data.all? { |req| req['reply_status'] == 'PENDING' }).to be true + end + end + end + + describe 'Email Notifications on Acceptance' do + let(:team_member_token) { JsonWebToken.encode({id: student1.id}) } + let(:team_member_headers) { { 'Authorization' => "Bearer #{team_member_token}" } } + + before(:each) do + ActiveJob::Base.queue_adapter = :test + end + + after(:each) do + clear_enqueued_jobs + end + + it 'sends acceptance email to requester when join request is accepted' do + participant1 # Ensure participant exists + participant2 # Ensure participant exists + + # We use deliver_now (synchronous) so we can't test job enqueueing + # Instead, verify the request is accepted and status is updated + patch "/api/v1/join_team_requests/#{join_team_request.id}/accept", headers: team_member_headers + + expect(response).to have_http_status(:ok) + join_team_request.reload + expect(join_team_request.reply_status).to eq('ACCEPTED') + end + + it 'sends email to correct recipient (requester)' do + participant1 # Ensure participant exists + participant2 # Ensure participant exists + + patch "/api/v1/join_team_requests/#{join_team_request.id}/accept", headers: team_member_headers + + expect(response).to have_http_status(:ok) + # Verify request was accepted (email is sent synchronously) + body = JSON.parse(response.body) + expect(body['join_team_request']['reply_status']).to eq('ACCEPTED') + end + + it 'does not send email if acceptance fails due to full team' do + assignment.update!(max_team_size: 1) + participant1 # Ensure participant exists + participant2 # Ensure participant exists + + patch "/api/v1/join_team_requests/#{join_team_request.id}/accept", headers: team_member_headers + + # Request should fail with unprocessable_entity status + expect(response).to have_http_status(:unprocessable_entity) + join_team_request.reload + expect(join_team_request.reply_status).to eq('PENDING') # Status should not change + end + + it 'updates reply_status to ACCEPTED before sending email' do + participant1 # Ensure participant exists + participant2 # Ensure participant exists + + patch "/api/v1/join_team_requests/#{join_team_request.id}/accept", headers: team_member_headers + + join_team_request.reload + expect(join_team_request.reply_status).to eq('ACCEPTED') + end + + it 'adds participant to team before sending email' do + participant1 # Ensure participant exists + participant2 # Ensure participant exists + + patch "/api/v1/join_team_requests/#{join_team_request.id}/accept", headers: team_member_headers + + expect(team1.participants.reload).to include(participant2) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5c35e0b5d..bf34384d8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,14 +18,17 @@ require 'simplecov' require 'coveralls' require "simplecov_json_formatter" -Coveralls.wear! 'rails' +# Coveralls.wear! 'rails' SimpleCov.formatter = SimpleCov::Formatter::JSONFormatter # SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ # SimpleCov::Formatter::HTMLFormatter, # Coveralls::SimpleCov::Formatter # ]) -SimpleCov.start 'rails' +if !ENV['COVERAGE_STARTED'] + SimpleCov.start 'rails' + ENV['COVERAGE_STARTED'] = 'true' +end RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index 54879bf02..02ac7e411 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -41,7 +41,7 @@ url: 'http://{defaultHost}', variables: { defaultHost: { - default: 'localhost:3002' + default: '152.7.176.23:3002' } } } diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index cc0294e73..1837f420c 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -1335,4 +1335,4 @@ servers: - url: http://{defaultHost} variables: defaultHost: - default: localhost:3002 + default: 152.7.176.23:3002