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://youtu.be/IidhwZ_PBq8

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: The frontend is the the continuation of E2429. The purpose of this improvement is to implement the features to retrieve the data of student tasks from the backend. 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 involves retrieving data from several tables. The first step is to access this data via an API. The name of the assignment is sourced from the 'assignment' table, while the course name is derived 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
      };
    });
  }



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 constructs a StudentTask instance from a Participant object, mapping participant attributes to task attributes for later reference.




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.



After we finish the implementation of list, we will move on to view. Here is the current design of view:

  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 current design is to provide detailed information about a specific sutdent task based on the participant id, then it gathers different information from the participant along with the pther required information such as topic and time line. Therefore, the student task is highly related with participant.
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.


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. The following cases will be tested:

  • 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.

Swagger UI

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)