08 — Form Specifications (Exams Module)
- 1. Create Exam Form (S8)
- 2. Edit Exam Form (S8a)
- 3. Add Subject Slot Form (S4)
- 4. Enter Marks Form (S3, per row)
- 5. Validation error mapping (all forms)
Every form field, field-by-field: label, widget, keyboard, validation, errors, defaults. All validations mirror the DTO decorators exactly — anything the server does not validate is marked client-only (see 01_Product_Overview.md §10 OQ-2/OQ-3/OQ-6). DTO sources:
examination.dto.ts,examination-subject.dto.ts. Error codes per 00-shared/07 §3.
1. Create Exam Form (S8)
Submit: POST /api/v1/examinations (examination.controller.ts:27-29);
payload = CreateExaminationDto (examination.dto.ts:4-26). Server forces
status: 'draft' (examination.service.ts:48).
| # | Field | Label | Widget | Keyboard | Server validation | Client-only validation | Default | Error |
|---|---|---|---|---|---|---|---|---|
| 1 | academicYearId | Academic year | AppDropdown | — | @IsMongoId (examination.dto.ts:6-7) | — | none (required) | "Select the academic year" / 400 VALIDATION_ERROR |
| 2 | name | Exam name | AppTextField | text (autocorrect off) | @IsString (examination.dto.ts:10-11) | required, ≤100 chars, trim | — | "Enter the exam name" |
| 3 | type | Exam type | AppDropdown | — | @IsString (no enum decorator, examination.dto.ts:13-17; schema enum ['midterm','final','unit_test','quarterly','other'], examination.schema.ts:15-20 — invalid values fail at save() as a validator error) | picker constrained to the 5 values | — | "Choose a type" |
| 4 | startDate | Start date | AppDatePicker | — | @IsDateString (examination.dto.ts:19-21) | client-only: ≤ endDate (OQ-6) | — | "Start date must be on or before end date" |
| 5 | endDate | End date | AppDatePicker | — | @IsDateString (examination.dto.ts:23-25) | client-only: ≥ startDate (OQ-6) | — | "End date must be on or after start date" |
Submit behavior: disabled until required fields valid; spinner on submit; on 400
VALIDATION_ERROR → inline field errors (focus first invalid); success → 201 doc →
pop to S2 with snackbar "Exam created".
Offline: form is online-only; unsaved draft persisted to local draft box on
back (optional, (proposed)).
2. Edit Exam Form (S8a)
Submit: PATCH /api/v1/examinations/:id (examination.controller.ts:36-41);
payload = UpdateExaminationDto (examination.dto.ts:28-53) — all optional
(@IsOptional), only changed fields sent ($set, base.repository.ts:57-66).
Prefill from GET /examinations/:id; academicYearId locked (not in DTO).
| # | Field | Server validation | Client rules |
|---|---|---|---|
| 1 | name? | @IsOptional @IsString | as §1 |
| 2 | type? | @IsOptional @IsString | picker only |
| 3 | startDate? / endDate? | @IsOptional @IsDateString | window rule as §1 |
| 4 | status? | @IsOptional @IsString — free string, schema enum NOT enforced on PATCH (updateById without runValidators, base.repository.ts:57-66; OQ-5) | client only ever sends an explicit draft → active transition (OQ-1); never published; once published, edit unreachable (immutability contract) |
Errors: 400 field errors; 404 "Examination not found." (examination.service.ts:87)
→ pop + snackbar "This exam no longer exists."
3. Add Subject Slot Form (S4)
Submit: POST /api/v1/examinations/:id/subjects (examination.controller.ts:45-50;
examinationId injected from route, examination.service.ts:49); payload =
CreateExaminationSubjectDto (examination-subject.dto.ts:11-45).
| # | Field | Label | Widget | Keyboard | Server validation | Client-only validation | Default | Error |
|---|---|---|---|---|---|---|---|---|
| 1 | subjectId | Subject | AppDropdown | — | @IsMongoId (examination-subject.dto.ts:16-18) | client-only: warn "already scheduled" if duplicate (no server dedupe — OQ-3) | — | "Select a subject" |
| 2 | classId | Class | AppDropdown | — | @IsMongoId (examination-subject.dto.ts:20-22) | — | — | "Select a class" |
| 3 | date | Exam date | AppDatePicker | — | @IsDateString (examination-subject.dto.ts:24-26) | client-only: within exam startDate..endDate; warn overlap with existing slot times (OQ-2) | — | "Date must be within the exam window" |
| 4 | startTime | Start time | AppTextField | TextInputType.datetime (HH:mm) | @IsString (examination-subject.dto.ts:28-30; example '09:00') | required, HH:mm pattern | 09:00 | "Enter a valid time (HH:mm)" |
| 5 | endTime | End time | AppTextField | datetime | @IsString (examination-subject.dto.ts:31-34) | client-only: endTime > startTime (OQ-6) | 12:00 | "End time must be after start time" |
| 6 | maximumMarks | Maximum marks | AppTextField | number (digits) | @IsNumber @Min(1) (examination-subject.dto.ts:36-39) | client-only: ≥ passingMarks; sane cap 1000 | 100 | "Maximum marks must be at least 1" |
| 7 | passingMarks | Passing marks | AppTextField | number | @IsNumber @Min(1) (examination-subject.dto.ts:41-44) | client-only: ≤ maximumMarks (OQ-6) | 33 | "Passing marks cannot exceed maximum marks" |
Conflict dialog: on overlap/duplicate detection (fields 1–5), submit shows
AppDialog "Overlaps with Subject X 09:00–12:00 — add anyway?" (server won't
reject; OQ-2/OQ-3).
Errors: 400 VALIDATION_ERROR inline; 404 "Exam subject not found." — n/a here;
exam missing → 404 → pop.
4. Enter Marks Form (S3, per row)
Submit: POST /api/v1/results/exam-subject/:examSubjectId/marks
(result.controller.ts:26-31); payload = EnterMarksDto
(examination-subject.dto.ts:47-66). Upsert: an existing
(studentId, examinationSubjectId) doc is updated in place
(examination.service.ts:147-175; unique index examination-result.schema.ts:31-33).
| # | Field | Label | Widget | Keyboard | Server validation | Client-only validation | Default | Error |
|---|---|---|---|---|---|---|---|---|
| 1 | studentId | (hidden) | filled from roster row | — | @IsMongoId (examination-subject.dto.ts:49-51) | — | roster student | — |
| 2 | marksObtained | Marks | MarksTextField | number, digits only | @IsNumber @Min(0) (examination-subject.dto.ts:52-55) | critical: marksObtained ≤ maximumMarks of the slot — server rejects with 404 NotFoundException('Marks cannot exceed maximum.') (examination.service.ts:145-146), not 422 | — | inline "Marks cannot exceed {max}"; on server 404 → refresh slot max, re-enter |
| 3 | grade? | Grade | GradeChip → grade sheet (text) | text | @IsOptional @IsString (examination-subject.dto.ts:57-60) — free text, never computed server-side per subject (OQ-9) | optional; ≤10 chars | — | — |
| 4 | remarks? | Remarks | AppTextField multiline (2–3 lines) | text | @IsOptional @IsString (examination-subject.dto.ts:62-65) | optional; ≤500 chars | — | — |
Row submit cadence: save on focus-loss or explicit save tap; retry per failed
row; offline → row queued (see 10_Interaction_Specification.md §3,
13_State_Management.md §4). Marks blank → not submitted (no clear endpoint);
client offers "Clear" by submitting 0 with confirm ("0 is stored — server treats
missing marks as 0 in report cards", result.service.ts:70).
5. Validation error mapping (all forms)
| Server code | Client handling |
|---|---|
400 VALIDATION_ERROR | inline field errors (details[] per 00-shared/07 §3), focus first invalid |
| 401 / 403 | session flow / permission screen (00-shared/06 §3.6) |
404 RESOURCE_NOT_FOUND | resource-gone state (marks: "Marks cannot exceed maximum." is 404 — special-cased, examination.service.ts:146) |
409 DUPLICATE_RESOURCE | not expected today (no unique constraints on exams/slots, OQ-3) |
429 RATE_LIMITED | backoff, no auto-retry (00-shared/07 §4) |
| 5xx | generic + requestId, retry |