EduTrack Online — Frontend Build Order (Phase 1: SiteOwner)

Overview

Build these pages in order. Each page depends on the previous one.


Page 1: Login Page

APIs to use (from API_PUBLIC.md):

Action Method Endpoint Purpose
Login POST /accounts/login/ Authenticate user, returns JWT cookies + {role, name, is_active}
Forgot Password POST /accounts/forgot-password/ Send OTP to email (always returns 200)
Verify OTP POST /accounts/verify-otp/ Verify 6-digit code, get reset_token
Reset Passwor d POST /accounts/reset-password/ Set new password with OTP + reset_token

Flow:

  1. User enters username + password → POST /accounts/login/
  2. If 200 → store {role, name} in React state, redirect to Teachers Page (if role=siteowner)
  3. If 401 → show error message
  4. "Forgot Password?" link → step-by-step: email → OTP → new password

Required: Login form, Forgot Password modal/flow.


Page 2: Student Register Page (Public)

APIs to use (from API_PUBLIC.md):

Action Method Endpoint Purpose
Verify email POST /accounts/verify-email/request/ Request OTP for email verification
Confirm email POST /accounts/verify-email/confirm/ Verify OTP, get verification_token
Check field availability GET /accounts/check-field/?field=username&value=john Real-time check if username/email/phone is taken
Verify email POST /accounts/verify-email/request/ Request OTP for email verification
Confirm email POST /accounts/verify-email/confirm/ Verify OTP, get verification_token
Register POST /accounts/student/register/ Create student account (pending approval)

3-Step Registration Flow:

Step 1: POST /verify-email/request/ { email } → OTP sent to email Step 2: POST /verify-email/confirm/ { email, otp } → Returns { verified, email, verification_token } → Save verification_token in React state Step 3: POST /accounts/student/register/ { verification_token, gmail, ...all fields } → Server validates token matches gmail → Creates student if valid

Step 3 form fields:

Dropdown data: Fetch from Settings endpoints (see Page 6).

Business rules:


Page 3: Teachers Page (SiteOwner Dashboard)

This is the main landing page after login. Shows all teachers as cards.

APIs to use (from API_SITEOWNER.md):

Action Method Endpoint Purpose
List teachers GET /accounts/teachers/ Paginated list with search, filter
Create teacher POST /accounts/teachers/ Create new teacher account

Teacher Card shows:

Teacher List Filters:

Teacher Create Form fields:

Grade filtering logic: The GET /accounts/subjects/ response includes grade_ids for each subject. When the user selects a subject, filter the grades multi-select to only show grades whose IDs are in subject.grade_ids:

const subject = subjects.find(s => s.id === selectedSubjectId); const validGrades = allGrades.filter(g => subject.grade_ids.includes(g.id));

If no subject is selected, disable the grades field with "Select a subject first".

Backend validation: If a grade outside the subject's range is submitted, the API returns 400 with {"grades": ["Subject 'Math' is not available for grade '1st Secondary'."]}.

Success response: Returns full teacher profile with ID. Use this to navigate to the new teacher's detail.


Page 4: Teacher Detail Page (View/Edit Teacher)

Clicking on a teacher card or the edit button from the Teachers Page.

APIs to use:

Action Method Endpoint Purpose
Get teacher GET /accounts/teachers/<id>/ Full teacher detail with subject/grades, bunny_collection_id
Update teacher PATCH /accounts/teachers/<id>/ Update teacher fields (including bunny_collection_id and optional password)
Create collection POST /accounts/teachers/<id>/create-collection/ Auto-create a Bunny collection and assign it
List assistants GET /accounts/teacher/assistants/?teacher=<id> View assistants for this teacher

Teacher Detail sections:

  1. Profile Info — name, phone, gmail, subjects, grades, active status, bunny_collection_id. Editable.
  2. Bunny Collection — shows current bunny_collection_id (if assigned). Button to assign a collection: PATCH /accounts/teachers/<id>/ with { "bunny_collection_id": "..." }. Or create one via POST /accounts/teachers/<id>/create-collection/.
  3. Assistants List — read-only table showing assigned assistants (name, phone, gmail, is_active). Note: assistants are created later by the teacher themselves.
  4. Create Course Button → opens a form to create a course for this teacher.

Course Create Form (inline):

Success: After creating a course, the page should show a success message and optionally navigate to the Teacher Courses Page.


Page 5: Teacher Courses Page

List all courses for a specific teacher. Accessed via "View Courses" button on teacher card.

APIs to use:

Action Method Endpoint Purpose
List courses GET /courses/?teacher=<id> Paginated list of courses for this teacher
Get course GET /courses/<id>/ Full course detail
Update course PATCH /courses/<id>/ Update course name/description/active
Delete course DELETE /courses/<id>/ Delete empty course (no enrollments/topics/lectures)

Course Card shows:

Course Edit Form:

Course Delete rules:


Page 6: Students Page (SiteOwner)

Manage all student registrations. Approve/decline/suspend students. View full profile with enrollments, per-course balances, and transaction history per course.

APIs to use:

Action Method Endpoint Purpose
List students GET /accounts/students/ Paginated list with search, sort, filters
Get student (with enrollments) GET /accounts/students/<id>/ Full student detail + enrollments[] array (course, teacher, status, balance)
Update student (status + profile) PATCH /accounts/students/<id>/ Update status + profile fields in one call
Transaction history GET /payments/transactions/?student=X&course=Y Full ledger for a specific student + course (requires both params)

Student List Filters:

Student Table columns:

Student Detail view (3 sections):

Section 1 — Profile Info:

Section 2 — Status Management:

Section 3 — Transaction History (per course):

Default view: Show pending students first (sort by most recent).


Page 7: Settings Page (Read-Only Reference)

Display all reference data for preview. No CRUD operations.

APIs to use (from API_PUBLIC.md):

Action Endpoint Purpose
List grades GET /accounts/grades/
List school types GET /accounts/school-types/
List divisions GET /accounts/divisions/
List subjects GET /accounts/subjects/
List governorates GET /accounts/governorates/
List areas GET /accounts/areas/
List video security GET /accounts/video-security/

Note: All these endpoints support ?all=true to bypass pagination and get complete lists for dropdowns.


Static Files Needed

No static files are served by the backend. The frontend should handle all assets (images, icons, logos) on its own.


Auth Flow Summary

1. POST /accounts/login/ with {username, password} 2. Server sets httpOnly cookies: access_token (30min), refresh_token (7d) 3. Server returns JSON: {role: "siteowner", name: "Admin", is_active: true} 4. Frontend stores {role, name} in React context/state 5. All subsequent requests automatically send cookies (browser handles this) 6. When access_token expires → POST /accounts/token/refresh/ (reads refresh cookie) 7. POST /accounts/logout/ to clear cookies

Note: Since cookies are httpOnly, the frontend CANNOT read the JWT. Session restoration works via GET /accounts/me/ which reads the cookie and returns {role, name, is_active}.


Error Handling

All API errors follow this pattern:

{"error": "Human-readable error message"}

HTTP Status codes:


Public (SEO) Endpoints — Landing / Teacher Listing / Subject Catalog Pages

Anonymous browsing is served by dedicated public endpoints — GET /courses/ and GET /courses/<id>/ now require authentication in all cases (even with ?teacher=), so the public pages must use these:

Action Endpoint Returns
Platform stats (landing hero) GET /accounts/public/stats/ {teacher_count, course_count, subject_count} — no student data
Public course list (catalog, filters subject/teacher/grade + search) GET /courses/public/ names, descriptions, covers, teacher/grade/subject, topic_count, total_lecturesno prices, no videos
Public course detail (SEO course page) GET /courses/public/<id>/ topics + lecture names only, topic pictures — no prices, no video IDs
Public teacher list GET /accounts/public/teachers/ includes courses_count (active courses)

Rules for the public pages: hide any prices/lecture counts behind login; the "teacher → their courses" drill-down on the public teacher page uses GET /courses/public/?teacher=<id>; a logged-in student's version of the same pages uses the authenticated endpoints (/courses/, /courses/by-subject/<id>/, /courses/<id>/preview/), which are eligibility-filtered and include eligibility.


Phase 2: Teacher & Assistant Pages

Build these in order after Phase 1 is complete.

IMPORTANT: All pages in Phase 2 apply to both Teacher and Assistant roles unless noted. The only exception is the Assistants management page — assistants can view coworkers but cannot create/delete.

Dashboards (Course Dashboard, Teacher Dashboard) are built last — after all features exist.


Page 1: My Courses Page (Entry Point)

Teacher logs in → POST /accounts/login/ → JWT cookies set → GET /accounts/me/ → redirects to My Courses.

Page layout: Grid of course cards, each showing:

Action Method Endpoint
My courses GET /courses/

Page 2: Content Management (Topics → Lectures → Videos)

Purpose: The core page where teachers build their course content hierarchy. Teacher spends most time here.

Navigation: Select course → Topics list → Click topic → Lectures list → Click lecture → Lecture Detail

Page layout (left-to-right hierarchy):

Course: Chemistry 3rd Secondary ┌────────────────────────────────────────────────┐ │ Unit 1: Chemical Reactions [+ Topic] │ │ ├── Lecture 1: Intro [3 videos] │ │ ├── Lecture 2: Balancing [2 videos] │ │ └── + Add Lecture │ │ │ │ Unit 2: Organic Chemistry │ │ ├── Lecture 3: Hydrocarbons [0 videos] │ │ └── + Add Lecture │ └────────────────────────────────────────────────┘

Topic CRUD:

Action Endpoint
Create POST /courses/topics/ { course, name, description, order }
Update PUT /courses/topics/<id>/
Delete DELETE /courses/topics/<id>/ (blocked if has lectures)

Lecture CRUD:

Action Endpoint
Create POST /courses/lectures/ { topic, name, price, discount, available_days, description }
Update PUT /courses/lectures/<id>/ (blocked if is_visible=false & active purchases exist)
Delete DELETE /courses/lectures/<id>/ (blocked if any purchases exist)

Click a lecture → Lecture Detail page with sub-tabs:

← Back to Content Lecture: Intro to Chemical Reactions Price: 50 EGP | Discount: 10 EGP | Available: 30 days [ Videos ] [ Purchases ] [ Homework ] [ Quiz ] [ Materials ] [ Prerequisites ]

Each sub-tab is built when its corresponding page below is completed.

Video tab (built here):

Action Endpoint
List videos GET /courses/videos/?lecture=<id>
Create video POST /courses/videos/ { lecture, name, order }
Get video GET /courses/videos/<id>/
Delete video DELETE /courses/videos/<id>/
Create Bunny upload POST /courses/videos/<id>/create-upload/

Bunny Upload Flow (TUS Direct Upload):

  1. Teacher clicks "Add Video" → modal: name, order → Video record created (bunny_video_id=null, status="Pending")
  2. Teacher clicks "Upload" on the pending row → POST .../create-upload/ → returns TUS credentials
  3. Frontend uploads directly to Bunny using tus-js-client:
  4. (Optional) Call POST /courses/videos/<id>/upload-complete/ after TUS onSuccess — faster status detection, but not required
  5. Frontend polls GET /courses/videos/<id>/ every 5 seconds — self-healing sync (backend checks Bunny API on each poll)
  6. UI shows live status: Created → Uploaded → Processing → Transcoding → Ready ✅
  7. When a ready status is reached (3/4), is_active=true, teacher sees "Ready" badge

TUS handles pause/resume nativelyfindPreviousUploads() + resumeFromPreviousUpload() stores progress in localStorage and resumes at the exact byte. Use 5 MB chunks (chunkSize: 5 * 1024 * 1024) and add 300ms delay between chunks via onChunkComplete to prevent WiFi saturation.

Note: The old POST /upload/ proxy endpoint has been removed (returns 410 Gone).

Video List UI Table: # │ Name │ Status │ Actions ──┼───────────────┼───────────────────────────────────┼──────────── 1 │ Intro │ ✅ Ready │ [Preview] [Delete] 2 │ Types │ ⬆️ ████████░░ 80% Uploading │ [Pause] [Cancel] 3 │ Graphs │ 📝 Pending (not uploaded yet) │ [Upload] [Delete] 4 │ Advanced │ ❌ Error — encoding failed │ [Retry] [Delete]

Playback Preview (teacher): GET /courses/videos/<id>/play/ returns Bunny Stream HLS URL. Teacher can preview own videos.

Student Preview toggle: Add ?as_student=true to any course/topic/lecture detail endpoint to see exactly what students see (only active topics, visible lectures, active videos).


Page 3: Lecture Detail — Students Progress Tab

Purpose: See ALL enrolled students for this lecture — who bought it, who didn't, their homework/quiz scores, watch progress, and reopen expired purchases.

Single API call: GET /courses/lectures/<id>/students-progress/

Reopen purchase: PATCH /courses/purchases/<id>/reopen/ (no body, 1 day, max 2)

The students-progress/ endpoint returns everything in one response (see API_TEACHER.md §8 for full response).

Pagination: Supports ?page= and ?page_size= (default 50, max 200). Response includes count, next, previous, results. Add "Load More" / page navigation when next is not null.

Top-level arrays:

Field Use
homeworks[] Lists ALL homeworks in this lecture: {id, title, total}
quizzes[] Lists ALL quizzes in this lecture: {id, title, total, max_attempts}

Per-student homeworks[] and quizzes[] always match the length and order of these top-level arrays. Empty array = no assessments of that type.

UI Layout (summary table):

← Back to Lecture Detail Lecture: Intro to Reactions Course: Chemistry 3rd Secondary — Teacher: Dr Hany ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Total enrolled: 25 students | Purchased: 20 | Not purchased: 5 [Search by student name or code...] (client-side filter) Student │ Code │ Bought │ Paid │ Watch │ Actions ──────────────┼─────────┼──────────┼──────────┼────────────┼───────────── Ahmed Ali │ 1234567 │ ✅ Yes │ 40 EGP │ 2/3 (67%) │ [View] Sara Ahmed │ 7654321 │ ❌ No │ — │ — │ [View] Mohamed Omar │ 1122334 │ ✅ Yes │ 40 EGP │ 3/3 (100%) │ ✅ Active Nora Hassan │ 9988776 │ ✅ Yes │ 40 EGP │ 0/3 (0%) │ [View] Khaled Ali │ 5544332 │ ✅ Yes │ 40 EGP │ 1/3 (33%) │ 🔴 Expired [Reopen] Page 1 of 2 — [Next ►]

Each column explained:

Column Source
Student student_name
Code student_code (7-digit code)
Bought is_purchased → ✅ Yes / ❌ No
Paid purchase.amount_paid (frozen snapshot, never changes)
Watch watch.watched_count / watch.total_videos (watch.percentage%)
Actions Based on state (see render logic below)

Render logic:

State Display
Not purchased (is_purchased=false) Gray row, "Not purchased" badge, [View] button
Purchased + Active (is_expired=false) Green "Active" badge, [View] button
Purchased + Expired + can reopen (is_expired=true, can_reopen=true) Red "Expired" badge, [Reopen] button (no [View] — reopen is the primary action)
Purchased + Expired + max reopens (can_reopen=false) Red "Expired (max reopens)" text, [View] button
No videos (total_videos=0) Watch column shows "—"

[View] button opens a modal/drawer with full student detail (see below). [Reopen] opens a confirmation dialog (see reopen flow below).

Reopen flow:

  1. Teacher clicks [Reopen] on an expired row
  2. Confirmation dialog: student name, current expiry (expires_at), reopens used (reopen_logs.length), remaining (2 - logs.length), sessions_used will be reset to 0
  3. Confirm ? PATCH /courses/purchases/<id>/reopen/ (no body)
  4. On success ? re-fetch students-progress/ for that page
  5. Access is restored for ~1 day from now (expires_at = max(expiry, now) + 1 day, extra_days resets to 0) — reopening a lecture expired long ago actually works now. is_expired recalculates, can_reopen updates, sessions_used is reset to 0 so the student can start new viewing sessions

[View] Modal — Full Student Detail

When the teacher clicks [View] on any row, a modal/drawer opens showing everything about that student for this lecture. No additional API call needed — all data is already in the students-progress/ response.

Modal layout:

┌─────────────────────────────────────────────────────────────────┐ │ Student: Ahmed Ali (1234567) [X] │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ── Purchase Info ──────────────────────────────────────────── │ │ Status: Purchased ✅ │ │ Paid: 40.00 EGP │ │ Purchased: July 1, 2026 │ │ Expires: August 2, 2026 (1 reopen used, 1 remaining) │ │ Watched: 2/3 videos (67%) │ │ │ │ ── Homeworks ──────────────────────────────────────────────── │ │ Week 1 HW │ ✅ Submitted │ Score: 2/3 │ Jul 1, 2026 │ │ Week 2 HW │ ❌ Not done │ — │ — │ │ │ │ ── Quizzes ────────────────────────────────────────────────── │ │ Quiz 1 │ ❌ Not done │ — │ Attempt 0/1 │ │ │ └─────────────────────────────────────────────────────────────────┘

Modal sections:

Section Content Data Source
Student header Name + code student_name, student_code
Purchase Info Status, paid amount, dates, reopen info, sessions used/remaining purchase object
Homeworks Table: title, submitted?, score/total, submission date homeworks[] per-student + homeworks[] top-level for total
Quizzes Table: title, submitted?, score/total, attempt#/max quizzes[] per-student + quizzes[] top-level for total/max_attempts

Homework table columns:

Column Source
Title homeworks[i].title
Status "✅ Submitted" if submitted=true, "❌ Not done" if false
Score score / total from top-level homeworks[i].total (null = "—")
Submission date If submitted, fetch from GET /learning/homework-submissions/<submission_id>/ or show "—"

Quiz table columns:

Column Source
Title quizzes[i].title
Status "✅ Submitted" if submitted=true, "❌ Not done" if false
Score score / total from top-level quizzes[i].total (null = "Not graded" if submitted, or "—")
Attempt attempt / max_attempts (from quizzes[i].max_attempts) — quizzes are multi-attempt; exams are single

Empty states inside modal:

Empty states for the main table:


Page 4: Homework Bubble Sheet (Standalone + Lecture Tab)

Purpose: Create bubble-sheet homework with questions. View submissions with per-question results.

Standalone management page: Table of all homeworks across courses with filters (?lecture=, ?is_published=).

Lecture tab: Same component, pre-filtered by ?lecture=<id>.

Action Endpoint
List GET /learning/homeworks/
Create POST /learning/homeworks/
Get GET /learning/homeworks/<id>/
Update PUT /learning/homeworks/<id>/
Delete DELETE /learning/homeworks/<id>/
List submissions GET /learning/homeworks/<id>/submissions/
Submission detail GET /learning/homework-submissions/<id>/

Create form:

Title: [Week 1 Homework______________] Description: [________________] Published: [ ] (toggle) Show grades to students: [ ] (toggle — when on, students see correct answers after submission) Bubble Questions: Q1: Choices [4 ▼] | Answer [A] | Explanation (optional): [_________] | Points: [1] Q2: Choices [5 ▼] | Answer [B] | Explanation (optional): [_________] | Points: [2] + Add Question

Bubble Question fields:

Request body:

{ "lecture": 5, "title": "Week 1 HW - Chapter 1", "description": "Chapter 1 exercises", "is_published": false, "bubble_questions": [ {"order": 1, "choices_count": 4, "correct_answer": "A", "answer_explanation": "Optional", "points": 1}, {"order": 2, "choices_count": 5, "correct_answer": "A,C", "answer_explanation": "", "points": 2} ] }

Submissions table (teacher view):

Student │ Code │ Score │ Status │ Submitted ─────────────┼─────────┼───────┼────────┼────────────── Ahmed Ali │ S-001 │ 2/3 │ Graded │ 2026-07-08 Sara Ahmed │ S-002 │ 3/3 │ Graded │ 2026-07-07

Click a submission → per-question results:

Q1: Student answered B | Correct: A | ❌ Wrong | Explanation: Because... Q2: Student answered B | Correct: B | ✅ Correct

Page 5: Quizzes (Standalone + Lecture Tab)

Purpose: Create quizzes with standalone questions (text + image + choices). View submissions, manually grade written answers, release scores/answers.

Action Endpoint
List GET /learning/quizzes/
Create POST /learning/quizzes/ { lecture, title, settings }
Get GET /learning/quizzes/<id>/ (with questions)
Update PUT /learning/quizzes/<id>/
Delete DELETE /learning/quizzes/<id>/
Update question image PATCH /learning/questions/<id>/ (multipart — image field)
Update question text PATCH /learning/questions/<id>/ (JSON — text, question_type, points_override)
List submissions GET /learning/quizzes/<id>/results/
Grade written POST /learning/quiz-submissions/<id>/grade-written/
Release scores POST /learning/quizzes/<id>/release-scores/
Release answers POST /learning/quizzes/<id>/release-answers/
Start quiz POST /learning/quizzes/<id>/start/
Resume quiz GET /learning/quizzes/<id>/resume/
Submit quiz POST /learning/quizzes/<id>/submit/
Save draft answer PATCH /learning/quiz-answers/<answer_id>/save-draft/
Student quiz history GET /learning/quizzes/
Delete submission (retake) DELETE /learning/quiz-submissions/<pk>/delete/

Settings form (5 fields only):

{ "timer_minutes": 30, "score_visibility": "immediate", "answers_visibility": "immediate", "question_order": "fixed", "max_attempts": 1 }
Field Values Default Description
timer_minutes 0–1440 0 0 = no limit. Max 1440 (24h).
score_visibility immediate, manual immediate manual = teacher must release scores
answers_visibility immediate, manual immediate manual = teacher must release correct answers
question_order fixed, random fixed Shuffle question order per student
max_attempts 1+ 1 Enforced for quizzes — how many attempts each student gets. When used up, the teacher must delete submissions to free a slot. (Exams ignore this — always single-attempt.)

Multi-attempt enforcement (quizzes): Students can start a new quiz attempt while one is in progress (the backend returns the existing in-progress attempt), and can submit up to max_attempts times. When all attempts are used, POST /start/ returns 400 "You have used all your allowed attempts...". The teacher frees a slot by deleting a submission via DELETE /learning/quiz-submissions/<pk>/delete/. Exams remain single-attempt — delete the submission to allow a retake.

Note on is_active: This field exists on the model for frontend hide/show toggling but is not enforced by the backend. Only is_published gates student access. Set is_published=true when the quiz is ready for students.

Questions: Standalone only (inline text + MCQ choices + written).
Images: NOT uploadable via JSON during inline creation. Upload question images separately via PATCH /learning/questions/<id>/ with multipart/form-data (image field). The response always returns the image as a full URL.

Sending questions inline during create/update:

{ "lecture": 5, "title": "Quiz Title", "settings": { ... }, "questions": [ { "text": "What is 2+2?", "question_type": "mcq_single", "order": 1, "points_override": 2, "choices": [ {"text": "4", "is_correct": true, "order": 1}, {"text": "5", "is_correct": false, "order": 2} ] } ] }

CAUTION: Sending questions on update (PUT/PATCH /quizzes/<id>/) upserts by question id: existing questions (with matching id) are updated in-place, new questions (no id) are created, questions absent from the payload are deleted. For auto-save without risking deletion, use individual PATCH /questions/<id>/ instead.

Limits: a quiz can have max 300 questions (create and update — 400 otherwise); each question max 20 points (points_override, null = 1).


Page 6: Exams (Standalone Course-Level)

Purpose: Create exams independent of lectures. Can be used as prerequisites.

Action Endpoint
List GET /learning/exams/
Create POST /learning/exams/ { course, title, open_date, close_date, settings }
Get GET /learning/exams/<id>/
Update PUT /learning/exams/<id>/
Delete DELETE /learning/exams/<id>/
Start exam POST /learning/exams/<id>/start/
Resume exam GET /learning/exams/<id>/resume/
Submit exam POST /learning/exams/<id>/submit/
List submissions GET /learning/exams/<id>/results/
Grade written POST /learning/exam-submissions/<id>/grade-written/
Release scores POST /learning/exams/<id>/release-scores/
Release answers POST /learning/exams/<id>/release-answers/
Student exam history GET /learning/exams/
Delete submission (retake) DELETE /learning/exam-submissions/<pk>/delete/

Settings: Same as quizzes (5 fields) plus AFTER_CLOSE visibility option (exams have open_date/close_date).

Note: Created at course level, not lecture level. Appears in student's Exams page and can gate lecture access via prerequisites.

Single-attempt enforcement (exams only): Exams allow exactly 1 attempt. Teachers can delete a submission via DELETE /learning/exam-submissions/<pk>/delete/ to let a student retake. (Quizzes are multi-attempt — see Page 5.)


Page 7: Study Materials (Lecture Detail → Materials Tab)

Action Endpoint
Upload POST /materials/ (multipart: lecture, title, file)
List GET /materials/?lecture=<id>
Get GET /materials/<id>/
Update PUT /materials/<id>/
Delete DELETE /materials/<id>/

Accepted formats: PDF, JPG, JPEG, PNG — Max size: 20 MB
Write access: Teachers and assistants only (students and siteowners are read-only)
File uploads get a unique UUID filename — no overwrites


Page 8: Lecture Prerequisites (Lecture Detail → Prerequisites Tab)

Purpose: Gate lecture video access behind quiz or exam passing scores.

Action Endpoint
Get available assessments (for dropdown) GET /courses/lectures/<id>/available-assessments/
List GET /courses/lectures/<id>/prerequisites/
Add POST /courses/lectures/<id>/prerequisites/ { content_type, content_id, passing_score }
Get detail GET /courses/lectures/<id>/prerequisites/<prereq_id>/
Update PUT|PATCH /courses/lectures/<id>/prerequisites/<prereq_id>/
Remove DELETE /courses/lectures/<id>/prerequisites/<prereq_id>/

Valid types: quiz, exam

The "available assessments" endpoint returns all quizzes and exams for a lecture in one call — so the prerequisite picker dropdown can be populated with a single request instead of three:

{ "quizzes": [{ "id": 5, "title": "Quiz 1", "lecture_id": 5, "lecture_name": "Intro to Reactions" }], "exams": [{ "id": 2, "title": "Midterm Exam" }] }

Prerequisite picker UI:

Current prerequisites for "Intro to Reactions": - Quiz #1 (must score >= 50%) [Remove] - Quiz #2 (must score >= 70%) [Remove] + Add Prerequisite Type: [Quiz ▼] ID: [____] Passing Score: [50]%

Page 9: Students Page (Enrollments + Purchases + Balances)

Purpose: Manage everything about every student in a course. Approve/reject enrollments, view balances, purchases, attendance, homework/quiz/exam grades.

Flow Overview:

1. Load page → GET /courses/enrollments/?course=X&status=approved → list of approved students + balances 2. Tabs: Pending | Approved | Rejected 3. Click student → expanded row with per-lecture purchases + homework/quiz/video status 4. Click student → see full details in expanded row (balance, purchases, activity from enrollment list + transactions)

Page layout:

Course: Chemistry 3rd Secondary Pending (3) | Approved (24) | Rejected (2) ── Pending Tab ────────────────────────────────────── Gets data from: GET /courses/enrollments/?course=X&status=pending Student │ Code │ Requested │ Actions ─────────────┼─────────┼──────────────┼──────────────────────────── Ali Hassan │ S-010 │ 2026-07-07 │ [Approve] [Reject] Nora Ahmed │ S-011 │ 2026-07-06 │ [Approve] [Reject] ── Approved Tab ────────────────────────────────────── Gets data from: GET /courses/enrollments/?course=X&status=approved (returns: student_id, student_name, student_code, balance, enrolled_at) Student │ Code │ Balance │ Actions ─────────────┼─────────┼──────────┼──────────────────────────── Ahmed Ali │ S-001 │ 200 EGP │ [Report] [Reopen] [History] ▶ Expanded (per-lecture data, fetched on expand): Data source: GET /courses/enrollments/?course=X&status=approved (enrollment list with balance) - Lecture 1: Intro → Bought Jul 1 → Expires Jul 31 → Watch 2/4 → [Reopen] - Lecture 2: Types → Bought Jul 3 → Expires Aug 2 → Watch 1/4 → [Reopen] - Quiz 1: 80% ✅ - Homework 1: 2/3 ── Full Report (modal / new page) ─────────────────── Data source: GET /payments/transactions/?student=X (one API call returns everything: student info, enrollment, balance, ALL lectures (bought + not bought) with per-lecture progress: videos watched, homework submitted, quiz taken, purchases, etc.) ── Balance History (modal) ───────────────────────── Data source: GET /payments/transactions/?student=X Shows every transaction with balance_before → balance_after: code redemptions (shows code, amount, balance snapshots), lecture purchases (shows lecture name, amount deducted, balance snapshots)

Endpoint Reference Table:

Action Endpoint Notes
Enrolled students + balances GET /courses/enrollments/?course=X&status=approved Main list for the approved tab
Pending enrollments GET /courses/enrollments/?course=X&status=pending Pending tab data
Approved enrollments GET /courses/enrollments/?course=X&status=approved Full enrollment details
Rejected enrollments GET /courses/enrollments/?course=X&status=rejected Rejected tab data
All pending (across courses) GET /courses/enrollments/?status=pending Dashboard badge showing total pending
Approve single POST /courses/enrollments/approve/ Body: { "response_note": "string" } — accepts pending AND rejected rows (re-approval)
Reject single POST /courses/enrollments/reject/ Body: { "response_note": "string" } (reason required) — pending only
Approve bulk POST /courses/enrollments/approve/ Body: { "enrollment_ids": [...], "action": "approve\|reject" }
Block student from course POST /courses/enrollments/<id>/block/ Body: { "reason": "..." } (required) — approved enrollments only; student loses access to videos/purchases/course page (403 {blocked: true, reason})
Unblock student POST /courses/enrollments/<id>/unblock/ No body — restores access
Per-lecture student progress GET /courses/enrollments/?course=X&status=approved ALL students for ONE lecture: purchases, homework, quizzes, videos
Student transactions GET /payments/transactions/?student=X ONE student for ALL lectures: everything
Balance transaction history GET /payments/transactions/?student=X Full ledger: balance_before, balance_after, code details, purchase details
Lightweight activity log
Reopen lecture access PATCH /courses/purchases/<id>/reopen/ No body — restores ~1 day of access from now (max 2 reopens, resets sessions)
Delete quiz submission (allow retake) DELETE /learning/quiz-submissions/<pk>/delete/ Teacher deletes submission; student can start again
Delete exam submission (allow retake) DELETE /learning/exam-submissions/<pk>/delete/ Same as quiz
Homework submission detail GET /learning/homework-submissions/<id>/ Per-question bubble answers
Quiz submission detail GET /learning/quiz-submissions/<id>/ MCQ choices, written answers
Exam submission detail GET /learning/exam-submissions/<id>/ Same as quiz

Expanded Row Data Flow:

  1. Teacher clicks a student row → fetch GET /payments/transactions/ for a specific lecture
  2. The report returns per-student data structure: purchased (T/F), amount_paid, expires_at, is_expired, homeworks[], quizzes[], videos[]
  3. Render each lecture's data in the expanded row
  4. For the students who haven't purchased → purchased: false → show [Buy] prompt instead

Full Report Data Flow:

  1. Teacher clicks [Report] → fetch GET /payments/transactions/?student=X
  2. Returns everything: student profile, enrollment, balance, ALL lectures (bought + not bought), per-lecture homework/quiz/video progress, ALL purchases, ALL submissions
  3. One API call replaces the need for multiple individual calls — complete picture in a single response
  4. For balance history drill-down: GET /payments/transactions/?student=X (balance_before → balance_after per action)

Page 10: Student Report

Purpose: Full report of everything about a student within a course.

Action Endpoint
Per-student course report GET /courses/<course_id>/students/<student_id>/lectures/ — ALL visible lectures with purchase status, homework/quiz scores, per-video watch progress

Response includes: student info, ALL lectures (bought + not bought) with per-lecture homework/quiz/video progress, purchases (with sessions/expiry + can_reopen), quiz submissions, homework submissions.

Balance history (separate): GET /payments/transactions/?student=X — full ledger with balance_before and balance_after for every code redemption and lecture purchase.


Page 11: Assistants Page (Teachers Only)

Purpose: Manage assistants who share all course management capabilities.

Action Endpoint
List GET /accounts/teacher/assistants/
Create POST /accounts/teacher/assistants/ { username, password, name, phone, gmail, gender }
Get GET /accounts/teacher/assistant/<id>/
Update PATCH /accounts/teacher/assistant/<id>/
Delete DELETE /accounts/teacher/assistant/<id>/ (must be deactivated first)

Note: Assistant creation is teacher-only. Assistants can view other assistants but cannot create/delete.


Page 12: Codes Page (Teacher + SiteOwner)

Purpose: Generate, track, and export recharge codes. Codes are money — every action is audited.

Roles: SiteOwner (all courses), Teacher (own courses only). Assistant can view their teacher's codes.


Page Layout (3 views):

┌──────────────────────────────────────────────────────────────────┐ │ Analytics Cards: Total Batches | Codes | Valid | Used | │ │ Blacklisted | Revenue Collected │ ├──────────────────────────────────────────────────────────────────┤ │ Filter Bar: [Teacher ▼] [Course ▼] [Clear] Showing 12 batches │ ├──────────────────────────────────────────────────────────────────┤ │ Batch List Table: │ │ Batch ID │ Course │ Teacher │ Codes │ Value │ Status │ Notes │ │ Batch #1 │ Chem │ Dr Hany │ 100 │ 50 │ ████░░ │ Center │ │ Batch #2 │ Phys │ Omar │ 50 │ 100 │ ██░░░░ │ School │ ├──────────────────────────────────────────────────────────────────┤ │ Pagination │ └──────────────────────────────────────────────────────────────────┘ Click a batch row → View individual codes in that batch: ┌──────────────────────────────────────────────────────────────────┐ │ ← Back to Batches │ │ Batch #1 — Chemistry 3rd Secondary — Dr Hany Hassanin │ │ Codes: 100 | Value: 50 EGP | Redeemed Value: 17,500 EGP │ ├──────────────────────────────────────────────────────────────────┤ │ Code (masked) │ Value │ Status │ Used By │ Used At │ │ X7K9 XXXX… │ 50 │ ✅ Used │ Ahmed / أحمد │ 2026-07-14 │ │ A1B2 XXXX… │ 50 │ 🟢 Valid │ — │ — │ └──────────────────────────────────────────────────────────────────┘ Click "History" → Full audit modal with date/type/code filters. Click "Blacklist" → OTP verification → irreversible blacklist.

APIs for the Page:

Action Method Endpoint Where to find Purpose
Analytics dashboard GET /payments/codes/analytics/ API_SITEOWNER.md §5 Returns total_batches, total_codes, valid/used/blacklisted counts + values (no revenue split — cuts were removed)
Batch list (paginated) GET /payments/codes/batches/ API_SITEOWNER.md §5 Grouped by batch_id, filters: ?teacher=X, ?course=X
Batch detail GET /payments/codes/batches/<uuid:batch_id>/ API_SITEOWNER.md §5 Individual codes inside a batch with their status
Generate codes POST /payments/codes/ API_SITEOWNER.md §5 Bulk create (1-1000). Body: {course, value, notes, count}
Code summary GET /payments/codes/summary/?course=X API_SITEOWNER.md §5 Aggregate totals: generated, redeemed, blacklisted, values
Code audit history GET /payments/codes/history/ API_SITEOWNER.md §5 Unified feed of redemptions + blacklists. Response also includes analytics block with total, redeemed, blacklisted, and revenue counts computed from the full filtered dataset (respects all query parameters). Filters: ?start_date=, ?end_date=, ?type=redeemed|blacklisted, ?search=X7K9, ?teacher=X, ?course=X. Paginated (20/page). Newest first.
Export all codes GET /payments/codes/export/ API_SITEOWNER.md §5 Download ALL visible codes as Excel (no ?batch= needed). Supports ?teacher=X, ?course=X. Columns: Code, Value, Status, Course, Teacher, Student, Redemption Date, Blacklist Date, etc.
Batch detail export GET /payments/codes/export/?batch=<uuid> API_SITEOWNER.md §5 Download .xlsx with full code details for a specific batch
Batch blacklist codes POST /payments/codes/batch-blacklist/ API_SITEOWNER.md §5 Body: { code_ids: [...], otp: "..." }. Blacklists multiple codes with one OTP. Only valid codes affected — used/blacklisted skipped.
Request blacklist OTP POST /payments/codes/<pk>/request-blacklist-otp/ API_SITEOWNER.md §5 SiteOwner only. Sends OTP to their gmail
Request batch blacklist OTP POST /payments/codes/request-blacklist-otp/ API_SITEOWNER.md §5 SiteOwner only. Same OTP works for single and batch blacklist.
Blacklist code POST /payments/codes/<pk>/blacklist/ API_SITEOWNER.md §5 SiteOwner only. One-way irreversible. Requires 6-digit OTP
Transaction history GET /payments/transactions/?transaction_type=code_redeemed API_SITEOWNER.md §6 Full audit log with date filters. Filter by ?start_date=, ?end_date=, ?code=

Analytics Cards (from GET /codes/analytics/):

Card Field Description
Total Batches total_batches Number of distinct batch_id values
Total Codes total_codes Total codes across all batches
Valid valid_count Codes not yet redeemed or blacklisted
Used used_count Successfully redeemed codes
Blacklisted blacklisted_count Permanently deactivated by SiteOwner
Revenue Collected used_value Total EGP of all redeemed codes

Note: Teacher's Cut / SiteOwner's Cut cards were removed from the backend — GET /payments/codes/analytics/ returns only the 8 fields above. The video_security.price_per_student platform-fee split is no longer computed.


Code Model Fields:

Field Description
code Auto-generated 16-char random code (XXXX-XXXX-XXXX-XXXX)
value 1–1000 EGP. Set at creation, never editable
status validused or blacklisted (both terminal)
batch_id UUID grouping codes from the same bulk generation
batch_name Human-readable name ("Dr Hany Hassanin Batch #1"), per-teacher sequential counter
expires_at Auto-set to 1 year from creation, not editable
notes Optional text (e.g. "Batch for Center X")
created_by SiteOwner or Teacher who generated it
used_by Student who redeemed it (null if unused)
used_at When it was redeemed
blacklisted_by SiteOwner who blacklisted it
blacklisted_at When it was blacklisted

Code Status Flow:

Valid ──→ Redeemed (used) — Student self-redeem or Teacher recharge Valid ──→ Blacklisted — SiteOwner only, OTP required, irreversible

Business Rules:


Excel Export Columns (from GET /codes/export/?batch=<uuid>): Code | Value (EGP) | Status | Course | Redeemed By | Redeemed At | Blacklisted By | Blacklisted At | Created At | Expires At | Notes


Page Flow for SiteOwner:

1. Page loads → GET /payments/codes/analytics/ → populate analytics cards (Total Batches, Codes, Valid, Used, Blacklisted, Revenue Collected) 2. GET /payments/codes/batches/ → populate batch table (paginated, 20 per page) 3. Teacher dropdown → GET /payments/codes/batches/?teacher=X → filter batches 4. Course dropdown (dependent on teacher) → GET /payments/codes/batches/?course=X → filter batches 5. Click batch row → GET /payments/codes/batches/<uuid>/ → show individual codes with `code_masked` ("ABDC XXXX XXXX XXXX") by default, "Reveal" button to show full code 6. Click "Export" on batch → GET /payments/codes/export/?batch=<uuid> → download .xlsx 7. Click "Export All" → GET /payments/codes/export/ → download all codes .xlsx 8. Click "History" → open modal → GET /payments/codes/history/ with default date range (last 30 days) → populate history table with filters 9. In History modal: filter by type, code search, date range → refetch or client-filter 10. Click "Generate Batch" → POST /payments/codes/ → modal with course/count/value/notes 11. Select one or more codes via checkboxes → Click "Blacklist Selected" → POST /codes/request-blacklist-otp/ → OTP modal 12. Enter OTP → POST /codes/batch-blacklist/ with { code_ids: [...] } → codes permanently blacklisted

Page Flow for Teacher (same as above, but:

Page 12: Course Dashboard (Unified — Built Last)

When all features exist, build a single page that unifies everything into 4 tabs:

┌────────────────────────────────────────────────────────────┐ │ ← Chemistry 3rd Secondary │ │ │ │ ┌──────────┬─────────────────────┬──────────┬───────────┐ │ │ │ 📊 │ 👥 │ 📚 │ 📋 │ │ │ │Overview │ Students │ Content │ Reports │ │ │ │ (Stats) │ Same as Page 9 │ Same as │ Same as │ │ │ │ │ (embedded) │ Page 2 │ Page 10 │ │ │ │ │ │ (embedded)│ (embedded)│ │ │ └──────────┴─────────────────────┴──────────┴───────────┘ │ └────────────────────────────────────────────────────────────┘

Tab 1 — Overview: GET /courses/<id>/ — enrolled/pending/rejected counts, topic/lecture/video counts.
For deeper analytics: GET /courses/<id>/analytics/ — per-topic and per-lecture purchase breakdowns with revenue.


Page 13: Teacher Dashboard (Last — Standalone)

Purpose: Landing page after teacher login. Aggregated stats across all courses, plus action items that need the teacher's attention. Single call to GET /courses/teacher/dashboard/ replaces multiple API calls.

GET /courses/teacher/dashboard/ Response:

{ "summary": { "total_courses": 3, "active_courses": 3, "total_students": 24, "pending_enrollments": 4, "total_purchases": 18, "total_revenue": "720.00", "outstanding_cut": "160.00" }, "actions_needed": { "pending_enrollments": [ { "enrollment_id": 1, "student_id": 10, "student_name": "Ahmed Ali / أحمد", "student_code": "1234567", "course_id": 28, "course_name": "Chemistry 3rd Secondary 2027", "enrolled_at": "2026-07-30T10:00:00Z" } ], "ungraded_written": { "total_pending": 5, "quizzes": [ { "quiz_id": 94, "quiz_title": "Important Quiz", "lecture_name": "Lecture 1", "course_name": "...", "pending_count": 3 } ], "exams": [ { "exam_id": 2, "exam_title": "Midterm", "course_name": "...", "pending_count": 2 } ] } }, "recent_enrollments": [ { "id": 1, "student_name": "Ahmed Ali / أحمد", "student_code": "1234567", "course_name": "Chemistry 3rd Secondary 2027", "status": "pending", "enrolled_at": "2026-07-30T10:00:00Z" } ], "recent_purchases": [ { "id": 1, "student_name": "Ahmed Ali / أحمد", "student_code": "1234567", "lecture_name": "Lecture 1", "course_name": "Chemistry 3rd Secondary 2027", "amount_paid": "40.00", "purchased_at": "2026-07-30T11:00:00Z" } ], "courses": [ { "id": 28, "name": "Chemistry 3rd Secondary 2027", "is_active": true, "enrolled_count": 10, "pending_count": 2, "purchase_count": 8, "revenue": "320.00" } ] }

Page layout:

Dashboard — Welcome back, Dr Hany ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [Summary cards] Total Courses: 3 (2 active) │ Students: 24 │ Purchases: 18 │ Revenue: 720 EGP [Actions Needed] ← NEW — teacher's to-do list ┌─ Pending Enrollments (4) ──────────────────────────────────┐ │ Ahmed Ali / أحمد (1234567) — Chemistry 3rd Sec 2027 │ │ requested Jul 30 [Approve] [Reject] │ └────────────────────────────────────────────────────────────┘ ┌─ Ungraded Written Answers (5) ─────────────────────────────┐ │ Quiz: Important Quiz (Lecture 1, Chemistry) — 3 pending │ │ → link to quiz written-answers review │ │ Exam: Midterm (Chemistry) — 2 pending │ │ → link to exam written-answers review │ └────────────────────────────────────────────────────────────┘ [Recent Activity] Recent Enrollments (last 10) — table: student, code, course, status, date Recent Purchases (last 10) — table: student, code, lecture, course, amount, date [Course Breakdown] Course card grid — name, active badge, enrolled/pending counts, purchase count, revenue, "Open" → Content Management

Field usage guide:

Section Frontend use
summary Summary cards at top: total_courses (+ active_courses subtitle like "3 active"), total_students, pending_enrollments (also shown as badge count on the Actions card), total_purchases, total_revenue
actions_needed.pending_enrollments[] Approve/Reject queue. Render each item with student_name, student_code, course_name, enrolled_at. Clicking [Approve] / [Reject] calls POST /courses/enrollments/approve/ or POST /courses/enrollments/reject/ with {"enrollment_ids": [enrollment_id]} — then re-fetch the dashboard. Empty array → show "No pending enrollments ✅"
actions_needed.ungraded_written Grading to-do. Show total_pending badge. Each quizzes[] item links to GET /learning/quizzes/<quiz_id>/written-answers/; each exams[] item links to GET /learning/exams/<exam_id>/written-answers/. pending_count is the number of answers to grade for that assessment. Empty quizzes/exams arrays → show "All written answers graded ✅"
recent_enrollments[] Recent activity feed/table (last 10, any status — show status badge: pending/approved/rejected)
recent_purchases[] Recent revenue feed/table — show amount_paid + purchased_at
courses[] Per-course cards: name, is_active badge, enrolled_count + pending_count (badge if > 0), purchase_count, revenue. Card click → My Courses / Content Management page

Business rules for the frontend:


Page 14: Cut Invoices (SiteOwner) + My Cuts (Teacher)

Purpose (SiteOwner): Manual platform-cut invoicing. Siteowner sees per-teacher lecture volume (filterable by month/date range), creates an invoice with a date range + cut per lecture, and tracks unpaid/paid/cancelled invoices. Purpose (Teacher): see their own invoices (owed/settled) — transparency.

APIs (SiteOwner — all under /payments/cuts/):

Action Method Endpoint Purpose
Overview GET /payments/cuts/overview/?teacher=&search=&start=&end= Per-teacher total/purchased lectures, outstanding/paid totals, per-course breakdown; rows include profile_picture; ?teacher=<id> embeds lectures[] + lectures_count (teacher detail — no separate call needed). Teachers/assistants are auto-scoped to their own teacher (?teacher=/?search= ignored)
Itemized preview GET /payments/cuts/lectures/?teacher=&search=&start=&end=&course= Individual purchases in range (verify before invoicing)
List invoices GET /payments/cuts/invoices/?teacher=&status=&search=&start=&end= Paginated invoice list
Create invoice POST /payments/cuts/invoices/ {teacher, start_date, end_date, cut_per_lecture, discount, note?} → 201 with computed lectures_count, gross_total, total_owed
Invoice detail GET /payments/cuts/invoices/<id>/ Single invoice
Update invoice PATCH /payments/cuts/invoices/<id>/ Edit cut/discount (blocked when paid), note, paid_note, status (paid sets paid_at)
Cancel invoice DELETE /payments/cuts/invoices/<id>/ Soft-cancel (status → cancelled)

API (Teacher):

Action Method Endpoint Purpose
My invoices GET /payments/cuts/my/?search= Teacher's own unpaid/paid invoices

SiteOwner page layout (/siteowner/cuts):

Filter bar: [Teacher ▼] [Search...] [Start date] [End date] [Clear] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Summary cards: Total Lectures | Purchased Lectures | Outstanding | Collected ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Overview table (per teacher): Teacher │ Lectures │ Purchased │ Outstanding │ Paid │ Actions Dr Hany │ 42 │ 320 │ 6400 EGP │ 1200 │ [Create Invoice] [▸ Expand] ▸ per-course rows: course name, grade, lectures, purchased, revenue ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Invoices table: Teacher │ Period │ Count │ Cut/Lec │ Total │ Status │ Actions Dr Hany │ Aug 1–31, 2026 │ 320 │ 20 EGP │ 6400 EGP │ Unpaid │ [Mark Paid] [Cancel] [View]

Create Invoice modal: teacher (pre-filled), start/end dates, cut_per_lecture, discount, note. Live preview: "N lectures in range × X EGP − discount = Y EGP" (N fetched from /cuts/lectures/?teacher=&start=&end=; validate discount ≤ gross client-side, server enforces too). Overlapping-period and discount>gross errors (400) shown inline.

Mark Paid flow: optional Payment Note field → PATCH {status: "paid", paid_note}. Reverting to unpaid clears the note.

Invoices table: teacher | period | count | cut/lecture | discount | gross | total | status badge | paid note | actions.

Teacher page (/teacher/cuts): read-only table of their invoices — period, lectures count, cut, discount, gross/total owed, status badge, note, paid note. Empty state "No invoices yet."

Business rules (backend-enforced):


Phase 3: Student Pages (Build After Phase 2)

Build in dependency order. Pages without dependencies can be built in parallel.

Dependency graph:

Early (independent): Page 1: Profile Page 2: Recharge Codes Page 3: Transaction History Chain (build in order): Page 4: Course Preview ↓ Page 5: My Enrollments ↓ Page 6: Course Lectures ↓ Page 7: Purchase Lecture ↓ Page 8: Lecture Player Independent (can build anytime after Chain): Page 9: Homework Bubble Sheet Page 10: Quiz Taking Page 12: My Lectures (aggregate — all purchased lectures) Page 13: My Homeworks (aggregate — all available homeworks) Page 14: My Quizzes (aggregate — all available quizzes) Last (build after everything): Page 11: Student Dashboard

Student authentication behavior by status: After login (POST /accounts/login/), the response includes status for students:


Page 1: Profile (Independent — Build Any Time)

Purpose: View profile + change password + edit profile if declined. No student-page dependencies — can be built immediately after Phase 1.

APIs to use (from API_STUDENT.md):

Action Method Endpoint Roles
Get profile GET /accounts/profile/me/ Student only
Update profile PATCH /accounts/profile/me/ Student only (declined only)
Change password POST /accounts/change-password/ Any authenticated

GET /accounts/profile/me/ Response:

{ "id": 1, "user_id": 1, "username": "ahmed123", "student_code": "1234567", "name_ar": "أحمد", "name_en": "Ahmed", "full_name": "Ahmed / أحمد", "phone_number": "01234567890", "father_number": "01112345678", "mother_number": "01087654321", "school_type": 1, "school_type_name": "Public School", "grade": 3, "grade_name": "3rd Secondary", "division": 2, "division_name": "Scientific", "school_name": "Al-Haram Secondary School", "birth_date": "2008-05-15", "gender": "male", "gmail": "ahmed@gmail.com", "governorate": 1, "governorate_name": "Cairo", "area": 5, "area_name": "Nasr City", "status": "declined", "is_active": false, "can_access_course": false, "status_history": [ { "from_status": "pending", "to_status": "declined", "reason": "Missing father's phone number. Please re-submit with correct info.", "changed_by": "Admin � Username of the admin who made the change", "created_at": "2026-07-15T10:00:00Z" } ], "created_at": "2026-06-01T10:00:00Z", "updated_at": "2026-07-18T10:00:00Z" }

PATCH /accounts/profile/me/ Request: Same fields as GET (all optional for PATCH). Can use JSON or multipart. Only sends if status === 'declined'.

POST /accounts/change-password/ Request:

{ "old_password": "String (Required) — Current password", "new_password": "String (Required) — Minimum 8 characters", "new_password_confirm": "String (Required) — Must match new_password" }

POST /accounts/change-password/ Response — 200 OK:

{ "message": "Password changed successfully. Please log in again." }

Error Responses:

Status Endpoint Condition
403 PATCH profile Verified / pending / suspended profiles cannot edit
400 PATCH profile Email exists, area/governorate mismatch
400 Change password Missing fields, passwords don't match, too short, wrong old password
400 Change password Weak password (common, entirely numeric, too similar to username)

Page layout:

Populates from: profile endpoint


Page 2: Recharge Codes (Independent — Build Any Time)

Purpose: Redeem physical voucher codes to add balance to a specific course.

APIs to use (from API_STUDENT.md):

Action Method Endpoint Roles
My balances GET /payments/balance/ Student only
Course balance GET /courses/<id>/balance/ Student only
Redeem code POST /payments/codes/redeem/ Student only

POST /payments/codes/redeem/ Request:

{"code": "X7K9-M2P4-QR1W-L5D8", "course": 1}

POST /payments/codes/redeem/ Response — 200 OK:

{ "detail": "Code redeemed successfully. Added 50.00 EGP to your Chemistry balance.", "new_balance": "200.00", "transaction_id": 42 }

Error Responses:

Status Condition
400 Code already used / expired / wrong course
403 Not enrolled and approved
404 Invalid code

Page layout:

Populates from: balance endpoint for course list + balances; redeem for code application


Page 3: Transaction History (Independent — Build Any Time)

Purpose: View full ledger of all code redemptions and lecture purchases with per-course balance tracking.

API: GET /payments/transactions/ — auto-filtered to the logged-in student. Paginated.

Query Parameters:

Parameter Type Description
page Integer Page number (default: 1)
page_size Integer Items per page (default: 50)
transaction_type String code_redeemed or lecture_purchase
search String Search in description
start_date / end_date String Date range filter

Response:

{ "count": 12, "results": [ { "course_name": "Chemistry 3rd Secondary", "teacher_name": "Dr Hany Hassanin", "teacher_picture": "https://.../profile_pictures/drhany.jpg", "transaction_type": "code_redeemed", "transaction_type_display": "Code Redeemed", "amount": "50.00", "balance_before": "100.00", "balance_after": "150.00", "description": "Code X7K9-M2P4-QR1W-L5D8", "created_at": "2026-07-19T10:00:00Z" } ] }

Page layout:

Populates from: transactions endpoint


Page 4: Course Preview

Purpose: Preview a course before enrolling — shows full structure with topics, lectures, prices, and teacher info. Available to any authenticated user.

APIs to use (from API_STUDENT.md):

Action Method Endpoint Purpose
Preview course GET /courses/<id>/preview/ Topics + lectures with prices
Enroll in course POST /courses/enrollments/enroll/ Request enrollment
Cancel enrollment DELETE /courses/enrollments/<id>/cancel/ Cancel pending request

GET /courses/<id>/preview/ Response:

{ "id": 1, "name": "Chemistry 3rd Secondary", "description": "Full course covering chemical reactions...", "cover_picture": "https://.../cover.jpg", "teacher": {"id": 5, "name": "Dr Hany", "profile_picture": "..."}, "grade": {"id": 3, "name": "3rd Secondary"}, "subject": {"id": 2, "name": "Chemistry"}, "topic_count": 5, "total_lectures": 20, "topics": [ { "id": 1, "name": "Chemical Reactions", "lecture_count": 4, "picture": "https://.../topics/pictures/chemical.jpg", "lectures": [ { "id": 10, "name": "Intro to Reactions", "price": "50.00", "final_price": "40.00", "discount": "10.00", "available_days": 30, "video_count": 2 } ] } ] }

Page layout:

Populates from: preview endpoint for course data; enroll for enrollment


Page 5: My Enrollments

Purpose: View all enrolled courses with teacher info, topics/lectures count. Each card links to the course lectures page.

APIs to use (from API_STUDENT.md):

Action Method Endpoint Purpose
My enrollments GET /courses/enrollments/ All student enrollments (auto-filtered). Use ?status=approved for approved only

GET /courses/enrollments/ Response (student view):

[ { "id": 1, "student": 1, "student_name": "Ahmed / ????", "student_code": "1234567", "course_name": "Chemistry 3rd Secondary", "grade_name": "3rd Secondary", "teacher_name": "Dr Hany", "subject_name": "Chemistry", "status": "approved", "status_display": "Approved", "balance": "150.00", "cover_picture": "https://.../covers/chem.jpg", "topic_count": 5, "total_lectures": 20, "enrolled_at": "2026-07-01T10:00:00Z", "responded_by": null, "responded_by_name": null, "responded_at": null, "response_note": null } ]

Note: cover_picture, topic_count, total_lectures are only non-null for approved enrollments.

Page layout:

Populates from: enrollments endpoint with ?status=approved


Page 6: Course Lectures

Purpose: View all topics inside an enrolled course. Each topic is a clickable square card that navigates to the lecture player or purchase page.

APIs to use (from API_STUDENT.md):

Action Method Endpoint Purpose
Course lectures GET /courses/<id>/lectures/ Topics + lectures with purchase status

GET /courses/<id>/lectures/ Response:

{ "course": {"id": 1, "name": "Chemistry 3rd Secondary"}, "topics": [ { "id": 1, "name": "Chemical Reactions", "order": 1, "lectures": [ { "id": 10, "name": "Intro to Reactions", "price": "50.00", "final_price": "40.00", "order": 1, "video_count": 2, "is_purchased": true, "purchase": { "id": 5, "purchased_at": "2026-07-01T10:00:00Z", "expires_at": "2026-07-31T10:00:00Z", "effective_expiry": "2026-07-31T10:00:00Z", "is_expired": false, "amount_paid": "40.00", "extra_days": 0, "max_watch_count": 4, "sessions_used": 1, "sessions_remaining": 3, "has_active_session": true, "active_session": { "id": 1, "purchase": 5, "started_at": "2026-07-26T10:00:00Z", "expires_at": "2026-07-26T16:00:00Z", "is_active": true, "remaining_seconds": 18000 } } } ] } ] }

Page layout:

Populates from: lectures endpoint — topics with lecture purchase status


Page 7: Purchase Lecture

Purpose: View lecture details and purchase access by deducting from course balance.

APIs to use (from API_STUDENT.md):

Action Method Endpoint Purpose
Lecture detail GET /courses/lectures/<id>/ Full lecture info with videos, counts, prerequisites
Buy lecture POST /courses/purchases/buy/ Deduct balance and grant access
Course balance GET /courses/<id>/balance/ Current balance

POST /courses/purchases/buy/ Request:

{"lecture": 10}

Response — 201 Created:

{ "id": 5, "lecture_name": "Intro to Reactions", "topic_name": "Chemical Reactions", "course_name": "Chemistry 3rd Secondary", "teacher_name": "Dr Hany", "purchased_at": "2026-07-18T10:00:00Z", "expires_at": "2026-08-17T10:00:00Z", "amount_paid": "40.00", "is_expired": false }

Error Responses:

Status Condition
403 Not enrolled/approved
400 Already purchased
400 Insufficient balance

Page layout:

Populates from: courses/lectures/<id>/ for lecture detail; buy endpoint; balance endpoint


Page 8: Lecture Player

Purpose: Watch purchased lecture videos, access materials, view quizzes and homeworks. Entry points: Course Lectures page (Page 6) and My Lectures page (Page 12).

APIs to use (from API_STUDENT.md):

Action Method Endpoint Purpose
Check session GET /courses/purchases/<pk>/active-session/ Check if 6h viewing window is active
Start session POST /courses/purchases/<pk>/start-watching/ Start a 6h viewing session (consumes 1 from max)
Lecture detail GET /courses/lectures/<id>/ Lecture info with videos list, counts
Playback URL GET /courses/videos/<id>/play/ Signed Bunny Stream URL
List progress GET /courses/progress/ All video watch progress across lectures
Update progress POST /courses/progress/update/ Save watch progress
Video progress detail GET /courses/progress/<video_id>/ Progress for a specific video
List materials GET /materials/ Study materials for purchased lectures
Quizzes list GET /learning/quizzes/ Quiz submissions for this lecture
Homeworks list GET /learning/homeworks/ Homework submissions

GET /courses/videos/<id>/play/ Response:

{ "playback_url": "https://vz-...b-cdn.net/{guid}/playlist.m3u8" }

Access Rules:

Check Blocked?
Not purchased 403
Expired 403
No active viewing session 403
Prerequisite not passed 403

Session-based viewing (new flow): Instead of per-video watch counting, the system uses 6-hour viewing sessions. The frontend must check session status before showing the player:

  1. On entering the lecture player page, call GET /courses/purchases/<pk>/active-session/ to check session status
  2. Based on response, show one of 3 UI states:
State Condition UI
No active session + watches remaining has_active_session: false, sessions_remaining > 0 Show "Start watching" button with remaining count (e.g., "3 sessions remaining"). Click ? POST /courses/purchases/<pk>/start-watching/ ? creates 6h session ? redirects to player
Active session has_active_session: true, session.is_active: true Show "Resume watching" with countdown timer (remaining time from session.remaining_seconds). Player is accessible.
No watches remaining sessions_remaining: 0 Show "No watches remaining. Contact your teacher." message with a disabled button.

Page layout:

Progress tracking (important � read carefully):

The system tracks two independent values. Both must reach >= 90% for is_completed:

Field What it tracks Prevents Sent when
progress_seconds Furthest playback position reached Scrubbing ahead to skip content Every update (including regular intervals)
cumulative_watch_seconds Wall-clock time spent genuinely playing Watching only 30% on repeat Milestones only (25%, 50%, 75%, 90%)

Frontend logic (client-side):

// Track cumulative watch time in localStorage (survives page refreshes) const STORAGE_KEY = `watch_time_${videoId}`; let cumulativeSeconds = Number(localStorage.getItem(STORAGE_KEY)) || 0; let lastTick = Date.now(); videoPlayer.on('timeupdate', () => { const now = Date.now(); const elapsed = (now - lastTick) / 1000; // Real seconds passed if (!videoPlayer.paused) { cumulativeSeconds += elapsed; localStorage.setItem(STORAGE_KEY, Math.round(cumulativeSeconds)); } lastTick = now; });

Milestone detection � send cumulative_watch_seconds only 4-6 times total per video:

const DURATION = 600; // video length in seconds const MILESTONES = [0.25, 0.50, 0.75, 0.90]; let lastReportedMilestone = -1; function checkMilestone() { const milestone = Math.floor(cumulativeSeconds / DURATION / 0.25); if (milestone > lastReportedMilestone && milestone < MILESTONES.length) { lastReportedMilestone = milestone; fetch('/courses/progress/update/', { method: 'POST', body: JSON.stringify({ video: videoId, progress_seconds: Math.round(videoPlayer.currentTime), // current position cumulative_watch_seconds: Math.round(cumulativeSeconds), // cumulative time (milestone) duration_seconds: DURATION }) }); } }

On tab close (beforeunload): Send one final update with the latest cumulative_watch_seconds.

Example: 10-min video (600s), student watches normally:

Time (real) Event progress_seconds sent cumulative_watch_seconds sent
0:00 Page load
0:30 Regular update 30
2:30 Milestone hit (25%) 150 150 ?
5:00 Regular update 300
5:00 Milestone hit (50%) 300 300 ?
7:30 Regular update 450
7:30 Milestone hit (75%) 450 450 ?
9:00 Milestone hit (90%) 540 540 ? (is_completed = True)

Total requests per video: ~6 (4 milestones + 2-3 regular position updates + 1 on close)

Populates from: active-session endpoint (on load), start-watching endpoint (to start session), play endpoint (after session active), materials endpoint, quizzes/ and homeworks/ endpoints


Page 9: Homework Bubble Sheet (Standalone Page)

Purpose: Answer bubble-sheet homework — no question text, just A/B/C/D choices in a compact grid. Opened from Lecture Player's homework card.

APIs to use (from API_STUDENT.md):

Action Method Endpoint Purpose
Homework detail GET /learning/homeworks/<id>/ Bubble questions (only order + choices_count)
Save draft PATCH /learning/homeworks/<id>/draft/ Auto-save answers
Load draft GET /learning/homeworks/<id>/draft/ Restore saved answers
Submit POST /learning/homeworks/<id>/submit/ Submit for auto-grading
View results GET /learning/homework-submissions/<submission_id>/ Per-question results

GET /learning/homeworks/<id>/ Response (student view):

{ "id": 1, "lecture": 10, "title": "Week 1 HW", "description": "Chapter 1 exercises", "bubble_questions": [ {"id": 1, "order": 1, "choices_count": 4}, {"id": 2, "order": 2, "choices_count": 5} ] }

Note: HomeworkBubbleQuestion has NO text field — only order, choices_count, correct_answer, points.

POST /learning/homeworks/<id>/submit/ Request:

{ "bubble_answers": [ {"bubble_question_id": 1, "selected_choice": "A"}, {"bubble_question_id": 2, "selected_choice": "B,D"} ] }

Response — 200 OK:

{ "detail": "Homework submitted and auto-graded.", "submission_id": 10, "score": "2.00", "status": "graded" }

Page layout (compact — no scroll):

Populates from: homeworks/<id> for questions; draft for save/load; submit for grading; homework-submissions/<id> for results


Page 10: Quiz Taking (Standalone Page)

Purpose: Take a timed quiz one question at a time with navigator, auto-save drafts, and view detailed results after submission.

APIs to use (from API_STUDENT.md):

Action Method Endpoint Purpose
Quiz detail GET /learning/quizzes/<id>/ Quiz info (settings visible, questions hidden until started)
Start quiz POST /learning/quizzes/<id>/start/ Create attempt, get questions + submission_id
Resume quiz GET /learning/quizzes/<id>/resume/ Restore active attempt with saved answers
Save draft PATCH /learning/quiz-answers/<answer_id>/save-draft/ Auto-save per answer
Submit quiz POST /learning/quizzes/<id>/submit/ Submit all answers
View results GET /learning/quiz-submissions/<id>/ Per-question results

POST /learning/quizzes/<id>/start/ Response:

{ "submission_id": 15, "timer_minutes": 30, "started_at": "2026-07-18T10:00:00Z", "questions": [ { "answer_id": 50, "quiz_question_id": 10, "text": "What is 2+2?", "image": null, "question_type": "mcq_single", "points": 2, "choices": [{"id": 100, "text": "4", "order": 1}, {"id": 101, "text": "5", "order": 2}] } ] }

POST /learning/quizzes/<id>/submit/ Request:

{ "answers": [ {"quiz_question_id": 10, "choice_ids": [100], "written_answer": ""} ] }

Response — 200 OK:

{ "detail": "Quiz submitted successfully.", "submission_id": 15, "score": "5.00", "score_visible": true, "answers_visible": true }

Settings fields (from QuizSettings):

Field Values Description
timer_minutes 0–1440 Time limit. 0 = no limit.
max_attempts 1+ Max attempts allowed (quizzes only — exams are always single-attempt)
score_visibility immediate / manual When score is shown to student
answers_visibility immediate / manual When correct answers are shown
question_order fixed / random Order of questions

Page flow:

  1. Start screen: Quiz title, question count, settings displayed (timer, max attempts, score/answer visibility, question order). "Start Quiz" button.
  2. Quiz in progress (after start):
  3. On page refresh: GET /learning/quizzes/<id>/resume/ → restore timer, questions, saved answers
  4. Results screen (after submit):

Populates from: quizzes/<id> for info; start for beginning; resume for recovery; save-draft for auto-save; submit for grading; quiz-submissions/<id> for results


Page 11: Student Dashboard (Build Last)

Purpose: Landing page after student login. Shows enrolled courses and quick links to all features. Uses the consolidated dashboard endpoint � replaces 5 separate API calls.

APIs to use (from API_STUDENT.md):

Action Method Endpoint Roles
Dashboard GET /courses/student/dashboard/ Student only

GET /courses/student/dashboard/ Response:

{ "enrolled_courses": [ { "id": "integer", "name": "string", "cover_picture": "string (URL) | null", "teacher_name": "string", "teacher_picture": "string (URL) | null", "grade_name": "string", "balance": "string (decimal)", "total_lectures": "integer", "purchased_lectures": "integer" } ], "my_lectures": { "total": "integer", "active": "integer", "expired": "integer" }, "nearly_expired_lectures": [ { "purchase_id": "integer", "lecture_id": "integer", "lecture_name": "string", "topic_name": "string", "course_name": "string", "expires_at": "datetime (ISO 8601)", "remaining_hours": "integer" } ], "homeworks": { "total": "integer", "submitted": "integer", "graded": "integer", "pending": "integer" }, "quizzes": { "total": "integer", "not_started": "integer", "in_progress": "integer", "submitted": "integer" } }

Page layout:

Populates from: /courses/student/dashboard/ (single call � replaces 5 separate API calls)


Page 12: My Lectures (Aggregate)

Purpose: View all purchased lectures across all enrolled courses. Filter and navigate to watch. APIs to use (from API_STUDENT.md �14):

Action Method Endpoint Purpose
My lectures GET /courses/my-lectures/ All purchased lectures across courses with content counts

GET /courses/my-lectures/ Response:

[ { "purchase_id": 1, "lecture_id": 10, "lecture_name": "Intro to Reactions", "topic_id": 1, "topic_name": "Chemical Reactions", "course_id": 1, "course_name": "Chemistry 3rd Secondary", "teacher_name": "Dr Hany", "price": "40.00", "purchased_at": "2026-07-01T10:00:00Z", "expires_at": "2026-07-31T10:00:00Z", "effective_expiry": "2026-07-31T10:00:00Z", "is_expired": false, "extra_days": 0, "max_watch_count": 4, "sessions_used": 1, "sessions_remaining": 3, "has_active_session": false, "video_count": 3, "materials_count": 2, "homeworks_count": 1, "quizzes_count": 1 } ]

Page layout:

Populates from: my-lectures endpoint (single call � no more iteration across courses)


Page 13: My Homeworks (Aggregate)

Purpose: View all available homeworks across all purchased lectures. Start homework or view submission results.

APIs to use (from API_STUDENT.md �14):

Action Method Endpoint Purpose
My homeworks GET /learning/homeworks/ All homeworks for purchased lectures with submission status

GET /learning/homeworks/ Response (paginated � use /learning/homeworks/?all=true for non-paginated):

[ { "homework_id": 1, "title": "Week 1 HW", "description": "Chapter 1 exercises", "is_published": true, "show_grades": true, "total_points": 3, "lecture_id": 10, "lecture_name": "Intro to Reactions", "topic_id": 1, "topic_name": "Chemical Reactions", "course_id": 1, "course_name": "Chemistry 3rd Secondary", "teacher_name": "Dr Hany", "is_submitted": true, "submission_id": 5, "score": "2.00", "status": "graded", "submitted_at": "2026-07-18T10:00:00Z" } ]

Page layout:

Populates from: homeworks/ endpoint (single call � no merging needed)


Page 14: My Quizzes (Aggregate)

Purpose: View all available quizzes across all purchased lectures. Start, resume, or view quiz results.

APIs to use (from API_STUDENT.md �14):

Action Method Endpoint Purpose
My quizzes GET /learning/quizzes/ All quizzes for purchased lectures with status

GET /learning/quizzes/ Response (paginated � use /learning/quizzes/?all=true for non-paginated):

[ { "quiz_id": 1, "title": "Quiz 1", "description": "Chapter 1 quiz", "total_points": "10.00", "settings": { "timer_minutes": 10, "score_visibility": "immediate", "answers_visibility": "immediate" }, "lecture_id": 10, "lecture_name": "Intro to Reactions", "topic_id": 1, "topic_name": "Chemical Reactions", "course_id": 1, "course_name": "Chemistry 3rd Secondary", "teacher_name": "Dr Hany", "quiz_status": "submitted", "submission_id": 15, "score": "5.00", "score_visible": true, "answers_visible": true, "started_at": "2026-07-18T10:00:00Z", "submitted_at": "2026-07-18T10:30:00Z" } ]

Page layout:

Populates from: quizzes/ endpoint (single call � 3 statuses included, no merging needed)