CSC/ECE 517 Fall 2024 - E2476. Reimplement student teams controller.rb

From Expertiza_Wiki
Jump to navigation Jump to search

Introduction

Expertiza is an open-source system built on Ruby on Rails, collaboratively maintained by students and faculty at NC State. This platform gives instructors extensive control over assignments, enabling them to add topics, create assignment groups, and manage peer reviews for various types of coursework. Expertiza is well-equipped to handle submissions across multiple formats, including URLs and wiki pages, and it offers a peer review system that facilitates constructive feedback among students.

A key focus of recent development in Expertiza has been the shift to a client-server architecture, where Ruby on Rails functions as the backend and React is utilized for the frontend. This architectural update requires reworking existing modules into RESTful API endpoints, allowing React components to interact efficiently with the backend. By adopting this approach, Expertiza aims to enhance modularity, performance, and scalability.

Project Description

The project mainly focuses on reimplementing the StudentTeamsController in Expertiza's backend, which is designed for student-facing operations where students participate in teams. This includes actions that directly affect individual team members, such as adding or removing students from teams, viewing team details, and managing their participation.

The TeamsController, on the other hand, is focused on instructor-driven operations that handle high-level team management across different contexts, such as courses and assignments. It manages operations like team creation, deletion, randomization, and bulk management actions.

Problem Statement

The current implementation of the StudentTeamsController in the Expertiza system is overly complex and tightly coupled, leading to difficulties in maintaining and extending the codebase. The controller is responsible for managing student teams in assignments, handling tasks like team creation, editing, management, and the integration of topics in assignments that require team-based decisions. However, it suffers from issues such as:

  • Code redundancy and inefficiency due to business logic being embedded directly in the controller.
  • Lack of modularity and poor separation of concerns, violating the Single Responsibility Principle (SRP). For example, it also handles the integration with topics when assignments involve choosing topics, complicating the team’s logic.
  • Difficult-to-understand methods and tightly coupled logic for handling assignment-specific conditions, such as team-topic associations.
  • Poor user feedback that does not provide clear or actionable information after critical operations.

These problems create a system that is hard to scale, maintain, and extend, especially as new features are added. To address these challenges, the goal is to reimplement the StudentTeamsController in the newer Expertiza backend codebase by adhering to object-oriented design principles, specifically focusing on maintainability, scalability, and modularity. The new design will improve the user experience by simplifying controller logic, providing clearer feedback, and ensuring that the system is easier to extend and maintain in the future.

Objectives

The main objective is to migrate the Student Task controller logic from serving HTML responses in the Ruby on Rails MVC model to returning JSON responses as a RESTful API, as part of the backend implementation. While reimplementing the controller, the following considerations will be incorporated to ensure a clean, maintainable, and scalable API:

  • Reimplement the controller logic and write methods which appropriately follow the Single Responsibility Principle (SRP).
  • Move business logic from the controller to appropriate model classes like Team or AssignmentTeam.
  • Ensure all method names clearly describe their purpose and represent actions.
  • Reimplement a Prototype pattern for creating mentored teams, leveraging the AssignmentTeam class.
  • Automatically generate team names when a team is first referred to unless the user has specified a name when requesting team creation.
  • Handle team requirements with better abstraction:
  • Try to eliminate direct logic in the controller that checks for specific assignment requirements such as topic handling.
  • Rename student_team_requirements_met? to something more descriptive and break it into smaller methods checking individual conditions.
  • Aim to decouple topics from this controller entirely, ensuring better abstraction, that can involve changes to other classes or introducing service objects to handle specific requirements.
  • Improve feedback to users and ensure that the user receives clear and actionable feedback after operations like team creation, removal of users, and other key actions.
  • Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods.

Development Strategy

Our development strategy centers around Test-Driven Development (TDD) principles applied to refactoring the student_teams_controller.rb and other related classes such as Team or AssignmentTeam model. We design out test cases and iteratively refactor code to address identified inefficiencies while ensuring successful test outcomes. Additionally, we emphasize on the strict adoption of SRP and DRY principles as per the object-oriented programming conventions. Our goal is to reimplement the existing functionalities In the newly designed back-end foundation to foster a more efficient and organized codebase utilizing newer technologies. This approach will significantly enhance reusability and scalability across all the application while maintaining clean codebase.

Files Modified:

  • Created ==>
    • app/controllers/api/v1/student_teams_controller.rb
    • app/models/assignment_team.rb
    • app/models/team.rb
    • spec/requests/api/v1/student_teams_controller_spec.rb
    • db/migrate/20241201203840_create_assignment_teams.rb
    • db/migrate/20241201220020_add_auto_assign_mentor_to_assignments.rb
    • db/migrate/20241201221122_add_type_to_teams.rb
    • db/migrate/20241202002102_add_name_to_teams.rb
  • Updated ==>
    • app/models/team.rb
    • config/routes.rb
    • db/schema.rb
    • db/seeds.rb
    • spec/swagger_helper.rb

Project Design and Implementation

The main approach to be followed throughout this project is to embrace the Prototyping Pattern within the controller while taking into account the current functionalities to be reimplemented within the student_teams_controller.rb that can capitalize its capability to encapsulate distinct controller actions. This strategic implementation aims to ensure seamless extensibility and enhanced usability. By employing this pattern, we establish a structured framework for defining and managing the diverse operations the controller handles, promoting a more organized and manageable code structure.

Functionality

1. Reimplement the Controller to Follow SRP:

  • Break down large, complex methods into smaller, focused methods that adhere to the Single Responsibility Principle (SRP).
  • Reimplement existing methods and logic while ensuring that each class and method have a clear, well-defined responsibility.
  • Move business logic and complex operations out of the controller and into the appropriate model classes (e.g., Team, AssignmentTeam), ensuring the controller only handles HTTP-specific concerns (e.g., request handling, responses).

2. Improve Method Naming and Clarity:

  • Ensure all method names are descriptive and represent the intent of the operation (e.g., renaming methods like student_team_requirements_met? to something more specific like validate_topic_requirements_for_team_creation).
  • Adopt consistent naming conventions to improve the readability of the codebase.

3. Implement the Prototype Pattern for Team Creation:

  • Leverage the Prototype pattern to create mentored teams through the AssignmentTeam class, allowing for easier management and creation of teams with a mentored structure.
  • Automatically generate team names when a team is first created, unless a user has specified a custom name to improve the user experience and simplify team management.

4. Abstract Assignment-Specific Logic:

  • Move logic related to assignment-specific requirements (e.g., topic handling) away from the controller, reducing complexity and increasing modularity.
  • Introduce service objects or model methods to handle specific requirements like topic assignment, thereby decoupling assignment-related logic from the controller.
  • Break down the student_team_requirements_met? method into smaller, more specific checks for different requirements (e.g., topic validation, minimum team size, etc.).

5. Improve User Feedback:

  • Enhance the clarity and actionability of feedback messages after key operations (e.g., team creation, user removal, etc.).
  • Provide users with clear, helpful notifications on the success or failure of their actions, including any necessary next steps or troubleshooting guidance.
  • Ensure error handling is robust, and feedback is specific (e.g., specify which conditions were not met when creating or modifying teams).

6. Commenting and Documentation:

  • Add comprehensive comments throughout the controller to clearly explain the purpose and functionality of custom methods and refactored logic.
  • Ensure that code is well-documented, making it easier for future developers to understand and extend the functionality of the controller.

Tasks Completed

Methods:

  • AssignmentTeam Model:
    • To access every participant connected to an assignment, the assignment_participants method was added.
    • For including a user in the assignment as a participant, the add_participant method was added.
    • To see if the team has gotten any peer reviews, the received_any_peer_review? method was put into practice.
    • Using a placeholder for future mentor assignment logic, the upgrade_to_mentored_team method was put into practice to replicate the present team and get it ready for mentoring.
    • In order to ensure deep duplication of associated records (such as signed_up_teams, teams_users, and participants), the dup method was added to duplicate a team and its associations.
  • Team Model:
    • Improved the whole procedure to determine if a team has reached its maximum number of members.
    • Counted the number of team members using the size approach, removing those with "(Mentor)" in their names.
    • In order to eliminate duplicate members and check for team capacity, the add_member method was improved.
    • To remove a user from the team, the remove_team_user method was put into place. If the team becomes empty and hasn't been subjected to any peer reviews, the team will be deleted.
  • Controller - StudentTeamsController.rb
    • index: Implemented in order to retrieve and restore every student team together with all of its members.
    • show: Created to retrieve and provide information about a certain student team and its members.
    • create: Added logic to create a new team for a student, using AssignmentTeam#upgraded_to_mentored_team for mentor-eligible teams and checking for existing team names.
    • update: Implemented to enable team details, especially the team name, to be updated while ensuring unique team names through validations.
    • destroy: To remove a certain team this was added and, if successful, deliver a 204 No Content response.
    • remove_participant: Check if the user is part of the given team and removes from that team, also handles corner cases where no more participants are there in a team.
    • add_participant: Added this to add the given participant to respective team.

Testing Plan

We have performed comprehensive testing of the student_teams_controller.rb and related classes using RSPEC and Swagger to construct the required test fixtures for testing extensive scenarios. Our approach involves reimplementing the existing test cases provided to the older code base and make sure that it is enhanced by covering more test scenarios to minimize code smells. We aim to verify the reimplemented functionality, validate the restructured code in the student_teams_controller.rb and reduce any design or code smell. Below are the test scenarios for each of API endpoints -

1. List all Student Teams

  • Description: List all student teams.
  • Response: 200 OK
  • Test Case Scenarios:
  • Scenario 1: List all Student Teams
  • Expectation: Successfully retrieves a list of all student teams. The response status is 200, the content type is `application/json`, and the response body contains 3 student teams.
RSpec.describe 'Student Teams API', type: :request do
path '/api/v1/student_teams' do
   # List all Student Teams
   get('List all Student Teams') do
     tags 'Student Teams'
     produces 'application/json'
     security [Bearer: {}]
     response(200, 'successful') do
       after do |example|
         example.metadata[:response][:content] = {
           'application/json' => {
             example: JSON.parse(response.body, symbolize_names: true)
           }
         }
       end
       run_test!
     end
   end
  • Postman Screenshot

2. Create a Student Team

  • Description: Create a new student team.
  • Response: 201 Created
  • Test Data prep:
 post 'Create a Student Team' do
     tags 'Student Teams'
     consumes 'application/json'
     let(:institution) do
       Institution.create!(
         name: 'North Carolina State University'
       )
     end
     let!(:instructor_role) { Role.find_or_create_by(name: 'Instructor') }
     let!(:student_role) { Role.find_or_create_by(name: 'Student') }
     let(:instructor_user) do
       User.create!(
         name: 'Dr. Ed Gehringer1',
         email: 'gehringer@example.com',
         password: 'password123',
         full_name: 'admin admin',
         institution_id: institution.id,
         role_id: instructor_role.id,
         handle: 'instructor'
       )
     end
     let(:course) do
       Course.create!(
         name: '2476. Refactor',
         directory_path: '/',
         info: 'OODD',
         private: false,
         instructor_id: instructor_user.id,
         institution_id: institution.id
       )
     end
     let(:assignment) do
       Assignment.create!(
         title: 'Project 4 BRO',
         description: '2476. Reimplementing',
         course_id: course.id,
         instructor_id: instructor_user.id
       )
     end
     let!(:students) do
       created_students = []
       3.times do |i|
         student = User.create!(
           name: "Student #{i + 1}",
           email: "Student#{i + 1}@gmail.com",
           password: 'password123',
           full_name: "Student #{i + 1}",
           institution_id: institution.id,
           role_id: student_role.id,
           handle: "Student #{i + 1}"
         )
         AssignmentParticipant.create!(
           assignment: assignment,
           user: student,
           handle: "Student #{i + 1}"
         )
         created_students << student
       end
       created_students
     end
     parameter name: :student_team_request, in: :body, schema: {
       type: :object,
       properties: {
         team: {
           type: :object,
           properties: {
             name: {type: :string}
           }
         },
         student_id: { type: :integer}
       }
     }
     security [Bearer: {}]
  • Scenarios:
  • Scenario 1: Successfully create a team
  • Expectation: Successfully creates a new student team with a valid name and student ID. The response status is 201, and the response body includes the team name.
  • Scenario 2: Team name is already in use (conflict)
  • Attempts to create a student team with a name that is already in use.
  • Expectation: The response status is 422, and the response body includes an error message indicating that the team name is already in use.
  • Scenario 3: Invalid team creation (empty name)
  • Expectation: Attempts to create a student team with an empty string name. The response status is 201, with team name autogenerated.
     # Scenario 1: Successfully create a team
     response(201, 'created') do
       let(:student_team_request) do
         { team: {name: "HelloThere"}, student_id: students.first.id}
       end
       after do |example|
         example.metadata[:response][:content] = {
           'application/json' => {
             example: JSON.parse(response.body, symbolize_names: true)
           }
         }
       end
       run_test! do
         expect(response.status).to eq(201)
       end
     end
     # Scenario 2: Team name is already in use (conflict)
     response(422, 'unprocessable entity') do
       let(:existing_team) { create(:team, name: 'ABCD10', assignment_id: assignment.id) }
       let(:team) do
         post '/api/v1/student_teams', params: { team: { name: 'ABCD10' }, student_id: students.first.id }, as: :json
       end
       run_test! do
         expect(response.status).to eq(422)
         expect(response.body).to include('"error":"That team name is already in use."')
       end
     end
     # Scenario 3: Invalid team creation (empty name)
     response(422, 'unprocessable entity') do
       let(:student_team_request) do
         { team: {name: ""}, student_id: students.first.id}
       end
       after do |example|
         example.metadata[:response][:content] = {
           'application/json' => {
             example: JSON.parse(response.body, symbolize_names: true)
           }
         }
       end
       run_test! do
         expect(response.status).to eq(422)
         expect(response.body).to include('error')
       end
     end
     response(404, 'not_found') do
       let(:team) do
         post '/api/v1/student_teams', params: { team: { name: 'ABCD10' }, student_id: 0 }, as: :json
       end
         run_test! do
           expect(response.status).to eq(404)
           expect(response.body).to include('not found')
         end
     end
   end
  • Postman
  • Scenario 1:

  • Scenario 2:

  • Scenario 3:

3. Update a Student Team

  • Description: Update the details of a student team.
  • Response: 200 OK
  • Scenarios:
  • Scenario 1: Team name updated successfully
  • Expectation: Successfully updates the name of a student team. The response status is 200, and the response body includes the updated team name.
  • Scenario 2: Invalid team update (empty name)
  • Expectation: Attempts to update a student team with an empty name. The response status is 422, and the response body includes an error message indicating that the team name should not be empty.
  • Scenario 3: Team name already in use
  • Expectation: Attempts to update a student team with a name that is already in use. The response status is 422, and the response body includes an error message indicating that the team name is already in use.
  • Scenario 4: Invalid team ID
  • Expectation: Attempts to update a student team with a non-existent team ID. The response status is 404, and the response body includes an error message indicating that the team was not found.
 patch('Update a Student Team') do
     tags 'Student Teams'
     consumes 'application/json'
     produces 'application/json'
     security [Bearer: {}]
     parameter name: :student_team_request, in: :body, schema: {
       type: :object,
       properties: {
         team: {
           type: :object,
           properties: {
             name: {type: :string}
           }
         },
         team_id: { type: :integer}
       }
     }
     # Scenario 1: Team name updated successfully
     response(200, 'successful') do
       after do |example|
         example.metadata[:response][:content] = {
           'application/json' => {
             example: JSON.parse(response.body, symbolize_names: true)
           }
         }
       end
       run_test! do
         expect(response.status).to eq(200)
       end
     end
     # Scenario 2: Invalid team updation (empty name)
     response(422, 'unprocessable entity') do
       let(:student_team_request) do
         { team: {name: ""}, team_id: 2}
       end
       after do |example|
         example.metadata[:response][:content] = {
           'application/json' => {
             example: JSON.parse(response.body, symbolize_names: true)
           }
         }
       end
       run_test! do
         expect(response.status).to eq(422)
         expect(response.body).to include('Team name should not be empty')
       end
     end
     # Scenario 3: Team Name already present
     response(422, 'unprocessable entity') do
       let(:existing_team) { create(:team, name: 'ABCD10', team_id: 3) }
       let(:team) do
         post '/api/v1/student_teams', params: { team: { name: 'ABCD10' }, team_id: 2 }, as: :json
       end
       run_test! do
         expect(response.status).to eq(422)
         expect(response.body).to include('"error":"That team name is already in use."')
       end
     end
     #Scenario 4 : Invalid Team ID
     response(404, 'not_found') do
       let(:team) do
         post '/api/v1/student_teams', params: { team: { name: 'ABCD10' }, team_id: 0 }, as: :json
       end
         run_test! do
           expect(response.status).to eq(404)
           expect(response.body).to include('not found')
         end
     end
   end
 end
  • Postman
    • Scenario 1:
    • Scenario 2:

    • Scenario 3:

    • Scenario 4:

4. Show a Student Team

  • Description: Retrieve the details of a student team by its ID.
  • Response: 200 OK
  • Scenarios:
  • Scenario 1: Show Student Team
  • Expectation: Successfully retrieves the details of a student team by its ID. The response status is 200, and the response body includes the details of the student team.
  • Scenario 2: Team not found
  • Expectation: Attempts to retrieve the details of a student team with an invalid ID. The response status is 404, and the response body includes an error message indicating that the team was not found.
 path '/api/v1/student_teams/{id}' do
   parameter name: 'id', in: :path, type: :integer
   # Get request on /api/v1/student_teams/{id} returns the response 200 succesful - when correct id passed is present in the database
   get('Show a Student Team') do
     tags 'Student Teams'
     produces 'application/json'
     security [Bearer: {}]
     response(200, 'successful') do
       after do |example|
         example.metadata[:response][:content] = {
           'application/json' => {
             example: JSON.parse(response.body, symbolize_names: true)
           }
         }
       end
       run_test! do
         expect(response.status).to eq(200)
       end
     end
     # Get request on /api/v1/student_teams/{id} returns the response 404 not found - when correct id passed is not present in the database
     response(404, 'not_found') do
       let(:id) { 'invalid' }
         run_test! do
           expect(response.status).to eq(404)
         end
     end
   end
  • Postman:
    • Scenario 1:

    • Scenario 2:

5. Delete a Student Team

  • Description: Delete a student team by its ID.
  • Response: 204 No Content
  • Scenarios:
  • Scenario 1: Successfully delete a student team
  • Expectation: Successfully deletes a student team by its ID. The response status is 204, and the student team is no longer present in the database.
  • Scenario 2: Team not found
  • Expectation: Attempts to delete a student team with an invalid ID. The response status is 404, and the response body includes an error message indicating that the team was not found.
 delete('Delete a Student Team') do
     tags 'Student Teams'
     produces 'application/json'
     security [Bearer: {}]
     # delete request on /api/v1/student_teams/{id} returns 204 succesful response when id present in the database is deleted successfully
     response(204, 'successful') do
       after do |example|
         example.metadata[:response][:content] = {
           'application/json' => {
             example: 
           }
         }
       end
       run_test! do
         expect(response.status).to eq(204)
       end
     end
     # delete request on /api/v1/student_teams/{id} returns 404 not found response, when id is not present in the database
     response(404, 'not found') do
       let(:id) { 0 }
       run_test! do
         expect(response.status).to eq(404)
       end
     end
   end
 end
  • Postman:
    • Scenario 1:

    • Scenario 2:

6. Add a Participant in Student Team

  • Description: Add a participant to a student team.
  • Response: 200 OK
  • Test Data Prep
 path '/api/v1/student_teams/{id}/add_participant' do
   parameter name: 'id', in: :path, type: :integer, description: 'ID of Team'
   parameter name: 'student_id', in: :query, type: :integer, description: 'ID of the Student'
   let(:student) { create(:user, :student) }
   let(:assignment) { create(:assignment) }
   let(:student_participant) { create(:assignment_participant, user: student, assignment: assignment) }
   let(:team) { create(:assignment_team, assignment: assignment) }
   let(:id) { team.id }
   let(:student_id) { student.id }
  • Scenarios:
  • Scenario 1: Successfully add a participant to a student team
  • Expectation: Successfully adds a participant to a student team by their ID. The response status is 200, and the participant is added to the team.
   patch('Add a participant to Student Team') do
     tags 'Student Teams'
     consumes 'application/json'
     produces 'application/json'
     security [Bearer: {}]
     before do
       team.add_member(student, assignment.id)
     end
     response(200, 'successful') do
       after do |example|
         example.metadata[:response][:content] = {
           'application/json' => {
             example: JSON.parse(response.body, symbolize_names: true)
           }
         }
       end
       run_test! 
     end
   end
 end
  • Postman:

7. Remove a Participant in Student Team

  • Description: Remove a participant from a student team.
  • Response: 204 No Content
  • Test data prep:
 path '/api/v1/student_teams/{id}/remove_participant' do
   parameter name: 'id', in: :path, type: :integer, description: 'ID of Team'
   parameter name: 'student_id', in: :query, type: :integer, description: 'ID of the Student'
   let(:student) { create(:user, :student) }
   let(:assignment) { create(:assignment) }
   let(:student_participant) { create(:assignment_participant, user: student, assignment: assignment) }
   let(:team) { create(:assignment_team, assignment: assignment) }
   let(:id) { team.id }
   let(:student_id) { student.id }
   before do
     team.add_member(student, assignment.id)
   end
  • Scenarios:
  • Scenario 1: Successfully remove a participant from a student team
  • Expectation: Successfully removes a participant from a student team by their ID. The response status is 204, and the participant is no longer a member of the team.
  • Scenario 2: Team not found
  • Expectation: Attempts to remove a participant from a student team with an invalid team ID. The response status is 404, and the response body includes an error message indicating that the team was not found.
  • Scenario 3: User not found
  • Expectation: Attempts to remove a participant from a student team with an invalid user ID. The response status is 404, and the response body includes an error message indicating that the user was not found.
   delete('remove participant') do
     tags 'Student Teams'
     produces 'application/json'
     security [Bearer: {}]
     response(204, 'successful') do
       let(:Authorization) { "Bearer #{@token}" }
       run_test! do
         expect(response.status).to eq(204)
         expect(team.members).not_to include(student)
       end
     end
     response(404, 'team not found') do
       let(:id) { -1 } # Invalid ID
       run_test! do
         expect(response.status).to eq(404)
         expect(JSON.parse(response.body)['error']).to eq('Team not found.')
       end
     end
     response(404, 'user not found') do
       let(:student_id) { -1 } # Invalid student ID
       run_test! do
         expect(response.status).to eq(404)
         expect(JSON.parse(response.body)['error']).to eq('User not found.')
       end
     end
   end
 end
  • Postman:
  • Scenario 1:

  • Scenario 2:

  • Scenario 3:

GitHub Pull Request

https://github.com/expertiza/reimplementation-back-end/pull/136

Demo Video

https://www.youtube.com/watch?v=8_d_Y9EcgHc

Mentor

  • Ammana, Sahithi <sammana@ncsu.edu>

Team Members

  • Eathamukkala, Akarsh Reddy <aeatham@ncsu.edu>
  • Koul, Anmol <akoul2@ncsu.edu>
  • More, Harsh Manoj <hmore@ncsu.edu>