<?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=Admin</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=Admin"/>
	<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Special:Contributions/Admin"/>
	<updated>2026-05-15T15:29:20Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.41.0</generator>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2609._Review_calibration&amp;diff=167976</id>
		<title>CSC/ECE 517 Spring 2026 - E2609. Review calibration</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2609._Review_calibration&amp;diff=167976"/>
		<updated>2026-04-17T00:49:54Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Problem statements */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project overview ==&lt;br /&gt;
&lt;br /&gt;
Expertiza supports course assignments with peer review. The reimplementation exposes a '''Rails JSON API''' and a '''React''' single-page application.&lt;br /&gt;
&lt;br /&gt;
||| This should describe the motivation for calibration. It's a way to make sure that the reviewer knows what they're doing.  The instructor evaluates a &amp;quot;calibration submission&amp;quot; before the students do, and then the system compares how close the student's review is to the instructor's review. If it's close, the student can be deemed competent. ||| This project implements a '''calibration assignment''' in the reimplementation: instructors use the '''Calibration''' tab to register '''calibration participants''' by username, use '''Begin''' to open the same rubric UI students see, and '''enter the calibration review''' as an instructor &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; on a &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;. A '''comparison report''' summarizes how each student’s calibration review compares to the '''instructor’s review''' of the same submission, per rubric item. As in the course description, students review work that '''course staff''' have already reviewed; the system stores reviews and rubric scores for training and reporting.&lt;br /&gt;
&lt;br /&gt;
== Problem statements ==&lt;br /&gt;
&lt;br /&gt;
# '''Data model.''' Normal peer review and calibration must be distinguishable. The implementation adds &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; on &amp;lt;code&amp;gt;response_maps&amp;lt;/code&amp;gt; ||| response maps are the way that Expertiza knows who's reviewing whom.||| so the system can select the right maps for listing, saving the instructor calibration review at the &amp;lt;code&amp;gt;instructor_response&amp;lt;/code&amp;gt; endpoint, and report logic.&lt;br /&gt;
# '''Instructor workflow.''' Teaching staff add calibration participants by username, open the review via '''Begin''', and save drafts or submit the instructor calibration review. Submit must lock that review so comparison reports stay stable.&lt;br /&gt;
# '''Reporting.''' For each calibration submission the instructor needs a view that compares many student calibration responses to one instructor response on the same rubric, with per-item summaries suitable for charts.&lt;br /&gt;
# '''Authorization''' Only instructor or teaching-assistant roles for the assignment may use the calibration instructor APIs.&lt;br /&gt;
# '''Controller boundaries''' Instructor calibration maps, instructor-review persistence, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' are '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' responsibilities. One '''new student review controller''' covers '''student'''-scoped HTTP: review listing, calibration links, and comparison affordances. Submitted content uses the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' stack.&lt;br /&gt;
# '''Student workflow ''' Students see their work as reviewers through a '''student review''' API: list the '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' relevant to them. Where &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; applies, provide a '''link to the calibration comparison''' for instructor versus class scores and enough '''score/feedback''' context that the student can see '''how they compare''' to the instructor on that calibration submission. Teaching-staff map management remains on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''; the student API lists maps and comparison links and reuses the same report JSON where appropriate.&lt;br /&gt;
&lt;br /&gt;
== Design goals ==&lt;br /&gt;
&lt;br /&gt;
* '''Clarity''' — A reader can follow the instructor path from configuration through '''Begin''' and the instructor calibration review to the comparison report, '''and''' the student path from their review list to calibration comparison where applicable.&lt;br /&gt;
* '''Separation''' — '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' owns every teaching-staff calibration endpoint: the map list &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;, add participant, '''Begin''', instructor response, map delete, and the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. A '''dedicated student review controller''' for student-facing HTTP lists student reviews / maps and exposes calibration comparison entry points, delegating instructor tab behavior and report aggregation to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
* '''DRY''' — One rubric &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence path using a '''shared rubric writer''' for instructor calibration and other reviews where applicable. One submitted-content path via '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' for list rows and report payloads.&lt;br /&gt;
* '''Minimal footprint''' — Ship what the course feature needs: an instructor path to enter calibration reviews and a student path to compare their calibration work to the instructor’s. Prefer small methods, clear names, focused PRs, and plain comments on the calibration surface.&lt;br /&gt;
* '''Testability''' — Prefer factories and request specs for repeatable scenarios.&lt;br /&gt;
* '''Documentation''' — Controllers are described in prose with routes in the appendix; screenshots pair UI with the backing API.&lt;br /&gt;
&lt;br /&gt;
== Database design ==&lt;br /&gt;
&lt;br /&gt;
Column: &amp;lt;code&amp;gt;response_maps.for_calibration&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Type: boolean, default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;NOT NULL&amp;lt;/code&amp;gt;.&lt;br /&gt;
* When &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the row is part of the calibration workflow for that assignment: either the instructor-to-calibration-participant map or a student calibrator map that shares the same &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt; as other calibration maps for that participant.&lt;br /&gt;
* When &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the row behaves as a normal peer-review map.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Solution approach ==&lt;br /&gt;
&lt;br /&gt;
=== Phase 1 — Calibration participants and response maps ===&lt;br /&gt;
&lt;br /&gt;
Instructors register calibration participants on the assignment and obtain a &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; that backs the instructor &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; for that calibration submission. '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', extended for calibration, validates assignment and teaching-staff role, ensures the target user is an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; when required, sets &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; on the map, and returns list rows with display name, submitted artifact summary using the same helpers as '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''', and review status. Adding a participant by username is idempotent.&lt;br /&gt;
&lt;br /&gt;
=== Phase 2 — Reporting and comparison ===&lt;br /&gt;
&lt;br /&gt;
The comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' returns the JSON consumed by the Calibration report React page and is implemented on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. It returns one payload for the stacked chart and rubric detail views. The server resolves the instructor calibration map, identifies the reviewee as the calibration participant, loads rubric &amp;lt;code&amp;gt;Item&amp;lt;/code&amp;gt;s, loads instructor and student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; data for the same reviewed work. Per map, aggregation uses '''submitted''' student responses, taking the latest row by &amp;lt;code&amp;gt;updated_at&amp;lt;/code&amp;gt; among submitted responses, then runs &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; for per-item buckets.&lt;br /&gt;
&lt;br /&gt;
=== Phase 3 — Instructor calibration review UI ===&lt;br /&gt;
&lt;br /&gt;
The instructor uses the same rubric experience as students: draft and submit, then the submitted rubric is locked for further edits. That matches the course description of a normal-looking rubric review after '''Begin''' and establishes a stable instructor review for downstream comparison reports.&lt;br /&gt;
&lt;br /&gt;
=== High-level architecture ===&lt;br /&gt;
&lt;br /&gt;
All '''teaching-staff''' calibration JSON — the calibration map '''list''', mutating map and review actions including add participant, '''Begin''', instructor response, and delete, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' — goes through '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. Student-facing review HTTP lives in '''one new student review controller''' whose Ruby class and path names follow &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* '''Review mappings — JSON, teaching staff.''' Extended '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' serves the Calibration tab: list calibration maps, add a participant and the corresponding &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; map, '''Begin''' metadata, save or submit the instructor calibration review, optional map delete, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. Implement the report action as a thin entry point that delegates to POROs or private methods for assembly. &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence for saves uses the same shared rubric writer as elsewhere where possible.&lt;br /&gt;
&lt;br /&gt;
* '''Report — JSON, teaching staff.''' The report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' is a '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' action; declare it as a member or collection route in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt; next to the action implementation. It returns JSON: rubric items, instructor and student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; data for the reviewee, &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt;, reviewee hyperlinks/files via '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''Student reviews — JSON.''' The '''student review controller''' lists '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' for the student; for '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' rows it exposes a '''link''' to the teaching-staff comparison report JSON and UI to compare '''student vs instructor''' scores and feedback. Thin layer over existing review models; instructor map listing and report aggregation stay in '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''React SPA.''' Assignment editor → Calibration tab → '''Begin''' → instructor rubric page → report page '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''Server-side helpers.''' &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; builds per-item chart buckets. Submitted hyperlinks/files always go through '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''; details are under '''Implementation''' and '''Backend'''.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== UML diagram ==&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-domain-class-uml.png|720px|center]]&lt;br /&gt;
&lt;br /&gt;
Instructors and students are &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;s. The instructor’s calibration map is a &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; true whose reviewer is the instructor participant and whose reviewee is the calibration participant. Student calibration maps share the same &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt;, the calibration author, but use other &amp;lt;code&amp;gt;reviewer_id&amp;lt;/code&amp;gt; values. &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; store scores. List and report use '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' for hyperlinks/files; &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; supports report aggregation.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Flow: Review calibration ==&lt;br /&gt;
&lt;br /&gt;
'''Figure 1a — Calibration setup, overview.''' Student lane: review list and submit via the student review stack. Staff lane: Calibration tab and rubric send teaching-staff calibration traffic to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. The two lanes can happen in any order; details and routes are in '''Implementation''' and the '''Appendix'''.&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-pipelines-flow.png|820px|center]]&lt;br /&gt;
&lt;br /&gt;
'''Figure 1b — Calibration report, overview.''' Teaching staff open the report in React; the page issues one &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' and receives either &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt; report JSON. Assembly of rubric, scores, &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt;, and the &amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt; stack is specified in code and in '''Implementation''' and '''Backend'''; the diagram highlights the browser–Rails exchange.&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-flow-report.png|720px|center]]&lt;br /&gt;
&lt;br /&gt;
Figure 1b: maps for one calibration run share a &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt; aggregates '''submitted''' student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; rows per map using the latest &amp;lt;code&amp;gt;updated_at&amp;lt;/code&amp;gt; among submitted rows.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area&lt;br /&gt;
! Implementation &lt;br /&gt;
! Verify&lt;br /&gt;
|-&lt;br /&gt;
| Teaching-staff HTTP&lt;br /&gt;
| Implement list, create, begin, instructor response save, map delete, '''and''' comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', optionally split into a Rails concern mixed into that controller, scoped with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; and teaching-staff authorization. Add '''one new student review controller''': list student '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews'''; for &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; items, '''link''' to the comparison report and expose '''student vs instructor''' score/feedback comparison. Keep the student controller thin: delegate to existing review models and reuse the same report JSON payload shape where appropriate.&lt;br /&gt;
| Request specs for all &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; calibration actions; request specs for student review list, calibration link, and comparison paths; coverage lives on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' for teaching-staff calibration HTTP.&lt;br /&gt;
|-&lt;br /&gt;
| Report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| Report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''': thin entry + PORO/private steps; stable JSON for the React report page. Uses &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; and the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' stack for reviewee hyperlinks/files.&lt;br /&gt;
| Request spec for report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' documents required JSON keys and error cases.&lt;br /&gt;
|-&lt;br /&gt;
| Instructor review write&lt;br /&gt;
| Instructor calibration draft/submit on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. Delegate &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence to a '''shared rubric writer'''; action handles guards, validation, and JSON only.&lt;br /&gt;
| Request specs: draft, submit, 422, invalid &amp;lt;code&amp;gt;answers&amp;lt;/code&amp;gt; payload.&lt;br /&gt;
|-&lt;br /&gt;
| Submitted content&lt;br /&gt;
| Build list and report &amp;lt;code&amp;gt;{ hyperlinks, files }&amp;lt;/code&amp;gt; through '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' or the same service object that controller already delegates to.&lt;br /&gt;
| Specs assert list and report payloads match submitted-content for the same participant.&lt;br /&gt;
|-&lt;br /&gt;
| Team + participant bootstrap&lt;br /&gt;
| Implement solo-team and related bootstrap logic in a '''team/participant service''' called from map '''create''', with '''&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;''' updates limited to what map create and listing need. Keep &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; hyperlink behavior correct for those paths.&lt;br /&gt;
| Service/unit tests + create path; hyperlink regressions if any.&lt;br /&gt;
|-&lt;br /&gt;
| Scope hygiene&lt;br /&gt;
| Scope edits to calibration controllers, services, routes, and React surfaces named in this document, plus the smallest shared-model touches that map create and listing require.&lt;br /&gt;
| Code review checklist; small, focused diffs.&lt;br /&gt;
|-&lt;br /&gt;
| Report UI&lt;br /&gt;
| &amp;lt;code&amp;gt;CalibrationReview&amp;lt;/code&amp;gt; and related report components match the mockup while consuming the report JSON shape from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
| Vitest; refresh '''Figure 2''' left pane when the shipped UI changes.&lt;br /&gt;
|-&lt;br /&gt;
| E2E · repo · docs&lt;br /&gt;
| Browser E2E over &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; calibration routes and report tab. Record generated-asset rules for this repo in the PR checklist or team wiki so commits stay consistent.&lt;br /&gt;
| CI or &amp;lt;code&amp;gt;test:e2e&amp;lt;/code&amp;gt;; PR checklist.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Backend — controller behavior ==&lt;br /&gt;
&lt;br /&gt;
Teaching-staff APIs use auth, '''&amp;lt;code&amp;gt;authorize&amp;lt;/code&amp;gt;''', and staff checks consistent with the assignment. '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', extended for calibration, hosts all teaching-staff calibration endpoints: it lists calibration maps where the current user is the instructor reviewer and &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; is true; each row includes map/reviewee fields, '''&amp;lt;code&amp;gt;participant_name&amp;lt;/code&amp;gt;''', '''&amp;lt;code&amp;gt;review_status&amp;lt;/code&amp;gt;''' from the latest '''&amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;''', and '''&amp;lt;code&amp;gt;submitted_content&amp;lt;/code&amp;gt;''' built with the '''same service/helpers as &amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''. '''Create''' resolves username to '''&amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;''', runs team bootstrap via a '''dedicated service''', '''&amp;lt;code&amp;gt;find_or_create_by!&amp;lt;/code&amp;gt;''' the instructor→reviewee map with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; true, returns '''&amp;lt;code&amp;gt;201&amp;lt;/code&amp;gt;'''. '''Begin''' returns '''&amp;lt;code&amp;gt;map_id&amp;lt;/code&amp;gt;''', optional '''&amp;lt;code&amp;gt;response_id&amp;lt;/code&amp;gt;''', '''&amp;lt;code&amp;gt;redirect_to&amp;lt;/code&amp;gt;'''. '''Instructor response save''' validates input, enforces the submitted-state rules, delegates persistence to a '''shared writer''', returns '''&amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt;''' JSON. '''Destroy''' returns '''&amp;lt;code&amp;gt;204&amp;lt;/code&amp;gt;''' after ownership checks. '''Comparison-report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' for the instructor’s calibration map id on the same controller returns rubric items, '''&amp;lt;code&amp;gt;instructor_response&amp;lt;/code&amp;gt;''', submitted student '''&amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;s''', '''&amp;lt;code&amp;gt;CalibrationPerItemSummary.build&amp;lt;/code&amp;gt;''', reviewee hyperlinks/files via the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt; stack''', '''&amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt;''' JSON, or '''&amp;lt;code&amp;gt;404&amp;lt;/code&amp;gt;''' / '''&amp;lt;code&amp;gt;422&amp;lt;/code&amp;gt;''' when assignment, map, or rubric preconditions fail.&lt;br /&gt;
&lt;br /&gt;
'''Student review controller — new, thin layer''' — Lists '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' for the logged-in student as reviewer; for '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' maps, provides a '''link''' to the calibration comparison using the instructor-facing report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' or a student-safe JSON projection, plus fields and UI so the student can see '''how their scores and feedback compare''' to the instructor’s. Delegates to existing review / &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; models; instructor Calibration tab CRUD, rubric save, and server-side report aggregation live in '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Appendix — API routes ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method&lt;br /&gt;
! Path pattern&lt;br /&gt;
! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration list.''' List calibration &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; rows for teaching staff; exact path under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; is defined in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration create.''' Add calibration participant; request includes &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt;. Exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;DELETE&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration map delete.''' Remove one calibration map by id; exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Begin.''' Return navigation JSON for instructor calibration rubric SPA; exact member path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Instructor response.''' Save draft or submit instructor calibration review; exact member path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration report.''' Return comparison report JSON for the instructor calibration map id; exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Student review controller.''' Student review list, calibration report links, and comparison to instructor; verbs and paths declared in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Frontend — data expectations and user flow ==&lt;br /&gt;
&lt;br /&gt;
'''Calibration tab table:''' Each row shows map id, display name, submitted links and files summary, and status: '''Not started''', '''Draft''', or '''Submitted'''. Data comes from the teaching-staff calibration '''list''' &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; routes, with submitted fields from the same pipeline as '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
'''Report page:''' Rubric items in order, instructor scores and comments per item, each student’s scores, and per-item buckets from the report JSON.&lt;br /&gt;
&lt;br /&gt;
'''User flow:'''&lt;br /&gt;
&lt;br /&gt;
# Open Assignment editor, then Calibration tab.&lt;br /&gt;
# Enter username; system ensures participant and &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; map.&lt;br /&gt;
# &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; calibration list on &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt;; table refreshes.&lt;br /&gt;
# Begin; &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; begin; SPA opens the instructor calibration review page.&lt;br /&gt;
# Save draft or submit; &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; instructor response on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
# Open report; &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; comparison JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''; charts and rubric detail render.&lt;br /&gt;
&lt;br /&gt;
'''Student review list: ''' The student review controller lists the student’s reviews / '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s'''. Calibration rows show a '''link''' to open the comparison for instructor versus class scores and, where the UI implements it, the student’s position relative to the instructor.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Assignment editor — Calibration tab ===&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-assignment-tab.png|480px|center]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.2em;&amp;quot;&amp;gt;'''Figure 1.''' Reference UI — assignment editor Calibration tab: add participants, open report or '''Begin''' the instructor calibration review, and see submitted items. Each row is filled from the calibration '''list''' '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt;, with &amp;lt;code&amp;gt;participant_name&amp;lt;/code&amp;gt;, submitted content from the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' pipeline, and &amp;lt;code&amp;gt;review_status&amp;lt;/code&amp;gt;. Adding the same username again is idempotent.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Calibration report: comparison UI ===&lt;br /&gt;
&lt;br /&gt;
{| style=&amp;quot;width:100%; max-width:980px; margin:0.8em auto; border-collapse:separate; border-spacing:10px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center; width:50%;&amp;quot; | '''Reference UI — Class comparison, stacked chart tab'''&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center; width:50%;&amp;quot; | '''Mockup chart layout; same report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; JSON from &amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center;&amp;quot; | [[File:E2609-calibration-report-stacked.png|440px]]&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center;&amp;quot; | [[File:E2609-calibration-stacked-chart-improved-mockup.png|440px]]&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.35em;&amp;quot;&amp;gt;'''Figure 2.''' Side by side: reference stacked-chart tab versus the intended layout. The mockup adds per-criterion axis labels, framing, legend, and full criterion text in tooltips in &amp;lt;code&amp;gt;CalibrationReview&amp;lt;/code&amp;gt;. Same report JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-report-rubric-detail.png|480px|center]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.2em;&amp;quot;&amp;gt;'''Figure 3.''' Reference UI — rubric detail: instructor review versus class summary and reviewer selections, using the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Test Plan ===&lt;br /&gt;
&lt;br /&gt;
==== Automated Testing — RSpec ====&lt;br /&gt;
&lt;br /&gt;
* '''Request specs''' for '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' calibration: list, create participant/map, begin, instructor response for draft, submit, and error paths, destroy, and comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. Co-locate spec files under &amp;lt;code&amp;gt;spec/requests/&amp;lt;/code&amp;gt; with the route names in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* '''Request specs''' for the '''student review controller''': list reviews/maps; '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' rows include correct report link or comparison payload; responses match the logged-in student reviewer.&lt;br /&gt;
* Service specs for aggregation logic, for example &amp;lt;code&amp;gt;spec/services/calibration_per_item_summary_spec.rb&amp;lt;/code&amp;gt; next to &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt;.&lt;br /&gt;
* '''Factories / fixtures''' for &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; maps, participants, and submitted responses as needed for the above.&lt;br /&gt;
&lt;br /&gt;
==== Frontend Testing — Vitest ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/CalibrationReview.test.tsx&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/CalibrationInstructorReview.test.tsx&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationReportNormalize.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationSummary.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationReportDemo.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== End-to-end — E2E ====&lt;br /&gt;
&lt;br /&gt;
# Teaching staff: assignment editor → Calibration tab → calibration map list &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; → table or empty state renders cleanly in the browser.&lt;br /&gt;
# Teaching staff: comparison report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' → “Class comparison (stacked)” and “Rubric detail” render; chart region populated when &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt; is non-empty.&lt;br /&gt;
# Teaching staff: '''Begin''', including opening the calibration review by direct URL if supported → draft save → submit → UI matches post-submit lock rules.&lt;br /&gt;
# Student: open student review list → calibration row shows '''link''' to comparison for instructor versus class view → student can see '''how their review compares''' to the instructor’s when that panel is implemented.&lt;br /&gt;
&lt;br /&gt;
==== Manual UI Testing ====&lt;br /&gt;
&lt;br /&gt;
# Login as instructor/TA.&lt;br /&gt;
# Open assignment editor → Calibration tab.&lt;br /&gt;
# Add a calibration participant by username.&lt;br /&gt;
# Click '''View report''' and verify stacked and rubric detail tabs.&lt;br /&gt;
# Enter/edit instructor review: save draft, submit, and confirm the form locks.&lt;br /&gt;
# Login as student calibrator: open student review list; for a calibration map, follow the report/comparison link and confirm scores or feedback align with expectations versus the instructor view.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
* Add an overall calibration score and trend visualization per participant.&lt;br /&gt;
* Provide additional endpoints for:&lt;br /&gt;
** calibration summary per map,&lt;br /&gt;
** detailed agreement breakdown per item across all rounds.&lt;br /&gt;
* Improve diagnostics, such as highlighting criteria with the largest score spread across reviewers.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza wiki main page]&lt;br /&gt;
* Front-end pull request: [https://github.com/expertiza/reimplementation-front-end/pull/164 expertiza/reimplementation-front-end#164]&lt;br /&gt;
* Back-end pull request: [https://github.com/expertiza/reimplementation-back-end/pull/323 expertiza/reimplementation-back-end#323]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Team ==&lt;br /&gt;
&lt;br /&gt;
* Mentor: Dr. Ed Gehringer&lt;br /&gt;
* Team members: Xiangjun Mi, Rujuta Palimkar, Emma Hassler&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2609._Review_calibration&amp;diff=167975</id>
		<title>CSC/ECE 517 Spring 2026 - E2609. Review calibration</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2609._Review_calibration&amp;diff=167975"/>
		<updated>2026-04-17T00:46:53Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Project overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project overview ==&lt;br /&gt;
&lt;br /&gt;
Expertiza supports course assignments with peer review. The reimplementation exposes a '''Rails JSON API''' and a '''React''' single-page application.&lt;br /&gt;
&lt;br /&gt;
||| This should describe the motivation for calibration. It's a way to make sure that the reviewer knows what they're doing.  The instructor evaluates a &amp;quot;calibration submission&amp;quot; before the students do, and then the system compares how close the student's review is to the instructor's review. If it's close, the student can be deemed competent. ||| This project implements a '''calibration assignment''' in the reimplementation: instructors use the '''Calibration''' tab to register '''calibration participants''' by username, use '''Begin''' to open the same rubric UI students see, and '''enter the calibration review''' as an instructor &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; on a &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;. A '''comparison report''' summarizes how each student’s calibration review compares to the '''instructor’s review''' of the same submission, per rubric item. As in the course description, students review work that '''course staff''' have already reviewed; the system stores reviews and rubric scores for training and reporting.&lt;br /&gt;
&lt;br /&gt;
== Problem statements ==&lt;br /&gt;
&lt;br /&gt;
# '''Data model.''' Normal peer review and calibration must be distinguishable. The implementation adds &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; on &amp;lt;code&amp;gt;response_maps&amp;lt;/code&amp;gt; so the system can select the right maps for listing, saving the instructor calibration review at the &amp;lt;code&amp;gt;instructor_response&amp;lt;/code&amp;gt; endpoint, and report logic.&lt;br /&gt;
# '''Instructor workflow.''' Teaching staff add calibration participants by username, open the review via '''Begin''', and save drafts or submit the instructor calibration review. Submit must lock that review so comparison reports stay stable.&lt;br /&gt;
# '''Reporting.''' For each calibration submission the instructor needs a view that compares many student calibration responses to one instructor response on the same rubric, with per-item summaries suitable for charts.&lt;br /&gt;
# '''Authorization''' Only instructor or teaching-assistant roles for the assignment may use the calibration instructor APIs.&lt;br /&gt;
# '''Controller boundaries''' Instructor calibration maps, instructor-review persistence, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' are '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' responsibilities. One '''new student review controller''' covers '''student'''-scoped HTTP: review listing, calibration links, and comparison affordances. Submitted content uses the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' stack.&lt;br /&gt;
# '''Student workflow ''' Students see their work as reviewers through a '''student review''' API: list the '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' relevant to them. Where &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; applies, provide a '''link to the calibration comparison''' for instructor versus class scores and enough '''score/feedback''' context that the student can see '''how they compare''' to the instructor on that calibration submission. Teaching-staff map management remains on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''; the student API lists maps and comparison links and reuses the same report JSON where appropriate.&lt;br /&gt;
&lt;br /&gt;
== Design goals ==&lt;br /&gt;
&lt;br /&gt;
* '''Clarity''' — A reader can follow the instructor path from configuration through '''Begin''' and the instructor calibration review to the comparison report, '''and''' the student path from their review list to calibration comparison where applicable.&lt;br /&gt;
* '''Separation''' — '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' owns every teaching-staff calibration endpoint: the map list &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;, add participant, '''Begin''', instructor response, map delete, and the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. A '''dedicated student review controller''' for student-facing HTTP lists student reviews / maps and exposes calibration comparison entry points, delegating instructor tab behavior and report aggregation to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
* '''DRY''' — One rubric &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence path using a '''shared rubric writer''' for instructor calibration and other reviews where applicable. One submitted-content path via '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' for list rows and report payloads.&lt;br /&gt;
* '''Minimal footprint''' — Ship what the course feature needs: an instructor path to enter calibration reviews and a student path to compare their calibration work to the instructor’s. Prefer small methods, clear names, focused PRs, and plain comments on the calibration surface.&lt;br /&gt;
* '''Testability''' — Prefer factories and request specs for repeatable scenarios.&lt;br /&gt;
* '''Documentation''' — Controllers are described in prose with routes in the appendix; screenshots pair UI with the backing API.&lt;br /&gt;
&lt;br /&gt;
== Database design ==&lt;br /&gt;
&lt;br /&gt;
Column: &amp;lt;code&amp;gt;response_maps.for_calibration&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Type: boolean, default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;NOT NULL&amp;lt;/code&amp;gt;.&lt;br /&gt;
* When &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the row is part of the calibration workflow for that assignment: either the instructor-to-calibration-participant map or a student calibrator map that shares the same &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt; as other calibration maps for that participant.&lt;br /&gt;
* When &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the row behaves as a normal peer-review map.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Solution approach ==&lt;br /&gt;
&lt;br /&gt;
=== Phase 1 — Calibration participants and response maps ===&lt;br /&gt;
&lt;br /&gt;
Instructors register calibration participants on the assignment and obtain a &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; that backs the instructor &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; for that calibration submission. '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', extended for calibration, validates assignment and teaching-staff role, ensures the target user is an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; when required, sets &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; on the map, and returns list rows with display name, submitted artifact summary using the same helpers as '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''', and review status. Adding a participant by username is idempotent.&lt;br /&gt;
&lt;br /&gt;
=== Phase 2 — Reporting and comparison ===&lt;br /&gt;
&lt;br /&gt;
The comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' returns the JSON consumed by the Calibration report React page and is implemented on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. It returns one payload for the stacked chart and rubric detail views. The server resolves the instructor calibration map, identifies the reviewee as the calibration participant, loads rubric &amp;lt;code&amp;gt;Item&amp;lt;/code&amp;gt;s, loads instructor and student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; data for the same reviewed work. Per map, aggregation uses '''submitted''' student responses, taking the latest row by &amp;lt;code&amp;gt;updated_at&amp;lt;/code&amp;gt; among submitted responses, then runs &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; for per-item buckets.&lt;br /&gt;
&lt;br /&gt;
=== Phase 3 — Instructor calibration review UI ===&lt;br /&gt;
&lt;br /&gt;
The instructor uses the same rubric experience as students: draft and submit, then the submitted rubric is locked for further edits. That matches the course description of a normal-looking rubric review after '''Begin''' and establishes a stable instructor review for downstream comparison reports.&lt;br /&gt;
&lt;br /&gt;
=== High-level architecture ===&lt;br /&gt;
&lt;br /&gt;
All '''teaching-staff''' calibration JSON — the calibration map '''list''', mutating map and review actions including add participant, '''Begin''', instructor response, and delete, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' — goes through '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. Student-facing review HTTP lives in '''one new student review controller''' whose Ruby class and path names follow &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* '''Review mappings — JSON, teaching staff.''' Extended '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' serves the Calibration tab: list calibration maps, add a participant and the corresponding &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; map, '''Begin''' metadata, save or submit the instructor calibration review, optional map delete, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. Implement the report action as a thin entry point that delegates to POROs or private methods for assembly. &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence for saves uses the same shared rubric writer as elsewhere where possible.&lt;br /&gt;
&lt;br /&gt;
* '''Report — JSON, teaching staff.''' The report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' is a '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' action; declare it as a member or collection route in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt; next to the action implementation. It returns JSON: rubric items, instructor and student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; data for the reviewee, &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt;, reviewee hyperlinks/files via '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''Student reviews — JSON.''' The '''student review controller''' lists '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' for the student; for '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' rows it exposes a '''link''' to the teaching-staff comparison report JSON and UI to compare '''student vs instructor''' scores and feedback. Thin layer over existing review models; instructor map listing and report aggregation stay in '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''React SPA.''' Assignment editor → Calibration tab → '''Begin''' → instructor rubric page → report page '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''Server-side helpers.''' &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; builds per-item chart buckets. Submitted hyperlinks/files always go through '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''; details are under '''Implementation''' and '''Backend'''.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== UML diagram ==&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-domain-class-uml.png|720px|center]]&lt;br /&gt;
&lt;br /&gt;
Instructors and students are &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;s. The instructor’s calibration map is a &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; true whose reviewer is the instructor participant and whose reviewee is the calibration participant. Student calibration maps share the same &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt;, the calibration author, but use other &amp;lt;code&amp;gt;reviewer_id&amp;lt;/code&amp;gt; values. &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; store scores. List and report use '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' for hyperlinks/files; &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; supports report aggregation.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Flow: Review calibration ==&lt;br /&gt;
&lt;br /&gt;
'''Figure 1a — Calibration setup, overview.''' Student lane: review list and submit via the student review stack. Staff lane: Calibration tab and rubric send teaching-staff calibration traffic to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. The two lanes can happen in any order; details and routes are in '''Implementation''' and the '''Appendix'''.&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-pipelines-flow.png|820px|center]]&lt;br /&gt;
&lt;br /&gt;
'''Figure 1b — Calibration report, overview.''' Teaching staff open the report in React; the page issues one &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' and receives either &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt; report JSON. Assembly of rubric, scores, &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt;, and the &amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt; stack is specified in code and in '''Implementation''' and '''Backend'''; the diagram highlights the browser–Rails exchange.&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-flow-report.png|720px|center]]&lt;br /&gt;
&lt;br /&gt;
Figure 1b: maps for one calibration run share a &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt; aggregates '''submitted''' student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; rows per map using the latest &amp;lt;code&amp;gt;updated_at&amp;lt;/code&amp;gt; among submitted rows.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area&lt;br /&gt;
! Implementation &lt;br /&gt;
! Verify&lt;br /&gt;
|-&lt;br /&gt;
| Teaching-staff HTTP&lt;br /&gt;
| Implement list, create, begin, instructor response save, map delete, '''and''' comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', optionally split into a Rails concern mixed into that controller, scoped with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; and teaching-staff authorization. Add '''one new student review controller''': list student '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews'''; for &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; items, '''link''' to the comparison report and expose '''student vs instructor''' score/feedback comparison. Keep the student controller thin: delegate to existing review models and reuse the same report JSON payload shape where appropriate.&lt;br /&gt;
| Request specs for all &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; calibration actions; request specs for student review list, calibration link, and comparison paths; coverage lives on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' for teaching-staff calibration HTTP.&lt;br /&gt;
|-&lt;br /&gt;
| Report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| Report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''': thin entry + PORO/private steps; stable JSON for the React report page. Uses &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; and the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' stack for reviewee hyperlinks/files.&lt;br /&gt;
| Request spec for report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' documents required JSON keys and error cases.&lt;br /&gt;
|-&lt;br /&gt;
| Instructor review write&lt;br /&gt;
| Instructor calibration draft/submit on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. Delegate &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence to a '''shared rubric writer'''; action handles guards, validation, and JSON only.&lt;br /&gt;
| Request specs: draft, submit, 422, invalid &amp;lt;code&amp;gt;answers&amp;lt;/code&amp;gt; payload.&lt;br /&gt;
|-&lt;br /&gt;
| Submitted content&lt;br /&gt;
| Build list and report &amp;lt;code&amp;gt;{ hyperlinks, files }&amp;lt;/code&amp;gt; through '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' or the same service object that controller already delegates to.&lt;br /&gt;
| Specs assert list and report payloads match submitted-content for the same participant.&lt;br /&gt;
|-&lt;br /&gt;
| Team + participant bootstrap&lt;br /&gt;
| Implement solo-team and related bootstrap logic in a '''team/participant service''' called from map '''create''', with '''&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;''' updates limited to what map create and listing need. Keep &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; hyperlink behavior correct for those paths.&lt;br /&gt;
| Service/unit tests + create path; hyperlink regressions if any.&lt;br /&gt;
|-&lt;br /&gt;
| Scope hygiene&lt;br /&gt;
| Scope edits to calibration controllers, services, routes, and React surfaces named in this document, plus the smallest shared-model touches that map create and listing require.&lt;br /&gt;
| Code review checklist; small, focused diffs.&lt;br /&gt;
|-&lt;br /&gt;
| Report UI&lt;br /&gt;
| &amp;lt;code&amp;gt;CalibrationReview&amp;lt;/code&amp;gt; and related report components match the mockup while consuming the report JSON shape from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
| Vitest; refresh '''Figure 2''' left pane when the shipped UI changes.&lt;br /&gt;
|-&lt;br /&gt;
| E2E · repo · docs&lt;br /&gt;
| Browser E2E over &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; calibration routes and report tab. Record generated-asset rules for this repo in the PR checklist or team wiki so commits stay consistent.&lt;br /&gt;
| CI or &amp;lt;code&amp;gt;test:e2e&amp;lt;/code&amp;gt;; PR checklist.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Backend — controller behavior ==&lt;br /&gt;
&lt;br /&gt;
Teaching-staff APIs use auth, '''&amp;lt;code&amp;gt;authorize&amp;lt;/code&amp;gt;''', and staff checks consistent with the assignment. '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', extended for calibration, hosts all teaching-staff calibration endpoints: it lists calibration maps where the current user is the instructor reviewer and &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; is true; each row includes map/reviewee fields, '''&amp;lt;code&amp;gt;participant_name&amp;lt;/code&amp;gt;''', '''&amp;lt;code&amp;gt;review_status&amp;lt;/code&amp;gt;''' from the latest '''&amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;''', and '''&amp;lt;code&amp;gt;submitted_content&amp;lt;/code&amp;gt;''' built with the '''same service/helpers as &amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''. '''Create''' resolves username to '''&amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;''', runs team bootstrap via a '''dedicated service''', '''&amp;lt;code&amp;gt;find_or_create_by!&amp;lt;/code&amp;gt;''' the instructor→reviewee map with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; true, returns '''&amp;lt;code&amp;gt;201&amp;lt;/code&amp;gt;'''. '''Begin''' returns '''&amp;lt;code&amp;gt;map_id&amp;lt;/code&amp;gt;''', optional '''&amp;lt;code&amp;gt;response_id&amp;lt;/code&amp;gt;''', '''&amp;lt;code&amp;gt;redirect_to&amp;lt;/code&amp;gt;'''. '''Instructor response save''' validates input, enforces the submitted-state rules, delegates persistence to a '''shared writer''', returns '''&amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt;''' JSON. '''Destroy''' returns '''&amp;lt;code&amp;gt;204&amp;lt;/code&amp;gt;''' after ownership checks. '''Comparison-report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' for the instructor’s calibration map id on the same controller returns rubric items, '''&amp;lt;code&amp;gt;instructor_response&amp;lt;/code&amp;gt;''', submitted student '''&amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;s''', '''&amp;lt;code&amp;gt;CalibrationPerItemSummary.build&amp;lt;/code&amp;gt;''', reviewee hyperlinks/files via the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt; stack''', '''&amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt;''' JSON, or '''&amp;lt;code&amp;gt;404&amp;lt;/code&amp;gt;''' / '''&amp;lt;code&amp;gt;422&amp;lt;/code&amp;gt;''' when assignment, map, or rubric preconditions fail.&lt;br /&gt;
&lt;br /&gt;
'''Student review controller — new, thin layer''' — Lists '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' for the logged-in student as reviewer; for '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' maps, provides a '''link''' to the calibration comparison using the instructor-facing report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' or a student-safe JSON projection, plus fields and UI so the student can see '''how their scores and feedback compare''' to the instructor’s. Delegates to existing review / &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; models; instructor Calibration tab CRUD, rubric save, and server-side report aggregation live in '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Appendix — API routes ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method&lt;br /&gt;
! Path pattern&lt;br /&gt;
! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration list.''' List calibration &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; rows for teaching staff; exact path under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; is defined in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration create.''' Add calibration participant; request includes &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt;. Exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;DELETE&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration map delete.''' Remove one calibration map by id; exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Begin.''' Return navigation JSON for instructor calibration rubric SPA; exact member path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Instructor response.''' Save draft or submit instructor calibration review; exact member path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration report.''' Return comparison report JSON for the instructor calibration map id; exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Student review controller.''' Student review list, calibration report links, and comparison to instructor; verbs and paths declared in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Frontend — data expectations and user flow ==&lt;br /&gt;
&lt;br /&gt;
'''Calibration tab table:''' Each row shows map id, display name, submitted links and files summary, and status: '''Not started''', '''Draft''', or '''Submitted'''. Data comes from the teaching-staff calibration '''list''' &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; routes, with submitted fields from the same pipeline as '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
'''Report page:''' Rubric items in order, instructor scores and comments per item, each student’s scores, and per-item buckets from the report JSON.&lt;br /&gt;
&lt;br /&gt;
'''User flow:'''&lt;br /&gt;
&lt;br /&gt;
# Open Assignment editor, then Calibration tab.&lt;br /&gt;
# Enter username; system ensures participant and &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; map.&lt;br /&gt;
# &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; calibration list on &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt;; table refreshes.&lt;br /&gt;
# Begin; &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; begin; SPA opens the instructor calibration review page.&lt;br /&gt;
# Save draft or submit; &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; instructor response on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
# Open report; &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; comparison JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''; charts and rubric detail render.&lt;br /&gt;
&lt;br /&gt;
'''Student review list: ''' The student review controller lists the student’s reviews / '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s'''. Calibration rows show a '''link''' to open the comparison for instructor versus class scores and, where the UI implements it, the student’s position relative to the instructor.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Assignment editor — Calibration tab ===&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-assignment-tab.png|480px|center]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.2em;&amp;quot;&amp;gt;'''Figure 1.''' Reference UI — assignment editor Calibration tab: add participants, open report or '''Begin''' the instructor calibration review, and see submitted items. Each row is filled from the calibration '''list''' '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt;, with &amp;lt;code&amp;gt;participant_name&amp;lt;/code&amp;gt;, submitted content from the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' pipeline, and &amp;lt;code&amp;gt;review_status&amp;lt;/code&amp;gt;. Adding the same username again is idempotent.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Calibration report: comparison UI ===&lt;br /&gt;
&lt;br /&gt;
{| style=&amp;quot;width:100%; max-width:980px; margin:0.8em auto; border-collapse:separate; border-spacing:10px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center; width:50%;&amp;quot; | '''Reference UI — Class comparison, stacked chart tab'''&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center; width:50%;&amp;quot; | '''Mockup chart layout; same report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; JSON from &amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center;&amp;quot; | [[File:E2609-calibration-report-stacked.png|440px]]&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center;&amp;quot; | [[File:E2609-calibration-stacked-chart-improved-mockup.png|440px]]&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.35em;&amp;quot;&amp;gt;'''Figure 2.''' Side by side: reference stacked-chart tab versus the intended layout. The mockup adds per-criterion axis labels, framing, legend, and full criterion text in tooltips in &amp;lt;code&amp;gt;CalibrationReview&amp;lt;/code&amp;gt;. Same report JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-report-rubric-detail.png|480px|center]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.2em;&amp;quot;&amp;gt;'''Figure 3.''' Reference UI — rubric detail: instructor review versus class summary and reviewer selections, using the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Test Plan ===&lt;br /&gt;
&lt;br /&gt;
==== Automated Testing — RSpec ====&lt;br /&gt;
&lt;br /&gt;
* '''Request specs''' for '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' calibration: list, create participant/map, begin, instructor response for draft, submit, and error paths, destroy, and comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. Co-locate spec files under &amp;lt;code&amp;gt;spec/requests/&amp;lt;/code&amp;gt; with the route names in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* '''Request specs''' for the '''student review controller''': list reviews/maps; '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' rows include correct report link or comparison payload; responses match the logged-in student reviewer.&lt;br /&gt;
* Service specs for aggregation logic, for example &amp;lt;code&amp;gt;spec/services/calibration_per_item_summary_spec.rb&amp;lt;/code&amp;gt; next to &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt;.&lt;br /&gt;
* '''Factories / fixtures''' for &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; maps, participants, and submitted responses as needed for the above.&lt;br /&gt;
&lt;br /&gt;
==== Frontend Testing — Vitest ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/CalibrationReview.test.tsx&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/CalibrationInstructorReview.test.tsx&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationReportNormalize.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationSummary.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationReportDemo.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== End-to-end — E2E ====&lt;br /&gt;
&lt;br /&gt;
# Teaching staff: assignment editor → Calibration tab → calibration map list &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; → table or empty state renders cleanly in the browser.&lt;br /&gt;
# Teaching staff: comparison report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' → “Class comparison (stacked)” and “Rubric detail” render; chart region populated when &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt; is non-empty.&lt;br /&gt;
# Teaching staff: '''Begin''', including opening the calibration review by direct URL if supported → draft save → submit → UI matches post-submit lock rules.&lt;br /&gt;
# Student: open student review list → calibration row shows '''link''' to comparison for instructor versus class view → student can see '''how their review compares''' to the instructor’s when that panel is implemented.&lt;br /&gt;
&lt;br /&gt;
==== Manual UI Testing ====&lt;br /&gt;
&lt;br /&gt;
# Login as instructor/TA.&lt;br /&gt;
# Open assignment editor → Calibration tab.&lt;br /&gt;
# Add a calibration participant by username.&lt;br /&gt;
# Click '''View report''' and verify stacked and rubric detail tabs.&lt;br /&gt;
# Enter/edit instructor review: save draft, submit, and confirm the form locks.&lt;br /&gt;
# Login as student calibrator: open student review list; for a calibration map, follow the report/comparison link and confirm scores or feedback align with expectations versus the instructor view.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
* Add an overall calibration score and trend visualization per participant.&lt;br /&gt;
* Provide additional endpoints for:&lt;br /&gt;
** calibration summary per map,&lt;br /&gt;
** detailed agreement breakdown per item across all rounds.&lt;br /&gt;
* Improve diagnostics, such as highlighting criteria with the largest score spread across reviewers.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza wiki main page]&lt;br /&gt;
* Front-end pull request: [https://github.com/expertiza/reimplementation-front-end/pull/164 expertiza/reimplementation-front-end#164]&lt;br /&gt;
* Back-end pull request: [https://github.com/expertiza/reimplementation-back-end/pull/323 expertiza/reimplementation-back-end#323]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Team ==&lt;br /&gt;
&lt;br /&gt;
* Mentor: Dr. Ed Gehringer&lt;br /&gt;
* Team members: Xiangjun Mi, Rujuta Palimkar, Emma Hassler&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167958</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167958"/>
		<updated>2026-04-14T21:45:09Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Purpose */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
Expertiza currently authenticates users with its own login page, implemented by the Expertiza application.  Expertiza has been used at many campuses, however, and each has their own SSO (single signon) protocol that students and staff use to log into other applications. It is more secure for applications to use the standard approach at sites where they are in use, and it also frees Expertiza from managing passwords, and thus removes the risk of compromise. By integrating [https://openid.net/developers/how-connect-works/ OIDC] login, users can authenticate using their existing university credentials, providing a familiar and streamlined login experience. Traditional username and password login will continue to be supported alongside OIDC, allowing users to choose their preferred authentication method.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users select a provider from a dropdown on the login page that redirects them to the school's OIDC provider, authenticates them, and redirects back to the application with a valid session. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown. On provider selection, the frontend posts to the backend, which returns an authorization URL. After the user completes the OIDC flow, the IdP redirects back to the frontend callback, which posts the authorization code and state to the backend to complete login.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Linking ===&lt;br /&gt;
Match the authenticated user's email from the ID token to an existing local account. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, an error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;). Client credentials (client ID, client secret) are stored in environment variables and injected via ERB. Providers that support OIDC discovery have their endpoints and JWKS keys fetched automatically. The system supports multiple OIDC provider configurations simultaneously. The configuration is validated at boot via an initializer, surfacing missing values immediately on deploy rather than at login time.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, and PKCE code verifier are stored server-side in an &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and deleted after use. This table is named generically to support future federated auth protocols such as SAML. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
Gracefully handle failures such as no local account matching the authenticated email, expired or invalid state parameters, token exchange errors, ID token verification errors, and user-denied consent at the IdP.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request specs use WebMock to stub the identity provider's discovery, token, and JWKS endpoints, allowing the full controller logic (state management, token exchange, ID token verification, user matching) to be tested without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC Provider-2026-04-06-223511.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;. For providers with &amp;lt;code&amp;gt;discovery: true&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document is fetched using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Discovery results are not aggressively cached to allow for key rotation; on signature verification failure, keys are re-fetched and verification is retried once.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, and creation timestamp. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint (and a temporary &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; for direct IdP redirect during backend-only testing) that accepts the authorization code and state. Look up the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row by state, rejecting the request if no row is found or if the row is older than 5 minutes. Delete the row to prevent reuse. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. Extract the user's email from the ID token claims and look up a matching local user. If a match is found, issue a session JWT using the same &amp;lt;code&amp;gt;JsonWebToken.encode&amp;lt;/code&amp;gt; method and payload structure as the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; action. If no match is found, return a 404 error indicating no local account exists for that email.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):''' On page load, the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider below the existing username and password form. If the request fails or returns empty, the component renders nothing and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
* '''Initiate Login (Step 2):''' When the user selects a provider from the dropdown, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the selected provider id. On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
The implementation uses the '''Strategy pattern''' for provider configuration. Each OIDC provider is defined declaratively in YAML with its own credentials, scopes, and endpoints, while the controller logic remains provider-agnostic. Adding a new identity provider requires only a new configuration block and environment variables, with no code changes.&lt;br /&gt;
&lt;br /&gt;
=== Schema ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
No foreign keys or associations to other tables. This table is intentionally generic to support future federated auth protocols such as SAML.&lt;br /&gt;
&lt;br /&gt;
== Library Choice ==&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
== File Diffs and Additions (Prototype and subject to change) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (Rails) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/controllers/oidc_login_controller.rb app/controllers/oidc_login_controller.rb]  — Controller with providers, client_select, and callback actions&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/oidc_config.rb app/models/oidc_config.rb]                 — YAML config loader with validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/auth_request.rb app/models/auth_request.rb]                — ActiveRecord model for state/nonce/PKCE storage&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/oidc_providers.yml config/oidc_providers.yml]                 — Provider configuration (ERB for env var injection)&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/initializers/oidc.rb config/initializers/oidc.rb]               — Boot-time config validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/db/migrate/20260407003623_create_auth_requests.rb db/migrate/*_create_auth_requests.rb]      — Migration for auth_requests table&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb]          — Config loading, validation, missing keys, public_list secrets exclusion, provider lookup&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/auth_request_spec.rb spec/models/auth_request_spec.rb]         — State uniqueness, expiry scope, cleanup of stale rows&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb]         — Endpoint tests covering:&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
* '''TODO''' Returns empty array when no providers configured&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* '''TODO''' Returns authorization URL with expected query parameters (client_id, redirect_uri, scope, state, nonce, code_challenge)&lt;br /&gt;
* '''TODO''' Creates an auth_requests row&lt;br /&gt;
* '''TODO''' Returns 404 for unknown provider&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Happy Path'''&lt;br /&gt;
* '''TODO''' Exchanges valid code and state for a session JWT with same payload structure as password login&lt;br /&gt;
* '''TODO''' Deletes the auth_requests row after successful use&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — CSRF Protection (State Validation)'''&lt;br /&gt;
* '''TODO''' Rejects unknown state (422)&lt;br /&gt;
* '''TODO''' Rejects expired state older than 5 minutes (422)&lt;br /&gt;
* '''TODO''' Rejects already-consumed state / replay (422)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Replay Protection (Nonce Validation)'''&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched nonce&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Token Verification'''&lt;br /&gt;
* '''TODO''' Rejects ID token with invalid signature&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched issuer&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched audience (client_id)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — User Matching'''&lt;br /&gt;
* '''TODO''' Returns 404 when no local user matches the email&lt;br /&gt;
* '''TODO''' Matches correct user when multiple users exist&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — PKCE'''&lt;br /&gt;
* '''TODO''' Sends code_verifier to the token endpoint during code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (React) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.tsx src/components/OidcLogin/OidcLogin.tsx]     — Provider dropdown component&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.tsx src/pages/OidcCallback/OidcCallback.tsx]    — Callback page handling code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.test.tsx src/components/OidcLogin/OidcLogin.test.tsx]     — Provider dropdown component tests&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx]    — Callback page tests&lt;br /&gt;
&lt;br /&gt;
'''OidcLogin Component'''&lt;br /&gt;
* '''TODO''' Renders dropdown with providers when GET /auth/providers returns a non-empty list&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers fails&lt;br /&gt;
* '''TODO''' Calls POST /auth/client-select with selected provider id on dropdown change&lt;br /&gt;
* '''TODO''' Redirects browser to returned authorization URL on successful client-select&lt;br /&gt;
* '''TODO''' Does not redirect when client-select fails&lt;br /&gt;
* '''TODO''' Existing login form remains visible and functional alongside the dropdown&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* '''TODO''' Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* '''TODO''' Stores session JWT and dispatches auth state on success&lt;br /&gt;
* '''TODO''' Redirects to dashboard on successful login&lt;br /&gt;
* '''TODO''' Displays error alert and redirects to login on backend failure (e.g. no matching account)&lt;br /&gt;
* '''TODO''' Displays error and redirects to login when IdP returns an error parameter (e.g. user denied consent) without calling the backend&lt;br /&gt;
* '''TODO''' Redirects to login when code or state query parameters are missing&lt;br /&gt;
* '''TODO''' Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 GET  /auth/callback       → React OidcCallback component&lt;br /&gt;
&lt;br /&gt;
== Development Stories (Planning) ==&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML at boot, exposing methods to list providers and look up a provider by id.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Raise a clear boot-time error if required values are missing via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add unit tests for config loading, validation, and missing key detection.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — Auth Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, and PKCE values, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, indexed, unique), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; model with a uniqueness validation on state, an &amp;lt;code&amp;gt;expired&amp;lt;/code&amp;gt; scope, and a &amp;lt;code&amp;gt;cleanup_expired&amp;lt;/code&amp;gt; class method.&lt;br /&gt;
* Add a rake task or scheduled job to periodically clean up expired rows.&lt;br /&gt;
* Add unit tests for creation, lookup, expiry, and deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format and a case with multiple providers.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept a &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; param and look up the provider config.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier/challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Add request specs covering the happy path, unknown provider, and that the auth_requests row is created.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params.&lt;br /&gt;
* Look up and delete the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row. Reject if not found or expired.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, client_id, and nonce.&lt;br /&gt;
* Extract the email claim and match to an existing local user.&lt;br /&gt;
* On match: issue a session JWT using the same payload structure as &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and return it with the user profile.&lt;br /&gt;
* On no match: return a 404 error indicating no local account was found.&lt;br /&gt;
* Handle &amp;lt;code&amp;gt;ActiveRecord::RecordNotFound&amp;lt;/code&amp;gt; (invalid/expired state) and &amp;lt;code&amp;gt;InvalidToken&amp;lt;/code&amp;gt; (verification failure) exceptions.&lt;br /&gt;
* Add request specs covering the happy path, expired state, invalid state, and no matching user.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — Provider Dropdown on Login Page ===&lt;br /&gt;
'''As a''' user, '''I want''' to see a provider dropdown on the login page, '''so that''' I can authenticate with my school credentials.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Render a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown with a disabled &amp;quot;Sign in with...&amp;quot; default option.&lt;br /&gt;
* If the request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' selecting a provider from the dropdown to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On selection change, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the redirect and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC backend, '''so that''' I have confidence the endpoints and models work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add request specs for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Mock the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Cover error scenarios: expired state, invalid state, unknown provider, no matching user, invalid ID token.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; covering creation, uniqueness, expiry scope, and cleanup.&lt;br /&gt;
* Add unit tests for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering loading, validation, and missing key detection.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; is unaffected.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt;: renders dropdown when providers are returned, renders nothing on empty or failed response, triggers &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; on selection.&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;: posts code and state to backend on mount, redirects to dashboard on success, displays error and redirects to login on failure, handles IdP error parameter without calling the backend.&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Additional Refactoring Stories ===&lt;br /&gt;
&lt;br /&gt;
==== Story 11: Backend — Unified Session Response ====&lt;br /&gt;
'''As a''' developer, '''I want''' the session object returned to the frontend clearly defined and reusable by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic from &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (e.g. &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Define a consistent response structure (e.g. &amp;lt;code&amp;gt;{ token, user: { id, name, full_name, role, institution_id } }&amp;lt;/code&amp;gt;) and use it in both controllers.&lt;br /&gt;
* Update the existing password login endpoint to use the shared method without changing its external response shape.&lt;br /&gt;
* Add or update request specs for both login endpoints to assert the response structure matches.&lt;br /&gt;
&lt;br /&gt;
==== Story 12: Frontend — Externalize Hardcoded Configuration ====&lt;br /&gt;
'''As a''' developer, '''I want''' all hardcoded values moved to isolated configuration files, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the frontend API base URL to an environment variable or shared config file (e.g. &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;) and replace all hardcoded references.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
==== Story 13: Backend — Swagger Documentation for Provider Endpoints ====&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC provider endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (including success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
==== Story 14: Backend — Cleanup Expired Auth Requests ====&lt;br /&gt;
'''As a''' developer, '''I want''' expired auth request rows cleaned up automatically, '''so that''' the table does not grow unbounded from abandoned login attempts.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a recurring ActiveJob solid_queue &amp;lt;code&amp;gt;auth_requests:cleanup&amp;lt;/code&amp;gt; that calls &amp;lt;code&amp;gt;AuthRequest.cleanup_expired&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The task deletes all rows with &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; older than 5 minutes.&lt;br /&gt;
* Schedule the task to run periodically (e.g. 24 hours).&lt;br /&gt;
* Add a test verifying that only expired rows are deleted.&lt;br /&gt;
&lt;br /&gt;
== Demo ==&lt;br /&gt;
&lt;br /&gt;
todo add screenshots of oidc login at each step&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167956</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167956"/>
		<updated>2026-04-14T21:38:55Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Purpose */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
By integrating OIDC login, users can authenticate using their existing university credentials, providing a familiar and streamlined login experience. Traditional username and password login will continue to be supported alongside OIDC, allowing users to choose their preferred authentication method.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users select a provider from a dropdown on the login page that redirects them to the school's OIDC provider, authenticates them, and redirects back to the application with a valid session. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown. On provider selection, the frontend posts to the backend, which returns an authorization URL. After the user completes the OIDC flow, the IdP redirects back to the frontend callback, which posts the authorization code and state to the backend to complete login.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Linking ===&lt;br /&gt;
Match the authenticated user's email from the ID token to an existing local account. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, an error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;). Client credentials (client ID, client secret) are stored in environment variables and injected via ERB. Providers that support OIDC discovery have their endpoints and JWKS keys fetched automatically. The system supports multiple OIDC provider configurations simultaneously. The configuration is validated at boot via an initializer, surfacing missing values immediately on deploy rather than at login time.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, and PKCE code verifier are stored server-side in an &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and deleted after use. This table is named generically to support future federated auth protocols such as SAML. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
Gracefully handle failures such as no local account matching the authenticated email, expired or invalid state parameters, token exchange errors, ID token verification errors, and user-denied consent at the IdP.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request specs use WebMock to stub the identity provider's discovery, token, and JWKS endpoints, allowing the full controller logic (state management, token exchange, ID token verification, user matching) to be tested without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC Provider-2026-04-06-223511.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;. For providers with &amp;lt;code&amp;gt;discovery: true&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document is fetched using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Discovery results are not aggressively cached to allow for key rotation; on signature verification failure, keys are re-fetched and verification is retried once.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, and creation timestamp. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint (and a temporary &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; for direct IdP redirect during backend-only testing) that accepts the authorization code and state. Look up the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row by state, rejecting the request if no row is found or if the row is older than 5 minutes. Delete the row to prevent reuse. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. Extract the user's email from the ID token claims and look up a matching local user. If a match is found, issue a session JWT using the same &amp;lt;code&amp;gt;JsonWebToken.encode&amp;lt;/code&amp;gt; method and payload structure as the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; action. If no match is found, return a 404 error indicating no local account exists for that email.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):''' On page load, the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider below the existing username and password form. If the request fails or returns empty, the component renders nothing and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
* '''Initiate Login (Step 2):''' When the user selects a provider from the dropdown, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the selected provider id. On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
The implementation uses the '''Strategy pattern''' for provider configuration. Each OIDC provider is defined declaratively in YAML with its own credentials, scopes, and endpoints, while the controller logic remains provider-agnostic. Adding a new identity provider requires only a new configuration block and environment variables, with no code changes.&lt;br /&gt;
&lt;br /&gt;
=== Schema ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
No foreign keys or associations to other tables. This table is intentionally generic to support future federated auth protocols such as SAML.&lt;br /&gt;
&lt;br /&gt;
== Library Choice ==&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
== File Diffs and Additions (Prototype and subject to change) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (Rails) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/controllers/oidc_login_controller.rb app/controllers/oidc_login_controller.rb]  — Controller with providers, client_select, and callback actions&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/oidc_config.rb app/models/oidc_config.rb]                 — YAML config loader with validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/auth_request.rb app/models/auth_request.rb]                — ActiveRecord model for state/nonce/PKCE storage&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/oidc_providers.yml config/oidc_providers.yml]                 — Provider configuration (ERB for env var injection)&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/initializers/oidc.rb config/initializers/oidc.rb]               — Boot-time config validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/db/migrate/20260407003623_create_auth_requests.rb db/migrate/*_create_auth_requests.rb]      — Migration for auth_requests table&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb]          — Config loading, validation, missing keys, public_list secrets exclusion, provider lookup&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/auth_request_spec.rb spec/models/auth_request_spec.rb]         — State uniqueness, expiry scope, cleanup of stale rows&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb]         — Endpoint tests covering:&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
* '''TODO''' Returns empty array when no providers configured&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* '''TODO''' Returns authorization URL with expected query parameters (client_id, redirect_uri, scope, state, nonce, code_challenge)&lt;br /&gt;
* '''TODO''' Creates an auth_requests row&lt;br /&gt;
* '''TODO''' Returns 404 for unknown provider&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Happy Path'''&lt;br /&gt;
* '''TODO''' Exchanges valid code and state for a session JWT with same payload structure as password login&lt;br /&gt;
* '''TODO''' Deletes the auth_requests row after successful use&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — CSRF Protection (State Validation)'''&lt;br /&gt;
* '''TODO''' Rejects unknown state (422)&lt;br /&gt;
* '''TODO''' Rejects expired state older than 5 minutes (422)&lt;br /&gt;
* '''TODO''' Rejects already-consumed state / replay (422)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Replay Protection (Nonce Validation)'''&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched nonce&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Token Verification'''&lt;br /&gt;
* '''TODO''' Rejects ID token with invalid signature&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched issuer&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched audience (client_id)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — User Matching'''&lt;br /&gt;
* '''TODO''' Returns 404 when no local user matches the email&lt;br /&gt;
* '''TODO''' Matches correct user when multiple users exist&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — PKCE'''&lt;br /&gt;
* '''TODO''' Sends code_verifier to the token endpoint during code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (React) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.tsx src/components/OidcLogin/OidcLogin.tsx]     — Provider dropdown component&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.tsx src/pages/OidcCallback/OidcCallback.tsx]    — Callback page handling code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.test.tsx src/components/OidcLogin/OidcLogin.test.tsx]     — Provider dropdown component tests&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx]    — Callback page tests&lt;br /&gt;
&lt;br /&gt;
'''OidcLogin Component'''&lt;br /&gt;
* '''TODO''' Renders dropdown with providers when GET /auth/providers returns a non-empty list&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers fails&lt;br /&gt;
* '''TODO''' Calls POST /auth/client-select with selected provider id on dropdown change&lt;br /&gt;
* '''TODO''' Redirects browser to returned authorization URL on successful client-select&lt;br /&gt;
* '''TODO''' Does not redirect when client-select fails&lt;br /&gt;
* '''TODO''' Existing login form remains visible and functional alongside the dropdown&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* '''TODO''' Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* '''TODO''' Stores session JWT and dispatches auth state on success&lt;br /&gt;
* '''TODO''' Redirects to dashboard on successful login&lt;br /&gt;
* '''TODO''' Displays error alert and redirects to login on backend failure (e.g. no matching account)&lt;br /&gt;
* '''TODO''' Displays error and redirects to login when IdP returns an error parameter (e.g. user denied consent) without calling the backend&lt;br /&gt;
* '''TODO''' Redirects to login when code or state query parameters are missing&lt;br /&gt;
* '''TODO''' Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 GET  /auth/callback       → React OidcCallback component&lt;br /&gt;
&lt;br /&gt;
== Development Stories (Planning) ==&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML at boot, exposing methods to list providers and look up a provider by id.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Raise a clear boot-time error if required values are missing via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add unit tests for config loading, validation, and missing key detection.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — Auth Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, and PKCE values, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, indexed, unique), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; model with a uniqueness validation on state, an &amp;lt;code&amp;gt;expired&amp;lt;/code&amp;gt; scope, and a &amp;lt;code&amp;gt;cleanup_expired&amp;lt;/code&amp;gt; class method.&lt;br /&gt;
* Add a rake task or scheduled job to periodically clean up expired rows.&lt;br /&gt;
* Add unit tests for creation, lookup, expiry, and deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format and a case with multiple providers.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept a &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; param and look up the provider config.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier/challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Add request specs covering the happy path, unknown provider, and that the auth_requests row is created.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params.&lt;br /&gt;
* Look up and delete the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row. Reject if not found or expired.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, client_id, and nonce.&lt;br /&gt;
* Extract the email claim and match to an existing local user.&lt;br /&gt;
* On match: issue a session JWT using the same payload structure as &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and return it with the user profile.&lt;br /&gt;
* On no match: return a 404 error indicating no local account was found.&lt;br /&gt;
* Handle &amp;lt;code&amp;gt;ActiveRecord::RecordNotFound&amp;lt;/code&amp;gt; (invalid/expired state) and &amp;lt;code&amp;gt;InvalidToken&amp;lt;/code&amp;gt; (verification failure) exceptions.&lt;br /&gt;
* Add request specs covering the happy path, expired state, invalid state, and no matching user.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — Provider Dropdown on Login Page ===&lt;br /&gt;
'''As a''' user, '''I want''' to see a provider dropdown on the login page, '''so that''' I can authenticate with my school credentials.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Render a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown with a disabled &amp;quot;Sign in with...&amp;quot; default option.&lt;br /&gt;
* If the request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' selecting a provider from the dropdown to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On selection change, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the redirect and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC backend, '''so that''' I have confidence the endpoints and models work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add request specs for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Mock the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Cover error scenarios: expired state, invalid state, unknown provider, no matching user, invalid ID token.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; covering creation, uniqueness, expiry scope, and cleanup.&lt;br /&gt;
* Add unit tests for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering loading, validation, and missing key detection.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; is unaffected.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt;: renders dropdown when providers are returned, renders nothing on empty or failed response, triggers &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; on selection.&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;: posts code and state to backend on mount, redirects to dashboard on success, displays error and redirects to login on failure, handles IdP error parameter without calling the backend.&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Additional Refactoring Stories ===&lt;br /&gt;
&lt;br /&gt;
==== Story 11: Backend — Unified Session Response ====&lt;br /&gt;
'''As a''' developer, '''I want''' the session object returned to the frontend clearly defined and reusable by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic from &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (e.g. &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Define a consistent response structure (e.g. &amp;lt;code&amp;gt;{ token, user: { id, name, full_name, role, institution_id } }&amp;lt;/code&amp;gt;) and use it in both controllers.&lt;br /&gt;
* Update the existing password login endpoint to use the shared method without changing its external response shape.&lt;br /&gt;
* Add or update request specs for both login endpoints to assert the response structure matches.&lt;br /&gt;
&lt;br /&gt;
==== Story 12: Frontend — Externalize Hardcoded Configuration ====&lt;br /&gt;
'''As a''' developer, '''I want''' all hardcoded values moved to isolated configuration files, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the frontend API base URL to an environment variable or shared config file (e.g. &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;) and replace all hardcoded references.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
==== Story 13: Backend — Swagger Documentation for Provider Endpoints ====&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC provider endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (including success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
==== Story 14: Backend — Cleanup Expired Auth Requests ====&lt;br /&gt;
'''As a''' developer, '''I want''' expired auth request rows cleaned up automatically, '''so that''' the table does not grow unbounded from abandoned login attempts.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a recurring ActiveJob solid_queue &amp;lt;code&amp;gt;auth_requests:cleanup&amp;lt;/code&amp;gt; that calls &amp;lt;code&amp;gt;AuthRequest.cleanup_expired&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The task deletes all rows with &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; older than 5 minutes.&lt;br /&gt;
* Schedule the task to run periodically (e.g. 24 hours).&lt;br /&gt;
* Add a test verifying that only expired rows are deleted.&lt;br /&gt;
&lt;br /&gt;
== Demo ==&lt;br /&gt;
&lt;br /&gt;
todo add screenshots of oidc login at each step&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167955</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167955"/>
		<updated>2026-04-14T21:38:30Z</updated>

		<summary type="html">&lt;p&gt;Admin: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
By integrating [[https://openid.net/developers/how-connect-works/|OIDC]] login, users can authenticate using their existing university credentials, providing a familiar and streamlined login experience. Traditional username and password login will continue to be supported alongside OIDC, allowing users to choose their preferred authentication method.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users select a provider from a dropdown on the login page that redirects them to the school's OIDC provider, authenticates them, and redirects back to the application with a valid session. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown. On provider selection, the frontend posts to the backend, which returns an authorization URL. After the user completes the OIDC flow, the IdP redirects back to the frontend callback, which posts the authorization code and state to the backend to complete login.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Linking ===&lt;br /&gt;
Match the authenticated user's email from the ID token to an existing local account. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, an error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;). Client credentials (client ID, client secret) are stored in environment variables and injected via ERB. Providers that support OIDC discovery have their endpoints and JWKS keys fetched automatically. The system supports multiple OIDC provider configurations simultaneously. The configuration is validated at boot via an initializer, surfacing missing values immediately on deploy rather than at login time.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, and PKCE code verifier are stored server-side in an &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and deleted after use. This table is named generically to support future federated auth protocols such as SAML. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
Gracefully handle failures such as no local account matching the authenticated email, expired or invalid state parameters, token exchange errors, ID token verification errors, and user-denied consent at the IdP.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request specs use WebMock to stub the identity provider's discovery, token, and JWKS endpoints, allowing the full controller logic (state management, token exchange, ID token verification, user matching) to be tested without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC Provider-2026-04-06-223511.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;. For providers with &amp;lt;code&amp;gt;discovery: true&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document is fetched using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Discovery results are not aggressively cached to allow for key rotation; on signature verification failure, keys are re-fetched and verification is retried once.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, and creation timestamp. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint (and a temporary &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; for direct IdP redirect during backend-only testing) that accepts the authorization code and state. Look up the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row by state, rejecting the request if no row is found or if the row is older than 5 minutes. Delete the row to prevent reuse. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. Extract the user's email from the ID token claims and look up a matching local user. If a match is found, issue a session JWT using the same &amp;lt;code&amp;gt;JsonWebToken.encode&amp;lt;/code&amp;gt; method and payload structure as the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; action. If no match is found, return a 404 error indicating no local account exists for that email.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):''' On page load, the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider below the existing username and password form. If the request fails or returns empty, the component renders nothing and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
* '''Initiate Login (Step 2):''' When the user selects a provider from the dropdown, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the selected provider id. On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
The implementation uses the '''Strategy pattern''' for provider configuration. Each OIDC provider is defined declaratively in YAML with its own credentials, scopes, and endpoints, while the controller logic remains provider-agnostic. Adding a new identity provider requires only a new configuration block and environment variables, with no code changes.&lt;br /&gt;
&lt;br /&gt;
=== Schema ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
No foreign keys or associations to other tables. This table is intentionally generic to support future federated auth protocols such as SAML.&lt;br /&gt;
&lt;br /&gt;
== Library Choice ==&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
== File Diffs and Additions (Prototype and subject to change) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (Rails) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/controllers/oidc_login_controller.rb app/controllers/oidc_login_controller.rb]  — Controller with providers, client_select, and callback actions&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/oidc_config.rb app/models/oidc_config.rb]                 — YAML config loader with validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/auth_request.rb app/models/auth_request.rb]                — ActiveRecord model for state/nonce/PKCE storage&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/oidc_providers.yml config/oidc_providers.yml]                 — Provider configuration (ERB for env var injection)&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/initializers/oidc.rb config/initializers/oidc.rb]               — Boot-time config validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/db/migrate/20260407003623_create_auth_requests.rb db/migrate/*_create_auth_requests.rb]      — Migration for auth_requests table&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb]          — Config loading, validation, missing keys, public_list secrets exclusion, provider lookup&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/auth_request_spec.rb spec/models/auth_request_spec.rb]         — State uniqueness, expiry scope, cleanup of stale rows&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb]         — Endpoint tests covering:&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
* '''TODO''' Returns empty array when no providers configured&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* '''TODO''' Returns authorization URL with expected query parameters (client_id, redirect_uri, scope, state, nonce, code_challenge)&lt;br /&gt;
* '''TODO''' Creates an auth_requests row&lt;br /&gt;
* '''TODO''' Returns 404 for unknown provider&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Happy Path'''&lt;br /&gt;
* '''TODO''' Exchanges valid code and state for a session JWT with same payload structure as password login&lt;br /&gt;
* '''TODO''' Deletes the auth_requests row after successful use&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — CSRF Protection (State Validation)'''&lt;br /&gt;
* '''TODO''' Rejects unknown state (422)&lt;br /&gt;
* '''TODO''' Rejects expired state older than 5 minutes (422)&lt;br /&gt;
* '''TODO''' Rejects already-consumed state / replay (422)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Replay Protection (Nonce Validation)'''&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched nonce&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Token Verification'''&lt;br /&gt;
* '''TODO''' Rejects ID token with invalid signature&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched issuer&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched audience (client_id)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — User Matching'''&lt;br /&gt;
* '''TODO''' Returns 404 when no local user matches the email&lt;br /&gt;
* '''TODO''' Matches correct user when multiple users exist&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — PKCE'''&lt;br /&gt;
* '''TODO''' Sends code_verifier to the token endpoint during code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (React) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.tsx src/components/OidcLogin/OidcLogin.tsx]     — Provider dropdown component&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.tsx src/pages/OidcCallback/OidcCallback.tsx]    — Callback page handling code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.test.tsx src/components/OidcLogin/OidcLogin.test.tsx]     — Provider dropdown component tests&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx]    — Callback page tests&lt;br /&gt;
&lt;br /&gt;
'''OidcLogin Component'''&lt;br /&gt;
* '''TODO''' Renders dropdown with providers when GET /auth/providers returns a non-empty list&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers fails&lt;br /&gt;
* '''TODO''' Calls POST /auth/client-select with selected provider id on dropdown change&lt;br /&gt;
* '''TODO''' Redirects browser to returned authorization URL on successful client-select&lt;br /&gt;
* '''TODO''' Does not redirect when client-select fails&lt;br /&gt;
* '''TODO''' Existing login form remains visible and functional alongside the dropdown&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* '''TODO''' Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* '''TODO''' Stores session JWT and dispatches auth state on success&lt;br /&gt;
* '''TODO''' Redirects to dashboard on successful login&lt;br /&gt;
* '''TODO''' Displays error alert and redirects to login on backend failure (e.g. no matching account)&lt;br /&gt;
* '''TODO''' Displays error and redirects to login when IdP returns an error parameter (e.g. user denied consent) without calling the backend&lt;br /&gt;
* '''TODO''' Redirects to login when code or state query parameters are missing&lt;br /&gt;
* '''TODO''' Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 GET  /auth/callback       → React OidcCallback component&lt;br /&gt;
&lt;br /&gt;
== Development Stories (Planning) ==&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML at boot, exposing methods to list providers and look up a provider by id.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Raise a clear boot-time error if required values are missing via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add unit tests for config loading, validation, and missing key detection.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — Auth Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, and PKCE values, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, indexed, unique), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; model with a uniqueness validation on state, an &amp;lt;code&amp;gt;expired&amp;lt;/code&amp;gt; scope, and a &amp;lt;code&amp;gt;cleanup_expired&amp;lt;/code&amp;gt; class method.&lt;br /&gt;
* Add a rake task or scheduled job to periodically clean up expired rows.&lt;br /&gt;
* Add unit tests for creation, lookup, expiry, and deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format and a case with multiple providers.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept a &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; param and look up the provider config.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier/challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Add request specs covering the happy path, unknown provider, and that the auth_requests row is created.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params.&lt;br /&gt;
* Look up and delete the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row. Reject if not found or expired.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, client_id, and nonce.&lt;br /&gt;
* Extract the email claim and match to an existing local user.&lt;br /&gt;
* On match: issue a session JWT using the same payload structure as &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and return it with the user profile.&lt;br /&gt;
* On no match: return a 404 error indicating no local account was found.&lt;br /&gt;
* Handle &amp;lt;code&amp;gt;ActiveRecord::RecordNotFound&amp;lt;/code&amp;gt; (invalid/expired state) and &amp;lt;code&amp;gt;InvalidToken&amp;lt;/code&amp;gt; (verification failure) exceptions.&lt;br /&gt;
* Add request specs covering the happy path, expired state, invalid state, and no matching user.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — Provider Dropdown on Login Page ===&lt;br /&gt;
'''As a''' user, '''I want''' to see a provider dropdown on the login page, '''so that''' I can authenticate with my school credentials.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Render a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown with a disabled &amp;quot;Sign in with...&amp;quot; default option.&lt;br /&gt;
* If the request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' selecting a provider from the dropdown to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On selection change, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the redirect and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC backend, '''so that''' I have confidence the endpoints and models work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add request specs for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Mock the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Cover error scenarios: expired state, invalid state, unknown provider, no matching user, invalid ID token.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; covering creation, uniqueness, expiry scope, and cleanup.&lt;br /&gt;
* Add unit tests for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering loading, validation, and missing key detection.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; is unaffected.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt;: renders dropdown when providers are returned, renders nothing on empty or failed response, triggers &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; on selection.&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;: posts code and state to backend on mount, redirects to dashboard on success, displays error and redirects to login on failure, handles IdP error parameter without calling the backend.&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Additional Refactoring Stories ===&lt;br /&gt;
&lt;br /&gt;
==== Story 11: Backend — Unified Session Response ====&lt;br /&gt;
'''As a''' developer, '''I want''' the session object returned to the frontend clearly defined and reusable by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic from &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (e.g. &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Define a consistent response structure (e.g. &amp;lt;code&amp;gt;{ token, user: { id, name, full_name, role, institution_id } }&amp;lt;/code&amp;gt;) and use it in both controllers.&lt;br /&gt;
* Update the existing password login endpoint to use the shared method without changing its external response shape.&lt;br /&gt;
* Add or update request specs for both login endpoints to assert the response structure matches.&lt;br /&gt;
&lt;br /&gt;
==== Story 12: Frontend — Externalize Hardcoded Configuration ====&lt;br /&gt;
'''As a''' developer, '''I want''' all hardcoded values moved to isolated configuration files, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the frontend API base URL to an environment variable or shared config file (e.g. &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;) and replace all hardcoded references.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
==== Story 13: Backend — Swagger Documentation for Provider Endpoints ====&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC provider endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (including success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
==== Story 14: Backend — Cleanup Expired Auth Requests ====&lt;br /&gt;
'''As a''' developer, '''I want''' expired auth request rows cleaned up automatically, '''so that''' the table does not grow unbounded from abandoned login attempts.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a recurring ActiveJob solid_queue &amp;lt;code&amp;gt;auth_requests:cleanup&amp;lt;/code&amp;gt; that calls &amp;lt;code&amp;gt;AuthRequest.cleanup_expired&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The task deletes all rows with &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; older than 5 minutes.&lt;br /&gt;
* Schedule the task to run periodically (e.g. 24 hours).&lt;br /&gt;
* Add a test verifying that only expired rows are deleted.&lt;br /&gt;
&lt;br /&gt;
== Demo ==&lt;br /&gt;
&lt;br /&gt;
todo add screenshots of oidc login at each step&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167954</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167954"/>
		<updated>2026-04-14T21:38:04Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Purpose */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
By integrating [[OIDC|https://openid.net/developers/how-connect-works/]] login, users can authenticate using their existing university credentials, providing a familiar and streamlined login experience. Traditional username and password login will continue to be supported alongside OIDC, allowing users to choose their preferred authentication method.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users select a provider from a dropdown on the login page that redirects them to the school's OIDC provider, authenticates them, and redirects back to the application with a valid session. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown. On provider selection, the frontend posts to the backend, which returns an authorization URL. After the user completes the OIDC flow, the IdP redirects back to the frontend callback, which posts the authorization code and state to the backend to complete login.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Linking ===&lt;br /&gt;
Match the authenticated user's email from the ID token to an existing local account. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, an error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;). Client credentials (client ID, client secret) are stored in environment variables and injected via ERB. Providers that support OIDC discovery have their endpoints and JWKS keys fetched automatically. The system supports multiple OIDC provider configurations simultaneously. The configuration is validated at boot via an initializer, surfacing missing values immediately on deploy rather than at login time.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, and PKCE code verifier are stored server-side in an &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and deleted after use. This table is named generically to support future federated auth protocols such as SAML. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
Gracefully handle failures such as no local account matching the authenticated email, expired or invalid state parameters, token exchange errors, ID token verification errors, and user-denied consent at the IdP.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request specs use WebMock to stub the identity provider's discovery, token, and JWKS endpoints, allowing the full controller logic (state management, token exchange, ID token verification, user matching) to be tested without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC Provider-2026-04-06-223511.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;. For providers with &amp;lt;code&amp;gt;discovery: true&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document is fetched using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Discovery results are not aggressively cached to allow for key rotation; on signature verification failure, keys are re-fetched and verification is retried once.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, and creation timestamp. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint (and a temporary &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; for direct IdP redirect during backend-only testing) that accepts the authorization code and state. Look up the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row by state, rejecting the request if no row is found or if the row is older than 5 minutes. Delete the row to prevent reuse. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. Extract the user's email from the ID token claims and look up a matching local user. If a match is found, issue a session JWT using the same &amp;lt;code&amp;gt;JsonWebToken.encode&amp;lt;/code&amp;gt; method and payload structure as the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; action. If no match is found, return a 404 error indicating no local account exists for that email.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):''' On page load, the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider below the existing username and password form. If the request fails or returns empty, the component renders nothing and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
* '''Initiate Login (Step 2):''' When the user selects a provider from the dropdown, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the selected provider id. On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
The implementation uses the '''Strategy pattern''' for provider configuration. Each OIDC provider is defined declaratively in YAML with its own credentials, scopes, and endpoints, while the controller logic remains provider-agnostic. Adding a new identity provider requires only a new configuration block and environment variables, with no code changes.&lt;br /&gt;
&lt;br /&gt;
=== Schema ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
No foreign keys or associations to other tables. This table is intentionally generic to support future federated auth protocols such as SAML.&lt;br /&gt;
&lt;br /&gt;
== Library Choice ==&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
== File Diffs and Additions (Prototype and subject to change) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (Rails) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/controllers/oidc_login_controller.rb app/controllers/oidc_login_controller.rb]  — Controller with providers, client_select, and callback actions&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/oidc_config.rb app/models/oidc_config.rb]                 — YAML config loader with validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/auth_request.rb app/models/auth_request.rb]                — ActiveRecord model for state/nonce/PKCE storage&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/oidc_providers.yml config/oidc_providers.yml]                 — Provider configuration (ERB for env var injection)&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/initializers/oidc.rb config/initializers/oidc.rb]               — Boot-time config validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/db/migrate/20260407003623_create_auth_requests.rb db/migrate/*_create_auth_requests.rb]      — Migration for auth_requests table&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb]          — Config loading, validation, missing keys, public_list secrets exclusion, provider lookup&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/auth_request_spec.rb spec/models/auth_request_spec.rb]         — State uniqueness, expiry scope, cleanup of stale rows&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb]         — Endpoint tests covering:&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
* '''TODO''' Returns empty array when no providers configured&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* '''TODO''' Returns authorization URL with expected query parameters (client_id, redirect_uri, scope, state, nonce, code_challenge)&lt;br /&gt;
* '''TODO''' Creates an auth_requests row&lt;br /&gt;
* '''TODO''' Returns 404 for unknown provider&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Happy Path'''&lt;br /&gt;
* '''TODO''' Exchanges valid code and state for a session JWT with same payload structure as password login&lt;br /&gt;
* '''TODO''' Deletes the auth_requests row after successful use&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — CSRF Protection (State Validation)'''&lt;br /&gt;
* '''TODO''' Rejects unknown state (422)&lt;br /&gt;
* '''TODO''' Rejects expired state older than 5 minutes (422)&lt;br /&gt;
* '''TODO''' Rejects already-consumed state / replay (422)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Replay Protection (Nonce Validation)'''&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched nonce&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Token Verification'''&lt;br /&gt;
* '''TODO''' Rejects ID token with invalid signature&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched issuer&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched audience (client_id)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — User Matching'''&lt;br /&gt;
* '''TODO''' Returns 404 when no local user matches the email&lt;br /&gt;
* '''TODO''' Matches correct user when multiple users exist&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — PKCE'''&lt;br /&gt;
* '''TODO''' Sends code_verifier to the token endpoint during code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (React) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.tsx src/components/OidcLogin/OidcLogin.tsx]     — Provider dropdown component&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.tsx src/pages/OidcCallback/OidcCallback.tsx]    — Callback page handling code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.test.tsx src/components/OidcLogin/OidcLogin.test.tsx]     — Provider dropdown component tests&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx]    — Callback page tests&lt;br /&gt;
&lt;br /&gt;
'''OidcLogin Component'''&lt;br /&gt;
* '''TODO''' Renders dropdown with providers when GET /auth/providers returns a non-empty list&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers fails&lt;br /&gt;
* '''TODO''' Calls POST /auth/client-select with selected provider id on dropdown change&lt;br /&gt;
* '''TODO''' Redirects browser to returned authorization URL on successful client-select&lt;br /&gt;
* '''TODO''' Does not redirect when client-select fails&lt;br /&gt;
* '''TODO''' Existing login form remains visible and functional alongside the dropdown&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* '''TODO''' Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* '''TODO''' Stores session JWT and dispatches auth state on success&lt;br /&gt;
* '''TODO''' Redirects to dashboard on successful login&lt;br /&gt;
* '''TODO''' Displays error alert and redirects to login on backend failure (e.g. no matching account)&lt;br /&gt;
* '''TODO''' Displays error and redirects to login when IdP returns an error parameter (e.g. user denied consent) without calling the backend&lt;br /&gt;
* '''TODO''' Redirects to login when code or state query parameters are missing&lt;br /&gt;
* '''TODO''' Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 GET  /auth/callback       → React OidcCallback component&lt;br /&gt;
&lt;br /&gt;
== Development Stories (Planning) ==&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML at boot, exposing methods to list providers and look up a provider by id.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Raise a clear boot-time error if required values are missing via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add unit tests for config loading, validation, and missing key detection.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — Auth Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, and PKCE values, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, indexed, unique), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; model with a uniqueness validation on state, an &amp;lt;code&amp;gt;expired&amp;lt;/code&amp;gt; scope, and a &amp;lt;code&amp;gt;cleanup_expired&amp;lt;/code&amp;gt; class method.&lt;br /&gt;
* Add a rake task or scheduled job to periodically clean up expired rows.&lt;br /&gt;
* Add unit tests for creation, lookup, expiry, and deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format and a case with multiple providers.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept a &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; param and look up the provider config.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier/challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Add request specs covering the happy path, unknown provider, and that the auth_requests row is created.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params.&lt;br /&gt;
* Look up and delete the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row. Reject if not found or expired.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, client_id, and nonce.&lt;br /&gt;
* Extract the email claim and match to an existing local user.&lt;br /&gt;
* On match: issue a session JWT using the same payload structure as &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and return it with the user profile.&lt;br /&gt;
* On no match: return a 404 error indicating no local account was found.&lt;br /&gt;
* Handle &amp;lt;code&amp;gt;ActiveRecord::RecordNotFound&amp;lt;/code&amp;gt; (invalid/expired state) and &amp;lt;code&amp;gt;InvalidToken&amp;lt;/code&amp;gt; (verification failure) exceptions.&lt;br /&gt;
* Add request specs covering the happy path, expired state, invalid state, and no matching user.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — Provider Dropdown on Login Page ===&lt;br /&gt;
'''As a''' user, '''I want''' to see a provider dropdown on the login page, '''so that''' I can authenticate with my school credentials.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Render a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown with a disabled &amp;quot;Sign in with...&amp;quot; default option.&lt;br /&gt;
* If the request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' selecting a provider from the dropdown to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On selection change, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the redirect and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC backend, '''so that''' I have confidence the endpoints and models work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add request specs for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Mock the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Cover error scenarios: expired state, invalid state, unknown provider, no matching user, invalid ID token.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; covering creation, uniqueness, expiry scope, and cleanup.&lt;br /&gt;
* Add unit tests for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering loading, validation, and missing key detection.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; is unaffected.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt;: renders dropdown when providers are returned, renders nothing on empty or failed response, triggers &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; on selection.&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;: posts code and state to backend on mount, redirects to dashboard on success, displays error and redirects to login on failure, handles IdP error parameter without calling the backend.&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Additional Refactoring Stories ===&lt;br /&gt;
&lt;br /&gt;
==== Story 11: Backend — Unified Session Response ====&lt;br /&gt;
'''As a''' developer, '''I want''' the session object returned to the frontend clearly defined and reusable by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic from &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (e.g. &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Define a consistent response structure (e.g. &amp;lt;code&amp;gt;{ token, user: { id, name, full_name, role, institution_id } }&amp;lt;/code&amp;gt;) and use it in both controllers.&lt;br /&gt;
* Update the existing password login endpoint to use the shared method without changing its external response shape.&lt;br /&gt;
* Add or update request specs for both login endpoints to assert the response structure matches.&lt;br /&gt;
&lt;br /&gt;
==== Story 12: Frontend — Externalize Hardcoded Configuration ====&lt;br /&gt;
'''As a''' developer, '''I want''' all hardcoded values moved to isolated configuration files, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the frontend API base URL to an environment variable or shared config file (e.g. &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;) and replace all hardcoded references.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
==== Story 13: Backend — Swagger Documentation for Provider Endpoints ====&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC provider endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (including success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
==== Story 14: Backend — Cleanup Expired Auth Requests ====&lt;br /&gt;
'''As a''' developer, '''I want''' expired auth request rows cleaned up automatically, '''so that''' the table does not grow unbounded from abandoned login attempts.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a recurring ActiveJob solid_queue &amp;lt;code&amp;gt;auth_requests:cleanup&amp;lt;/code&amp;gt; that calls &amp;lt;code&amp;gt;AuthRequest.cleanup_expired&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The task deletes all rows with &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; older than 5 minutes.&lt;br /&gt;
* Schedule the task to run periodically (e.g. 24 hours).&lt;br /&gt;
* Add a test verifying that only expired rows are deleted.&lt;br /&gt;
&lt;br /&gt;
== Demo ==&lt;br /&gt;
&lt;br /&gt;
todo add screenshots of oidc login at each step&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167776</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167776"/>
		<updated>2026-04-09T01:52:26Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Model Specs */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the fundamental unit of participation: they allow students to reserve topics, collaborate on coursework, and submit assignments for evaluation.&lt;br /&gt;
&lt;br /&gt;
This project focuses on ensuring the correctness of the Team hierarchy and strengthening automated test coverage. The work emphasizes membership validity (enrolment-based participation), preventing duplicate membership across teams, enforcing capacity limits when configured, ensuring MentoredTeam uses duty-based mentor identification, and validating controller authorization and HTTP response behavior.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy is composed of exactly four classes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; — STI superclass providing shared associations and common behaviours&lt;br /&gt;
* &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; — course-scoped teams that persist across a course&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; — assignment-scoped teams created for a single assignment&lt;br /&gt;
* &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; with a designated mentor&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of team exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) persist throughout a course. In some instructional designs (e.g., Team-Based Learning), these teams are reused for multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) exist only for a single assignment. When mentors are used, assignment teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || Shared membership/associations || Parent class for all team types (STI)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Member must be a course participant || Can be converted to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Member must be an assignment participant || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
&lt;br /&gt;
A critical design point (and a core source of bugs in this area) is the separation between a user's '''role''' and a participant's '''duty'''.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level assigned to a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || A function assigned to a participant ''within a specific team'' || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not privileged system accounts; they are normal users assigned the duty of Mentor on one team. Therefore, mentor logic must use participant duty rather than user role.&lt;br /&gt;
&lt;br /&gt;
== Previous Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== 1) MentoredTeam used role-based assumptions ===&lt;br /&gt;
The incorrect approach was to treat “mentor” as a system-level role check. This is incompatible with Expertiza’s domain model, where mentor is a team-level duty assigned to a participant.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' MentoredTeam mentor assignment and mentor discovery use &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; and the participant’s &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 2) Capacity enforcement was inconsistent / bypassable ===&lt;br /&gt;
Membership could be created through multiple paths (domain method, controllers, join-request acceptance). If enforcement only happened in one place, direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; could bypass capacity checks.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity is enforced at both:&lt;br /&gt;
* the domain method level (&amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;)&lt;br /&gt;
* the join model level (&amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; validation backstop)&lt;br /&gt;
&lt;br /&gt;
Join-request acceptance also checks capacity inside a lock for race-safety.&lt;br /&gt;
&lt;br /&gt;
=== 3) TeamsController authorisation was not explicit ===&lt;br /&gt;
The authorisation framework existed (global before_action), but &amp;lt;code&amp;gt;TeamsController&amp;lt;/code&amp;gt; did not implement an explicit access policy. As a result, student actions could succeed where they should be forbidden.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' TeamsController now defines &amp;lt;code&amp;gt;action_allowed?&amp;lt;/code&amp;gt;, and request specs verify correct &amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt; behaviour.&lt;br /&gt;
&lt;br /&gt;
=== 4) CourseTeam capacity was assumed without schema support ===&lt;br /&gt;
The current schema does not provide a &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt; attribute for courses. Attempting to enforce CourseTeam capacity via &amp;lt;code&amp;gt;course.max_team_size&amp;lt;/code&amp;gt; results in runtime errors and breaks previously passing tests.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity enforcement is based on assignment configuration (&amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;). CourseTeams remain uncapped by default unless the schema is extended in the future.&lt;br /&gt;
&lt;br /&gt;
== Implementation Summary  ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Enrollment-based membership ===&lt;br /&gt;
Membership is grounded in valid participation. A user can only be added if the appropriate participant record exists in the correct parent scope.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2) Prevent duplicate membership across teams ===&lt;br /&gt;
The join model prevents a participant from appearing on multiple teams:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A unique index migration reinforces this at the database level.&lt;br /&gt;
&lt;br /&gt;
=== 3) Capacity enforcement (configured via Assignment) ===&lt;br /&gt;
Capacity is enforced via the assignment’s &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;. When at capacity, attempts to add are rejected with an error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def full?&lt;br /&gt;
  max = max_size&lt;br /&gt;
  return false if max.blank?&lt;br /&gt;
  participants.count &amp;gt;= max&lt;br /&gt;
end&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;
# app/models/team.rb&lt;br /&gt;
return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4) Join-model capacity backstop ===&lt;br /&gt;
Even direct join-table creation is blocked when a team is at capacity.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5) Race-safe join request acceptance ===&lt;br /&gt;
Acceptance checks capacity while holding a lock to prevent two concurrent acceptances from overfilling a team.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb (accept)&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    if team.full?&lt;br /&gt;
      # reject accept&lt;br /&gt;
    end&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    # reject if result failed&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6) TeamsController authorisation and HTTP codes ===&lt;br /&gt;
TeamsController now declares an explicit policy:&lt;br /&gt;
&lt;br /&gt;
* Teaching staff (TA and above): allowed&lt;br /&gt;
* Students: cannot list all teams, cannot manage membership, can only view teams they belong to&lt;br /&gt;
&lt;br /&gt;
Request specs assert that restricted actions return &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage ==&lt;br /&gt;
&lt;br /&gt;
The test suite is organised into model specs (domain rules) and request specs (API behaviour + HTTP status codes).&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team membership + validations || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Type/parent validation, membership checks, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || AssignmentTeam capacity and error rejection&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal and mentor lookup by duty&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness, presence, and capacity backstop (no bypass)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove + authorisation status codes&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorisation, create/accept/decline, capacity rejection and rollback behaviour&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Duty update authorisation, list/add/delete flows&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Important Example Tests  ===&lt;br /&gt;
&lt;br /&gt;
==== Capacity and size limits ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/team_capacity_spec.rb&lt;br /&gt;
it 'rejects member when team is full' do&lt;br /&gt;
  2.times { |i| team.add_member(make_participant(&amp;quot;full#{i}&amp;quot;)) }&lt;br /&gt;
  extra  = make_participant('extra')&lt;br /&gt;
&lt;br /&gt;
  result = team.add_member(extra)&lt;br /&gt;
&lt;br /&gt;
  expect(result[:success]).to be false&lt;br /&gt;
  expect(result[:error]).to match(/capacity/i)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies domain-level capacity enforcement returns a structured failure and a meaningful error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/teams_participant_spec.rb&lt;br /&gt;
describe 'capacity validation (team_not_full)' do&lt;br /&gt;
  it 'raises when creating beyond capacity' do&lt;br /&gt;
    assignment.update!(max_team_size: 1)&lt;br /&gt;
    team = AssignmentTeam.create!(name: 'Cap Team2', parent_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
    u1 = make_user('cap2_u1')&lt;br /&gt;
    p1 = AssignmentParticipant.create!(user: u1, parent_id: assignment.id, handle: u1.name)&lt;br /&gt;
    TeamsParticipant.create!(team: team, participant: p1, user: u1)&lt;br /&gt;
&lt;br /&gt;
    u2 = make_user('cap2_u2')&lt;br /&gt;
    p2 = AssignmentParticipant.create!(user: u2, parent_id: assignment.id, handle: u2.name)&lt;br /&gt;
&lt;br /&gt;
    expect {&lt;br /&gt;
      TeamsParticipant.create!(team: team, participant: p2, user: u2)&lt;br /&gt;
    }.to raise_error(ActiveRecord::RecordInvalid)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures capacity cannot be bypassed by direct join-table creation.&lt;br /&gt;
&lt;br /&gt;
==== Join request acceptance rollback correctness ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/join_team_requests_controller_spec.rb&lt;br /&gt;
it 'does not change reply_status or add participant when team is full' do&lt;br /&gt;
  assignment.update!(max_team_size: 1)&lt;br /&gt;
&lt;br /&gt;
  patch &amp;quot;/join_team_requests/#{join_team_request.id}/accept&amp;quot;, headers: team_member_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
  expect(join_team_request.reload.reply_status).to eq('PENDING')&lt;br /&gt;
  expect(team1.participants.reload).not_to include(participant2)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies the accept endpoint rejects at capacity and does not mutate state.&lt;br /&gt;
&lt;br /&gt;
==== TeamsController authorisation / HTTP status codes ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/teams_controller_spec.rb&lt;br /&gt;
it 'returns 403 for student on GET /teams' do&lt;br /&gt;
  student_token   = JsonWebToken.encode(id: other_user.id)&lt;br /&gt;
  student_headers = { Authorization: &amp;quot;Bearer #{student_token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  get '/teams', headers: student_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:forbidden)&lt;br /&gt;
  expect(JSON.parse(response.body)).to have_key('error')&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures a student cannot list teams and receives &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
==== MentoredTeam duty-based mentor behaviour ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/mentored_team_spec.rb&lt;br /&gt;
it 'identifies mentor by duty not by role' do&lt;br /&gt;
  mentor_duty&lt;br /&gt;
  participant = make_participant('duty_mentor')&lt;br /&gt;
  team.add_member(participant)&lt;br /&gt;
  team.assign_mentor(participant.user)&lt;br /&gt;
&lt;br /&gt;
  expect(team.send(:mentor)).to eq(participant.user)&lt;br /&gt;
  expect(participant.reload.duty).to eq(mentor_duty)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: proves mentor identification is duty-based, matching Expertiza’s domain model.&lt;br /&gt;
== Demo Video==&lt;br /&gt;
https://www.youtube.com/watch?v=KZh-Kwhduok&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the Teams hierarchy by ensuring membership is grounded in enrolment, preventing duplicate membership, enforcing assignment team capacity with both domain-level checks and a join-table backstop, making join-request acceptance race-safe with locking, and implementing explicit TeamsController authorisation rules with request specs that validate correct HTTP response codes. MentoredTeam is tested to rely on participant duty (Mentor) rather than user role.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)&lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167775</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167775"/>
		<updated>2026-04-09T01:51:48Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Model Specs */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the fundamental unit of participation: they allow students to reserve topics, collaborate on coursework, and submit assignments for evaluation.&lt;br /&gt;
&lt;br /&gt;
This project focuses on ensuring the correctness of the Team hierarchy and strengthening automated test coverage. The work emphasizes membership validity (enrolment-based participation), preventing duplicate membership across teams, enforcing capacity limits when configured, ensuring MentoredTeam uses duty-based mentor identification, and validating controller authorization and HTTP response behavior.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy is composed of exactly four classes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; — STI superclass providing shared associations and common behaviours&lt;br /&gt;
* &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; — course-scoped teams that persist across a course&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; — assignment-scoped teams created for a single assignment&lt;br /&gt;
* &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; with a designated mentor&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of team exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) persist throughout a course. In some instructional designs (e.g., Team-Based Learning), these teams are reused for multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) exist only for a single assignment. When mentors are used, assignment teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || Shared membership/associations || Parent class for all team types (STI)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Member must be a course participant || Can be converted to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Member must be an assignment participant || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
&lt;br /&gt;
A critical design point (and a core source of bugs in this area) is the separation between a user's '''role''' and a participant's '''duty'''.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level assigned to a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || A function assigned to a participant ''within a specific team'' || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not privileged system accounts; they are normal users assigned the duty of Mentor on one team. Therefore, mentor logic must use participant duty rather than user role.&lt;br /&gt;
&lt;br /&gt;
== Previous Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== 1) MentoredTeam used role-based assumptions ===&lt;br /&gt;
The incorrect approach was to treat “mentor” as a system-level role check. This is incompatible with Expertiza’s domain model, where mentor is a team-level duty assigned to a participant.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' MentoredTeam mentor assignment and mentor discovery use &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; and the participant’s &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 2) Capacity enforcement was inconsistent / bypassable ===&lt;br /&gt;
Membership could be created through multiple paths (domain method, controllers, join-request acceptance). If enforcement only happened in one place, direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; could bypass capacity checks.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity is enforced at both:&lt;br /&gt;
* the domain method level (&amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;)&lt;br /&gt;
* the join model level (&amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; validation backstop)&lt;br /&gt;
&lt;br /&gt;
Join-request acceptance also checks capacity inside a lock for race-safety.&lt;br /&gt;
&lt;br /&gt;
=== 3) TeamsController authorisation was not explicit ===&lt;br /&gt;
The authorisation framework existed (global before_action), but &amp;lt;code&amp;gt;TeamsController&amp;lt;/code&amp;gt; did not implement an explicit access policy. As a result, student actions could succeed where they should be forbidden.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' TeamsController now defines &amp;lt;code&amp;gt;action_allowed?&amp;lt;/code&amp;gt;, and request specs verify correct &amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt; behaviour.&lt;br /&gt;
&lt;br /&gt;
=== 4) CourseTeam capacity was assumed without schema support ===&lt;br /&gt;
The current schema does not provide a &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt; attribute for courses. Attempting to enforce CourseTeam capacity via &amp;lt;code&amp;gt;course.max_team_size&amp;lt;/code&amp;gt; results in runtime errors and breaks previously passing tests.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity enforcement is based on assignment configuration (&amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;). CourseTeams remain uncapped by default unless the schema is extended in the future.&lt;br /&gt;
&lt;br /&gt;
== Implementation Summary  ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Enrollment-based membership ===&lt;br /&gt;
Membership is grounded in valid participation. A user can only be added if the appropriate participant record exists in the correct parent scope.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2) Prevent duplicate membership across teams ===&lt;br /&gt;
The join model prevents a participant from appearing on multiple teams:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A unique index migration reinforces this at the database level.&lt;br /&gt;
&lt;br /&gt;
=== 3) Capacity enforcement (configured via Assignment) ===&lt;br /&gt;
Capacity is enforced via the assignment’s &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;. When at capacity, attempts to add are rejected with an error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def full?&lt;br /&gt;
  max = max_size&lt;br /&gt;
  return false if max.blank?&lt;br /&gt;
  participants.count &amp;gt;= max&lt;br /&gt;
end&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;
# app/models/team.rb&lt;br /&gt;
return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4) Join-model capacity backstop ===&lt;br /&gt;
Even direct join-table creation is blocked when a team is at capacity.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5) Race-safe join request acceptance ===&lt;br /&gt;
Acceptance checks capacity while holding a lock to prevent two concurrent acceptances from overfilling a team.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb (accept)&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    if team.full?&lt;br /&gt;
      # reject accept&lt;br /&gt;
    end&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    # reject if result failed&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6) TeamsController authorisation and HTTP codes ===&lt;br /&gt;
TeamsController now declares an explicit policy:&lt;br /&gt;
&lt;br /&gt;
* Teaching staff (TA and above): allowed&lt;br /&gt;
* Students: cannot list all teams, cannot manage membership, can only view teams they belong to&lt;br /&gt;
&lt;br /&gt;
Request specs assert that restricted actions return &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage ==&lt;br /&gt;
&lt;br /&gt;
The test suite is organised into model specs (domain rules) and request specs (API behaviour + HTTP status codes).&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team membership + validations || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Type/parent validation, membership checks, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || AssignmentTeam capacity and error rejection&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behaviour || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behaviour || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal and mentor lookup by duty&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness, presence, and capacity backstop (no bypass)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove + authorisation status codes&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorisation, create/accept/decline, capacity rejection and rollback behaviour&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Duty update authorisation, list/add/delete flows&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Important Example Tests  ===&lt;br /&gt;
&lt;br /&gt;
==== Capacity and size limits ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/team_capacity_spec.rb&lt;br /&gt;
it 'rejects member when team is full' do&lt;br /&gt;
  2.times { |i| team.add_member(make_participant(&amp;quot;full#{i}&amp;quot;)) }&lt;br /&gt;
  extra  = make_participant('extra')&lt;br /&gt;
&lt;br /&gt;
  result = team.add_member(extra)&lt;br /&gt;
&lt;br /&gt;
  expect(result[:success]).to be false&lt;br /&gt;
  expect(result[:error]).to match(/capacity/i)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies domain-level capacity enforcement returns a structured failure and a meaningful error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/teams_participant_spec.rb&lt;br /&gt;
describe 'capacity validation (team_not_full)' do&lt;br /&gt;
  it 'raises when creating beyond capacity' do&lt;br /&gt;
    assignment.update!(max_team_size: 1)&lt;br /&gt;
    team = AssignmentTeam.create!(name: 'Cap Team2', parent_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
    u1 = make_user('cap2_u1')&lt;br /&gt;
    p1 = AssignmentParticipant.create!(user: u1, parent_id: assignment.id, handle: u1.name)&lt;br /&gt;
    TeamsParticipant.create!(team: team, participant: p1, user: u1)&lt;br /&gt;
&lt;br /&gt;
    u2 = make_user('cap2_u2')&lt;br /&gt;
    p2 = AssignmentParticipant.create!(user: u2, parent_id: assignment.id, handle: u2.name)&lt;br /&gt;
&lt;br /&gt;
    expect {&lt;br /&gt;
      TeamsParticipant.create!(team: team, participant: p2, user: u2)&lt;br /&gt;
    }.to raise_error(ActiveRecord::RecordInvalid)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures capacity cannot be bypassed by direct join-table creation.&lt;br /&gt;
&lt;br /&gt;
==== Join request acceptance rollback correctness ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/join_team_requests_controller_spec.rb&lt;br /&gt;
it 'does not change reply_status or add participant when team is full' do&lt;br /&gt;
  assignment.update!(max_team_size: 1)&lt;br /&gt;
&lt;br /&gt;
  patch &amp;quot;/join_team_requests/#{join_team_request.id}/accept&amp;quot;, headers: team_member_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
  expect(join_team_request.reload.reply_status).to eq('PENDING')&lt;br /&gt;
  expect(team1.participants.reload).not_to include(participant2)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies the accept endpoint rejects at capacity and does not mutate state.&lt;br /&gt;
&lt;br /&gt;
==== TeamsController authorisation / HTTP status codes ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/teams_controller_spec.rb&lt;br /&gt;
it 'returns 403 for student on GET /teams' do&lt;br /&gt;
  student_token   = JsonWebToken.encode(id: other_user.id)&lt;br /&gt;
  student_headers = { Authorization: &amp;quot;Bearer #{student_token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  get '/teams', headers: student_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:forbidden)&lt;br /&gt;
  expect(JSON.parse(response.body)).to have_key('error')&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures a student cannot list teams and receives &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
==== MentoredTeam duty-based mentor behaviour ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/mentored_team_spec.rb&lt;br /&gt;
it 'identifies mentor by duty not by role' do&lt;br /&gt;
  mentor_duty&lt;br /&gt;
  participant = make_participant('duty_mentor')&lt;br /&gt;
  team.add_member(participant)&lt;br /&gt;
  team.assign_mentor(participant.user)&lt;br /&gt;
&lt;br /&gt;
  expect(team.send(:mentor)).to eq(participant.user)&lt;br /&gt;
  expect(participant.reload.duty).to eq(mentor_duty)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: proves mentor identification is duty-based, matching Expertiza’s domain model.&lt;br /&gt;
== Demo Video==&lt;br /&gt;
https://www.youtube.com/watch?v=KZh-Kwhduok&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the Teams hierarchy by ensuring membership is grounded in enrolment, preventing duplicate membership, enforcing assignment team capacity with both domain-level checks and a join-table backstop, making join-request acceptance race-safe with locking, and implementing explicit TeamsController authorisation rules with request specs that validate correct HTTP response codes. MentoredTeam is tested to rely on participant duty (Mentor) rather than user role.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)&lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167774</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167774"/>
		<updated>2026-04-09T01:49:47Z</updated>

		<summary type="html">&lt;p&gt;Admin: Changed several British spellings (flagged by the spellchecker) to American spellings&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the fundamental unit of participation: they allow students to reserve topics, collaborate on coursework, and submit assignments for evaluation.&lt;br /&gt;
&lt;br /&gt;
This project focuses on ensuring the correctness of the Team hierarchy and strengthening automated test coverage. The work emphasizes membership validity (enrolment-based participation), preventing duplicate membership across teams, enforcing capacity limits when configured, ensuring MentoredTeam uses duty-based mentor identification, and validating controller authorization and HTTP response behavior.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy is composed of exactly four classes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; — STI superclass providing shared associations and common behaviours&lt;br /&gt;
* &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; — course-scoped teams that persist across a course&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; — assignment-scoped teams created for a single assignment&lt;br /&gt;
* &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; with a designated mentor&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of team exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) persist throughout a course. In some instructional designs (e.g., Team-Based Learning), these teams are reused for multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) exist only for a single assignment. When mentors are used, assignment teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || Shared membership/associations || Parent class for all team types (STI)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Member must be a course participant || Can be converted to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Member must be an assignment participant || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
&lt;br /&gt;
A critical design point (and a core source of bugs in this area) is the separation between a user's '''role''' and a participant's '''duty'''.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level assigned to a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || A function assigned to a participant ''within a specific team'' || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not privileged system accounts; they are normal users assigned the duty of Mentor on one team. Therefore, mentor logic must use participant duty rather than user role.&lt;br /&gt;
&lt;br /&gt;
== Previous Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== 1) MentoredTeam used role-based assumptions ===&lt;br /&gt;
The incorrect approach was to treat “mentor” as a system-level role check. This is incompatible with Expertiza’s domain model, where mentor is a team-level duty assigned to a participant.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' MentoredTeam mentor assignment and mentor discovery use &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; and the participant’s &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 2) Capacity enforcement was inconsistent / bypassable ===&lt;br /&gt;
Membership could be created through multiple paths (domain method, controllers, join-request acceptance). If enforcement only happened in one place, direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; could bypass capacity checks.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity is enforced at both:&lt;br /&gt;
* the domain method level (&amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;)&lt;br /&gt;
* the join model level (&amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; validation backstop)&lt;br /&gt;
&lt;br /&gt;
Join-request acceptance also checks capacity inside a lock for race-safety.&lt;br /&gt;
&lt;br /&gt;
=== 3) TeamsController authorisation was not explicit ===&lt;br /&gt;
The authorisation framework existed (global before_action), but &amp;lt;code&amp;gt;TeamsController&amp;lt;/code&amp;gt; did not implement an explicit access policy. As a result, student actions could succeed where they should be forbidden.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' TeamsController now defines &amp;lt;code&amp;gt;action_allowed?&amp;lt;/code&amp;gt;, and request specs verify correct &amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt; behaviour.&lt;br /&gt;
&lt;br /&gt;
=== 4) CourseTeam capacity was assumed without schema support ===&lt;br /&gt;
The current schema does not provide a &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt; attribute for courses. Attempting to enforce CourseTeam capacity via &amp;lt;code&amp;gt;course.max_team_size&amp;lt;/code&amp;gt; results in runtime errors and breaks previously passing tests.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity enforcement is based on assignment configuration (&amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;). CourseTeams remain uncapped by default unless the schema is extended in the future.&lt;br /&gt;
&lt;br /&gt;
== Implementation Summary  ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Enrollment-based membership ===&lt;br /&gt;
Membership is grounded in valid participation. A user can only be added if the appropriate participant record exists in the correct parent scope.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2) Prevent duplicate membership across teams ===&lt;br /&gt;
The join model prevents a participant from appearing on multiple teams:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A unique index migration reinforces this at the database level.&lt;br /&gt;
&lt;br /&gt;
=== 3) Capacity enforcement (configured via Assignment) ===&lt;br /&gt;
Capacity is enforced via the assignment’s &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;. When at capacity, attempts to add are rejected with an error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def full?&lt;br /&gt;
  max = max_size&lt;br /&gt;
  return false if max.blank?&lt;br /&gt;
  participants.count &amp;gt;= max&lt;br /&gt;
end&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;
# app/models/team.rb&lt;br /&gt;
return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4) Join-model capacity backstop ===&lt;br /&gt;
Even direct join-table creation is blocked when a team is at capacity.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5) Race-safe join request acceptance ===&lt;br /&gt;
Acceptance checks capacity while holding a lock to prevent two concurrent acceptances from overfilling a team.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb (accept)&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    if team.full?&lt;br /&gt;
      # reject accept&lt;br /&gt;
    end&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    # reject if result failed&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6) TeamsController authorisation and HTTP codes ===&lt;br /&gt;
TeamsController now declares an explicit policy:&lt;br /&gt;
&lt;br /&gt;
* Teaching staff (TA and above): allowed&lt;br /&gt;
* Students: cannot list all teams, cannot manage membership, can only view teams they belong to&lt;br /&gt;
&lt;br /&gt;
Request specs assert that restricted actions return &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage ==&lt;br /&gt;
&lt;br /&gt;
The test suite is organised into model specs (domain rules) and request specs (API behaviour + HTTP status codes).&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team membership + validations || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Type/parent validation, membership checks, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behaviour || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || AssignmentTeam capacity and error rejection&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behaviour || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behaviour || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal and mentor lookup by duty&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness, presence, and capacity backstop (no bypass)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove + authorisation status codes&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorisation, create/accept/decline, capacity rejection and rollback behaviour&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Duty update authorisation, list/add/delete flows&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Important Example Tests  ===&lt;br /&gt;
&lt;br /&gt;
==== Capacity and size limits ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/team_capacity_spec.rb&lt;br /&gt;
it 'rejects member when team is full' do&lt;br /&gt;
  2.times { |i| team.add_member(make_participant(&amp;quot;full#{i}&amp;quot;)) }&lt;br /&gt;
  extra  = make_participant('extra')&lt;br /&gt;
&lt;br /&gt;
  result = team.add_member(extra)&lt;br /&gt;
&lt;br /&gt;
  expect(result[:success]).to be false&lt;br /&gt;
  expect(result[:error]).to match(/capacity/i)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies domain-level capacity enforcement returns a structured failure and a meaningful error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/teams_participant_spec.rb&lt;br /&gt;
describe 'capacity validation (team_not_full)' do&lt;br /&gt;
  it 'raises when creating beyond capacity' do&lt;br /&gt;
    assignment.update!(max_team_size: 1)&lt;br /&gt;
    team = AssignmentTeam.create!(name: 'Cap Team2', parent_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
    u1 = make_user('cap2_u1')&lt;br /&gt;
    p1 = AssignmentParticipant.create!(user: u1, parent_id: assignment.id, handle: u1.name)&lt;br /&gt;
    TeamsParticipant.create!(team: team, participant: p1, user: u1)&lt;br /&gt;
&lt;br /&gt;
    u2 = make_user('cap2_u2')&lt;br /&gt;
    p2 = AssignmentParticipant.create!(user: u2, parent_id: assignment.id, handle: u2.name)&lt;br /&gt;
&lt;br /&gt;
    expect {&lt;br /&gt;
      TeamsParticipant.create!(team: team, participant: p2, user: u2)&lt;br /&gt;
    }.to raise_error(ActiveRecord::RecordInvalid)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures capacity cannot be bypassed by direct join-table creation.&lt;br /&gt;
&lt;br /&gt;
==== Join request acceptance rollback correctness ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/join_team_requests_controller_spec.rb&lt;br /&gt;
it 'does not change reply_status or add participant when team is full' do&lt;br /&gt;
  assignment.update!(max_team_size: 1)&lt;br /&gt;
&lt;br /&gt;
  patch &amp;quot;/join_team_requests/#{join_team_request.id}/accept&amp;quot;, headers: team_member_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
  expect(join_team_request.reload.reply_status).to eq('PENDING')&lt;br /&gt;
  expect(team1.participants.reload).not_to include(participant2)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies the accept endpoint rejects at capacity and does not mutate state.&lt;br /&gt;
&lt;br /&gt;
==== TeamsController authorisation / HTTP status codes ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/teams_controller_spec.rb&lt;br /&gt;
it 'returns 403 for student on GET /teams' do&lt;br /&gt;
  student_token   = JsonWebToken.encode(id: other_user.id)&lt;br /&gt;
  student_headers = { Authorization: &amp;quot;Bearer #{student_token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  get '/teams', headers: student_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:forbidden)&lt;br /&gt;
  expect(JSON.parse(response.body)).to have_key('error')&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures a student cannot list teams and receives &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
==== MentoredTeam duty-based mentor behaviour ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/mentored_team_spec.rb&lt;br /&gt;
it 'identifies mentor by duty not by role' do&lt;br /&gt;
  mentor_duty&lt;br /&gt;
  participant = make_participant('duty_mentor')&lt;br /&gt;
  team.add_member(participant)&lt;br /&gt;
  team.assign_mentor(participant.user)&lt;br /&gt;
&lt;br /&gt;
  expect(team.send(:mentor)).to eq(participant.user)&lt;br /&gt;
  expect(participant.reload.duty).to eq(mentor_duty)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: proves mentor identification is duty-based, matching Expertiza’s domain model.&lt;br /&gt;
== Demo Video==&lt;br /&gt;
https://www.youtube.com/watch?v=KZh-Kwhduok&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the Teams hierarchy by ensuring membership is grounded in enrolment, preventing duplicate membership, enforcing assignment team capacity with both domain-level checks and a join-table backstop, making join-request acceptance race-safe with locking, and implementing explicit TeamsController authorisation rules with request specs that validate correct HTTP response codes. MentoredTeam is tested to rely on participant duty (Mentor) rather than user role.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)&lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Assignments_questionnaires&amp;diff=167680</id>
		<title>Assignments questionnaires</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Assignments_questionnaires&amp;diff=167680"/>
		<updated>2026-04-02T02:02:01Z</updated>

		<summary type="html">&lt;p&gt;Admin: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Assignments Questionnaire Variable Documentation ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description&lt;br /&gt;
|- &lt;br /&gt;
!id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|The unique record id&lt;br /&gt;
|- &lt;br /&gt;
!questionnaire_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|This references a questionnaire that is used in t he assignment&lt;br /&gt;
|- &lt;br /&gt;
!assignment_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|This references the assignment in which the questionnaire is used&lt;br /&gt;
|-&lt;br /&gt;
!notification_limit&lt;br /&gt;
|int&lt;br /&gt;
|If a new review has a score that is more than this # of percentage points away from the current average review score for this artifact, then the instructor will be emailed.&lt;br /&gt;
|-&lt;br /&gt;
!questionnaire_weight&lt;br /&gt;
|int&lt;br /&gt;
|Gives the percentage of the contributor's overall score that is based on this questionnaire.&lt;br /&gt;
|-&lt;br /&gt;
!used_in_round&lt;br /&gt;
|int&lt;br /&gt;
|Tells which round (1, 2, ...) of review this questionnaire is used in.  If null, I suspect that the questionnaire is used in all rounds.&lt;br /&gt;
|-&lt;br /&gt;
!dropdown&lt;br /&gt;
|tinyint(1)&lt;br /&gt;
|I believe that if this is true, instead of using a rubric to evaluate this kind of work (e.g., review of work, teammate review), a simple dropdown with integers from 1 to MAX_SCORE is used instead.&lt;br /&gt;
|-&lt;br /&gt;
!topic_id&lt;br /&gt;
|int&lt;br /&gt;
|If this field is non-null, then this rubric is used only to review this particular topic.&lt;br /&gt;
|-&lt;br /&gt;
!duty_id&lt;br /&gt;
|int&lt;br /&gt;
|If this is non-null, then this rubric is used only to review a team member who takes on a particular role (such as scrum master, tester, or documentor).&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Parents Tables ==&lt;br /&gt;
Tables referred by the Assignment Badges Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:assignments_questionnaires_imported.png]]&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Child Tables ==&lt;br /&gt;
No Tables refer the Assignment Badges Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Response_maps&amp;diff=167562</id>
		<title>Response maps</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Response_maps&amp;diff=167562"/>
		<updated>2026-03-28T00:38:17Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Response maps variable documentation (Reimplementation) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Maps a connection between the [[participants]] as reviewers and [[participants]] or [[teams]] as reviewees&lt;br /&gt;
&lt;br /&gt;
==Response maps variable documentation (Reimplementation)==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The unique record id&lt;br /&gt;
|- &lt;br /&gt;
!reviewed_object_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The object being reviewed in the [[responses|response]]. In the reimplemented system, this will always be an [[assignment]].&lt;br /&gt;
|-&lt;br /&gt;
!reviewer_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The [[participants|participant]] (actually AssignmentParticipant) providing the response&lt;br /&gt;
|-&lt;br /&gt;
!reviewee_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|Dependent on which subclass of ResponseMap this object is.  For a ReviewResponseMap, the [[teams|team]] (AssignmentTeam) receiving the response.  For a TeammateReviewResponseMap, the AssignmentParticipant who is being reviewed.&lt;br /&gt;
|-&lt;br /&gt;
!type&lt;br /&gt;
|varchar(255)&lt;br /&gt;
|Used for subclassing the response map. Available subclasses are ReviewResponseMap, MetareviewResponseMap, FeedbackResponseMap, TeammateReviewResponseMap  &lt;br /&gt;
|-&lt;br /&gt;
!created_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time for when the record was created&lt;br /&gt;
|-&lt;br /&gt;
!updated_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time when the last update was made&lt;br /&gt;
|-&lt;br /&gt;
!for_calibration&lt;br /&gt;
|int(11)&lt;br /&gt;
|Tells whether the record will be used for calibration or not.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Response maps variable documentation (2023 version)==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The unique record id&lt;br /&gt;
|- &lt;br /&gt;
!reviewed_object_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The object being reviewed in the [[responses|response]]. Possible objects include other ResponseMaps or [[assignments]]&lt;br /&gt;
|-&lt;br /&gt;
!reviewer_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The [[participants|participant]] (actually AssignmentParticipant) providing the response&lt;br /&gt;
|-&lt;br /&gt;
!reviewee_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|Dependent on which subclass of ResponseMap this object is.  For a ReviewResponseMap, the [[teams|team]] (AssignmentTeam) receiving the response.  For a TeammateReviewResponseMap, the AssignmentParticipant who is being reviewed.&lt;br /&gt;
|-&lt;br /&gt;
!type&lt;br /&gt;
|varchar(255)&lt;br /&gt;
|Used for subclassing the response map. Available subclasses are ReviewResponseMap, MetareviewResponseMap, FeedbackResponseMap, TeammateReviewResponseMap  &lt;br /&gt;
|-&lt;br /&gt;
!created_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time for when the record was created&lt;br /&gt;
|-&lt;br /&gt;
!updated_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time when the last update was made&lt;br /&gt;
|-&lt;br /&gt;
!calibrate_to&lt;br /&gt;
|tinyint(1)&lt;br /&gt;
|Tells whether the record will be used for calibration or not.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Back to the [[Documentation_on_Database_Tables|database documentation]]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Teams&amp;diff=167560</id>
		<title>Teams</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Teams&amp;diff=167560"/>
		<updated>2026-03-27T00:23:21Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Teams Variable Documentation (Reimplementation) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Teams Variable Documentation (Reimplementation) ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|&lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Name of the team&lt;br /&gt;
|- &lt;br /&gt;
!parent_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|ID of assignment or course that this team is participating in&lt;br /&gt;
|- &lt;br /&gt;
!type   &lt;br /&gt;
|varchar(255)&lt;br /&gt;
  &lt;br /&gt;
|Type of the team, either AssignmentTeam or CourseTeam&lt;br /&gt;
|- &lt;br /&gt;
!directory_num&lt;br /&gt;
|int(11) &lt;br /&gt;
|directory number where files submitted by this team are stored&lt;br /&gt;
|- &lt;br /&gt;
!grade_for_submission&lt;br /&gt;
|int(11) &lt;br /&gt;
|Stores the grade assigned by the instructor, if any&lt;br /&gt;
|- &lt;br /&gt;
!comment_for_submission&lt;br /&gt;
|text&lt;br /&gt;
|Stores feedback from the instructor for the team&lt;br /&gt;
|} &lt;br /&gt;
&lt;br /&gt;
== Teams Variable Documentation (2023 version) ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|&lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Name of the team&lt;br /&gt;
|- &lt;br /&gt;
!parent_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|ID of assignment or course that this team is participating in&lt;br /&gt;
|- &lt;br /&gt;
!type   &lt;br /&gt;
|varchar(255)&lt;br /&gt;
  &lt;br /&gt;
|Type of the team, either AssignmentTeam or CourseTeam&lt;br /&gt;
|- &lt;br /&gt;
!comments_for_advertisement  &lt;br /&gt;
|text&lt;br /&gt;
|text for comments&lt;br /&gt;
|- &lt;br /&gt;
!advertise_for_partner  &lt;br /&gt;
|tinyint(1) &lt;br /&gt;
|Boolean that tells whether the team is advertising for a partner&lt;br /&gt;
|- &lt;br /&gt;
!submitted_hyperlinks  &lt;br /&gt;
|text &lt;br /&gt;
|List of submitted hyperlinks&lt;br /&gt;
|- &lt;br /&gt;
!directory_num&lt;br /&gt;
|int(11) &lt;br /&gt;
|directory number where files submitted by this team are stored&lt;br /&gt;
|- &lt;br /&gt;
!grade_for_submission&lt;br /&gt;
|int(11) &lt;br /&gt;
|Stores the grade assigned by the instructor, if any&lt;br /&gt;
|- &lt;br /&gt;
!comment_for_submission&lt;br /&gt;
|text&lt;br /&gt;
|Stores feedback from the instructor for the team&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram of tables referencing users table  ==&lt;br /&gt;
The following image shows the tables referencing teams table&lt;br /&gt;
[[File:teams_export.png]]&lt;br /&gt;
&lt;br /&gt;
== E/R diagram of tables users table is referencing to ==&lt;br /&gt;
The teams table is not referenced by any other tables&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167531</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167531"/>
		<updated>2026-03-15T20:06:39Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Least‑Reviewed Submission Strategy */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Overview =&lt;br /&gt;
The review‑assignment subsystem ''in the reimplemented Expertiza'' supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
*A reviewer may not be assigned to their own team.&lt;br /&gt;
*A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
*Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167530</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167530"/>
		<updated>2026-03-15T20:06:04Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Overview =&lt;br /&gt;
The review‑assignment subsystem ''in the reimplemented Expertiza'' supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167529</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167529"/>
		<updated>2026-03-15T20:05:26Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Overview =&lt;br /&gt;
The review‑assignment subsystem &lt;br /&gt;
'in the reimplemented Expertiza'&lt;br /&gt;
supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167528</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167528"/>
		<updated>2026-03-15T20:04:10Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Overview =&lt;br /&gt;
The review‑assignment subsystem 'in the reimplemented Expertiza' supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167527</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167527"/>
		<updated>2026-03-15T20:03:33Z</updated>

		<summary type="html">&lt;p&gt;Admin: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Overview =&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167526</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167526"/>
		<updated>2026-03-15T20:03:09Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Common Eligibility Rules */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167525</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167525"/>
		<updated>2026-03-15T20:02:49Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Strategy Classes */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167524</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167524"/>
		<updated>2026-03-15T20:02:25Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Instructor Grading of Reviews */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167523</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167523"/>
		<updated>2026-03-15T20:02:03Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Topic‑Balanced Dynamic Assignment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
Locates the review mapping&lt;br /&gt;
&lt;br /&gt;
Records the instructor’s grade and comment&lt;br /&gt;
&lt;br /&gt;
Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167522</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167522"/>
		<updated>2026-03-15T20:01:43Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Topic‑Balanced Dynamic Assignment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
&lt;br /&gt;
Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
&lt;br /&gt;
Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
Locates the review mapping&lt;br /&gt;
&lt;br /&gt;
Records the instructor’s grade and comment&lt;br /&gt;
&lt;br /&gt;
Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167521</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167521"/>
		<updated>2026-03-15T20:01:16Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* CSV‑Based Assignment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
** is not their own team,&lt;br /&gt;
** has not already been reviewed by them,&lt;br /&gt;
** has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
&lt;br /&gt;
Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
&lt;br /&gt;
Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
Locates the review mapping&lt;br /&gt;
&lt;br /&gt;
Records the instructor’s grade and comment&lt;br /&gt;
&lt;br /&gt;
Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167520</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167520"/>
		<updated>2026-03-15T20:00:31Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Round‑Robin Assignment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
&lt;br /&gt;
The team is located by name within the assignment.&lt;br /&gt;
&lt;br /&gt;
A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
** is not their own team,&lt;br /&gt;
** has not already been reviewed by them,&lt;br /&gt;
** has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
&lt;br /&gt;
Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
&lt;br /&gt;
Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
Locates the review mapping&lt;br /&gt;
&lt;br /&gt;
Records the instructor’s grade and comment&lt;br /&gt;
&lt;br /&gt;
Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167519</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167519"/>
		<updated>2026-03-15T19:59:10Z</updated>

		<summary type="html">&lt;p&gt;Admin: Created page with &amp;quot;Review Assignment System Overview The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.  This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMa...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
Deterministic ordering&lt;br /&gt;
&lt;br /&gt;
Each team receives exactly one reviewer&lt;br /&gt;
&lt;br /&gt;
Reviewers are reused as needed&lt;br /&gt;
&lt;br /&gt;
No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
&lt;br /&gt;
The team is located by name within the assignment.&lt;br /&gt;
&lt;br /&gt;
A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
** is not their own team,&lt;br /&gt;
** has not already been reviewed by them,&lt;br /&gt;
** has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
&lt;br /&gt;
Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
&lt;br /&gt;
Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
Locates the review mapping&lt;br /&gt;
&lt;br /&gt;
Records the instructor’s grade and comment&lt;br /&gt;
&lt;br /&gt;
Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Users&amp;diff=167503</id>
		<title>Users</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Users&amp;diff=167503"/>
		<updated>2026-03-08T19:28:51Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Users Variable Documentation (Reimplementation) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Users Variable Documentation (Reimplementation) ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Name !!Datatype !!Description&lt;br /&gt;
|- &lt;br /&gt;
!id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Primary key of the entry in table, auto increments by default&lt;br /&gt;
|- &lt;br /&gt;
!username&lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|User name of the respective user (formerly name)&lt;br /&gt;
|- &lt;br /&gt;
!crypted_password  &lt;br /&gt;
|varchar(40)  &lt;br /&gt;
|Password of the respective user&lt;br /&gt;
|- &lt;br /&gt;
!role_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Role which this user corresponds to FK into ROLES table.  Examples are super_admin, admin, instructor, etc.&lt;br /&gt;
|- &lt;br /&gt;
!password_salt  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|&lt;br /&gt;
|- &lt;br /&gt;
!name&lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Registered full name of the user, e.g., Lastname, firstname (formerly fullname)&lt;br /&gt;
|- &lt;br /&gt;
!email  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Registered email of the respective user.&lt;br /&gt;
|- &lt;br /&gt;
!parent_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Parent of the respective user; corresponding to the user who created this user.  The value of this field is the primary key in the users table for the parent user, e.g., 2 if the parent is the super-admin (username admin).&lt;br /&gt;
|- &lt;br /&gt;
!private_by_default  &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|If the user is private by default; essentially Boolean. It seems that this is not used in the system, don't know what it was intended to mean.  Could be removed.&lt;br /&gt;
|- &lt;br /&gt;
!email_on_submission  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|If user is to be e-mailed when something they reviewed is resubmitted.&lt;br /&gt;
|- &lt;br /&gt;
!is_new_user  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|Determines whether the user has logged in before; if not, upon login the user should be presented with the new-user agreement.&lt;br /&gt;
|- &lt;br /&gt;
!handle  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|A handle is an alternate name by which the user would want to be known, e.g., on the wiki, if this assignment involves writing on a wiki.  This allows the user to appear anonymous.  In the code, this is sometimes known as the &amp;quot;default handle&amp;quot;, because users can also create assignment-specific handles.&lt;br /&gt;
|- &lt;br /&gt;
!time_zone_pref&lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Timezone that times should be displayed in when the user logs in.&lt;br /&gt;
|- &lt;br /&gt;
!language_pref&lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Language that the user wants to use in interacting with Expertiza.&lt;br /&gt;
|- &lt;br /&gt;
!public_key&lt;br /&gt;
|mediumtext&lt;br /&gt;
|Public key used for data-encryption by user.  This is used and should stay in the table, though whether the use is meaningful is not clear.&lt;br /&gt;
|- &lt;br /&gt;
!copy_of_emails&lt;br /&gt;
|tinyint(1)&lt;br /&gt;
|Whether this user wants to receive copies of e-mails sent for all assignments they are participating in.  This is only implemented for instructors.  Instructors would enable it so they start getting copies of all emails being sent related to their assignments.  It is a way that the instructor can be reassured that emails are in fact being sent.&lt;br /&gt;
|- &lt;br /&gt;
!institution_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|id of the institution that the user is associated with&lt;br /&gt;
|} &lt;br /&gt;
&lt;br /&gt;
== Users Variable Documentation (2023 version) ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Name !!Datatype !!Description&lt;br /&gt;
|- &lt;br /&gt;
!id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Primary key of the entry in table, auto increments by default&lt;br /&gt;
|- &lt;br /&gt;
!name  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|User name of the respective user&lt;br /&gt;
|- &lt;br /&gt;
!crypted_password  &lt;br /&gt;
|varchar(40)  &lt;br /&gt;
|Password of the respective user&lt;br /&gt;
|- &lt;br /&gt;
!role_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Role which this user corresponds to FK into ROLES table.  Examples are super_admin, admin, instructor, etc.&lt;br /&gt;
|- &lt;br /&gt;
!password_salt  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|&lt;br /&gt;
|- &lt;br /&gt;
!fullname  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Registered full name of the user, e.g., Lastname, firstname&lt;br /&gt;
|- &lt;br /&gt;
!email  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Registered email of the respective user.&lt;br /&gt;
|- &lt;br /&gt;
!parent_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Parent of the respective user; corresponding to the user who created this user.  The value of this field is the primary key in the users table for the parent user, e.g., 2 if the parent is the super-admin (username admin).&lt;br /&gt;
|- &lt;br /&gt;
!private_by_default  &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|If the user is private by default; essentially Boolean. It seems that this is not used in the system, don't know what it was intended to mean.  Could be removed.&lt;br /&gt;
|- &lt;br /&gt;
!mru_directory_path  &lt;br /&gt;
|varchar(128)  &lt;br /&gt;
|To allow the user to log back in to the same directory that (s)he logged out from before.&lt;br /&gt;
|- &lt;br /&gt;
!email_on_review  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|Whether the user should be e-mailed when a new review is received for their work.&lt;br /&gt;
|- &lt;br /&gt;
!email_on_submission  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|If user is to be e-mailed when something they reviewed is resubmitted.&lt;br /&gt;
|- &lt;br /&gt;
!email_on_review_of_review  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|Whether the user should be e-mailed when one of their reviews is meta-reviewed by another reviewer.&lt;br /&gt;
|- &lt;br /&gt;
!is_new_user  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|Determines whether the user has logged in before; if not, upon login the user should be presented with the new-user agreement.&lt;br /&gt;
|- &lt;br /&gt;
!master_permssion_granted  &lt;br /&gt;
|tiny_int(4)  &lt;br /&gt;
|If the user has the master user permission is granted.  What?! What does master permission mean?!  Well, doesn't matter, because this field is set but not read in existing code.&lt;br /&gt;
|- &lt;br /&gt;
!handle  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|A handle is an alternate name by which the user would want to be known, e.g., on the wiki, if this assignment involves writing on a wiki.  This allows the user to appear anonymous.  In the code, this is sometimes known as the &amp;quot;default handle&amp;quot;, because users can also create assignment-specific handles.&lt;br /&gt;
|- &lt;br /&gt;
!digital_certificate  &lt;br /&gt;
|mediumtext &lt;br /&gt;
|Digital certificate generated for the user to protect his password.  This is never read and could be removed.&lt;br /&gt;
|- &lt;br /&gt;
!timezonepref&lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Timezone that times should be displayed in when the user logs in.&lt;br /&gt;
|- &lt;br /&gt;
!public_key&lt;br /&gt;
|mediumtext&lt;br /&gt;
|Public key used for data-encryption by user.  This is used and should stay in the table, though whether the use is meaningful is not clear.&lt;br /&gt;
|- &lt;br /&gt;
!copy_of_emails&lt;br /&gt;
|tinyint(1)&lt;br /&gt;
|Whether this user wants to receive copies of e-mails sent for all assignments they are participating in.  This is only implemented for instructors.  Instructors would enable it so they start getting copies of all emails being sent related to their assignments.  It is a way that the instructor can be reassured that emails are in fact being sent.&lt;br /&gt;
|- &lt;br /&gt;
!institution_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|id of the institution that the user is associated with&lt;br /&gt;
|} &lt;br /&gt;
&lt;br /&gt;
== E/R diagram of tables referencing users table  ==&lt;br /&gt;
The following image shows the tables referencing users table&lt;br /&gt;
[[File:Users_export.png]]&lt;br /&gt;
&lt;br /&gt;
== E/R diagram of tables users table is referencing to ==&lt;br /&gt;
The users table is not referenced by any other tables&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires&amp;diff=167502</id>
		<title>Questionnaires</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires&amp;diff=167502"/>
		<updated>2026-03-06T01:06:33Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Questionnaires documentation (Reimplementation) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Questionnaires documentation (Reimplementation)==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description&lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|This object's unique ID value. One per object. &lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(64)  &lt;br /&gt;
|name of the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!instructor_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|id of instructor who created the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!private   &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|whether questionnaire is visible to other instructors&lt;br /&gt;
|- &lt;br /&gt;
!min_item_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the minimum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!max_item_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the maximum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!created_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was created at&lt;br /&gt;
|- &lt;br /&gt;
!updated_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was last updated&lt;br /&gt;
|- &lt;br /&gt;
!default_num_choices   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|default number of scoring increments&lt;br /&gt;
|- &lt;br /&gt;
!type  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Subclassing for the questionnaire. Possible types are ReviewQuestionnaire, MetareviewQuestionnaire, AuthorFeedbackQuestionnaire, BookmarkRatingQuestionnaire, QuizQuestionniare, SurveyQuestionnaire, CourseEvaluationQuestionnaire, TeammateReviewQuestionnaire, GlobalSurveyQuestionnaire&lt;br /&gt;
|-&lt;br /&gt;
!instruction_loc&lt;br /&gt;
|TEXT&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Questionnaires documentation (2023 version)==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description&lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|This object's unique ID value. One per object. &lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(64)  &lt;br /&gt;
|name of the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!instructor_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|id of instructor who created the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!private   &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|whether questionnaire is visible to other instructors&lt;br /&gt;
|- &lt;br /&gt;
!min_question_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the minimum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!max_question_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the maximum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!created_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was created at&lt;br /&gt;
|- &lt;br /&gt;
!updated_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was last updated&lt;br /&gt;
|- &lt;br /&gt;
!default_num_choices   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|default number of scoring increments&lt;br /&gt;
|- &lt;br /&gt;
!type  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Subclassing for the questionnaire. Possible types are ReviewQuestionnaire, MetareviewQuestionnaire, AuthorFeedbackQuestionnaire, QuizQuestionniare, SurveyQuestionnaire, CourseEvaluationQuestionnaire, TeammateReviewQuestionnaire, GlobalSurveyQuestionnaire&lt;br /&gt;
|-&lt;br /&gt;
!display_type  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Character representation of type used in tree display. Possible values are Review, Metareview, AuthorFeedback, Survey, CourseEvaluation, TeammateReview GlobalSurvey&lt;br /&gt;
|-&lt;br /&gt;
!instruction_loc&lt;br /&gt;
|TEXT&lt;br /&gt;
|&lt;br /&gt;
|} &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to [http://wikis.lib.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires&amp;diff=167501</id>
		<title>Questionnaires</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires&amp;diff=167501"/>
		<updated>2026-03-06T01:02:21Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Questionnaires documentation (Reimplementation) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Questionnaires documentation (Reimplementation)==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description&lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|This object's unique ID value. One per object. &lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(64)  &lt;br /&gt;
|name of the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!instructor_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|id of instructor who created the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!private   &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|whether questionnaire is visible to other instructors&lt;br /&gt;
|- &lt;br /&gt;
!min_item_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the minimum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!max_item_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the maximum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!created_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was created at&lt;br /&gt;
|- &lt;br /&gt;
!updated_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was last updated&lt;br /&gt;
|- &lt;br /&gt;
!default_num_choices   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|default number of scoring increments&lt;br /&gt;
|- &lt;br /&gt;
!type  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Subclassing for the questionnaire. Possible types are ReviewQuestionnaire, MetareviewQuestionnaire, AuthorFeedbackQuestionnaire, QuizQuestionniare, SurveyQuestionnaire, CourseEvaluationQuestionnaire, TeammateReviewQuestionnaire, GlobalSurveyQuestionnaire&lt;br /&gt;
|-&lt;br /&gt;
!instruction_loc&lt;br /&gt;
|TEXT&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Questionnaires documentation (2023 version)==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description&lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|This object's unique ID value. One per object. &lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(64)  &lt;br /&gt;
|name of the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!instructor_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|id of instructor who created the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!private   &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|whether questionnaire is visible to other instructors&lt;br /&gt;
|- &lt;br /&gt;
!min_question_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the minimum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!max_question_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the maximum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!created_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was created at&lt;br /&gt;
|- &lt;br /&gt;
!updated_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was last updated&lt;br /&gt;
|- &lt;br /&gt;
!default_num_choices   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|default number of scoring increments&lt;br /&gt;
|- &lt;br /&gt;
!type  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Subclassing for the questionnaire. Possible types are ReviewQuestionnaire, MetareviewQuestionnaire, AuthorFeedbackQuestionnaire, QuizQuestionniare, SurveyQuestionnaire, CourseEvaluationQuestionnaire, TeammateReviewQuestionnaire, GlobalSurveyQuestionnaire&lt;br /&gt;
|-&lt;br /&gt;
!display_type  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Character representation of type used in tree display. Possible values are Review, Metareview, AuthorFeedback, Survey, CourseEvaluation, TeammateReview GlobalSurvey&lt;br /&gt;
|-&lt;br /&gt;
!instruction_loc&lt;br /&gt;
|TEXT&lt;br /&gt;
|&lt;br /&gt;
|} &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to [http://wikis.lib.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Answers&amp;diff=167500</id>
		<title>Answers</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Answers&amp;diff=167500"/>
		<updated>2026-02-09T21:16:46Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Answers Variable Documentation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Answers table gives information regarding the answers associated to each of the responses.&lt;br /&gt;
== Answers Variable Documentation ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|- &lt;br /&gt;
!id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Unique ID for each Answers record.&lt;br /&gt;
|- &lt;br /&gt;
!question_id   &lt;br /&gt;
|int(11) &lt;br /&gt;
|ID of Question.&lt;br /&gt;
|- &lt;br /&gt;
!answer   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Numeric value of the answer.&lt;br /&gt;
|- &lt;br /&gt;
!comments  &lt;br /&gt;
|text  &lt;br /&gt;
|Comment given to the answer.&lt;br /&gt;
|- &lt;br /&gt;
!response_id   &lt;br /&gt;
|int(11) &lt;br /&gt;
|ID of the response associated with this Answer.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Parents Tables ==&lt;br /&gt;
Tables referred by the Answers Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:answers_imported.png]]&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Child Tables ==&lt;br /&gt;
Tables which refer the Answers Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:answers_exported.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2568._Finish_tabbed_view_for_Assignments,_including_Topics_and_Calibration_tabs&amp;diff=167458</id>
		<title>CSC/ECE 517 Fall 2025 - E2568. Finish tabbed view for Assignments, including Topics and Calibration tabs</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2568._Finish_tabbed_view_for_Assignments,_including_Topics_and_Calibration_tabs&amp;diff=167458"/>
		<updated>2025-12-08T21:42:37Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Calibration Tab */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Overview ===&lt;br /&gt;
This project is a continuation of [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2558._Tabbed_view_for_creating_and_editing_assignments E2558]. The Edit Assignment front end will get an additional tab for review calibration based on the backend work of [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2564._Review_calibration E2564]. Additionally, the Topics tab created as part of E2558 will be integrated with the topic table created by [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2557._Specialized_rubrics_for_different_topic_types E2557]. A more comprehensive testing suite for the AssignmentEditor tabbed view will also be developed. The images below are samples provided to the team.&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Expertiza is an open-source assignment management system with support for teams and peer-reviewing. In the context of Expertiza, calibration refers to sample reviews provided for the purpose of determining if a student is a competent reviewer. If the student's reviews align with the samples, the student is assumed to be competent, assisting in grading and review quality. This new frontend will display these instructor-provided sample reviews in the new Calibration tab where they can be viewed and edited. The topic table is used to create, edit, and delete assignment topics, as well as view which students/groups have signed up for which topic once the assignment is live.&lt;br /&gt;
&lt;br /&gt;
=== Requirements ===&lt;br /&gt;
&lt;br /&gt;
==== Calibration Tab ====&lt;br /&gt;
* Tab header should be &amp;quot;Submit reviews for calibration&amp;quot;&lt;br /&gt;
* Table should have three columns titled:&lt;br /&gt;
** Participant name: displays the name in the format of &amp;quot;user-ID (Name)&amp;quot;&lt;br /&gt;
** Action: contains a link to &amp;quot;Begin&amp;quot; that once clicked changes to the links &amp;quot;View&amp;quot; and &amp;quot;Edit&amp;quot;&lt;br /&gt;
** Submitted item(s): provides a view of the submitted calibration materials&lt;br /&gt;
[[File:E2568_Calibration.png|border|500px|Sample image]]&lt;br /&gt;
&lt;br /&gt;
==== Topics Table Integration ====&lt;br /&gt;
* Communicate with the respective team to ensure compatibility&lt;br /&gt;
* Work with them to adjust any requirements or details&lt;br /&gt;
[[File:E2568 Table integration.png|border|500px|Sample image]]&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
=== Front-end work ===&lt;br /&gt;
* We need to add a new Calibration tab to the existing setup&lt;br /&gt;
* These changes will happen in the &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; file along with the rest of the editor tabs.&lt;br /&gt;
* Since there's a table to be added, we will use the previously built table component to avoid having to create a custom design. This also helps ensure the design stays consistent with the rest of the site.&lt;br /&gt;
* We'll need to store certain states on this tab to trigger the visibility of other buttons. These states will be stored in the &amp;lt;code&amp;gt;AssignmentUtils.tsx&amp;lt;/code&amp;gt; file.&lt;br /&gt;
* To integrate the table from project E2557, we'll have to make changes to the existing tab layout and add any dependencies the new table has.&lt;br /&gt;
&lt;br /&gt;
=== Back-end work ===&lt;br /&gt;
* Need to update the database to support the saving of added calibration parameters&lt;br /&gt;
&lt;br /&gt;
=== Design Principles used ===&lt;br /&gt;
# Don't Repeat Yourself: Shared table rendering logic reused via helper components. API utility reused for all fetch calls.&lt;br /&gt;
# Single Responsibility Principle: Each tab component handles only its own view and logic.&lt;br /&gt;
# Open/Closed Principle: Since the tabs are modular, we can add new tabs without modifying existing ones. This makes it much easier to add or change them without messing with the rest of the structure.&lt;br /&gt;
&lt;br /&gt;
=== Testing Plan ===&lt;br /&gt;
* For frontend tests, we will focus on getting the system setup from scratch since the Assignment Editor doesn't have one as of yet. &lt;br /&gt;
* This will involve adding tests top check whether appropriate components are visible when they should be, and if the different states stored for each tab stay consistent across the session.&lt;br /&gt;
* We'll also focus on checking if the data seen in the tables on the tabs is correct and aligns with what we're fetching from the backend&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
=== Front-end ===&lt;br /&gt;
Our front-end implementation can be found in &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentUtil.tsx&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;AssignmentEditor.test.tsx&amp;lt;/code&amp;gt; within &amp;lt;code&amp;gt;src/pages/Assignments&amp;lt;/code&amp;gt;. &amp;lt;code&amp;gt;App.tsx&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;custom.scss&amp;lt;/code&amp;gt; were also updated in &amp;lt;code&amp;gt;src&amp;lt;/code&amp;gt; to implement correct routing for assignment creation and add the correct tab styling respectively.&lt;br /&gt;
==== Calibration Tab ====&lt;br /&gt;
[[File:E2568_Calibration_implementation.png|border|500px]]&lt;br /&gt;
&amp;lt;small&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;Tab eventKey=&amp;quot;calibration&amp;quot; title=&amp;quot;Calibration&amp;quot;&amp;gt;&lt;br /&gt;
	&amp;lt;h3&amp;gt;Submit reviews for calibration&amp;lt;/h3&amp;gt;&lt;br /&gt;
	&amp;lt;div&amp;gt;&lt;br /&gt;
		&amp;lt;div style={{ display: 'ruby', marginTop: '30px' }}&amp;gt;&lt;br /&gt;
			&amp;lt;Table&lt;br /&gt;
				showColumnFilter={false}&lt;br /&gt;
                showGlobalFilter={false}&lt;br /&gt;
                showPagination={false}&lt;br /&gt;
                data={[&lt;br /&gt;
                    ...calibrationSubmissions.map((calibrationSubmission: any) =&amp;gt; ({&lt;br /&gt;
                        id: calibrationSubmission.id,&lt;br /&gt;
                        participant_name: calibrationSubmission.participant_name,&lt;br /&gt;
                        review_status: calibrationSubmission.review_status,&lt;br /&gt;
                        submitted_content: calibrationSubmission.submitted_content,&lt;br /&gt;
                    })),&lt;br /&gt;
                ]}&lt;br /&gt;
                columns={[&lt;br /&gt;
                    {&lt;br /&gt;
                        accessorKey: &amp;quot;participant_name&amp;quot;, header: &amp;quot;Participant name&amp;quot;, enableSorting: false, enableColumnFilter: false&lt;br /&gt;
                    },&lt;br /&gt;
                    {&lt;br /&gt;
                        cell: ({ row }) =&amp;gt; {&lt;br /&gt;
                            if (row.original.review_status === &amp;quot;not_started&amp;quot;) {&lt;br /&gt;
                                return &amp;lt;a href={`/assignments/edit/${assignmentData.id}/calibration/${row.original.id}`}&amp;gt;Begin&amp;lt;/a&amp;gt;;&lt;br /&gt;
                            } else {&lt;br /&gt;
                                return &amp;lt;div style={{ display: 'flex', alignItems: 'center', columnGap: '5px' }}&amp;gt;&lt;br /&gt;
									&amp;lt;a href={`/assignments/edit/${assignmentData.id}/calibration/${row.original.id}`}&amp;gt;View&amp;lt;/a&amp;gt;&lt;br /&gt;
									|&lt;br /&gt;
									&amp;lt;a href={`/assignments/edit/${assignmentData.id}/calibration/${row.original.id}`}&amp;gt;Edit&amp;lt;/a&amp;gt;&lt;br /&gt;
                                &amp;lt;/div&amp;gt;;&lt;br /&gt;
                            }&lt;br /&gt;
                        },&lt;br /&gt;
                        accessorKey: &amp;quot;action&amp;quot;, header: &amp;quot;Action&amp;quot;, enableSorting: false, enableColumnFilter: false&lt;br /&gt;
                    },&lt;br /&gt;
                    {&lt;br /&gt;
                        cell: ({ row }) =&amp;gt; &amp;lt;&amp;gt;&lt;br /&gt;
                            &amp;lt;div&amp;gt;Hyperlinks:&amp;lt;/div&amp;gt;&lt;br /&gt;
                            {&lt;br /&gt;
                                row.original.submitted_content.hyperlinks.map((item: any, index: number) =&amp;gt; {&lt;br /&gt;
									return &amp;lt;a key={index} href={item}&amp;gt;{item}&amp;lt;/a&amp;gt;;&lt;br /&gt;
                                })&lt;br /&gt;
                            }&lt;br /&gt;
                            &amp;lt;div style={{ marginTop: '10px' }}&amp;gt;Files:&amp;lt;/div&amp;gt;&lt;br /&gt;
                            {&lt;br /&gt;
                                row.original.submitted_content.files.map((item: any, index: number) =&amp;gt; {&lt;br /&gt;
									return &amp;lt;a key={index} href={item}&amp;gt;{item}&amp;lt;/a&amp;gt;;&lt;br /&gt;
                                })&lt;br /&gt;
                            }&lt;br /&gt;
                        &amp;lt;/&amp;gt;,&lt;br /&gt;
                        accessorKey: &amp;quot;submitted_content&amp;quot;, header: &amp;quot;Submitted items(s)&amp;quot;, enableSorting: false, enableColumnFilter: false&lt;br /&gt;
                    },&lt;br /&gt;
                ]}&lt;br /&gt;
            /&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/Tab&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Back-end ===&lt;br /&gt;
The back-end additions are in &amp;lt;code&amp;gt;db/migrate&amp;lt;/code&amp;gt; and are called &amp;lt;code&amp;gt;add_frontend_fields_to_assignments.rb&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_remaining_fields_to_assignments.rb&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;add_apply_late_policy_to_assignments.rb&amp;lt;/code&amp;gt;. The full file names include date codes that are not included here for clarity.&lt;br /&gt;
&lt;br /&gt;
The fields taken from the completed form to be inserted into the database are&lt;br /&gt;
&amp;lt;small&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
export interface AssignmentData {&lt;br /&gt;
  id?: number;&lt;br /&gt;
  name: string;&lt;br /&gt;
  directory_path: string;&lt;br /&gt;
  spec_location: string;&lt;br /&gt;
  private: boolean;&lt;br /&gt;
  show_template_review: boolean;&lt;br /&gt;
  require_quiz: boolean;&lt;br /&gt;
  has_badge: boolean;&lt;br /&gt;
  staggered_deadline: boolean;&lt;br /&gt;
  is_calibrated: boolean;&lt;br /&gt;
&lt;br /&gt;
  // Teams / mentors / topics&lt;br /&gt;
  has_teams?: boolean;&lt;br /&gt;
  max_team_size?: number;&lt;br /&gt;
  show_teammate_review?: boolean;&lt;br /&gt;
  is_pair_programming?: boolean;&lt;br /&gt;
  has_mentors?: boolean;&lt;br /&gt;
  has_topics?: boolean;&lt;br /&gt;
&lt;br /&gt;
  // Review strategy / limits&lt;br /&gt;
  review_topic_threshold?: number;&lt;br /&gt;
  maximum_number_of_reviews_per_submission?: number;&lt;br /&gt;
  review_strategy?: string;&lt;br /&gt;
  review_rubric_varies_by_round?: boolean;&lt;br /&gt;
  review_rubric_varies_by_topic?: boolean;&lt;br /&gt;
  review_rubric_varies_by_role?: boolean;&lt;br /&gt;
  has_max_review_limit?: boolean;&lt;br /&gt;
  set_allowed_number_of_reviews_per_reviewer?: number;&lt;br /&gt;
  set_required_number_of_reviews_per_reviewer?: number;&lt;br /&gt;
  is_review_anonymous?: boolean;&lt;br /&gt;
  is_review_done_by_teams?: boolean;&lt;br /&gt;
  allow_self_reviews?: boolean;&lt;br /&gt;
  reviews_visible_to_other_reviewers?: boolean;&lt;br /&gt;
  number_of_review_rounds?: number;&lt;br /&gt;
&lt;br /&gt;
  // Dates / penalties&lt;br /&gt;
  days_between_submissions?: number;&lt;br /&gt;
  late_policy_id?: number;&lt;br /&gt;
  is_penalty_calculated?: boolean;&lt;br /&gt;
  calculate_penalty?: boolean;&lt;br /&gt;
  apply_late_policy?: boolean;&lt;br /&gt;
&lt;br /&gt;
  // Deadline toggles&lt;br /&gt;
  use_signup_deadline?: boolean;&lt;br /&gt;
  use_drop_topic_deadline?: boolean;&lt;br /&gt;
  use_team_formation_deadline?: boolean;&lt;br /&gt;
&lt;br /&gt;
  // Rubric weights / notification limits&lt;br /&gt;
  weights?: number[];&lt;br /&gt;
  notification_limits?: number[];&lt;br /&gt;
  use_date_updater?: boolean[];&lt;br /&gt;
&lt;br /&gt;
  // Per-deadline permissions&lt;br /&gt;
  submission_allowed?: boolean[];&lt;br /&gt;
  review_allowed?: boolean[];&lt;br /&gt;
  teammate_allowed?: boolean[];&lt;br /&gt;
  metareview_allowed?: boolean[];&lt;br /&gt;
  reminder?: number[];&lt;br /&gt;
&lt;br /&gt;
  // Misc flags from the form&lt;br /&gt;
  allow_tag_prompts?: boolean;&lt;br /&gt;
  course_id?: number;&lt;br /&gt;
  has_quizzes?: boolean;&lt;br /&gt;
  calibration_for_training?: boolean;&lt;br /&gt;
  available_to_students?: boolean;&lt;br /&gt;
  allow_topic_suggestion_from_students?: boolean;&lt;br /&gt;
  enable_bidding_for_topics?: boolean;&lt;br /&gt;
  enable_bidding_for_reviews?: boolean;&lt;br /&gt;
  enable_authors_to_review_other_topics?: boolean;&lt;br /&gt;
  allow_reviewer_to_choose_topic_to_review?: boolean;&lt;br /&gt;
  allow_participants_to_create_bookmarks?: boolean;&lt;br /&gt;
  auto_assign_mentors?: boolean;&lt;br /&gt;
  staggered_deadline_assignment?: boolean;&lt;br /&gt;
&lt;br /&gt;
  // These are used only in tables / dynamic fields&lt;br /&gt;
  questionnaire?: any;&lt;br /&gt;
  date_time?: any[];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
Another goal for this project as a continuation was to expand our test suite for the tabbed view from E2558. The test file for &amp;lt;code&amp;gt;AssignmentEditor.tsx&amp;lt;/code&amp;gt; is currently in the same &amp;lt;code&amp;gt;src/pages/Assignments&amp;lt;/code&amp;gt; directory and is named &amp;lt;code&amp;gt;AssignmentEditor.test.tsx&amp;lt;/code&amp;gt;. It uses Jest and React testing libraries and mocks to ensure the AssignmentEditor renders properly and that it displays all of the proper tabs and form fields. The tests also cover tab switching and UI updates when certain checkboxes are selected.&lt;br /&gt;
&lt;br /&gt;
=== Sample Test Code ===&lt;br /&gt;
==== Render Framework ====&lt;br /&gt;
&amp;lt;small&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
const renderComponent = (mode: 'create' | 'update' = 'create') =&amp;gt; {&lt;br /&gt;
    const store = createMockStore();&lt;br /&gt;
    return render(&lt;br /&gt;
        &amp;lt;Provider store={store}&amp;gt;&lt;br /&gt;
            &amp;lt;BrowserRouter&amp;gt;&lt;br /&gt;
                &amp;lt;AssignmentEditor mode={mode} /&amp;gt;&lt;br /&gt;
            &amp;lt;/BrowserRouter&amp;gt;&lt;br /&gt;
        &amp;lt;/Provider&amp;gt;&lt;br /&gt;
    );&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Specific Render Test Example ====&lt;br /&gt;
&amp;lt;small&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
it('renders Review strategy tab with select', () =&amp;gt; {&lt;br /&gt;
        renderComponent();&lt;br /&gt;
        const reviewStrategyTab = screen.getByText('Review strategy');&lt;br /&gt;
        fireEvent.click(reviewStrategyTab);&lt;br /&gt;
        expect(screen.getByTestId('select-review_strategy')).toBeInTheDocument();&lt;br /&gt;
    });&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Mock Example ====&lt;br /&gt;
&amp;lt;small&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;typescript&amp;quot;&amp;gt;&lt;br /&gt;
jest.mock('components/Form/FormSelect', () =&amp;gt; ({&lt;br /&gt;
    __esModule: true,&lt;br /&gt;
    default: ({ name, options }: any) =&amp;gt; (&lt;br /&gt;
        &amp;lt;select data-testid={`select-${name}`}&amp;gt;&lt;br /&gt;
            {options?.map((opt: any) =&amp;gt; (&lt;br /&gt;
                &amp;lt;option key={opt.value} value={opt.value}&amp;gt;&lt;br /&gt;
                    {opt.label}&lt;br /&gt;
                &amp;lt;/option&amp;gt;&lt;br /&gt;
            ))}&lt;br /&gt;
        &amp;lt;/select&amp;gt;&lt;br /&gt;
    ),&lt;br /&gt;
}));&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Team Members ==&lt;br /&gt;
* Keyur Gondhalekar&lt;br /&gt;
* Nishad Tardalkar&lt;br /&gt;
* Evan Shea&lt;br /&gt;
* Mentor: Koushik Gudipelly&lt;br /&gt;
&lt;br /&gt;
== Additional Resources ==&lt;br /&gt;
* [https://www.youtube.com/watch?v=SPI0XYOWGZc Video demo (Youtube)]&lt;br /&gt;
&lt;br /&gt;
=== Current Project Repos and Pull Requests ===&lt;br /&gt;
* [https://github.com/KeyurGondhalekar/reimplementation-front-end Front-end reimplementation]&lt;br /&gt;
* [https://github.com/expertiza/reimplementation-front-end/pull/129/ Front-end pull request]&lt;br /&gt;
* [https://github.com/KeyurGondhalekar/reimplementation-back-end Back-end reimplementation]&lt;br /&gt;
* [https://github.com/KeyurGondhalekar/reimplementation-back-end/ Back-end pull request]&lt;br /&gt;
&lt;br /&gt;
=== Prior Project (E2558) ===&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2558._Tabbed_view_for_creating_and_editing_assignments E2558 Wiki Page]&lt;br /&gt;
* [https://github.com/expertiza/reimplementation-front-end Current front-end implementation repo]&lt;br /&gt;
* [https://github.com/expertiza/reimplementation-back-end Current back-end implementation repo]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2566._Finish_DueDates&amp;diff=167449</id>
		<title>CSC/ECE 517 Fall 2025 - E2566. Finish DueDates</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2566._Finish_DueDates&amp;diff=167449"/>
		<updated>2025-12-06T01:32:38Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Proposed Solution */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
&lt;br /&gt;
This project aims to complete the implementation of the DueDate system in Expertiza. The current implementation consists mostly of class methods and lacks proper definition of different deadline types and permission checking functionality. This redesign will create a more robust, readable, and maintainable due date system.&lt;br /&gt;
&lt;br /&gt;
=== Existing Components ===&lt;br /&gt;
* &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; model with polymorphic parent association (Assignment/SignUpTopic)&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentDueDate&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;TopicDueDate&amp;lt;/code&amp;gt; subclasses&lt;br /&gt;
* Basic CRUD operations (within &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentForm&amp;lt;/code&amp;gt;) and sorting functionality (&amp;lt;code&amp;gt;deadline_sort&amp;lt;/code&amp;gt;)&lt;br /&gt;
* Database schema with &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt; field&lt;br /&gt;
&lt;br /&gt;
=== Current Behavior ===&lt;br /&gt;
&lt;br /&gt;
====Admin's View====&lt;br /&gt;
[[File:Duedates.png|800px]]&lt;br /&gt;
&lt;br /&gt;
When editing an assignment, due dates can be modified under the Due Dates tab. Each row contains the following columns: &amp;lt;code&amp;gt;Date &amp;amp; Time&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Use Date Updater&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Submission Allowed&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Review Allowed&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teammate Review Allowed&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Meta-Review Allowed&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;Reminder&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
[[File:Assignments-CurrentStage.png|800px]]&lt;br /&gt;
&lt;br /&gt;
In the Assignments tab, the current stage and stage deadline of each assignment is displayed.&lt;br /&gt;
&lt;br /&gt;
[[File:Due-date-topic-instructor.png|800px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Due-date-topic-instructor-2.png|800px]]&lt;br /&gt;
&lt;br /&gt;
In the Topics subtab, due dates for each topic are displayed.&lt;br /&gt;
&lt;br /&gt;
====Student's View====&lt;br /&gt;
[[File:Student-assignments.png|800px]]&lt;br /&gt;
&lt;br /&gt;
Students can view due date information under &amp;lt;code&amp;gt;Current State&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;Stage Deadline&amp;lt;/code&amp;gt;. The actions available to students are determined by the current DueDate status.&lt;br /&gt;
&lt;br /&gt;
[[File:Student-duedate-not-over.png|800px]]&lt;br /&gt;
&lt;br /&gt;
During the submission period, students can access the &amp;lt;code&amp;gt;Submit a hyperlink&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;Submit a file&amp;lt;/code&amp;gt; sections.&lt;br /&gt;
&lt;br /&gt;
[[File:Student-duedate-over.png|800px]]&lt;br /&gt;
&lt;br /&gt;
Once the due date has passed, students cannot submit either a hyperlink or a file.&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
&lt;br /&gt;
Currently, there are some glaring issues with how due_dates was created in the first place, including redundancy and lack of modularity.&lt;br /&gt;
&lt;br /&gt;
'''Modeling &amp;amp; Structural Issues'''&lt;br /&gt;
# No dedicated &amp;lt;code&amp;gt;DeadlineType&amp;lt;/code&amp;gt; model to define different kinds of deadlines. The existing &amp;lt;code&amp;gt;DeadlineType&amp;lt;/code&amp;gt; class only serves as an association for &amp;lt;code&amp;gt;AssignmentDueDate&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;TopicDueDate&amp;lt;/code&amp;gt; models, and is never referenced in the current &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; implementation.&lt;br /&gt;
# Duplicate &amp;lt;code&amp;gt;team_formation&amp;lt;/code&amp;gt; entries in &amp;lt;code&amp;gt;deadline_types&amp;lt;/code&amp;gt; table, which may lead to potential bugs.&lt;br /&gt;
# Incomplete deadline type definitions — &amp;lt;code&amp;gt;teammate_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;quiz&amp;lt;/code&amp;gt; need to be added.&lt;br /&gt;
# No clear separation of concerns — data persistence, business logic, and permission logic are mixed in a single model.&lt;br /&gt;
&lt;br /&gt;
'''Permission &amp;amp; Behavior Issues'''&lt;br /&gt;
# Overuse of class methods making the code difficult to maintain.&lt;br /&gt;
# Missing permission checking logic for user actions that combines both role-based and time-based constraints.&lt;br /&gt;
&lt;br /&gt;
=== Tasks Identified ===&lt;br /&gt;
&lt;br /&gt;
'''Modeling &amp;amp; Structural'''&lt;br /&gt;
# Refactor the &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; model to separate persistence, business logic, and permission logic into distinct layers.&lt;br /&gt;
# Implement reusable mixins (&amp;lt;code&amp;gt;DueDatePermissions&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDateActions&amp;lt;/code&amp;gt;) to handle permission evaluation and deadline-related operations.&lt;br /&gt;
# Introduce instance-level methods (e.g., &amp;lt;code&amp;gt;next_due_date&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;copy&amp;lt;/code&amp;gt;) to replace class-level utilities and improve testability.&lt;br /&gt;
&lt;br /&gt;
'''Permission &amp;amp; Behavior'''&lt;br /&gt;
# Integrate role-based and time-based permission checks within &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;Participant&amp;lt;/code&amp;gt; to ensure consistent access control.&lt;br /&gt;
# Redesign the &amp;lt;code&amp;gt;DeadlineType&amp;lt;/code&amp;gt; model to act as the single source of truth for all deadline categories, replacing hard-coded IDs and redundant entries.&lt;br /&gt;
# Add missing deadline types (&amp;lt;code&amp;gt;teammate_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;quiz&amp;lt;/code&amp;gt;) and remove duplicate &amp;lt;code&amp;gt;team_formation&amp;lt;/code&amp;gt; entries for data integrity.&lt;br /&gt;
&lt;br /&gt;
== Proposed Solution ==&lt;br /&gt;
&lt;br /&gt;
The proposed solution restructures the due date system by separating concerns across dedicated models and mixin modules. Deadline definitions (&amp;lt;code&amp;gt;DeadlineType&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DeadlineRight&amp;lt;/code&amp;gt;), core deadline data (&amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;), and behavioral logic (&amp;lt;code&amp;gt;DueDateActions&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDatePermissions&amp;lt;/code&amp;gt;) are isolated so each component has a single clear responsibility. This modular design improves readability, reduces coupling, and makes deadline-related behavior easier to extend and maintain.&lt;br /&gt;
&lt;br /&gt;
=== Architecture Overview ===&lt;br /&gt;
&lt;br /&gt;
[[File:Duedate-diagram.png|800px]]&lt;br /&gt;
&lt;br /&gt;
This diagram visualizes the redesigned architecture with five distinct layers:&lt;br /&gt;
&lt;br /&gt;
* '''Layer 1 (Blue)''': Parent models (Assignment, SignUpTopic) that own due dates&lt;br /&gt;
* '''Layer 2 (Orange)''': DueDateActions module providing action-level queries&lt;br /&gt;
* '''Layer 3 (Red)''': DueDate model managing core deadline data&lt;br /&gt;
* '''Layer 4 (Orange)''': DueDatePermissions module evaluating permissions&lt;br /&gt;
* '''Layer 5 (Green)''': Reference data models (DeadlineType, DeadlineRight, Participant)&lt;br /&gt;
&lt;br /&gt;
The flow works as follows: Parent models include DueDateActions, which queries DueDate objects. Each DueDate includes DueDatePermissions to evaluate whether actions are allowed based on DeadlineRight states and Participant role flags.&lt;br /&gt;
&lt;br /&gt;
=== DeadlineType Model ===&lt;br /&gt;
&lt;br /&gt;
==== Current Problems ====&lt;br /&gt;
&lt;br /&gt;
Although the current codebase includes a &amp;lt;code&amp;gt;DeadlineType&amp;lt;/code&amp;gt; model, it only serves as a passive lookup table. In practice, most parts of the system still rely on hard-coded numeric IDs or manual lookups:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Example of current usage&lt;br /&gt;
deadline_type_id = DeadlineType.find_by(name: 'review').id&lt;br /&gt;
if due_date.deadline_type_id == deadline_type_id&lt;br /&gt;
  # Perform review-related logic&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This leads to several structural issues:&lt;br /&gt;
* Inconsistent references (some use IDs, others strings)&lt;br /&gt;
* Tight coupling between &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;deadline_type_id&amp;lt;/code&amp;gt;&lt;br /&gt;
* Lack of semantic helper methods (e.g., &amp;lt;code&amp;gt;review?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;submission?&amp;lt;/code&amp;gt;)&lt;br /&gt;
* Data duplication and integrity risks (two &amp;lt;code&amp;gt;team_formation&amp;lt;/code&amp;gt; rows)&lt;br /&gt;
&lt;br /&gt;
==== Database Schema ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+ &amp;lt;code&amp;gt;deadline_types&amp;lt;/code&amp;gt; Table Structure&lt;br /&gt;
|- &lt;br /&gt;
! Column !! Type !! Description&lt;br /&gt;
|-&lt;br /&gt;
| id || BIGINT || Primary key, referenced by &amp;lt;code&amp;gt;due_dates.deadline_type_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| name || VARCHAR(255) || Unique symbolic name (e.g., &amp;lt;code&amp;gt;submission&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;review&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| description || TEXT || Human-readable explanation of the deadline category&lt;br /&gt;
|-&lt;br /&gt;
| created_at || TIMESTAMP || Creation timestamp&lt;br /&gt;
|-&lt;br /&gt;
| updated_at || TIMESTAMP || Last update timestamp&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Deadline Type Definitions ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+ Canonical Deadline Type Entries&lt;br /&gt;
|-&lt;br /&gt;
! ID !! Name !! Description&lt;br /&gt;
|-&lt;br /&gt;
| 1 || submission || Student submission deadlines&lt;br /&gt;
|-&lt;br /&gt;
| 2 || review || Peer review deadlines&lt;br /&gt;
|-&lt;br /&gt;
| 3 || teammate_review || Team member evaluation deadlines&lt;br /&gt;
|-&lt;br /&gt;
| 5 || metareview || Meta-review deadlines (backward compatibility)&lt;br /&gt;
|-&lt;br /&gt;
| 6 || drop_topic || Topic drop deadlines&lt;br /&gt;
|-&lt;br /&gt;
| 7 || signup || Topic signup deadlines&lt;br /&gt;
|-&lt;br /&gt;
| 8 || team_formation || Team formation deadlines (duplicate cleaned)&lt;br /&gt;
|-&lt;br /&gt;
| 11 || quiz || Quiz completion deadlines&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== DeadlineRight Model ===&lt;br /&gt;
&lt;br /&gt;
==== Purpose ====&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;DeadlineRight&amp;lt;/code&amp;gt; model represents the permission level for a specific action at a given deadline:&lt;br /&gt;
&lt;br /&gt;
* '''OK''': Action is allowed without penalty&lt;br /&gt;
* '''Late''': Action is allowed but marked as late&lt;br /&gt;
* '''No''': Action is not permitted&lt;br /&gt;
&lt;br /&gt;
These values are referenced by &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; through fields such as &amp;lt;code&amp;gt;submission_allowed_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;review_allowed_id&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;quiz_allowed_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Database Schema ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+ &amp;lt;code&amp;gt;deadline_rights&amp;lt;/code&amp;gt; Table Structure&lt;br /&gt;
|-&lt;br /&gt;
! Column !! Type !! Description&lt;br /&gt;
|-&lt;br /&gt;
| id || BIGINT || Primary key, referenced by &amp;lt;code&amp;gt;due_dates.*_allowed_id&amp;lt;/code&amp;gt; fields&lt;br /&gt;
|-&lt;br /&gt;
| name || VARCHAR(255) || Unique permission name (&amp;lt;code&amp;gt;No&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Late&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| created_at || TIMESTAMP || Creation timestamp&lt;br /&gt;
|-&lt;br /&gt;
| updated_at || TIMESTAMP || Last update timestamp&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== DueDate Model Refactoring ===&lt;br /&gt;
&lt;br /&gt;
==== Current Problems ====&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; model currently mixes persistence logic, deadline calculation, and permission checking, violating the Single Responsibility Principle. Most of its logic is implemented as class methods, which makes testing and extension difficult.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Old design: class method that mixes concerns&lt;br /&gt;
def self.copy(old_assignment_id, new_assignment_id)&lt;br /&gt;
  duedates = where(parent_id: old_assignment_id)&lt;br /&gt;
  duedates.each do |orig_due_date|&lt;br /&gt;
    new_due_date = orig_due_date.dup&lt;br /&gt;
    new_due_date.parent_id = new_assignment_id&lt;br /&gt;
    new_due_date.save&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Improved Design ====&lt;br /&gt;
&lt;br /&gt;
The redesigned &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; model is focused on representing a single deadline entry. Behavior has been extracted into appropriate modules:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+ Responsibility Distribution After Refactoring&lt;br /&gt;
|-&lt;br /&gt;
! Concern !! Where It Lives&lt;br /&gt;
|-&lt;br /&gt;
| Permission checking || &amp;lt;code&amp;gt;DueDatePermissions&amp;lt;/code&amp;gt; module (included in DueDate)&lt;br /&gt;
|-&lt;br /&gt;
| Deadline-related actions || &amp;lt;code&amp;gt;DueDateActions&amp;lt;/code&amp;gt; module (included in parent models)&lt;br /&gt;
|-&lt;br /&gt;
| Deadline type semantics || &amp;lt;code&amp;gt;DeadlineType&amp;lt;/code&amp;gt; model&lt;br /&gt;
|-&lt;br /&gt;
| Core deadline data || &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt; model&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Key improvements:&lt;br /&gt;
* '''Instance-based behavior''': Replaced class methods with instance methods&lt;br /&gt;
* '''Clear query methods''': Simple predicates like &amp;lt;code&amp;gt;upcoming?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;overdue?&amp;lt;/code&amp;gt;&lt;br /&gt;
* '''Scopes for common queries''': &amp;lt;code&amp;gt;.upcoming&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;.overdue&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;.for_round&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;.for_deadline_type&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== DueDatePermissions Module ===&lt;br /&gt;
&lt;br /&gt;
==== Current Problems ====&lt;br /&gt;
&lt;br /&gt;
Currently, permission checks are split into two disconnected layers:&lt;br /&gt;
&lt;br /&gt;
'''Role-based permissions''' (in participant_helpers.rb):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def participant_permissions(authorization)&lt;br /&gt;
  case authorization&lt;br /&gt;
  when 'reader' then { can_submit: false, can_review: true, can_take_quiz: true }&lt;br /&gt;
  # ...&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Deadline-based checks''' (in Assignment model):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def submission_allowed(topic_id = nil)&lt;br /&gt;
  check_condition('submission_allowed_id', topic_id)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two layers never interact, leading to inconsistencies.&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution ====&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;DueDatePermissions&amp;lt;/code&amp;gt; module integrates both layers:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+ Permission Methods&lt;br /&gt;
|-&lt;br /&gt;
! Method !! What It Checks&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;can_submit?(participant)&amp;lt;/code&amp;gt; || Submission deadline is OK/Late AND participant can submit&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;can_review?(participant)&amp;lt;/code&amp;gt; || Review deadline is OK/Late AND participant can review&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;can_take_quiz?(participant)&amp;lt;/code&amp;gt; || Quiz deadline is OK/Late AND participant can take quiz&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;can_review_teammate?(participant)&amp;lt;/code&amp;gt; || Teammate review deadline is OK/Late AND participant can review&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;activity_permissible?(activity)&amp;lt;/code&amp;gt; || Generic checker for any activity&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;permission_status_for(action)&amp;lt;/code&amp;gt; || Returns 'OK', 'Late', or 'No' for display&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== DueDateActions Module ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;DueDateActions&amp;lt;/code&amp;gt; module provides a unified interface for models that own due dates (&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;SignUpTopic&amp;lt;/code&amp;gt;). This centralizes the logic for querying upcoming deadlines and determining whether specific activities are currently allowed.&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+ Methods in DueDateActions&lt;br /&gt;
|-&lt;br /&gt;
! Method !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;activity_permissible?(activity)&amp;lt;/code&amp;gt; || Checks if an activity is permitted at the current time&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;submission_permissible?&amp;lt;/code&amp;gt; || Convenience wrapper for submission checking&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;review_permissible?&amp;lt;/code&amp;gt; || Convenience wrapper for review checking&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;next_due_date(topic_id)&amp;lt;/code&amp;gt; || Returns the next applicable deadline (which may or may not be specific to the ProjectTopic the team has chosen)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;current_stage&amp;lt;/code&amp;gt; || Returns human-readable stage name (e.g., &amp;quot;review&amp;quot;, &amp;quot;submission&amp;quot;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;has_topic_specific_deadlines?&amp;lt;/code&amp;gt; || Determines if assignment uses topic-specific deadlines&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;copy_due_dates_to(new_parent)&amp;lt;/code&amp;gt; || Utility for cloning due dates to another assignment&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Deadline Resolution Flow ====&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+ How next_due_date Works&lt;br /&gt;
|-&lt;br /&gt;
! Step !! Description&lt;br /&gt;
|-&lt;br /&gt;
| 1. Topic-specific check || If topic ID provided and assignment uses staggered deadlines, check topic-level deadlines first&lt;br /&gt;
|-&lt;br /&gt;
| 2. Assignment-level fallback || If no topic deadline exists, use nearest assignment-level deadline&lt;br /&gt;
|-&lt;br /&gt;
| 3. Permission evaluation || Delegate to DueDatePermissions to determine if action is allowed&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Implementation Summary ==&lt;br /&gt;
&lt;br /&gt;
=== Models Created/Modified ===&lt;br /&gt;
&lt;br /&gt;
* '''DeadlineType''' — Canonical deadline type definitions with semantic helper methods&lt;br /&gt;
* '''DeadlineRight''' — Permission state model (OK/Late/No) with comparison methods&lt;br /&gt;
* '''DueDate''' — Refactored with instance methods, scopes, and permission checking&lt;br /&gt;
* '''TopicDueDate''' — STI subclass for topic-specific deadlines&lt;br /&gt;
* '''Assignment''' — Includes DueDateActions mixin&lt;br /&gt;
* '''SignUpTopic''' — Includes DueDateActions mixin&lt;br /&gt;
&lt;br /&gt;
=== Modules Created ===&lt;br /&gt;
&lt;br /&gt;
* '''DueDatePermissions''' — Permission checking combining deadline and role constraints&lt;br /&gt;
* '''DueDateActions''' — Deadline-related operations for parent models&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
&lt;br /&gt;
# '''Separation of Concerns''': Permission logic separated into dedicated modules&lt;br /&gt;
# '''Instance Methods''': Replaced class methods with testable instance methods&lt;br /&gt;
# '''Polymorphic Design''': Single DueDate model serves both Assignment and SignUpTopic&lt;br /&gt;
# '''Unified Permissions''': Role-based and time-based checks combined in one interface&lt;br /&gt;
# '''Data Integrity''': Removed duplicate deadline types, enforced foreign key constraints&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
This test suite provides coverage for both the Assignment model and the DueDate model.&lt;br /&gt;
The Assignment tests also exercise all behaviors provided by the DueDateActions mixin, since that module is included directly in the Assignment class.&lt;br /&gt;
&lt;br /&gt;
The tests are designed to verify how deadlines influence allowed activities (submission, review, quiz, etc.), how the system determines the next relevant deadline, and how deadlines are ordered.&lt;br /&gt;
&lt;br /&gt;
=== Coverage Summary ===&lt;br /&gt;
&lt;br /&gt;
==== Assignment (DueDateActions mixin included) ====&lt;br /&gt;
The Assignment spec validates:&lt;br /&gt;
&lt;br /&gt;
activity permission logic:&lt;br /&gt;
* &amp;lt;code&amp;gt;activity_permissible?&amp;lt;/code&amp;gt; – evaluates permissions using the next upcoming deadline&lt;br /&gt;
* &amp;lt;code&amp;gt;activity_permissible_for?&amp;lt;/code&amp;gt; – finds the most recent past deadline relative to a specific time&lt;br /&gt;
&lt;br /&gt;
deadline-based navigation:&lt;br /&gt;
* &amp;lt;code&amp;gt;next_due_date&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;deadlines_properly_ordered?&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
delegation helpers such as:&lt;br /&gt;
* &amp;lt;code&amp;gt;submission_permissible?&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;review_permissible?&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These tests ensure that Assignment correctly interprets the state of its associated due dates.&lt;br /&gt;
&lt;br /&gt;
==== DueDate Model ====&lt;br /&gt;
The DueDate spec validates:&lt;br /&gt;
&lt;br /&gt;
required field validations (parent, due_at, deadline_type_id)&lt;br /&gt;
round field rules (cannot be zero or negative, sets default when nil)&lt;br /&gt;
scopes:&lt;br /&gt;
* &amp;lt;code&amp;gt;.upcoming&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;.overdue&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;.for_round&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;.for_deadline_type&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
instance methods:&lt;br /&gt;
* &amp;lt;code&amp;gt;#overdue?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;#upcoming?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;#set&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;#copy&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;#deadline_type_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;#last_deadline?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;#&amp;lt;=&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
class methods:&lt;br /&gt;
* &amp;lt;code&amp;gt;.sort_due_dates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;.any_future_due_dates?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;.copy&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
callback:&lt;br /&gt;
* &amp;lt;code&amp;gt;before_save :set_default_round&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Rationale for Test Design ===&lt;br /&gt;
&lt;br /&gt;
The test suites for DueDate and Assignment (including DueDateActions) are designed to validate two different layers of behavior:&lt;br /&gt;
* DueDate tests verify record-level behavior: validations, scopes, and individual instance methods.&lt;br /&gt;
* Assignment tests verify behavior that depends on collections of due dates, such as selecting the next deadline or determining whether an activity is permissible.&lt;br /&gt;
&lt;br /&gt;
All tests use real database records (DeadlineType, DeadlineRight, DueDate) to ensure that associations, validations, and permission checks reflect real application behavior.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Test Structure ===&lt;br /&gt;
&lt;br /&gt;
The suite uses nested RSpec contexts:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
describe 'activity_permissible_for?' do&lt;br /&gt;
  context 'when past deadline exists' do&lt;br /&gt;
    it 'returns permission from most recent past deadline'&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  context 'when only future deadlines exist' do&lt;br /&gt;
    it 'returns false'&lt;br /&gt;
  end&lt;br /&gt;
end &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contexts clearly describe the situation being tested, while individual examples describe expected behaviors.&lt;br /&gt;
&lt;br /&gt;
== Demo ==&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
&lt;br /&gt;
[https://youtu.be/sTrVRGSaBlE Click to watch on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Future Work ==&lt;br /&gt;
&lt;br /&gt;
* Add deadline notification system using the new permission framework&lt;br /&gt;
* Implement deadline templates for common assignment patterns&lt;br /&gt;
* Add API endpoints for mobile app integration&lt;br /&gt;
* Create admin dashboard for monitoring deadline compliance across courses&lt;br /&gt;
* Add automated deadline extension workflows based on configurable rules&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2560._Framework_for_Import_and_Export&amp;diff=167448</id>
		<title>CSC/ECE 517 Fall 2025 - E2560. Framework for Import and Export</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2560._Framework_for_Import_and_Export&amp;diff=167448"/>
		<updated>2025-12-06T00:35:52Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Approach */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Important Note ==&lt;br /&gt;
The user db schema is being changed soon. It will be removing the field &amp;lt;code&amp;gt;full_name&amp;lt;/code&amp;gt; and adding the field &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt;. Since our implementation mirrors fields from the db, the following will need to be changed:&lt;br /&gt;
&lt;br /&gt;
* In &amp;lt;code&amp;gt;app/models/user.rb&amp;lt;/code&amp;gt;, change line 5 to match this: &amp;lt;code&amp;gt;mandatory_fields :name, :email, :password, :username&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
The export/import functionality is one of the most valuable tools for instructors when setting up assignments in Expertiza. Instructors often have lists of students, teams, and other data from their learning management systems. The ability to seamlessly export and import this data into Expertiza significantly reduces setup time and effort.&lt;br /&gt;
&lt;br /&gt;
* '''Import Functionality Video''' — https://youtu.be/OYMAJIws0gI&lt;br /&gt;
* '''Export Functionality Video''' — https://youtu.be/vhzYiPaoAK8&lt;br /&gt;
&lt;br /&gt;
== Objective ==&lt;br /&gt;
The previous version of Expertiza offered multiple export and import features—for example, exporting students, teams, rubrics, topics, and more. These features typically retrieve data from the database and save it in a specified format. However, the same logic is often duplicated across different implementations for various data types.&lt;br /&gt;
&lt;br /&gt;
The goal of this project is to design and implement a generic export/import framework that can handle various types of data based on input parameters. It will create a module that accepts database table names and column names to determine what data should be exported or imported. This module should be flexible and reusable across all export/import features in Expertiza.&lt;br /&gt;
&lt;br /&gt;
== Approach ==&lt;br /&gt;
&lt;br /&gt;
The challenge of this project was making sure our functionality was generic. It needed to be able to run for any class that required it, with the necessary fields and other configuration details. To handle this challenge, we decided to create an overall service that would handle all cases of importing and exporting. To facilitate this, we created a mixin for classes that want to be able to import and export. This mixin provides a place to declare mandatory fields for import/export, and provides functions to facilitate it. The service uses the functions to generically handle any case. &lt;br /&gt;
&lt;br /&gt;
In the case of duplicate data being entered into the system (e.g., two users with the same username), we choose to go with a strategy approach. We created actions for handling duplicates that can be added to each class. Users can't choose between these actions in the frontend, and the chosen action is then performed if a duplicate is found. This initially includes skipping duplicates, changing the offending field, or updating the record. We have left it so that more actions can be created if there are other cases.&lt;br /&gt;
&lt;br /&gt;
== Design Document ==&lt;br /&gt;
=== Class Diagram ===&lt;br /&gt;
[[File:CSC517Final_5.png|1200px]]&lt;br /&gt;
&lt;br /&gt;
==== How It Fits Together ====&lt;br /&gt;
&lt;br /&gt;
When importing a file (through &amp;lt;code&amp;gt;import_controller.rb&amp;lt;/code&amp;gt;), the request passes the file, the class being imported to, the fields in order, a bool telling whether to use the headers in the CSV or not, and a selected duplicate action. The import service (&amp;lt;code&amp;gt;Import.rb&amp;lt;/code&amp;gt;) takes these values and passes them to the &amp;lt;code&amp;gt;#try_import_record&amp;lt;/code&amp;gt; function included in the &amp;lt;code&amp;gt;importable_exportable_helper.rb&amp;lt;/code&amp;gt;. This function goes through each row and calls &amp;lt;code&amp;gt;#import_row&amp;lt;/code&amp;gt;. Importing each row takes the values in the current row, looks for any associated &amp;lt;code&amp;gt;ExternalClasses&amp;lt;/code&amp;gt; that the values reference, and creates an object based on the values. It then tries to save this object. If the object is a duplicate, it is marked as such and not saved to the database yet. If it is not a duplicate, it then tries to create any External classes as specified in the CSV file. For example, the User class looks up the &amp;lt;code&amp;gt;ExternalClass Role&amp;lt;/code&amp;gt;, and the Item class creates the &amp;lt;code&amp;gt;ExternalClass QuestionAdvice&amp;lt;/code&amp;gt;. Once all the rows have been created in this way, the duplicate records are passed back out to the service to be dealt with. Each duplicate record has its existing counterpart found in the database, and is then dealt with by the duplicate action that the user selected when importing the file. Once those are dealt with, the result of the import is passed back through the controller to the user as a success.&lt;br /&gt;
&lt;br /&gt;
When exporting a file (through &amp;lt;code&amp;gt;export_controller.rb&amp;lt;/code&amp;gt;), the request passes the order of the headers and the class being imported. These are passed to the export service (&amp;lt;code&amp;gt;Export.rb&amp;lt;/code&amp;gt;). The export service puts all the headers into a CSV, then picks out the values associated with the base class (ie. the fields of the User class), and puts them into an array. It then goes through each of those classes &amp;lt;code&amp;gt;ExternalClasses&amp;lt;/code&amp;gt;, gets the values, and places them in the CSV. After the same has been done to every row, this CSV is passed back to the &amp;lt;code&amp;gt;export_controller.rb&amp;lt;/code&amp;gt;, and given to the user successfully.&lt;br /&gt;
&lt;br /&gt;
=== Architecture ===&lt;br /&gt;
The system is divided into two main layers:&lt;br /&gt;
&lt;br /&gt;
* '''Backend''' — Handles import/export logic, data validation, and duplicate record management.&lt;br /&gt;
* '''Frontend''' — Provides user interfaces for triggering imports and exports, communicating with backend controllers.&lt;br /&gt;
----&lt;br /&gt;
==== Backend Components ====&lt;br /&gt;
&lt;br /&gt;
===== FieldMapping =====&lt;br /&gt;
Represents the relationship between class fields and their corresponding data columns.&lt;br /&gt;
* '''''Attributes:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;class: Class&amp;lt;/code&amp;gt; — The associated class for data mapping.&lt;br /&gt;
** &amp;lt;code&amp;gt;ordered_fields: Array[String]&amp;lt;/code&amp;gt; — Field order for import/export operations.&lt;br /&gt;
&lt;br /&gt;
===== DuplicateAction (Mixin) =====&lt;br /&gt;
Defines actions to take when duplicate records are detected.&lt;br /&gt;
* '''''Attributes'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;name: String&amp;lt;/code&amp;gt; — Name of the duplicate action.&lt;br /&gt;
* '''''Methods:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;on_duplicate_record()&amp;lt;/code&amp;gt; — Handles logic when a duplicate record is found.&lt;br /&gt;
&lt;br /&gt;
===== ImportableExportable (Mixin) =====&lt;br /&gt;
Provides shared functionality for classes that can be imported or exported.&lt;br /&gt;
* '''''Attributes:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;mandatory_fields: Array[String]&amp;lt;/code&amp;gt; — Fields required for processing.&lt;br /&gt;
** &amp;lt;code&amp;gt;optional_fields: Array[String]&amp;lt;/code&amp;gt; — Fields that can optionally be included.&lt;br /&gt;
** &amp;lt;code&amp;gt;available_duplicate_actions: Array[DuplicateAction]&amp;lt;/code&amp;gt; — List of supported duplicate handling strategies.&lt;br /&gt;
* '''''Methods:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;register_detail(class, field: String)&amp;lt;/code&amp;gt;&lt;br /&gt;
** &amp;lt;code&amp;gt;try_insert_record(DuplicateAction)&amp;lt;/code&amp;gt;&lt;br /&gt;
** &amp;lt;code&amp;gt;is_duplicate_record()&amp;lt;/code&amp;gt;&lt;br /&gt;
** &amp;lt;code&amp;gt;get_detail_field(record, detail_class, detail_field: String)&amp;lt;/code&amp;gt;&lt;br /&gt;
** &amp;lt;code&amp;gt;to_hash()&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== ImportExportManager =====&lt;br /&gt;
Acts as the central interface for managing import and export workflows.&lt;br /&gt;
* '''''Methods:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;export(class, mapping: FieldMapping, filename: String)&amp;lt;/code&amp;gt; — Exports class data based on mapping.&lt;br /&gt;
** &amp;lt;code&amp;gt;import(class, mapping: FieldMapping, filename: String, use_header: bool)&amp;lt;/code&amp;gt; — Imports data into the specified class.&lt;br /&gt;
** &amp;lt;code&amp;gt;default_mapping(class)&amp;lt;/code&amp;gt; — Retrieves the default mapping configuration.&lt;br /&gt;
** &amp;lt;code&amp;gt;suggest_filename(class, mode: String)&amp;lt;/code&amp;gt; — Suggests a filename based on class and mode.&lt;br /&gt;
&lt;br /&gt;
===== ImportController =====&lt;br /&gt;
Handles import-related HTTP requests.&lt;br /&gt;
* '''''Endpoints:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt; — Returns import configuration data.&lt;br /&gt;
** &amp;lt;code&amp;gt;import&amp;lt;/code&amp;gt; — Accepts incoming data and triggers import operations.&lt;br /&gt;
* '''''Interactions:'''''&lt;br /&gt;
** Sends mandatory and optional fields, as well as available duplicate actions, to the frontend.&lt;br /&gt;
** Receives chosen ordered fields and duplicate action from the frontend.&lt;br /&gt;
&lt;br /&gt;
===== ExportController =====&lt;br /&gt;
Handles export-related HTTP requests.&lt;br /&gt;
* '''''Endpoints:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt; — Provides available export configurations.&lt;br /&gt;
** &amp;lt;code&amp;gt;export&amp;lt;/code&amp;gt; — Generates export files based on selected parameters.&lt;br /&gt;
* '''''Interactions:'''''&lt;br /&gt;
** Sends mandatory and optional fields to the frontend.&lt;br /&gt;
** Receives chosen ordered fields for export.&lt;br /&gt;
----&lt;br /&gt;
==== Frontend Components ====&lt;br /&gt;
&lt;br /&gt;
===== Import (.tsx) =====&lt;br /&gt;
* '''''Methods:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;on_import&amp;lt;/code&amp;gt; — Initiates the import process.&lt;br /&gt;
* '''''Interactions:'''''&lt;br /&gt;
** Receives mandatory/optional field data and duplicate actions.&lt;br /&gt;
** Sends selected ordered fields and chosen duplicate handling action back to the backend.&lt;br /&gt;
&lt;br /&gt;
===== Export (.tsx) =====&lt;br /&gt;
* '''''Methods:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;on_export&amp;lt;/code&amp;gt; — Initiates the export process.&lt;br /&gt;
* '''''Interactions:'''''&lt;br /&gt;
** Receives mandatory/optional field data from backend.&lt;br /&gt;
** Sends selected ordered fields for export.&lt;br /&gt;
----&lt;br /&gt;
==== Data Flow ====&lt;br /&gt;
&lt;br /&gt;
===== Import Sequence =====&lt;br /&gt;
# Frontend requests import configuration.&lt;br /&gt;
# Backend responds with mandatory/optional fields and duplicate actions.&lt;br /&gt;
# User selects ordered fields and a duplicate action.&lt;br /&gt;
# Frontend sends selections to backend for processing via &amp;lt;code&amp;gt;ImportController.import&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Export Sequence =====&lt;br /&gt;
# Frontend requests export configuration.&lt;br /&gt;
# Backend sends available fields.&lt;br /&gt;
# User selects ordered fields to include.&lt;br /&gt;
# Frontend sends selections to backend via &amp;lt;code&amp;gt;ExportController.export&amp;lt;/code&amp;gt;.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Use Case Diagram ===&lt;br /&gt;
[[File:CSC517FinalUseCases_2.png]]&lt;br /&gt;
&lt;br /&gt;
==== Primary Actor ====&lt;br /&gt;
&lt;br /&gt;
'''Instructor / Administrator'''&lt;br /&gt;
* Initiates both import and export operations for any class (e.g., Users, Teams, Rubrics).&lt;br /&gt;
* Goal is to efficiently transfer data into or out of the system without requiring separate tools for each entity.&lt;br /&gt;
&lt;br /&gt;
==== Use Cases ====&lt;br /&gt;
&lt;br /&gt;
===== Main Use Cases =====&lt;br /&gt;
&lt;br /&gt;
;Import data for any class from a CSV file&lt;br /&gt;
# The actor uploads a CSV file.&lt;br /&gt;
# The system reads available headers or prompts the actor to select them.&lt;br /&gt;
# The actor confirms header order and selects a duplicate-handling option.&lt;br /&gt;
# The system validates data and imports records according to the selected duplicate policy.&lt;br /&gt;
&lt;br /&gt;
;Export data for any class to a CSV file&lt;br /&gt;
# The actor selects which fields to include.&lt;br /&gt;
# The actor orders these fields as desired.&lt;br /&gt;
# The system generates a downloadable CSV file containing the chosen data.&lt;br /&gt;
&lt;br /&gt;
===== Included Use Cases =====&lt;br /&gt;
&lt;br /&gt;
;Choose/Auto Read Headers Included in the File&lt;br /&gt;
* When importing, the system attempts to automatically detect headers from the uploaded CSV.&lt;br /&gt;
* If headers are missing or unclear, the actor manually specifies them.&lt;br /&gt;
&lt;br /&gt;
;Choose the Order of the Headers&lt;br /&gt;
* Applies to both import and export operations.&lt;br /&gt;
* The actor arranges the order of headers to ensure correct mapping between CSV columns and database fields.&lt;br /&gt;
&lt;br /&gt;
;Choose Headers that will be Included in the File&lt;br /&gt;
* Applies primarily to export operations.&lt;br /&gt;
* The actor selects specific headers to include in the output CSV file, ensuring only relevant data is exported.&lt;br /&gt;
&lt;br /&gt;
==== Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Both the '''Import''' and '''Export''' use cases include the sub-use cases for choosing headers and their order.&lt;br /&gt;
* The actor directly initiates the primary use cases and indirectly interacts with the included ones through system prompts.&lt;br /&gt;
&lt;br /&gt;
==== System Responsibilities ====&lt;br /&gt;
&lt;br /&gt;
* Provide the actor with available class fields and supported duplicate actions.&lt;br /&gt;
* Manage CSV validation, field mapping, and record processing.&lt;br /&gt;
* Generate clear feedback on import/export results (e.g., success, skipped, or error entries).&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Design Notes ====&lt;br /&gt;
The design emphasizes modularity and extensibility because the same flow supports multiple data entities using a unified import/export framework. &lt;br /&gt;
* Included use cases ensure flexibility for various file structures and actor preferences.&lt;br /&gt;
* Error handling (e.g., missing headers, duplicate data) is encapsulated within the main import process.&lt;br /&gt;
&lt;br /&gt;
== Usage ==&lt;br /&gt;
&lt;br /&gt;
=== Creating CSV for Importing===&lt;br /&gt;
----&lt;br /&gt;
When creating a CSV that will be imported, there are a few guidelines that should be followed to ensure information is uploaded successfully.&lt;br /&gt;
&lt;br /&gt;
;Guidelines&lt;br /&gt;
# CSV should be made with headers that names align with the fields in the program. The names should be in Capital case (ie Name, User id, Txt, etc). These need to align with the program version of the fields (ie. name, user_id, txt) because they will be converted programmatically.&lt;br /&gt;
# CSV must include all mandatory fields and necessary external fields. Optional fields may be included as well. To find these, check the import page for the item you are trying to import. &lt;br /&gt;
# For Headers that refer to an external class, the class name should be appended to the front of the field. (ex. If a user is trying to reference a Role by its name field, the header should be &amp;quot;Role name&amp;quot;, not &amp;quot;name&amp;quot;). &lt;br /&gt;
# For CSVs that contain Duplicate Headers for External classes (Ex. Items and QuestionAdvice), make sure the external class appears in the CSV grouped together and in sequence.&lt;br /&gt;
&lt;br /&gt;
=== Importing a File ===&lt;br /&gt;
----&lt;br /&gt;
The Import Modal is a reusable modal used to upload CSV files for different models (Items, Teams, Users, Assignments, etc.).&lt;br /&gt;
----&lt;br /&gt;
; Opening the Import Modal&lt;br /&gt;
:* From the relevant page (e.g., Items index), click the Import button. This opens the Import &amp;lt;ModelName&amp;gt; modal.&lt;br /&gt;
:* As soon as the modal opens, the frontend automatically calls:&lt;br /&gt;
:*: GET /import/&amp;lt;modelClass&amp;gt; to fetch import metadata for that model. This metadata is what populates the lists at the top of the modal.&lt;br /&gt;
; Field Requirements&lt;br /&gt;
:* At the top of the modal you’ll see three lists: &lt;br /&gt;
:# Mandatory fields – columns that must be present in your import for it to succeed.&lt;br /&gt;
:# Optional fields – extra columns you may include if you have that data.&lt;br /&gt;
:# External fields – related fields that can be pulled from or linked to other records (e.g., questionnaire_name, question_advice_id, etc.)&lt;br /&gt;
:* These lists are read-only and meant to guide how you structure your CSV.&lt;br /&gt;
[[File:ImportFrontend.png|600px]]&lt;br /&gt;
; Selecting a CSV&lt;br /&gt;
:* Under CSV file:&lt;br /&gt;
:# Click Choose File and select your .csv file.&lt;br /&gt;
:# Decide whether your file’s first row is a header row:&lt;br /&gt;
:#* First row contains headers (switch ON)&lt;br /&gt;
:#** Use this if your first row has column names like txt, weight, seq.&lt;br /&gt;
:#** The backend will match CSV headers to field names automatically.&lt;br /&gt;
:#* First row contains headers (switch OFF)&lt;br /&gt;
:#** Use this if your file has no header row (just data).&lt;br /&gt;
[[File:UploadFile.png|600px]]&lt;br /&gt;
; Manual Column Mapping&lt;br /&gt;
:* If “First row contains headers” is OFF, the modal:&lt;br /&gt;
:** Reads the first one or two lines from your CSV.&lt;br /&gt;
:** For each column, shows:&lt;br /&gt;
:*** A dropdown to choose which field this column represents.&lt;br /&gt;
:*** A “First Row Value” snippet so you can see an example value from that column.&lt;br /&gt;
:* You must:&lt;br /&gt;
:*# Go through each column.&lt;br /&gt;
:*# Use the dropdown to select the corresponding field name (e.g., map column 1 → txt, column 2 → weight, etc.).&lt;br /&gt;
:*# Ensure that all mandatory fields appear in at least one column.&lt;br /&gt;
:** If mandatory fields are missing from your selection, the modal will block the import and show a status message.&lt;br /&gt;
[[File:ColumnMapping.png|600px]]&lt;br /&gt;
; Handling Duplicates&lt;br /&gt;
:* In the Duplicate handling section:&lt;br /&gt;
:** If the backend has configured options, you’ll see one or more radio buttons (e.g., skip, overwrite).&lt;br /&gt;
:** Choose how the system should behave if it encounters duplicate records during the import.&lt;br /&gt;
:* If no duplicate options are available, you’ll see: “No duplicate options.”&lt;br /&gt;
; Running the Import&lt;br /&gt;
:* When everything is set:&lt;br /&gt;
:*# Click &amp;quot;import&amp;quot;&lt;br /&gt;
:*# The frontend sends a POST /import/&amp;lt;modelClass&amp;gt; request with:&lt;br /&gt;
:*#* csv_file – the file you selected.&lt;br /&gt;
:*#* use_headers – true or false depending on the toggle.&lt;br /&gt;
:*#* ordered_fields – only included if header mode is off, listing the fields you selected for each column.&lt;br /&gt;
:* While the request is in progress, the modal shows a loading state.&lt;br /&gt;
:* When the response returns, the Status line at the bottom will show either:&lt;br /&gt;
:** A success message from the backend (e.g., “Import complete” or a custom message)&lt;br /&gt;
:** An error message if something went wrong (e.g., missing mandatory fields, invalid format)&lt;br /&gt;
:* You can then close the modal with cancel or by using the close icon.&lt;br /&gt;
[[File:ImportComplete.png|600px]]&lt;br /&gt;
&lt;br /&gt;
=== Exporting a File ===&lt;br /&gt;
----&lt;br /&gt;
; Opening the Export Modal&lt;br /&gt;
:* Click the Export button on the page for the model you want to export (e.g., Teams).&lt;br /&gt;
:* When the modal opens, it automatically makes a request to:&lt;br /&gt;
:*: `GET /export/&amp;lt;modelClass&amp;gt;`&lt;br /&gt;
:* to fetch the field metadata for the selected model.&lt;br /&gt;
; Field Lists Displayed&lt;br /&gt;
:* The modal uses the metadata received from the backend to show three types of fields:&lt;br /&gt;
:# Mandatory fields – These must always be included in the export and cannot be unchecked.&lt;br /&gt;
:# Optional fields – These may be included in the export at the user’s discretion.&lt;br /&gt;
:# External fields – Additional related fields made available for export (if applicable).&lt;br /&gt;
:* Users can hover over an info icon to view the full field list in a tooltip.&lt;br /&gt;
; Choosing Columns to Export&lt;br /&gt;
:* The modal provides a checkbox list of all available fields.&lt;br /&gt;
:* Users may:&lt;br /&gt;
:# Check or uncheck optional fields.&lt;br /&gt;
:# View mandatory fields (locked and always selected).&lt;br /&gt;
:# Reorder any fields using ↑ and ↓ arrows, which determines the ordering of columns in the exported CSV.&lt;br /&gt;
; Column Ordering&lt;br /&gt;
:* Fields appear in the export file in the exact order shown in the modal.&lt;br /&gt;
:* The user can:&lt;br /&gt;
:# Move any non-mandatory field up.&lt;br /&gt;
:# Move any non-mandatory field down.&lt;br /&gt;
:* Mandatory fields are always included and their order can also be adjusted if they are part of the list returned by the backend.&lt;br /&gt;
; Generating the CSV&lt;br /&gt;
:* When satisfied with selections:&lt;br /&gt;
:** Click export&lt;br /&gt;
:* The frontend gathers:&lt;br /&gt;
:** The selected fields&lt;br /&gt;
:* It then generates a CSV where:&lt;br /&gt;
:** The first row contains the field names (in the chosen order)&lt;br /&gt;
:** All following rows contain the corresponding values&lt;br /&gt;
:** A downloadable `.csv` file is automatically created and saved to the user’s system.&lt;br /&gt;
; Status Feedback&lt;br /&gt;
:* The modal displays a status message during and after the export operation, for example:&lt;br /&gt;
:# “Generating CSV…”&lt;br /&gt;
:# “Export complete”&lt;br /&gt;
:* If no fields are selected, the modal will prevent exporting and display a warning.&lt;br /&gt;
[[File:ExportFile.png|600px]]&lt;br /&gt;
&lt;br /&gt;
=== Making a class Importable/Exportable ===&lt;br /&gt;
==== Backend ====&lt;br /&gt;
# Add the Import/Export mixin to the top of the class '&amp;lt;code&amp;gt;extend ImportableExportableHelper&amp;lt;/code&amp;gt;'&lt;br /&gt;
# Specify which fields are mandatory for import/export (in snake_case)&lt;br /&gt;
# Specify any external classes that will need to be looked up or created (ie. looking up the Role of a User by id)&lt;br /&gt;
# Specify the available duplicate actions. (By default, the offending fields are transformed.)&lt;br /&gt;
&lt;br /&gt;
[[File:UserEx.png]]&lt;br /&gt;
&lt;br /&gt;
''Example of adding helper to the User model''&lt;br /&gt;
&lt;br /&gt;
'''Note''': If you are adding this helper to a class that has sub classes:&lt;br /&gt;
Case 1: If you want the sub classes have the same fields, external classes, and duplicate actions as the super class:&lt;br /&gt;
* Do the steps listed above in the super class&lt;br /&gt;
* Additionally, do only the first step in the sub class. &lt;br /&gt;
&lt;br /&gt;
Case 2: If you want to specify different fields, external classes, or duplicate actions for sub classes:&lt;br /&gt;
* Do not do any of the steps for the super class&lt;br /&gt;
* Do all steps for each individual sub class&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:ItemEx.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:QuizItemEx.png]]&lt;br /&gt;
&lt;br /&gt;
''Example of Case 1: Adding helper to the Item model, then QuizItem model''&lt;br /&gt;
&lt;br /&gt;
==== Frontend ====&lt;br /&gt;
# Create the showModal variable&lt;br /&gt;
# Create the handleCloseModal method&lt;br /&gt;
# Add the ImportModal or ExportModal to your frontend file. In the tag, add the show variable, onHide method, and the string of the class that is being imported/exported. (ie Model Class User =&amp;gt; &amp;quot;User&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Fixtures ===&lt;br /&gt;
* &amp;lt;code&amp;gt;empty.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A completely empty CSV file. This file is used to ensure that if no content is included, the import service will fail gracefully.&lt;br /&gt;
* &amp;lt;code&amp;gt;empty_with_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file that only contains the headers for the User class. This file is used to ensure a file with no records is handled gracefully.&lt;br /&gt;
* &amp;lt;code&amp;gt;multiple_users_no_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file with no headers for importing records for the User class. It contains multiple User records (John Doe, Jane Doe) to import. &lt;br /&gt;
* &amp;lt;code&amp;gt;multiple_users_with_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file with headers for importing records for the User class. It contains multiple User records (John Doe, Jane Doe) to import. &lt;br /&gt;
* &amp;lt;code&amp;gt;questionnaire_item_with_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file with headers for importing records for the Item class. These Items make up a questionnaire. It contains a single Item that needs to be imported.&lt;br /&gt;
* &amp;lt;code&amp;gt;single_user_no_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file with headers for importing records for the User class. It contains a single User (John Doe) who needs to be imported.&lt;br /&gt;
* &amp;lt;code&amp;gt;single_user_with_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file with headers for importing records for the User class. It contains a single User (John Doe) who needs to be imported.&lt;br /&gt;
* &amp;lt;code&amp;gt;users_duplicate_records.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file containing multiple duplicate records that need to be handled.&lt;br /&gt;
* &amp;lt;code&amp;gt;single_user_email_invalid.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file that contains a user with an invalid email. That is an Internal field that needs to be validated.&lt;br /&gt;
* &amp;lt;code&amp;gt;single_user_role_doe_not_exist.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file that contains a user with a role that does not exist. That is a field from an External Class that needs to be looked up.&lt;br /&gt;
&lt;br /&gt;
=== Unit Testing ===&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
==== ImportableExportableHelper - Importing ====&lt;br /&gt;
----&lt;br /&gt;
;Create tests for each of the different importable classes&lt;br /&gt;
# Show a class with no headers can be imported (User)&lt;br /&gt;
#: This test tries to import the 'single_user_no_headers.csv' file. This file doesn't have headers, so the test passes a list of headers to the import statement. This CSV file calls for the Role Name of the user, which is set to Student. This tests whether the import statement is able to look up a role by its name. It is able to do this because the name is a lookup field for the External Class Role, which is assigned to the User Class.&lt;br /&gt;
# Show a class with headers can be imported (User)&lt;br /&gt;
#: This test tries to import the 'single_user_with_headers.csv' file. This file has headers, so the test passes nil to the header variable of the import statement. This CSV file calls for the Role ID of the user, which is set to 4 (Teaching Assistant). This tests whether the import statement is able to look up a role by its ID. It can do this even though ID is not the set lookup field, because the lookup function uses the primary key if the lookup field isn't in the file.&lt;br /&gt;
# Show a class with multiple records can be imported (User)&lt;br /&gt;
#: This test tries to import the 'multiple_users_with_headers.csv' file. This file contains multiple user records that need to be imported (John Doe and Jane Doe). This tests that multiple records successfully get imported with the right information.&lt;br /&gt;
# Show a class with external create classes can take duplicate headers (Questionnaire w/ multiple Advice)&lt;br /&gt;
#: This test tries to import the 'questionnaire_item_with_headers.csv' file that creates an external class. Not only that, but it can detect if there are multiple instances of this external class and save them individually. &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:TestUserNoHeaders.png]]&lt;br /&gt;
&lt;br /&gt;
''Import Export Helper Tests: Test Case 1 Implementation''&lt;br /&gt;
&lt;br /&gt;
[[File:TestQuizItem.png]]&lt;br /&gt;
&lt;br /&gt;
''Import Export Helper Tests: Test Case 4 Implementation''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Create Tests to test Errors/Edge Cases&lt;br /&gt;
# Import a class with external lookup class that doesn't exist&lt;br /&gt;
#: Tries to import a csv with an invalid external field. The user's role doesn't exist, so looking up this field should fail and ultimately cause the import to fail.&lt;br /&gt;
# Import a class with an invalid field (User with ian nvalid email)&lt;br /&gt;
#: Tries to import a csv with an invalid internal field. The user email is validated inside the User class, so make sure that validation raises the correct error.&lt;br /&gt;
# Import an empty CSV (With Headers)&lt;br /&gt;
#: Tries to import an empty csv with no records inside. This is tested with the &amp;lt;code&amp;gt;use_header&amp;lt;/code&amp;gt; option set to true.&lt;br /&gt;
# Import an empty CSV (Without Headers)&lt;br /&gt;
#: Tries to import an empty csv with nothing inside. This is tested with the &amp;lt;code&amp;gt;use_header&amp;lt;/code&amp;gt; option set to false.&lt;br /&gt;
&lt;br /&gt;
[[File:TextLookupBad.png]]&lt;br /&gt;
&lt;br /&gt;
''Import Export Helper Tests: Error Test case 2 and 3 Implementation''&lt;br /&gt;
&lt;br /&gt;
==== ImportExportService - Importing ====&lt;br /&gt;
----&lt;br /&gt;
;Create tests to make sure the Service works&lt;br /&gt;
# Test Get Import: 200 response&lt;br /&gt;
#: Test that Import#Index recieves the response, and returns the correct values&lt;br /&gt;
# Test Post Import: 200 response&lt;br /&gt;
#: Test that Import#Import recieves the response with data, and can return a response.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:TestImportService.png]]&lt;br /&gt;
&lt;br /&gt;
''Import Service Tests: Test case 1 Implementation''&lt;br /&gt;
&lt;br /&gt;
;Create Tests to test Errors/Edge Cases&lt;br /&gt;
# Test Post Import: 422 response&lt;br /&gt;
# Test Post Import: 500 response&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== ImportExportService -  Exporting ====&lt;br /&gt;
----&lt;br /&gt;
;Create tests for each of the different exportable classes&lt;br /&gt;
* Export a class with the default headers&lt;br /&gt;
* Export a class with only the mandatory headers&lt;br /&gt;
* Export a class with only some optional headers&lt;br /&gt;
* Export a class with a different order of headers&lt;br /&gt;
&lt;br /&gt;
;Create tests to test Errors/Edge Cases&lt;br /&gt;
* Export a class with chosen headers that are empty&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Frontend Testing ===&lt;br /&gt;
----&lt;br /&gt;
==== Import Modal ====&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
* Shows loading state when isLoading is true&lt;br /&gt;
*: Ensures that when `useAPI` reports `isLoading=true`, the modal hides all content and displays a “Loading…” message.&lt;br /&gt;
* Renders field summary and duplicate options from metadata&lt;br /&gt;
*: Confirms that metadata returned from the backend (mandatory, optional, external fields, and duplicate handling actions) are rendered correctly in the modal.&lt;br /&gt;
* Shows status when importing with no file selected&lt;br /&gt;
*: Validates that clicking “import” without choosing a CSV sets an error status message and prevents the import call.&lt;br /&gt;
* Shows column mapping and first-row values when header mode is off&lt;br /&gt;
*: After uploading a CSV and disabling the “First row contains headers” toggle, verifies that the “Column order” UI appears, column dropdowns are rendered, and the first-row preview text is displayed.&lt;br /&gt;
* Calls sendImport when import is valid&lt;br /&gt;
*: Uploads a valid CSV containing mandatory fields and clicks “import.” Checks that the `sendRequest` mock is called, proving that the modal sends a correctly prepared API request.&lt;br /&gt;
* Calls onHide when cancel is clicked&lt;br /&gt;
*: Simulates clicking the “cancel” button and confirms the modal fires the `onHide` callback to close itself.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
* Adding event-based entity registration.&lt;br /&gt;
* Extending support for new duplicate resolution policies.&lt;br /&gt;
* Integration with the engine demo for visualization and debugging.&lt;br /&gt;
* More extensive testing for functionality and edge cases.&lt;br /&gt;
* Add a space in the import modal that shows all the values that will be imported.&lt;br /&gt;
* Add the helper method across the website wherever the import/export functionality is required.&lt;br /&gt;
:* The helper is currently in:&lt;br /&gt;
::* QuizItem &lt;br /&gt;
::* Item&lt;br /&gt;
::* QuestionAdvice&lt;br /&gt;
::* Team&lt;br /&gt;
::* User&lt;br /&gt;
&lt;br /&gt;
=== Other Notes/Recommendations ===&lt;br /&gt;
* For Items, limit the string values that question_type can be using built-in validation rules as well, or else importing will allow just any string.&lt;br /&gt;
* For Questionnaires, break_before can't be false because of the validation rules. Maybe try this `validates :break_before, inclusion: { in: [true, false] }`&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2560._Framework_for_Import_and_Export&amp;diff=167447</id>
		<title>CSC/ECE 517 Fall 2025 - E2560. Framework for Import and Export</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2560._Framework_for_Import_and_Export&amp;diff=167447"/>
		<updated>2025-12-06T00:33:44Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Objective */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Important Note ==&lt;br /&gt;
The user db schema is being changed soon. It will be removing the field &amp;lt;code&amp;gt;full_name&amp;lt;/code&amp;gt; and adding the field &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt;. Since our implementation mirrors fields from the db, the following will need to be changed:&lt;br /&gt;
&lt;br /&gt;
* In &amp;lt;code&amp;gt;app/models/user.rb&amp;lt;/code&amp;gt;, change line 5 to match this: &amp;lt;code&amp;gt;mandatory_fields :name, :email, :password, :username&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
The export/import functionality is one of the most valuable tools for instructors when setting up assignments in Expertiza. Instructors often have lists of students, teams, and other data from their learning management systems. The ability to seamlessly export and import this data into Expertiza significantly reduces setup time and effort.&lt;br /&gt;
&lt;br /&gt;
* '''Import Functionality Video''' — https://youtu.be/OYMAJIws0gI&lt;br /&gt;
* '''Export Functionality Video''' — https://youtu.be/vhzYiPaoAK8&lt;br /&gt;
&lt;br /&gt;
== Objective ==&lt;br /&gt;
The previous version of Expertiza offered multiple export and import features—for example, exporting students, teams, rubrics, topics, and more. These features typically retrieve data from the database and save it in a specified format. However, the same logic is often duplicated across different implementations for various data types.&lt;br /&gt;
&lt;br /&gt;
The goal of this project is to design and implement a generic export/import framework that can handle various types of data based on input parameters. It will create a module that accepts database table names and column names to determine what data should be exported or imported. This module should be flexible and reusable across all export/import features in Expertiza.&lt;br /&gt;
&lt;br /&gt;
== Approach ==&lt;br /&gt;
&lt;br /&gt;
The challenge of this project was making sure our functionality was generic. It needed to be able to run for any class that required it, with the necessary fields and other configuration details. To handle this challenge, we decided to create an overall service that would handle all cases of importing and exporting. To facilitate this, we created a mixin for classes that want to be able to import and export. This mixin provides a place to declare mandatory fields for import/export, and provides functions to facilitate it. The service uses the functions to generically handle any case. In the case of duplicate data being entered into the system, we choose to go with a strategy approach. We created actions for handling duplicates that can be added to each class. Users can't choose between these actions in the frontend, and the chosen action is then performed if a duplicate is found. This initially includes skipping duplicates, changing the offending field, or updating the record. We have left it so that more actions can be created if there are other cases. &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Design Document ==&lt;br /&gt;
=== Class Diagram ===&lt;br /&gt;
[[File:CSC517Final_5.png|1200px]]&lt;br /&gt;
&lt;br /&gt;
==== How It Fits Together ====&lt;br /&gt;
&lt;br /&gt;
When importing a file (through &amp;lt;code&amp;gt;import_controller.rb&amp;lt;/code&amp;gt;), the request passes the file, the class being imported to, the fields in order, a bool telling whether to use the headers in the CSV or not, and a selected duplicate action. The import service (&amp;lt;code&amp;gt;Import.rb&amp;lt;/code&amp;gt;) takes these values and passes them to the &amp;lt;code&amp;gt;#try_import_record&amp;lt;/code&amp;gt; function included in the &amp;lt;code&amp;gt;importable_exportable_helper.rb&amp;lt;/code&amp;gt;. This function goes through each row and calls &amp;lt;code&amp;gt;#import_row&amp;lt;/code&amp;gt;. Importing each row takes the values in the current row, looks for any associated &amp;lt;code&amp;gt;ExternalClasses&amp;lt;/code&amp;gt; that the values reference, and creates an object based on the values. It then tries to save this object. If the object is a duplicate, it is marked as such and not saved to the database yet. If it is not a duplicate, it then tries to create any External classes as specified in the CSV file. For example, the User class looks up the &amp;lt;code&amp;gt;ExternalClass Role&amp;lt;/code&amp;gt;, and the Item class creates the &amp;lt;code&amp;gt;ExternalClass QuestionAdvice&amp;lt;/code&amp;gt;. Once all the rows have been created in this way, the duplicate records are passed back out to the service to be dealt with. Each duplicate record has its existing counterpart found in the database, and is then dealt with by the duplicate action that the user selected when importing the file. Once those are dealt with, the result of the import is passed back through the controller to the user as a success.&lt;br /&gt;
&lt;br /&gt;
When exporting a file (through &amp;lt;code&amp;gt;export_controller.rb&amp;lt;/code&amp;gt;), the request passes the order of the headers and the class being imported. These are passed to the export service (&amp;lt;code&amp;gt;Export.rb&amp;lt;/code&amp;gt;). The export service puts all the headers into a CSV, then picks out the values associated with the base class (ie. the fields of the User class), and puts them into an array. It then goes through each of those classes &amp;lt;code&amp;gt;ExternalClasses&amp;lt;/code&amp;gt;, gets the values, and places them in the CSV. After the same has been done to every row, this CSV is passed back to the &amp;lt;code&amp;gt;export_controller.rb&amp;lt;/code&amp;gt;, and given to the user successfully.&lt;br /&gt;
&lt;br /&gt;
=== Architecture ===&lt;br /&gt;
The system is divided into two main layers:&lt;br /&gt;
&lt;br /&gt;
* '''Backend''' — Handles import/export logic, data validation, and duplicate record management.&lt;br /&gt;
* '''Frontend''' — Provides user interfaces for triggering imports and exports, communicating with backend controllers.&lt;br /&gt;
----&lt;br /&gt;
==== Backend Components ====&lt;br /&gt;
&lt;br /&gt;
===== FieldMapping =====&lt;br /&gt;
Represents the relationship between class fields and their corresponding data columns.&lt;br /&gt;
* '''''Attributes:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;class: Class&amp;lt;/code&amp;gt; — The associated class for data mapping.&lt;br /&gt;
** &amp;lt;code&amp;gt;ordered_fields: Array[String]&amp;lt;/code&amp;gt; — Field order for import/export operations.&lt;br /&gt;
&lt;br /&gt;
===== DuplicateAction (Mixin) =====&lt;br /&gt;
Defines actions to take when duplicate records are detected.&lt;br /&gt;
* '''''Attributes'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;name: String&amp;lt;/code&amp;gt; — Name of the duplicate action.&lt;br /&gt;
* '''''Methods:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;on_duplicate_record()&amp;lt;/code&amp;gt; — Handles logic when a duplicate record is found.&lt;br /&gt;
&lt;br /&gt;
===== ImportableExportable (Mixin) =====&lt;br /&gt;
Provides shared functionality for classes that can be imported or exported.&lt;br /&gt;
* '''''Attributes:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;mandatory_fields: Array[String]&amp;lt;/code&amp;gt; — Fields required for processing.&lt;br /&gt;
** &amp;lt;code&amp;gt;optional_fields: Array[String]&amp;lt;/code&amp;gt; — Fields that can optionally be included.&lt;br /&gt;
** &amp;lt;code&amp;gt;available_duplicate_actions: Array[DuplicateAction]&amp;lt;/code&amp;gt; — List of supported duplicate handling strategies.&lt;br /&gt;
* '''''Methods:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;register_detail(class, field: String)&amp;lt;/code&amp;gt;&lt;br /&gt;
** &amp;lt;code&amp;gt;try_insert_record(DuplicateAction)&amp;lt;/code&amp;gt;&lt;br /&gt;
** &amp;lt;code&amp;gt;is_duplicate_record()&amp;lt;/code&amp;gt;&lt;br /&gt;
** &amp;lt;code&amp;gt;get_detail_field(record, detail_class, detail_field: String)&amp;lt;/code&amp;gt;&lt;br /&gt;
** &amp;lt;code&amp;gt;to_hash()&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== ImportExportManager =====&lt;br /&gt;
Acts as the central interface for managing import and export workflows.&lt;br /&gt;
* '''''Methods:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;export(class, mapping: FieldMapping, filename: String)&amp;lt;/code&amp;gt; — Exports class data based on mapping.&lt;br /&gt;
** &amp;lt;code&amp;gt;import(class, mapping: FieldMapping, filename: String, use_header: bool)&amp;lt;/code&amp;gt; — Imports data into the specified class.&lt;br /&gt;
** &amp;lt;code&amp;gt;default_mapping(class)&amp;lt;/code&amp;gt; — Retrieves the default mapping configuration.&lt;br /&gt;
** &amp;lt;code&amp;gt;suggest_filename(class, mode: String)&amp;lt;/code&amp;gt; — Suggests a filename based on class and mode.&lt;br /&gt;
&lt;br /&gt;
===== ImportController =====&lt;br /&gt;
Handles import-related HTTP requests.&lt;br /&gt;
* '''''Endpoints:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt; — Returns import configuration data.&lt;br /&gt;
** &amp;lt;code&amp;gt;import&amp;lt;/code&amp;gt; — Accepts incoming data and triggers import operations.&lt;br /&gt;
* '''''Interactions:'''''&lt;br /&gt;
** Sends mandatory and optional fields, as well as available duplicate actions, to the frontend.&lt;br /&gt;
** Receives chosen ordered fields and duplicate action from the frontend.&lt;br /&gt;
&lt;br /&gt;
===== ExportController =====&lt;br /&gt;
Handles export-related HTTP requests.&lt;br /&gt;
* '''''Endpoints:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt; — Provides available export configurations.&lt;br /&gt;
** &amp;lt;code&amp;gt;export&amp;lt;/code&amp;gt; — Generates export files based on selected parameters.&lt;br /&gt;
* '''''Interactions:'''''&lt;br /&gt;
** Sends mandatory and optional fields to the frontend.&lt;br /&gt;
** Receives chosen ordered fields for export.&lt;br /&gt;
----&lt;br /&gt;
==== Frontend Components ====&lt;br /&gt;
&lt;br /&gt;
===== Import (.tsx) =====&lt;br /&gt;
* '''''Methods:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;on_import&amp;lt;/code&amp;gt; — Initiates the import process.&lt;br /&gt;
* '''''Interactions:'''''&lt;br /&gt;
** Receives mandatory/optional field data and duplicate actions.&lt;br /&gt;
** Sends selected ordered fields and chosen duplicate handling action back to the backend.&lt;br /&gt;
&lt;br /&gt;
===== Export (.tsx) =====&lt;br /&gt;
* '''''Methods:'''''&lt;br /&gt;
** &amp;lt;code&amp;gt;on_export&amp;lt;/code&amp;gt; — Initiates the export process.&lt;br /&gt;
* '''''Interactions:'''''&lt;br /&gt;
** Receives mandatory/optional field data from backend.&lt;br /&gt;
** Sends selected ordered fields for export.&lt;br /&gt;
----&lt;br /&gt;
==== Data Flow ====&lt;br /&gt;
&lt;br /&gt;
===== Import Sequence =====&lt;br /&gt;
# Frontend requests import configuration.&lt;br /&gt;
# Backend responds with mandatory/optional fields and duplicate actions.&lt;br /&gt;
# User selects ordered fields and a duplicate action.&lt;br /&gt;
# Frontend sends selections to backend for processing via &amp;lt;code&amp;gt;ImportController.import&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Export Sequence =====&lt;br /&gt;
# Frontend requests export configuration.&lt;br /&gt;
# Backend sends available fields.&lt;br /&gt;
# User selects ordered fields to include.&lt;br /&gt;
# Frontend sends selections to backend via &amp;lt;code&amp;gt;ExportController.export&amp;lt;/code&amp;gt;.&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Use Case Diagram ===&lt;br /&gt;
[[File:CSC517FinalUseCases_2.png]]&lt;br /&gt;
&lt;br /&gt;
==== Primary Actor ====&lt;br /&gt;
&lt;br /&gt;
'''Instructor / Administrator'''&lt;br /&gt;
* Initiates both import and export operations for any class (e.g., Users, Teams, Rubrics).&lt;br /&gt;
* Goal is to efficiently transfer data into or out of the system without requiring separate tools for each entity.&lt;br /&gt;
&lt;br /&gt;
==== Use Cases ====&lt;br /&gt;
&lt;br /&gt;
===== Main Use Cases =====&lt;br /&gt;
&lt;br /&gt;
;Import data for any class from a CSV file&lt;br /&gt;
# The actor uploads a CSV file.&lt;br /&gt;
# The system reads available headers or prompts the actor to select them.&lt;br /&gt;
# The actor confirms header order and selects a duplicate-handling option.&lt;br /&gt;
# The system validates data and imports records according to the selected duplicate policy.&lt;br /&gt;
&lt;br /&gt;
;Export data for any class to a CSV file&lt;br /&gt;
# The actor selects which fields to include.&lt;br /&gt;
# The actor orders these fields as desired.&lt;br /&gt;
# The system generates a downloadable CSV file containing the chosen data.&lt;br /&gt;
&lt;br /&gt;
===== Included Use Cases =====&lt;br /&gt;
&lt;br /&gt;
;Choose/Auto Read Headers Included in the File&lt;br /&gt;
* When importing, the system attempts to automatically detect headers from the uploaded CSV.&lt;br /&gt;
* If headers are missing or unclear, the actor manually specifies them.&lt;br /&gt;
&lt;br /&gt;
;Choose the Order of the Headers&lt;br /&gt;
* Applies to both import and export operations.&lt;br /&gt;
* The actor arranges the order of headers to ensure correct mapping between CSV columns and database fields.&lt;br /&gt;
&lt;br /&gt;
;Choose Headers that will be Included in the File&lt;br /&gt;
* Applies primarily to export operations.&lt;br /&gt;
* The actor selects specific headers to include in the output CSV file, ensuring only relevant data is exported.&lt;br /&gt;
&lt;br /&gt;
==== Relationships ====&lt;br /&gt;
&lt;br /&gt;
* Both the '''Import''' and '''Export''' use cases include the sub-use cases for choosing headers and their order.&lt;br /&gt;
* The actor directly initiates the primary use cases and indirectly interacts with the included ones through system prompts.&lt;br /&gt;
&lt;br /&gt;
==== System Responsibilities ====&lt;br /&gt;
&lt;br /&gt;
* Provide the actor with available class fields and supported duplicate actions.&lt;br /&gt;
* Manage CSV validation, field mapping, and record processing.&lt;br /&gt;
* Generate clear feedback on import/export results (e.g., success, skipped, or error entries).&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Design Notes ====&lt;br /&gt;
The design emphasizes modularity and extensibility because the same flow supports multiple data entities using a unified import/export framework. &lt;br /&gt;
* Included use cases ensure flexibility for various file structures and actor preferences.&lt;br /&gt;
* Error handling (e.g., missing headers, duplicate data) is encapsulated within the main import process.&lt;br /&gt;
&lt;br /&gt;
== Usage ==&lt;br /&gt;
&lt;br /&gt;
=== Creating CSV for Importing===&lt;br /&gt;
----&lt;br /&gt;
When creating a CSV that will be imported, there are a few guidelines that should be followed to ensure information is uploaded successfully.&lt;br /&gt;
&lt;br /&gt;
;Guidelines&lt;br /&gt;
# CSV should be made with headers that names align with the fields in the program. The names should be in Capital case (ie Name, User id, Txt, etc). These need to align with the program version of the fields (ie. name, user_id, txt) because they will be converted programmatically.&lt;br /&gt;
# CSV must include all mandatory fields and necessary external fields. Optional fields may be included as well. To find these, check the import page for the item you are trying to import. &lt;br /&gt;
# For Headers that refer to an external class, the class name should be appended to the front of the field. (ex. If a user is trying to reference a Role by its name field, the header should be &amp;quot;Role name&amp;quot;, not &amp;quot;name&amp;quot;). &lt;br /&gt;
# For CSVs that contain Duplicate Headers for External classes (Ex. Items and QuestionAdvice), make sure the external class appears in the CSV grouped together and in sequence.&lt;br /&gt;
&lt;br /&gt;
=== Importing a File ===&lt;br /&gt;
----&lt;br /&gt;
The Import Modal is a reusable modal used to upload CSV files for different models (Items, Teams, Users, Assignments, etc.).&lt;br /&gt;
----&lt;br /&gt;
; Opening the Import Modal&lt;br /&gt;
:* From the relevant page (e.g., Items index), click the Import button. This opens the Import &amp;lt;ModelName&amp;gt; modal.&lt;br /&gt;
:* As soon as the modal opens, the frontend automatically calls:&lt;br /&gt;
:*: GET /import/&amp;lt;modelClass&amp;gt; to fetch import metadata for that model. This metadata is what populates the lists at the top of the modal.&lt;br /&gt;
; Field Requirements&lt;br /&gt;
:* At the top of the modal you’ll see three lists: &lt;br /&gt;
:# Mandatory fields – columns that must be present in your import for it to succeed.&lt;br /&gt;
:# Optional fields – extra columns you may include if you have that data.&lt;br /&gt;
:# External fields – related fields that can be pulled from or linked to other records (e.g., questionnaire_name, question_advice_id, etc.)&lt;br /&gt;
:* These lists are read-only and meant to guide how you structure your CSV.&lt;br /&gt;
[[File:ImportFrontend.png|600px]]&lt;br /&gt;
; Selecting a CSV&lt;br /&gt;
:* Under CSV file:&lt;br /&gt;
:# Click Choose File and select your .csv file.&lt;br /&gt;
:# Decide whether your file’s first row is a header row:&lt;br /&gt;
:#* First row contains headers (switch ON)&lt;br /&gt;
:#** Use this if your first row has column names like txt, weight, seq.&lt;br /&gt;
:#** The backend will match CSV headers to field names automatically.&lt;br /&gt;
:#* First row contains headers (switch OFF)&lt;br /&gt;
:#** Use this if your file has no header row (just data).&lt;br /&gt;
[[File:UploadFile.png|600px]]&lt;br /&gt;
; Manual Column Mapping&lt;br /&gt;
:* If “First row contains headers” is OFF, the modal:&lt;br /&gt;
:** Reads the first one or two lines from your CSV.&lt;br /&gt;
:** For each column, shows:&lt;br /&gt;
:*** A dropdown to choose which field this column represents.&lt;br /&gt;
:*** A “First Row Value” snippet so you can see an example value from that column.&lt;br /&gt;
:* You must:&lt;br /&gt;
:*# Go through each column.&lt;br /&gt;
:*# Use the dropdown to select the corresponding field name (e.g., map column 1 → txt, column 2 → weight, etc.).&lt;br /&gt;
:*# Ensure that all mandatory fields appear in at least one column.&lt;br /&gt;
:** If mandatory fields are missing from your selection, the modal will block the import and show a status message.&lt;br /&gt;
[[File:ColumnMapping.png|600px]]&lt;br /&gt;
; Handling Duplicates&lt;br /&gt;
:* In the Duplicate handling section:&lt;br /&gt;
:** If the backend has configured options, you’ll see one or more radio buttons (e.g., skip, overwrite).&lt;br /&gt;
:** Choose how the system should behave if it encounters duplicate records during the import.&lt;br /&gt;
:* If no duplicate options are available, you’ll see: “No duplicate options.”&lt;br /&gt;
; Running the Import&lt;br /&gt;
:* When everything is set:&lt;br /&gt;
:*# Click &amp;quot;import&amp;quot;&lt;br /&gt;
:*# The frontend sends a POST /import/&amp;lt;modelClass&amp;gt; request with:&lt;br /&gt;
:*#* csv_file – the file you selected.&lt;br /&gt;
:*#* use_headers – true or false depending on the toggle.&lt;br /&gt;
:*#* ordered_fields – only included if header mode is off, listing the fields you selected for each column.&lt;br /&gt;
:* While the request is in progress, the modal shows a loading state.&lt;br /&gt;
:* When the response returns, the Status line at the bottom will show either:&lt;br /&gt;
:** A success message from the backend (e.g., “Import complete” or a custom message)&lt;br /&gt;
:** An error message if something went wrong (e.g., missing mandatory fields, invalid format)&lt;br /&gt;
:* You can then close the modal with cancel or by using the close icon.&lt;br /&gt;
[[File:ImportComplete.png|600px]]&lt;br /&gt;
&lt;br /&gt;
=== Exporting a File ===&lt;br /&gt;
----&lt;br /&gt;
; Opening the Export Modal&lt;br /&gt;
:* Click the Export button on the page for the model you want to export (e.g., Teams).&lt;br /&gt;
:* When the modal opens, it automatically makes a request to:&lt;br /&gt;
:*: `GET /export/&amp;lt;modelClass&amp;gt;`&lt;br /&gt;
:* to fetch the field metadata for the selected model.&lt;br /&gt;
; Field Lists Displayed&lt;br /&gt;
:* The modal uses the metadata received from the backend to show three types of fields:&lt;br /&gt;
:# Mandatory fields – These must always be included in the export and cannot be unchecked.&lt;br /&gt;
:# Optional fields – These may be included in the export at the user’s discretion.&lt;br /&gt;
:# External fields – Additional related fields made available for export (if applicable).&lt;br /&gt;
:* Users can hover over an info icon to view the full field list in a tooltip.&lt;br /&gt;
; Choosing Columns to Export&lt;br /&gt;
:* The modal provides a checkbox list of all available fields.&lt;br /&gt;
:* Users may:&lt;br /&gt;
:# Check or uncheck optional fields.&lt;br /&gt;
:# View mandatory fields (locked and always selected).&lt;br /&gt;
:# Reorder any fields using ↑ and ↓ arrows, which determines the ordering of columns in the exported CSV.&lt;br /&gt;
; Column Ordering&lt;br /&gt;
:* Fields appear in the export file in the exact order shown in the modal.&lt;br /&gt;
:* The user can:&lt;br /&gt;
:# Move any non-mandatory field up.&lt;br /&gt;
:# Move any non-mandatory field down.&lt;br /&gt;
:* Mandatory fields are always included and their order can also be adjusted if they are part of the list returned by the backend.&lt;br /&gt;
; Generating the CSV&lt;br /&gt;
:* When satisfied with selections:&lt;br /&gt;
:** Click export&lt;br /&gt;
:* The frontend gathers:&lt;br /&gt;
:** The selected fields&lt;br /&gt;
:* It then generates a CSV where:&lt;br /&gt;
:** The first row contains the field names (in the chosen order)&lt;br /&gt;
:** All following rows contain the corresponding values&lt;br /&gt;
:** A downloadable `.csv` file is automatically created and saved to the user’s system.&lt;br /&gt;
; Status Feedback&lt;br /&gt;
:* The modal displays a status message during and after the export operation, for example:&lt;br /&gt;
:# “Generating CSV…”&lt;br /&gt;
:# “Export complete”&lt;br /&gt;
:* If no fields are selected, the modal will prevent exporting and display a warning.&lt;br /&gt;
[[File:ExportFile.png|600px]]&lt;br /&gt;
&lt;br /&gt;
=== Making a class Importable/Exportable ===&lt;br /&gt;
==== Backend ====&lt;br /&gt;
# Add the Import/Export mixin to the top of the class '&amp;lt;code&amp;gt;extend ImportableExportableHelper&amp;lt;/code&amp;gt;'&lt;br /&gt;
# Specify which fields are mandatory for import/export (in snake_case)&lt;br /&gt;
# Specify any external classes that will need to be looked up or created (ie. looking up the Role of a User by id)&lt;br /&gt;
# Specify the available duplicate actions. (By default, the offending fields are transformed.)&lt;br /&gt;
&lt;br /&gt;
[[File:UserEx.png]]&lt;br /&gt;
&lt;br /&gt;
''Example of adding helper to the User model''&lt;br /&gt;
&lt;br /&gt;
'''Note''': If you are adding this helper to a class that has sub classes:&lt;br /&gt;
Case 1: If you want the sub classes have the same fields, external classes, and duplicate actions as the super class:&lt;br /&gt;
* Do the steps listed above in the super class&lt;br /&gt;
* Additionally, do only the first step in the sub class. &lt;br /&gt;
&lt;br /&gt;
Case 2: If you want to specify different fields, external classes, or duplicate actions for sub classes:&lt;br /&gt;
* Do not do any of the steps for the super class&lt;br /&gt;
* Do all steps for each individual sub class&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:ItemEx.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:QuizItemEx.png]]&lt;br /&gt;
&lt;br /&gt;
''Example of Case 1: Adding helper to the Item model, then QuizItem model''&lt;br /&gt;
&lt;br /&gt;
==== Frontend ====&lt;br /&gt;
# Create the showModal variable&lt;br /&gt;
# Create the handleCloseModal method&lt;br /&gt;
# Add the ImportModal or ExportModal to your frontend file. In the tag, add the show variable, onHide method, and the string of the class that is being imported/exported. (ie Model Class User =&amp;gt; &amp;quot;User&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
=== Fixtures ===&lt;br /&gt;
* &amp;lt;code&amp;gt;empty.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A completely empty CSV file. This file is used to ensure that if no content is included, the import service will fail gracefully.&lt;br /&gt;
* &amp;lt;code&amp;gt;empty_with_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file that only contains the headers for the User class. This file is used to ensure a file with no records is handled gracefully.&lt;br /&gt;
* &amp;lt;code&amp;gt;multiple_users_no_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file with no headers for importing records for the User class. It contains multiple User records (John Doe, Jane Doe) to import. &lt;br /&gt;
* &amp;lt;code&amp;gt;multiple_users_with_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file with headers for importing records for the User class. It contains multiple User records (John Doe, Jane Doe) to import. &lt;br /&gt;
* &amp;lt;code&amp;gt;questionnaire_item_with_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file with headers for importing records for the Item class. These Items make up a questionnaire. It contains a single Item that needs to be imported.&lt;br /&gt;
* &amp;lt;code&amp;gt;single_user_no_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file with headers for importing records for the User class. It contains a single User (John Doe) who needs to be imported.&lt;br /&gt;
* &amp;lt;code&amp;gt;single_user_with_headers.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file with headers for importing records for the User class. It contains a single User (John Doe) who needs to be imported.&lt;br /&gt;
* &amp;lt;code&amp;gt;users_duplicate_records.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file containing multiple duplicate records that need to be handled.&lt;br /&gt;
* &amp;lt;code&amp;gt;single_user_email_invalid.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file that contains a user with an invalid email. That is an Internal field that needs to be validated.&lt;br /&gt;
* &amp;lt;code&amp;gt;single_user_role_doe_not_exist.csv&amp;lt;/code&amp;gt;&lt;br /&gt;
*: A CSV file that contains a user with a role that does not exist. That is a field from an External Class that needs to be looked up.&lt;br /&gt;
&lt;br /&gt;
=== Unit Testing ===&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
==== ImportableExportableHelper - Importing ====&lt;br /&gt;
----&lt;br /&gt;
;Create tests for each of the different importable classes&lt;br /&gt;
# Show a class with no headers can be imported (User)&lt;br /&gt;
#: This test tries to import the 'single_user_no_headers.csv' file. This file doesn't have headers, so the test passes a list of headers to the import statement. This CSV file calls for the Role Name of the user, which is set to Student. This tests whether the import statement is able to look up a role by its name. It is able to do this because the name is a lookup field for the External Class Role, which is assigned to the User Class.&lt;br /&gt;
# Show a class with headers can be imported (User)&lt;br /&gt;
#: This test tries to import the 'single_user_with_headers.csv' file. This file has headers, so the test passes nil to the header variable of the import statement. This CSV file calls for the Role ID of the user, which is set to 4 (Teaching Assistant). This tests whether the import statement is able to look up a role by its ID. It can do this even though ID is not the set lookup field, because the lookup function uses the primary key if the lookup field isn't in the file.&lt;br /&gt;
# Show a class with multiple records can be imported (User)&lt;br /&gt;
#: This test tries to import the 'multiple_users_with_headers.csv' file. This file contains multiple user records that need to be imported (John Doe and Jane Doe). This tests that multiple records successfully get imported with the right information.&lt;br /&gt;
# Show a class with external create classes can take duplicate headers (Questionnaire w/ multiple Advice)&lt;br /&gt;
#: This test tries to import the 'questionnaire_item_with_headers.csv' file that creates an external class. Not only that, but it can detect if there are multiple instances of this external class and save them individually. &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:TestUserNoHeaders.png]]&lt;br /&gt;
&lt;br /&gt;
''Import Export Helper Tests: Test Case 1 Implementation''&lt;br /&gt;
&lt;br /&gt;
[[File:TestQuizItem.png]]&lt;br /&gt;
&lt;br /&gt;
''Import Export Helper Tests: Test Case 4 Implementation''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Create Tests to test Errors/Edge Cases&lt;br /&gt;
# Import a class with external lookup class that doesn't exist&lt;br /&gt;
#: Tries to import a csv with an invalid external field. The user's role doesn't exist, so looking up this field should fail and ultimately cause the import to fail.&lt;br /&gt;
# Import a class with an invalid field (User with ian nvalid email)&lt;br /&gt;
#: Tries to import a csv with an invalid internal field. The user email is validated inside the User class, so make sure that validation raises the correct error.&lt;br /&gt;
# Import an empty CSV (With Headers)&lt;br /&gt;
#: Tries to import an empty csv with no records inside. This is tested with the &amp;lt;code&amp;gt;use_header&amp;lt;/code&amp;gt; option set to true.&lt;br /&gt;
# Import an empty CSV (Without Headers)&lt;br /&gt;
#: Tries to import an empty csv with nothing inside. This is tested with the &amp;lt;code&amp;gt;use_header&amp;lt;/code&amp;gt; option set to false.&lt;br /&gt;
&lt;br /&gt;
[[File:TextLookupBad.png]]&lt;br /&gt;
&lt;br /&gt;
''Import Export Helper Tests: Error Test case 2 and 3 Implementation''&lt;br /&gt;
&lt;br /&gt;
==== ImportExportService - Importing ====&lt;br /&gt;
----&lt;br /&gt;
;Create tests to make sure the Service works&lt;br /&gt;
# Test Get Import: 200 response&lt;br /&gt;
#: Test that Import#Index recieves the response, and returns the correct values&lt;br /&gt;
# Test Post Import: 200 response&lt;br /&gt;
#: Test that Import#Import recieves the response with data, and can return a response.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:TestImportService.png]]&lt;br /&gt;
&lt;br /&gt;
''Import Service Tests: Test case 1 Implementation''&lt;br /&gt;
&lt;br /&gt;
;Create Tests to test Errors/Edge Cases&lt;br /&gt;
# Test Post Import: 422 response&lt;br /&gt;
# Test Post Import: 500 response&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== ImportExportService -  Exporting ====&lt;br /&gt;
----&lt;br /&gt;
;Create tests for each of the different exportable classes&lt;br /&gt;
* Export a class with the default headers&lt;br /&gt;
* Export a class with only the mandatory headers&lt;br /&gt;
* Export a class with only some optional headers&lt;br /&gt;
* Export a class with a different order of headers&lt;br /&gt;
&lt;br /&gt;
;Create tests to test Errors/Edge Cases&lt;br /&gt;
* Export a class with chosen headers that are empty&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Frontend Testing ===&lt;br /&gt;
----&lt;br /&gt;
==== Import Modal ====&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
* Shows loading state when isLoading is true&lt;br /&gt;
*: Ensures that when `useAPI` reports `isLoading=true`, the modal hides all content and displays a “Loading…” message.&lt;br /&gt;
* Renders field summary and duplicate options from metadata&lt;br /&gt;
*: Confirms that metadata returned from the backend (mandatory, optional, external fields, and duplicate handling actions) are rendered correctly in the modal.&lt;br /&gt;
* Shows status when importing with no file selected&lt;br /&gt;
*: Validates that clicking “import” without choosing a CSV sets an error status message and prevents the import call.&lt;br /&gt;
* Shows column mapping and first-row values when header mode is off&lt;br /&gt;
*: After uploading a CSV and disabling the “First row contains headers” toggle, verifies that the “Column order” UI appears, column dropdowns are rendered, and the first-row preview text is displayed.&lt;br /&gt;
* Calls sendImport when import is valid&lt;br /&gt;
*: Uploads a valid CSV containing mandatory fields and clicks “import.” Checks that the `sendRequest` mock is called, proving that the modal sends a correctly prepared API request.&lt;br /&gt;
* Calls onHide when cancel is clicked&lt;br /&gt;
*: Simulates clicking the “cancel” button and confirms the modal fires the `onHide` callback to close itself.&lt;br /&gt;
&lt;br /&gt;
== Future work ==&lt;br /&gt;
* Adding event-based entity registration.&lt;br /&gt;
* Extending support for new duplicate resolution policies.&lt;br /&gt;
* Integration with the engine demo for visualization and debugging.&lt;br /&gt;
* More extensive testing for functionality and edge cases.&lt;br /&gt;
* Add a space in the import modal that shows all the values that will be imported.&lt;br /&gt;
* Add the helper method across the website wherever the import/export functionality is required.&lt;br /&gt;
:* The helper is currently in:&lt;br /&gt;
::* QuizItem &lt;br /&gt;
::* Item&lt;br /&gt;
::* QuestionAdvice&lt;br /&gt;
::* Team&lt;br /&gt;
::* User&lt;br /&gt;
&lt;br /&gt;
=== Other Notes/Recommendations ===&lt;br /&gt;
* For Items, limit the string values that question_type can be using built-in validation rules as well, or else importing will allow just any string.&lt;br /&gt;
* For Questionnaires, break_before can't be false because of the validation rules. Maybe try this `validates :break_before, inclusion: { in: [true, false] }`&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Response_maps&amp;diff=167128</id>
		<title>Response maps</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Response_maps&amp;diff=167128"/>
		<updated>2025-11-12T19:05:10Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Response maps variable documentation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Maps a connection between the [[participants]] as reviewers and [[participants]] or [[teams]] as reviewees&lt;br /&gt;
&lt;br /&gt;
==Response maps variable documentation==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The unique record id&lt;br /&gt;
|- &lt;br /&gt;
!reviewed_object_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The object being reviewed in the [[responses|response]]. Possible objects include other ResponseMaps or [[assignments]]&lt;br /&gt;
|-&lt;br /&gt;
!reviewer_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The [[participants|participant]] (actually AssignmentParticipant) providing the response&lt;br /&gt;
|-&lt;br /&gt;
!reviewee_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|Dependent on which subclass of ResponseMap this object is.  For a ReviewResponseMap, the [[teams|team]] (AssignmentTeam) receiving the response.  For a TeammateReviewResponseMap, the AssignmentParticipant who is being reviewed.&lt;br /&gt;
|-&lt;br /&gt;
!type&lt;br /&gt;
|varchar(255)&lt;br /&gt;
|Used for subclassing the response map. Available subclasses are ReviewResponseMap, MetareviewResponseMap, FeedbackResponseMap, TeammateReviewResponseMap  &lt;br /&gt;
|-&lt;br /&gt;
!created_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time for when the record was created&lt;br /&gt;
|-&lt;br /&gt;
!updated_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time when the last update was made&lt;br /&gt;
|-&lt;br /&gt;
!calibrate_to&lt;br /&gt;
|tinyint(1)&lt;br /&gt;
|Tells whether the record will be used for calibration or not.  *In the new system, this field is named for_calibration.*&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to the [[Documentation_on_Database_Tables|database documentation]]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Response_maps&amp;diff=167127</id>
		<title>Response maps</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Response_maps&amp;diff=167127"/>
		<updated>2025-11-12T19:04:45Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Response maps variable documentation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Maps a connection between the [[participants]] as reviewers and [[participants]] or [[teams]] as reviewees&lt;br /&gt;
&lt;br /&gt;
==Response maps variable documentation==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The unique record id&lt;br /&gt;
|- &lt;br /&gt;
!reviewed_object_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The object being reviewed in the [[responses|response]]. Possible objects include other ResponseMaps or [[assignments]]&lt;br /&gt;
|-&lt;br /&gt;
!reviewer_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The [[participants|participant]] (actually AssignmentParticipant) providing the response&lt;br /&gt;
|-&lt;br /&gt;
!reviewee_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|Dependent on which subclass of ResponseMap this object is.  For a ReviewResponseMap, the [[teams|team]] (AssignmentTeam) receiving the response.  For a TeammateReviewResponseMap, the AssignmentParticipant who is being reviewed.&lt;br /&gt;
|-&lt;br /&gt;
!type&lt;br /&gt;
|varchar(255)&lt;br /&gt;
|Used for subclassing the response map. Available subclasses are ReviewResponseMap, MetareviewResponseMap, FeedbackResponseMap, TeammateReviewResponseMap  &lt;br /&gt;
|-&lt;br /&gt;
!created_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time for when the record was created&lt;br /&gt;
|-&lt;br /&gt;
!updated_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time when the last update was made&lt;br /&gt;
|-&lt;br /&gt;
!calibrate_to&lt;br /&gt;
|tinyint(1)&lt;br /&gt;
|Tells whether the record will be used for calibration or not.  _In the new system, this field is named for_calibration_.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to the [[Documentation_on_Database_Tables|database documentation]]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2563._Review_tableau&amp;diff=167126</id>
		<title>CSC/ECE 517 Fall 2025 - E2563. Review tableau</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2563._Review_tableau&amp;diff=167126"/>
		<updated>2025-11-12T17:14:07Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Core Requirements */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This page contains information about E2563.Review calibration, which was a project in CSC517 Fall 2025. &lt;br /&gt;
&lt;br /&gt;
Please see below for a description of the design of the project.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
Expertiza is an open-source application based on the Ruby on Rails framework. Instructors can create and manage assignments, and students can form teams for each one, as well as peer review other teams’ work. The frontend is written in TypeScript, and the backend is written in Ruby. The current codebase can be found here (FRONTEND, BACKEND), as well as in the external info section below. This work is part of an ongoing reimplementation effort of the following old Expertiza repository.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
Instructors need to be able to view the responses made by a student during rounds of peer review in order to accurately assign a grade for that review. To streamline this process, our goal is to design and implement a dedicated page that allows instructors to view all grading reviews submitted by a given student. This page, named the '''review tableau page''', should organize and present the reviews in a clear, structured, and accessible manner. Specifically, the page must display all student grading reviews, grouped by assignment, round, and topic, with each group being rendered as a separate table.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Core Requirements ===&lt;br /&gt;
* A user with the '''Instructor''' role can view the review tableau(s) for a given student from a dedicated page (the ‘tableau page’) of the Expertiza site.&lt;br /&gt;
* The tableau page can be accessed by clicking a “Summary” link under the “Reviews Done” column when viewing students in the instructor’s rewiew-grading dashboard.&lt;br /&gt;
* The tableau page has the following contents:&lt;br /&gt;
** A header, displaying the text “Review by Student, “ followed by the student’s ID.&lt;br /&gt;
** Individual tables (‘tableaus’), each corresponding to a round of reviewing from a specific assignment with a specific rubric used during the review.&lt;br /&gt;
*** Tableaus are displayed in a single column on the tableau page.&lt;br /&gt;
*** Tableaus are sorted first by the '''round in which the review was made''', and then finally by the rubric used for review. This ensures that topics made chronologically close together are displayed sequentially. In the usual case, where the '''same''' rubric is used for all submissions, there will be only a single tableau. If rubric varies by topics, there may be multiple tableaus, reflecting the fact that the rubric may be different for different submissions reviewed by this reviewer.&lt;br /&gt;
* An individual tableau has the following contents:&lt;br /&gt;
** A label for the course number, semester, and year in which the review was made.&lt;br /&gt;
** A label for the assignment for which the review was made.&lt;br /&gt;
** A label for the round number for which the review was made.&lt;br /&gt;
** The primary table, which contains:&lt;br /&gt;
*** A highlighted column, populated with questions asked on the rubric.&lt;br /&gt;
*** An arbitrary number of additional columns, each corresponding to a review for that assignment and round that uses the rubric. Each of these is labelled after the team being reviewed and annotated with the date/time the review was submitted.&lt;br /&gt;
**** Each row of these columns is populated with the student’s responses for the corresponding review and question.&lt;br /&gt;
** Note that a single tableau records the results of all reviews made using the same rubric for a particular round. If a student provided reviews for topics (frontend, backend, full stack) with varying rubrics, then a tableau will be created for each type of rubric filled out.&lt;br /&gt;
* Creating a backend API that can retrieve the student’s grading reviews and sort them per each assignment, round, and topic. ''(Functional and local development)''&lt;br /&gt;
&lt;br /&gt;
=== Design Requirements ===&lt;br /&gt;
[[File:Sample_of_the_review_tableau_layout.png|500px]]&lt;br /&gt;
* Follow the same design guidelines as reimplementation-front-end (https://github.com/AnvithaReddyGutha/reimplementation-front-end/blob/main/design_document.md).&lt;br /&gt;
* Use the same widgets (circles with numbers inside) that are used on the Response views.&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
''To be modified later as implementing''&lt;br /&gt;
* Create File for Reviews Done Page&lt;br /&gt;
::- Main page that displays a table of students with &amp;quot;summary&amp;quot; links&lt;br /&gt;
::- Utility functions for loading student data&lt;br /&gt;
&lt;br /&gt;
* Create File for Review Tableau Page&lt;br /&gt;
::- Main page to work on following the requirements above&lt;br /&gt;
&lt;br /&gt;
== Results ==&lt;br /&gt;
''To be determined!''&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
=== Manual Testing ===&lt;br /&gt;
# Login to Expertiza with an admin or instructor account&lt;br /&gt;
# There should be a “Reviews Done” button on the dashboard. Click on “Reviews Done” button&lt;br /&gt;
# A table contains all students who should show up. Click on the “summary” link for one mock student to get to the Review Tableau page&lt;br /&gt;
# Confirm table displays correctly with the assignment name, student name, review rounds -&amp;gt; review tables following each rubric&lt;br /&gt;
&lt;br /&gt;
=== Automated Testing ===&lt;br /&gt;
* Unit test cases for correct headers, nested tables, and correct layout.&lt;br /&gt;
&lt;br /&gt;
== Team Members ==&lt;br /&gt;
Adam Imbert (apimbert)&lt;br /&gt;
‎&amp;lt;br /&amp;gt;&lt;br /&gt;
Bestin Lalu (blalu)&lt;br /&gt;
‎&amp;lt;br /&amp;gt;&lt;br /&gt;
Yumo Shen (jshen23)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2563._Review_tableau&amp;diff=167125</id>
		<title>CSC/ECE 517 Fall 2025 - E2563. Review tableau</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2563._Review_tableau&amp;diff=167125"/>
		<updated>2025-11-12T17:09:59Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Core Requirements */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This page contains information about E2563.Review calibration, which was a project in CSC517 Fall 2025. &lt;br /&gt;
&lt;br /&gt;
Please see below for a description of the design of the project.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
Expertiza is an open-source application based on the Ruby on Rails framework. Instructors can create and manage assignments, and students can form teams for each one, as well as peer review other teams’ work. The frontend is written in TypeScript, and the backend is written in Ruby. The current codebase can be found here (FRONTEND, BACKEND), as well as in the external info section below. This work is part of an ongoing reimplementation effort of the following old Expertiza repository.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
Instructors need to be able to view the responses made by a student during rounds of peer review in order to accurately assign a grade for that review. To streamline this process, our goal is to design and implement a dedicated page that allows instructors to view all grading reviews submitted by a given student. This page, named the '''review tableau page''', should organize and present the reviews in a clear, structured, and accessible manner. Specifically, the page must display all student grading reviews, grouped by assignment, round, and topic, with each group being rendered as a separate table.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Core Requirements ===&lt;br /&gt;
* A user with the '''Instructor''' role can view the review tableau(s) for a given student from a dedicated page (the ‘tableau page’) of the Expertiza site.&lt;br /&gt;
* The tableau page can be accessed by clicking a “Summary” link under the “Reviews Done” column when viewing students in the instructor’s rewiew-grading dashboard.&lt;br /&gt;
* The tableau page has the following contents:&lt;br /&gt;
** A header, displaying the text “Review by Student, “ followed by the student’s ID.&lt;br /&gt;
** Individual tables (‘tableaus’), each corresponding to a round of reviewing from a specific assignment with a specific rubric used during the review.&lt;br /&gt;
*** Tableaus are displayed in a single column on the tableau page.&lt;br /&gt;
*** Tableaus are sorted first by '''assignment''', then '''round in which the review was made''', then finally by '''topic reviewed for'''. This ensures that topics made chronologically close together are displayed sequentially.&lt;br /&gt;
* An individual tableau has the following contents:&lt;br /&gt;
** A label for the course number, semester, and year in which the review was made.&lt;br /&gt;
** A label for the assignment for which the review was made.&lt;br /&gt;
** A label for the round number for which the review was made.&lt;br /&gt;
** The primary table, which contains:&lt;br /&gt;
*** A highlighted column, populated with questions asked on the rubric.&lt;br /&gt;
*** An arbitrary number of additional columns, each corresponding to a review for that assignment and round that uses the rubric. Each of these is labelled after the team being reviewed and annotated with the date/time the review was submitted.&lt;br /&gt;
**** Each row of these columns is populated with the student’s responses for the corresponding review and question.&lt;br /&gt;
** Note that a single tableau records the results of all reviews made using the same rubric for a particular round. If a student provided reviews for topics (frontend, backend, full stack) with varying rubrics, then a tableau will be created for each type of rubric filled out.&lt;br /&gt;
* Creating a backend API that can retrieve the student’s grading reviews and sort them per each assignment, round, and topic. ''(Functional and local development)''&lt;br /&gt;
&lt;br /&gt;
=== Design Requirements ===&lt;br /&gt;
[[File:Sample_of_the_review_tableau_layout.png|500px]]&lt;br /&gt;
* Follow the same design guidelines as reimplementation-front-end (https://github.com/AnvithaReddyGutha/reimplementation-front-end/blob/main/design_document.md).&lt;br /&gt;
* Use the same widgets (circles with numbers inside) that are used on the Response views.&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
''To be modified later as implementing''&lt;br /&gt;
* Create File for Reviews Done Page&lt;br /&gt;
::- Main page that displays a table of students with &amp;quot;summary&amp;quot; links&lt;br /&gt;
::- Utility functions for loading student data&lt;br /&gt;
&lt;br /&gt;
* Create File for Review Tableau Page&lt;br /&gt;
::- Main page to work on following the requirements above&lt;br /&gt;
&lt;br /&gt;
== Results ==&lt;br /&gt;
''To be determined!''&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
=== Manual Testing ===&lt;br /&gt;
# Login to Expertiza with an admin or instructor account&lt;br /&gt;
# There should be a “Reviews Done” button on the dashboard. Click on “Reviews Done” button&lt;br /&gt;
# A table contains all students who should show up. Click on the “summary” link for one mock student to get to the Review Tableau page&lt;br /&gt;
# Confirm table displays correctly with the assignment name, student name, review rounds -&amp;gt; review tables following each rubric&lt;br /&gt;
&lt;br /&gt;
=== Automated Testing ===&lt;br /&gt;
* Unit test cases for correct headers, nested tables, and correct layout.&lt;br /&gt;
&lt;br /&gt;
== Team Members ==&lt;br /&gt;
Adam Imbert (apimbert)&lt;br /&gt;
‎&amp;lt;br /&amp;gt;&lt;br /&gt;
Bestin Lalu (blalu)&lt;br /&gt;
‎&amp;lt;br /&amp;gt;&lt;br /&gt;
Yumo Shen (jshen23)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2563._Review_tableau&amp;diff=167124</id>
		<title>CSC/ECE 517 Fall 2025 - E2563. Review tableau</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2563._Review_tableau&amp;diff=167124"/>
		<updated>2025-11-12T17:08:30Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Core Requirements */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This page contains information about E2563.Review calibration, which was a project in CSC517 Fall 2025. &lt;br /&gt;
&lt;br /&gt;
Please see below for a description of the design of the project.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
Expertiza is an open-source application based on the Ruby on Rails framework. Instructors can create and manage assignments, and students can form teams for each one, as well as peer review other teams’ work. The frontend is written in TypeScript, and the backend is written in Ruby. The current codebase can be found here (FRONTEND, BACKEND), as well as in the external info section below. This work is part of an ongoing reimplementation effort of the following old Expertiza repository.&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
Instructors need to be able to view the responses made by a student during rounds of peer review in order to accurately assign a grade for that review. To streamline this process, our goal is to design and implement a dedicated page that allows instructors to view all grading reviews submitted by a given student. This page, named the '''review tableau page''', should organize and present the reviews in a clear, structured, and accessible manner. Specifically, the page must display all student grading reviews, grouped by assignment, round, and topic, with each group being rendered as a separate table.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Core Requirements ===&lt;br /&gt;
* A user with the '''Instructor''' role can view the review tableau(s) for a given student from a dedicated page (the ‘tableau page’) of the Expertiza site.&lt;br /&gt;
* The tableau page can be accessed by clicking a “Summary” link under the “Reviews Done” column when viewing students in the instructor’s rewiew-grading dashboard.&lt;br /&gt;
* The tableau page has the following contents:&lt;br /&gt;
** A header, displaying the text “Review by Student, “ followed by the student’s ID.&lt;br /&gt;
** Individual tables (‘tableaus’), each corresponding to a round of reviewing from a specific assignment, as well as the rubric used during the review.&lt;br /&gt;
*** Tableaus are displayed in a single column on the tableau page.&lt;br /&gt;
*** Tableaus are sorted first by '''assignment''', then '''round in which the review was made''', then finally by '''topic reviewed for'''. This ensures that topics made chronologically close together are displayed sequentially.&lt;br /&gt;
* An individual tableau has the following contents:&lt;br /&gt;
** A label for the course number, semester, and year in which the review was made.&lt;br /&gt;
** A label for the assignment for which the review was made.&lt;br /&gt;
** A label for the round number for which the review was made.&lt;br /&gt;
** The primary table, which contains:&lt;br /&gt;
*** A highlighted column, populated with questions asked on the rubric.&lt;br /&gt;
*** An arbitrary number of additional columns, each corresponding to a review for that assignment and round that uses the rubric. Each of these is labelled after the team being reviewed and annotated with the date/time the review was submitted.&lt;br /&gt;
**** Each row of these columns is populated with the student’s responses for the corresponding review and question.&lt;br /&gt;
** Note that a single tableau records the results of all reviews made using the same rubric for a particular round. If a student provided reviews for topics (frontend, backend, full stack) with varying rubrics, then a tableau will be created for each type of rubric filled out.&lt;br /&gt;
* Creating a backend API that can retrieve the student’s grading reviews and sort them per each assignment, round, and topic. ''(Functional and local development)''&lt;br /&gt;
&lt;br /&gt;
=== Design Requirements ===&lt;br /&gt;
[[File:Sample_of_the_review_tableau_layout.png|500px]]&lt;br /&gt;
* Follow the same design guidelines as reimplementation-front-end (https://github.com/AnvithaReddyGutha/reimplementation-front-end/blob/main/design_document.md).&lt;br /&gt;
* Use the same widgets (circles with numbers inside) that are used on the Response views.&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
''To be modified later as implementing''&lt;br /&gt;
* Create File for Reviews Done Page&lt;br /&gt;
::- Main page that displays a table of students with &amp;quot;summary&amp;quot; links&lt;br /&gt;
::- Utility functions for loading student data&lt;br /&gt;
&lt;br /&gt;
* Create File for Review Tableau Page&lt;br /&gt;
::- Main page to work on following the requirements above&lt;br /&gt;
&lt;br /&gt;
== Results ==&lt;br /&gt;
''To be determined!''&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
=== Manual Testing ===&lt;br /&gt;
# Login to Expertiza with an admin or instructor account&lt;br /&gt;
# There should be a “Reviews Done” button on the dashboard. Click on “Reviews Done” button&lt;br /&gt;
# A table contains all students who should show up. Click on the “summary” link for one mock student to get to the Review Tableau page&lt;br /&gt;
# Confirm table displays correctly with the assignment name, student name, review rounds -&amp;gt; review tables following each rubric&lt;br /&gt;
&lt;br /&gt;
=== Automated Testing ===&lt;br /&gt;
* Unit test cases for correct headers, nested tables, and correct layout.&lt;br /&gt;
&lt;br /&gt;
== Team Members ==&lt;br /&gt;
Adam Imbert (apimbert)&lt;br /&gt;
‎&amp;lt;br /&amp;gt;&lt;br /&gt;
Bestin Lalu (blalu)&lt;br /&gt;
‎&amp;lt;br /&amp;gt;&lt;br /&gt;
Yumo Shen (jshen23)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Assignments&amp;diff=167122</id>
		<title>Assignments</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Assignments&amp;diff=167122"/>
		<updated>2025-11-12T02:32:46Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Assignment Variable Documentation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Assignments table contains one record for each assignment created by an [[users|instructor]] or [[users|administrator]].&lt;br /&gt;
&lt;br /&gt;
==Assignment Variable Documentation==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|- &lt;br /&gt;
!id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|This object's unique ID value. One per object.&lt;br /&gt;
|- &lt;br /&gt;
!created_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|The date/time stamp when this record was created&lt;br /&gt;
|- &lt;br /&gt;
!updated_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|The date/time stamp when this record was last modified&lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|The name given to this assignment by the instructor/administrator who created it&lt;br /&gt;
|- &lt;br /&gt;
!directory_path   &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|If this is an assignment to which files are being submitted, this is the first part of the directory path for the submissions. If this is a wiki assignment, it is the base URL for wiki pages submitted by the creators&lt;br /&gt;
|- &lt;br /&gt;
!submitter_count   &lt;br /&gt;
|int(10)  &lt;br /&gt;
|Number of creators who have submitted so far to this assignment&lt;br /&gt;
|- &lt;br /&gt;
!course_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|ID in the [http://wiki.expertiza.ncsu.edu/index.php/Courses_table Courses] table of the course with which this assignment is associated&lt;br /&gt;
|- &lt;br /&gt;
!instructor_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|ID of a instructor who created the assignment&lt;br /&gt;
|- &lt;br /&gt;
!private   &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|whether assignment is visible to other instructors&lt;br /&gt;
|- &lt;br /&gt;
!num_reviews   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|number of reviews done by a student for this assignment&lt;br /&gt;
|- &lt;br /&gt;
!num_review_of_reviews   &lt;br /&gt;
|int(10)  &lt;br /&gt;
|number of reviews of reviews done by a student for this assignment&lt;br /&gt;
|- &lt;br /&gt;
!num_review_of_reviewers   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|number of reviewers who have reviewed the assignment.&lt;br /&gt;
|- &lt;br /&gt;
!reviews_visible_to_all   &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|if false, other reviewers can't see this reviewer's review.&lt;br /&gt;
|- &lt;br /&gt;
!num_reviewers   &lt;br /&gt;
|int(10)  &lt;br /&gt;
|Number of reviewers.&lt;br /&gt;
|- &lt;br /&gt;
!spec_location   &lt;br /&gt;
|text  &lt;br /&gt;
|url of the assignment.&lt;br /&gt;
|- &lt;br /&gt;
!max_team_size &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Maximum number of participants allowed on the team.&lt;br /&gt;
|- &lt;br /&gt;
!staggered_deadline &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether deadlines are staggered or not *************&lt;br /&gt;
|- &lt;br /&gt;
!allow_suggestions &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether participants in an assignment with topics may suggest additional topics for the instructor to approve.&lt;br /&gt;
|- &lt;br /&gt;
!days_between_submissions &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Number of Days available between submissions *************&lt;br /&gt;
|- &lt;br /&gt;
!review_assignment_strategy &lt;br /&gt;
|varchar(12)  &lt;br /&gt;
|&lt;br /&gt;
|- &lt;br /&gt;
!review_topic_threshold&lt;br /&gt;
|int(11)  &lt;br /&gt;
|***********************************&lt;br /&gt;
|- &lt;br /&gt;
!copy_flag &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Whether the assignment has a copy or not. *************&lt;br /&gt;
|- &lt;br /&gt;
!rounds_of_reviews &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Total number of rounds of reviews done for the assignment *************&lt;br /&gt;
|- &lt;br /&gt;
!microtask &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether this is a microtask assignment, i.e., if it has topics that are worth varying amounts of credit, and may be selected by students.&lt;br /&gt;
|- &lt;br /&gt;
!require_quiz &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether the assignment requires a quiz or not. *************&lt;br /&gt;
|- &lt;br /&gt;
!num_quiz_questions &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Total number of questions present for the quiz. *************&lt;br /&gt;
|- &lt;br /&gt;
!is_coding_assignment &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether whether the given assignment is a coding assignment or not *************&lt;br /&gt;
|- &lt;br /&gt;
!is_intelligent &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicates whether topics are to be bid on, and then the &amp;quot;intelligent assignment&amp;quot; algorithm will assign teams to topics *************&lt;br /&gt;
|- &lt;br /&gt;
!calculate_penalty &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether penalty needs to be calculated for the given assignment. *************&lt;br /&gt;
|- &lt;br /&gt;
!late_policy_id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|ID in the ******late policy table***** *************. Indicating which late policy is applied here.&lt;br /&gt;
|- &lt;br /&gt;
!is_penalty_calculated &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether the penalty for the assignment is calculated or not. *************&lt;br /&gt;
|- &lt;br /&gt;
!max_bids &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Total amount of bids given for this assignment. *************&lt;br /&gt;
|- &lt;br /&gt;
!show_teammate_reviews &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether teammate reviews are shown or not. *************&lt;br /&gt;
|- &lt;br /&gt;
!availability_flag &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether the given assignment is available or not. *************&lt;br /&gt;
|- &lt;br /&gt;
!use_bookmark &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether bookmarks are used or not. *************&lt;br /&gt;
|- &lt;br /&gt;
!can_review_same_topic &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether a reviewer can review the same topic again. *************&lt;br /&gt;
|- &lt;br /&gt;
!can_choose_topic_to_review &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether a reviewer can choose a topic to review *************&lt;br /&gt;
|- &lt;br /&gt;
!is_calibrated &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether the assignment is calibrated or not. *************&lt;br /&gt;
|- &lt;br /&gt;
!is_selfreview_enabled &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether an author can review his/her work. *************&lt;br /&gt;
|- &lt;br /&gt;
!reputation_algorithm &lt;br /&gt;
|varchar(12)  &lt;br /&gt;
|This field gives us the information of the reputation algorithm used for this assignment. *************&lt;br /&gt;
|- &lt;br /&gt;
!is_anonymous &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|Indicating whether whether the reviewer information is anonymous or not. *************&lt;br /&gt;
|- &lt;br /&gt;
!num_reviews_required  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Total number of reviews required for this assignment. *************&lt;br /&gt;
|- &lt;br /&gt;
!num_metareviews_required  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Total number of metareviews required for this assignment. *************&lt;br /&gt;
|- &lt;br /&gt;
!num_metareviews_allowed  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Total number of metareviews allowed for this assignment. *************&lt;br /&gt;
|- &lt;br /&gt;
!num_reviews_allowed  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Total number of reviews allowed for this assignment. *************&lt;br /&gt;
|- &lt;br /&gt;
!simicheck  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Similarity Check between assignments. *************&lt;br /&gt;
|- &lt;br /&gt;
!simicheck_threshold&lt;br /&gt;
|int(11)  &lt;br /&gt;
|Similarity check threshold value for the given assignment. *************&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Parents Tables ==&lt;br /&gt;
Tables referred by the Assignment Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:assignment_imported.png]]&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Child Tables ==&lt;br /&gt;
Tables which refer the Assignment Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:assignment_exported.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to the [[Documentation_on_Database_Tables|database documentation]]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2557._Specialized_rubrics_for_different_topic_types&amp;diff=166966</id>
		<title>CSC/ECE 517 Fall 2025 - E2557. Specialized rubrics for different topic types</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2025_-_E2557._Specialized_rubrics_for_different_topic_types&amp;diff=166966"/>
		<updated>2025-11-09T18:28:39Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Motivation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Fall 2025 – E2557. Specialized Rubrics for Different Topic Types =&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
In the current Expertiza system, all projects within a course share a single rubric. This means that projects of different types — such as refactoring projects, testing projects, or front-end projects — must be evaluated using the same criteria.  &lt;br /&gt;
&lt;br /&gt;
The goal of this project is to enable instructors to assign '''different rubrics to different project topics''', allowing for more accurate and flexible assessment methods.  &lt;br /&gt;
&lt;br /&gt;
== Motivation ==&lt;br /&gt;
Expertiza supports a variety of project types across CSC/ECE 517 courses. However, because only one rubric can be assigned to an assignment at present, instructors cannot tailor rubrics to the distinct expectations of each project type.  &lt;br /&gt;
&lt;br /&gt;
By implementing specialized rubrics per topic, the evaluation process can better reflect the unique goals and deliverables of each project type.&lt;br /&gt;
&lt;br /&gt;
== Functional Requirements ==&lt;br /&gt;
Note that several of these functional requirements were shared by the E2552 group, particularly any that rely on frontend implementation.&lt;br /&gt;
&lt;br /&gt;
* By default, each course project uses the rubric specified on the '''Rubrics''' tab.&lt;br /&gt;
* On the '''Topics''' tab (in the Assignment Edit page), a '''dropdown''' should appear next to each topic.  &lt;br /&gt;
** This dropdown allows instructors to choose a rubric specific to that topic.&lt;br /&gt;
** If no rubric is selected, the assignment’s default rubric is used.&lt;br /&gt;
* The '''signup sheet''' interface should display the rubric chosen for each topic.&lt;br /&gt;
* The '''rubric-selection logic''' should not reside within the `signup_sheet_controller`.  &lt;br /&gt;
** Any non-topic-related logic should instead be handled in another controller or helper.&lt;br /&gt;
* A new column, `topic_id`, must be added to the `assignment_questionnaires` table to link a questionnaire to a specific topic.&lt;br /&gt;
&lt;br /&gt;
== Implementation Details ==&lt;br /&gt;
To implement the requirements, we have made the following changes to the backend repository:&lt;br /&gt;
* Implemented rubric_list, a new GET API endpoint in sign_up_topics_controller.rb, which returns the topics and rubrics associated with a specific assignment. These are serialized into a readable JSON format to facilitate frontend use.&lt;br /&gt;
* Inserted &amp;quot;topic_id&amp;quot; as a foreign key assignment_questionnaire.rb, and added a &amp;quot;questionnaire&amp;quot; field to sign_up_topic.rb. These allow topics to be assigned on a per-assignment, per-round basis.&lt;br /&gt;
* Implemented rubric_for_review, a function in the sign_up_topic.rb model that returns the appropriate questionnaire for a topic given a specific round number that questionnaire corresponds to.&lt;br /&gt;
* Implemented has_specific_rubric?, a function in the sign_up_topic.rb model that returns whether or not the topic has an associated rubric for the given round.&lt;br /&gt;
* Introduced topic_rubrics_helper.rb, a helper class designed to carry the assign_rubric_to_topic function. This associates the given assignment, topic, questionnaire, and round number with each other in the database in an unobtrusive way that does not gunk up other classes.&lt;br /&gt;
* Created sign_up_topic_spec.rb, a testing class designed to test the aforementioned new functions in the SignUpTopic class.&lt;br /&gt;
&lt;br /&gt;
== Integration of AI ==&lt;br /&gt;
Artificially intelligent LLM models (ChatGPT, Claude.ai) were leveraged to streamline development and consult regarding critical design decisions.&lt;br /&gt;
&lt;br /&gt;
Firstly: [https://claude.ai/share/fbd40465-86ae-498c-bb22-1b719784a928 unspecific, project-encompassing] conversations served to give us a high-level understanding of what tasks needed to be completed, and what components might comprise a completed implementation. Having this overview is important for project planning, especially with the shorter time window we gave ourselves to complete implementation. While we did not have access to Issues to populate a GitHub project page, we were able to break the implementation into tasks for our own internal production management.&lt;br /&gt;
&lt;br /&gt;
Secondly: [https://chatgpt.com/c/690227fe-a6c4-832d-a485-e284f1a457f0 troubleshooting] or [https://chatgpt.com/share/6902cda2-364c-8007-b18d-d804023b73d1 class-specific] conversations allowed us to isolate a component of the implementation and (hopefully) keep everything modular, extensible, and maintainable. And, of course, promptly fix any bugs we encountered in setup or execution of our implementation!&lt;br /&gt;
&lt;br /&gt;
== Future Work ==&lt;br /&gt;
Due to unfortunate time constraints, both circumstantial and self-imposed, the implementation as it stands is lacking in a number of key categories. Chiefly, our rspec tests are undertested due to issues running the rspec test suite. As such, we recommend careful code review is applied to the resulting PR before it is considered for merging. The PR is set to &amp;quot;DRAFT&amp;quot; to reflect this.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
With that being said, here are a few action steps for future teams tackling this problem:&lt;br /&gt;
* Finalize the API integration with the front-end dropdown component.&lt;br /&gt;
* Implement logic to persist rubric–topic associations.&lt;br /&gt;
* Ensure that current automated test coverage for the new functionality is sufficient and correct.&lt;br /&gt;
* Conduct usability testing and gather instructor feedback, as always!&lt;br /&gt;
&lt;br /&gt;
== Useful Links ==&lt;br /&gt;
* [https://github.com/Prismly/reimplementation-back-end/tree/main Team GitHub Repository]&lt;br /&gt;
* [https://github.com/expertiza/reimplementation-back-end/pull/226 Backend Pull Request]&lt;br /&gt;
* [https://docs.google.com/presentation/d/1dltJz9vOSq-tDVyGDUtLTfuC5gVlylsdjNs7TPJTQBk/edit?usp=sharing Demo Slide Deck] (requires NCSU Google Account)&lt;br /&gt;
&lt;br /&gt;
== Team Information ==&lt;br /&gt;
* Adam Imbert (apimbert)&lt;br /&gt;
* Iman Khan (ikhan7)&lt;br /&gt;
* Niranjan Rajendran (nrajend4)&lt;br /&gt;
* '''Mentor''': Ed Gehringer (efg)&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Original codebase feature page: [[CSC/ECE 517 Spring 2020 - E2026. Specialized rubrics for different topic types|Project E2026 Wiki Page]]&lt;br /&gt;
* Reimplementation precedent feature page: [[CSC/ECE_517_Spring_2025_-_E2513._Reimplement_sign_up_topic.rb_as_project_topic.rb|Project E2513 Wiki Page]]&lt;br /&gt;
&lt;br /&gt;
----&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Teams&amp;diff=166778</id>
		<title>Teams</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Teams&amp;diff=166778"/>
		<updated>2025-10-28T17:44:51Z</updated>

		<summary type="html">&lt;p&gt;Admin: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|&lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Name of the team&lt;br /&gt;
|- &lt;br /&gt;
!parent_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|ID of assignment or course that this team is participating in&lt;br /&gt;
|- &lt;br /&gt;
!type   &lt;br /&gt;
|varchar(255)&lt;br /&gt;
  &lt;br /&gt;
|Type of the team, either AssignmentTeam or CourseTeam&lt;br /&gt;
|- &lt;br /&gt;
!comments_for_advertisement  &lt;br /&gt;
|text&lt;br /&gt;
|text for comments&lt;br /&gt;
|- &lt;br /&gt;
!advertise_for_partner  &lt;br /&gt;
|tinyint(1) &lt;br /&gt;
|Boolean that tells whether the team is advertising for a partner&lt;br /&gt;
|- &lt;br /&gt;
!submitted_hyperlinks  &lt;br /&gt;
|text &lt;br /&gt;
|List of submitted hyperlinks&lt;br /&gt;
|- &lt;br /&gt;
!directory_num&lt;br /&gt;
|int(11) &lt;br /&gt;
|directory number where files submitted by this team are stored&lt;br /&gt;
|- &lt;br /&gt;
!grade_for_submission&lt;br /&gt;
|int(11) &lt;br /&gt;
|Stores the grade assigned by the instructor, if any&lt;br /&gt;
|- &lt;br /&gt;
!comment_for_submission&lt;br /&gt;
|text&lt;br /&gt;
|Stores feedback from the instructor for the team&lt;br /&gt;
|} &lt;br /&gt;
== E/R diagram of tables referencing users table  ==&lt;br /&gt;
The following image shows the tables referencing teams table&lt;br /&gt;
[[File:teams_export.png]]&lt;br /&gt;
&lt;br /&gt;
== E/R diagram of tables users table is referencing to ==&lt;br /&gt;
The teams table is not referenced by any other tables&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Teams&amp;diff=166777</id>
		<title>Teams</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Teams&amp;diff=166777"/>
		<updated>2025-10-28T17:44:37Z</updated>

		<summary type="html">&lt;p&gt;Admin: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|&lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Name of the team&lt;br /&gt;
|- &lt;br /&gt;
!parent_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|ID of assignment or course that this team is participating in&lt;br /&gt;
|- &lt;br /&gt;
!type   &lt;br /&gt;
|varchar(255)&lt;br /&gt;
  &lt;br /&gt;
|Type of the team, either AssignmentTeam or CourseTeam&lt;br /&gt;
|- &lt;br /&gt;
!comments_for_advertisement  &lt;br /&gt;
|text&lt;br /&gt;
|text for comments&lt;br /&gt;
|- &lt;br /&gt;
!advertise_for_partner  &lt;br /&gt;
|tinyint(1) &lt;br /&gt;
|Boolean that tells whether the team is advertising for a partners&lt;br /&gt;
|- &lt;br /&gt;
!submitted_hyperlinks  &lt;br /&gt;
|text &lt;br /&gt;
|List of submitted hyperlinks&lt;br /&gt;
|- &lt;br /&gt;
!directory_num&lt;br /&gt;
|int(11) &lt;br /&gt;
|directory number where files submitted by this team are stored&lt;br /&gt;
|- &lt;br /&gt;
!grade_for_submission&lt;br /&gt;
|int(11) &lt;br /&gt;
|Stores the grade assigned by the instructor, if any&lt;br /&gt;
|- &lt;br /&gt;
!comment_for_submission&lt;br /&gt;
|text&lt;br /&gt;
|Stores feedback from the instructor for the team&lt;br /&gt;
|} &lt;br /&gt;
== E/R diagram of tables referencing users table  ==&lt;br /&gt;
The following image shows the tables referencing teams table&lt;br /&gt;
[[File:teams_export.png]]&lt;br /&gt;
&lt;br /&gt;
== E/R diagram of tables users table is referencing to ==&lt;br /&gt;
The teams table is not referenced by any other tables&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Participants&amp;diff=166737</id>
		<title>Participants</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Participants&amp;diff=166737"/>
		<updated>2025-10-17T14:28:43Z</updated>

		<summary type="html">&lt;p&gt;Admin: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Users associated with either a [http://wiki.expertiza.ncsu.edu/index.php/Courses_table course] or an [http://wiki.expertiza.ncsu.edu/index.php/Assignments assignment]&lt;br /&gt;
&lt;br /&gt;
==Participants variable documentation==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique identifier for the person participating&lt;br /&gt;
|-&lt;br /&gt;
!can_submit&lt;br /&gt;
|BIT&lt;br /&gt;
|bit to identify whether the person can submit or not&lt;br /&gt;
|-&lt;br /&gt;
!can_review&lt;br /&gt;
|BIT&lt;br /&gt;
|bit to identify whether the person can review or not&lt;br /&gt;
|-&lt;br /&gt;
!user_id&lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique id referring to the user &lt;br /&gt;
|-&lt;br /&gt;
!parent_id&lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique id referring to parent assignment or course&lt;br /&gt;
|-&lt;br /&gt;
!submitted_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time of the submission made&lt;br /&gt;
|-&lt;br /&gt;
!permission_granted&lt;br /&gt;
|BIT&lt;br /&gt;
|this bit tells whether the instructor can use the work that the student has submitted later on.&lt;br /&gt;
|-&lt;br /&gt;
!penalty_accumulated&lt;br /&gt;
|INT UNSIGNED&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
!grade&lt;br /&gt;
|FLOAT&lt;br /&gt;
|Grade allotted for the submission after evaluation&lt;br /&gt;
|-&lt;br /&gt;
!type&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|either CourseParticipant or AssignmentParticipant&lt;br /&gt;
|-&lt;br /&gt;
!handle&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|the handle that the user wants to be known by in this assignment&lt;br /&gt;
|-&lt;br /&gt;
!time_stamp&lt;br /&gt;
|DATETIME&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
!digital_signature&lt;br /&gt;
|TEXT&lt;br /&gt;
|to record what was the evaluation&lt;br /&gt;
|-&lt;br /&gt;
!duty&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|the &amp;quot;role&amp;quot; of this person on their team, e.g., tester.  Used only when roles are assigned to team members.&lt;br /&gt;
|-&lt;br /&gt;
!can_take_quiz&lt;br /&gt;
|BIT&lt;br /&gt;
|this bit signifies whether the participant can take a quiz on the work they are reviewing&lt;br /&gt;
|-&lt;br /&gt;
|Hamer&lt;br /&gt;
|FLOAT&lt;br /&gt;
|the reputation of this participant using the Hamer algorithm&lt;br /&gt;
|-&lt;br /&gt;
|Lauw&lt;br /&gt;
|FLOAT&lt;br /&gt;
|the reputation of this participant using the Lauw algorithm&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Parents Tables ==&lt;br /&gt;
Tables referred by the Participants Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:participants_eer.jpg]]&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Participants&amp;diff=166736</id>
		<title>Participants</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Participants&amp;diff=166736"/>
		<updated>2025-10-17T14:27:49Z</updated>

		<summary type="html">&lt;p&gt;Admin: Undo revision 166733 by Admin (talk)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Users associated with either a [http://wiki.expertiza.ncsu.edu/index.php/Courses_table course] or an [http://wiki.expertiza.ncsu.edu/index.php/Assignments assignment]&lt;br /&gt;
&lt;br /&gt;
==Participants variable documentation==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique identifier for the person participating&lt;br /&gt;
|-&lt;br /&gt;
!can_submit&lt;br /&gt;
|BIT&lt;br /&gt;
|bit to identify whether the person can submit or not&lt;br /&gt;
|-&lt;br /&gt;
!can_review&lt;br /&gt;
|BIT&lt;br /&gt;
|bit to identify whether the person can review or not&lt;br /&gt;
|-&lt;br /&gt;
!user_id&lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique id referring to the user &lt;br /&gt;
|-&lt;br /&gt;
!parent_id&lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique id referring to parent assignment or course&lt;br /&gt;
|-&lt;br /&gt;
!submitted_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time of the submission made&lt;br /&gt;
|-&lt;br /&gt;
!permission_granted&lt;br /&gt;
|BIT&lt;br /&gt;
|this bit tells whether the instructor can use the work that the student has submitted later on.&lt;br /&gt;
|-&lt;br /&gt;
!penalty_accumulated&lt;br /&gt;
|INT UNSIGNED&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
!grade&lt;br /&gt;
|FLOAT&lt;br /&gt;
|Grade allotted for the submission after evaluation&lt;br /&gt;
|-&lt;br /&gt;
!type&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|either CourseParticipant or AssignmentParticipant&lt;br /&gt;
|-&lt;br /&gt;
!handle&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|the handle that the user wants to be known by in this assignment&lt;br /&gt;
!time_stamp&lt;br /&gt;
|DATETIME&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
!digital_signature&lt;br /&gt;
|TEXT&lt;br /&gt;
|to record what was the evaluation&lt;br /&gt;
|-&lt;br /&gt;
!duty&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|the &amp;quot;role&amp;quot; of this person on their team, e.g., tester.  Used only when roles are assigned to team members.&lt;br /&gt;
|-&lt;br /&gt;
!can_take_quiz&lt;br /&gt;
|BIT&lt;br /&gt;
|this bit signifies whether the participant can take a quiz on the work they are reviewing&lt;br /&gt;
|-&lt;br /&gt;
|Hamer&lt;br /&gt;
|FLOAT&lt;br /&gt;
|the reputation of this participant using the Hamer algorithm&lt;br /&gt;
|-&lt;br /&gt;
|Lauw&lt;br /&gt;
|FLOAT&lt;br /&gt;
|the reputation of this participant using the Lauw algorithm&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Parents Tables ==&lt;br /&gt;
Tables referred by the Participants Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:participants_eer.jpg]]&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Participants&amp;diff=166735</id>
		<title>Participants</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Participants&amp;diff=166735"/>
		<updated>2025-10-17T14:27:20Z</updated>

		<summary type="html">&lt;p&gt;Admin: Undo revision 166734 by Admin (talk)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Users associated with either a [http://wiki.expertiza.ncsu.edu/index.php/Courses_table course] or an [http://wiki.expertiza.ncsu.edu/index.php/Assignments assignment]&lt;br /&gt;
&lt;br /&gt;
==Participants variable documentation==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique identifier for the person participating&lt;br /&gt;
|-&lt;br /&gt;
!can_submit&lt;br /&gt;
|BIT&lt;br /&gt;
|bit to identify whether the person can submit or not&lt;br /&gt;
|-&lt;br /&gt;
!can_review&lt;br /&gt;
|BIT&lt;br /&gt;
|bit to identify whether the person can review or not&lt;br /&gt;
|-&lt;br /&gt;
!user_id&lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique id referring to the user &lt;br /&gt;
|-&lt;br /&gt;
!parent_id&lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique id referring to parent assignment or course&lt;br /&gt;
|-&lt;br /&gt;
!submitted_at&lt;br /&gt;
|-&lt;br /&gt;
DATETIME&lt;br /&gt;
|Date and Time of the submission made&lt;br /&gt;
|-&lt;br /&gt;
!permission_granted&lt;br /&gt;
|BIT&lt;br /&gt;
|this bit tells whether the instructor can use the work that the student has submitted later on.&lt;br /&gt;
|-&lt;br /&gt;
!penalty_accumulated&lt;br /&gt;
|INT UNSIGNED&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
!grade&lt;br /&gt;
|FLOAT&lt;br /&gt;
|Grade allotted for the submission after evaluation&lt;br /&gt;
|-&lt;br /&gt;
!type&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|either CourseParticipant or AssignmentParticipant&lt;br /&gt;
|-&lt;br /&gt;
!handle&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|the handle that the user wants to be known by in this assignment&lt;br /&gt;
!time_stamp&lt;br /&gt;
|DATETIME&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
!digital_signature&lt;br /&gt;
|TEXT&lt;br /&gt;
|to record what was the evaluation&lt;br /&gt;
|-&lt;br /&gt;
!duty&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|the &amp;quot;role&amp;quot; of this person on their team, e.g., tester.  Used only when roles are assigned to team members.&lt;br /&gt;
|-&lt;br /&gt;
!can_take_quiz&lt;br /&gt;
|BIT&lt;br /&gt;
|this bit signifies whether the participant can take a quiz on the work they are reviewing&lt;br /&gt;
|-&lt;br /&gt;
|Hamer&lt;br /&gt;
|FLOAT&lt;br /&gt;
|the reputation of this participant using the Hamer algorithm&lt;br /&gt;
|-&lt;br /&gt;
|Lauw&lt;br /&gt;
|FLOAT&lt;br /&gt;
|the reputation of this participant using the Lauw algorithm&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Parents Tables ==&lt;br /&gt;
Tables referred by the Participants Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:participants_eer.jpg]]&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Participants&amp;diff=166734</id>
		<title>Participants</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Participants&amp;diff=166734"/>
		<updated>2025-10-17T14:26:10Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Participants variable documentation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Users associated with either a [http://wiki.expertiza.ncsu.edu/index.php/Courses_table course] or an [http://wiki.expertiza.ncsu.edu/index.php/Assignments assignment]&lt;br /&gt;
&lt;br /&gt;
==Participants variable documentation==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique identifier for the person participating&lt;br /&gt;
|-&lt;br /&gt;
!can_submit&lt;br /&gt;
|BIT&lt;br /&gt;
|bit to identify whether the person can submit or not&lt;br /&gt;
|-&lt;br /&gt;
!can_review&lt;br /&gt;
|BIT&lt;br /&gt;
|bit to identify whether the person can review or not&lt;br /&gt;
|-&lt;br /&gt;
!user_id&lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique id referring to the user &lt;br /&gt;
|-&lt;br /&gt;
!parent_id&lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique id referring to parent assignment or course&lt;br /&gt;
|-&lt;br /&gt;
!submitted_at&lt;br /&gt;
DATETIME&lt;br /&gt;
|Date and Time of the submission made&lt;br /&gt;
|-&lt;br /&gt;
!permission_granted&lt;br /&gt;
|BIT&lt;br /&gt;
|this bit tells whether the instructor can use the work that the student has submitted later on.&lt;br /&gt;
|-&lt;br /&gt;
!penalty_accumulated&lt;br /&gt;
|INT UNSIGNED&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
!grade&lt;br /&gt;
|FLOAT&lt;br /&gt;
|Grade allotted for the submission after evaluation&lt;br /&gt;
|-&lt;br /&gt;
!type&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|either CourseParticipant or AssignmentParticipant&lt;br /&gt;
|-&lt;br /&gt;
!handle&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|the handle that the user wants to be known by in this assignment&lt;br /&gt;
!time_stamp&lt;br /&gt;
|DATETIME&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
!digital_signature&lt;br /&gt;
|TEXT&lt;br /&gt;
|to record what was the evaluation&lt;br /&gt;
|-&lt;br /&gt;
!duty&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|the &amp;quot;role&amp;quot; of this person on their team, e.g., tester.  Used only when roles are assigned to team members.&lt;br /&gt;
|-&lt;br /&gt;
!can_take_quiz&lt;br /&gt;
|BIT&lt;br /&gt;
|this bit signifies whether the participant can take a quiz on the work they are reviewing&lt;br /&gt;
|-&lt;br /&gt;
|Hamer&lt;br /&gt;
|FLOAT&lt;br /&gt;
|the reputation of this participant using the Hamer algorithm&lt;br /&gt;
|-&lt;br /&gt;
|Lauw&lt;br /&gt;
|FLOAT&lt;br /&gt;
|the reputation of this participant using the Lauw algorithm&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Parents Tables ==&lt;br /&gt;
Tables referred by the Participants Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:participants_eer.jpg]]&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Participants&amp;diff=166733</id>
		<title>Participants</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Participants&amp;diff=166733"/>
		<updated>2025-10-17T14:25:09Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Participants variable documentation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Users associated with either a [http://wiki.expertiza.ncsu.edu/index.php/Courses_table course] or an [http://wiki.expertiza.ncsu.edu/index.php/Assignments assignment]&lt;br /&gt;
&lt;br /&gt;
==Participants variable documentation==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique identifier for the person participating&lt;br /&gt;
|-&lt;br /&gt;
!can_submit&lt;br /&gt;
|BIT&lt;br /&gt;
|bit to identify whether the person can submit or not&lt;br /&gt;
|-&lt;br /&gt;
!can_review&lt;br /&gt;
|BIT&lt;br /&gt;
|bit to identify whether the person can review or not&lt;br /&gt;
|-&lt;br /&gt;
!user_id&lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique id referring to the user &lt;br /&gt;
|-&lt;br /&gt;
!parent_id&lt;br /&gt;
|INT(10)&lt;br /&gt;
|unique id referring to parent assignment or course&lt;br /&gt;
|-&lt;br /&gt;
!submitted_at&lt;br /&gt;
|-&lt;br /&gt;
DATETIME&lt;br /&gt;
|Date and Time of the submission made&lt;br /&gt;
|-&lt;br /&gt;
!permission_granted&lt;br /&gt;
|BIT&lt;br /&gt;
|this bit tells whether the instructor can use the work that the student has submitted later on.&lt;br /&gt;
|-&lt;br /&gt;
!penalty_accumulated&lt;br /&gt;
|INT UNSIGNED&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
!grade&lt;br /&gt;
|FLOAT&lt;br /&gt;
|Grade allotted for the submission after evaluation&lt;br /&gt;
|-&lt;br /&gt;
!type&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|either CourseParticipant or AssignmentParticipant&lt;br /&gt;
|-&lt;br /&gt;
!handle&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|the handle that the user wants to be known by in this assignment&lt;br /&gt;
!time_stamp&lt;br /&gt;
|DATETIME&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
!digital_signature&lt;br /&gt;
|TEXT&lt;br /&gt;
|to record what was the evaluation&lt;br /&gt;
|-&lt;br /&gt;
!duty&lt;br /&gt;
|VARCHAR(255)&lt;br /&gt;
|the &amp;quot;role&amp;quot; of this person on their team, e.g., tester.  Used only when roles are assigned to team members.&lt;br /&gt;
|-&lt;br /&gt;
!can_take_quiz&lt;br /&gt;
|BIT&lt;br /&gt;
|this bit signifies whether the participant can take a quiz on the work they are reviewing&lt;br /&gt;
|-&lt;br /&gt;
|Hamer&lt;br /&gt;
|FLOAT&lt;br /&gt;
|the reputation of this participant using the Hamer algorithm&lt;br /&gt;
|-&lt;br /&gt;
|Lauw&lt;br /&gt;
|FLOAT&lt;br /&gt;
|the reputation of this participant using the Lauw algorithm&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Parents Tables ==&lt;br /&gt;
Tables referred by the Participants Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:participants_eer.jpg]]&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Answer_tags&amp;diff=166732</id>
		<title>Answer tags</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Answer_tags&amp;diff=166732"/>
		<updated>2025-09-26T15:14:04Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Answer Tags Variable Documentation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The Answer Tags table contains tags that have been assigned to rubric comments based on the characteristics those comments have.&lt;br /&gt;
== Answer Tags Variable Documentation ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|- &lt;br /&gt;
!id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Unique ID for each Answer_tag record.&lt;br /&gt;
|- &lt;br /&gt;
!answer_id   &lt;br /&gt;
|int(11) &lt;br /&gt;
|ID of the Answer comment that is being tagged.&lt;br /&gt;
|- &lt;br /&gt;
!tag_prompt_deployment_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|When someone tags a comment, they tag it for a particular characteristic.  The deployment_id tells which characteristic&lt;br /&gt;
|- &lt;br /&gt;
!user_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|ID of the User who set the tag&lt;br /&gt;
|- &lt;br /&gt;
!value   &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Value of the Tag (not set, yes, or no).&lt;br /&gt;
|- &lt;br /&gt;
!created_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|Date time value when the record was created.&lt;br /&gt;
|- &lt;br /&gt;
!updated_at  &lt;br /&gt;
|datetime  &lt;br /&gt;
|Date time value when the record was updated.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Parents Tables ==&lt;br /&gt;
Tables referred by the Answer Tags Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:answer_tags_imported.png]]&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Child Tables ==&lt;br /&gt;
No Tables refer the Answer Tags Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Design_for_Automatic_Evaluation_of_Peer_Reviews&amp;diff=166731</id>
		<title>Design for Automatic Evaluation of Peer Reviews</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Design_for_Automatic_Evaluation_of_Peer_Reviews&amp;diff=166731"/>
		<updated>2025-07-16T18:15:57Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* How it is going to be added to Expertiza */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Evaluate Using LLMs Integration =&lt;br /&gt;
&lt;br /&gt;
== What we want to do ==&lt;br /&gt;
&lt;br /&gt;
The goal of this project is to integrate LLMs with the peer-assessment process to improve learning.  This can be done in various ways, including&lt;br /&gt;
* Letting the LLM rate the submission and grade reviewers on how close their review is to the LLM’s (LLM as oracle).&lt;br /&gt;
* Using the LLM to read a review and give the reviewer advice on how to improve it (LLM as advisor).&lt;br /&gt;
* Using the LLM to rate the reviews and use that metric to weight reviewers’ score in calculating a grade (LLM as reputation system)&lt;br /&gt;
* Letting the LLM to do the review and ask reviewers (or authors) if they agree with the LLM and why or why not (LLM as opening salvo)&lt;br /&gt;
&lt;br /&gt;
Students learn best by doing reviews.  So, maybe you want the LLM to teach students to review.&lt;br /&gt;
&lt;br /&gt;
We also want to show instructors '''an evaluation of each reviewer’s overall reviewing performance'''.&lt;br /&gt;
Rather than looking at individual reviews, instructors will get a '''summary report''' describing how well each reviewer performed overall.&lt;br /&gt;
The evaluation includes aspects like quality of comments, score consistency, engagement, and other rubric-related metrics.&lt;br /&gt;
This report will be generated using a '''Large Language Model (LLM)''', such as GPT.&lt;br /&gt;
&lt;br /&gt;
Thus, instructors and TAs will be able to '''edit, overwrite, and finalize reviewer evaluation''' based on the LLM-generated suggestions.&lt;br /&gt;
&lt;br /&gt;
== How it is going to be added to Expertiza ==&lt;br /&gt;
&lt;br /&gt;
We can add an &amp;quot;Evaluate using LLM&amp;quot; option to the '''dropdown menu''' alongside Review Report, Author Feedback Report, etc.&lt;br /&gt;
&lt;br /&gt;
When the instructor selects &amp;quot;Evaluate using LLM&amp;quot; and clicks View:&lt;br /&gt;
* Review data (responses, reviewer info, reviewee info, scores, comments, etc.) is gathered.&lt;br /&gt;
* The data is sent to an '''external API''' (currently stubbed with fake data).&lt;br /&gt;
* The returned evaluation is populated into an '''editable table view''' similar to the existing Review Report.&lt;br /&gt;
&lt;br /&gt;
=== Classes ===&lt;br /&gt;
* '''LlmEvaluationService''': Located in `app/services/llm_evaluation_service.rb`. Handles outbound API requests and inbound responses.&lt;br /&gt;
* '''ReportsController''': New method `llm_evaluation_report` added to generate the LLM evaluation page.&lt;br /&gt;
&lt;br /&gt;
=== Web Service(s) ===&lt;br /&gt;
* '''External API call''': Currently stubbed with fake data.&lt;br /&gt;
* '''View partial''': `_llm_evaluation_report.html.erb` created to display editable evaluations.&lt;br /&gt;
&lt;br /&gt;
== How much of it is implemented now ==&lt;br /&gt;
&lt;br /&gt;
The following parts are fully working:&lt;br /&gt;
* &amp;quot;Evaluate using LLM&amp;quot; dropdown option in the _searchbox.html.erb partial.&lt;br /&gt;
* Routing to the correct controller action (`llm_evaluation_report`).&lt;br /&gt;
* Service object (`LlmEvaluationService`) created to collect and send review data.&lt;br /&gt;
* Stubbed API returning a hardcoded response.&lt;br /&gt;
* Editable report table rendered via a new partial called _llm_evaluation_report.html.erb.&lt;br /&gt;
* &amp;quot;Overwrite&amp;quot; button added (placeholder functionality). Modify the submit method to overwrite the already existing grades and comments in the reviews_grade table with the LLM generated grades and comments.&lt;br /&gt;
&lt;br /&gt;
Thus, the feature '''end-to-end works with fake data''' for now.&lt;br /&gt;
&lt;br /&gt;
== How to continue development ==&lt;br /&gt;
&lt;br /&gt;
To complete this project:&lt;br /&gt;
* Connect to the '''real LLM API''' instead of fake responses.&lt;br /&gt;
* Work on the schema for reviews_grade table to incorporate the LLM generated scores and feedback in a new column.&lt;br /&gt;
* Implement '''saving''' functionality for the &amp;quot;Overwrite&amp;quot; button which will overwrite the grade_for_reviewer and comment_for_reviewer that already exists with the LLM generated grades and comments.&lt;br /&gt;
* Add robust '''error handling''' (for timeout, API errors, etc.).&lt;br /&gt;
* Extend support for '''multiple rounds''' and '''varying rubrics'''.&lt;br /&gt;
* Add '''RSpec tests''' for the service and controller logic.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Pull request details ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The pull request contains the following:&lt;br /&gt;
&lt;br /&gt;
*'''New dropdown entry: Evaluate using LLM'''  &lt;br /&gt;
A new option titled &amp;quot;Evaluate using LLM&amp;quot; has been added alongside existing report types (such as Review report, Author feedback report) in the reports searchbox partial (`_searchbox.html.erb`). This allows instructors and TAs to select the LLM-based evaluation report from the interface just like any other report type.&lt;br /&gt;
&lt;br /&gt;
*'''New controller action: llm_evaluation_report'''  &lt;br /&gt;
The `llm_evaluation_report` method has been introduced inside the `ReportsController`. This action is responsible for gathering the necessary assignment and participant data, invoking the service object to fetch LLM-evaluated responses, and rendering the new LLM Evaluation Report view for the instructor or TA.&lt;br /&gt;
&lt;br /&gt;
*'''New service object: LlmEvaluationService'''  &lt;br /&gt;
A service class `LlmEvaluationService` has been created under `app/services/llm_evaluation_service.rb`. This service collects review and reviewer information from the database, formats it into the expected API request payload, sends a POST request to an external (currently dummy) LLM evaluation API using `HTTParty`, and parses the received response into a structured format that can be easily rendered in the view.&lt;br /&gt;
&lt;br /&gt;
*'''Dummy API interaction using HTTParty'''  &lt;br /&gt;
For now, the API interaction is stubbed using a static JSON response that simulates an LLM’s feedback. This stubbed interaction ensures the full data flow is functional even without a live backend service. The dummy API response provides evaluation metrics such as reviewer scores, average scores, and LLM-generated comments for the reviews.&lt;br /&gt;
&lt;br /&gt;
*'''New view partial: _llm_evaluation_report.html.erb'''  &lt;br /&gt;
A new view partial `_llm_evaluation_report.html.erb` has been created to display the LLM evaluation report. The styling and table structure closely follow the traditional Review Report design. It presents reviewer names, the number of reviews completed, teams reviewed, scores (both awarded and average), review volume metrics, and editable input fields for grades and comments.&lt;br /&gt;
&lt;br /&gt;
*'''Editable fields populated with API-returned evaluation data'''  &lt;br /&gt;
The fields for assigning grades and writing comments are populated directly from the LLM-evaluated API data. These fields remain editable so that instructors and TAs can modify the suggested grades and comments before deciding to overwrite and save them manually if needed.&lt;br /&gt;
&lt;br /&gt;
*'''&amp;quot;Overwrite&amp;quot; button for future saving'''  &lt;br /&gt;
Each review entry now features an &amp;quot;Overwrite&amp;quot; button that is intended to allow instructors to save changes made to the LLM-suggested evaluations. Currently, this button is connected to a placeholder action, and future development will implement functionality to update and persist these evaluations into the database.&lt;br /&gt;
&lt;br /&gt;
*'''Updated routes and views to integrate the feature cleanly'''  &lt;br /&gt;
The `routes.rb` file was modified to define a route for the new `llm_evaluation_report` controller action. Additionally, `response_report.html.haml` was updated to render the `_llm_evaluation_report.html.erb` partial when the user selects the &amp;quot;Evaluate using LLM&amp;quot; report type, ensuring a seamless integration into the existing reporting infrastructure.&lt;br /&gt;
&lt;br /&gt;
The pull request provides a '''working prototype''' ready to be connected to a real API.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Design_for_Automatic_Evaluation_of_Peer_Reviews&amp;diff=166730</id>
		<title>Design for Automatic Evaluation of Peer Reviews</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Design_for_Automatic_Evaluation_of_Peer_Reviews&amp;diff=166730"/>
		<updated>2025-07-16T18:13:25Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* What we want to do */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Evaluate Using LLMs Integration =&lt;br /&gt;
&lt;br /&gt;
== What we want to do ==&lt;br /&gt;
&lt;br /&gt;
The goal of this project is to integrate LLMs with the peer-assessment process to improve learning.  This can be done in various ways, including&lt;br /&gt;
* Letting the LLM rate the submission and grade reviewers on how close their review is to the LLM’s (LLM as oracle).&lt;br /&gt;
* Using the LLM to read a review and give the reviewer advice on how to improve it (LLM as advisor).&lt;br /&gt;
* Using the LLM to rate the reviews and use that metric to weight reviewers’ score in calculating a grade (LLM as reputation system)&lt;br /&gt;
* Letting the LLM to do the review and ask reviewers (or authors) if they agree with the LLM and why or why not (LLM as opening salvo)&lt;br /&gt;
&lt;br /&gt;
Students learn best by doing reviews.  So, maybe you want the LLM to teach students to review.&lt;br /&gt;
&lt;br /&gt;
We also want to show instructors '''an evaluation of each reviewer’s overall reviewing performance'''.&lt;br /&gt;
Rather than looking at individual reviews, instructors will get a '''summary report''' describing how well each reviewer performed overall.&lt;br /&gt;
The evaluation includes aspects like quality of comments, score consistency, engagement, and other rubric-related metrics.&lt;br /&gt;
This report will be generated using a '''Large Language Model (LLM)''', such as GPT.&lt;br /&gt;
&lt;br /&gt;
Thus, instructors and TAs will be able to '''edit, overwrite, and finalize reviewer evaluation''' based on the LLM-generated suggestions.&lt;br /&gt;
&lt;br /&gt;
== How it is going to be added to Expertiza ==&lt;br /&gt;
&lt;br /&gt;
The &amp;quot;Evaluate using LLM&amp;quot; option has been added to the '''dropdown menu''' alongside Review Report, Author Feedback Report, etc.&lt;br /&gt;
&lt;br /&gt;
When the instructor selects &amp;quot;Evaluate using LLM&amp;quot; and clicks View:&lt;br /&gt;
* Review data (responses, reviewer info, reviewee info, scores, comments, etc.) is gathered.&lt;br /&gt;
* The data is sent to an '''external API''' (currently stubbed with fake data).&lt;br /&gt;
* The returned evaluation is populated into an '''editable table view''' similar to the existing Review Report.&lt;br /&gt;
&lt;br /&gt;
=== Classes ===&lt;br /&gt;
* '''LlmEvaluationService''': Located in `app/services/llm_evaluation_service.rb`. Handles outbound API requests and inbound responses.&lt;br /&gt;
* '''ReportsController''': New method `llm_evaluation_report` added to generate the LLM evaluation page.&lt;br /&gt;
&lt;br /&gt;
=== Web Service(s) ===&lt;br /&gt;
* '''External API call''': Currently stubbed with fake data.&lt;br /&gt;
* '''View partial''': `_llm_evaluation_report.html.erb` created to display editable evaluations.&lt;br /&gt;
&lt;br /&gt;
== How much of it is implemented now ==&lt;br /&gt;
&lt;br /&gt;
The following parts are fully working:&lt;br /&gt;
* &amp;quot;Evaluate using LLM&amp;quot; dropdown option in the _searchbox.html.erb partial.&lt;br /&gt;
* Routing to the correct controller action (`llm_evaluation_report`).&lt;br /&gt;
* Service object (`LlmEvaluationService`) created to collect and send review data.&lt;br /&gt;
* Stubbed API returning a hardcoded response.&lt;br /&gt;
* Editable report table rendered via a new partial called _llm_evaluation_report.html.erb.&lt;br /&gt;
* &amp;quot;Overwrite&amp;quot; button added (placeholder functionality). Modify the submit method to overwrite the already existing grades and comments in the reviews_grade table with the LLM generated grades and comments.&lt;br /&gt;
&lt;br /&gt;
Thus, the feature '''end-to-end works with fake data''' for now.&lt;br /&gt;
&lt;br /&gt;
== How to continue development ==&lt;br /&gt;
&lt;br /&gt;
To complete this project:&lt;br /&gt;
* Connect to the '''real LLM API''' instead of fake responses.&lt;br /&gt;
* Work on the schema for reviews_grade table to incorporate the LLM generated scores and feedback in a new column.&lt;br /&gt;
* Implement '''saving''' functionality for the &amp;quot;Overwrite&amp;quot; button which will overwrite the grade_for_reviewer and comment_for_reviewer that already exists with the LLM generated grades and comments.&lt;br /&gt;
* Add robust '''error handling''' (for timeout, API errors, etc.).&lt;br /&gt;
* Extend support for '''multiple rounds''' and '''varying rubrics'''.&lt;br /&gt;
* Add '''RSpec tests''' for the service and controller logic.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Pull request details ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The pull request contains the following:&lt;br /&gt;
&lt;br /&gt;
*'''New dropdown entry: Evaluate using LLM'''  &lt;br /&gt;
A new option titled &amp;quot;Evaluate using LLM&amp;quot; has been added alongside existing report types (such as Review report, Author feedback report) in the reports searchbox partial (`_searchbox.html.erb`). This allows instructors and TAs to select the LLM-based evaluation report from the interface just like any other report type.&lt;br /&gt;
&lt;br /&gt;
*'''New controller action: llm_evaluation_report'''  &lt;br /&gt;
The `llm_evaluation_report` method has been introduced inside the `ReportsController`. This action is responsible for gathering the necessary assignment and participant data, invoking the service object to fetch LLM-evaluated responses, and rendering the new LLM Evaluation Report view for the instructor or TA.&lt;br /&gt;
&lt;br /&gt;
*'''New service object: LlmEvaluationService'''  &lt;br /&gt;
A service class `LlmEvaluationService` has been created under `app/services/llm_evaluation_service.rb`. This service collects review and reviewer information from the database, formats it into the expected API request payload, sends a POST request to an external (currently dummy) LLM evaluation API using `HTTParty`, and parses the received response into a structured format that can be easily rendered in the view.&lt;br /&gt;
&lt;br /&gt;
*'''Dummy API interaction using HTTParty'''  &lt;br /&gt;
For now, the API interaction is stubbed using a static JSON response that simulates an LLM’s feedback. This stubbed interaction ensures the full data flow is functional even without a live backend service. The dummy API response provides evaluation metrics such as reviewer scores, average scores, and LLM-generated comments for the reviews.&lt;br /&gt;
&lt;br /&gt;
*'''New view partial: _llm_evaluation_report.html.erb'''  &lt;br /&gt;
A new view partial `_llm_evaluation_report.html.erb` has been created to display the LLM evaluation report. The styling and table structure closely follow the traditional Review Report design. It presents reviewer names, the number of reviews completed, teams reviewed, scores (both awarded and average), review volume metrics, and editable input fields for grades and comments.&lt;br /&gt;
&lt;br /&gt;
*'''Editable fields populated with API-returned evaluation data'''  &lt;br /&gt;
The fields for assigning grades and writing comments are populated directly from the LLM-evaluated API data. These fields remain editable so that instructors and TAs can modify the suggested grades and comments before deciding to overwrite and save them manually if needed.&lt;br /&gt;
&lt;br /&gt;
*'''&amp;quot;Overwrite&amp;quot; button for future saving'''  &lt;br /&gt;
Each review entry now features an &amp;quot;Overwrite&amp;quot; button that is intended to allow instructors to save changes made to the LLM-suggested evaluations. Currently, this button is connected to a placeholder action, and future development will implement functionality to update and persist these evaluations into the database.&lt;br /&gt;
&lt;br /&gt;
*'''Updated routes and views to integrate the feature cleanly'''  &lt;br /&gt;
The `routes.rb` file was modified to define a route for the new `llm_evaluation_report` controller action. Additionally, `response_report.html.haml` was updated to render the `_llm_evaluation_report.html.erb` partial when the user selects the &amp;quot;Evaluate using LLM&amp;quot; report type, ensuring a seamless integration into the existing reporting infrastructure.&lt;br /&gt;
&lt;br /&gt;
The pull request provides a '''working prototype''' ready to be connected to a real API.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
</feed>