CSC/ECE 517 Spring 2024 - E2442 Reimplement student task controller

From Expertiza_Wiki
Jump to navigation Jump to search

Links

Front end: https://github.com/ychen-207523/reimplementation-front-end

Back end: https://github.com/ychen-207523/reimplementation-back-end

Pull Request - Frontend: https://github.com/expertiza/reimplementation-front-end/pull/38

Pull Request - Backend: https://github.com/expertiza/reimplementation-back-end/pull/102

Test video: https://www.youtube.com/watch?v=zZ0Ea3HKqBo

Expertiza

Expertiza is a Moodle-like open source project developed with Ruby on Rails, designed to enhance the educational experience by allowing students, TAs and instructors to interact on assignments and projects. For instructors, Expertiza can help to create new assignments, review and grade the students’ submissions. For students, Expertiza can provide features to form teams, submit assignments, and provide peer evaluations.

Problem Statement

The scope of this project is the continuation of E2429. In the previous part, we have implemented the student_task table with React and TypeScript. On this project, we are going to work on the student_task_controller.rb and student_task.rb based on the original design of Expertiza and reimplement them.

student_task.rb: This file represents the student task model, will be where we define the relationship between student tasks and other entities such as courses, participants and assignments in the system, as well as any custom logic related to student tasks

student_task_controller.rb: This file is the controller of student task, we will implement features related with authentication and listing, viewing the student tasks.The main functions this project will focus on are list, which is to gather and organize student tasks to facilitate the display of tasks, and view, which is to display detailed information about a specific student task based on the student id.

The student task is highly related with other models such as participant and topic, which have not been implemented or finished yet. For the scope of this project, we will establish the foundational elements of the StudentTask to ensure its capability to interface with the frontend effectively. This approach allows us to set up a framework that can support future teams to incorporate the future implementation into student_task.

Principles

In order to ensure our code is maintainable, extensible, and as simple as possible, we have attempted to follow several universal design principles. Similarly, we have tried to be conscious of and avoid design smells that can indicate code is too rigid, fragile, complex, or difficult to understand.

Single Responsibility Principle (SRP)

While developing the student task controller and model, the single responsibility principle (SRP) is important to maintain. The SRP states that each class should not have more than one reason to change- in other words, it should do one thing and do it well. The first implication of this principle is that the controller should not contain significant application logic; the controller’s primary responsibility is to handle requests and call the appropriate methods. Therefore, it should delegate to the model class for any complex logic. The principle can also be applied to functions within the model class itself. Ideally, the student task controller will call simple and reusable functions in the model class that each perform one task. For instance, here's our student task controller's list function:

    def list
      @student_tasks = StudentTask.create_tasks_from_user current_user
      render json: @student_tasks
    end

app/controllers/api/v1/student_tasks_controller.rb (https://github.com/ychen-207523/reimplementation-back-end/blob/main/app/controllers/api/v1/student_tasks_controller.rb)

In this way, the controller correctly fulfills its single responsibility by handling the request and delegating to the model class.

Don't Repeat Yourself (DRY)

The reusability of components follows another important principle- do not repeat yourself, also known as DRY . Essentially, the same code should not exist in two different places if it does the same thing- instead, the design of components should enable the code to be written once and called multiple times. This reduces the complexity of the application, ensures the code always does the same thing, and makes testing the code simpler and more effective. We considered the DRY principle throughout development, ensuring we developed simple, fine-grained, and reusable functions for the student task model that can be reused by other components in the future. A clear example is in our student task controller tests:

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

...
...

spec/requests/api/v1/student_tasks_controller_spec.rb (https://github.com/ychen-207523/reimplementation-back-end/blob/main/spec/requests/api/v1/student_tasks_controller_spec.rb)


Here, we have common functionality (simulating a log-in to test authenticated endpoints) that we need to run before each test. Instead of putting this code at the start of each method, we factor it out into a common method and apply it in the before(:each) block. This allows us to follow the DRY principle and ensure this code works consistently to keep individual test components independent.

Open-Closed Principle

The open-closed principle states that classes should be open for extension but closed for modification. Following this principle, the student task controller should interact with the model through method calls encapsulated in the model and not modify any data for the model itself. Additionally, we need to be cognizant of the overall design of the model and controller so they don’t need to change in order to implement new functionality in the future. Again, this will be achieved by keeping the code modular and ensuring the student task model’s methods adhere to principles of encapsulation and single responsibility.

Readability

Finally, it’s important to make code as readable as possible. If developers understand code more quickly, the code is more maintainable, extensible, and less likely to cause unforeseen errors. Therefore, we will follow several best practices for code readability:

  • Narrowest Scope: Give a variable the narrowest scope you can. By giving the narrowest scope by default, we constrain variables to the context in which they are needed, reducing the need for developers to reason about too many variables.
  • Conciseness: Code should be as concise as possible while being simple to understand.
  • Variable Names: Variable names should be long enough to be descriptive, but not longer than necessary. Variables that are only used once or twice may be a bit longer than variables used throughout the code, since the developer may be much more familiar with these variables.
  • Consistency: Follow conventions that exist in the present code. Unless there are fundamental differences, a problem should not be solved in different ways by the same code base. By using existing solutions and conventions, we make our code easier to use in the context of the system and help developers understand the implementation.
  • Commenting: Provide informative comments about the purpose of code. Obvious things that are clarified by concise and readable syntax don’t need to be explained; instead, we will focus on providing developers with context for any sequence of operations that merits explanation.

Implementation

Prerequisite: Given that the StudentTask heavily relies on data from the Participant table, we will utilize the existing Participant model, controller, database table, and schema. Once the new Participant model and controller are completed, the future team should integrate these updates to ensure that the StudentTask remains fully functional.
Another inseparable element of the StudentTask table is the topic; however, implementing the supporting backend for topics is beyond the scope of our current project. Therefore, we will utilize JSON data to mimic the interaction and data structure of topics. This method allows us to simluate the topic without implementing. Also, The future team can integrate the implemented topic part into student task easily without conflict.

Frontend

frontend: The frontend is the the continue implementation of E2429. The purpose of this improvement is to implement the features to retrieve the data of student tasks from the backend by API calls. Here are the key changes:

const fetchData = useCallback(async () => {
    try {
      const [assignments, courses] = await Promise.all([
        fetchStudentTasks({ url: `/student_tasks/list` }),
        fetchAssignments({ url: `/assignments` }),
        fetchCourses({ url: '/courses' }),
      ]);
      // Handle the responses as needed
    } catch (err) {
      // Handle any errors that occur during the fetch
      console.error("Error fetching data:", err);
    }
  }, [fetchStudentTasks, fetchAssignments, fetchCourses]);
  // fetch data from backend
  useEffect(() => {
    fetchData();
  }, [fetchData]);

The student task contains the data from several tables including participant and assignment. The first step is to access this data via an API. The name of the assignment can be retrieved from assignment table, and the course name can be obtained from the course table. After we successfully retrieved the data from the database, the next step is to merge them together so they can be passed to the Table. To achieve this goal, we create a new merged dataset that integrates student_task, assignments, and courses. After this step, the mergedData is passed to table component.

let mergedData: Array<any & { courseName?: string }> = [];

  if (studentTasks && assignmentResponse && coursesResponse) {
    mergedData = studentTasks.data.map((studentTask: any) => {
      // Find the related assignment from assignmentResponse using assignment name
      const assignment = assignmentResponse.data.find((a: any) => a.name === studentTask.assignment);
      // Using the course_id from the related assignment, find the course from coursesResponse
      const course = coursesResponse.data.find((c: any) => c.id === assignment.course_id);
      return {
        ...studentTask,
        courseName: course ? course.name : 'Unknown', // Add the course name to the merged data
      };
    });
  }



Model

student_task.rb: The class represents the student task used in the assignments page. Here is the breakdown of this model:

     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_from_participant creates a StudentTask instance from a Participant object. Because in Expertiza, student task is a temporal data that shares the same data with participant. The assignment name can used to link to assignment table, from which we can retrieve course information. This design follows the DRY principle by using the existing data in the database.

def self.from_user(user)
      Participant.where(user_id: user.id)
                 .map { |participant| StudentTask.create_from_participant(participant) }
                 .sort_by(&:stage_deadline)
    end

This method generates an array of StudentTask instances for all participants linked to an user of provided id. The array is sorted by the stage deadline.

def self.from_participant_id(id)
      create_from_participant(Participant.find_by(id: id))
    end

This method creates a StudentTask instance using a participant identified by a given ID by calling create_from_participant method. It takes participant id as a parameter, then retrieves the participant's details from the database using the find_by method, and use the participant to create an instance of student_task.



Controller (List Function)

student_task_controller.rb: This file contains the endpoints provided by our functionality- the list endpoint and the view endpoint.

Information for the list endpoint is obtained in the old system by querying the participants database (there is no separate student task database). To maintain this for the new system, we created a Student Task model function we can call from the view. In this way, we keep the bulk of application logic in the model class and allow the controller to focus on its single responsibility- routing and transforming requests.

Here is the implementation of the list endpoint:

    def list
      @student_tasks = StudentTask.create_tasks_from_user current_user
      render json: @student_tasks
    end


This list function is attached to the /api/v1/student_tasks endpoint using routes.rb. It calls StudentTask's create_tasks_from_user method on the authenticated user (current_user) and returns the result as JSON for the front end.

The old student task controller handled other functionality as well- checking if the user is a new user, handling impersonation, and getting students who have teamed with a user. However, this is a result of using rails for the front end and the back end, and trying to implement something similar would just result in tight coupling between the front and back end (as well as violating the SRP). Instead, this functionality should be appropriately split into different endpoints and modularized to improve the maintainability, flexibility and testability of the code.



Controller (View Function)

After finishing the implementation of list, we moved on to the view function. Here is the prior design of that function:

  def view
    StudentTask.from_participant_id params[:id]
    @participant = AssignmentParticipant.find(params[:id])
    @can_submit = @participant.can_submit
    @can_review = @participant.can_review
    @can_take_quiz = @participant.can_take_quiz
    @authorization = @participant.authorization
    @team = @participant.team
    denied unless current_user_id?(@participant.user_id)
    @assignment = @participant.assignment
    @can_provide_suggestions = @assignment.allow_suggestions
    @topic_id = SignedUpTeam.topic_id(@assignment.id, @participant.user_id)
    @topics = SignUpTopic.where(assignment_id: @assignment.id)
    @use_bookmark = @assignment.use_bookmark
    # Timeline feature
    @timeline_list = StudentTask.get_timeline_data(@assignment, @participant, @team)
    # To get the current active reviewers of a team assignment.
    # Used in the view to disable or enable the link for sending email to reviewers.
    @review_mappings = review_mappings(@assignment, @team.id) if @team
  end

The prior design is to provide detailed information about a specific student task based on the participant id, then it gathers different information from the participant along with the other required information such as topic and time line. Therefore, the student task is highly related with participant.


The 'view' function in the is designed to provide detailed information about a specific student task by accessing data associated with a given participant ID. This endpoint is crucial for the front-end in order to display data about an participant's specific student task, such as their team, review status, scores, and other basic information. Below is an explanation of how this controller action is implemented and its integration with other components.

Implementation Details
Data Retrieval

The function starts by capturing a participant's ID from the request parameters. The ID directly corresponds to the participant involved associated with the student task. It then calls a model method to retrieve the associated StudentTask based on this ID, encapsulating the logic to fetch and construct the student task data from the participant's details.

@student_task = StudentTask.from_participant_id(params[:id])


Model Interaction

Participant Lookup: The StudentTask model includes a method from_participant_id which first finds the Participant object by ID using Participant.find_by(id: id).

Task Creation: After retrieving the participant, it utilizes create_from_participant to construct a new StudentTask instance. This method takes the participant object and extracts various attributes to form a data structure representing the student task.

// student_task.rb

def self.from_participant_id(id)
    create_from_participant(Participant.find_by(id: id))
end

def self.create_from_participant(participant)
    new(
        assignment: participant.assignment.name,
        topic: participant.topic,
        current_stage: participant.current_stage,
        stage_deadline: parse_stage_deadline(participant.stage_deadline),
        permission_granted: participant.permission_granted,
        participant: participant
    )
end
JSON Rendering

Once the StudentTask instance is created, the controller action finalizes the process by rendering the student task data as JSON. This step makes it possible for the frontend to consume and display the data appropriately.

render json: @student_task


For our project, we intend to keep the same design idea, we will start from implementing list function to provide a comprehensive and filtered overview of student task based on the role of the current user. This will give us a solid start point. After we finsih implementing list, we will move on to view, focusing on offering detailed information about individual student tasks. The design principles include the Single Responsibility Principle (SRP), ensuring each component of our application is tasked with a single responsibility, and the Principle of Least Privilege (PoLP), ensuring users access only the data and actions relevant to their role.

UML

The user sends request through the UI to student_task_controller.
In List function, the controller sends request to student_task model for an instance, the student_task model will gather information from participant based on the participant parameter and send the intance to controller. After assembling these instances with the necessary details, the StudentTask model returns them to the controller, which then renders the appropriate view to display the tasks to the user.
In the view function, it gathers the information of a specific StudentTask based on the given participant id. It also retrieves the information of the participant such as ability to submit work, review others, take quizzes, authentication and etc.This process enables a detailed and personalized overview of the student task.

Testing

For this project we have completed the list and view functions in the student task controller. We used RSwag and Rspec to implement the controller test cases, and postman and swaggerUI to visualize endpoints.

Postman

In early stages of development, the team used Postman (https://www.postman.com) in order to test API endpoints exposed by the student task controller. Specifically, the endpoints for the list function and the view function were checked with Postman to verify their parameters, headers, and response values. A Postman collection was first created to enable us to quickly and simply test code changes without manually navigating through the UI each time. The collection contains a request that logs in (and sets authorization variables for the other requests) as well as a list request and a view request:

On the left, you can see the post request and two get requests under "Student Tasks". In the top-center is the API we are hitting, and in the bottom we see the response from sending the request.

Rspec/Rswag

Continuing, we developed automated test cases using Rspec/RSwag (https://github.com/rswag/rswag). Functional testing of the controller itself, and the methods we've implemented, are tested with Rspec (https://rspec.info/features/6-0/rspec-rails/controller-specs/). We tested success and error responses with valid and badly-formed requests in order to ensure our components are reliable and secure. Automating tests is important for preserving existing functionality, especially when changes are made.

spec/models/student_task_spec.rb(https://github.com/ychen-207523/reimplementation-back-end/blob/main/spec/models/student_task_spec.rb):
This file is to test student_task model and ensure that the class methods and attributes behave as expected.

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

This part is desgined to provide the test data befor each test. Double object can be used to represent an actual object, in this case, we use double to represent the assignment and participant for our test cases. The following is a breakdown for our tests:

  • initialize test: This test checks if the new studentTask instance is created with correct attributes.
  • from_participant test: This tests that creates a new StudentTask instance from a Participant object. It checks that the method correctly assigned the attributes to the new instance of studentTask.
  • parse_stage_deadline test: This test checks the method parse_stage_deadline, make sure it can correctly parse the date string to a Time object. The first part of the test checks when the method receives a valid date string. The second part of the test checks when it receives an invalid date string, it should returns a time one year from the current date. For the purpose of testing, we set the Now to be 2024-05-01. So, the correct return value will be 2025-05-01.
  • from_participant_id test: This test checks method to get a Participant by id and then use that participant to create a StudentTask instance. For the test purpose, we use let the participant to retrieve the mocked participant data, and then checks if the create_from_participant can receive the correct participant.


The purpose of the list function is to provide the user interface with data for displaying the student task table. Accordingly, we have tested the following cases for the list function:

  • Ensure a user gets all their tasks with correct attributes when they request.
  • Ensure a user is unable to view tasks without proper authorization, and that they get an error response instead.

For example, this is part of the first test for the list controller:


  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

spec/requests/api/v1/student_tasks_controller_spec.rb (https://github.com/ychen-207523/reimplementation-back-end/blob/main/spec/requests/api/v1/student_tasks_controller_spec.rb)

The test first scopes a path and defines a get request we can implement several tests for. First, it defines a parameter with key 'Authorization' and gives a bearer token stored in an instance variable (computed in the before(:each) block of the spec) using the let syntax. Then, calling run_test! validates that the response code is correct. In a later test, we also assert statements about the contents of the response by using a block with run_test!:

      # 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

spec/requests/api/v1/student_tasks_controller_spec.rb (https://github.com/ychen-207523/reimplementation-back-end/blob/main/spec/requests/api/v1/student_tasks_controller_spec.rb)

Here, we assert we have the correct response as well as the fact that the response returned an array of length 5. Finally, we assert the types of all returned data and that the topic is equal to assignment (something manually configured for the seeded test data).



The purpose of the `view` function is to provide the user interface with data for displaying one student task. Accordingly, we have tested the following cases for the `view` function:

  • Ensure a user is able to view any task retrieved from the table.
  • Ensure the user gets correct attributes when they request.
  • Ensure a user may view a task without error when some data is missing.

For example, this is part of the first test for the `view` controller:

  path '/api/v1/student_tasks/view' do
    get 'Retrieve a specific student task by ID' do
      # Tag for testing purposes.
      tags 'StudentTasks'
      produces 'application/json'

      # Define parameters to send with request.
      parameter name: 'id', in: :query, type: :Integer, required: true
      parameter name: 'Authorization', in: :header, type: :string

      # Ensure an authorized request with a valid ID gets a 200 response.
      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

spec/requests/api/v1/student_tasks_controller_spec.rb (https://github.com/ychen-207523/reimplementation-back-end/blob/main/spec/requests/api/v1/student_tasks_controller_spec.rb)

The test first scopes a path and defines a GET request. It specifies parameters for 'id' and 'Authorization' to simulate an authorized access to a specific student task. The `run_test!` function validates the response code and content:

      # Ensure an authorized request retrieves the task with correct attributes.
      response '200', 'authorized request has proper JSON schema' do
        let(:'Authorization') { "Bearer #{@token}" }
        let(:id) { 1 }

        run_test! do |response|
          data = JSON.parse(response.body)
          expect(data).to include('assignment')
          expect(data).to include('current_stage')
          expect(data).to include('stage_deadline')
          expect(data).to include('topic')
          expect(data['permission_granted']).to be_in([true, false])
        end
      end

spec/requests/api/v1/student_tasks_controller_spec.rb (https://github.com/ychen-207523/reimplementation-back-end/blob/main/spec/requests/api/v1/student_tasks_controller_spec.rb)

Here, we assert the correctness of the response as well as verify that the response includes essential attributes of the student task. We also ensure that these attributes are of the expected types and that the function handles partial or missing data accordingly.

Swagger UI

List
  /api/v1/student_tasks/list:
      get:
        summary: List all Student Tasks
        tags:
          - Student Tasks
        responses:
          '200':
            description: An array of student tasks

The Sagger UI is to check if the API request can be sent to list all student tasks correctly. The first step it to run Swagger UI and provide a valid bearerAuth, one can find the valid value in the network tab of the inspect tool. After the authencation is succeed, find the Student Tasks tags and click Try it out. Since list function does not require any parameter, click execute.
As shown in the picture, the result returns OK, meaning that the request is sent successfully.

View
  /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

This is the SwaggerUI test for view function. It takes a parameter of id in query to represent the participant id, and it calls the view function in the student_task_controller. Just as the list function, the first step is to provide a valid bearerAuth. And the authentication is succeed, click Try it out and provide a valid participant id to execute the test.
Based on the screenshot, the status is OK, suggesting that the request has been successfully sent. But please be noted that at current stage, the view page has not been implemented yet. So if one copy the url and paste it into the browser, it would still show 404 error.

Process

Project Board

We utilize a project board on GitHub in order to track and delegate our tasks that the reimplementation is comprised of.

project board

We initialize our tasks on the board and so that they can be tracked and our progress can be followed by each team member, regardless of their level of responsibility in a single task. That way, even if a team member isn't directly responsible for a sub-task, a single source of truth displays to everyone the state of the project at any point in time.

Moreover, our design documentation should allow us to create a roadmap and tasks before designing code, so that the course of action is clear and the tasks can be evenly delegated.

Code Quality

By laying out all of the incremental steps we have in order to reimplement the student task controller, we ensure that all boxes are checked in redesigning the software. Moreover, adequate comments throughout the code, and adherence to best practices will ensure our code is readable, robust, and inline with industry standards.

Moreover, sufficient testing and peer reviews should also ensure our code is up to the highest quality.

Communication

We communicate on a regular basis using a text group chat. Moreover, we have scheduled formal calls at least once a week to discuss current objectives and progress. Additionally we often jump on a less unscheduled calls to pair program and problem solve.

Team

Students:

David White (dawhite4@ncsu.edu)

Henry McKinney (hmckinn@ncsu.edu)

Yunfei Chen (ychen267@ncsu.edu)


Mentor:

Kashika Malick (kmalick@ncsu.edu)