From 13a0fdc5c7983b9b078c5b16d458a57a7a66ec2e Mon Sep 17 00:00:00 2001 From: Ameya Vaichalkar <57044378+ameyagv@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:29:00 -0400 Subject: [PATCH] E2424 project (#122) * Added instructions for creating test objects in SQL server and starting up backend services * bookmark scaffold files created * add student_task model and its tests * improve student_task test * Re-scaffolded student task controller, generated db migrations, implemented basic list function and got basic JSON response functionality working * Reverted creation of student task table * Un-commented seed file to properly seed database * Added view function in student_tasks_controller.rb and cleaned up the file * Updated view function * add course seed * Added permission granted flag to participants * Removed comments and simplified code for student task controller and model. * Added permission granted to student task model rspec test, reformatted file a bit * Got initial authenticated/unauthenticated rspec tests working for student task controller. * add course to seeds.rb * Removed set student method throwing errors * Added view function route for student_task_controller * add Student_task list in SwaggerUI * Create bookmarks_controller * crud ops * add score related functions for bookmark * Create bookmarks_controller_spec.rb * Adding setup.sh to automate the setup inside containers (#84) * Adding files to setup * Updating setup script with working seed file * Adding a setup script to try to automate setup * Adding setup.sh as entrypoint * add db creation initilization script --------- Co-authored-by: Muhammad Ali Qureshi Co-authored-by: ameyagv * deleted unnecessary files * modified sign_up_topic * removed gemfile.lock and schema.rb * added gemfile.lock and schema.rb * changes bookmarks controller * Bolstered test suite for controller list function and improved readability * added bookmark rating functions * added bookmark controller tests * add swaggerUI * update student_task model * schema update * swagger config update * add view function * delete view in SwaggerUI as it is out of scope * add commits to student_task.rb * Added comments for student task controller * add SwaggerUI view * resovle sign_up_topic migration * update schema.rb --------- Co-authored-by: hmckinn Co-authored-by: Akshat Nitin Savla Co-authored-by: Yunfei Chen Co-authored-by: david12white <45209118+david12white@users.noreply.github.com> Co-authored-by: David White Co-authored-by: ychen-207523 <125923497+ychen-207523@users.noreply.github.com> Co-authored-by: Mitanshu Reshamwala <42906426+MitanshuShaBa@users.noreply.github.com> Co-authored-by: Mitanshu Reshamwala Co-authored-by: vyshnavi-adusumelli <35067896+vyshnavi-adusumelli@users.noreply.github.com> Co-authored-by: Ali Qureshi <61347679+qureshi-ali@users.noreply.github.com> Co-authored-by: Muhammad Ali Qureshi Co-authored-by: Yunfei Chen --- Gemfile.lock | 3 + .../api/v1/student_tasks_controller.rb | 25 ++++ app/models/student_task.rb | 48 +++++++ config/routes.rb | 6 + ..._add_permission_granted_to_participants.rb | 2 +- .../20240415155554_create_student_tasks.rb | 13 ++ ...40415163413_add_columns_to_participants.rb | 7 + ...20240415192048_drop_student_task_tabkle.rb | 5 + db/schema.rb | 7 +- spec/factories.rb | 8 ++ spec/models/student_task_spec.rb | 83 ++++++++++++ .../api/v1/student_tasks_controller_spec.rb | 125 ++++++++++++++++++ spec/routing/student_tasks_routing_spec.rb | 30 +++++ swagger/v1/swagger.yaml | 28 ++++ 14 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 app/controllers/api/v1/student_tasks_controller.rb create mode 100644 app/models/student_task.rb create mode 100644 db/migrate/20240415155554_create_student_tasks.rb create mode 100644 db/migrate/20240415163413_add_columns_to_participants.rb create mode 100644 db/migrate/20240415192048_drop_student_task_tabkle.rb create mode 100644 spec/models/student_task_spec.rb create mode 100644 spec/requests/api/v1/student_tasks_controller_spec.rb create mode 100644 spec/routing/student_tasks_routing_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 807f4defa..cb84960a2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -130,6 +130,8 @@ GEM nio4r (2.5.9) nokogiri (1.15.2-aarch64-linux) racc (~> 1.4) + nokogiri (1.15.2-arm64-darwin) + racc (~> 1.4) nokogiri (1.15.2-x64-mingw-ucrt) racc (~> 1.4) nokogiri (1.15.2-x86_64-linux) @@ -243,6 +245,7 @@ GEM PLATFORMS aarch64-linux + arm64-darwin-22 x64-mingw-ucrt x86_64-linux diff --git a/app/controllers/api/v1/student_tasks_controller.rb b/app/controllers/api/v1/student_tasks_controller.rb new file mode 100644 index 000000000..6eb784b73 --- /dev/null +++ b/app/controllers/api/v1/student_tasks_controller.rb @@ -0,0 +1,25 @@ +class Api::V1::StudentTasksController < ApplicationController + + # List retrieves all student tasks associated with the current logged-in user. + def list + # Retrieves all tasks that belong to the current user. + @student_tasks = StudentTask.from_user(current_user) + # Render the list of student tasks as JSON. + render json: @student_tasks, status: :ok + end + + def show + render json: @student_task, status: :ok + end + + # The view function retrieves a student task based on a participant's ID. + # It is meant to provide an endpoint where tasks can be queried based on participant ID. + def view + # Retrieves the student task where the participant's ID matches the provided parameter. + # This function will be used for clicking on a specific student task to "view" its details. + @student_task = StudentTask.from_participant_id(params[:id]) + # Render the found student task as JSON. + render json: @student_task, status: :ok + end + +end diff --git a/app/models/student_task.rb b/app/models/student_task.rb new file mode 100644 index 000000000..a69316f22 --- /dev/null +++ b/app/models/student_task.rb @@ -0,0 +1,48 @@ +class StudentTask + attr_accessor :assignment, :current_stage, :participant, :stage_deadline, :topic, :permission_granted + + # Initializes a new instance of the StudentTask class + def initialize(args) + @assignment = args[:assignment] + @current_stage = args[:current_stage] + @participant = args[:participant] + @stage_deadline = args[:stage_deadline] + @topic = args[:topic] + @permission_granted = args[:permission_granted] + end + + # create a new StudentTask instance from a Participant object.cccccccc + def self.create_from_participant(participant) + new( + assignment: participant.assignment.name, # Name of the assignment associated with the student task + topic: participant.topic, # Current stage of the assignment process + current_stage: participant.current_stage, # Participant object + stage_deadline: parse_stage_deadline(participant.stage_deadline), # Deadline for the current stage of the assignment + permission_granted: participant.permission_granted, # Topic of the assignment + participant: participant # Boolean indicating if Publishing Rights is enabled + ) + end + + + # create an array of StudentTask instances for all participants linked to a user, sorted by deadline. + def self.from_user(user) + Participant.where(user_id: user.id) + .map { |participant| StudentTask.create_from_participant(participant) } + .sort_by(&:stage_deadline) + end + + # create a StudentTask instance from a participant of the provided id + def self.from_participant_id(id) + create_from_participant(Participant.find_by(id: id)) + end + + private + + # Parses a date string to a Time object, if parsing fails, set the time to be one year after current + def self.parse_stage_deadline(date_string) + Time.parse(date_string) + rescue StandardError + Time.now + 1.year + end + +end diff --git a/config/routes.rb b/config/routes.rb index 134c56878..fc8a710a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,6 +46,12 @@ 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 diff --git a/db/migrate/20231028012101_add_permission_granted_to_participants.rb b/db/migrate/20231028012101_add_permission_granted_to_participants.rb index f704fc300..ee64f4407 100644 --- a/db/migrate/20231028012101_add_permission_granted_to_participants.rb +++ b/db/migrate/20231028012101_add_permission_granted_to_participants.rb @@ -1,5 +1,5 @@ class AddPermissionGrantedToParticipants < ActiveRecord::Migration[7.0] def change - add_column :participants, :permission_granted, :boolean + add_column :participants, :permission_granted, :boolean, default: false end end diff --git a/db/migrate/20240415155554_create_student_tasks.rb b/db/migrate/20240415155554_create_student_tasks.rb new file mode 100644 index 000000000..3dabe33bd --- /dev/null +++ b/db/migrate/20240415155554_create_student_tasks.rb @@ -0,0 +1,13 @@ +class CreateStudentTasks < ActiveRecord::Migration[7.0] + def change + create_table :student_tasks do |t| + t.references :assignment, null: false, foreign_key: true + t.string :current_stage + t.references :participant, null: false, foreign_key: true + t.datetime :stage_deadline + t.string :topic + + t.timestamps + end + end +end diff --git a/db/migrate/20240415163413_add_columns_to_participants.rb b/db/migrate/20240415163413_add_columns_to_participants.rb new file mode 100644 index 000000000..96e884161 --- /dev/null +++ b/db/migrate/20240415163413_add_columns_to_participants.rb @@ -0,0 +1,7 @@ +class AddColumnsToParticipants < ActiveRecord::Migration[7.0] + def change + add_column :participants, :topic, :string + add_column :participants, :current_stage, :string + add_column :participants, :stage_deadline, :datetime + end +end diff --git a/db/migrate/20240415192048_drop_student_task_tabkle.rb b/db/migrate/20240415192048_drop_student_task_tabkle.rb new file mode 100644 index 000000000..d7cccd2dd --- /dev/null +++ b/db/migrate/20240415192048_drop_student_task_tabkle.rb @@ -0,0 +1,5 @@ +class DropStudentTaskTabkle < ActiveRecord::Migration[7.0] + def change + drop_table :student_tasks + end +end diff --git a/db/schema.rb b/db/schema.rb index deda3ae23..1770d8997 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_03_24_000112) do +ActiveRecord::Schema[7.0].define(version: 2024_04_15_192048) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -181,9 +181,12 @@ t.boolean "can_submit", default: true t.boolean "can_review", default: true t.string "handle" - t.boolean "permission_granted" + t.boolean "permission_granted", default: false t.bigint "join_team_request_id" t.bigint "team_id" + t.string "topic" + t.string "current_stage" + t.datetime "stage_deadline" t.index ["assignment_id"], name: "index_participants_on_assignment_id" t.index ["join_team_request_id"], name: "index_participants_on_join_team_request_id" t.index ["team_id"], name: "index_participants_on_team_id" diff --git a/spec/factories.rb b/spec/factories.rb index 85a4e092c..758fa51a2 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,4 +1,12 @@ FactoryBot.define do + factory :student_task do + assignment { nil } + current_stage { "MyString" } + participant { nil } + stage_deadline { "2024-04-15 15:55:54" } + topic { "MyString" } + end + factory :join_team_request do end diff --git a/spec/models/student_task_spec.rb b/spec/models/student_task_spec.rb new file mode 100644 index 000000000..e4321ae89 --- /dev/null +++ b/spec/models/student_task_spec.rb @@ -0,0 +1,83 @@ +require 'rails_helper' + + +RSpec.describe StudentTask, type: :model do + before(:each) do + @assignment = double(name: "Final Project") + @participant = double( + assignment: @assignment, + topic: "E2442", + current_stage: "finished", + stage_deadline: "2024-04-23", + permission_granted: true + ) + + end + + describe ".initialize" do + it "correctly assigns all attributes" do + args = { + assignment: @assignment, + current_stage: "finished", + participant: @participant, + stage_deadline: "2024-04-23", + topic: "E2442", + permission_granted: false + } + + student_task = StudentTask.new(args) + + expect(student_task.assignment.name).to eq("Final Project") + expect(student_task.current_stage).to eq("finished") + expect(student_task.participant).to eq(@participant) + expect(student_task.stage_deadline).to eq("2024-04-23") + expect(student_task.topic).to eq("E2442") + expect(student_task.permission_granted).to be false + end + end + + describe ".from_participant" do + it "creates an instance from a participant instance" do + + student_task = StudentTask.create_from_participant(@participant) + + expect(student_task.assignment).to eq(@participant.assignment.name) + expect(student_task.topic).to eq(@participant.topic) + expect(student_task.current_stage).to eq(@participant.current_stage) + expect(student_task.stage_deadline).to eq(Time.parse(@participant.stage_deadline)) + expect(student_task.permission_granted).to be @participant.permission_granted + expect(student_task.participant).to be @participant + end + end + + describe ".parse_stage_deadline" do + context "valid date string" do + it "parses the date string into a Time object" do + valid_date = "2024-04-25" + expect(StudentTask.send(:parse_stage_deadline, valid_date)).to eq(Time.parse("2024-04-25")) + end + end + + context "invalid date string" do + it "returns current time plus one year" do + invalid_date = "invalid input" + # Set the now to be 2024-05-01 for testing purpose + allow(Time).to receive(:now).and_return(Time.new(2024, 5, 1)) + expected_time = Time.new(2025, 5, 1) + expect(StudentTask.send(:parse_stage_deadline, invalid_date)).to eq(expected_time) + end + end + end + + describe ".from_participant_id" do + it "fetches a participant by id and creates a student task from it" do + allow(Participant).to receive(:find_by).with(id: 1).and_return(@participant) + + expect(Participant).to receive(:find_by).with(id: 1).and_return(@participant) + expect(StudentTask).to receive(:create_from_participant).with(@participant) + + StudentTask.from_participant_id(1) + end + end + +end \ No newline at end of file diff --git a/spec/requests/api/v1/student_tasks_controller_spec.rb b/spec/requests/api/v1/student_tasks_controller_spec.rb new file mode 100644 index 000000000..794a5af62 --- /dev/null +++ b/spec/requests/api/v1/student_tasks_controller_spec.rb @@ -0,0 +1,125 @@ +require 'swagger_helper' + +def login_user() + # Give the login details of a DB-seeded user. (see seeds.rb) + login_details = { + user_name: "john", + password: "password123" + } + + # Make the request to the login function. + post '/login', params: login_details + + # return the token from the response + json_response = JSON.parse(response.body) + json_response['token'] +end + +describe 'StudentTasks API', type: :request do + + # Re-login and get the token after each request. + before(:each) do + @token = login_user + end + + path '/api/v1/student_tasks/list' do + get 'student tasks list' do + # Tag for testing purposes. + tags 'StudentTasks' + produces 'application/json' + + # Define parameter to send with request. + parameter name: 'Authorization', :in => :header, :type => :string + + # Ensure an authorized request gets a 200 response. + response '200', 'authorized request has success response' do + # Attach parameter to request. + let(:'Authorization') {"Bearer #{@token}"} + + run_test! + end + + # Ensure an authorized test gets the right data for the logged-in user. + response '200', 'authorized request has proper JSON schema' do + # Attach parameter to request. + let(:'Authorization') {"Bearer #{@token}"} + + # Run test and give expectations about result. + run_test! do |response| + data = JSON.parse(response.body) + expect(data).to be_instance_of(Array) + expect(data.length()).to be 5 + + # Ensure the objects have the correct type. + data.each do |task| + expect(task['assignment']).to be_instance_of(String) + expect(task['current_stage']).to be_instance_of(String) + expect(task['stage_deadline']).to be_instance_of(String) + expect(task['topic']).to be_instance_of(String) + expect(task['permission_granted']).to be_in([true, false]) + + # Not true in general case- this is only for the seeded data. + expect(task['assignment']).to eql(task['topic']) + end + end + end + + # Ensure a request with an invalid bearer token gets a 401 response. + response '401', 'unauthorized request has error response' do + let(:'Authorization') {"Bearer "} + run_test! + end + + # Ensure a request with an invalid bearer token gets the proper error response. + response '401', 'unauthorized request has error response' do + let(:'Authorization') {"Bearer "} + run_test! do |response| + data = JSON.parse(response.body) + expect(data["error"]).to eql("Not Authorized") + end + end + end + end + + path '/api/v1/student_tasks/view' do + get 'Retrieve a specific student task by ID' do + tags 'StudentTasks' + produces 'application/json' + parameter name: 'id', in: :query, type: :Integer, required: true + parameter name: 'Authorization', in: :header, type: :string + + response '200', 'successful retrieval of a student task' do + let(:'Authorization') { "Bearer #{@token}" } + let(:id) { 1 } + + run_test! do |response| + data = JSON.parse(response.body) + expect(data['assignment']).to be_instance_of(String) + expect(data['current_stage']).to be_instance_of(String) + expect(data['stage_deadline']).to be_instance_of(String) + expect(data['topic']).to be_instance_of(String) + expect(data['permission_granted']).to be_in([true, false]) + end + end + + response '500', 'participant not found' do + let(:'Authorization') { "Bearer #{@token}" } + let(:id) { -1 } + + run_test! do |response| + expect(response.status).to eq(500) + end + end + + response '401', 'unauthorized request has error response' do + let(:'Authorization') { "Bearer " } + let(:id) { 'any_id' } + run_test! do |response| + data = JSON.parse(response.body) + expect(data["error"]).to eql("Not Authorized") + end + end + end + + end +end \ No newline at end of file diff --git a/spec/routing/student_tasks_routing_spec.rb b/spec/routing/student_tasks_routing_spec.rb new file mode 100644 index 000000000..902e75db2 --- /dev/null +++ b/spec/routing/student_tasks_routing_spec.rb @@ -0,0 +1,30 @@ +require "rails_helper" + +RSpec.describe StudentTasksController, type: :routing do + describe "routing" do + it "routes to #index" do + expect(get: "/student_tasks").to route_to("student_tasks#index") + end + + it "routes to #show" do + expect(get: "/student_tasks/1").to route_to("student_tasks#show", id: "1") + end + + + it "routes to #create" do + expect(post: "/student_tasks").to route_to("student_tasks#create") + end + + it "routes to #update via PUT" do + expect(put: "/student_tasks/1").to route_to("student_tasks#update", id: "1") + end + + it "routes to #update via PATCH" do + expect(patch: "/student_tasks/1").to route_to("student_tasks#update", id: "1") + end + + it "routes to #destroy" do + expect(delete: "/student_tasks/1").to route_to("student_tasks#destroy", id: "1") + end + end +end diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index b660f13f1..f23cb7b20 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -1303,6 +1303,34 @@ paths: required: - user_name - password + /api/v1/student_tasks/list: + get: + summary: List all Student Tasks + tags: + - Student Tasks + responses: + '200': + description: An array of student tasks + /api/v1/student_tasks/view: + get: + summary: View a student task + tags: + - Student Tasks + parameters: + - in: query + name: id + schema: + type: string + required: true + description: The ID of the student task to retrieve + responses: + '200': + description: A specific student task + + + + + servers: - url: http://{defaultHost} variables: