EduTrack Online - Student API

Version: 1.3 Date: July 16, 2026 Target Audience: Next.js Frontend Developers (Student Features) Backend: Django REST Framework


Table of Contents

  1. Profile Management
  2. Change Password
  3. Course Discovery
  4. Enrollments
  5. Course Lectures
  6. Purchases
  7. Video Playback
  8. Video Watch Progress
  9. Balance
  10. Homeworks
  11. Quizzes
  12. Exams
  13. Study Materials
  14. Student Dashboard

GET /learning/exams//resume/

Description: Resume an active (not yet submitted) exam attempt. Returns remaining time, questions, and saved draft answers.

Authentication: Student

Success Response — 200 OK:

{ "submission_id": "integer", "timer_minutes": "integer", "timer_remaining": "number | null", "started_at": "datetime (ISO 8601)", "questions": [ { "answer_id": "integer", "question_id": "integer", "question_text": "string", "question_image": "string (URL) | null", "question_type": "string", "points": "integer", "choices": [ {"id": "integer", "text": "string", "image": "string (URL) | null", "order": "integer"} ], "saved_choice_ids": "array[integer]", "saved_written_answer": "string" } ] }

Error Responses:

Status Condition
400 No active attempt found. Start the exam first.
404 Exam not found or not available.


1. Profile Management


GET /accounts/profile/me/

Description: Get the current student's own profile.

Authentication: Student

Success Response — 200 OK:

{ "id": "Integer", "user_id": "Integer", "username": "String", "student_code": "String", "name_ar": "String", "name_en": "String", "full_name": "String", "phone_number": "String", "father_number": "String | null", "mother_number": "String | null", "school_type": "Integer | null", "school_type_name": "String | null", "grade": "Integer | null", "grade_name": "String | null", "division": "Integer | null", "division_name": "String | null", "school_name": "String | null", "birth_date": "Date (YYYY-MM-DD)", "gender": "String (male|female)", "gmail": "String", "governorate": "Integer", "governorate_name": "String | null", "area": "Integer", "area_name": "String | null", "status": "String", "is_active": "Boolean", "can_access_course": "Boolean", "status_history": [ { "changed_by": "String | null — Username of the admin who made the change", "from_status": "String | null", "to_status": "String", "reason": "String | null", "created_at": "DateTime (ISO 8601)" } ], "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" }

PATCH /accounts/profile/me/

Description: Update the current student's own profile. Only works when status='declined' — other statuses are blocked.

Authentication: Student

Content-Type: multipart/form-data or application/json

Request Body: All fields from the profile are optional for PATCH. Same validation rules as registration apply.

Success Response — 200 OK: Same structure as GET. On success, if the student was declined, their status auto-resets to pending and is_active becomes false — the SiteOwner must review and re-activate.

Error Responses:

Status Condition Response Body
403 Profile is verified {"error": "Cannot update a verified profile. Contact an administrator."}
403 Profile is pending or suspended {"error": "Cannot update profile while account is in this state."}
400 Email exists (other user) {"gmail": ["This email is already associated with an account."]}
400 Area/governorate mismatch {"area": ["The selected area does not belong..."]}
400 Missing required fields for state Same as registration validation errors

2. Change Password


POST /accounts/change-password/

Description: Allows an authenticated user to change their password. Requires the current password for verification. Invalidates all refresh tokens on success.

Authentication: Any authenticated user

Content-Type: application/json

Request Body:

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

Success Response — 200 OK:

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

Error Responses:

Status Condition Response Body
400 Missing fields {"error": "All password fields are required"}
400 Passwords don't match {"error": "New passwords do not match"}
400 Password too short {"error": "Password must be at least 8 characters"}
400 Weak password {"error": "This password is too common."} or {"error": "This password is entirely numeric."}
400 Wrong old password {"error": "Current password is incorrect"}
401 Not authenticated {"detail": "Authentication credentials were not provided."}

Business Rules:


3. Course Discovery


GET /courses/by-subject/<subject_id>/

Description: List courses for a specific subject. Auto-filtered by the authenticated student's grade, division, and school type. Only active courses are returned.

Authentication: Any authenticated user

Query Parameters:

Parameter Type Description
teacher Integer Filter by teacher ID
search String Search by name, description, or teacher name
ordering String created_at, name, -created_at (default: -created_at)

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "name": "String", "teacher": "Integer", "teacher_name": "String", "grade": "Integer", "grade_name": "String", "subject": "Integer", "subject_name": "String", "description": "String | null", "cover_picture": "String (URL) | null", "is_active": "Boolean", "topic_count": "Integer", "eligibility": { "eligible": "Boolean — Whether this student may enroll (grade/school_type/division match)", "reason": "String | null — 'grade' | 'division' | 'school_type' when not eligible" }, "created_at": "DateTime (ISO 8601)" } ] }

Error Responses:

Status Condition Response Body
401 Not authenticated {"detail": "Authentication credentials were not provided."}

Business Rules:


GET /courses/<id>/preview/

Description: Preview a course before enrolling. Shows topics and lectures with prices, but NO video URLs. Available to any authenticated user.

Authentication: Any authenticated user

Success Response — 200 OK:

{ "id": "Integer", "name": "String", "description": "String | null", "cover_picture": "String (URL) | null", "teacher": { "id": "Integer", "name": "String", "profile_picture": "String (URL) | null" }, "grade": { "id": "Integer", "name": "String" }, "subject": { "id": "Integer", "name": "String" }, "topic_count": "Integer", "total_lectures": "Integer", "eligibility": { "eligible": "Boolean — Whether this student may enroll (grade/school_type/division match)", "reason": "String | null — 'grade' | 'division' | 'school_type' when not eligible" }, "topics": [ { "id": "Integer", "name": "String", "description": "String | null", "order": "Integer", "lecture_count": "Integer", "picture": "String (URL) | null", "lectures": [ { "id": "Integer", "name": "String", "description": "String | null", "price": "String — Decimal as string", "final_price": "String — Decimal as string", "discount": "String — Decimal as string", "available_days": "Integer", "order": "Integer", "video_count": "Integer" } ] } ], "created_at": "DateTime (ISO 8601)" }

Error Responses:

Status Condition Response Body
401 Not authenticated {"detail": "Authentication credentials were not provided."}
404 Course not found {"error": "Course not found"}

4. Enrollments


POST /courses/enrollments/enroll/

Description: Student requests enrollment in a course. Creates a PENDING enrollment.

Authentication: Student

Content-Type: application/json

Request Body:

{ "course": "Integer (Required) — Course ID" }

Success Response — 201 Created:

{ "id": "Integer", "student": "Integer", "student_name": "String", "student_code": "String", "course": "Integer", "course_name": "String", "grade_name": "String", "teacher_name": "String", "status": "String — pending", "status_display": "String — Pending", "is_blocked": "Boolean", "block_reason": "String | null", "blocked_by_name": "String | null", "blocked_at": "DateTime | null", "unblocked_by_name": "String | null", "unblocked_at": "DateTime | null", "enrolled_at": "DateTime (ISO 8601)", "responded_by": "Integer | null", "responded_by_name": "String | null", "responded_at": "DateTime | null", "response_note": "String | null" }

Error Responses:

Status Condition Response Body
400 No student profile {"detail": "Student profile not found."}
403 Account not verified {"detail": "Your account is pending approval. You cannot enroll until an administrator verifies your account."}
400 Already enrolled {"detail": "You are already enrolled or have a pending request for this course."}
400 Grade mismatch {"detail": "String"}

Business Rules:


GET /courses/enrollments/

Description: Student views their own enrollments. Returns all statuses (pending/approved/rejected). For approved enrollments, includes course cover picture and topic/lecture counts.

Authentication: Student

Query Parameters:

Parameter Type Description
status String Filter: pending, approved, rejected

Success Response — 200 OK:

[ { "id": "Integer", "student": "Integer", "student_name": "String (en / ar)", "student_code": "String", "phone_number": "String", "father_number": "String | null", "mother_number": "String | null", "course": "Integer", "course_name": "String", "grade_name": "String", "teacher_name": "String", "subject_name": "String | null", "status": "String (pending|approved|rejected)", "status_display": "String (Pending|Approved|Rejected)", "balance": "String (Decimal) — '0.00' if no balance", "cover_picture": "String (URL) | null", "topic_count": "Integer | null — Only for approved enrollments", "total_lectures": "Integer | null — Only for approved enrollments", "is_blocked": "Boolean — Whether the teacher blocked this student from the course", "block_reason": "String | null — Why (when blocked)", "blocked_by_name": "String | null", "blocked_at": "DateTime | null", "unblocked_by_name": "String | null", "unblocked_at": "DateTime | null", "enrolled_at": "DateTime (ISO 8601)", "responded_by": "Integer | null", "responded_by_name": "String | null", "responded_at": "DateTime | null", "response_note": "String | null" } ]

Blocked courses: when is_blocked is true, the student cannot watch videos, buy lectures, or open the course — API calls return 403 with {blocked: true, reason}. The block is per-course and is set/removed by the teacher.


POST /courses/enrollments/<id>/cancel/

Description: Student cancels their own pending enrollment. The enrollment is deleted.

Authentication: Student

Success Response — 200 OK:

{ "message": "Enrollment cancelled successfully" }

Error Responses:

Status Condition Response Body
403 Not own enrollment {"error": "You can only cancel your own enrollments"}
400 Not pending {"error": "Cannot cancel enrollment with status: X"}
404 Enrollment not found {"error": "Enrollment not found"}

5. Course Lectures


GET /courses/<id>/lectures/

Description: Get lectures for a course the student is enrolled in. Shows purchase status for each lecture.

Authentication: Student (must be approved enrolled)

Success Response — 200 OK:

{ "course": { "id": "Integer", "name": "String" }, "topics": [ { "id": "Integer", "name": "String", "picture": "String (URL) | null", "order": "Integer", "lectures": [ { "id": "Integer", "name": "String", "description": "String | null", "price": "String — Decimal as string", "final_price": "String — Decimal as string", "discount": "String — Decimal as string", "available_days": "Integer", "order": "Integer", "video_count": "Integer", "materials_count": "Integer", "homeworks_count": "Integer", "quizzes_count": "Integer", "is_purchased": "Boolean", "purchase": { "id": "Integer", "purchased_at": "DateTime (ISO 8601)", "expires_at": "DateTime (ISO 8601)", "effective_expiry": "DateTime (ISO 8601)", "is_expired": "Boolean", "extra_days": "Integer", "amount_paid": "String — Decimal as string", "max_watch_count": "Integer — Maximum viewing sessions (default: 4)", "sessions_used": "Integer — Viewing sessions consumed", "sessions_remaining": "Integer — Remaining sessions", "has_active_session": "Boolean — Whether a 6-hour window is open", "active_session": "Object — LectureViewingSession data (if active)" } } ] } ] }

Note: The purchase object is only included if is_purchased is true. When present, it includes max_watch_count, sessions_used, sessions_remaining, has_active_session, and active_session (if a 6-hour viewing window is open).

Error Responses:

Status Condition Response Body
400 No student profile {"error": "Student profile not found"}
403 Blocked from course {"error": "You have been blocked from this course.", "blocked": true, "reason": "…"}
403 Not enrolled {"error": "You are not enrolled in this course"}
404 Course not found {"error": "Course not found"}

GET /courses/lectures/<id>/

Description: Get full details for a single lecture, including videos list, materials count, homework/quiz counts, and prerequisites. Students can access this for any lecture in a course they're enrolled in.

Authentication: Student (must be approved enrolled in the course)

Success Response — 200 OK:

{ "id": "Integer", "topic": "Integer", "topic_name": "String", "course_name": "String", "teacher_name": "String", "name": "String", "description": "String | null", "price": "Decimal", "discount": "Decimal", "final_price": "Decimal", "available_days": "Integer", "is_visible": "Boolean", "picture": "String (URL) | null", "order": "Integer", "video_count": "Integer", "materials_count": "Integer — Number of active study materials", "homeworks_count": "Integer — Number of published homeworks", "quizzes_count": "Integer — Number of published quizzes", "prerequisites": [ { "content_type": "String (quiz|exam)", "content_name": "String", "passing_score": "Decimal" } ], "videos": [ { "id": "Integer", "name": "String", "order": "Integer", "is_active": "Boolean", "thumbnail_url": "String | null" } ], "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" }

Error Responses:

Status Condition Response Body
400 No student profile {"error": "Student profile not found"}
403 Not enrolled {"error": "You are not enrolled in this course"}
404 Lecture not found {"error": "Lecture not found"}

GET /courses/lectures/<id>/available-assessments/

Description: Returns all quizzes and exams available for a lecture. Students see only published assessments. Teachers see all.

Authentication: Any authenticated user (students see only published)

Success Response — 200 OK:

{ "quizzes": [ { "id": "Integer", "title": "String", "lecture_id": "Integer", "lecture_name": "String" } ], "exams": [ { "id": "Integer", "title": "String" } ] }

Error Responses:

Status Condition
404 Lecture not found

GET /courses/lectures/<id>/prerequisites/

Description: List all prerequisites for a lecture. Shows what quiz or exam must be passed before accessing the lecture's videos.

Authentication: Any authenticated user (students can read)

Success Response — 200 OK:

{ "count": "Integer", "results": [ { "id": "Integer", "lecture": "Integer", "lecture_name": "String", "content_type": "String (quiz|exam)", "content_id": "Integer", "passing_score": "Decimal", "content_name": "String", "created_at": "DateTime (ISO 8601)" } ] }

Error Responses:

Status Condition
404 Lecture not found

6. Purchases


POST /courses/purchases/buy/

Description: Student purchases a lecture. Deducts the lecture's final price from the student's course balance.

Authentication: Student

Content-Type: application/json

Request Body:

{ "lecture": "Integer (Required) — Lecture ID" }

Success Response — 201 Created:

{ "id": "Integer", "student": "Integer", "lecture": "Integer", "lecture_name": "String", "topic_name": "String", "course_name": "String", "teacher_name": "String", "purchased_at": "DateTime (ISO 8601)", "expires_at": "DateTime (ISO 8601)", "effective_expiry": "DateTime (ISO 8601)", "amount_paid": "Decimal", "extra_days": "Integer", "is_expired": "Boolean", "reopened_by": "Integer | null", "reopened_by_name": "String | null", "reopened_at": "DateTime | null", "reopen_logs": [ { "reopened_by": "String | null", "reopened_at": "DateTime" } ], "max_watch_count": "Integer — Maximum viewing sessions (default: 4)", "sessions_used": "Integer — Viewing sessions consumed" }

Error Responses:

Status Condition Response Body
400 No student profile {"detail": "Student profile not found."}
403 Account not verified {"detail": "Your account must be verified before purchasing lectures."}
403 Blocked from course {"detail": "You have been blocked from this course.", "blocked": true, "reason": "…"}
403 Not enrolled/approved {"detail": "You must be enrolled and approved in this course before purchasing lectures."}
400 Already purchased and active {"detail": "You have already purchased this lecture and it's still active."}
400 Insufficient balance {"detail": "Insufficient balance. You need X more."}

Business Rules:


GET /courses/purchases/

Description: Student views their own purchased lectures.

Authentication: Student

Success Response — 200 OK: Array of purchased lectures.


POST /courses/purchases/<pk>/start-watching/

Description: Start a 6-hour viewing session for a purchased lecture. If there's already an active session (within 6h), returns the existing session. If not, creates a new 6h session and increments sessions_used.

Authentication: Student (must own the purchase)

Request Body: None

Success Response — 200 OK (existing session resumed):

{ "session": { "id": 1, "purchase": 5, "started_at": "2026-07-26T10:00:00Z", "expires_at": "2026-07-26T16:00:00Z", "is_active": true, "remaining_seconds": 21540 }, "sessions_used": 1, "sessions_remaining": 3, "message": "Resuming existing session." }

Success Response — 201 Created (new session):

{ "session": { "id": 2, "purchase": 5, "started_at": "2026-07-26T10:00:00Z", "expires_at": "2026-07-26T16:00:00Z", "is_active": true, "remaining_seconds": 21599 }, "sessions_used": 2, "sessions_remaining": 2, "message": "Session started. You have 6 hours to watch all videos." }

Error Responses:

Status Condition Response Body
400 No student profile {"error": "Student profile not found."}
403 Purchase expired {"error": "Your access to this lecture has expired."}
403 All sessions used {"error": "You have used all your viewing sessions. Contact your teacher to reopen access.", "sessions_used": 4, "sessions_remaining": 0}
404 Purchase not found {"detail": "Not found."}

Business Rules:


GET /courses/purchases/<pk>/active-session/

Description: Check for an active viewing session for a purchased lecture. Read-only — no session is created. Frontend uses this to decide which button to show: "Start watching" (no active session) or "Resume watching" (active session).

Authentication: Student (must own the purchase)

Success Response — 200 OK (active session exists):

{ "has_active_session": true, "session": { "id": 1, "purchase": 5, "started_at": "2026-07-26T10:00:00Z", "expires_at": "2026-07-26T16:00:00Z", "is_active": true, "remaining_seconds": 12000 }, "sessions_used": 1, "sessions_remaining": 3 }

Success Response — 200 OK (no active session, purchase still active):

{ "has_active_session": false, "is_expired": false, "sessions_used": 1, "sessions_remaining": 3 }

Success Response — 200 OK (purchase expired):

{ "has_active_session": false, "is_expired": true, "error": "Your access to this lecture has expired." }

Error Responses:

Status Condition Response Body
400 No student profile {"error": "Student profile not found."}
404 Purchase not found {"detail": "Not found."}

7. Video Playback


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

Description: Get a playback URL for a video. Enforces purchase check, expiry, active viewing session check, and prerequisites before returning the URL. Library 725542 uses MediaCage Basic DRM → the actual playback happens in Bunny's embed iframe; use embed_url. playback_url is the direct HLS manifest, kept for preview/dev contexts.

Authentication: Student (must have purchased the lecture)

Request Body: None

Success Response — 200 OK:

{ "playback_url": "String — HLS playlist URL (e.g. https://cdn/{guid}/playlist.m3u8)", "embed_url": "String — Bunny embed iframe URL, e.g. https://player.mediadelivery.net/embed/725542/{guid} (?token=…&expires=… when token auth is enabled)", "token": "String | null — Embed token (SHA256_HEX) when token auth is configured, else null", "expires_at": "Integer | null — Unix seconds the embed token expires (null when no token)", "expires_in": "Integer — Seconds the URL is valid for" }

Token expiry (students): the embed token is tied to the student's active 6-hour viewing sessionexpires_in = max(session_remaining + 15 min, 1 hour). A shared embed URL dies with the session window, so a copied link cannot be reused after the student's session ends. Teachers/assistants/siteowner previews get a flat 2 hours.

Access Rules:

Check Blocked?
Lecture not purchased 403
Purchase expired 403
No active viewing session 403
Prerequisite quiz not passed 403
Prerequisite exam not passed 403

Error Responses:

Status Condition Response Body
403 Blocked from course {"error": "You have been blocked from this course.", "blocked": true, "reason": "…"}
403 Not purchased {"error": "You have not purchased this lecture."}
403 Expired {"error": "Your access to this lecture has expired."}
403 No active session {"error": "No active viewing session. Click \"Start watching\" to begin a 6-hour session.", "sessions_used": 1, "sessions_remaining": 3}
403 Prerequisite not met {"error": "You must pass the quiz (ID X) before accessing this video."}
404 Video not found/not active {"error": "Video is not available."}

8. Video Watch Progress


GET /courses/progress/

Description: List the student's video watch progress.

Authentication: Student

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "student": "Integer", "student_name": "String", "student_code": "String", "video": "Integer", "video_name": "String", "lecture_name": "String", "course_name": "String", "progress_seconds": "Integer", "cumulative_watch_seconds": "Integer", "duration_seconds": "Integer | null", "progress_percentage": "Decimal — Based on cumulative_watch_seconds", "position_percentage": "Decimal — Based on progress_seconds (furthest position)", "is_completed": "Boolean", "last_watched_at": "DateTime (ISO 8601)", "platform_data": "Object | null" } ] }

POST /courses/progress/update/

Description: Update watch progress for a video. Requires an active viewing session (see start-watching endpoint).

Authentication: Student

Content-Type: application/json

Request Body:

{ "video": "Integer (Required) — Video ID", "progress_seconds": "Integer (Required) — Current playback position (furthest point reached)", "cumulative_watch_seconds": "Integer (Optional) — Cumulative wall-clock seconds genuinely watched while playing", "duration_seconds": "Integer (Optional) — Total video duration", "platform_data": "Object (Optional) — Platform-specific data" }

Success Response — 200 OK:

{ "id": "Integer", "student": "Integer", "video": "Integer", "progress_seconds": "Integer", "cumulative_watch_seconds": "Integer", "duration_seconds": "Integer | null", "progress_percentage": "Decimal — Based on cumulative_watch_seconds", "position_percentage": "Decimal — Based on progress_seconds (furthest position)", "is_completed": "Boolean", "last_watched_at": "DateTime (ISO 8601)", "platform_data": "Object | null" }

Error Responses:

Status Condition Response Body
400 Missing video {"video": ["This field is required."]}
403 Lecture not purchased {"detail": "You must purchase this lecture to track progress."}
403 No active session {"error": "No active viewing session. Click \"Start watching\" to begin a 6-hour session.", "sessions_used": 1, "sessions_remaining": 3}

Business Rules:


GET /courses/progress/<video_id>/

Description: Get progress for a specific video.

Authentication: Student

Success Response — 200 OK: Single progress object.

Error Responses:

Status Condition Response Body
404 Progress not found {"detail": "Not found."}

9. Balance


GET /payments/balance/

Description: Get all course balances for the authenticated student. Returns one entry per approved-enrolled course, with the student's available balance. Newly approved students with no transaction history are included with balance: "0.00".

Authentication: Student

Success Response — 200 OK:

[ { "id": "Integer | null — null if no CourseBalance record exists (balance is 0)", "student": "Integer", "student_name": "String", "student_code": "String", "course": "Integer", "course_name": "String", "teacher_name": "String", "teacher_picture": "String (URL) | null", "balance": "String — Decimal as string (e.g. '150.00' or '0.00')", "updated_at": "DateTime (ISO 8601) | null" } ]

Business Rules:


GET /courses/<id>/balance/

Description: Get a student's balance for a specific course. Returns a single balance value for the given course.

Authentication: Student (must be enrolled in the course)

Success Response — 200 OK:

{ "course_id": 1, "course_name": "Chemistry 3rd Secondary", "balance": "150.00" }

Error Responses:

Status Condition Response Body
401 Not authenticated {"detail": "Authentication credentials were not provided."}
404 Course not found {"error": "Course not found"}

POST /payments/codes/redeem/

Description: Student redeems a physical voucher code to add balance to a specific course. Codes are course-specific and can only be used once.

Authentication: Student (must be approved enrolled in the course)

Request Body:

{ "code": "String (Required) — Recharge code (e.g. 'X7K9-M2P4-QR1W-L5D8')", "course": "Integer (Required) — Course ID" }

Success Response — 200 OK:

{ "detail": "String - Success message", "new_balance": "String (Decimal) - New balance", "transaction_id": "Integer" }

Error Responses:

Status Condition Response Body
400 Code already used {"detail": "This code has already been used."}
400 Code expired {"detail": "This code has expired."}
400 Wrong course {"detail": "This code cannot be used for this course."}
403 Not enrolled {"detail": "You must be enrolled and approved..."}
404 Invalid code {"detail": "Invalid code."}

Business Rules:


GET /payments/transactions/

Description: Full balance transaction ledger for the logged-in student. Shows every balance change with before and after snapshots. Auto-filtered to the authenticated student.

Authentication: Student (auto-filtered to own transactions)

Query Parameters:

Parameter Type Description
course Integer Filter by course ID
transaction_type String Filter: code_redeemed or lecture_purchase
search String Search in description text
start_date String Filter by date range start (YYYY-MM-DD)
end_date String Filter by date range end (YYYY-MM-DD)
ordering String created_at or -created_at (default: newest first)
page Integer Page number
page_size Integer Items per page (default: 50)

Success Response — 200 OK:

{ "count": "Integer", "results": [ { "id": "Integer", "student": "Integer", "student_name": "String (name_en / name_ar)", "course": "Integer", "course_name": "String", "teacher_name": "String", "teacher_picture": "String (URL) | null", "transaction_type": "String (code_redeemed|lecture_purchase)", "transaction_type_display": "String (Code Redeemed|Lecture Purchased)", "amount": "String (Decimal)", "balance_before": "String (Decimal)", "balance_after": "String (Decimal)", "source_type": "String", "source_type_display": "String", "source_id": "Integer", "description": "String", "metadata": { "code": "String — The code string (for code_redeemed)", "lecture_name": "String — Lecture name (for lecture_purchase)" }, "performed_by": "Integer | null", "performed_by_name": "String | null — 'Student' for self-purchases, teacher name for code redemptions", "created_at": "DateTime (ISO 8601)" } ] }

10. Homeworks

Homework uses the Bubble Sheet model. Students see question numbers with A/B/C/D choices, select answers, and submit. All questions are auto-graded immediately.


GET /learning/homeworks/

Description: List homeworks. For students, returns homeworks for purchased lectures including submission status, score, and grade visibility.

Authentication: Student

Success Response — 200 OK:

{ "count": "integer", "results": [ { "id": "integer", "lecture": "integer", "lecture_name": "string", "title": "string", "description": "string", "is_published": "boolean", "show_grades": "boolean", "total_points": "integer", "question_count": "integer", "is_submitted": "boolean", "submission_id": "integer | null", "score": "string (decimal) | null", "status": "string (submitted|graded) | null", "submitted_at": "datetime (ISO 8601) | null", "created_at": "datetime (ISO 8601)" } ] }

GET /learning/homeworks/<id>/

Description: Get the homework detail (bubble questions with choices count). Student sees only question number and choices count — no correct answers.

Authentication: Student (must have purchased the lecture)

Access Control:

Success Response — 200 OK:

{ "id": "integer", "lecture": "integer", "lecture_name": "string", "title": "string — Homework title", "description": "string", "bubble_questions": [ {"id": "integer", "order": "integer", "choices_count": "integer"}, {"id": "integer", "order": "integer", "choices_count": "integer"} ] }

PATCH /learning/homeworks/<id>/draft/

Description: Auto-save homework answers as a draft. Students can close the page and come back — their choices are restored.

Request Body:

{ "answers": { "1": "A", "2": "B", "3": "A,C" } }

Success Response — 200 OK:

{ "answers": {"1": "A", "2": "B", "3": "A,C"}, "updated_at": "datetime (ISO 8601)" }

GET /learning/homeworks/<id>/draft/

Description: Load the saved draft answers. Returns empty object if no draft exists.

Success Response — 200 OK:

{ "answers": {"1": "A", "2": "B", "3": "A,C"}, "updated_at": "datetime (ISO 8601)" }

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

Description: Submit bubble sheet answers. Auto-graded immediately.

Authentication: Student

Access Control:

Request Body:

{ "bubble_answers": [ {"bubble_question_id": "integer", "selected_choice": "A"}, {"bubble_question_id": "integer", "selected_choice": "C"}, {"bubble_question_id": "integer", "selected_choice": "A,C"} ] }

Success Response — 200 OK:

{ "detail": "Homework submitted and auto-graded.", "submission_id": "integer", "score": "string (decimal)", "status": "string (submitted|graded)" }

GET /learning/homework-submissions/<id>/

Description: View submission results with per-question details.

Access Control:

Authentication: Student (own submissions), Teacher, Assistant

Success Response — 200 OK:

{ "id": "integer", "homework": "integer", "score": "string (decimal)", "status": "string", "submitted_at": "datetime (ISO 8601)", "total_points": "integer", "bubble_answers": [ { "bubble_question": "integer", "question_order": "integer", "selected_choice": "string", "correct_answer": "string | null — Only shown to students if show_grades=true", "answer_explanation": "string | null — Only shown to students if show_grades=true", "is_correct": "boolean", "points_earned": "string (decimal)" } ] }

11. Quizzes


Index

# Endpoint Method Who
11.1 /learning/quizzes/ GET Student
11.2 /learning/quizzes/<id>/ GET Any authenticated
11.3 /learning/quizzes/<id>/start/ POST Student
11.4 /learning/quizzes/<id>/resume/ GET Student
11.5 /learning/quiz-answers/<id>/save-draft/ PATCH Student
11.6 /learning/quizzes/<id>/submit/ POST Student
11.7 /learning/quiz-submissions/<id>/ GET Student (own)

11.1 GET /learning/quizzes/

Description: List quizzes. For students, returns quizzes for purchased lectures including quiz status, submission info, score, and visibility.

Authentication: Student

Success Response — 200 OK:

{ "count": "integer", "results": [ { "id": "integer", "lecture": "integer", "lecture_name": "string", "title": "string", "description": "string", "is_active": "boolean", "is_published": "boolean", "total_points": "integer", "question_count": "integer", "settings": { "timer_minutes": "integer", "score_visibility": "string (immediate|manual)", "answers_visibility": "string (immediate|manual)", "question_order": "string (fixed|random)", "max_attempts": "integer — Number of allowed attempts (quizzes are multi-attempt)" }, "quiz_status": "string (not_started|in_progress|submitted)", "submission_id": "integer | null", "score": "string (decimal) | null", "score_visible": "boolean", "created_at": "datetime (ISO 8601)" } ] }

11.2 GET /learning/quizzes//

Description: View quiz details. What you see depends on whether you've started the quiz.

Access Control:

Status What you see
Not started (no purchase or no attempt) id, title, description, total_points, question_count, settingsquestions are hidden
Has started (active or submitted attempt) Full quiz with questions, choices, your saved/submitted answers
Not purchased the lecture 404 Not Found

Authentication: Any authenticated user

Notes:


11.3 POST /learning/quizzes//start/

Description: Start a quiz attempt. Timer starts counting from this moment. The returned submission_id is used for submitting answers and saving drafts. Quizzes are multi-attempt — one attempt per student is allowed up to settings.max_attempts (default 1).

Authentication: Student

Access Checks (all must pass):

Check Failure response
You must have purchased the lecture 403
You must be enrolled and approved in the course 403
Quiz must be published (is_published=true) 404
Attempts remaining (submitted attempts < max_attempts) 400

Request Body: None

Success Response — 200 OK:

{ "submission_id": "integer — Save this! Required for submit and draft endpoints", "timer_minutes": "integer — Time limit. 0 means no time limit.", "started_at": "datetime (ISO 8601) — When this attempt began", "questions": [ { "answer_id": "integer — ID to use when saving drafts or submitting", "quiz_question_id": "integer", "question_text": "string", "question_image": "string (URL) | null", "question_type": "string (mcq_single|mcq_multiple|written)", "points": "integer", "choices": [ {"id": "integer", "text": "string", "image": "string (URL) | null", "order": "integer"} ] } ] }

Error Responses:

Status Body
403 {"detail": "You must purchase this lecture to take the quiz."}
403 {"detail": "You must be enrolled in this course."}
400 {"detail": "You already have an active quiz attempt. Resuming it.", "submission_id": X, "status": "in_progress"} — call GET /resume/ instead
400 {"detail": "You have used all your allowed attempts for this quiz. If you need to retake, ask your teacher to delete your previous submissions."}
404 {"detail": "Quiz not found or not available."}

11.4 GET /learning/quizzes//resume/

Description: Resume an active quiz attempt (one that hasn't been submitted yet). Returns remaining time, questions, and any previously saved draft answers. Use this when the page is reloaded or the student returns to the quiz.

Authentication: Student

Success Response — 200 OK:

{ "submission_id": "integer", "timer_minutes": "integer — Original time limit", "timer_remaining": "number | null — Minutes remaining. Null if no timer. Can be 0 if expired.", "started_at": "datetime (ISO 8601)", "questions": [ { "answer_id": "integer", "quiz_question_id": "integer", "question_text": "string", "question_image": "string (URL) | null", "question_type": "string", "points": "integer", "choices": [ {"id": "integer", "text": "string", "order": "integer"} ], "saved_choice_ids": "array[integer] — Previously saved MCQ choices (empty array if none)", "saved_written_answer": "string — Previously saved written answer (empty string if none)" } ] }

Error Responses:

Status Condition
400 No active attempt found — you must start the quiz first
404 Quiz not found or not available

11.5 PATCH /learning/quiz-answers/<answer_id>/save-draft/

Description: Save individual answer choices while the student is working. This is the auto-save endpoint — call it periodically (every 5-10 seconds) to preserve progress. Saved answers are restored when you resume the quiz. This does NOT submit the quiz — you must call /submit/ separately.

Authentication: Student

Request Body:

{ "choice_ids": "array[integer] — Selected choice IDs (for mcq_single or mcq_multiple questions)", "written_answer": "string — Written answer text (for written questions)" }

Success Response — 200 OK:

{ "saved": true, "answer_id": "integer" }

11.6 POST /learning/quizzes//submit/

Description: Submit quiz answers for auto-grading. MCQ questions are graded immediately. Written questions receive 0 points until the teacher manually grades them.

Grading Rules:

Question Type Correct Points
mcq_single Exactly the correct choice(s) selected Full points
mcq_single Wrong choice(s) selected 0
mcq_multiple All correct choices selected AND no extras Full points
mcq_multiple Subset of correct choices selected (no extras) Partial credit (proportional)
mcq_multiple Any incorrect choice selected 0
written Auto-graded 0 until teacher grades manually

Timer Behavior: If the timer has expired (elapsed time >= timer_minutes), the submission is auto-submitted and marked is_timed_out with score 0; the submit request returns 400 {"detail": "Time limit exceeded. Quiz auto-submitted with score 0."}. The student must submit BEFORE time runs out.

Authentication: Student

Request Body:

{ "answers": [ { "quiz_question_id": "integer (required)", "choice_ids": "array[integer] — Selected choice IDs (for mcq_single and mcq_multiple)", "written_answer": "string — Written answer text (for written questions)" } ] }

Success Response — 200 OK:

{ "detail": "Quiz submitted successfully.", "submission_id": "integer", "score": "string (decimal) | null — Null if score_visibility=manual and not released", "score_visible": "boolean", "answers_visible": "boolean" }

Error Responses:

Status Condition
400 No active attempt found (start the quiz first)
400 Quiz already submitted
400 Time limit exceeded (auto-submitted, is_timed_out=true)

11.7 GET /learning/quiz-submissions//

Description: View a specific submission with per-answer results. Shows selected choices, whether correct, and points earned.

Authentication: Student (own submissions only)

Access Control:

Success Response — 200 OK:

{ "id": "integer", "quiz": "integer", "quiz_title": "string", "student": "integer", "student_name": "string", "student_name_ar": "string", "student_code": "string", "score": "string (decimal) | null", "started_at": "datetime (ISO 8601)", "submitted_at": "datetime (ISO 8601) | null", "is_timed_out": "boolean", "score_visible": "boolean", "answers_visible": "boolean", "answers": [ { "quiz_question": "integer", "question_text": "string", "selected_standalone_choice_texts": "array[string]", "correct_standalone_choice_texts": "array[string] — Only shown if answers_visible=true", "written_answer": "string", "is_correct": "boolean | string | null — For MCQ: true/false. For written: 'corrected' or 'not_corrected'.", "points_earned": "integer | float — e.g. 1 instead of '1.00'. Float if non-integer.", "feedback": "string | null — Teacher's feedback for written questions (null if not graded or not a written question).", "display_order": "integer" } ] }

12. Exams


Index

# Endpoint Method Who
12.1 /learning/exams/ GET Student
12.2 /learning/exams/<id>/start/ POST Student
12.3 /learning/exams/<id>/resume/ GET Student
12.4 /learning/exams/<id>/submit/ POST Student
12.5 /learning/exam-submissions/<id>/ GET Student (own)

12.1 GET /learning/exams/

Description: List exams. For students, returns exams for their enrolled courses including quiz status, submission info, score, and visibility.

Authentication: Student

Success Response — 200 OK:

{ "count": "integer", "results": [ { "id": "integer", "course": "integer", "course_name": "string", "title": "string", "description": "string", "is_active": "boolean", "is_published": "boolean", "total_points": "integer", "question_count": "integer", "open_date": "datetime (ISO 8601) | null", "close_date": "datetime (ISO 8601) | null", "quiz_status": "string (not_started|in_progress|submitted)", "submission_id": "integer | null", "score": "string (decimal) | null", "score_visible": "boolean", "created_at": "datetime (ISO 8601)" } ] }

12.2 POST /learning/exams//start/

Description: Start an exam attempt. Works identically to quiz start. Timer begins, questions are returned. The exam must be within open_date and close_date to start. Exams are single-attempt — a student can start an exam only once, ever.

Authentication: Student

Access Checks:

Check Failure response
Must be enrolled and approved in the course 403
Current time must be after open_date (if set) 403
Current time must be before close_date (if set) 403
Must not have already started this exam 400

Request Body: None

Error Responses:

Status Body
403 {"detail": "You must be enrolled in this course."}
403 {"detail": "This exam is not yet open."}
403 {"detail": "This exam has closed."}
400 {"detail": "You have already started this exam. If you need to retake, ask your teacher to delete your previous submission."}
404 {"detail": "Exam not found or not available."}

Success Response — 200 OK:

{ "submission_id": "integer", "timer_minutes": "integer", "started_at": "datetime (ISO 8601)", "questions": [ { "answer_id": "integer", "question_id": "integer", "question_text": "string", "question_image": "string (URL) | null", "question_type": "string (mcq_single|mcq_multiple|written)", "points": "integer", "choices": [ {"id": "integer", "text": "string", "image": "string (URL) | null", "order": "integer"} ] } ] }

12.3 POST /learning/exams//submit/

Description: Submit exam answers. Same grading logic as quizzes. Auto-grades MCQ, stores written for manual grading. Timer rules apply — if time expires, submission is rejected.

Authentication: Student

Request Body:

{ "answers": [ { "question_id": "integer (required)", "choice_ids": "array[integer] — Selected choice IDs (for MCQ)", "written_answer": "string — Written answer text (for written questions)" } ] }

Success Response — 200 OK:

{ "detail": "Exam submitted successfully.", "submission_id": "integer", "score": "string (decimal) | null", "score_visible": "boolean", "answers_visible": "boolean" }

12.4 GET /learning/exam-submissions//

Description: View a specific exam submission with per-answer results.

Authentication: Student (own submissions only)

Access Control:

Success Response: Same structure as quiz submission detail (see 11.7).


13. Study Materials


GET /materials/

Description: Get all study materials for lectures the student has purchased.

Authentication: Student

Success Response — 200 OK:

[ { "id": "integer - Unique identifier", "lecture": "integer - Lecture ID", "lecture_name": "String - Lecture name", "title": "String - Material title", "file_url": "String (URL) - File URL", "is_active": "boolean - Whether this item is active", "created_at": "DateTime (ISO 8601)" } ]

14. Student Dashboard

These endpoints power the student dashboard pages — a unified view of all purchased lectures, homeworks, and quizzes across all enrolled courses.


GET /courses/my-lectures/

Description: Returns ALL purchased lectures across all enrolled courses for the student. Includes expiry status, active session indicator, content counts (videos, materials, homeworks, quizzes), and session tracking.

Authentication: Student (requires student_profile)

Success Response — 200 OK:

[ { "purchase_id": "integer", "lecture_id": "integer", "lecture_name": "string", "topic_id": "integer", "topic_name": "string", "topic_picture": "string (URL) | null", "lecture_picture": "string (URL) | null", "course_id": "integer", "course_name": "string", "teacher_name": "string", "price": "string (decimal)", "purchased_at": "datetime (ISO 8601)", "expires_at": "datetime (ISO 8601)", "effective_expiry": "datetime (ISO 8601)", "is_expired": "boolean", "extra_days": "integer", "max_watch_count": "integer", "sessions_used": "integer", "sessions_remaining": "integer", "has_active_session": "boolean", "video_count": "integer", "materials_count": "integer", "homeworks_count": "integer", "quizzes_count": "integer" } ]

Error Responses:

Status Condition Response Body
400 No student profile {"error": "Student profile not found"}
401 Not authenticated {"detail": "Authentication credentials were not provided."}

Blocked courses are excluded: lectures belonging to a course where the student's approved enrollment is blocked by the teacher do not appear in this list.


GET /learning/homeworks/

Description: List homeworks with submission status. For students, shows all homeworks for purchased lectures including whether submitted, score, and grade visibility. (Replaces the removed /homeworks/all/ endpoint.)

Authentication: Student

Success Response — 200 OK:

{ "count": "integer", "results": [ { "id": "integer", "lecture": "integer", "lecture_name": "string", "title": "string", "description": "string", "is_published": "boolean", "show_grades": "boolean", "total_points": "integer", "question_count": "integer", "is_submitted": "boolean", "submission_id": "integer | null", "score": "string (decimal) | null", "status": "string (submitted|graded) | null", "submitted_at": "datetime (ISO 8601) | null", "created_at": "datetime (ISO 8601)" } ] }

Error Responses:

Status Condition Response Body
400 No student profile {"error": "Student profile not found"}
401 Not authenticated {"detail": "Authentication credentials were not provided."}

GET /learning/quizzes/

Description: List quizzes with student progress status. For students, shows all quizzes for purchased lectures with quiz status (not_started/in_progress/submitted), submission info, and score visibility. (Replaces the removed /quizzes/all/ endpoint.)

Authentication: Student

Success Response — 200 OK:

{ "count": "integer", "results": [ { "id": "integer", "lecture": "integer", "lecture_name": "string", "title": "string", "description": "string", "is_active": "boolean", "is_published": "boolean", "total_points": "integer", "question_count": "integer", "settings": { "timer_minutes": "integer", "score_visibility": "string", "answers_visibility": "string", "question_order": "string", "max_attempts": "integer" }, "quiz_status": "string (not_started|in_progress|submitted)", "submission_id": "integer | null", "score": "string (decimal) | null", "score_visible": "boolean", "created_at": "datetime (ISO 8601)" } ] }

Error Responses:

Status Condition Response Body
400 No student profile {"error": "Student profile not found"}
401 Not authenticated {"detail": "Authentication credentials were not provided."}

GET /courses/student/dashboard/

Description: One consolidated student landing page endpoint. Returns enrolled courses with balances, lecture stats, nearly-expired lectures, homework stats, and quiz stats in a single response. Replaces 5 separate API calls.

Authentication: Student

Success Response — 200 OK:

{ "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" } }

Error Responses:

Status Condition Response Body
400 No student profile {"error": "Student profile not found"}
401 Not authenticated {"detail": "Authentication credentials were not provided."}

Documentation generated by OpenCode AI Agent Project: EduTrack Online Backend Last Updated: July 12, 2026