<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Ywang374</id>
	<title>Expertiza_Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Ywang374"/>
	<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Special:Contributions/Ywang374"/>
	<updated>2026-08-11T01:11:49Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.41.0</generator>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167413</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167413"/>
		<updated>2025-12-03T04:44:09Z</updated>

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

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

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

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167339</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167339"/>
		<updated>2025-12-03T00:35:48Z</updated>

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

		<summary type="html">&lt;p&gt;Ywang374: /* Backend */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== assignments Table and assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Form and Rubrics UI ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;formInitialValues&amp;lt;/code&amp;gt; is constructed from defaults when creating a new assignment (&amp;lt;code&amp;gt;initialValues&amp;lt;/code&amp;gt;) and from loader data (&amp;lt;code&amp;gt;assignmentData&amp;lt;/code&amp;gt;) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) from existing &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt;, ensuring that the Rubrics dropdowns display the current rubric selections.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const roundSelections: Record&amp;lt;number, { id: number; name: string }&amp;gt; = {};&lt;br /&gt;
  (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
    if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
      roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Build initial form values from existing assignment data (update) or defaults (create)&lt;br /&gt;
  const formInitialValues: any = mode === &amp;quot;update&amp;quot; ? { ...assignmentData } : { ...initialValues };&lt;br /&gt;
&lt;br /&gt;
  if (mode === &amp;quot;update&amp;quot;) {&lt;br /&gt;
    // Prefill per-round questionnaire selections and ids&lt;br /&gt;
    (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
      if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
        formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;&lt;br /&gt;
        formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Options for the rubric dropdowns now come from the full questionnaire list (&amp;lt;code&amp;gt;assignmentData.questionnaires&amp;lt;/code&amp;gt;).&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) =&amp;gt; ({&lt;br /&gt;
    label: q.name,&lt;br /&gt;
    value: q.id,&lt;br /&gt;
  }));&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The number of round rows rendered is controlled by &amp;lt;code&amp;gt;formik.values.number_of_review_rounds&amp;lt;/code&amp;gt;. When &amp;lt;code&amp;gt;review_rubric_varies_by_round&amp;lt;/code&amp;gt; is true, a row is rendered per review round; otherwise, a single rubric row is displayed.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
...&lt;br /&gt;
data={[&lt;br /&gt;
                      ...(() =&amp;gt; {&lt;br /&gt;
                        const rounds = formik.values.number_of_review_rounds ?? 0;&lt;br /&gt;
                        if (formik.values.review_rubric_varies_by_round) {&lt;br /&gt;
                          return Array.from({ length: rounds }, (_, i) =&amp;gt; ([&lt;br /&gt;
                            {&lt;br /&gt;
                              id: i + 1,&lt;br /&gt;
                              title: `Review round ${i + 1}:`,&lt;br /&gt;
                              questionnaire_options: questionnaireOptions,&lt;br /&gt;
                              selected_questionnaire: roundSelections[i + 1]?.id,&lt;br /&gt;
                              questionnaire_type: 'dropdown',&lt;br /&gt;
                            },&lt;br /&gt;
                       // rest of the code&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The submission process continues to use &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; to construct and submit &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt;, along with &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, ensuring that backend updates remain synchronized with UI selections.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;), while the controller simply exposes the computed values. &lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== Testing Assignment Model =====&lt;br /&gt;
&lt;br /&gt;
The following RSpec tests exercise the new behavior for computing review rounds and determining whether rubrics vary by round.&lt;br /&gt;
&lt;br /&gt;
These definitions are shared by all test cases:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
include RolesHelper&lt;br /&gt;
before(:all) { @roles = create_roles_hierarchy } # Create the full roles hierarchy once for creating the instructor role later&lt;br /&gt;
let(:institution) { Institution.create!(name: &amp;quot;NC State&amp;quot;) } # All users belong to the same institution to satisfy foreign key constraints.&lt;br /&gt;
let(:instructor) { User.create!(name: &amp;quot;instructor&amp;quot;, full_name: &amp;quot;Instructor User&amp;quot;,&lt;br /&gt;
                                email: &amp;quot;instructor@example.com&amp;quot;, password_digest: &amp;quot;password&amp;quot;,&lt;br /&gt;
                                role_id: @roles[:instructor].id, institution_id: institution.id) }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. '''Counts review due dates to determine number of rounds'''&lt;br /&gt;
&lt;br /&gt;
This test verifies that &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; returns the correct number of review rounds by counting only review-type due dates (with &amp;lt;code&amp;gt;deadline_type_id = DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt;) attached to the assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#num_review_rounds' do&lt;br /&gt;
  it 'counts review due dates to determine the number of rounds' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Round Count', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.num_review_rounds).to eq(2)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. '''Ignores non-review deadlines when counting rounds'''&lt;br /&gt;
&lt;br /&gt;
This test ensures that &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; 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.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#num_review_rounds' do&lt;br /&gt;
  it 'ignores non-review deadlines when counting rounds' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Mixed Deadlines', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,&lt;br /&gt;
                              deadline_type_id: 99,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.num_review_rounds).to eq(1)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. '''Returns false when per-round rubrics are disabled'''&lt;br /&gt;
&lt;br /&gt;
This test checks that &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; when &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled on the assignment, even if a rubric is linked to a specific round via &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#varying_rubrics_by_round?' do&lt;br /&gt;
  let(:questionnaire) {&lt;br /&gt;
    Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,&lt;br /&gt;
                          questionnaire_type: 'ReviewQuestionnaire',&lt;br /&gt;
                          display_type: 'Review', min_question_score: 0,&lt;br /&gt;
                          max_question_score: 5)&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  it 'returns false when vary_by_round is disabled even if rounds exist' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'No Vary', instructor: instructor, vary_by_round: false)&lt;br /&gt;
    AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.varying_rubrics_by_round?).to be false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. '''Returns true when per-round rubrics are enabled and at least one round-specific rubric exists'''&lt;br /&gt;
&lt;br /&gt;
This test confirms that &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; when &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is enabled and there is at least one &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; with a non-nil &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, indicating a round-specific rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#varying_rubrics_by_round?' do&lt;br /&gt;
  let(:questionnaire) {&lt;br /&gt;
    Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,&lt;br /&gt;
                          questionnaire_type: 'ReviewQuestionnaire',&lt;br /&gt;
                          display_type: 'Review', min_question_score: 0,&lt;br /&gt;
                          max_question_score: 5)&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  it 'returns true when vary_by_round is enabled and a round-specific rubric exists' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Vary', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.varying_rubrics_by_round?).to be true&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Testing results'''&lt;br /&gt;
&lt;br /&gt;
The tests have successfully passed.&lt;br /&gt;
&lt;br /&gt;
[[File:Backendmodel.jpg|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== Testing Assignment Controller =====&lt;br /&gt;
&lt;br /&gt;
The following request specs validate that the backend correctly exposes assignment details, including round-varying rubrics, due dates, and computed attributes via the &amp;lt;code&amp;gt;GET /assignments/{id}&amp;lt;/code&amp;gt; endpoint.&lt;br /&gt;
&lt;br /&gt;
A questionnaire is created once to be associated with assignments used in test cases:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
let!(:questionnaire) do&lt;br /&gt;
  Questionnaire.create!(&lt;br /&gt;
    name: &amp;quot;Review Rubric&amp;quot;,&lt;br /&gt;
    instructor: prof,&lt;br /&gt;
    private: false,&lt;br /&gt;
    min_question_score: 0,&lt;br /&gt;
    max_question_score: 10,&lt;br /&gt;
    questionnaire_type: &amp;quot;ReviewQuestionnaire&amp;quot;&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. '''Successful retrieval of assignment with rubrics and due dates'''&lt;br /&gt;
&lt;br /&gt;
This test verifies that when an assignment has per-round rubrics enabled, the API returns:&lt;br /&gt;
* the assignment ID&lt;br /&gt;
* associated &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; including embedded questionnaire details&lt;br /&gt;
* review due dates&lt;br /&gt;
* the computed number of review rounds&lt;br /&gt;
* &amp;lt;code&amp;gt;varying_rubrics_by_round = true&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# -------------------------------------------------------------------------&lt;br /&gt;
# GET /assignments/{id} (Show assignment)&lt;br /&gt;
# -------------------------------------------------------------------------&lt;br /&gt;
path '/assignments/{id}' do&lt;br /&gt;
  parameter name: 'id', in: :path, type: :integer, description: 'Assignment ID'&lt;br /&gt;
&lt;br /&gt;
  get 'Show assignment details with rubrics and due dates' do&lt;br /&gt;
    tags 'Assignments'&lt;br /&gt;
    produces 'application/json'&lt;br /&gt;
    parameter name: 'Content-Type', in: :header, type: :string&lt;br /&gt;
    let('Content-Type') { 'application/json' }&lt;br /&gt;
&lt;br /&gt;
    response '200', 'assignment found' do&lt;br /&gt;
      let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        assignment.update!(vary_by_round: true)&lt;br /&gt;
        AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
        AssignmentDueDate.create!(&lt;br /&gt;
          parent: assignment,&lt;br /&gt;
          due_at: Time.zone.now + 1.day,&lt;br /&gt;
          deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
          submission_allowed_id: 1,&lt;br /&gt;
          review_allowed_id: 1,&lt;br /&gt;
          round: 1&lt;br /&gt;
        )&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
        expect(data['id']).to eq(assignment.id)&lt;br /&gt;
        expect(data['assignment_questionnaires'].first['questionnaire']['id']).to eq(questionnaire.id)&lt;br /&gt;
        expect(data['due_dates'].length).to eq(1)&lt;br /&gt;
        expect(data['num_review_rounds']).to eq(1)&lt;br /&gt;
        expect(data['varying_rubrics_by_round']).to eq(true)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. '''Assignment not found case'''&lt;br /&gt;
&lt;br /&gt;
This test ensures a &amp;lt;code&amp;gt;404&amp;lt;/code&amp;gt; error is returned when requesting an assignment that does not exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
    response '404', 'assignment not found' do&lt;br /&gt;
      let(:id) { 999 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(response).to have_http_status(:not_found)&lt;br /&gt;
        expect(data['error']).to eq('Assignment not found')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Testing results'''&lt;br /&gt;
&lt;br /&gt;
The tests have successfully passed.&lt;br /&gt;
&lt;br /&gt;
[[File:Backendcontroller.jpg|800px]]&lt;br /&gt;
&lt;br /&gt;
==== Swagger UI ====&lt;br /&gt;
We only enriched the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself didn't change and we didn't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167335</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167335"/>
		<updated>2025-12-03T00:17:11Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* 1. RSpec */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== assignments Table and assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Form and Rubrics UI ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;formInitialValues&amp;lt;/code&amp;gt; is constructed from defaults when creating a new assignment (&amp;lt;code&amp;gt;initialValues&amp;lt;/code&amp;gt;) and from loader data (&amp;lt;code&amp;gt;assignmentData&amp;lt;/code&amp;gt;) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) from existing &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt;, ensuring that the Rubrics dropdowns display the current rubric selections.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const roundSelections: Record&amp;lt;number, { id: number; name: string }&amp;gt; = {};&lt;br /&gt;
  (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
    if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
      roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Build initial form values from existing assignment data (update) or defaults (create)&lt;br /&gt;
  const formInitialValues: any = mode === &amp;quot;update&amp;quot; ? { ...assignmentData } : { ...initialValues };&lt;br /&gt;
&lt;br /&gt;
  if (mode === &amp;quot;update&amp;quot;) {&lt;br /&gt;
    // Prefill per-round questionnaire selections and ids&lt;br /&gt;
    (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
      if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
        formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;&lt;br /&gt;
        formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Options for the rubric dropdowns now come from the full questionnaire list (&amp;lt;code&amp;gt;assignmentData.questionnaires&amp;lt;/code&amp;gt;).&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) =&amp;gt; ({&lt;br /&gt;
    label: q.name,&lt;br /&gt;
    value: q.id,&lt;br /&gt;
  }));&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The number of round rows rendered is controlled by &amp;lt;code&amp;gt;formik.values.number_of_review_rounds&amp;lt;/code&amp;gt;. When &amp;lt;code&amp;gt;review_rubric_varies_by_round&amp;lt;/code&amp;gt; is true, a row is rendered per review round; otherwise, a single rubric row is displayed.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
...&lt;br /&gt;
data={[&lt;br /&gt;
                      ...(() =&amp;gt; {&lt;br /&gt;
                        const rounds = formik.values.number_of_review_rounds ?? 0;&lt;br /&gt;
                        if (formik.values.review_rubric_varies_by_round) {&lt;br /&gt;
                          return Array.from({ length: rounds }, (_, i) =&amp;gt; ([&lt;br /&gt;
                            {&lt;br /&gt;
                              id: i + 1,&lt;br /&gt;
                              title: `Review round ${i + 1}:`,&lt;br /&gt;
                              questionnaire_options: questionnaireOptions,&lt;br /&gt;
                              selected_questionnaire: roundSelections[i + 1]?.id,&lt;br /&gt;
                              questionnaire_type: 'dropdown',&lt;br /&gt;
                            },&lt;br /&gt;
                       // rest of the code&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The submission process continues to use &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; to construct and submit &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt;, along with &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, ensuring that backend updates remain synchronized with UI selections.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;), while the controller simply exposes the computed values. &lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== Testing Assignment Model =====&lt;br /&gt;
&lt;br /&gt;
The following RSpec tests exercise the new behavior for computing review rounds and determining whether rubrics vary by round.&lt;br /&gt;
&lt;br /&gt;
These definitions are shared by all test cases:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
include RolesHelper&lt;br /&gt;
before(:all) { @roles = create_roles_hierarchy } # Create the full roles hierarchy once for creating the instructor role later&lt;br /&gt;
let(:institution) { Institution.create!(name: &amp;quot;NC State&amp;quot;) } # All users belong to the same institution to satisfy foreign key constraints.&lt;br /&gt;
let(:instructor) { User.create!(name: &amp;quot;instructor&amp;quot;, full_name: &amp;quot;Instructor User&amp;quot;,&lt;br /&gt;
                                email: &amp;quot;instructor@example.com&amp;quot;, password_digest: &amp;quot;password&amp;quot;,&lt;br /&gt;
                                role_id: @roles[:instructor].id, institution_id: institution.id) }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. '''Counts review due dates to determine number of rounds'''&lt;br /&gt;
&lt;br /&gt;
This test verifies that &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; returns the correct number of review rounds by counting only review-type due dates (with &amp;lt;code&amp;gt;deadline_type_id = DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt;) attached to the assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#num_review_rounds' do&lt;br /&gt;
  it 'counts review due dates to determine the number of rounds' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Round Count', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.num_review_rounds).to eq(2)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. '''Ignores non-review deadlines when counting rounds'''&lt;br /&gt;
&lt;br /&gt;
This test ensures that &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; 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.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#num_review_rounds' do&lt;br /&gt;
  it 'ignores non-review deadlines when counting rounds' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Mixed Deadlines', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,&lt;br /&gt;
                              deadline_type_id: 99,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.num_review_rounds).to eq(1)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. '''Returns false when per-round rubrics are disabled'''&lt;br /&gt;
&lt;br /&gt;
This test checks that &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; when &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled on the assignment, even if a rubric is linked to a specific round via &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#varying_rubrics_by_round?' do&lt;br /&gt;
  let(:questionnaire) {&lt;br /&gt;
    Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,&lt;br /&gt;
                          questionnaire_type: 'ReviewQuestionnaire',&lt;br /&gt;
                          display_type: 'Review', min_question_score: 0,&lt;br /&gt;
                          max_question_score: 5)&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  it 'returns false when vary_by_round is disabled even if rounds exist' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'No Vary', instructor: instructor, vary_by_round: false)&lt;br /&gt;
    AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.varying_rubrics_by_round?).to be false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. '''Returns true when per-round rubrics are enabled and at least one round-specific rubric exists'''&lt;br /&gt;
&lt;br /&gt;
This test confirms that &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; when &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is enabled and there is at least one &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; with a non-nil &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, indicating a round-specific rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#varying_rubrics_by_round?' do&lt;br /&gt;
  let(:questionnaire) {&lt;br /&gt;
    Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,&lt;br /&gt;
                          questionnaire_type: 'ReviewQuestionnaire',&lt;br /&gt;
                          display_type: 'Review', min_question_score: 0,&lt;br /&gt;
                          max_question_score: 5)&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  it 'returns true when vary_by_round is enabled and a round-specific rubric exists' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Vary', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.varying_rubrics_by_round?).to be true&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Testing results'''&lt;br /&gt;
The tests have successfully passed.&lt;br /&gt;
&lt;br /&gt;
[[File:Backendmodel.jpg|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== Testing Assignment Controller =====&lt;br /&gt;
&lt;br /&gt;
The following request specs validate that the backend correctly exposes assignment details, including round-varying rubrics, due dates, and computed attributes via the &amp;lt;code&amp;gt;GET /assignments/{id}&amp;lt;/code&amp;gt; endpoint.&lt;br /&gt;
&lt;br /&gt;
A questionnaire is created once to be associated with assignments used in test cases:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
let!(:questionnaire) do&lt;br /&gt;
  Questionnaire.create!(&lt;br /&gt;
    name: &amp;quot;Review Rubric&amp;quot;,&lt;br /&gt;
    instructor: prof,&lt;br /&gt;
    private: false,&lt;br /&gt;
    min_question_score: 0,&lt;br /&gt;
    max_question_score: 10,&lt;br /&gt;
    questionnaire_type: &amp;quot;ReviewQuestionnaire&amp;quot;&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. '''Successful retrieval of assignment with rubrics and due dates'''&lt;br /&gt;
&lt;br /&gt;
This test verifies that when an assignment has per-round rubrics enabled, the API returns:&lt;br /&gt;
* the assignment ID&lt;br /&gt;
* associated &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; including embedded questionnaire details&lt;br /&gt;
* review due dates&lt;br /&gt;
* the computed number of review rounds&lt;br /&gt;
* &amp;lt;code&amp;gt;varying_rubrics_by_round = true&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# -------------------------------------------------------------------------&lt;br /&gt;
# GET /assignments/{id} (Show assignment)&lt;br /&gt;
# -------------------------------------------------------------------------&lt;br /&gt;
path '/assignments/{id}' do&lt;br /&gt;
  parameter name: 'id', in: :path, type: :integer, description: 'Assignment ID'&lt;br /&gt;
&lt;br /&gt;
  get 'Show assignment details with rubrics and due dates' do&lt;br /&gt;
    tags 'Assignments'&lt;br /&gt;
    produces 'application/json'&lt;br /&gt;
    parameter name: 'Content-Type', in: :header, type: :string&lt;br /&gt;
    let('Content-Type') { 'application/json' }&lt;br /&gt;
&lt;br /&gt;
    response '200', 'assignment found' do&lt;br /&gt;
      let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        assignment.update!(vary_by_round: true)&lt;br /&gt;
        AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
        AssignmentDueDate.create!(&lt;br /&gt;
          parent: assignment,&lt;br /&gt;
          due_at: Time.zone.now + 1.day,&lt;br /&gt;
          deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
          submission_allowed_id: 1,&lt;br /&gt;
          review_allowed_id: 1,&lt;br /&gt;
          round: 1&lt;br /&gt;
        )&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
        expect(data['id']).to eq(assignment.id)&lt;br /&gt;
        expect(data['assignment_questionnaires'].first['questionnaire']['id']).to eq(questionnaire.id)&lt;br /&gt;
        expect(data['due_dates'].length).to eq(1)&lt;br /&gt;
        expect(data['num_review_rounds']).to eq(1)&lt;br /&gt;
        expect(data['varying_rubrics_by_round']).to eq(true)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. '''Assignment not found case'''&lt;br /&gt;
&lt;br /&gt;
This test ensures a &amp;lt;code&amp;gt;404&amp;lt;/code&amp;gt; error is returned when requesting an assignment that does not exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
    response '404', 'assignment not found' do&lt;br /&gt;
      let(:id) { 999 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(response).to have_http_status(:not_found)&lt;br /&gt;
        expect(data['error']).to eq('Assignment not found')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Testing results'''&lt;br /&gt;
The tests have successfully passed.&lt;br /&gt;
&lt;br /&gt;
[[File:Backendcontroller.jpg|700px]]&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:Backendcontroller.jpg&amp;diff=167334</id>
		<title>File:Backendcontroller.jpg</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:Backendcontroller.jpg&amp;diff=167334"/>
		<updated>2025-12-03T00:16:25Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167333</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167333"/>
		<updated>2025-12-03T00:15:44Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* 1. RSpec */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== assignments Table and assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Form and Rubrics UI ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;formInitialValues&amp;lt;/code&amp;gt; is constructed from defaults when creating a new assignment (&amp;lt;code&amp;gt;initialValues&amp;lt;/code&amp;gt;) and from loader data (&amp;lt;code&amp;gt;assignmentData&amp;lt;/code&amp;gt;) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) from existing &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt;, ensuring that the Rubrics dropdowns display the current rubric selections.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const roundSelections: Record&amp;lt;number, { id: number; name: string }&amp;gt; = {};&lt;br /&gt;
  (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
    if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
      roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Build initial form values from existing assignment data (update) or defaults (create)&lt;br /&gt;
  const formInitialValues: any = mode === &amp;quot;update&amp;quot; ? { ...assignmentData } : { ...initialValues };&lt;br /&gt;
&lt;br /&gt;
  if (mode === &amp;quot;update&amp;quot;) {&lt;br /&gt;
    // Prefill per-round questionnaire selections and ids&lt;br /&gt;
    (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
      if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
        formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;&lt;br /&gt;
        formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Options for the rubric dropdowns now come from the full questionnaire list (&amp;lt;code&amp;gt;assignmentData.questionnaires&amp;lt;/code&amp;gt;).&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) =&amp;gt; ({&lt;br /&gt;
    label: q.name,&lt;br /&gt;
    value: q.id,&lt;br /&gt;
  }));&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The number of round rows rendered is controlled by &amp;lt;code&amp;gt;formik.values.number_of_review_rounds&amp;lt;/code&amp;gt;. When &amp;lt;code&amp;gt;review_rubric_varies_by_round&amp;lt;/code&amp;gt; is true, a row is rendered per review round; otherwise, a single rubric row is displayed.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
...&lt;br /&gt;
data={[&lt;br /&gt;
                      ...(() =&amp;gt; {&lt;br /&gt;
                        const rounds = formik.values.number_of_review_rounds ?? 0;&lt;br /&gt;
                        if (formik.values.review_rubric_varies_by_round) {&lt;br /&gt;
                          return Array.from({ length: rounds }, (_, i) =&amp;gt; ([&lt;br /&gt;
                            {&lt;br /&gt;
                              id: i + 1,&lt;br /&gt;
                              title: `Review round ${i + 1}:`,&lt;br /&gt;
                              questionnaire_options: questionnaireOptions,&lt;br /&gt;
                              selected_questionnaire: roundSelections[i + 1]?.id,&lt;br /&gt;
                              questionnaire_type: 'dropdown',&lt;br /&gt;
                            },&lt;br /&gt;
                       // rest of the code&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The submission process continues to use &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; to construct and submit &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt;, along with &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, ensuring that backend updates remain synchronized with UI selections.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;), while the controller simply exposes the computed values. &lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== Testing Assignment Model =====&lt;br /&gt;
&lt;br /&gt;
The following RSpec tests exercise the new behavior for computing review rounds and determining whether rubrics vary by round.&lt;br /&gt;
&lt;br /&gt;
These definitions are shared by all test cases:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
include RolesHelper&lt;br /&gt;
before(:all) { @roles = create_roles_hierarchy } # Create the full roles hierarchy once for creating the instructor role later&lt;br /&gt;
let(:institution) { Institution.create!(name: &amp;quot;NC State&amp;quot;) } # All users belong to the same institution to satisfy foreign key constraints.&lt;br /&gt;
let(:instructor) { User.create!(name: &amp;quot;instructor&amp;quot;, full_name: &amp;quot;Instructor User&amp;quot;,&lt;br /&gt;
                                email: &amp;quot;instructor@example.com&amp;quot;, password_digest: &amp;quot;password&amp;quot;,&lt;br /&gt;
                                role_id: @roles[:instructor].id, institution_id: institution.id) }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. '''Counts review due dates to determine number of rounds'''&lt;br /&gt;
&lt;br /&gt;
This test verifies that &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; returns the correct number of review rounds by counting only review-type due dates (with &amp;lt;code&amp;gt;deadline_type_id = DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt;) attached to the assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#num_review_rounds' do&lt;br /&gt;
  it 'counts review due dates to determine the number of rounds' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Round Count', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.num_review_rounds).to eq(2)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. '''Ignores non-review deadlines when counting rounds'''&lt;br /&gt;
&lt;br /&gt;
This test ensures that &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; 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.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#num_review_rounds' do&lt;br /&gt;
  it 'ignores non-review deadlines when counting rounds' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Mixed Deadlines', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,&lt;br /&gt;
                              deadline_type_id: 99,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.num_review_rounds).to eq(1)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. '''Returns false when per-round rubrics are disabled'''&lt;br /&gt;
&lt;br /&gt;
This test checks that &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; when &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled on the assignment, even if a rubric is linked to a specific round via &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#varying_rubrics_by_round?' do&lt;br /&gt;
  let(:questionnaire) {&lt;br /&gt;
    Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,&lt;br /&gt;
                          questionnaire_type: 'ReviewQuestionnaire',&lt;br /&gt;
                          display_type: 'Review', min_question_score: 0,&lt;br /&gt;
                          max_question_score: 5)&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  it 'returns false when vary_by_round is disabled even if rounds exist' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'No Vary', instructor: instructor, vary_by_round: false)&lt;br /&gt;
    AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.varying_rubrics_by_round?).to be false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. '''Returns true when per-round rubrics are enabled and at least one round-specific rubric exists'''&lt;br /&gt;
&lt;br /&gt;
This test confirms that &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; when &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is enabled and there is at least one &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; with a non-nil &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, indicating a round-specific rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#varying_rubrics_by_round?' do&lt;br /&gt;
  let(:questionnaire) {&lt;br /&gt;
    Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,&lt;br /&gt;
                          questionnaire_type: 'ReviewQuestionnaire',&lt;br /&gt;
                          display_type: 'Review', min_question_score: 0,&lt;br /&gt;
                          max_question_score: 5)&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  it 'returns true when vary_by_round is enabled and a round-specific rubric exists' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Vary', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.varying_rubrics_by_round?).to be true&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Testing results'''&lt;br /&gt;
The tests have successfully passed.&lt;br /&gt;
&lt;br /&gt;
[[File:Backendmodel.jpg|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== Testing Assignment Controller =====&lt;br /&gt;
&lt;br /&gt;
The following request specs validate that the backend correctly exposes assignment details, including round-varying rubrics, due dates, and computed attributes via the &amp;lt;code&amp;gt;GET /assignments/{id}&amp;lt;/code&amp;gt; endpoint.&lt;br /&gt;
&lt;br /&gt;
A questionnaire is created once to be associated with assignments used in test cases:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
let!(:questionnaire) do&lt;br /&gt;
  Questionnaire.create!(&lt;br /&gt;
    name: &amp;quot;Review Rubric&amp;quot;,&lt;br /&gt;
    instructor: prof,&lt;br /&gt;
    private: false,&lt;br /&gt;
    min_question_score: 0,&lt;br /&gt;
    max_question_score: 10,&lt;br /&gt;
    questionnaire_type: &amp;quot;ReviewQuestionnaire&amp;quot;&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. '''Successful retrieval of assignment with rubrics and due dates'''&lt;br /&gt;
&lt;br /&gt;
This test verifies that when an assignment has per-round rubrics enabled, the API returns:&lt;br /&gt;
* the assignment ID&lt;br /&gt;
* associated &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; including embedded questionnaire details&lt;br /&gt;
* review due dates&lt;br /&gt;
* the computed number of review rounds&lt;br /&gt;
* &amp;lt;code&amp;gt;varying_rubrics_by_round = true&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# -------------------------------------------------------------------------&lt;br /&gt;
# GET /assignments/{id} (Show assignment)&lt;br /&gt;
# -------------------------------------------------------------------------&lt;br /&gt;
path '/assignments/{id}' do&lt;br /&gt;
  parameter name: 'id', in: :path, type: :integer, description: 'Assignment ID'&lt;br /&gt;
&lt;br /&gt;
  get 'Show assignment details with rubrics and due dates' do&lt;br /&gt;
    tags 'Assignments'&lt;br /&gt;
    produces 'application/json'&lt;br /&gt;
    parameter name: 'Content-Type', in: :header, type: :string&lt;br /&gt;
    let('Content-Type') { 'application/json' }&lt;br /&gt;
&lt;br /&gt;
    response '200', 'assignment found' do&lt;br /&gt;
      let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        assignment.update!(vary_by_round: true)&lt;br /&gt;
        AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
        AssignmentDueDate.create!(&lt;br /&gt;
          parent: assignment,&lt;br /&gt;
          due_at: Time.zone.now + 1.day,&lt;br /&gt;
          deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
          submission_allowed_id: 1,&lt;br /&gt;
          review_allowed_id: 1,&lt;br /&gt;
          round: 1&lt;br /&gt;
        )&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
        expect(data['id']).to eq(assignment.id)&lt;br /&gt;
        expect(data['assignment_questionnaires'].first['questionnaire']['id']).to eq(questionnaire.id)&lt;br /&gt;
        expect(data['due_dates'].length).to eq(1)&lt;br /&gt;
        expect(data['num_review_rounds']).to eq(1)&lt;br /&gt;
        expect(data['varying_rubrics_by_round']).to eq(true)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. '''Assignment not found case'''&lt;br /&gt;
&lt;br /&gt;
This test ensures a &amp;lt;code&amp;gt;404&amp;lt;/code&amp;gt; error is returned when requesting an assignment that does not exist.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
    response '404', 'assignment not found' do&lt;br /&gt;
      let(:id) { 999 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(response).to have_http_status(:not_found)&lt;br /&gt;
        expect(data['error']).to eq('Assignment not found')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Testing results'''&lt;br /&gt;
The tests have successfully passed.&lt;br /&gt;
&lt;br /&gt;
[[File:Backendcontroller.jpg|700px]]&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:Backendmodel.jpg&amp;diff=167332</id>
		<title>File:Backendmodel.jpg</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:Backendmodel.jpg&amp;diff=167332"/>
		<updated>2025-12-03T00:11:40Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167331</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167331"/>
		<updated>2025-12-03T00:11:02Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* 1. RSpec */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== assignments Table and assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Form and Rubrics UI ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;formInitialValues&amp;lt;/code&amp;gt; is constructed from defaults when creating a new assignment (&amp;lt;code&amp;gt;initialValues&amp;lt;/code&amp;gt;) and from loader data (&amp;lt;code&amp;gt;assignmentData&amp;lt;/code&amp;gt;) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) from existing &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt;, ensuring that the Rubrics dropdowns display the current rubric selections.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const roundSelections: Record&amp;lt;number, { id: number; name: string }&amp;gt; = {};&lt;br /&gt;
  (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
    if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
      roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Build initial form values from existing assignment data (update) or defaults (create)&lt;br /&gt;
  const formInitialValues: any = mode === &amp;quot;update&amp;quot; ? { ...assignmentData } : { ...initialValues };&lt;br /&gt;
&lt;br /&gt;
  if (mode === &amp;quot;update&amp;quot;) {&lt;br /&gt;
    // Prefill per-round questionnaire selections and ids&lt;br /&gt;
    (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
      if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
        formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;&lt;br /&gt;
        formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Options for the rubric dropdowns now come from the full questionnaire list (&amp;lt;code&amp;gt;assignmentData.questionnaires&amp;lt;/code&amp;gt;).&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) =&amp;gt; ({&lt;br /&gt;
    label: q.name,&lt;br /&gt;
    value: q.id,&lt;br /&gt;
  }));&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The number of round rows rendered is controlled by &amp;lt;code&amp;gt;formik.values.number_of_review_rounds&amp;lt;/code&amp;gt;. When &amp;lt;code&amp;gt;review_rubric_varies_by_round&amp;lt;/code&amp;gt; is true, a row is rendered per review round; otherwise, a single rubric row is displayed.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
...&lt;br /&gt;
data={[&lt;br /&gt;
                      ...(() =&amp;gt; {&lt;br /&gt;
                        const rounds = formik.values.number_of_review_rounds ?? 0;&lt;br /&gt;
                        if (formik.values.review_rubric_varies_by_round) {&lt;br /&gt;
                          return Array.from({ length: rounds }, (_, i) =&amp;gt; ([&lt;br /&gt;
                            {&lt;br /&gt;
                              id: i + 1,&lt;br /&gt;
                              title: `Review round ${i + 1}:`,&lt;br /&gt;
                              questionnaire_options: questionnaireOptions,&lt;br /&gt;
                              selected_questionnaire: roundSelections[i + 1]?.id,&lt;br /&gt;
                              questionnaire_type: 'dropdown',&lt;br /&gt;
                            },&lt;br /&gt;
                       // rest of the code&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The submission process continues to use &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; to construct and submit &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt;, along with &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, ensuring that backend updates remain synchronized with UI selections.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;), while the controller simply exposes the computed values. &lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== Testing Assignment Model =====&lt;br /&gt;
&lt;br /&gt;
The following RSpec tests exercise the new behavior for computing review rounds and determining whether rubrics vary by round.&lt;br /&gt;
&lt;br /&gt;
These definitions are shared by all test cases:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
include RolesHelper&lt;br /&gt;
before(:all) { @roles = create_roles_hierarchy } # Create the full roles hierarchy once for creating the instructor role later&lt;br /&gt;
let(:institution) { Institution.create!(name: &amp;quot;NC State&amp;quot;) } # All users belong to the same institution to satisfy foreign key constraints.&lt;br /&gt;
let(:instructor) { User.create!(name: &amp;quot;instructor&amp;quot;, full_name: &amp;quot;Instructor User&amp;quot;,&lt;br /&gt;
                                email: &amp;quot;instructor@example.com&amp;quot;, password_digest: &amp;quot;password&amp;quot;,&lt;br /&gt;
                                role_id: @roles[:instructor].id, institution_id: institution.id) }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. '''Counts review due dates to determine number of rounds'''&lt;br /&gt;
&lt;br /&gt;
This test verifies that &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; returns the correct number of review rounds by counting only review-type due dates (with &amp;lt;code&amp;gt;deadline_type_id = DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt;) attached to the assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#num_review_rounds' do&lt;br /&gt;
  it 'counts review due dates to determine the number of rounds' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Round Count', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.num_review_rounds).to eq(2)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. '''Ignores non-review deadlines when counting rounds'''&lt;br /&gt;
&lt;br /&gt;
This test ensures that &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; 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.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#num_review_rounds' do&lt;br /&gt;
  it 'ignores non-review deadlines when counting rounds' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Mixed Deadlines', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,&lt;br /&gt;
                              deadline_type_id: 99,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.num_review_rounds).to eq(1)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. '''Returns false when per-round rubrics are disabled'''&lt;br /&gt;
&lt;br /&gt;
This test checks that &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; when &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled on the assignment, even if a rubric is linked to a specific round via &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#varying_rubrics_by_round?' do&lt;br /&gt;
  let(:questionnaire) {&lt;br /&gt;
    Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,&lt;br /&gt;
                          questionnaire_type: 'ReviewQuestionnaire',&lt;br /&gt;
                          display_type: 'Review', min_question_score: 0,&lt;br /&gt;
                          max_question_score: 5)&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  it 'returns false when vary_by_round is disabled even if rounds exist' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'No Vary', instructor: instructor, vary_by_round: false)&lt;br /&gt;
    AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.varying_rubrics_by_round?).to be false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. '''Returns true when per-round rubrics are enabled and at least one round-specific rubric exists'''&lt;br /&gt;
&lt;br /&gt;
This test confirms that &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; when &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is enabled and there is at least one &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; with a non-nil &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, indicating a round-specific rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#varying_rubrics_by_round?' do&lt;br /&gt;
  let(:questionnaire) {&lt;br /&gt;
    Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,&lt;br /&gt;
                          questionnaire_type: 'ReviewQuestionnaire',&lt;br /&gt;
                          display_type: 'Review', min_question_score: 0,&lt;br /&gt;
                          max_question_score: 5)&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  it 'returns true when vary_by_round is enabled and a round-specific rubric exists' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Vary', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.varying_rubrics_by_round?).to be true&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Testing results'''&lt;br /&gt;
The tests have successfully passed.&lt;br /&gt;
[[File:backendmodel.jpg]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== Testing Assignment Controller =====&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167330</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167330"/>
		<updated>2025-12-03T00:03:42Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Backend */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== assignments Table and assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Form and Rubrics UI ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;formInitialValues&amp;lt;/code&amp;gt; is constructed from defaults when creating a new assignment (&amp;lt;code&amp;gt;initialValues&amp;lt;/code&amp;gt;) and from loader data (&amp;lt;code&amp;gt;assignmentData&amp;lt;/code&amp;gt;) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) from existing &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt;, ensuring that the Rubrics dropdowns display the current rubric selections.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const roundSelections: Record&amp;lt;number, { id: number; name: string }&amp;gt; = {};&lt;br /&gt;
  (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
    if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
      roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Build initial form values from existing assignment data (update) or defaults (create)&lt;br /&gt;
  const formInitialValues: any = mode === &amp;quot;update&amp;quot; ? { ...assignmentData } : { ...initialValues };&lt;br /&gt;
&lt;br /&gt;
  if (mode === &amp;quot;update&amp;quot;) {&lt;br /&gt;
    // Prefill per-round questionnaire selections and ids&lt;br /&gt;
    (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
      if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
        formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;&lt;br /&gt;
        formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Options for the rubric dropdowns now come from the full questionnaire list (&amp;lt;code&amp;gt;assignmentData.questionnaires&amp;lt;/code&amp;gt;).&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) =&amp;gt; ({&lt;br /&gt;
    label: q.name,&lt;br /&gt;
    value: q.id,&lt;br /&gt;
  }));&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The number of round rows rendered is controlled by &amp;lt;code&amp;gt;formik.values.number_of_review_rounds&amp;lt;/code&amp;gt;. When &amp;lt;code&amp;gt;review_rubric_varies_by_round&amp;lt;/code&amp;gt; is true, a row is rendered per review round; otherwise, a single rubric row is displayed.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
...&lt;br /&gt;
data={[&lt;br /&gt;
                      ...(() =&amp;gt; {&lt;br /&gt;
                        const rounds = formik.values.number_of_review_rounds ?? 0;&lt;br /&gt;
                        if (formik.values.review_rubric_varies_by_round) {&lt;br /&gt;
                          return Array.from({ length: rounds }, (_, i) =&amp;gt; ([&lt;br /&gt;
                            {&lt;br /&gt;
                              id: i + 1,&lt;br /&gt;
                              title: `Review round ${i + 1}:`,&lt;br /&gt;
                              questionnaire_options: questionnaireOptions,&lt;br /&gt;
                              selected_questionnaire: roundSelections[i + 1]?.id,&lt;br /&gt;
                              questionnaire_type: 'dropdown',&lt;br /&gt;
                            },&lt;br /&gt;
                       // rest of the code&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The submission process continues to use &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; to construct and submit &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt;, along with &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, ensuring that backend updates remain synchronized with UI selections.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;), while the controller simply exposes the computed values. &lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== Testing Assignment Model =====&lt;br /&gt;
&lt;br /&gt;
The following RSpec tests exercise the new behavior for computing review rounds and determining whether rubrics vary by round.&lt;br /&gt;
&lt;br /&gt;
These definitions are shared by all test cases:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
include RolesHelper&lt;br /&gt;
before(:all) { @roles = create_roles_hierarchy } # Create the full roles hierarchy once for creating the instructor role later&lt;br /&gt;
let(:institution) { Institution.create!(name: &amp;quot;NC State&amp;quot;) } # All users belong to the same institution to satisfy foreign key constraints.&lt;br /&gt;
let(:instructor) { User.create!(name: &amp;quot;instructor&amp;quot;, full_name: &amp;quot;Instructor User&amp;quot;,&lt;br /&gt;
                                email: &amp;quot;instructor@example.com&amp;quot;, password_digest: &amp;quot;password&amp;quot;,&lt;br /&gt;
                                role_id: @roles[:instructor].id, institution_id: institution.id) }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. '''Counts review due dates to determine number of rounds'''&lt;br /&gt;
&lt;br /&gt;
This test verifies that &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; returns the correct number of review rounds by counting only review-type due dates (with &amp;lt;code&amp;gt;deadline_type_id = DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt;) attached to the assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#num_review_rounds' do&lt;br /&gt;
  it 'counts review due dates to determine the number of rounds' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Round Count', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.num_review_rounds).to eq(2)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. '''Ignores non-review deadlines when counting rounds'''&lt;br /&gt;
&lt;br /&gt;
This test ensures that &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; 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.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#num_review_rounds' do&lt;br /&gt;
  it 'ignores non-review deadlines when counting rounds' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Mixed Deadlines', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 1.day.from_now,&lt;br /&gt;
                              deadline_type_id: 99,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
    AssignmentDueDate.create!(parent: assignment, due_at: 2.days.from_now,&lt;br /&gt;
                              deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID,&lt;br /&gt;
                              submission_allowed_id: 3, review_allowed_id: 3)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.num_review_rounds).to eq(1)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. '''Returns false when per-round rubrics are disabled'''&lt;br /&gt;
&lt;br /&gt;
This test checks that &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; when &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled on the assignment, even if a rubric is linked to a specific round via &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#varying_rubrics_by_round?' do&lt;br /&gt;
  let(:questionnaire) {&lt;br /&gt;
    Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,&lt;br /&gt;
                          questionnaire_type: 'ReviewQuestionnaire',&lt;br /&gt;
                          display_type: 'Review', min_question_score: 0,&lt;br /&gt;
                          max_question_score: 5)&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  it 'returns false when vary_by_round is disabled even if rounds exist' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'No Vary', instructor: instructor, vary_by_round: false)&lt;br /&gt;
    AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.varying_rubrics_by_round?).to be false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. '''Returns true when per-round rubrics are enabled and at least one round-specific rubric exists'''&lt;br /&gt;
&lt;br /&gt;
This test confirms that &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; when &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is enabled and there is at least one &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; with a non-nil &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, indicating a round-specific rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe '#varying_rubrics_by_round?' do&lt;br /&gt;
  let(:questionnaire) {&lt;br /&gt;
    Questionnaire.create!(name: 'Review Q', instructor_id: instructor.id,&lt;br /&gt;
                          questionnaire_type: 'ReviewQuestionnaire',&lt;br /&gt;
                          display_type: 'Review', min_question_score: 0,&lt;br /&gt;
                          max_question_score: 5)&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  it 'returns true when vary_by_round is enabled and a round-specific rubric exists' do&lt;br /&gt;
    assignment = Assignment.create!(name: 'Vary', instructor: instructor, vary_by_round: true)&lt;br /&gt;
    AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire, used_in_round: 1)&lt;br /&gt;
&lt;br /&gt;
    expect(assignment.varying_rubrics_by_round?).to be true&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== Testing Assignment Controller =====&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167327</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167327"/>
		<updated>2025-12-02T23:56:19Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Assignments Table and Assignment_questionnaires Table */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== assignments Table and assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Form and Rubrics UI ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;formInitialValues&amp;lt;/code&amp;gt; is constructed from defaults when creating a new assignment (&amp;lt;code&amp;gt;initialValues&amp;lt;/code&amp;gt;) and from loader data (&amp;lt;code&amp;gt;assignmentData&amp;lt;/code&amp;gt;) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) from existing &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt;, ensuring that the Rubrics dropdowns display the current rubric selections.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const roundSelections: Record&amp;lt;number, { id: number; name: string }&amp;gt; = {};&lt;br /&gt;
  (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
    if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
      roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Build initial form values from existing assignment data (update) or defaults (create)&lt;br /&gt;
  const formInitialValues: any = mode === &amp;quot;update&amp;quot; ? { ...assignmentData } : { ...initialValues };&lt;br /&gt;
&lt;br /&gt;
  if (mode === &amp;quot;update&amp;quot;) {&lt;br /&gt;
    // Prefill per-round questionnaire selections and ids&lt;br /&gt;
    (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
      if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
        formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;&lt;br /&gt;
        formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Options for the rubric dropdowns now come from the full questionnaire list (&amp;lt;code&amp;gt;assignmentData.questionnaires&amp;lt;/code&amp;gt;).&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) =&amp;gt; ({&lt;br /&gt;
    label: q.name,&lt;br /&gt;
    value: q.id,&lt;br /&gt;
  }));&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The number of round rows rendered is controlled by &amp;lt;code&amp;gt;formik.values.number_of_review_rounds&amp;lt;/code&amp;gt;. When &amp;lt;code&amp;gt;review_rubric_varies_by_round&amp;lt;/code&amp;gt; is true, a row is rendered per review round; otherwise, a single rubric row is displayed.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
...&lt;br /&gt;
data={[&lt;br /&gt;
                      ...(() =&amp;gt; {&lt;br /&gt;
                        const rounds = formik.values.number_of_review_rounds ?? 0;&lt;br /&gt;
                        if (formik.values.review_rubric_varies_by_round) {&lt;br /&gt;
                          return Array.from({ length: rounds }, (_, i) =&amp;gt; ([&lt;br /&gt;
                            {&lt;br /&gt;
                              id: i + 1,&lt;br /&gt;
                              title: `Review round ${i + 1}:`,&lt;br /&gt;
                              questionnaire_options: questionnaireOptions,&lt;br /&gt;
                              selected_questionnaire: roundSelections[i + 1]?.id,&lt;br /&gt;
                              questionnaire_type: 'dropdown',&lt;br /&gt;
                            },&lt;br /&gt;
                       // rest of the code&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The submission process continues to use &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; to construct and submit &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt;, along with &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, ensuring that backend updates remain synchronized with UI selections.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;), while the controller simply exposes the computed values. &lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167326</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167326"/>
		<updated>2025-12-02T23:55:04Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Design Principles */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Form and Rubrics UI ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;formInitialValues&amp;lt;/code&amp;gt; is constructed from defaults when creating a new assignment (&amp;lt;code&amp;gt;initialValues&amp;lt;/code&amp;gt;) and from loader data (&amp;lt;code&amp;gt;assignmentData&amp;lt;/code&amp;gt;) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) from existing &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt;, ensuring that the Rubrics dropdowns display the current rubric selections.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const roundSelections: Record&amp;lt;number, { id: number; name: string }&amp;gt; = {};&lt;br /&gt;
  (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
    if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
      roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Build initial form values from existing assignment data (update) or defaults (create)&lt;br /&gt;
  const formInitialValues: any = mode === &amp;quot;update&amp;quot; ? { ...assignmentData } : { ...initialValues };&lt;br /&gt;
&lt;br /&gt;
  if (mode === &amp;quot;update&amp;quot;) {&lt;br /&gt;
    // Prefill per-round questionnaire selections and ids&lt;br /&gt;
    (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
      if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
        formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;&lt;br /&gt;
        formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Options for the rubric dropdowns now come from the full questionnaire list (&amp;lt;code&amp;gt;assignmentData.questionnaires&amp;lt;/code&amp;gt;).&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) =&amp;gt; ({&lt;br /&gt;
    label: q.name,&lt;br /&gt;
    value: q.id,&lt;br /&gt;
  }));&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The number of round rows rendered is controlled by &amp;lt;code&amp;gt;formik.values.number_of_review_rounds&amp;lt;/code&amp;gt;. When &amp;lt;code&amp;gt;review_rubric_varies_by_round&amp;lt;/code&amp;gt; is true, a row is rendered per review round; otherwise, a single rubric row is displayed.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
...&lt;br /&gt;
data={[&lt;br /&gt;
                      ...(() =&amp;gt; {&lt;br /&gt;
                        const rounds = formik.values.number_of_review_rounds ?? 0;&lt;br /&gt;
                        if (formik.values.review_rubric_varies_by_round) {&lt;br /&gt;
                          return Array.from({ length: rounds }, (_, i) =&amp;gt; ([&lt;br /&gt;
                            {&lt;br /&gt;
                              id: i + 1,&lt;br /&gt;
                              title: `Review round ${i + 1}:`,&lt;br /&gt;
                              questionnaire_options: questionnaireOptions,&lt;br /&gt;
                              selected_questionnaire: roundSelections[i + 1]?.id,&lt;br /&gt;
                              questionnaire_type: 'dropdown',&lt;br /&gt;
                            },&lt;br /&gt;
                       // rest of the code&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The submission process continues to use &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; to construct and submit &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt;, along with &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, ensuring that backend updates remain synchronized with UI selections.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;), while the controller simply exposes the computed values. &lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Implementations ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167323</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167323"/>
		<updated>2025-12-02T23:51:47Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Helper Module */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Form and Rubrics UI ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;formInitialValues&amp;lt;/code&amp;gt; is constructed from defaults when creating a new assignment (&amp;lt;code&amp;gt;initialValues&amp;lt;/code&amp;gt;) and from loader data (&amp;lt;code&amp;gt;assignmentData&amp;lt;/code&amp;gt;) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) from existing &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt;, ensuring that the Rubrics dropdowns display the current rubric selections.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const roundSelections: Record&amp;lt;number, { id: number; name: string }&amp;gt; = {};&lt;br /&gt;
  (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
    if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
      roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Build initial form values from existing assignment data (update) or defaults (create)&lt;br /&gt;
  const formInitialValues: any = mode === &amp;quot;update&amp;quot; ? { ...assignmentData } : { ...initialValues };&lt;br /&gt;
&lt;br /&gt;
  if (mode === &amp;quot;update&amp;quot;) {&lt;br /&gt;
    // Prefill per-round questionnaire selections and ids&lt;br /&gt;
    (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
      if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
        formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;&lt;br /&gt;
        formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Options for the rubric dropdowns now come from the full questionnaire list (&amp;lt;code&amp;gt;assignmentData.questionnaires&amp;lt;/code&amp;gt;).&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) =&amp;gt; ({&lt;br /&gt;
    label: q.name,&lt;br /&gt;
    value: q.id,&lt;br /&gt;
  }));&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The number of round rows rendered is controlled by &amp;lt;code&amp;gt;formik.values.number_of_review_rounds&amp;lt;/code&amp;gt;. When &amp;lt;code&amp;gt;review_rubric_varies_by_round&amp;lt;/code&amp;gt; is true, a row is rendered per review round; otherwise, a single rubric row is displayed.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
...&lt;br /&gt;
data={[&lt;br /&gt;
                      ...(() =&amp;gt; {&lt;br /&gt;
                        const rounds = formik.values.number_of_review_rounds ?? 0;&lt;br /&gt;
                        if (formik.values.review_rubric_varies_by_round) {&lt;br /&gt;
                          return Array.from({ length: rounds }, (_, i) =&amp;gt; ([&lt;br /&gt;
                            {&lt;br /&gt;
                              id: i + 1,&lt;br /&gt;
                              title: `Review round ${i + 1}:`,&lt;br /&gt;
                              questionnaire_options: questionnaireOptions,&lt;br /&gt;
                              selected_questionnaire: roundSelections[i + 1]?.id,&lt;br /&gt;
                              questionnaire_type: 'dropdown',&lt;br /&gt;
                            },&lt;br /&gt;
                       // rest of the code&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The submission process continues to use &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; to construct and submit &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt;, along with &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, ensuring that backend updates remain synchronized with UI selections.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167321</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167321"/>
		<updated>2025-12-02T23:49:51Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Frontend */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Form and Rubrics UI ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;formInitialValues&amp;lt;/code&amp;gt; is constructed from defaults when creating a new assignment (&amp;lt;code&amp;gt;initialValues&amp;lt;/code&amp;gt;) and from loader data (&amp;lt;code&amp;gt;assignmentData&amp;lt;/code&amp;gt;) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) from existing &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt;, ensuring that the Rubrics dropdowns display the current rubric selections.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const roundSelections: Record&amp;lt;number, { id: number; name: string }&amp;gt; = {};&lt;br /&gt;
  (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
    if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
      roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Build initial form values from existing assignment data (update) or defaults (create)&lt;br /&gt;
  const formInitialValues: any = mode === &amp;quot;update&amp;quot; ? { ...assignmentData } : { ...initialValues };&lt;br /&gt;
&lt;br /&gt;
  if (mode === &amp;quot;update&amp;quot;) {&lt;br /&gt;
    // Prefill per-round questionnaire selections and ids&lt;br /&gt;
    (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
      if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
        formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;&lt;br /&gt;
        formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Options for the rubric dropdowns now come from the full questionnaire list (&amp;lt;code&amp;gt;assignmentData.questionnaires&amp;lt;/code&amp;gt;).&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
  const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) =&amp;gt; ({&lt;br /&gt;
    label: q.name,&lt;br /&gt;
    value: q.id,&lt;br /&gt;
  }));&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The number of round rows rendered is controlled by &amp;lt;code&amp;gt;formik.values.number_of_review_rounds&amp;lt;/code&amp;gt;. When &amp;lt;code&amp;gt;review_rubric_varies_by_round&amp;lt;/code&amp;gt; is true, a row is rendered per review round; otherwise, a single rubric row is displayed.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
...&lt;br /&gt;
data={[&lt;br /&gt;
                      ...(() =&amp;gt; {&lt;br /&gt;
                        const rounds = formik.values.number_of_review_rounds ?? 0;&lt;br /&gt;
                        if (formik.values.review_rubric_varies_by_round) {&lt;br /&gt;
                          return Array.from({ length: rounds }, (_, i) =&amp;gt; ([&lt;br /&gt;
                            {&lt;br /&gt;
                              id: i + 1,&lt;br /&gt;
                              title: `Review round ${i + 1}:`,&lt;br /&gt;
                              questionnaire_options: questionnaireOptions,&lt;br /&gt;
                              selected_questionnaire: roundSelections[i + 1]?.id,&lt;br /&gt;
                              questionnaire_type: 'dropdown',&lt;br /&gt;
                            },&lt;br /&gt;
                       // rest of the code&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The submission process continues to use &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; to construct and submit &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt;, along with &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, ensuring that backend updates remain synchronized with UI selections.&lt;br /&gt;
&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167319</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167319"/>
		<updated>2025-12-02T23:48:49Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Frontend */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Form and Rubrics UI ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;formInitialValues&amp;lt;/code&amp;gt; is constructed from defaults when creating a new assignment (&amp;lt;code&amp;gt;initialValues&amp;lt;/code&amp;gt;) and from loader data (&amp;lt;code&amp;gt;assignmentData&amp;lt;/code&amp;gt;) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) from existing &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt;, ensuring that the Rubrics dropdowns display the current rubric selections.&lt;br /&gt;
&lt;br /&gt;
  const roundSelections: Record&amp;lt;number, { id: number; name: string }&amp;gt; = {};&lt;br /&gt;
  (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
    if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
      roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Build initial form values from existing assignment data (update) or defaults (create)&lt;br /&gt;
  const formInitialValues: any = mode === &amp;quot;update&amp;quot; ? { ...assignmentData } : { ...initialValues };&lt;br /&gt;
&lt;br /&gt;
  if (mode === &amp;quot;update&amp;quot;) {&lt;br /&gt;
    // Prefill per-round questionnaire selections and ids&lt;br /&gt;
    (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
      if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
        formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;&lt;br /&gt;
        formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
Options for the rubric dropdowns now come from the full questionnaire list (&amp;lt;code&amp;gt;assignmentData.questionnaires&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
  const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) =&amp;gt; ({&lt;br /&gt;
    label: q.name,&lt;br /&gt;
    value: q.id,&lt;br /&gt;
  }));&lt;br /&gt;
&lt;br /&gt;
The number of round rows rendered is controlled by &amp;lt;code&amp;gt;formik.values.number_of_review_rounds&amp;lt;/code&amp;gt;. When &amp;lt;code&amp;gt;review_rubric_varies_by_round&amp;lt;/code&amp;gt; is true, a row is rendered per review round; otherwise, a single rubric row is displayed.&lt;br /&gt;
&lt;br /&gt;
...&lt;br /&gt;
data={[&lt;br /&gt;
                      ...(() =&amp;gt; {&lt;br /&gt;
                        const rounds = formik.values.number_of_review_rounds ?? 0;&lt;br /&gt;
                        if (formik.values.review_rubric_varies_by_round) {&lt;br /&gt;
                          return Array.from({ length: rounds }, (_, i) =&amp;gt; ([&lt;br /&gt;
                            {&lt;br /&gt;
                              id: i + 1,&lt;br /&gt;
                              title: `Review round ${i + 1}:`,&lt;br /&gt;
                              questionnaire_options: questionnaireOptions,&lt;br /&gt;
                              selected_questionnaire: roundSelections[i + 1]?.id,&lt;br /&gt;
                              questionnaire_type: 'dropdown',&lt;br /&gt;
                            },&lt;br /&gt;
                       // rest of the code&lt;br /&gt;
&lt;br /&gt;
The submission process continues to use &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; to construct and submit &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt;, along with &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, ensuring that backend updates remain synchronized with UI selections.&lt;br /&gt;
&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167317</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167317"/>
		<updated>2025-12-02T23:47:39Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Editor Rendering */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Form and Rubrics UI ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;formInitialValues&amp;lt;/code&amp;gt; is constructed from defaults when creating a new assignment (&amp;lt;code&amp;gt;initialValues&amp;lt;/code&amp;gt;) and from loader data (&amp;lt;code&amp;gt;assignmentData&amp;lt;/code&amp;gt;) when updating an existing one. In update mode, the editor also pre-seeds per-round fields (such as &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) from existing &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt;, ensuring that the Rubrics dropdowns display the current rubric selections.&lt;br /&gt;
&lt;br /&gt;
  const roundSelections: Record&amp;lt;number, { id: number; name: string }&amp;gt; = {};&lt;br /&gt;
  (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
    if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
      roundSelections[aq.used_in_round] = { id: aq.questionnaire.id, name: aq.questionnaire.name };&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Build initial form values from existing assignment data (update) or defaults (create)&lt;br /&gt;
  const formInitialValues: any = mode === &amp;quot;update&amp;quot; ? { ...assignmentData } : { ...initialValues };&lt;br /&gt;
&lt;br /&gt;
  if (mode === &amp;quot;update&amp;quot;) {&lt;br /&gt;
    // Prefill per-round questionnaire selections and ids&lt;br /&gt;
    (assignmentData.assignment_questionnaires || []).forEach((aq: any) =&amp;gt; {&lt;br /&gt;
      if (aq.used_in_round &amp;amp;&amp;amp; aq.questionnaire) {&lt;br /&gt;
        formInitialValues[`questionnaire_round_${aq.used_in_round}`] = aq.questionnaire.id;&lt;br /&gt;
        formInitialValues[`assignment_questionnaire_id_${aq.used_in_round}`] = aq.id;&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
Options for the rubric dropdowns now come from the full questionnaire list (&amp;lt;code&amp;gt;assignmentData.questionnaires&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
  const questionnaireOptions = (assignmentData.questionnaires || []).map((q: any) =&amp;gt; ({&lt;br /&gt;
    label: q.name,&lt;br /&gt;
    value: q.id,&lt;br /&gt;
  }));&lt;br /&gt;
&lt;br /&gt;
The number of round rows rendered is controlled by &amp;lt;code&amp;gt;formik.values.number_of_review_rounds&amp;lt;/code&amp;gt;. When &amp;lt;code&amp;gt;review_rubric_varies_by_round&amp;lt;/code&amp;gt; is true, a row is rendered per review round; otherwise, a single rubric row is displayed.&lt;br /&gt;
&lt;br /&gt;
...&lt;br /&gt;
data={[&lt;br /&gt;
                      ...(() =&amp;gt; {&lt;br /&gt;
                        const rounds = formik.values.number_of_review_rounds ?? 0;&lt;br /&gt;
                        if (formik.values.review_rubric_varies_by_round) {&lt;br /&gt;
                          return Array.from({ length: rounds }, (_, i) =&amp;gt; ([&lt;br /&gt;
                            {&lt;br /&gt;
                              id: i + 1,&lt;br /&gt;
                              title: `Review round ${i + 1}:`,&lt;br /&gt;
                              questionnaire_options: questionnaireOptions,&lt;br /&gt;
                              selected_questionnaire: roundSelections[i + 1]?.id,&lt;br /&gt;
                              questionnaire_type: 'dropdown',&lt;br /&gt;
                            },&lt;br /&gt;
                       // rest of the code&lt;br /&gt;
&lt;br /&gt;
The submission process continues to use &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; to construct and submit &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt;, along with &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, ensuring that backend updates remain synchronized with UI selections.&lt;br /&gt;
&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167309</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167309"/>
		<updated>2025-12-02T23:36:37Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Data Transformations */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
1.1 &amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1.2 &amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167308</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167308"/>
		<updated>2025-12-02T23:35:41Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Implementations */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentResponse = (assignmentResponse: string) =&amp;gt; {&lt;br /&gt;
  const assignment: IAssignmentResponse = JSON.parse(assignmentResponse);&lt;br /&gt;
  const assignmentValues: IAssignmentFormValues = {&lt;br /&gt;
    ...&lt;br /&gt;
    review_rubric_varies_by_round: assignment.varying_rubrics_by_round ?? assignment.vary_by_round,&lt;br /&gt;
    number_of_review_rounds: assignment.num_review_rounds,&lt;br /&gt;
    due_dates: assignment.due_dates,&lt;br /&gt;
    assignment_questionnaires: assignment.assignment_questionnaires,&lt;br /&gt;
  };&lt;br /&gt;
  return assignmentValues;&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
export interface IAssignmentFormValues {&lt;br /&gt;
  ...&lt;br /&gt;
  due_dates?: { id: number; deadline_type_id: number; round?: number }[];&lt;br /&gt;
  assignment_questionnaires?: {&lt;br /&gt;
    id: number;&lt;br /&gt;
    used_in_round?: number;&lt;br /&gt;
    questionnaire?: { id: number; name: string };&lt;br /&gt;
  }[];&lt;br /&gt;
  [key: string]: any;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167307</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167307"/>
		<updated>2025-12-02T23:32:12Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Frontend */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Data Transformations ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt;: &lt;br /&gt;
&amp;lt;code&amp;gt;transformAssignmentResponse&amp;lt;/code&amp;gt; now maps the enriched &amp;lt;code&amp;gt;GET /assignments/:id&amp;lt;/code&amp;gt; payload into form values. It extracts &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; (with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt;), &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;, the computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and the computed &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; (falling back to &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;). This allows the Rubrics tab to prefill per-round rubric selections and to determine the correct number of review rounds.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;transformAssignmentRequest&amp;lt;/code&amp;gt; constructs &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; from the per-round form fields (e.g., &amp;lt;code&amp;gt;questionnaire_round_X&amp;lt;/code&amp;gt;), preserves existing join IDs (&amp;lt;code&amp;gt;assignment_questionnaire_id_X&amp;lt;/code&amp;gt;) during updates, and transmits &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; based on the checkbox state and round count. This ensures that per-round rubric assignments are persisted using Rails’ nested attributes mechanism.&lt;br /&gt;
&lt;br /&gt;
export const transformAssignmentRequest = (values: IAssignmentFormValues) =&amp;gt; {&lt;br /&gt;
  // Build nested attributes for assignment_questionnaires from the per-round form fields to create or update corresponding rows&lt;br /&gt;
  const assignmentQuestionnaires: { id?: number; questionnaire_id: number; used_in_round: number }[] = [];&lt;br /&gt;
  const roundCount = values.number_of_review_rounds ?? 0;&lt;br /&gt;
  for (let i = 1; i &amp;lt;= roundCount; i += 1) {&lt;br /&gt;
    const questionnaireId = values[`questionnaire_round_${i}`];&lt;br /&gt;
    if (questionnaireId) {&lt;br /&gt;
      const existingId = values[`assignment_questionnaire_id_${i}`];&lt;br /&gt;
      assignmentQuestionnaires.push({&lt;br /&gt;
        id: existingId,&lt;br /&gt;
        questionnaire_id: questionnaireId,&lt;br /&gt;
        used_in_round: i,&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  const assignment: IAssignmentRequest = {&lt;br /&gt;
    ...&lt;br /&gt;
    vary_by_round: values.review_rubric_varies_by_round,&lt;br /&gt;
    rounds_of_reviews: values.number_of_review_rounds,&lt;br /&gt;
    assignment_questionnaires_attributes: assignmentQuestionnaires,&lt;br /&gt;
&lt;br /&gt;
  };&lt;br /&gt;
  console.log(assignment);&lt;br /&gt;
  return JSON.stringify({ assignment });&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; exposes these response fields so type-checking stays accurate.&lt;br /&gt;
&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167306</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167306"/>
		<updated>2025-12-02T23:16:48Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* API Surface */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                     assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167305</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167305"/>
		<updated>2025-12-02T23:15:14Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Backend */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                       assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167304</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167304"/>
		<updated>2025-12-02T23:14:44Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* API Surface */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;reimplementation-back-end/app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; action was expanded so that the frontend can drive the Rubrics tab based on backend data. The response now renders computed &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt;, and includes &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; with embedded &amp;lt;code&amp;gt;questionnaire&amp;lt;/code&amp;gt; objects, along with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;update&amp;lt;/code&amp;gt; path continues to accept &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, and nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; (including &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;weight&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;), ensuring that per-round rubric edits are persisted correctly.&lt;br /&gt;
&lt;br /&gt;
# GET /assignments/:id&lt;br /&gt;
def show&lt;br /&gt;
  assignment = Assignment.includes(assignment_questionnaires: :questionnaire, due_dates: {}).find(params[:id])&lt;br /&gt;
  render json: assignment.as_json(&lt;br /&gt;
    methods: [:num_review_rounds, :varying_rubrics_by_round], # for review rubrics vary by round functionality&lt;br /&gt;
    include: {&lt;br /&gt;
      assignment_questionnaires: {&lt;br /&gt;
        include: :questionnaire&lt;br /&gt;
      },&lt;br /&gt;
      due_dates: {}&lt;br /&gt;
    }&lt;br /&gt;
  )&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;assignment_params&amp;lt;/code&amp;gt;, we now explicitly permit &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt;, alongside the nested &amp;lt;code&amp;gt;assignment_questionnaires_attributes&amp;lt;/code&amp;gt; hash (&amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;notification_limit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;_destroy&amp;lt;/code&amp;gt;). This enables toggling per-round rubric functionality and saving per-round questionnaire associations, weights, and notification limits through a single update request.&lt;br /&gt;
&lt;br /&gt;
def assignment_params&lt;br /&gt;
  params.require(:assignment).permit(:title, :description, :name, :directory_path, :spec_location, :vary_by_round, :rounds_of_reviews,&lt;br /&gt;
                                       assignment_questionnaires_attributes: [:id, :questionnaire_id, :used_in_round, :questionnaire_weight, :notification_limit, :_destroy])&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167302</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167302"/>
		<updated>2025-12-02T23:06:48Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Backend */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Added a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change lives in &amp;lt;code&amp;gt;db/migrate/20251125012619_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
class AddVaryByRoundToAssignments &amp;lt; ActiveRecord::Migration[8.0]&lt;br /&gt;
  def change&lt;br /&gt;
    add_column :assignments, :vary_by_round, :boolean, default: false, null: false&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; defined a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant in &amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;, referring to review deadlines. This uses &amp;lt;code&amp;gt;DueDate::REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; so 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.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REVIEW_DEADLINE_TYPE_ID = 2 &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Enabled &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist, by adjusting the method &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Returns review round count: prefer the number of review due dates; &lt;br /&gt;
# if none exist, fall back to the persisted rounds_of_reviews column (or 0).&lt;br /&gt;
def num_review_rounds&lt;br /&gt;
  review_rounds = due_dates.where(deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID).count&lt;br /&gt;
  review_rounds.positive? ? review_rounds : (rounds_of_reviews || 0)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*Updated existing method &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, which serves as the main predicate for determining whether an assignment is effectively using per-round rubrics. &lt;br /&gt;
**It first checks the assignment-level flag &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt;. If this flag is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the method immediately returns &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;. &lt;br /&gt;
**If &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the method queries &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; for at least one record with a non-null &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; value for the current assignment. &lt;br /&gt;
**The method returns &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; only when both conditions are satisfied: per-round rubrics are enabled on the assignment and at least one attached questionnaire explicitly specifies a round. &lt;br /&gt;
**To make this information easy to include in JSON responses, the non-predicate alias &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; simply delegates to &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;, avoiding a question mark in the rendered JSON key.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# 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&lt;br /&gt;
def varying_rubrics_by_round?&lt;br /&gt;
  return false unless vary_by_round&lt;br /&gt;
&lt;br /&gt;
  rubric_with_round = AssignmentQuestionnaire.where(assignment_id: id).where.not(used_in_round: nil).first&lt;br /&gt;
  # Check if any rubric has a specified round&lt;br /&gt;
  rubric_with_round.present?&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# Alias without question mark for JSON rendering convenience&lt;br /&gt;
def varying_rubrics_by_round&lt;br /&gt;
  varying_rubrics_by_round?&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*When fetching an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167275</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167275"/>
		<updated>2025-12-02T22:30:53Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Architecture Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:architecture.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:Architecture.jpg&amp;diff=167274</id>
		<title>File:Architecture.jpg</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:Architecture.jpg&amp;diff=167274"/>
		<updated>2025-12-02T22:30:17Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167273</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167273"/>
		<updated>2025-12-02T22:30:00Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Architecture Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167261</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167261"/>
		<updated>2025-12-02T21:49:06Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Implementations */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&amp;lt;mermaid&amp;gt;&lt;br /&gt;
flowchart TD&lt;br /&gt;
&lt;br /&gt;
A[Frontend (React UI)\n- transformAssignmentResponse\n- Rubrics dropdowns] &lt;br /&gt;
    --&amp;gt;|REST API call| B[AssignmentsController\n- loads assignment\n- returns vary_by_round\n- returns num_review_rounds\n- returns assignment_questionnaires]&lt;br /&gt;
&lt;br /&gt;
B --&amp;gt;|reads models| C[Models]&lt;br /&gt;
&lt;br /&gt;
C --&amp;gt; D[Assignment\n- varying_rubrics_by_round\n- num_review_rounds]&lt;br /&gt;
C --&amp;gt; E[DueDate\n- counts review deadlines (type=2)]&lt;br /&gt;
C --&amp;gt; F[AssignmentQuestionnaire\n- stores used_in_round mapping]&lt;br /&gt;
&lt;br /&gt;
F --&amp;gt; G[(DB)\nassignments\nassignments_questionnaires\nquestionnaires\ndue_dates]&lt;br /&gt;
&lt;br /&gt;
D --&amp;gt; G&lt;br /&gt;
E --&amp;gt; G&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/mermaid&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167260</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167260"/>
		<updated>2025-12-02T21:18:21Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Design */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167258</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167258"/>
		<updated>2025-12-02T21:16:11Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Motivation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167257</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167257"/>
		<updated>2025-12-02T21:15:46Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Motivation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
This project aims to automate the round count from due dates that are review deadlines, load rubric dropdowns from real questionnaires, and 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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167256</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167256"/>
		<updated>2025-12-02T21:15:37Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Motivation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
This project aims to automate the round count from due dates that are review deadlines, and load rubric dropdowns from real questionnaires, and 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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167255</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167255"/>
		<updated>2025-12-02T21:14:57Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Project Requirements */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
This project aims to automate the round count from due dates that are review deadlines, and loading rubric dropdowns from real questionnaires, and 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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the Rubrics tab dynamically reflects the number of review rounds and displays &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line. Instructors should be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
* Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
* Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
* Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
* Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
* When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
==== Test Requirements ====&lt;br /&gt;
&lt;br /&gt;
* Write tests for the new functionality.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167252</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167252"/>
		<updated>2025-12-02T21:09:30Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Motivation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
This project aims to automate the round count from due dates that are review deadlines, and loading rubric dropdowns from real questionnaires, and 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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, instructors must be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
The number of rounds must not be manually entered by the user.&lt;br /&gt;
&lt;br /&gt;
Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is enabled, each round must be associated with exactly one questionnaire.&lt;br /&gt;
&lt;br /&gt;
Modify the Rubrics tab on the assignment creation/edit page so that it dynamically reflects the number of review rounds:&lt;br /&gt;
&lt;br /&gt;
Display &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line.&lt;br /&gt;
&lt;br /&gt;
When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167251</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167251"/>
		<updated>2025-12-02T21:08:55Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
This project aims to automate the round count from due dates and loading rubric dropdowns from real questionnaires, and keep the Rubrics tab in sync with the backend’s actual due dates 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.&lt;br /&gt;
&lt;br /&gt;
=== Project Requirements ===&lt;br /&gt;
==== Functional Requirements ====&lt;br /&gt;
&lt;br /&gt;
Add a boolean attribute &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, instructors must be able to specify a distinct review rubric for each review round.&lt;br /&gt;
&lt;br /&gt;
Infer the number of review rounds automatically by examining all &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; objects associated with the assignment.&lt;br /&gt;
&lt;br /&gt;
Only deadlines whose &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; equals &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; (review deadlines) should be counted.&lt;br /&gt;
&lt;br /&gt;
The number of rounds must not be manually entered by the user.&lt;br /&gt;
&lt;br /&gt;
Store rubric–round associations in the &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table.&lt;br /&gt;
&lt;br /&gt;
Add a new column named &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to indicate which rubric applies to which review round.&lt;br /&gt;
&lt;br /&gt;
When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is enabled, each round must be associated with exactly one questionnaire.&lt;br /&gt;
&lt;br /&gt;
Modify the Rubrics tab on the assignment creation/edit page so that it dynamically reflects the number of review rounds:&lt;br /&gt;
&lt;br /&gt;
Display &amp;lt;code&amp;gt;Review Round 1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Round 2&amp;lt;/code&amp;gt;, etc., instead of a single Review rubric line.&lt;br /&gt;
&lt;br /&gt;
When &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; is disabled, retain the existing single-rubric interface.&lt;br /&gt;
&lt;br /&gt;
==== Data Requirements ====&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;assignments_questionnaires&amp;lt;/code&amp;gt; table must include an integer column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt; to track rubric usage per round.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table must include the boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; indicating whether rubrics differ across rounds.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167250</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167250"/>
		<updated>2025-12-02T21:01:26Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Current Behavior */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
This project aims to automate the round count from due dates and loading rubric dropdowns from real questionnaires, and keep the Rubrics tab in sync with the backend’s actual due dates 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.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167249</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167249"/>
		<updated>2025-12-02T21:01:14Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Current Behavior */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|900px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|900px]]&lt;br /&gt;
&lt;br /&gt;
This project aims to automate the round count from due dates and loading rubric dropdowns from real questionnaires, and keep the Rubrics tab in sync with the backend’s actual due dates 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.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167248</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167248"/>
		<updated>2025-12-02T21:01:04Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Introduction */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|800px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|800px]]&lt;br /&gt;
&lt;br /&gt;
This project aims to automate the round count from due dates and loading rubric dropdowns from real questionnaires, and keep the Rubrics tab in sync with the backend’s actual due dates 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.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167247</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167247"/>
		<updated>2025-12-02T21:00:41Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Current Behavior */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|700px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|700px]]&lt;br /&gt;
&lt;br /&gt;
This project aims to automate the round count from due dates and loading rubric dropdowns from real questionnaires, and keep the Rubrics tab in sync with the backend’s actual due dates 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.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167246</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167246"/>
		<updated>2025-12-02T21:00:24Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|600px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|600px]]&lt;br /&gt;
&lt;br /&gt;
This project aims to automate the round count from due dates and loading rubric dropdowns from real questionnaires, and keep the Rubrics tab in sync with the backend’s actual due dates 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.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167245</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167245"/>
		<updated>2025-12-02T21:00:12Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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.&lt;br /&gt;
 &lt;br /&gt;
[[File:currentbehavior1.jpg|500px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|500px]]&lt;br /&gt;
&lt;br /&gt;
This project aims to automate the round count from due dates and loading rubric dropdowns from real questionnaires, and keep the Rubrics tab in sync with the backend’s actual due dates 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.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167244</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167244"/>
		<updated>2025-12-02T20:59:33Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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. &lt;br /&gt;
[[File:currentbehavior1.jpg|300px]]&lt;br /&gt;
[[File:currentbehavior2.jpg|300px]]&lt;br /&gt;
&lt;br /&gt;
This project aims to automate the round count from due dates and loading rubric dropdowns from real questionnaires, and keep the Rubrics tab in sync with the backend’s actual due dates 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.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167243</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167243"/>
		<updated>2025-12-02T20:58:53Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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. &lt;br /&gt;
[[File:currentbehavior1.jpg]]&lt;br /&gt;
[[File:currentbehavior2.jpg]]&lt;br /&gt;
&lt;br /&gt;
This project aims to automate the round count from due dates and loading rubric dropdowns from real questionnaires, and keep the Rubrics tab in sync with the backend’s actual due dates 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.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:Currentbehavior1.jpg&amp;diff=167241</id>
		<title>File:Currentbehavior1.jpg</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:Currentbehavior1.jpg&amp;diff=167241"/>
		<updated>2025-12-02T20:49:18Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:Currentbehavior2.jpg&amp;diff=167240</id>
		<title>File:Currentbehavior2.jpg</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:Currentbehavior2.jpg&amp;diff=167240"/>
		<updated>2025-12-02T20:48:56Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167239</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167239"/>
		<updated>2025-12-02T20:32:46Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
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. The planned change is to drive the round count from due date objects and to populate the rubric dropdowns from actual questionnaire records. This project aims to automate the round count from due dates and loading rubric dropdowns from real questionnaires and keep the Rubrics tab in sync with the backend’s actual due dates and persisted questionnaire records for consistent behavior.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167209</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167209"/>
		<updated>2025-12-02T06:38:33Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* Links */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About Expertiza ==&lt;br /&gt;
[http://expertiza.ncsu.edu/ Expertiza] is an open source project based on [http://rubyonrails.org/ Ruby on Rails] framework. Expertiza allows the instructor to create new assignments and customize new or existing assignments which the students can then form teams in Expertiza to work on various projects and assignments. Students can also peer review other students' submissions. Expertiza supports submission across various document types, including the URLs and wiki pages. Students can also view their grades for the projects once the instructors have graded them.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
The current Expertiza system restricts assignments to a single review rubric that applies uniformly across all topics and rounds. This limitation constrains instructors’ ability to differentiate grading based on project type or review phase, reducing the flexibility and pedagogical richness of the system. To address these issues, projects E2557 and E2567 aim to enhance rubric management by allowing rubric customization per topic type and variation of rubrics across review rounds, improving the precision and fairness of assessments within Expertiza.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - https://github.com/expertiza/reimplementation-front-end/pull/123&lt;br /&gt;
*Git PR (back-end) - https://github.com/expertiza/reimplementation-back-end/pull/235&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167058</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167058"/>
		<updated>2025-11-10T12:32:39Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* (3) Spec Enhancements */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About Expertiza ==&lt;br /&gt;
[http://expertiza.ncsu.edu/ Expertiza] is an open source project based on [http://rubyonrails.org/ Ruby on Rails] framework. Expertiza allows the instructor to create new assignments and customize new or existing assignments which the students can then form teams in Expertiza to work on various projects and assignments. Students can also peer review other students' submissions. Expertiza supports submission across various document types, including the URLs and wiki pages. Students can also view their grades for the projects once the instructors have graded them.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
The current Expertiza system restricts assignments to a single review rubric that applies uniformly across all topics and rounds. This limitation constrains instructors’ ability to differentiate grading based on project type or review phase, reducing the flexibility and pedagogical richness of the system. To address these issues, projects E2557 and E2567 aim to enhance rubric management by allowing rubric customization per topic type and variation of rubrics across review rounds, improving the precision and fairness of assessments within Expertiza.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - TBC&lt;br /&gt;
*Git PR (back-end) - TBC&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
*Website - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Add model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167057</id>
		<title>CSC/ECE 517 Fall 2025 - E2567. Review rubrics varying by round</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2567._Review_rubrics_varying_by_round&amp;diff=167057"/>
		<updated>2025-11-10T12:32:19Z</updated>

		<summary type="html">&lt;p&gt;Ywang374: /* (2) Factory Support */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About Expertiza ==&lt;br /&gt;
[http://expertiza.ncsu.edu/ Expertiza] is an open source project based on [http://rubyonrails.org/ Ruby on Rails] framework. Expertiza allows the instructor to create new assignments and customize new or existing assignments which the students can then form teams in Expertiza to work on various projects and assignments. Students can also peer review other students' submissions. Expertiza supports submission across various document types, including the URLs and wiki pages. Students can also view their grades for the projects once the instructors have graded them.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
The current Expertiza system restricts assignments to a single review rubric that applies uniformly across all topics and rounds. This limitation constrains instructors’ ability to differentiate grading based on project type or review phase, reducing the flexibility and pedagogical richness of the system. To address these issues, projects E2557 and E2567 aim to enhance rubric management by allowing rubric customization per topic type and variation of rubrics across review rounds, improving the precision and fairness of assessments within Expertiza.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The current version of Expertiza uses a single rubric for all projects and all review rounds within an assignment. This design poses two major challenges:&lt;br /&gt;
&lt;br /&gt;
Lack of Differentiation by Project Type&lt;br /&gt;
In courses such as CSC/ECE 517, students undertake a variety of projects—front-end UI redesigns, back-end reimplementations, refactoring exercises, and testing frameworks. Despite their differing goals and evaluation criteria, all these projects are assessed using the same rubric. For example, a front-end project may prioritize usability and accessibility, while a refactoring project should emphasize code quality and maintainability. A single rubric fails to capture these distinct priorities, resulting in unfair or inconsistent evaluation.&lt;br /&gt;
&lt;br /&gt;
Inflexibility Across Review Rounds&lt;br /&gt;
Multi-round reviews often shift focus across iterations. Instructors may wish to emphasize functionality and completeness in early rounds, and style or documentation in later ones. Yet the system currently enforces the same rubric for every review round, limiting nuanced feedback and preventing progressive evaluation aligned with assignment milestones.&lt;br /&gt;
&lt;br /&gt;
These issues collectively reduce grading accuracy, hinder customized assessment, and make the feedback process less meaningful to students and instructors alike.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
&amp;lt;li&amp;gt; Enable instructors to assign different rubrics to specific topics and review rounds within the same assignment.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Add new database fields: topic_id, vary_by_round, and used_in_round to manage rubric associations.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Dynamically display rubric selectors (dropdowns) for each topic and review round on the assignment edit page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Automatically determine the number of review rounds from due_dates and link each round to its assigned rubric.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Ensure backward compatibility with existing single-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;li&amp;gt; Implement comprehensive unit and integration tests to verify correct rubric assignment and retrieval.&lt;br /&gt;
&lt;br /&gt;
== Links ==&lt;br /&gt;
*Git PR (front-end) - TBC&lt;br /&gt;
*Git PR (back-end) - TBC&lt;br /&gt;
*GitHub Repository (front-end) - https://github.com/YuWang1925wy/reimplementation-front-end&lt;br /&gt;
*GitHub Repository (back-end) - https://github.com/YuWang1925wy/reimplementation-back-end&lt;br /&gt;
*Demo Video - TBC&lt;br /&gt;
*Website - TBC&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Assignments Table and Assignment_questionnaires Table ====&lt;br /&gt;
&lt;br /&gt;
*Plan: add a boolean column &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; with default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt; to toggle per-round rubric behavior to &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt; table. Change will live in &amp;lt;code&amp;gt;db/migrate/20251101000000_add_vary_by_round_to_assignments.rb&amp;lt;/code&amp;gt;, with schema regeneration reflected in &amp;lt;code&amp;gt;db/schema.rb&amp;lt;/code&amp;gt;. As the &amp;lt;code&amp;gt;assignment_questionnaires&amp;lt;/code&amp;gt; table already has the column &amp;lt;code&amp;gt;used_in_round&amp;lt;/code&amp;gt;, we don't need to add extra code for this requirement.&lt;br /&gt;
&lt;br /&gt;
==== Review-Round Derivation ====&lt;br /&gt;
&lt;br /&gt;
*Plan: enable &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; to infer review rounds from its due dates, falling back to &amp;lt;code&amp;gt;rounds_of_reviews&amp;lt;/code&amp;gt; only when no review deadlines exist. Implementation adjustments will be made in &amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;, leveraging &amp;lt;code&amp;gt;DueDate.review_deadlines&amp;lt;/code&amp;gt;.&lt;br /&gt;
*&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; itself will define a &amp;lt;code&amp;gt;REVIEW_DEADLINE_TYPE_ID&amp;lt;/code&amp;gt; constant, a &amp;lt;code&amp;gt;review_deadlines&amp;lt;/code&amp;gt; scope, and an instance predicate so the logic is reusable (&amp;lt;code&amp;gt;app/models/due_date.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Rubric Selection Logic ====&lt;br /&gt;
&lt;br /&gt;
*Plan: update &amp;lt;code&amp;gt;SignUpTopic#rubric_for_review&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;#has_specific_rubric?&amp;lt;/code&amp;gt; to accept normalized round numbers, check topic-specific rubrics first, then fall back to default assignment rubrics per round or overall (&amp;lt;code&amp;gt;app/models/sign_up_topic.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: ensure helper entry points normalize the round parameter before touching the database. &amp;lt;code&amp;gt;TopicRubricsHelper.assign/remove/get&amp;lt;/code&amp;gt; will delegate through a shared &amp;lt;code&amp;gt;normalize_round&amp;lt;/code&amp;gt; method (&amp;lt;code&amp;gt;app/helpers/topic_rubrics_helper.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
*Plan: persist default assignment rubrics separately from topic-specific ones by adding a &amp;lt;code&amp;gt;default_assignment_questionnaires&amp;lt;/code&amp;gt; association in &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;app/models/assignment.rb&amp;lt;/code&amp;gt;).&lt;br /&gt;
==== API Surface ====&lt;br /&gt;
&lt;br /&gt;
*Plan: when the client fetches an assignment (GET &amp;lt;code&amp;gt;/assignments/:id&amp;lt;/code&amp;gt;), return &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the UI knows how many rubric rows to render. Also permit the new &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; attribute on create/update. These changes will go into &amp;lt;code&amp;gt;app/controllers/assignments_controller.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== Rubrics Overview ====&lt;br /&gt;
&lt;br /&gt;
We will source review-round metadata from the backend and keep it on the form. &amp;lt;code&amp;gt;AssignmentUtil.ts&amp;lt;/code&amp;gt; will parse &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;varying_rubrics_by_round&amp;lt;/code&amp;gt; so the editor always knows how many review rounds exist, defaulting to one when nothing is defined. &amp;lt;code&amp;gt;interfaces.ts&amp;lt;/code&amp;gt; will expose these response fields so type-checking stays accurate.&lt;br /&gt;
==== Editor Rendering ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; will import two helpers, derive the effective review-round count using &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt;, and build the rows displayed on the Rubrics tab via &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;. Each review round will show up as “Review Round n” with its own questionnaire dropdown and tag prompt row, ensuring we render as many rows as there are rounds. The same derived count will drive the Due Dates table so submission/review deadlines stay aligned with the Rubrics view. Initial form values will set &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; to 1 and introduce &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt; so the UI never collapses to zero rows.&lt;br /&gt;
==== Helper Module ====&lt;br /&gt;
&lt;br /&gt;
A new helper file, &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;, will contain the reusable logic for normalizing round counts (coercing strings, discarding non-positive values, falling back to backend metadata, defaulting to 1) and for generating the alternating dropdown/tag rows consumed by the table component. These helpers will keep &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; lean and make the behavior testable.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
Note that the current Rubrics tab already loops over &amp;lt;code&amp;gt;number_of_review_rounds&amp;lt;/code&amp;gt; and invoks &amp;lt;code&amp;gt;Array.from&amp;lt;/code&amp;gt; to render paired rows (see &amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:276-312&amp;lt;/code&amp;gt; before our edits). So as soon as the user entered “2” in the “Number of review rounds” input under Due Dates, the UI would emit “Review round 1” and “Review round 2” rows - there was no hard limitation to a single line.&lt;br /&gt;
The new work won't discard that approach; it will codify it. We will still generate a pair of rows per round, but now that logic will live in &amp;lt;code&amp;gt;buildReviewRoundRows&amp;lt;/code&amp;gt;, and the count will come from &amp;lt;code&amp;gt;deriveReviewRoundCount&amp;lt;/code&amp;gt; (both will be introduced in &amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;). In the editor (&amp;lt;code&amp;gt;src/pages/Assignments/AssignmentEditor.tsx:180-314&amp;lt;/code&amp;gt;) we call those helpers so we reuse the same “array per round” idea while adding guardrails: we will default to one round when nothing is set, and we will hydrate the count from the backend’s &amp;lt;code&amp;gt;review_rounds&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;num_review_rounds&amp;lt;/code&amp;gt; fields during edit, rather than relying solely on manual input.&lt;br /&gt;
&lt;br /&gt;
== Design Principles == &lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Backend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Backward compatibility first: adding the &amp;lt;code&amp;gt;vary_by_round&amp;lt;/code&amp;gt; flag and default fallbacks so existing assignments keep working even if they never set the new field (progressive enhancement instead of breaking change).&lt;br /&gt;
*Single source of truth: rather than hard-coding round counts, the assignment will derive them directly from &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; data (with a final fallback), ensuring round logic lives in one place that reflects the real deadlines.&lt;br /&gt;
*Separation of concerns: keeping business logic inside models/helpers (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;TopicRubricsHelper&amp;lt;/code&amp;gt;) while the controller simply exposes the computed values. Also splitting topic vs. default rubrics via a dedicated association.&lt;br /&gt;
*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.&lt;br /&gt;
*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.&lt;br /&gt;
&lt;br /&gt;
=== Guiding Principles Behind the Frontend Plan ===&lt;br /&gt;
&lt;br /&gt;
*Single source of truth: the count of review rounds comes from centralized helper logic, avoiding duplicated calculations and keeping tabs in sync.&lt;br /&gt;
*Resilience &amp;amp; sensible defaults: derive functions sanitize user input, fall back to backend metadata, and guarantee at least one review round so the UI never renders an invalid configuration.&lt;br /&gt;
*Separation of concerns: reusable helpers (&amp;lt;code&amp;gt;assignmentEditorHelpers.ts&amp;lt;/code&amp;gt;) keep rendering components lean and make business logic testable in isolation.&lt;br /&gt;
*Testability: unit tests ensure the helper logic behaves the way we expect, which guards against regressions as requirements evolve.&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. RSpec ====&lt;br /&gt;
&lt;br /&gt;
===== (1) API Surface =====&lt;br /&gt;
*Plan: add request-spec coverage to &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; confirming the enriched payload.&lt;br /&gt;
===== (2) Factory Support =====&lt;br /&gt;
&lt;br /&gt;
*Fill in previously missing factories so specs can build the new records easily:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/assignment_questionnaires.rb&amp;lt;/code&amp;gt; creates assignment-questionnaire join rows (round-aware, optional topic).&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/factories/sign_up_topics.rb&amp;lt;/code&amp;gt; introduces a simple topic factory tied to an assignment.&amp;lt;br&amp;gt;&lt;br /&gt;
Update &amp;lt;code&amp;gt;spec/factories/questionnaires.rb&amp;lt;/code&amp;gt; to avoid invalid default associations (sticks to instructor only).&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== (3) Spec Enhancements =====&lt;br /&gt;
&lt;br /&gt;
*Added model specs to exercise the new logic:&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/assignment_spec.rb&amp;lt;/code&amp;gt;: scenarios for deriving review rounds from due dates, fallback behavior, and &amp;lt;code&amp;gt;varying_rubrics_by_round?&amp;lt;/code&amp;gt;. &amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/due_date_spec.rb&amp;lt;/code&amp;gt;: coverage for the review_deadlines scope and review_deadline?.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;spec/models/sign_up_topic_spec.rb&amp;lt;/code&amp;gt;: ensures round-specific lookups normalize inputs and fall back correctly.&amp;lt;br&amp;gt;&lt;br /&gt;
*API spec updates in &amp;lt;code&amp;gt;spec/requests/api/v1/assignment_controller_spec.rb&amp;lt;/code&amp;gt; validate the augmented show response.&lt;br /&gt;
&lt;br /&gt;
==== 2. Swagger UI ====&lt;br /&gt;
We only plan to enrich the existing &amp;lt;code&amp;gt;GET/assignments/:id&amp;lt;/code&amp;gt;, so there is no new endpoints added for this project. Because the route itself won't change and we won't touch &amp;lt;code&amp;gt;swagger/v1/swagger.yaml&amp;lt;/code&amp;gt;, so spot-checking through Swagger UI is not necessary.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
&lt;br /&gt;
==== 1. Vitest ====&lt;br /&gt;
&amp;lt;code&amp;gt;assignmentEditorHelpers.test.ts&amp;lt;/code&amp;gt; will unit-test both helper functions, asserting that we honor explicit counts, fall back to server-provided arrays, and always emit paired review/tag rows for every round. These tests will run with &amp;lt;code&amp;gt;vitest&amp;lt;/code&amp;gt; alongside the rest of the front-end suite. &amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== 2. Testing from UI ====&lt;br /&gt;
We also plan to test the new functionality from UI.&lt;br /&gt;
&lt;br /&gt;
== Team == &lt;br /&gt;
*Mentor: Koushik Gudipelly (kgudipe@ncsu.edu)&lt;br /&gt;
*Iman Khan (ikhan7@ncsu.edu)&lt;br /&gt;
*Niranjan Rajendran (nrajend4@ncsu.edu)&lt;br /&gt;
*Yu Wang (ywang374@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Ywang374</name></author>
	</entry>
</feed>