CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round
Introduction
Current Behavior
When creating or editing an assignment, the Rubrics tab shows per-round dropdowns only if “Review rubric varies by round?” is checked. The number of rounds is manually entered on the Due Dates tab. The dropdown options are just predefined text entries; they’re not sourced from the questionnaires in the database.
Motivation
This project aims to automate the round count from due dates that are review deadlines, and load rubric dropdowns from real questionnaires, to keep the Rubrics tab in sync with the backend’s actual review deadlines and persisted questionnaire records for consistent behavior. From a user perspective, enabling true multi-round rubric support improves grading consistency, supports richer peer-review workflows, and aligns the feedback process with the instructional goals of the Expertiza platform — enhancing both evaluation accuracy and the overall educational experience.
Project Requirements
Functional Requirements
- Add a boolean attribute
vary_by_roundto theassignmentstable.
- When
vary_by_roundistrue, the Rubrics tab dynamically reflects the number of review rounds and displaysReview Round 1,Review Round 2, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.
- Infer the number of review rounds automatically by examining all
DueDateobjects associated with the assignment.
- Only deadlines whose
deadline_type_idequals2(review deadlines) should be counted.
- Store rubric–round associations in the
assignments_questionnairestable.
- Add a new column named
used_in_roundto indicate which rubric applies to which review round.
- When
vary_by_roundis disabled, retain the existing single-rubric interface.
Data Requirements
- The
assignments_questionnairestable must include an integer columnused_in_roundto track rubric usage per round.
- The
assignmentstable must include the boolean columnvary_by_roundindicating whether rubrics differ across rounds.
Test Requirements
- Write tests for the new functionality.
Implementations
Architecture Overview
Backend
assignments Table and assignment_questionnaires Table
- Added a boolean column
vary_by_roundwith defaultfalseto toggle per-round rubric behavior toassignmentstable. Change lives indb/migrate/20251125012619_add_vary_by_round_to_assignments.rb, with schema regeneration reflected indb/schema.rb. As theassignment_questionnairestable already has the columnused_in_round, we don't need to add extra code for this requirement.
class AddVaryByRoundToAssignments < ActiveRecord::Migration[8.0]
def change
add_column :assignments, :vary_by_round, :boolean, default: false, null: false
end
end
Review-Round Derivation
DueDatedefined aREVIEW_DEADLINE_TYPE_IDconstant inapp/models/due_date.rb, referring to review deadlines. This usesDueDate::REVIEW_DEADLINE_TYPE_IDso the round-count logic keys off the canonical review deadline marker, avoiding hard-coded magic numbers and ensuring the count stays consistent with how review due dates are modeled elsewhere.
REVIEW_DEADLINE_TYPE_ID = 2
- Enabled
Assignmentto infer review rounds from its due dates, falling back torounds_of_reviewsonly when no review deadlines exist, by adjusting the methodnum_review_roundsinapp/models/assignment.rb.
# Returns review round count: prefer the number of review due dates;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).
def num_review_rounds
review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count
review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)
end
- Updated existing method
varying_rubrics_by_round?inapp/models/assignment.rb, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics.- It first checks the assignment-level flag
vary_by_round. If this flag isfalse, the method immediately returnsfalse. - If
vary_by_roundistrue, the method queriesAssignmentQuestionnairefor at least one record with a non-nullused_in_roundvalue for the current assignment. - The method returns
trueonly when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. - To make this information easy to include in JSON responses, the non-predicate alias
varying_rubrics_by_roundsimply delegates tovarying_rubrics_by_round?, avoiding a question mark in the rendered JSON key.
- It first checks the assignment-level flag
# Returns true only if per-round rubrics are enabled on the assignment and at least one attached questionnaire specifies a round via used_in_round
def varying_rubrics_by_round?
return false unless vary_by_round
rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first
# Check if any rubric has a specified round
rubric_with_round.present?
end
# Alias without question mark for JSON rendering convenience
def varying_rubrics_by_round
varying_rubrics_by_round?
end
API Surface
In reimplementation-back-end/app/controllers/assignments_controller.rb, the show action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed num_review_rounds and varying_rubrics_by_round, and includes assignment_questionnaires with embedded questionnaire objects, along with due_dates.
The update path continues to accept vary_by_round, rounds_of_reviews, and nested assignment_questionnaires_attributes (including id, questionnaire_id, used_in_round, weight, and notification_limit), ensuring that per-round rubric edits are persisted correctly.
# GET /assignments/:id
def show
assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])
render json: assignment.as_json(
methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality
include: {
assignment_questionnaires: {
include: :questionnaire
},
due_dates: {}
}
)
end
In assignment_params, we now explicitly permit vary_by_round and rounds_of_reviews, alongside the nested assignment_questionnaires_attributes hash (id, questionnaire_id, used_in_round, questionnaire_weight, notification_limit, _destroy). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.
def assignment_params
params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,
assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])
end
Frontend
Data Transformations
1. AssignmentUtil.ts:
1.1 transformAssignmentResponse now maps the enriched GET /assignments/:id payload into form values. It extracts assignment_questionnaires (with embedded questionnaire), due_dates, the computed num_review_rounds, and the computed varying_rubrics_by_round (falling back to vary_by_round). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.
export const transformAssignmentResponse = (assignmentResponse: string) => {
const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);
const assignmentValues: IAssignmentFormValues = {
...
review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,
number_of_review_rounds: assignment.num_review_rounds,
due_dates: assignment.due_dates,
assignment_questionnaires: assignment.assignment_questionnaires,
};
return assignmentValues;
};
1.2 transformAssignmentRequest constructs assignment_questionnaires_attributes from the per-round form fields (e.g., questionnaire_round_X), preserves existing join IDs (assignment_questionnaire_id_X) during updates, and transmits vary_by_round and rounds_of_reviews based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.
export interface IAssignmentFormValues {
...
due_dates?: { id: number; deadline_type_id: number; round?: number }[];
assignment_questionnaires?: {
id: number;
used_in_round?: number;
questionnaire?: { id: number; name: string };
}[];
[key: string]: any;
}
export const transformAssignmentRequest = (values: IAssignmentFormValues) => {
// Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows
const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];
const roundCount = values.number_of_review_rounds ?? 0;
for (let i = 1; i <= roundCount; i += 1) {
const questionnaireId = values[`questionnaire_round_${i}`];
if (questionnaireId) {
const existingId = values[`assignment_questionnaire_id_${i}`];
assignmentQuestionnaires.push({
id: existingId,
questionnaire_id: questionnaireId,
used_in_round: i,
});
}
}
const assignment: IAssignmentRequest = {
...
vary_by_round: values.review_rubric_varies_by_round,
rounds_of_reviews: values.number_of_review_rounds,
assignment_questionnaires_attributes: assignmentQuestionnaires,
};
console.log(assignment);
return JSON.stringify({ assignment });
};
2. interfaces.ts exposes these response fields so type-checking stays accurate.
Form and Rubrics UI
In AssignmentEditor.tsx, formInitialValues is constructed from defaults when creating a new assignment (initialValues) and from loader data (assignmentData) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as questionnaire_round_X and assignment_questionnaire_id_X) from existing assignment_questionnaires, ensuring that the Rubrics dropdowns display the current rubric selections.
const roundSelections: Record<number, { id: number; name: string }> = {};
(assignmentData.assignment_questionnaires || []).forEach((aq: any) => {
if (aq.used_in_round && aq.questionnaire) {
roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };
}
});
// Build initial form values from existing assignment data (update) or defaults (create)
const formInitialValues: any = mode === "update" ? { ...assignmentData } : { ...initialValues };
if (mode === "update") {
// Prefill per-round questionnaire selections and ids
(assignmentData.assignment_questionnaires || []).forEach((aq: any) => {
if (aq.used_in_round && aq.questionnaire) {
formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;
formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;
}
});
}
Options for the rubric dropdowns now come from the full questionnaire list (assignmentData.questionnaires).
const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) => ({
label: q.name,
value: q.id,
}));
The number of round rows rendered is controlled by formik.values.number_of_review_rounds. When review_rubric_varies_by_round is true, a row is rendered per review round; otherwise, a single rubric row is displayed.
...
data={[
...(() => {
const rounds = formik.values.number_of_review_rounds ?? 0;
if (formik.values.review_rubric_varies_by_round) {
return Array.from({ length: rounds }, (_, i) => ([
{
id: i + 1,
title: `Review round ${i + 1}:`,
questionnaire_options: questionnaireOptions,
selected_questionnaire: roundSelections[i + 1]?.id,
questionnaire_type: 'dropdown',
},
// rest of the code
The submission process continues to use transformAssignmentRequest to construct and submit assignment_questionnaires_attributes, along with vary_by_round and rounds_of_reviews, ensuring that backend updates remain synchronized with UI selections.
Design Principles
Guiding Principles Behind the Backend Implementations
- Backward compatibility first: adding the
vary_by_roundflag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change). - Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from
DueDatedata (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines. - Separation of concerns: keeping business logic inside models/helpers (
AssignmentandDueDate), while the controller simply exposes the computed values. - Testability: introducing factories for every new entity that specs needed and exercised each new behavior in model/request specs, which enforces the contract for the new logic.
- Minimal API surface change: reusing existing endpoints, enriching responses instead of creating new routes, to limit churn for API consumers while still providing the data the frontend needs.
Guiding Principles Behind the Frontend Implementations
- Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.
- Resilience & sensible defaults: derive functions sanitize user input, fall back to backend metadata.
- Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.
Testing
Backend
RSpec
Testing Assignment Model
The following RSpec tests exercise the new behavior for computing review rounds and determining whether rubrics vary by round.
These definitions are shared by all test cases:
include RolesHelper
before(:all) { @roles = create_roles_hierarchy } # Create the full roles hierarchy once for creating the instructor role later
let(:institution) { Institution.create!(name: "NC State") } # All users belong to the same institution to satisfy foreign key constraints.
let(:instructor) { User.create!(name: "instructor", full_name: "Instructor User",
email: "instructor@example.com", password_digest: "password",
role_id: @roles[:instructor].id, institution_id: institution.id) }
1. Counts review due dates to determine number of rounds
This test verifies that num_review_rounds returns the correct number of review rounds by counting only review-type due dates (with deadline_type_id = DueDate::REVIEW_DEADLINE_TYPE_ID) attached to the assignment.
describe '#num_review_rounds' do
it 'counts review due dates to determine the number of rounds' do
assignment = Assignment.create!(name: 'Round Count', instructor: instructor, vary_by_round: true)
AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,
deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,
submission_allowed_id: 3, review_allowed_id: 3)
AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,
deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,
submission_allowed_id: 3, review_allowed_id: 3)
expect(assignment.num_review_rounds).to eq(2)
end
end
2. Ignores non-review deadlines when counting rounds
This test ensures that num_review_rounds ignores due dates that are not review deadlines. Even if multiple deadlines exist, only those with review deadline type should contribute to the round count.
describe '#num_review_rounds' do
it 'ignores non-review deadlines when counting rounds' do
assignment = Assignment.create!(name: 'Mixed Deadlines', instructor: instructor, vary_by_round: true)
AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,
deadline_type_id: 99,
submission_allowed_id: 3, review_allowed_id: 3)
AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,
deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,
submission_allowed_id: 3, review_allowed_id: 3)
expect(assignment.num_review_rounds).to eq(1)
end
end
3. Returns false when per-round rubrics are disabled
This test checks that varying_rubrics_by_round? returns false when vary_by_round is disabled on the assignment, even if a rubric is linked to a specific round via used_in_round.
describe '#varying_rubrics_by_round?' do
let(:questionnaire) {
Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,
questionnaire_type: 'ReviewQuestionnaire',
display_type: 'Review', min_question_score: 0,
max_question_score: 5)
}
it 'returns false when vary_by_round is disabled even if rounds exist' do
assignment = Assignment.create!(name: 'No Vary', instructor: instructor, vary_by_round: false)
AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)
expect(assignment.varying_rubrics_by_round?).to be false
end
end
4. Returns true when per-round rubrics are enabled and at least one round-specific rubric exists
This test confirms that varying_rubrics_by_round? returns true when vary_by_round is enabled and there is at least one AssignmentQuestionnaire with a non-nil used_in_round, indicating a round-specific rubric.
describe '#varying_rubrics_by_round?' do
let(:questionnaire) {
Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,
questionnaire_type: 'ReviewQuestionnaire',
display_type: 'Review', min_question_score: 0,
max_question_score: 5)
}
it 'returns true when vary_by_round is enabled and a round-specific rubric exists' do
assignment = Assignment.create!(name: 'Vary', instructor: instructor, vary_by_round: true)
AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)
expect(assignment.varying_rubrics_by_round?).to be true
end
end
Testing results
The tests have successfully passed.
Testing Assignment Controller
The following request specs validate that the backend correctly exposes assignment details, including round-varying rubrics, due dates, and computed attributes via the GET /assignments/{id} endpoint.
A questionnaire is created once to be associated with assignments used in test cases:
let!(:questionnaire) do
Questionnaire.create!(
name: "Review Rubric",
instructor: prof,
private: false,
min_question_score: 0,
max_question_score: 10,
questionnaire_type: "ReviewQuestionnaire"
)
end
1. Successful retrieval of assignment with rubrics and due dates
This test verifies that when an assignment has per-round rubrics enabled, the API returns:
- the assignment ID
- associated
assignment_questionnairesincluding embedded questionnaire details - review due dates
- the computed number of review rounds
varying_rubrics_by_round = true
# -------------------------------------------------------------------------
# GET /assignments/{id} (Show assignment)
# -------------------------------------------------------------------------
path '/assignments/{id}' do
parameter name: 'id', in: :path, type: :integer, description: 'Assignment ID'
get 'Show assignment details with rubrics and due dates' do
tags 'Assignments'
produces 'application/json'
parameter name: 'Content-Type', in: :header, type: :string
let('Content-Type') { 'application/json' }
response '200', 'assignment found' do
let(:id) { assignment.id }
before do
assignment.update!(vary_by_round: true)
AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)
AssignmentDueDate.create!(
parent: assignment,
due_at: Time.zone.now + 1.day,
deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,
submission_allowed_id: 1,
review_allowed_id: 1,
round: 1
)
end
run_test! do
data = JSON.parse(response.body)
expect(data['id']).to eq(assignment.id)
expect(data['assignment_questionnaires'].first['questionnaire']['id']).to eq(questionnaire.id)
expect(data['due_dates'].length).to eq(1)
expect(data['num_review_rounds']).to eq(1)
expect(data['varying_rubrics_by_round']).to eq(true)
end
end
2. Assignment not found case
This test ensures a 404 error is returned when requesting an assignment that does not exist.
response '404', 'assignment not found' do
let(:id) { 999 }
run_test! do
data = JSON.parse(response.body)
expect(response).to have_http_status(:not_found)
expect(data['error']).to eq('Assignment not found')
end
end
end
end
Testing results
The tests have successfully passed.
Swagger UI
We only enriched the existing GET/assignments/:id, so there is no new endpoints added for this project. Because the route itself didn't change and we didn't touch swagger/v1/swagger.yaml, so spot-checking through Swagger UI is not necessary.
Frontend
Vitest
The following Vitest/Jest-based frontend tests verify correct rendering and request construction for the updated Rubrics tab in the AssignmentEditor component, as well as correct payload generation in transformAssignmentRequest.
Each test initializes mock API hooks, Redux dispatch, and loader data to simulate the AssignmentEditor operating in update mode without making real network calls.
import React from "react";
import { render, screen, within } from "@testing-library/react";
import "@testing-library/jest-dom";
import { vi, beforeEach, describe, expect, it } from "vitest";
import AssignmentEditor from "./AssignmentEditor";
import { transformAssignmentRequest, IAssignmentFormValues } from "./AssignmentUtil";
// Mock useAPI to avoid real network calls
const sendRequestMock = vi.fn();
vi.mock("../../hooks/useAPI", () => ({
__esModule: true,
default: () => ({ data: null, error: null, sendRequest: sendRequestMock }),
}));
// Mock redux dispatch and selector
const dispatchMock = vi.fn();
vi.mock("react-redux", () => ({
useDispatch: () => dispatchMock,
useSelector: (selector: any) => selector({ authentication: { isAuthenticated: true } }),
}));
// Mock router context hooks
let loaderData: any;
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual<any>("react-router-dom");
return {
...actual,
useLoaderData: () => loaderData,
useLocation: () => ({ state: {} }),
useNavigate: () => vi.fn(),
};
});
1. Renders one rubric row per review round
This test verifies that when rubrics vary by round, the UI shows a separate dropdown row for each review round.
it("shows one row per review round when rubrics vary by round", () => {
loaderData = { ...baseAssignment };
render(<AssignmentEditor mode="update" />);
expect(screen.getByText("Review round 1:")).toBeInTheDocument();
expect(screen.getByText("Review round 2:")).toBeInTheDocument();
});
2. Shows a single rubric when per-round mode is disabled
This test ensures that disabling the review_rubric_varies_by_round flag results in only one rubric row, regardless of round count.
it("shows a single rubric row when rubrics do not vary by round", () => {
loaderData = { ...baseAssignment, review_rubric_varies_by_round: false };
render(<AssignmentEditor mode="update" />);
expect(screen.getByText("Review rubric:")).toBeInTheDocument();
expect(screen.queryByText("Review round 2:")).not.toBeInTheDocument();
});
3. Prefills selected rubric for each round
This test confirms that the dropdown values are populated from assignment_questionnaires in loader data, ensuring round-specific rubric persistence across edits.
it("prefills the selected questionnaire per round from loader data", () => {
loaderData = { ...baseAssignment };
render(<AssignmentEditor mode="update" />);
const round1Row = screen.getByText("Review round 1:").closest("tr");
const select = within(round1Row as HTMLElement).getByRole("combobox") as HTMLSelectElement;
expect(select.value).toBe("101");
});
4. Lists all questionnaires, including unlinked ones
This test checks that the dropdown displays all available questionnaires, not just those already linked to the assignment.
it("lists all available questionnaires, including unlinked ones", () => {
loaderData = { ...baseAssignment };
render(<AssignmentEditor mode="update" />);
const allOptions = screen.getAllByRole("option").map((opt) => opt.textContent);
expect(allOptions).toContain("Unlinked Rubric");
});
These tests below validate that the request payload is constructed correctly when form data is submitted.
5. Builds nested attributes for selected rounds
This test ensures transformAssignmentRequest generates the correct nested assignment_questionnaires_attributes, mapping selected rubrics to review rounds.
it("builds assignment_questionnaires_attributes for selected rounds", () => {
const values: IAssignmentFormValues = {
id: 1,
name: "Test Assignment",
directory_path: "assignment_1",
spec_location: "http://example.com",
private: false,
review_rubric_varies_by_round: true,
number_of_review_rounds: 2,
questionnaire_round_1: 101,
assignment_questionnaire_id_1: 10,
questionnaire_round_2: 102,
weights: [],
notification_limits: [],
use_date_updater: [],
submission_allowed: [],
review_allowed: [],
teammate_allowed: [],
metareview_allowed: [],
reminder: [],
};
const payload = JSON.parse(transformAssignmentRequest(values));
expect(payload.assignment.assignment_questionnaires_attributes).toEqual([
{ id: 10, questionnaire_id: 101, used_in_round: 1 },
{ questionnaire_id: 102, used_in_round: 2 },
]);
expect(payload.assignment.vary_by_round).toBe(true);
expect(payload.assignment.rounds_of_reviews).toBe(2);
});
6. Reuses existing ID and skips empty rounds
Ensures that a previously existing join row is preserved and that blank round selections are ignored.
it("includes existing id when present and skips rounds without selection", () => {
const values: IAssignmentFormValues = {
id: 1,
name: "Test Assignment",
questionnaire_round_1: 201,
assignment_questionnaire_id_1: 99,
review_rubric_varies_by_round: true,
number_of_review_rounds: 2,
weights: [],
notification_limits: [],
use_date_updater: [],
submission_allowed: [],
review_allowed: [],
teammate_allowed: [],
metareview_allowed: [],
reminder: [],
};
const payload = JSON.parse(transformAssignmentRequest(values));
expect(payload.assignment.assignment_questionnaires_attributes).toEqual([
{ id: 99, questionnaire_id: 201, used_in_round: 1 },
]);
});
7. Disables round-varying mode when unchecked
This test confirms that vary_by_round is set to false when the corresponding checkbox is disabled in the UI.
it("sets vary_by_round to false when checkbox is unchecked", () => {
const values: IAssignmentFormValues = {
id: 1,
name: "Test Assignment",
review_rubric_varies_by_round: false,
number_of_review_rounds: 1,
weights: [],
notification_limits: [],
use_date_updater: [],
submission_allowed: [],
review_allowed: [],
teammate_allowed: [],
metareview_allowed: [],
reminder: [],
};
const payload = JSON.parse(transformAssignmentRequest(values));
expect(payload.assignment.vary_by_round).toBe(false);
});
Testing results
To run only the test suite for the AssignmentEditor.tsx component, use:
npx vitest run src/pages/Assignments/AssignmentEditor.test.tsx --config vitest.config.mts
The test cases have successfully passed.
Links
- Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123
- Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235
- GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end
- GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end
- Demo Video - https://vimeo.com/1142815889?share=copy&fl=sv&fe=ci
Team
- Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)
- Iman Khan (ikhan7@ncsu.edu)
- Niranjan Rajendran (nrajend4@ncsu.edu)
- Yu Wang (ywang374@ncsu.edu)





