diff --git a/Gemfile b/Gemfile index b5f47ee4b..07d7934cb 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem 'puma', '~> 5.0' gem 'rails', '~> 8.0', '>= 8.0.1' gem 'rswag-api' gem 'rswag-ui' +gem 'paper_trail' # Build JSON APIs with ease [https://github.com/rails/jbuilder] # gem "jbuilder" diff --git a/Gemfile.lock b/Gemfile.lock index 9cca8ef5d..75b939e83 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -169,6 +169,9 @@ GEM racc (~> 1.4) nokogiri (1.15.2-x86_64-linux) racc (~> 1.4) + paper_trail (16.0.0) + activerecord (>= 6.1) + request_store (~> 1.4) parallel (1.23.0) parser (3.2.2.3) ast (~> 2.4.1) @@ -229,6 +232,8 @@ GEM regexp_parser (2.8.1) reline (0.6.0) io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) @@ -312,6 +317,7 @@ PLATFORMS aarch64-linux arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x64-mingw-ucrt x86_64-linux @@ -327,6 +333,7 @@ DEPENDENCIES jwt (~> 2.7, >= 2.7.1) lingua mysql2 (~> 0.5.5) + paper_trail puma (~> 5.0) rack-cors rails (~> 8.0, >= 8.0.1) diff --git a/README.md b/README.md index 6c91b930b..5cc0a5534 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,12 @@ alt="IMAGE ALT TEXT HERE" width="560" height="315" border="10" /> ### Database Credentials - username: root - password: expertiza + +### Admin User Credentials +- username: admin2@example.com +- password: password123 + +### Logging in to Swagger UI +TO login to the Swagger UI, go to http://localhost:3002/api-docs/index.html and log in at +the Authentication /login API using the Admin credentials above. Then, use the given token in +Authorize at the top of the page. diff --git a/app/controllers/api/v1/participants_controller.rb b/app/controllers/api/v1/participants_controller.rb index 75f982f44..0996f9344 100644 --- a/app/controllers/api/v1/participants_controller.rb +++ b/app/controllers/api/v1/participants_controller.rb @@ -1,194 +1,215 @@ class Api::V1::ParticipantsController < ApplicationController include ParticipantsHelper + # autocomplete :user, :name + def index + participants = Participant.all + render json: participants + end - # Return a list of participants for a given user - # params - user_id - # GET /participants/user/:user_id - def list_user_participants - user = find_user if params[:user_id].present? - return if params[:user_id].present? && user.nil? - - participants = filter_user_participants(user) - - if participants.nil? - render json: participants.errors, status: :unprocessable_entity + def create + @participant = Participant.new(participant_params) + if @participant.save + render json: @participant, status: :created else - render json: participants, status: :ok + render json: { errors: @participant.errors.full_messages }, status: :unprocessable_entity end end - # Return a list of participants for a given assignment - # params - assignment_id - # GET /participants/assignment/:assignment_id - def list_assignment_participants - assignment = find_assignment if params[:assignment_id].present? - return if params[:assignment_id].present? && assignment.nil? + before_action :set_participant, only: %i[show update destroy] + - participants = filter_assignment_participants(assignment) + # GET /api/v1/participants/:id + def show + render json: @participant + end - if participants.nil? - render json: participants.errors, status: :unprocessable_entity + # getting the user by user_index and retrive and how on swagger ui + def user_index + participants = Participant.where(user_id: params[:user_id]) + if participants.empty? + #render json: participants, status: :not_found + render json: { error: "User not found" }, status: :not_found else - render json: participants, status: :ok + render json: participants, status: :ok end end - # Return a specified participant - # params - id - # GET /participants/:id - def show - participant = Participant.find(params[:id]) - - if participant.nil? - render json: participant.errors, status: :unprocessable_entity + # updating the participant by request body of example { "can_submit": true, "can_review": true} + def update + if @participant.update(participant_params) + render json: @participant, status: :ok else - render json: participant, status: :created + render json: { errors: @participant.errors.full_messages }, status: :unprocessable_entity end end - # Assign the specified authorization to the participant and add them to an assignment - # POST /participants/:authorization - def add - user = find_user - return unless user - - assignment = find_assignment - return unless assignment - - authorization = validate_authorization - return unless authorization - - permissions = retrieve_participant_permissions(authorization) - - participant = assignment.add_participant(user) - participant.authorization = authorization - participant.can_submit = permissions[:can_submit] - participant.can_review = permissions[:can_review] - participant.can_take_quiz = permissions[:can_take_quiz] - participant.can_mentor = permissions[:can_mentor] + # destroying the user by the id of the specific user + def destroy + participant = Participant.find(params[:id]) + participant.destroy + render json: { message: "Participant deleted successfully" }, status: :no_content + rescue ActiveRecord::RecordNotFound + render json: { error: "Participant Not Found" }, status: :not_found + end - if participant.save - render json: participant, status: :created + # finding partcipant by assignment id + def assignment_index + participants = Participant.where(assignment_id: params[:assignment_id]) + #render json: participants, status: :ok + if participants.empty? + #render json: participants, status: :not_found + render json: { error: "Assignment not found" }, status: :not_found else - render json: participant.errors, status: :unprocessable_entity + render json: participants, status: :ok end end - # Update the specified participant to the specified authorization - # PATCH /participants/:id/:authorization - def update_authorization - participant = find_participant - return unless participant + #adding a participant with authorization - authorization = validate_authorization - return unless authorization + def add + assignment = Assignment.find(params[:id]) + user = User.find_or_create_by(user_params) # 👈 You were probably missing this line + + # Now you can safely use `user` below + handle = "#{user.name.parameterize}-#{SecureRandom.hex(2)}" + + # Updates RoleContext with appropriate strategy for user + update_context(params[:authorization]) + permissions = @context.get_permissions + + participant = assignment.participants.create!( + user: user, + handle: handle, + can_submit: permissions[:can_submit], + can_review: permissions[:can_review], + can_take_quiz: permissions[:can_take_quiz], + can_mentor: permissions[:can_mentor] + ) + + render json: participant, status: :created + end - permissions = retrieve_participant_permissions(authorization) + # Updating authorization of the participants + def update_authorization + # Get participant via ID + participant = Participant.find(params[:id]) - participant.authorization = authorization - participant.can_submit = permissions[:can_submit] - participant.can_review = permissions[:can_review] - participant.can_take_quiz = permissions[:can_take_quiz] - participant.can_mentor = permissions[:can_mentor] + # Updates the RoleContext with the necessary strategy and obtains permissions + update_context(params[:authorization]) + permissions = @context.get_permissions - if participant.save - render json: participant, status: :created - else - render json: participant.errors, status: :unprocessable_entity - end - end + # Updates the participant's permissions to match + participant.update!( + can_submit: permissions[:can_submit], + can_review: permissions[:can_review], + can_take_quiz: permissions[:can_take_quiz], + can_mentor: permissions[:can_mentor] + ) - # Delete a participant - # params - id - # DELETE /participants/:id - def destroy - participant = Participant.find_by(id: params[:id]) - - if participant.nil? - render json: { error: 'Not Found' }, status: :not_found - elsif participant.destroy - successful_deletion_message = if params[:team_id].nil? - "Participant #{params[:id]} in Assignment #{params[:assignment_id]} has been deleted successfully!" - else - "Participant #{params[:id]} in Team #{params[:team_id]} of Assignment #{params[:assignment_id]} has been deleted successfully!" - end - render json: { message: successful_deletion_message }, status: :ok - else - render json: participant.errors, status: :unprocessable_entity - end + render json: participant, status: :ok end - # Permitted parameters for creating a Participant object - def participant_params - params.require(:participant).permit(:user_id, :assignment_id, :authorization, :can_submit, - :can_review, :can_take_quiz, :can_mentor, :handle, - :team_id, :join_team_request_id, :permission_granted, - :topic, :current_stage, :stage_deadline) + private + + def user_params + params.require(:user).permit(:name) end - private + def set_participant + @participant = Participant.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render json: { error: "Participant Not Found" }, status: 404 - # Filters participants based on the provided user - # Returns participants ordered by their IDs - def filter_user_participants(user) - participants = Participant.all - participants = participants.where(user_id: user.id) if user - participants.order(:id) end - # Filters participants based on the provided assignment - # Returns participants ordered by their IDs - def filter_assignment_participants(assignment) - participants = Participant.all - participants = participants.where(assignment_id: assignment.id) if assignment - participants.order(:id) + def participant_params + params.require(:participant).permit(:user_id, :assignment_id) end - # Finds a user based on the user_id parameter - # Returns the user if found - def find_user - user_id = params[:user_id] - user = User.find_by(id: user_id) - render json: { error: 'User not found' }, status: :not_found unless user - user + def controller_locale + locale_for_student end - # Finds an assignment based on the assignment_id parameter - # Returns the assignment if found - def find_assignment - assignment_id = params[:assignment_id] - assignment = Assignment.find_by(id: assignment_id) - render json: { error: 'Assignment not found' }, status: :not_found unless assignment - assignment + # Deletes participants from an assignment + def delete + contributor = AssignmentParticipant.find(params[:id]) + name = contributor.name + assignment_id = contributor.assignment + begin + contributor.destroy + flash[:note] = "\"#{name}\" is no longer a participant in this assignment." + rescue StandardError + flash[:error] = + "\"#{name}\" was not removed from this assignment. Please ensure that \"#{name}\" is not a reviewer or metareviewer and try again." + end + redirect_to controller: 'review_mapping', action: 'list_mappings', id: assignment_id end - # Finds a participant based on the id parameter - # Returns the participant if found - def find_participant - participant_id = params[:id] - participant = Participant.find_by(id: participant_id) - render json: { error: 'Participant not found' }, status: :not_found unless participant - participant + # A ‘copyright grant’ means the author has given permission to the instructor to use the work outside the course. + # This is incompletely implemented, but the values in the last column in http://expertiza.ncsu.edu/student_task/list are sourced from here. + def view_copyright_grants + assignment_id = params[:id] + assignment = Assignment.find(assignment_id) + @assignment_name = assignment.name + @has_topics = false + @teams_info = [] + teams = Team.where(parent_id: assignment_id) + teams.each do |team| + team_info = {} + team_info[:name] = team.name(session[:ip]) + users = [] + team.users { |team_user| users.append(get_user_info(team_user, assignment)) } + team_info[:users] = users + @has_topics = get_signup_topics_for_assignment(assignment_id, team_info, team.id) + team_without_topic = SignedUpTeam.where('team_id = ?', team.id).none? + next if @has_topics && team_without_topic + + @teams_info.append(team_info) + end + @teams_info = @teams_info.sort_by { |hashmap| [hashmap[:topic_id] ? 0 : 1, hashmap[:topic_id] || 0] } end - # Validates that the authorization parameter is present and is one of the following valid authorizations: reader, reviewer, submitter, mentor - # Returns the authorization if valid - def validate_authorization - valid_authorizations = %w[reader reviewer submitter mentor] - authorization = params[:authorization] - authorization = authorization.downcase if authorization.present? + private - unless authorization - render json: { error: 'authorization is required' }, status: :unprocessable_entity - return - end + # Private method that ensures that the context is initialized and updates + # The strategy being used by the context given the + def update_context(role) + # Creates new RoleContext if one does not already exist + @context = RoleContext.new if @context.nil? + # Sets the assigned strategy for the context + @context.set_strategy_by_role(role) + end - unless valid_authorizations.include?(authorization) - render json: { error: 'authorization not valid. Valid authorizations are: Reader, Reviewer, Submitter, Mentor' }, - status: :unprocessable_entity - return + # Get the user info from the team user + def get_user_info(team_user, assignment) + user = {} + user[:name] = team_user.name + user[:fullname] = team_user.fullname + # set by default + permission_granted = false + assignment.participants.each do |participant| + permission_granted = participant.permission_granted? if team_user.id == participant.user.id end + # If permission is granted, set the publisting rights string + user[:pub_rights] = permission_granted ? 'Granted' : 'Denied' + user[:verified] = false + user + end - authorization + # Get the signup topics for the assignment + def get_signup_topics_for_assignment(assignment_id, team_info, team_id) + signup_topics = SignUpTopic.where('assignment_id = ?', assignment_id) + if signup_topics.any? + has_topics = true + signup_topics.each do |signup_topic| + signup_topic.signed_up_teams.each do |signed_up_team| + if signed_up_team.team_id == team_id + team_info[:topic_name] = signup_topic.topic_name + team_info[:topic_id] = signup_topic.topic_identifier.to_i + end + end + end + end + has_topics end end diff --git a/app/helpers/participants_helper.rb b/app/helpers/participants_helper.rb index 6a3e7011a..6116e4f55 100644 --- a/app/helpers/participants_helper.rb +++ b/app/helpers/participants_helper.rb @@ -25,4 +25,9 @@ def retrieve_participant_permissions(authorization) default_permissions.merge(permissions_map[authorization]) end + + +# def generate() +# raise NotImplementedError("Participant Factory not running yet.") +# end end diff --git a/app/models/Strategies/roles/mentor_strategy.rb b/app/models/Strategies/roles/mentor_strategy.rb new file mode 100644 index 000000000..79c76719b --- /dev/null +++ b/app/models/Strategies/roles/mentor_strategy.rb @@ -0,0 +1,20 @@ +# Strategy class for mentors; inherits from RoleStrategy +class MentorStrategy < RoleStrategy + + def validate_permissions + raise NotImplementedError("Not implemented") + end + + # Returns the permissions associated with the Mentor role in the + # form of a dictionary + # @return dictionary containing permissions + def get_permissions + return { + can_submit: false, + can_review: false, + can_take_quiz: false, + can_mentor: true + } + end + +end diff --git a/app/models/Strategies/roles/reviewer_strategy.rb b/app/models/Strategies/roles/reviewer_strategy.rb new file mode 100644 index 000000000..0729800ac --- /dev/null +++ b/app/models/Strategies/roles/reviewer_strategy.rb @@ -0,0 +1,20 @@ +# Strategy class for reviewers; inherits from RoleStrategy +class ReviewerStrategy < RoleStrategy + + def validate_permissions + raise NotImplementedError("Not implemented") + end + + # Returns the permissions associated with the Reviewer role in the + # form of a dictionary + # @return dictionary containing permissions + def get_permissions + return { + can_submit: false, + can_review: true, + can_take_quiz: false, + can_mentor: false + } + end + +end diff --git a/app/models/Strategies/roles/role_context.rb b/app/models/Strategies/roles/role_context.rb new file mode 100644 index 000000000..27ac3e68e --- /dev/null +++ b/app/models/Strategies/roles/role_context.rb @@ -0,0 +1,51 @@ +# A Context class that is able to execute role strategy methods from clients +class RoleContext + + # The RoleStrategy currently being used + attr_reader :strategy + + # Unimplemented + def validate_permissions + raise NotImplementedError + end + + # Returns the permissions associated with the role of the current strategy, + # but will return nil if there is no strategy set + # + # @return associated permissions for a given role; nil if no strategy + def get_permissions + @strategy ? return @strategy.get_permissions : return nil + end + + # Sets a strategy to the RoleContext for use with other functions + # + # @param [RoleStrategy] strategy that is to be set + def set_strategy(strategy) + @strategy = strategy + end + + # Sets a strategy via the role of the participant (param[:authorization]). This + # strategy can then be used for other functions that are a part of this. The + # function returns a boolean that signifies the success of the function. + # + # @param [String] role is the authorization string provided to identify roles + # @return true if strategy is successful, else false + def set_strategy_by_role(role) + # Get proper strategy given the role provided + case role + when 'Student' + @strategy = StudentStrategy.new + when 'Reviewer' + @strategy = ReviewerStrategy.new + when 'Teaching Assistant' + @strategy = TeacherAssistantStrategy.new + when 'Mentor' + @strategy = MentorStrategy.new + else + return false + end + # Returns true if the strategy is successsfully set + return true; + end + +end diff --git a/app/models/Strategies/roles/role_strategy.rb b/app/models/Strategies/roles/role_strategy.rb new file mode 100644 index 000000000..16bbab8ae --- /dev/null +++ b/app/models/Strategies/roles/role_strategy.rb @@ -0,0 +1,24 @@ +# This module's main purpose is to handle any validation for permissions related to roles +# that have been established within Expertiza. +# +# E2511: Currently, this class is meant to replace an old implementation requiring the +# roles to have its permissions be explicity defined in participants_helper via a large +# set of boolean values. This implementation seeks to reimplement this using a Strategy +# Design Pattern where RoleContext uses any relevant strategies to execute necessary +# functions related to a given Role. This can be extended in the future to include +# other Role functions if necessary as well +module RoleStrategy + + # This should validate whether or not a given role is able to access a certain + # functionality or not. + # @raise [NotImplementedError] if the function is not implemented + def validate_permissions + raise NotImplementedError("Classes that inherit from RoleStrategy must implement validate_permissions") + end + + # This should return a dictionary of permissions allocated to a given role + # @raise [NotImplementedError] if the function is not implemented + def get_permissions + raise NotImplementedError("Classes that inherit from RoleStrategy must implement validate_permissions") + end +end diff --git a/app/models/Strategies/roles/student_strategy.rb b/app/models/Strategies/roles/student_strategy.rb new file mode 100644 index 000000000..6b305a181 --- /dev/null +++ b/app/models/Strategies/roles/student_strategy.rb @@ -0,0 +1,20 @@ +# Strategy class for students; inherits from RoleStrategy +class StudentStrategy < RoleStrategy + + def validate_permissions + raise NotImplementedError("Not implemented") + end + + # Returns the permissions associated with the Student role in the + # form of a dictionary + # @return dictionary containing permissions + def get_permissions + return { + can_submit: true, + can_review: false, + can_take_quiz: false, + can_mentor: false + } + end + +end diff --git a/app/models/Strategies/roles/teacher_assistant_strategy.rb b/app/models/Strategies/roles/teacher_assistant_strategy.rb new file mode 100644 index 000000000..2be647b9d --- /dev/null +++ b/app/models/Strategies/roles/teacher_assistant_strategy.rb @@ -0,0 +1,20 @@ +# Strategy class for TAs; inherits from RoleStrategy +class TeacherAssistantStrategy < RoleStrategy + + def validate_permissions + raise NotImplementedError("Not implemented") + end + + # Returns the permissions associated with the TA role in the + # form of a dictionary + # @return dictionary containing permissions + def get_permissions + return { + can_submit: false, + can_review: true, + can_take_quiz: false, + can_mentor: true + } + end + +end diff --git a/app/models/role.rb b/app/models/role.rb index 4941e300d..cd554e816 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -47,6 +47,7 @@ def subordinate_roles_and_self # checks if the current role has all the privileges of the target role def all_privileges_of?(target_role) + return false if target_role.nil? || name.nil? privileges = { 'Student' => 1, 'Teaching Assistant' => 2, diff --git a/config/routes.rb b/config/routes.rb index 33df803e7..b27249510 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -110,14 +110,24 @@ end end - resources :participants do + # resources :participants, only: [:index, :show, :create, :update, :destroy] do + # collection do + # get '/user/:user_id', to: 'participants#user_index' + # get '/assignment/:assignment_id', to: 'participants#assignment_index' + # get '/:id', to: 'participants#show' + # post '/:authorization', to: 'participants#add' + # patch '/:id/:authorization', to: 'participants#update_authorization' + # delete '/:id', to: 'participants#destroy' + # end + # end + resources :participants, only: [:index, :show, :create, :update, :destroy] 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' + get '/user/:user_id', to: 'participants#user_index' + get '/assignment/:assignment_id', to: 'participants#assignment_index' + end + member do post '/:authorization', to: 'participants#add' - patch '/:id/:authorization', to: 'participants#update_authorization' - delete '/:id', to: 'participants#destroy' + patch '/:authorization', to: 'participants#update_authorization' end end end diff --git a/db/seeds.rb b/db/seeds.rb index b6de376f2..f154d9359 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,127 +1,138 @@ begin - #Create an instritution - inst_id = Institution.create!( - name: 'North Carolina State University', - ).id - - # Create an admin user - User.create!( - name: 'admin', - email: 'admin2@example.com', - password: 'password123', - full_name: 'admin admin', - institution_id: 1, - role_id: 1 + puts "Starting database seeding..." + + # Ensure an institution exists + inst = Institution.find_or_create_by!(name: 'North Carolina State University') + inst_id = inst.id + puts "Institution created with ID: #{inst_id}" + + # Ensure roles exist + admin_role = Role.find_or_create_by!(id: 1, name: "Admin") + instructor_role = Role.find_or_create_by!(id: 3, name: "Instructor") + student_role = Role.find_or_create_by!(id: 5, name: "Student") + puts "Roles ensured: Admin (#{admin_role.id}), Instructor (#{instructor_role.id}), Student (#{student_role.id})" + + # Create an admin user + admin_user = User.create!( + name: 'admin', + email: 'admin2@example.com', + password: 'password123', # Rails will automatically hash this + full_name: 'admin admin', + institution_id: inst_id, + role_id: admin_role.id + ) + puts "Admin user created with ID: #{admin_user.id}" + + # 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.create!( + name: Faker::Internet.unique.username, + email: Faker::Internet.unique.email, + password: "password", # Rails will hash the password + full_name: Faker::Name.name, + institution_id: inst_id, + role_id: instructor_role.id + ) + instructor_user_ids << instructor.id + puts "Created Instructor ID: #{instructor.id}" + end + + puts "Creating courses..." + course_ids = [] + num_courses.times do |i| + course = 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 + ) + course_ids << course.id + puts "Created Course ID: #{course.id}" + end + + puts "Creating assignments..." + assignment_ids = [] + num_assignments.times do |i| + assignment = 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 + ) + assignment_ids << assignment.id + puts "Created Assignment ID: #{assignment.id}" + end + + puts "Creating teams..." + team_ids = [] + num_teams.times do |i| + team = Team.create!( + assignment_id: assignment_ids[i % num_assignments] + ) + team_ids << team.id + puts "Created Team ID: #{team.id}" + end + + puts "Creating students..." + student_user_ids = [] + num_students.times do + student = User.create!( + name: Faker::Internet.unique.username, + email: Faker::Internet.unique.email, + password: "password", # Rails will hash the password + full_name: Faker::Name.name, + institution_id: inst_id, + role_id: student_role.id + ) + student_user_ids << student.id + puts "Created Student ID: #{student.id}" + end + + puts "Assigning students to teams..." + teams_users_ids = [] + num_students.times do |i| + team_id = team_ids[i % num_teams] + user_id = student_user_ids[i] + + puts "Creating TeamsUser with team_id: #{team_id}, user_id: #{user_id}" + teams_user = TeamsUser.create!( + team_id: team_id, + user_id: user_id ) - - - #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 << Team.create( - assignment_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, - ).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 << Participant.create( - user_id: student_user_ids[i], - assignment_id: assignment_ids[i%num_assignments], - team_id: team_ids[i%num_teams], - ).id - end - - - - - + teams_users_ids << teams_user.id + puts "Created TeamsUser with ID: #{teams_user.id}" + end + + puts "Assigning participants to students, teams, courses, and assignments..." + participant_ids = [] + num_students.times do |i| + user_id = student_user_ids[i] + assignment_id = assignment_ids[i % num_assignments] + team_id = team_ids[i % num_teams] + + participant = Participant.create!( + user_id: user_id, + assignment_id: assignment_id, + team_id: team_id + ) + participant_ids << participant.id + puts "Created Participant ID: #{participant.id}" + end + puts "Database seeding complete! ✅" rescue ActiveRecord::RecordInvalid => e - puts 'The db has already been seeded' -end + puts "❌ Seeding failed: #{e.message}" +end \ No newline at end of file diff --git a/spec/factories/participants.rb b/spec/factories/participants.rb new file mode 100644 index 000000000..679e8995e --- /dev/null +++ b/spec/factories/participants.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :participant do + association :user + association :assignment + can_submit { false } + can_review { false } + can_take_quiz { false } + can_mentor { false } + end +end \ No newline at end of file diff --git a/spec/models/strategies/roles/role_context_spec.rb b/spec/models/strategies/roles/role_context_spec.rb new file mode 100644 index 000000000..f7864028e --- /dev/null +++ b/spec/models/strategies/roles/role_context_spec.rb @@ -0,0 +1,10 @@ +describe RoleContext do +end + describe '#get_permissions' do + end + describe '#set_strategy' do + end + describe '#set_strategy_by_role' do + end + describe '#validate_permissions' do + end diff --git a/spec/requests/api/v1/participants_controller_spec.rb b/spec/requests/api/v1/participants_controller_spec.rb index d974640a3..17537c239 100644 --- a/spec/requests/api/v1/participants_controller_spec.rb +++ b/spec/requests/api/v1/participants_controller_spec.rb @@ -1,342 +1,181 @@ require 'swagger_helper' -require 'json_web_token' +require 'rails_helper' RSpec.describe 'Participants API', type: :request do before(:all) do - @roles = create_roles_hierarchy - end - - let(:studenta) do - User.create!( - name: "studenta", - password_digest: "password", - role_id: @roles[:student].id, - full_name: "Student A", - email: "testuser@example.com" - ) - end - - let(:studentb) do - User.create!( - name: "studentb", - password_digest: "password", - role_id: @roles[:student].id, - full_name: "Student B", - email: "testuser@example.com" - ) - end - - let!(:instructor) do - User.create!( - name: "Instructor", - password_digest: "password", - role_id: @roles[:instructor].id, - full_name: "Instructor Name", - email: "instructor@example.com" - ) - end - - let!(:assignment1) { Assignment.create!(name: 'Test Assignment 1', instructor_id: instructor.id) } - let!(:assignment2) { Assignment.create!(name: 'Test Assignment 2', instructor_id: instructor.id) } - let!(:participant1) { Participant.create!(id: 1, user_id: studenta.id, assignment_id: assignment1.id) } - let!(:participant2) { Participant.create!(id: 2, user_id: studenta.id, assignment_id: assignment2.id) } - - let(:token) { JsonWebToken.encode({id: studenta.id}) } - let(:Authorization) { "Bearer #{token}" } - - path '/api/v1/participants/user/{user_id}' do - get 'Retrieve participants for a specific user' do - tags 'Participants' - produces 'application/json' - - parameter name: :user_id, in: :path, type: :integer, description: 'ID of the user' - - response '200', 'Returns participants' do - let(:user_id) { studenta.id } - - run_test! do |response| - data = JSON.parse(response.body) - participant = data[0] - expect(participant).to be_a(Hash) - expect(participant['id']).to eq(participant1.id) - expect(participant['user_id']).to eq(studenta.id) - expect(participant['assignment_id']).to eq(assignment1.id) - end - end - - response '200', 'Participant not found with user_id' do - let(:user_id) { instructor.id } - - run_test! do |response| - data = JSON.parse(response.body) - expect(data).to be_an(Array) - expect(data).to be_empty - end - end - - response '404', 'User Not Found' do - let(:user_id) { 99 } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('User not found') - end - end - - response '401', 'Unauthorized' do - let(:user_id) { 1 } - let(:'Authorization') { 'Bearer invalid_token' } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('Not Authorized') - end - end + # Log in and retrieve the token once before all tests + post '/login', params: { user_name: 'admin2@example.com', password: 'password123' } + expect(response.status).to eq(200) + @token = JSON.parse(response.body)['token'] end - end - - path '/api/v1/participants/assignment/{assignment_id}' do - get 'Retrieve participants for a specific assignment' do - tags 'Participants' - produces 'application/json' - - parameter name: :assignment_id, in: :path, type: :integer, description: 'ID of the assignment' - - response '200', 'Returns participants' do - let(:assignment_id) { assignment1.id } - - run_test! do |response| - data = JSON.parse(response.body) - participant = data[0] - expect(participant).to be_a(Hash) - expect(participant['id']).to eq(participant1.id) - expect(participant['user_id']).to eq(studenta.id) - expect(participant['assignment_id']).to eq(assignment1.id) - end - end - - response '404', 'Assignment Not Found' do - let(:assignment_id) { 99 } - # let(:'Authorization') { "Bearer #{@token}" } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('Assignment not found') - end - end - - response '401', 'Unauthorized' do - let(:assignment_id) { 2 } - let(:'Authorization') { 'Bearer invalid_token' } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('Not Authorized') + + let(:valid_headers) { { 'Authorization' => "Bearer #{@token}" } } + + + path '/api/v1/participants/user/{user_id}' do + get 'Retrieve participants for a specific user' do + tags 'Participants' + produces 'application/json' + + parameter name: :user_id, in: :path, type: :integer, description: 'ID of the user' + parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token' + + # Checks if class returns a participant with given user_id if they exist + response '200', 'Returns participants' do + let(:user_id) { 4 } + let(:'Authorization') { "Bearer #{@token}" } + + run_test! do |response| + data = JSON.parse(response.body) + participant = data[0] + expect(participant).to be_a(Hash) + expect(participant['id']).to eq(1) + expect(participant['user_id']).to eq(4) + expect(participant['assignment_id']).to eq(1) + end + end + + # Checks that a 404 Not Found Error is given if participant with user_id is not found + response '404', 'User Not Found' do + let(:user_id) { 99 } + let(:'Authorization') { "Bearer #{@token}" } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eql('User not found') + end + end + + response '401', 'Unauthorized' do + let(:user_id) { 1 } + let(:'Authorization') { 'Bearer invalid_token' } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eql('Not Authorized') + end end end end - end - - path '/api/v1/participants/{id}' do - get 'Retrieve a specific participant' do - tags 'Participants' - produces 'application/json' - - parameter name: :id, in: :path, type: :integer, description: 'ID of the participant' - - response '201', 'Returns a participant' do - let(:id) { participant2.id } - run_test! do |response| - data = JSON.parse(response.body) - expect(data['user_id']).to eq(studenta.id) - expect(data['assignment_id']).to eq(assignment2.id) - end - end - - response '404', 'Participant not found' do - let(:id) { 99 } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('Not Found') - end - end - - response '401', 'Unauthorized' do - let(:id) { 2 } - let(:'Authorization') { 'Bearer invalid_token' } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('Not Authorized') + path '/api/v1/participants/assignment/{assignment_id}' do + get 'Retrieve participants for a specific assignment' do + tags 'Participants' + produces 'application/json' + + parameter name: :assignment_id, in: :path, type: :integer, description: 'ID of the assignment' + parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token' + + response '200', 'Returns participants' do + let(:assignment_id) { 2 } + let(:'Authorization') { "Bearer #{@token}" } + + run_test! do |response| + data = JSON.parse(response.body) + participant = data[0] + expect(participant).to be_a(Hash) + expect(participant['id']).to eq(2) + expect(participant['user_id']).to eq(5) + expect(participant['assignment_id']).to eq(2) + end + end + + # Checks that a 404 Not Found Error is given if participant with assignment_id is not found + response '404', 'Assignment Not Found' do + let(:assignment_id) { 99 } + let(:'Authorization') { "Bearer #{@token}" } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eql('Assignment not found') + end + end + + # Checks if endpoint checks for bearer token authorization + response '401', 'Unauthorized' do + let(:assignment_id) { 2 } + let(:'Authorization') { 'Bearer invalid_token' } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eql('Not Authorized') + end end end end - delete 'Delete a specific participant' do - tags 'Participants' - parameter name: :id, in: :path, type: :integer, description: 'ID of the participant' - - response '200', 'Participant deleted' do - let(:id) { participant2.id } - - run_test! do |response| - expect(JSON.parse(response.body)['message']).to include('Participant') - end - end - - response '404', 'Participant not found' do - let(:id) { 99 } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('Not Found') - end - end - - response '401', 'Unauthorized' do - let(:id) { 2 } - let(:'Authorization') { 'Bearer invalid_token' } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('Not Authorized') + path '/api/v1/participants/{id}' do + get 'Retrieve a specific participant' do + tags 'Participants' + produces 'application/json' + + parameter name: :id, in: :path, type: :integer, description: 'ID of the participant' + parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token' + + response '200', 'Returns a participant' do + let(:id) { 2 } + let(:'Authorization') { "Bearer #{@token}" } + + run_test! do |response| + data = JSON.parse(response.body) + expect(data['user_id']).to eq(5) + expect(data['assignment_id']).to eq(2) + end + end + + + response '404', 'Participant not found' do + let(:id) { 99 } + let(:'Authorization') { "Bearer #{@token}" } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eql('Participant Not Found') + end + end + + response '401', 'Unauthorized' do + let(:id) { 2 } + let(:'Authorization') { 'Bearer invalid_token' } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eql('Not Authorized') + end + end + end + + + delete 'Delete a specific participant' do + tags 'Participants' + parameter name: :id, in: :path, type: :integer, description: 'ID of the participant' + parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token' + + # Test deleting a participant + response '204', 'Participant deleted' do + let(:id) { 2 } + let(:'Authorization') { "Bearer #{@token}" } + + run_test! do |response| + #expect(JSON.parse(response.body)['message']).to eql('') + expect(response.body).to be_empty + end + end + + + response '404', 'Participant not found' do + let(:id) { 99 } + let(:'Authorization') { "Bearer #{@token}" } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eql('Participant Not Found') + end + end + + response '401', 'Unauthorized' do + let(:id) { 2 } + let(:'Authorization') { 'Bearer invalid_token' } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eql('Not Authorized') + end end end end - end - - path '/api/v1/participants/{id}/{authorization}' do - patch 'Update participant authorization' do - tags 'Participants' - consumes 'application/json' - produces 'application/json' - - parameter name: :id, in: :path, type: :integer, description: 'ID of the participant' - parameter name: :authorization, in: :path, type: :string, description: 'New authorization' - parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token' - - response '201', 'Participant updated' do - let(:id) { 2 } - let(:authorization) { 'mentor' } - - run_test! do |response| - data = JSON.parse(response.body) - expect(data['authorization']).to eq('mentor') - end - end - - response '404', 'Participant not found' do - let(:id) { 99 } - let(:authorization) { 'mentor' } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('Participant not found') - end - end - response '404', 'Participant not found' do - let(:id) { 99 } - let(:authorization) { 'teacher' } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('Participant not found') - end - end - - response '422', 'Authorization not found' do - let(:id) { 1 } - let(:authorization) { 'teacher' } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('authorization not valid. Valid authorizations are: Reader, Reviewer, Submitter, Mentor') - end - end - - response '401', 'Unauthorized' do - let(:id) { 2 } - let(:authorization) { 'mentor' } - let(:'Authorization') { 'Bearer invalid_token' } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('Not Authorized') - end - end - end end - path '/api/v1/participants/{authorization}' do - post 'Add a participant' do - tags 'Participants' - consumes 'application/json' - produces 'application/json' - - parameter name: :authorization, in: :path, type: :string, description: 'Authorization level (Reader, Reviewer, Submitter, Mentor)' - parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token' - parameter name: :participant, in: :body, schema: { - type: :object, - properties: { - user_id: { type: :integer, description: 'ID of the user' }, - assignment_id: { type: :integer, description: 'ID of the assignment' } - }, - required: %w[user_id assignment_id] - } - - response '201', 'Participant successfully added' do - let(:authorization) { 'mentor' } - let(:participant) { { user_id: studentb.id, assignment_id: assignment2.id } } - - run_test! do |response| - data = JSON.parse(response.body) - expect(data['user_id']).to eq(studentb.id) - expect(data['assignment_id']).to eq(assignment2.id) - expect(data['authorization']).to eq('mentor') - end - end - - def fetch_username(user_id) - User.find(user_id).name - end - - response '500', 'Participant already exist' do - let(:authorization) { 'mentor' } - let(:participant) { { user_id: studenta.id, assignment_id: assignment1.id } } - let(:name) { User.find(participant[:user_id]).name } - - run_test! do |response| - - expect(JSON.parse(response.body)['exception']).to eq("#") - end - end - - response '404', 'User not found' do - let(:authorization) { 'mentor' } - let(:participant) { { user_id: 99, assignment_id: 1 } } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eq('User not found') - end - end - - response '404', 'Assignment not found' do - let(:authorization) { 'mentor' } - let(:participant) { { user_id: studenta.id, assignment_id: 99 } } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eq('Assignment not found') - end - end - - response '422', 'Authorization not found' do - let(:authorization) { 'teacher' } - let(:participant) { { user_id: studentb.id, assignment_id: assignment1.id } } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to eql('authorization not valid. Valid authorizations are: Reader, Reviewer, Submitter, Mentor') - end - end - - response '422', 'Invalid authorization' do - let(:authorization) { 'invalid_auth' } - let(:participant) { { user_id: studentb.id, assignment_id: assignment1.id } } - - run_test! do |response| - expect(JSON.parse(response.body)['error']).to include('authorization not valid') - end - end - end - end -end \ No newline at end of file diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index de8081625..dfdf14c1f 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -12,6 +12,167 @@ components: security: - bearerAuth: [] paths: + "/api/v1/participants": + get: + summary: List all participants + tags: + - Participants + responses: + '200': + description: A list of participants + post: + summary: Create a new participant + tags: + - Participants + requestBody: + content: + application/json: + schema: + type: object + properties: + user_id: + type: integer + assignment_id: + type: integer + required: + - user_id + - assignment_id + responses: + '201': + description: Participant created + '422': + description: Unprocessable entity + + "/api/v1/participants/{id}": + parameters: + - name: id + in: path + required: true + schema: + type: integer + get: + summary: Retrieve a participant by ID + tags: + - Participants + responses: + '200': + description: Returns the participant details + '404': + description: Participant not found + put: + summary: Update a participant + tags: + - Participants + requestBody: + content: + application/json: + schema: + type: object + properties: + can_submit: + type: boolean + can_review: + type: boolean + required: + - can_submit + - can_review + responses: + '200': + description: Participant updated + '422': + description: Unprocessable entity + delete: + summary: Delete a participant + tags: + - Participants + responses: + '204': + description: Participant deleted + + "/api/v1/participants/user/{user_id}": + parameters: + - name: user_id + in: path + required: true + schema: + type: integer + get: + summary: Get participants by user ID + tags: + - Participants + responses: + '200': + description: List of participants for the user + + "/api/v1/participants/assignment/{assignment_id}": + parameters: + - name: assignment_id + in: path + required: true + schema: + type: integer + get: + summary: Get participants by assignment ID + tags: + - Participants + responses: + '200': + description: List of participants for the assignment + + "/api/v1/participants/{id}/{authorization}": + parameters: + - name: id + in: path + required: true + schema: + type: integer + - name: authorization + in: path + required: true + schema: + type: string + post: + summary: Add a participant with authorization + tags: + - Participants + requestBody: + content: + application/json: + schema: + type: object + properties: + user: + type: object + properties: + name: + type: string + required: + - user + responses: + '201': + description: Participant added successfully + '404': + description: User not found + patch: + summary: Update participant authorization + tags: + - Participants + requestBody: + content: + application/json: + schema: + type: object + properties: + can_submit: + type: boolean + can_review: + type: boolean + responses: + '200': + description: Authorization updated + '422': + description: Unprocessable entity + # ******* "/api/v1/account_requests/pending": get: summary: List all Pending Account Requests