- Restructure: move backend from new_project/ to backend/ - Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks) - Add docs/ with SRS specs, user stories, and workflow documentation - Update .gitignore for new directory layout Workflows implemented: WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration, WF4 General English Exam, WF5 Course Generation, WF6 Entity Student Onboarding, AI Course Generation, Adaptive Learning Engine UI, White-Label Branding, Score Release Made-with: Cursor
94 KiB
EnCoach Platform -- Backend Software Requirements Specification (Odoo 19)
Document Version: 1.1
Date: April 2026
Status: Active -- Ready for Development
Source Document: encoach_workflows_v3.pdf (v3.0, April 2026)
Frontend Reference: ENCOACH_WORKFLOWS_FRONTEND_SRS.md (v1.1)
Audience: Odoo Developer (full-stack, using Cursor IDE)
Scope: All new Odoo modules, database models, API endpoints, business logic, and AI integrations required to implement the 6 platform workflows, exam template paths (international + custom), adaptive learning engine (4 phases), Math/IT support, and white-labelling.
Implementation Context
| Artifact | Location |
|---|---|
| Frontend Repository | https://git.albousalh.com/devops/encoach_frontend_new_v2.git (branch: main) |
| Backend Repository | https://git.albousalh.com/devops/encoach_backend_new_v2.git (branch: main) |
| Staging Frontend | http://5.189.151.117:3000 |
| Staging Backend (Odoo 19) | http://5.189.151.117:8069 |
| Backend Stack | Odoo 19, Python 3.11, PostgreSQL 16, Docker Compose |
| AI Stack | OpenAI GPT (4o, 3.5-turbo), OpenAI Whisper (base model, local), AWS Polly (neural TTS), FAISS + Sentence Transformers (all-MiniLM-L6-v2) |
| LMS Foundation | OpenEduCat 19 (14 community modules) |
Table of Contents
Part I -- Context and Baseline
Part II -- Database Schema 4. Content Tables 5. Exam Tables 6. User and Profile Tables 7. Course Tables 8. Progress and Results Tables 9. Adaptive Learning Tables 10. Entity and Institutional Tables 11. AI Generation Tables
Part III -- Workflow 1: User Signup 12. CAPTCHA Integration 13. OTP Email Service 14. Onboarding Wizard Data Model
Part IV -- Workflow 2: Placement Test 15. CAT Engine 16. Question Bank with IRT Parameters 17. CEFR Mapping Algorithm 18. Speaking AI Evaluation
Part V -- Workflow 3: Exam Configuration (International + Custom) 19. Exam Template Architecture 20. Fixed Template System (International) 21. Content Pool Query Engine 22. Assembly Modes 23. Custom Template System 24. Exam Validation Rules
Part VI -- Workflow 4: General English Exam 25. Exam Session Management 26. Auto-Scoring Engine 27. Score Release Gate 28. PDF Report Generation with QR Code
Part VII -- Workflow 5: Course Generation 29. Gap Analysis Engine 30. Auto Course Structure Generation 31. Adaptive Progression Engine
Part VIII -- Workflow 6: Entity Student Onboarding 32. CSV Parsing and Validation 33. Bulk Account Creation 34. Credential Email Service 35. Entity Level Mapping
Part IX -- AI Course Generation 36. General English AI Content Pipeline 37. AI IELTS Content Pipeline 38. Quality Gate Engine 39. IELTS Standards Validation Engine
Part X -- Adaptive Learning Engine (4 Phases) 40. Phase 1 -- Rule-Based Engine (MVP) 41. Phase 2 -- IRT-Based CAT 42. Phase 3 -- Collaborative Filtering 43. Phase 4 -- Full ML
Part XI -- White-Labelling 44. Entity Branding Model 45. Subdomain Routing
Part XII -- Math and IT Backend 46. Subject-Specific Question Types 47. Subject Taxonomy Extension
Part XIII -- API Endpoint Specification 48. Auth and Signup Endpoints 49. Placement Test Endpoints 50. Exam Template Endpoints 51. IELTS Exam Endpoints 52. Custom Exam Endpoints 53. Exam Session Endpoints 54. Grading Endpoints 55. Course Generation Endpoints 56. AI Course Generation Endpoints 57. Entity Onboarding Endpoints 58. Adaptive Engine Endpoints 59. Entity and Branding Endpoints 60. Score Release Endpoints 61. Report and Verification Endpoints 62. Taxonomy Endpoints
Part XIV -- AI/ML Service Integration 63. OpenAI GPT Integration 64. OpenAI Whisper Integration 65. Quality Gate Algorithms 66. IRT Mathematical Model
Part I -- Context and Baseline
1. Introduction
1.1 Purpose
This document specifies every backend component required to implement the EnCoach platform workflows as defined in encoach_workflows_v3.pdf (v3.0). It covers Odoo modules, database models, API endpoints, business logic, AI/ML integrations, and the adaptive learning engine across all 4 implementation phases.
1.2 Scope
The backend must support 6 workflows, each with multiple phases:
| # | Workflow | Key Backend Components |
|---|---|---|
| 1 | User Signup (Individual) | CAPTCHA verification, OTP email, onboarding data model |
| 2 | Placement Test | CAT engine with IRT, question bank calibration, speaking AI eval |
| 3 | IELTS Exam Configuration | Fixed template system, content pool query, assembly modes |
| 4 | General English Exam | Session management, auto-scoring, score release gate, PDF+QR |
| 5 | Course Generation | Gap analysis engine, auto course structure, adaptive progression |
| 6 | Entity Student Onboarding | CSV parsing, bulk account creation, credential emails |
| -- | AI Course Generation | General English + IELTS content pipelines, quality gates |
| -- | Adaptive Learning (4 phases) | Rule-based, IRT-CAT, collaborative filter, full ML |
| -- | White-Labelling | Entity branding, subdomain routing |
| -- | Math/IT | Subject-specific question types, taxonomy extension |
1.3 Design Principles
These principles from the workflow document MUST be enforced in the backend:
- Exam type is always the root parameter. All content queries, scoring algorithms, and course generation logic branch on
exam_type(Academic vs General Training) first. - All results stored on all paths. Every exam attempt is written to
student_attemptsregardless of pass/fail. No early exit without a DB write. - Shared content database. The same content tables serve both exam and course workflows. Content is never duplicated.
- Entity data isolation. All student data records include
entity_id(nullable FK).entity_id=nullmeans individual student.entity_id=Xmeans institutional. Queries must be scoped by entity when the caller is an entity admin. - Score release gate. Results for official exams (
results_release_mode=manual_approval) are NOT visible to students until an entity admin approves release.
2. Existing System Summary
2.1 Current Module Inventory
The backend currently has 27 custom modules and 14 OpenEduCat modules:
Custom encoach_* modules (27):
| Module | Purpose |
|---|---|
encoach_core |
Base models, extends res.users |
encoach_api |
Main API controller package (16 controllers) |
encoach_lms_api |
LMS API controller package (29 controllers) |
encoach_adaptive_api |
Adaptive learning API (6 controllers) |
encoach_resources |
Resource management REST API |
encoach_taxonomy |
Subject taxonomy models |
encoach_adaptive |
Adaptive learning models |
encoach_adaptive_ai |
AI-powered adaptive features |
encoach_exam |
Exam engine models |
encoach_ai |
Core AI integration |
encoach_ai_generation |
AI content generation |
encoach_ai_grading |
AI grading engine |
encoach_ai_media |
AI media (TTS, video) |
encoach_courseware |
Chapter and material models |
encoach_communication |
Discussion boards, messaging |
encoach_notification |
Notification engine |
encoach_faq |
FAQ system |
encoach_approval |
Approval workflows |
encoach_assignment |
Assignment management |
encoach_classroom |
Classroom management |
encoach_stats |
Statistics and analytics |
encoach_training |
Training content |
encoach_subscription |
Subscriptions and payments |
encoach_registration |
Registration management |
encoach_ticket |
Support ticketing |
encoach_sis |
Student Information System integration |
encoach_branding |
White-label branding |
OpenEduCat modules (14):
openeducat_core, openeducat_timetable, openeducat_attendance, openeducat_exam, openeducat_assignment, openeducat_admission, openeducat_classroom, openeducat_facility, openeducat_fees, openeducat_library, openeducat_activity, openeducat_parent, openeducat_erp, theme_web_openeducat
2.2 Current API Routes
~377 REST endpoints across 4 controller packages:
| Package | Controllers | Route Count |
|---|---|---|
encoach_api |
16 | ~100 |
encoach_lms_api |
29 | ~200 |
encoach_adaptive_api |
6 | ~40 |
encoach_resources |
1 | ~10 |
2.3 Docker Deployment
services:
db:
image: postgres:16
environment:
POSTGRES_DB: encoach_v2
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
odoo:
build: .
image: encoach-backend:latest
ports:
- "8069:8069"
volumes:
- ./new_project/custom_addons:/mnt/custom_addons:ro
- ./new_project/openeducat_erp-19.0:/mnt/openeducat:ro
3. Module Architecture
3.1 New Modules Required
The following new Odoo modules must be created to support the workflow features:
| Module | Purpose | Dependencies |
|---|---|---|
encoach_signup |
CAPTCHA, OTP, onboarding wizard logic | encoach_core |
encoach_placement |
CAT engine, placement session, IRT scoring | encoach_core, encoach_exam, encoach_adaptive |
encoach_ielts_template |
Fixed IELTS template, content pool query, assembly | encoach_exam, encoach_resources |
encoach_scoring |
Auto-scoring, rubric scoring, score release gate | encoach_exam, encoach_core |
encoach_course_generation |
Gap analysis, auto course structure, module builder | encoach_core, encoach_resources, encoach_adaptive |
encoach_entity_onboarding |
CSV upload, bulk create, credential emails | encoach_core, encoach_sis |
encoach_ai_course |
AI course generation (English + IELTS pipelines) | encoach_ai_generation, encoach_resources, encoach_adaptive |
encoach_quality_gate |
Content quality validation engine | encoach_ai_generation |
encoach_ielts_validation |
IELTS-specific two-layer validation pipeline | encoach_quality_gate, encoach_ielts_template |
encoach_pdf_report |
PDF generation with QR code | encoach_scoring |
encoach_verification |
Public score verification | encoach_scoring |
encoach_math |
Math question types, formula storage | encoach_exam, encoach_taxonomy |
encoach_it |
IT question types, code execution | encoach_exam, encoach_taxonomy |
3.2 Modified Existing Modules
| Module | Modifications |
|---|---|
encoach_core |
Add first_login, account_source, entity_id fields to res.users |
encoach_branding |
Add white-label configuration fields (logo, colours, subdomain, favicon) |
encoach_adaptive |
Extend with IRT ability model, engine phases, signal/decision logging |
encoach_exam |
Add results_release_mode, IRT parameters (a, b, c) to questions |
encoach_resources |
Add cefr_level, grammar_topic, vocab_band, ai_generated, approved fields |
encoach_taxonomy |
Extend for Math and IT subject hierarchies |
Part II -- Database Schema
4. Content Tables
4.1 encoach.passage
| Field | Type | Required | Notes |
|---|---|---|---|
id |
Integer (auto) | Yes | Primary key |
exam_type |
Selection (academic, general_training, general_english) |
Yes | Root parameter |
section_num |
Integer | Yes | Passage position in section |
topic_category |
Char | Yes | e.g., "environment", "technology" |
body_text |
Text | Yes | Full passage content |
difficulty |
Selection (easy, medium, hard) |
Yes | |
status |
Selection (draft, active, retired, flagged) |
Yes | Default: draft |
word_count |
Integer | Yes | Computed on save |
ai_generated |
Boolean | No | Default: False |
approved |
Boolean | No | Default: False |
ielts_certified |
Boolean | No | Default: False |
generation_brief |
Jsonb | No | AI generation parameters |
validation_errors |
Jsonb | No | Quality gate error details |
cefr_level |
Selection (pre_a1,a1,a2,b1,b2,c1,c2) |
No | For CEFR-tagged content |
4.2 encoach.audio.file
| Field | Type | Required | Notes |
|---|---|---|---|
id |
Integer (auto) | Yes | |
exam_type |
Selection | Yes | |
part |
Integer (1--4) | Yes | Listening part number |
context_type |
Selection (conversation, monologue) |
Yes | |
topic |
Char | Yes | |
audio_url |
Char | Yes | URL or attachment reference |
transcript |
Text | No | Full transcript text |
difficulty |
Selection | Yes | |
ai_generated |
Boolean | No | |
approved |
Boolean | No | |
ielts_certified |
Boolean | No | |
generation_brief |
Jsonb | No | |
validation_errors |
Jsonb | No |
4.3 encoach.question
| Field | Type | Required | Notes |
|---|---|---|---|
id |
Integer (auto) | Yes | |
skill |
Selection (listening, reading, writing, speaking, grammar, vocabulary) |
Yes | |
source_id |
Many2one (passage/audio/prompt/card) | No | Link to source content |
question_type |
Selection | Yes | mcq, mcq_multi, tfng, ynng, gap_fill, short_answer, form_completion, note_completion, map_labelling, matching, summary_completion, heading_matching, matching_features, numerical, expression, code_completion, code_output, sql_query |
stem |
Text | Yes | Question text (supports KaTeX for Math) |
options |
Jsonb | No | For MCQ: [{"label": "A", "text": "..."}, ...] |
correct_answer |
Jsonb | Yes | For auto-scored: answer value(s). For numerical: {"value": 4, "tolerance": 0.01} |
marks |
Float | Yes | Default: 1.0 |
difficulty |
Selection | Yes | |
irt_a |
Float | No | IRT discrimination parameter |
irt_b |
Float | No | IRT difficulty parameter |
irt_c |
Float | No | IRT guessing parameter |
ai_generated |
Boolean | No | |
ielts_certified |
Boolean | No | |
format_validated |
Boolean | No | IELTS format compliance flag |
subject_id |
Many2one (encoach.subject) |
No | For subject-agnostic question bank |
topic_id |
Many2one (encoach.topic) |
No |
4.4 encoach.writing.prompt
| Field | Type | Required | Notes |
|---|---|---|---|
id |
Integer (auto) | Yes | |
exam_type |
Selection | Yes | |
task |
Selection (task1, task2) |
Yes | |
writing_type |
Char | Yes | e.g., "opinion_essay", "formal_letter" |
prompt_text |
Text | Yes | |
visual_url |
Char | No | For Academic Task 1 (charts/graphs) |
rubric_id |
Many2one (encoach.rubric) |
Yes | |
min_words |
Integer | Yes | 150 for T1, 250 for T2 |
model_answer |
Text | No | AI-generated model answer |
ai_generated |
Boolean | No | |
approved |
Boolean | No | |
ielts_certified |
Boolean | No | |
generation_brief |
Jsonb | No | |
validation_errors |
Jsonb | No |
4.5 encoach.speaking.card
| Field | Type | Required | Notes |
|---|---|---|---|
id |
Integer (auto) | Yes | |
part |
Integer (1--3) | Yes | Speaking part |
topic |
Char | Yes | |
questions |
Jsonb | Yes | List of question strings |
bullet_points |
Jsonb | No | For Part 2 cue card |
linked_card_id |
Many2one (encoach.speaking.card) |
No | Part 3 linked to Part 2 |
rubric_id |
Many2one (encoach.rubric) |
Yes | |
difficulty |
Selection | Yes | |
model_response |
Text | No | AI-generated model response |
ai_generated |
Boolean | No | |
approved |
Boolean | No | |
ielts_certified |
Boolean | No | |
generation_brief |
Jsonb | No | |
validation_errors |
Jsonb | No |
4.6 encoach.rubric
| Field | Type | Required | Notes |
|---|---|---|---|
id |
Integer (auto) | Yes | |
skill |
Selection | Yes | writing or speaking |
criteria |
Jsonb | Yes | e.g., [{"name": "Task Achievement", "max_score": 9, "descriptors": {...}}] |
exam_type |
Selection | No | IELTS-specific or general |
4.7 encoach.resource (modified)
Add fields to existing encoach.resource model:
| New Field | Type | Notes |
|---|---|---|
cefr_level |
Selection (pre_a1,a1,a2,b1,b2,c1,c2) |
For CEFR-tagged content |
grammar_topic |
Char | e.g., "present_simple", "conditionals" |
vocab_band |
Char | e.g., "high_freq_1000", "academic_word_list" |
ai_generated |
Boolean | Default: False |
approved |
Boolean | Default: False |
subject_id |
Many2one (encoach.subject) |
For multi-subject support |
5. Exam Tables
5.1 encoach.exam (modified)
Add fields to existing exam model:
| New Field | Type | Notes |
|---|---|---|
results_release_mode |
Selection (auto, manual_approval) |
Default: auto. Controls whether results are visible immediately or require admin approval. |
template_type |
Selection (ielts_academic, ielts_general_training, general_english, toefl, step, ic3, custom) |
Fixed exam template identifier |
assembly_mode |
Selection (auto, manual, hybrid) |
Question assembly mode |
target_band |
Float | Target band score |
randomize |
Boolean | Randomize question order |
5.2 encoach.exam.section
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
exam_id |
Many2one (encoach.exam) |
|
skill |
Selection | |
part_number |
Integer | |
time_limit_sec |
Integer | Section time limit in seconds |
question_count |
Integer | Required question count |
content_id |
Many2one | Reference to passage/audio/prompt |
5.3 encoach.exam.template (new)
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
name |
Char(200) | Template name |
type |
Selection (international, custom) |
Discriminator for the two template paths |
editable |
Boolean | False for international (locked structure), True for custom |
active |
Boolean | Only active templates are listed |
subject_id |
Many2one (encoach.taxonomy.subject) |
Subject this template belongs to |
entity_id |
Many2one (encoach.entity) |
NULL for international (global), FK for custom (entity-scoped) |
teacher_id |
Many2one (res.users) |
NULL for international, FK for custom |
structure |
JSON | Template structure definition (parts, skills, question counts, time limits) |
total_time_min |
Integer | Total exam duration in minutes |
pass_threshold |
Float | Minimum percentage to pass (optional) |
results_release_mode |
Selection (auto, manual_approval) |
Score release mode |
randomize_questions |
Boolean | Whether to randomize question order |
5.4 encoach.exam.custom (new)
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
title |
Char(200) | Exam title |
template_id |
Many2one (encoach.exam.template) |
Optional: reusable template reference |
subject_id |
Many2one (encoach.taxonomy.subject) |
Subject |
entity_id |
Many2one (encoach.entity) |
Scoped to entity; NULL for personal |
teacher_id |
Many2one (res.users) |
Creating teacher |
description |
Text | Exam instructions |
total_time_min |
Integer | Overall duration |
pass_threshold |
Float | Minimum score % |
results_release_mode |
Selection (auto, manual_approval) |
|
randomize_questions |
Boolean | |
status |
Selection (draft, published, archived) |
5.5 encoach.exam.custom.section (new)
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
exam_id |
Many2one (encoach.exam.custom) |
Parent custom exam |
title |
Char(200) | Section title |
skill |
Char(100) | Skill/category label |
question_count |
Integer | Required minimum question count |
time_limit_min |
Integer | Per-section time limit (optional) |
scoring_method |
Selection (auto, rubric, mixed) |
|
sequence |
Integer | Display order |
question_ids |
Many2many (encoach.question.bank) |
Assigned questions |
5.6 encoach.exam.assignment
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
exam_id |
Many2one (encoach.exam) |
|
student_id |
Many2one (res.users) |
|
batch_id |
Many2one (op.batch) |
Optional: assign to whole batch |
access_start |
Datetime | Optional access window start |
access_end |
Datetime | Optional access window end |
status |
Selection (assigned, started, completed, expired) |
6. User and Profile Tables
6.1 res.users (modified via _inherit)
| New Field | Type | Notes |
|---|---|---|
entity_id |
Many2one (encoach.entity) |
Nullable. Links student to institution. |
first_login |
Boolean | Default: True. Set to False after first password reset. |
account_source |
Selection (self_registered, entity_bulk_upload) |
How the account was created |
account_status |
Selection (unactivated, activated, suspended) |
Default: unactivated until onboarding wizard completed |
6.2 encoach.student.profile
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
user_id |
Many2one (res.users) |
|
cefr_level |
Selection | Current CEFR level from placement |
target_band |
Float | Target band/level |
learning_style |
Jsonb | ["visual", "reading"] |
learning_goal |
Char | From onboarding wizard |
hours_per_week |
Integer | Study commitment |
study_mode |
Selection (self_study, with_teacher) |
|
exam_date |
Date | Optional target exam date |
placement_completed |
Boolean | |
entity_id |
Many2one (encoach.entity) |
Denormalized for query performance |
6.3 encoach.gap.profile
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
student_id |
Many2one (res.users) |
|
source_type |
Selection (placement, exam) |
What generated this gap profile |
source_id |
Integer | ID of placement session or exam attempt |
skill_gaps |
Jsonb | [{"skill": "writing", "current": 5.5, "target": 7.0, "gap": 1.5, "priority": "high", "hours": 40}] |
question_type_weaknesses |
Jsonb | [{"skill": "reading", "type": "tfng", "error_rate": 0.6}] |
topic_weaknesses |
Jsonb | [{"category": "environment", "error_count": 3, "total": 5}] |
entity_id |
Many2one (encoach.entity) |
|
created_at |
Datetime |
7. Course Tables
7.1 encoach.course (modified)
| New Field | Type | Notes |
|---|---|---|
generation_source |
Selection (manual, auto_gap, ai_english, ai_ielts) |
How the course was created |
gap_profile_id |
Many2one (encoach.gap.profile) |
If generated from gap analysis |
progression_model |
Selection (linear, parallel, adaptive) |
Module ordering model |
target_band |
Float | |
study_hours_week |
Integer | |
entity_id |
Many2one (encoach.entity) |
7.2 encoach.course.module (modified)
| New Field | Type | Notes |
|---|---|---|
cefr_target |
Selection | Target CEFR for this module |
auto_generated |
Boolean | AI-generated module |
generation_brief |
Jsonb | AI generation parameters |
completion_criteria |
Selection (all_resources, score_threshold, teacher_approval) |
|
score_threshold |
Float | Required score if criteria = score_threshold |
prerequisite_module_id |
Many2one (encoach.course.module) |
For linear/adaptive ordering |
status |
Selection (locked, available, in_progress, completed, skipped) |
8. Progress and Results Tables
8.1 encoach.student.attempt
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
student_id |
Many2one (res.users) |
|
exam_id |
Many2one (encoach.exam) |
|
started_at |
Datetime | |
completed_at |
Datetime | |
status |
Selection (in_progress, completed, scoring, scored, released, pending_approval) |
|
listening_band |
Float | |
reading_band |
Float | |
writing_band |
Float | |
speaking_band |
Float | |
overall_band |
Float | |
cefr_level |
Selection | Derived from overall band |
is_placement |
Boolean | First attempt used as placement diagnostic |
entity_id |
Many2one (encoach.entity) |
Nullable FK |
released_at |
Datetime | When results were approved/released |
released_by |
Many2one (res.users) |
Admin who approved release |
8.2 encoach.student.answer
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
attempt_id |
Many2one (encoach.student.attempt) |
|
question_id |
Many2one (encoach.question) |
|
answer |
Jsonb | Student's answer |
is_correct |
Boolean | For auto-scored items |
score |
Float | Points earned |
time_spent_ms |
Integer | Time on this question |
flagged |
Boolean | Student flagged for review |
8.3 encoach.score
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
attempt_id |
Many2one (encoach.student.attempt) |
|
skill |
Selection | |
band_score |
Float | |
raw_score |
Float | |
max_score |
Float | |
cefr_level |
Selection | |
entity_id |
Many2one (encoach.entity) |
8.4 encoach.feedback
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
attempt_id |
Many2one | |
question_id |
Many2one | |
feedback_text |
Text | Per-question feedback |
source |
Selection (teacher, ai) |
|
rubric_scores |
Jsonb | For W/S: {"task_achievement": 6, "coherence": 7, ...} |
graded_by |
Many2one (res.users) |
Teacher or AI |
9. Adaptive Learning Tables
9.1 encoach.student.ability.model
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
student_id |
Many2one (res.users) |
|
subject_id |
Many2one (encoach.subject) |
|
skill |
Selection | |
theta |
Float | IRT ability estimate |
sem |
Float | Standard Error of Measurement |
last_updated |
Datetime |
9.2 encoach.cat.session
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
student_id |
Many2one (res.users) |
|
subject_id |
Many2one (encoach.subject) |
|
started_at |
Datetime | |
completed_at |
Datetime | |
status |
Selection (active, completed, abandoned) |
|
current_section |
Selection | Current dimension |
current_theta |
Float | Running ability estimate |
current_sem |
Float | Running SEM |
questions_answered |
Integer | |
autosave_data |
Jsonb | Last auto-saved state |
9.3 encoach.adaptive.event
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
student_id |
Many2one (res.users) |
|
course_id |
Many2one | |
event_type |
Selection (signal, decision) |
|
signal_name |
Char | e.g., "quiz_score", "time_on_task", "retry_count" |
signal_value |
Float | |
decision |
Char | e.g., "serve_harder", "insert_micro_lesson", "skip_module", "teacher_alert" |
context |
Jsonb | Additional detail |
created_at |
Datetime |
9.4 encoach.adaptive.path
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
student_id |
Many2one (res.users) |
|
course_id |
Many2one | |
module_queue |
Jsonb | Ordered list of module IDs the engine recommends |
source |
Selection (placement, exam, ai_generated) |
What triggered this path |
next_generation_brief |
Jsonb | Parameters for next AI content generation |
9.5 encoach.adaptive.settings
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
teacher_id |
Many2one (res.users) |
|
entity_id |
Many2one (encoach.entity) |
Optional: entity-wide defaults |
step_up_threshold |
Float | Default: 0.85 |
step_down_threshold |
Float | Default: 0.50 |
micro_lesson_trigger |
Integer | Default: 2 |
module_skip_threshold |
Float | Default: 0.95 |
no_progress_alert_days |
Integer | Default: 3 |
max_retries |
Integer | Default: 3 |
10. Entity and Institutional Tables
10.1 encoach.entity (modified)
| New Field | Type | Notes |
|---|---|---|
type |
Selection (university, school, corporate, government) |
|
logo_url |
Char | Entity logo path or URL |
logo_file |
Binary | Logo file attachment |
primary_color |
Char | Hex code, e.g., "#1a73e8" |
secondary_color |
Char | |
background_color |
Char | |
white_label_domain |
Char | e.g., "utas" for "utas.encoach.com" |
login_title |
Char | Custom login page title |
login_description |
Text | Custom login page text |
favicon |
Binary | Custom favicon |
results_release_mode |
Selection (auto, manual_approval) |
Default: auto |
10.2 encoach.entity.level.mapping
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
entity_id |
Many2one (encoach.entity) |
|
min_score |
Float | Minimum CEFR score |
max_score |
Float | Maximum CEFR score |
internal_level_name |
Char | e.g., "Level 1", "Foundation", "High Flyer" |
cefr_equivalent |
Selection | e.g., a1, b1_b2, c1 |
11. AI Generation Tables
11.1 encoach.ai.generation.log
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
student_id |
Many2one (res.users) |
Optional: for student-specific generation |
course_type |
Selection (general_english, ielts) |
|
brief |
Jsonb | Generation parameters sent to AI |
attempts |
Integer | Number of generation attempts (max 3) |
final_resource_id |
Many2one (encoach.resource) |
Resulting resource |
approved_by |
Many2one (res.users) |
Teacher who approved |
status |
Selection (generating, quality_check, pending_review, approved, rejected) |
|
created_at |
Datetime |
11.2 encoach.ai.ielts.generation.log
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
skill |
Selection | |
brief |
Jsonb | |
format_check_result |
Jsonb | Layer 1 validation results |
band_check_result |
Jsonb | CEFR band calibration results |
examiner_id |
Many2one (res.users) |
IELTS examiner assigned |
status |
Selection (generating, format_check, examiner_review, approved, rejected) |
|
attempts |
Integer |
11.3 encoach.ielts.standards.check
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
content_id |
Integer | ID of passage/audio/prompt/card |
content_type |
Selection (passage, audio, writing_prompt, speaking_card) |
|
check_type |
Selection (format_compliance, band_calibration, answer_key_completeness) |
|
passed |
Boolean | |
error_detail |
Jsonb | Specific errors found |
checked_at |
Datetime |
Part III -- Workflow 1: User Signup
12. CAPTCHA Integration
12.1 Implementation
Add CAPTCHA verification to the registration endpoint.
Service: encoach_signup/services/captcha.py
import requests
from odoo import api, models
from odoo.exceptions import ValidationError
class CaptchaService(models.AbstractModel):
_name = 'encoach.captcha.service'
def verify(self, token: str) -> bool:
"""Verify CAPTCHA token with provider (reCAPTCHA v2 or hCaptcha)."""
secret = self.env['ir.config_parameter'].sudo().get_param('encoach.captcha_secret_key')
provider = self.env['ir.config_parameter'].sudo().get_param('encoach.captcha_provider', 'recaptcha')
if provider == 'recaptcha':
url = 'https://www.google.com/recaptcha/api/siteverify'
else:
url = 'https://hcaptcha.com/siteverify'
response = requests.post(url, data={'secret': secret, 'response': token})
result = response.json()
return result.get('success', False)
Configuration (System Parameters):
| Key | Value | Notes |
|---|---|---|
encoach.captcha_provider |
recaptcha or hcaptcha |
CAPTCHA provider |
encoach.captcha_secret_key |
Server-side secret key | Stored as ir.config_parameter |
encoach.captcha_site_key |
Client-side site key | Returned by /api/config/captcha |
12.2 Functional Requirements
| ID | Requirement |
|---|---|
| CAP-01 | CAPTCHA verification is mandatory for POST /api/auth/register when account_source = self_registered. |
| CAP-02 | Entity bulk-upload accounts (account_source = entity_bulk_upload) bypass CAPTCHA. |
| CAP-03 | If CAPTCHA verification fails, return HTTP 400 with {"error": "CAPTCHA verification failed"}. |
13. OTP Email Service
13.1 Implementation
Service: encoach_signup/services/otp.py
import random
import hashlib
from datetime import datetime, timedelta
class OTPService(models.AbstractModel):
_name = 'encoach.otp.service'
def generate(self, email: str) -> str:
"""Generate a 6-digit OTP, store hash, return plaintext for email."""
otp = str(random.randint(100000, 999999))
otp_hash = hashlib.sha256(otp.encode()).hexdigest()
expires_at = datetime.utcnow() + timedelta(minutes=15)
self.env['encoach.otp'].create({
'email': email,
'otp_hash': otp_hash,
'expires_at': expires_at,
'attempts': 0,
})
return otp
def verify(self, email: str, otp: str) -> bool:
"""Verify OTP against stored hash."""
otp_hash = hashlib.sha256(otp.encode()).hexdigest()
record = self.env['encoach.otp'].search([
('email', '=', email),
('otp_hash', '=', otp_hash),
('expires_at', '>', datetime.utcnow()),
('used', '=', False),
], limit=1)
if record:
record.used = True
return True
return False
13.2 OTP Data Model: encoach.otp
| Field | Type | Notes |
|---|---|---|
email |
Char | |
otp_hash |
Char | SHA-256 hash of the OTP |
expires_at |
Datetime | 15 minutes from creation |
used |
Boolean | Default: False |
resend_count |
Integer | Max 3 |
created_at |
Datetime |
13.3 Email Template
The OTP email uses Odoo's mail.template with:
- Subject: "EnCoach -- Email Verification Code"
- Body: "Your verification code is: {OTP}. This code expires in 15 minutes."
- Also include a clickable verification link:
{base_url}/verify-email?email={email}&otp={otp}
14. Onboarding Wizard Data Model
14.1 Goals Endpoint
GET /api/onboarding/goals returns available goals dynamically from the platform's exam template registry:
@http.route('/api/onboarding/goals', type='json', auth='user', methods=['GET'])
def get_goals(self):
templates = self.env['encoach.exam.template'].search([('active', '=', True)])
goals = [{'id': t.code, 'title': t.name, 'description': t.description, 'icon': t.icon}
for t in templates]
# Add non-exam goals
goals.extend([
{'id': 'mathematics', 'title': 'Mathematics', 'description': '...', 'icon': 'calculator'},
{'id': 'it', 'title': 'Information Technology', 'description': '...', 'icon': 'code'},
])
return {'goals': goals}
14.2 Complete Wizard Endpoint
POST /api/onboarding/complete saves all wizard data to encoach.student.profile and changes account status to activated.
Part IV -- Workflow 2: Placement Test
15. CAT Engine
15.1 Algorithm
The Computer Adaptive Test engine uses Item Response Theory (IRT) to select questions and estimate ability:
- Initialize: Set starting ability estimate
theta = 0.0(median difficulty). Set SEM to maximum. - Select question: Choose the question from the bank whose difficulty parameter
bis closest to currenttheta, from the current section's question pool, that the student has not yet seen. - Score answer: Check correctness. Update
thetausing Maximum Likelihood Estimation (MLE) or Expected A Posteriori (EAP) method. - Update SEM: Recalculate Standard Error of Measurement.
- Termination check: If
SEM < 0.3OR maximum questions reached for this section, end section. - Next section: Move to the next dimension (Grammar -> Vocabulary -> Reading -> Speaking).
15.2 Python Implementation
Service: encoach_placement/services/cat_engine.py
import math
import numpy as np
class CATEngine:
"""Computer Adaptive Test engine using 3-Parameter Logistic IRT model."""
def probability(self, theta: float, a: float, b: float, c: float) -> float:
"""3PL IRT probability of correct response."""
exponent = -a * (theta - b)
return c + (1 - c) / (1 + math.exp(exponent))
def information(self, theta: float, a: float, b: float, c: float) -> float:
"""Fisher information for a question at given ability level."""
p = self.probability(theta, a, b, c)
q = 1 - p
numerator = a**2 * (p - c)**2 * q
denominator = (1 - c)**2 * p
return numerator / denominator if denominator > 0 else 0
def select_next_question(self, theta: float, available_questions: list) -> dict:
"""Select the question that provides maximum information at current theta."""
best_question = max(
available_questions,
key=lambda q: self.information(theta, q['irt_a'], q['irt_b'], q['irt_c'])
)
return best_question
def update_theta(self, theta: float, responses: list, questions: list) -> tuple:
"""Update ability estimate using MLE. Returns (new_theta, sem)."""
# Newton-Raphson iteration for MLE
for _ in range(20):
numerator = 0.0
denominator = 0.0
for resp, q in zip(responses, questions):
p = self.probability(theta, q['irt_a'], q['irt_b'], q['irt_c'])
numerator += q['irt_a'] * (resp - p)
denominator += q['irt_a']**2 * p * (1 - p)
if abs(denominator) < 1e-10:
break
theta += numerator / denominator
# Calculate SEM
total_info = sum(self.information(theta, q['irt_a'], q['irt_b'], q['irt_c']) for q in questions)
sem = 1.0 / math.sqrt(total_info) if total_info > 0 else float('inf')
return theta, sem
def should_terminate(self, sem: float, questions_answered: int, max_questions: int) -> bool:
"""Check if section should terminate."""
return sem < 0.3 or questions_answered >= max_questions
15.3 Session Management
The placement session persists in encoach.cat.session. Each answer submission:
- Scores the answer
- Updates
thetaandsem - Checks termination
- Selects next question (if not terminated)
- Auto-saves session state
15.4 Subject-Agnostic Design
The CAT engine is subject-agnostic. The subject_id on the session determines which question bank is queried. For Math, the dimensions are Arithmetic, Algebra, Geometry, Problem-Solving. For IT: Computer Basics, Programming Logic, Networking, Problem-Solving.
16. Question Bank with IRT Parameters
16.1 IRT Calibration
Every question in the bank must have IRT parameters:
| Parameter | Symbol | Range | Meaning |
|---|---|---|---|
| Discrimination | a |
0.5 -- 2.5 | How well the item differentiates between ability levels |
| Difficulty | b |
-3.0 -- 3.0 | Ability level at which P(correct) = 0.5 (for c=0) |
| Guessing | c |
0.0 -- 0.35 | Probability of correct response by guessing (MCQ: 0.25 for 4 options) |
16.2 Initial Calibration Strategy
For the initial launch, before real student data is available:
- Set
a = 1.0(default discrimination) for all items - Set
bbased on expert difficulty rating: Easy = -1.0, Medium = 0.0, Hard = 1.0 - Set
c = 0.25for MCQ (4 options),c = 0.0for open-ended - After 100+ student responses per item, recalibrate using real response data
17. CEFR Mapping Algorithm
17.1 Theta to CEFR Mapping
| Theta Range | CEFR Level | IELTS Band Equivalent |
|---|---|---|
| < -2.0 | Pre-A1 | -- |
| -2.0 to -1.0 | A1 | -- |
| -1.0 to 0.0 | A2 | 3.0 -- 3.5 |
| 0.0 to 1.0 | B1 | 4.0 -- 4.5 |
| 1.0 to 2.0 | B2 | 5.0 -- 5.5 |
| 2.0 to 3.0 | C1 | 6.0 -- 6.5 |
| > 3.0 | C2 | 7.0+ |
17.2 Implementation
def theta_to_cefr(theta: float) -> str:
if theta < -2.0: return 'pre_a1'
elif theta < -1.0: return 'a1'
elif theta < 0.0: return 'a2'
elif theta < 1.0: return 'b1'
elif theta < 2.0: return 'b2'
elif theta < 3.0: return 'c1'
else: return 'c2'
def theta_to_ielts_band(theta: float) -> float:
band = 3.0 + (theta + 2.0) * 1.0 # Linear mapping
return max(1.0, min(9.0, round(band * 2) / 2)) # Round to 0.5
18. Speaking AI Evaluation
18.1 Pipeline
Speaking responses are evaluated asynchronously:
- Student uploads audio via
POST /api/placement/speaking-upload - Audio stored in Odoo attachment system
- Background job (Odoo
ir.cronor queue) processes: a. Transcribe using OpenAI Whisper (localbasemodel) b. Evaluate using OpenAI GPT-4o with a rubric prompt c. Score on IELTS Speaking criteria: Fluency, Lexical Resource, Grammar, Pronunciation d. Store scores toencoach.scoreand updateencoach.student.attempt - Frontend polls for completion
18.2 GPT Evaluation Prompt
You are an IELTS Speaking examiner. Evaluate the following spoken response transcript.
Prompt: {prompt_text}
Transcript: {transcript}
Score each criterion on a scale of 0-9 following official IELTS band descriptors:
1. Fluency and Coherence
2. Lexical Resource
3. Grammatical Range and Accuracy
4. Pronunciation (based on transcript analysis only)
Return JSON: {"fluency": X, "lexical": X, "grammar": X, "pronunciation": X, "overall": X, "feedback": "..."}
Part V -- Workflow 3: Exam Configuration (International + Custom)
19. Exam Template Architecture
The platform supports two distinct exam template paths. Both share the same exam session engine, grading pipeline, and score release infrastructure. The difference lies in who defines the exam structure and whether it can be modified.
19.1 Two Template Paths
| Aspect | International Templates | Custom Templates |
|---|---|---|
| Examples | IELTS Academic, IELTS General Training, TOEFL, STEP, IC3 | University midterm, entity quiz, department exam |
| Created by | EnCoach development team (seeded via __manifest__.py data files during module installation) |
Teacher or Entity Admin (via platform API) |
| Structure modifiable? | No -- parts, question counts, time limits, and skills are permanently locked | Yes -- fully configurable by the creator |
| Question assembly | Teacher fills fixed structural slots using Auto/Manual/Hybrid modes | Teacher adds questions freely to self-defined sections |
| Scope | Available to all entities and individual students | Scoped to the creating teacher's entity |
| Data field | type = 'international', editable = False |
type = 'custom', editable = True |
19.2 Unified Template Model: encoach.exam.template
All templates (international and custom) are stored in the same Odoo model with a type discriminator field:
| Field | Type | Notes |
|---|---|---|
name |
Char(200) | Template name |
type |
Selection | international or custom |
editable |
Boolean | False for international, True for custom |
active |
Boolean | Only active templates are listed |
subject_id |
Many2one(encoach.taxonomy.subject) |
Subject this template belongs to |
entity_id |
Many2one(encoach.entity) |
NULL for international (available everywhere), FK for custom (entity-scoped) |
teacher_id |
Many2one(res.users) |
NULL for international, FK for custom |
total_time_min |
Integer | Total exam duration in minutes |
pass_threshold |
Float | Minimum percentage to pass (optional) |
results_release_mode |
Selection | auto or manual_approval |
randomize_questions |
Boolean | Whether to randomize question order |
created_at |
Datetime | Auto-set |
19.3 API Endpoints
| Method | Route | Description |
|---|---|---|
GET |
/api/exam/templates |
List all templates. Accepts query params: type=international|custom, subject_id. International templates are always included; custom templates are filtered by the current user's entity. |
GET |
/api/exam/templates/:id |
Get template detail with sections |
POST |
/api/exam/templates/custom |
Save a custom exam structure as a reusable template |
19.4 Seeding International Templates
International templates are seeded during module installation via Odoo data files (data/ielts_templates.xml). The developer defines each template's structure (parts, skills, question counts, time limits) as fixed records. These records have editable = False and type = 'international'. Teachers cannot modify these structures; they can only fill the question slots within them.
20. Fixed Template System (International)
20.1 IELTS Template Model: encoach.exam.template
| Field | Type | Notes |
|---|---|---|
id |
Integer (auto) | |
code |
Char | ielts_academic, ielts_general_training, toefl, step, ic3 |
name |
Char | "IELTS Academic" |
structure |
Jsonb | Fixed structure definition (see below) |
active |
Boolean | |
editable |
Boolean | False for IELTS -- structure cannot be modified |
20.2 IELTS Academic Structure (Stored in structure field)
{
"skills": [
{
"skill": "listening",
"parts": [
{"part": 1, "questions": 10, "content_type": "conversation", "time_sec": 360},
{"part": 2, "questions": 10, "content_type": "monologue", "time_sec": 360},
{"part": 3, "questions": 10, "content_type": "conversation", "time_sec": 360},
{"part": 4, "questions": 10, "content_type": "monologue", "time_sec": 480}
],
"total_questions": 40, "total_time_sec": 1800
},
{
"skill": "reading",
"parts": [
{"part": 1, "questions": 14, "text_type": "factual"},
{"part": 2, "questions": 13, "text_type": "analytical"},
{"part": 3, "questions": 13, "text_type": "argumentative"}
],
"total_questions": 40, "total_time_sec": 3600
},
{
"skill": "writing",
"parts": [
{"task": 1, "type": "visual_data", "min_words": 150, "time_sec": 1200},
{"task": 2, "type": "essay", "min_words": 250, "time_sec": 2400}
],
"total_time_sec": 3600
},
{
"skill": "speaking",
"parts": [
{"part": 1, "duration_sec": 300, "format": "familiar_topics"},
{"part": 2, "duration_sec": 240, "format": "cue_card"},
{"part": 3, "duration_sec": 300, "format": "abstract_discussion"}
],
"total_time_sec": 840
}
]
}
20.3 Enforcement
The template structure is READ-ONLY. The create_exam endpoint uses the template to pre-populate exam_sections. Attempts to modify question counts, part counts, or time limits are rejected with HTTP 400.
21. Content Pool Query Engine
21.1 Query Logic
When the content pool is requested for an IELTS exam section:
def get_content_pool(self, exam_id, skill, part, difficulty):
"""Return 3-5x more items than needed, with filters applied."""
exam = self.env['encoach.exam'].browse(exam_id)
student_id = exam.assignment_ids.mapped('student_id.id')
domain = [
('skill', '=', skill),
('difficulty', '=', difficulty),
('status', '=', 'active'),
]
if student_id:
seen_ids = self.env['encoach.student.answer'].search([
('attempt_id.student_id', 'in', student_id),
('question_id', '!=', False),
]).mapped('question_id.id')
domain.append(('id', 'not in', seen_ids))
# Exclude flagged and retired
domain.extend([
('status', '!=', 'flagged'),
('status', '!=', 'retired'),
])
required_count = exam.template_id.get_question_count(skill, part)
pool_size = required_count * 4 # 4x oversampling
questions = self.env['encoach.question'].search(domain, limit=pool_size)
# Apply topic diversity: no more than 30% from any single topic
# Apply difficulty curve: mix of easy/medium/hard within the pool
return questions
22. Assembly Modes
22.1 Auto Assembly
def auto_assemble(self, exam_id):
"""System selects questions to fill all structural slots."""
exam = self.env['encoach.exam'].browse(exam_id)
for section in exam.section_ids:
pool = self.get_content_pool(exam_id, section.skill, section.part_number, exam.difficulty)
selected = self._apply_selection_algorithm(pool, section.question_count)
section.question_ids = [(6, 0, [q.id for q in selected])]
return exam
22.2 Manual Assembly
The backend provides the content pool via GET /api/exam/ielts/:id/content-pool. The frontend sends selected question IDs via PUT /api/exam/ielts/:id/sections/:sectionId/questions.
22.3 Hybrid Assembly
The backend generates suggestions via POST /api/exam/ielts/:id/suggest. The frontend allows the teacher to accept, reject, or swap items. Final selection is submitted.
23. Custom Template System
23.1 Custom Template Creation
Custom templates are created by teachers or entity admins through the platform API. Unlike international templates, the creator defines the entire exam structure.
Module: encoach_exam (extends existing module)
23.2 Custom Exam Model: encoach.exam.custom
Custom exams reuse the encoach.exam.template model with type = 'custom' and editable = True. The key difference is that the structure field is populated from the teacher's input rather than from seeded data.
| Field | Type | Notes |
|---|---|---|
title |
Char(200) | Exam title |
template_id |
Many2one(encoach.exam.template) |
Links to the saved custom template (if teacher chose "Save as Template") |
subject_id |
Many2one(encoach.taxonomy.subject) |
Subject (English, Math, IT, etc.) |
entity_id |
Many2one(encoach.entity) |
Scoped to entity; NULL means personal exam |
teacher_id |
Many2one(res.users) |
Creating teacher |
description |
Text | Exam purpose and instructions |
total_time_min |
Integer | Overall exam duration |
pass_threshold |
Float | Minimum score % to pass (optional) |
results_release_mode |
Selection | auto or manual_approval |
randomize_questions |
Boolean | Randomize question order per student |
status |
Selection | draft, published, archived |
23.3 Custom Exam Sections: encoach.exam.custom.section
| Field | Type | Notes |
|---|---|---|
exam_id |
Many2one(encoach.exam.custom) |
Parent exam |
title |
Char(200) | Section title (e.g., "Part A -- Grammar") |
skill |
Char(100) | Skill/category label (free text or taxonomy reference) |
question_count |
Integer | Required minimum question count |
time_limit_min |
Integer | Per-section time limit (optional, shares global timer if not set) |
scoring_method |
Selection | auto, rubric, mixed |
sequence |
Integer | Display order |
question_ids |
Many2many(encoach.question.bank) |
Assigned questions |
23.4 Business Logic
class EncoachExamCustom(models.Model):
_name = 'encoach.exam.custom'
_description = 'Custom Exam'
@api.model
def create_custom_exam(self, vals):
"""Teacher creates a custom exam with full structural freedom."""
vals['type'] = 'custom'
vals['editable'] = True
vals['teacher_id'] = self.env.user.id
vals['entity_id'] = self.env.user.entity_id.id or False
exam = self.create(vals)
for section_data in vals.get('sections', []):
self.env['encoach.exam.custom.section'].create({
'exam_id': exam.id,
**section_data
})
return exam
def save_as_template(self):
"""Save this exam's structure as a reusable custom template."""
return self.env['encoach.exam.template'].create({
'name': f"{self.title} Template",
'type': 'custom',
'editable': True,
'entity_id': self.entity_id.id,
'teacher_id': self.teacher_id.id,
'structure': self._serialize_structure(),
})
23.5 API Endpoints
| Method | Route | Description |
|---|---|---|
POST |
/api/exam/custom/create |
Create a custom exam with sections and questions |
GET |
/api/exam/custom/:id |
Get custom exam detail |
PUT |
/api/exam/custom/:id |
Update custom exam (only while in draft status) |
DELETE |
/api/exam/custom/:id |
Delete custom exam (only while in draft status) |
POST |
/api/exam/custom/:id/save-template |
Save this exam structure as a reusable template |
23.6 Shared Infrastructure
Custom exams reuse the same infrastructure as international template exams:
- Exam session (Section 25): same student-facing exam interface
- Auto-scoring (Section 26): same scoring pipeline for auto-scored questions
- Rubric scoring (Section 26): same AI-assisted rubric grading
- Score release gate (Section 27): same approval workflow
- PDF reports (Section 28): same report generation
- Content pool (Section 21): same question bank for browsing questions
24. Exam Validation Rules
24.1 Validation Checks
The validation endpoint runs these checks (applies to both international and custom exams; custom exams use the teacher-defined constraints instead of template-fixed constraints):
| Check | Rule | Severity |
|---|---|---|
| Question Count | Each section must have exactly the number of questions specified by the template | Error (blocks publish) |
| Media URLs | All audio_url and visual_url references must resolve (HTTP HEAD check) | Error |
| Answer Keys | All auto-scored questions must have correct_answer populated |
Error |
| No Duplicates | No question ID appears in more than one section | Error |
| Rubrics | All Writing and Speaking tasks must have rubric_id linked |
Error |
| Time Specification | Each section must have time_limit_sec > 0 |
Error |
| Content Diversity | No more than 40% of questions from same topic_category | Warning |
24.2 Publishing Logic
On publish (status: draft -> published):
- Run all validation checks
- If any errors exist, return HTTP 400 with validation report
- If only warnings, allow publish with acknowledgement
- Lock the exam for editing (no further question changes)
- Create audit log entry
Part VI -- Workflow 4: General English Exam
25. Exam Session Management
23.1 Session Creation
When a student starts an exam:
- Create
encoach.student.attemptwithstatus = in_progress - Return all sections and questions (pre-loaded, unlike CAT)
- Start session timer
23.2 Auto-Save
POST /api/exam/:id/autosave receives the current answer state every 10 seconds:
{
"attempt_id": 42,
"answers": [
{"question_id": 1, "answer": "B"},
{"question_id": 2, "answer": "True"}
],
"current_section": "listening",
"time_remaining_sec": 1523
}
Auto-save updates encoach.student.answer records without closing the attempt.
23.3 Section Time Enforcement
When a section timer expires:
- All answers for that section are auto-submitted
- Unanswered questions are marked as blank (0 score)
- Student is moved to the next section
26. Auto-Scoring Engine
24.1 Scoring Logic
def auto_score(self, attempt_id):
"""Auto-score Listening and Reading sections."""
attempt = self.env['encoach.student.attempt'].browse(attempt_id)
for answer in attempt.answer_ids:
question = answer.question_id
if question.skill in ('listening', 'reading', 'grammar', 'vocabulary'):
answer.is_correct = self._check_answer(answer.answer, question.correct_answer, question.question_type)
answer.score = question.marks if answer.is_correct else 0.0
24.2 Band Score Calculation
def calculate_band(self, raw_score: float, max_score: float, skill: str) -> float:
"""Convert raw score to IELTS band score."""
percentage = raw_score / max_score
# IELTS uses a conversion table (not a simple formula)
# This is a simplified approximation
band = 1.0 + (percentage * 8.0)
return round(band * 2) / 2 # Round to nearest 0.5
The overall band is the arithmetic mean of the four skills, rounded to nearest 0.5.
27. Score Release Gate
25.1 Logic
def submit_exam(self, attempt_id):
"""Process exam submission and determine visibility."""
attempt = self.env['encoach.student.attempt'].browse(attempt_id)
self.auto_score(attempt_id)
self.calculate_bands(attempt_id)
exam = attempt.exam_id
if exam.results_release_mode == 'auto':
attempt.status = 'released'
attempt.released_at = fields.Datetime.now()
elif exam.results_release_mode == 'manual_approval':
attempt.status = 'pending_approval'
# Notify entity admin
self._notify_entity_admin(attempt)
25.2 Release Endpoint
POST /api/scores/:attemptId/release (admin only):
- Verify caller is an admin for the student's entity
- Set
status = released,released_at = now,released_by = caller - Notify student via notification engine
28. PDF Report Generation with QR Code
26.1 Implementation
Use Python reportlab library for PDF generation and qrcode library for QR codes.
Dependencies:
pip install reportlab qrcode[pil]
26.2 QR Code Content
import hashlib
import json
def generate_verification_hash(self, attempt):
"""Generate a signed verification hash for the QR code."""
secret = self.env['ir.config_parameter'].sudo().get_param('encoach.verification_secret')
data = f"{attempt.student_id.id}:{attempt.exam_id.id}:{attempt.overall_band}:{attempt.completed_at}"
return hashlib.sha256(f"{data}:{secret}".encode()).hexdigest()[:32]
def generate_qr_data(self, attempt):
"""Generate QR code data URL."""
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
hash_val = self.generate_verification_hash(attempt)
return f"{base_url}/verify/{hash_val}"
26.3 PDF Structure
The PDF includes: entity logo (or EnCoach logo), student info, exam details, per-skill scores, overall band, CEFR level, performance summary, and the QR code at bottom-right.
Part VII -- Workflow 5: Course Generation
29. Gap Analysis Engine
27.1 Algorithm
def generate_gap_profile(self, source_type, source_id, student_id):
"""Generate a gap profile from an exam attempt or placement session."""
if source_type == 'exam':
attempt = self.env['encoach.student.attempt'].browse(source_id)
profile = self.env['encoach.student.profile'].search([('user_id', '=', student_id)])
target = profile.target_band
skill_gaps = []
for skill in ['listening', 'reading', 'writing', 'speaking']:
current = getattr(attempt, f'{skill}_band')
gap = target - current
hours = self._estimate_hours(gap)
priority = 'high' if gap >= 1.5 else ('medium' if gap >= 1.0 else 'low')
skill_gaps.append({
'skill': skill, 'current': current, 'target': target,
'gap': gap, 'priority': priority, 'hours': hours
})
# Sort by gap size descending
skill_gaps.sort(key=lambda x: x['gap'], reverse=True)
# Analyse question-type and topic weaknesses
qt_weaknesses = self._analyse_question_types(attempt)
topic_weaknesses = self._analyse_topics(attempt)
return self.env['encoach.gap.profile'].create({
'student_id': student_id,
'source_type': source_type,
'source_id': source_id,
'skill_gaps': json.dumps(skill_gaps),
'question_type_weaknesses': json.dumps(qt_weaknesses),
'topic_weaknesses': json.dumps(topic_weaknesses),
'entity_id': attempt.entity_id.id,
})
30. Auto Course Structure Generation
28.1 Individual Path
def auto_generate_course(self, gap_profile_id):
"""Auto-generate a course from a gap profile (individual student path)."""
gap = self.env['encoach.gap.profile'].browse(gap_profile_id)
skill_gaps = json.loads(gap.skill_gaps)
# Create course
course = self.env['encoach.course'].create({
'name': f"Training Plan -- {gap.student_id.name}",
'generation_source': 'auto_gap',
'gap_profile_id': gap_profile_id,
'progression_model': 'adaptive',
'target_band': gap.student_id.student_profile_id.target_band,
})
# Create sections and modules per skill
for skill_gap in skill_gaps:
if skill_gap['gap'] <= 0:
continue
section = self.env['encoach.course.section'].create({
'course_id': course.id,
'skill': skill_gap['skill'],
'estimated_hours': skill_gap['hours'],
})
# Generate modules within section based on weakness analysis
self._generate_skill_modules(section, skill_gap, gap.question_type_weaknesses)
return course
31. Adaptive Progression Engine
This is the runtime component of the adaptive engine that manages module ordering during course delivery. It reads the adaptive settings (thresholds) and applies decisions.
29.1 Decision Logic (Phase 1: Rule-Based)
def process_checkpoint(self, student_id, course_id, module_id, score):
"""Process a module checkpoint score and make adaptive decisions."""
settings = self._get_settings(student_id)
module = self.env['encoach.course.module'].browse(module_id)
if score >= settings.module_skip_threshold:
# Skip to next module
self._log_event(student_id, course_id, 'decision', 'skip_module', score)
self._advance_to_next(student_id, course_id, skip=True)
elif score >= settings.step_up_threshold:
# Serve harder content
self._log_event(student_id, course_id, 'decision', 'serve_harder', score)
self._advance_to_next(student_id, course_id)
elif score < settings.step_down_threshold:
# Insert remedial content
self._log_event(student_id, course_id, 'decision', 'insert_remedial', score)
self._insert_remedial_module(student_id, course_id, module)
# Check for stuck pattern
retry_count = self._get_retry_count(student_id, module_id)
if retry_count >= settings.micro_lesson_trigger:
self._log_event(student_id, course_id, 'decision', 'insert_micro_lesson', retry_count)
self._insert_micro_lesson(student_id, course_id, module)
# Check no-progress alert
days_inactive = self._get_days_inactive(student_id, course_id)
if days_inactive >= settings.no_progress_alert_days:
self._log_event(student_id, course_id, 'decision', 'teacher_alert', days_inactive)
self._alert_teacher(student_id, course_id)
Part VIII -- Workflow 6: Entity Student Onboarding
32. CSV Parsing and Validation
30.1 Validation Rules
| Rule | Check | Severity |
|---|---|---|
| Required fields | student_name, institutional_email, national_id must be present |
Error |
| Email format | Must be valid email format | Error |
| Duplicate email | No duplicate emails within the CSV or against existing accounts | Error |
| National ID format | Non-empty string | Error |
| Student ID | Optional but validated if present | Warning |
| Programme | Optional | Info |
30.2 Implementation
@http.route('/api/entity/students/validate-csv', type='http', auth='user', methods=['POST'], csrf=False)
def validate_csv(self, **kwargs):
csv_file = request.httprequest.files.get('file')
reader = csv.DictReader(io.TextIOWrapper(csv_file, encoding='utf-8'))
results = []
existing_emails = set(self.env['res.users'].search([]).mapped('login'))
for i, row in enumerate(reader, start=2):
errors = []
if not row.get('student_name'):
errors.append('Missing required field: student_name')
if not row.get('institutional_email'):
errors.append('Missing required field: institutional_email')
elif not self._is_valid_email(row['institutional_email']):
errors.append('Invalid email format')
elif row['institutional_email'] in existing_emails:
errors.append('Duplicate email: account already exists')
if not row.get('national_id'):
errors.append('Missing required field: national_id')
results.append({
'row': i,
'student_name': row.get('student_name', ''),
'email': row.get('institutional_email', ''),
'status': 'error' if errors else 'valid',
'issues': errors,
})
return json.dumps({'validation': results})
33. Bulk Account Creation
def bulk_create_students(self, validated_rows, entity_id):
"""Create student accounts from validated CSV data."""
created = []
for row in validated_rows:
user = self.env['res.users'].create({
'name': row['student_name'],
'login': row['institutional_email'],
'password': row['national_id'],
'entity_id': entity_id,
'first_login': True,
'account_source': 'entity_bulk_upload',
'account_status': 'activated', # No wizard needed
})
# Create student profile
self.env['encoach.student.profile'].create({
'user_id': user.id,
'entity_id': entity_id,
})
created.append(user)
return created
34. Credential Email Service
def send_credentials(self, users, entity):
"""Send credential notification emails to newly created students."""
template = self.env.ref('encoach_entity_onboarding.email_credentials')
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
for user in users:
template.send_mail(user.id, force_send=True, email_values={
'email_to': user.login,
'subject': f'Welcome to {entity.name} Learning Platform',
'body_html': f"""
<p>Your account has been created on the {entity.name} learning platform.</p>
<p><strong>Login URL:</strong> {base_url}/login</p>
<p><strong>Username:</strong> {user.login}</p>
<p><strong>Temporary Password:</strong> Your National ID number</p>
<p>You will be asked to set a new password on first login.</p>
""",
})
35. Entity Level Mapping
33.1 Mapping Application
When placement results are stored for an entity student, the system also maps to the entity's internal levels:
def apply_entity_level_mapping(self, student_id, cefr_score):
"""Map CEFR score to entity's internal level system."""
student = self.env['res.users'].browse(student_id)
if not student.entity_id:
return None
mappings = self.env['encoach.entity.level.mapping'].search([
('entity_id', '=', student.entity_id.id),
('min_score', '<=', cefr_score),
('max_score', '>=', cefr_score),
], limit=1)
if mappings:
return {
'internal_level': mappings.internal_level_name,
'cefr_equivalent': mappings.cefr_equivalent,
}
return None
Part IX -- AI Course Generation
36. General English AI Content Pipeline
34.1 Pipeline Steps
- Receive generation brief from student profile (CEFR level, skill, grammar topic, vocab band, learning style)
- Generate content using OpenAI GPT (see Section 59 for prompts)
- Auto-tag content with CEFR level, grammar topic, vocab band, skill, resource type
- Quality gate (Section 36): readability, CEFR calibration, grammar accuracy, length
- Teacher review (if quality gate passes): approve or edit
- Store to DB with
approved = true
34.2 Content Types Generated
| Type | GPT Prompt Category | Output Format |
|---|---|---|
| Reading passages | Generate passage at CEFR level with comprehension questions | encoach.passage + encoach.question |
| Grammar exercises | Generate exercises with explanations and worked examples | encoach.resource (type=exercise) |
| Speaking prompts | Generate contextual speaking prompts | encoach.speaking.card |
| Vocabulary sets | Generate contextual vocabulary with definitions and usage | encoach.resource (type=vocabulary) |
37. AI IELTS Content Pipeline
35.1 Two-Layer Validation
This pipeline has an additional validation layer compared to General English:
- Generate IELTS-specific content using GPT with strict format constraints
- Layer 1: Automated IELTS standards check (Section 37)
- Format compliance (word counts, part structure, question types)
- CEFR band calibration
- Answer key and rubric completeness
- If fail: return to GPT with error details (max 3 attempts)
- Layer 2: IELTS examiner review (human)
- Content source gate: AI-generated = mandatory review, entity-uploaded = spot-check
- Examiner verifies: accuracy, difficulty, alignment with IELTS conditions, cultural sensitivity
- If reject: return to step 1 with examiner notes
- Store with
approved = trueANDielts_certified = true
38. Quality Gate Engine
36.1 Checks
class QualityGateEngine:
def check(self, content_type, content, target_cefr):
results = {
'readability': self._check_readability(content, target_cefr),
'cefr_calibration': self._check_cefr(content, target_cefr),
'grammar_accuracy': self._check_grammar(content),
'length_compliance': self._check_length(content, content_type),
}
passed = all(r['passed'] for r in results.values())
return {'passed': passed, 'checks': results}
def _check_readability(self, content, target_cefr):
"""Flesch-Kincaid readability score mapped to CEFR level."""
fk_score = textstat.flesch_kincaid_grade(content)
expected_range = CEFR_FK_RANGES[target_cefr] # e.g., B2: (8, 12)
passed = expected_range[0] <= fk_score <= expected_range[1]
return {'passed': passed, 'score': fk_score, 'expected': expected_range}
def _check_length(self, content, content_type):
"""Check word count is within expected range."""
word_count = len(content.split())
expected = CONTENT_LENGTH_RANGES[content_type]
passed = expected['min'] <= word_count <= expected['max']
return {'passed': passed, 'count': word_count, 'expected': expected}
Dependency: pip install textstat
39. IELTS Standards Validation Engine
37.1 Format Compliance Checks
class IELTSValidationEngine:
IELTS_WORD_LIMITS = {
'reading_passage_academic': {'min': 700, 'max': 850},
'reading_passage_general': {'min': 300, 'max': 500},
'writing_task1': {'min_words_required': 150},
'writing_task2': {'min_words_required': 250},
}
IELTS_QUESTION_COUNTS = {
'listening': [10, 10, 10, 10], # Parts 1-4
'reading': [14, 13, 13], # Passages 1-3
}
def validate_format(self, content_type, content, skill, part):
"""Check IELTS format compliance."""
errors = []
if content_type == 'passage':
word_count = len(content['body_text'].split())
limits = self.IELTS_WORD_LIMITS.get(f'reading_passage_{content["exam_type"]}')
if limits and not (limits['min'] <= word_count <= limits['max']):
errors.append(f"Word count {word_count} outside IELTS range {limits['min']}-{limits['max']}")
if content_type == 'question_set':
expected = self.IELTS_QUESTION_COUNTS.get(skill, [])[part - 1]
actual = len(content['questions'])
if actual != expected:
errors.append(f"Expected {expected} questions for {skill} part {part}, got {actual}")
return {'passed': len(errors) == 0, 'errors': errors}
Part X -- Adaptive Learning Engine (4 Phases)
40. Phase 1 -- Rule-Based Engine (MVP)
Complexity: Low Dependencies: None (no ML libraries required)
38.1 Scope
- Teacher sets explicit thresholds via
encoach.adaptive.settings - Engine reads performance signals after each module checkpoint
- Engine makes decisions based on simple if/then rules
- All decisions logged to
encoach.adaptive.event
38.2 Signals Read
| Signal | Source | Type |
|---|---|---|
| Quiz score | Module checkpoint exercises | Percentage (0-100) |
| Error rate | Per-question correctness | Percentage |
| Retry count | How many times student retried an exercise | Integer |
| Time on task | Time spent on each resource/module | Milliseconds |
| Video completion | Percentage of video watched | Percentage |
| Module completion | Whether module criteria met | Boolean |
38.3 Decisions Made
| Decision | Trigger | Action |
|---|---|---|
| Serve harder content | Score >= step_up_threshold | Unlock next-difficulty module |
| Serve easier content | Score < step_down_threshold | Insert prerequisite module |
| Insert micro-lesson | Retry count >= micro_lesson_trigger | Generate targeted micro-lesson |
| Skip module | Pre-test score >= module_skip_threshold | Mark module as skipped, advance |
| Teacher alert | Days inactive >= no_progress_alert_days | Create notification for teacher |
| Resource type switch | Learning style mismatch detected | Serve alternative resource type |
41. Phase 2 -- IRT-Based CAT
Complexity: Medium
Dependencies: numpy, scipy
39.1 Scope
- CAT engine (Section 15) is fully operational for placement tests
- Question bank is calibrated with real IRT parameters (a, b, c)
- Ability estimates are stored and updated in
encoach.student.ability.model - SEM-based termination replaces fixed question counts
39.2 Calibration Process
After 100+ student responses per item, recalibrate IRT parameters:
def recalibrate(self, question_id):
"""Recalibrate IRT parameters using real response data."""
responses = self.env['encoach.student.answer'].search([('question_id', '=', question_id)])
if len(responses) < 100:
return # Not enough data
# Extract response vector and ability estimates
data = [(r.is_correct, r.attempt_id.student_id.ability_model_id.theta) for r in responses]
# Use scipy.optimize to fit 3PL model
from scipy.optimize import minimize
# ... MLE fitting logic
42. Phase 3 -- Collaborative Filtering
Complexity: High
Dependencies: numpy, scipy, scikit-learn
40.1 Scope
- Build a student-student similarity matrix based on ability profiles
- Predict which resources/modules will be most effective for a student based on similar students' outcomes
- "Students like you also benefited from..." recommendations
40.2 Algorithm Outline
def get_recommendations(self, student_id, course_id):
"""Collaborative filtering based on ability model similarity."""
student_model = self.env['encoach.student.ability.model'].search([('student_id', '=', student_id)])
all_models = self.env['encoach.student.ability.model'].search([])
# Compute cosine similarity between student ability vectors
# Find top-K similar students
# Recommend resources that worked best for similar students
# "Worked best" = highest score improvement after consumption
43. Phase 4 -- Full ML
Complexity: Very High
Dependencies: numpy, scipy, scikit-learn, tensorflow or pytorch
41.1 Scope
- Dropout risk prediction: Predict which students are likely to abandon their course
- Optimal study time: Predict the best time of day/week for each student to study
- Next best question: Predict which question will provide the most learning value
41.2 Models Required
| Model | Input Features | Output | Training Data |
|---|---|---|---|
| Dropout Risk | days_inactive, score_trend, login_frequency, module_completion_rate | probability (0-1) | Historical student completion data |
| Study Time Optimizer | login_times, score_by_time_of_day, session_durations | recommended_time_slots | Historical session + score data |
| Next Best Question | current_theta, question_bank_features, response_history | question_id | Student response logs |
Part XI -- White-Labelling
44. Entity Branding Model
See Section 10.1 for the encoach.entity model with branding fields.
42.1 Branding API
GET /api/entity/branding (authenticated): Returns the branding for the current user's entity:
@http.route('/api/entity/branding', type='json', auth='user', methods=['GET'])
def get_branding(self):
user = request.env.user
if not user.entity_id:
return {'branding': None} # Use default
entity = user.entity_id
return {
'branding': {
'logo_url': entity.logo_url,
'primary_color': entity.primary_color,
'secondary_color': entity.secondary_color,
'background_color': entity.background_color,
'login_title': entity.login_title,
'login_description': entity.login_description,
}
}
45. Subdomain Routing
If the entity has a white_label_domain configured (e.g., "utas"), the platform should be accessible at utas.encoach.com. This requires:
- Nginx configuration: Wildcard subdomain
*.encoach.compointing to the same frontend - Frontend logic: On load, read the subdomain, call
GET /api/entity/branding?domain={subdomain}, apply branding - Backend lookup:
GET /api/entity/branding?domain=utasresolves to the entity withwhite_label_domain = "utas"
Part XII -- Math and IT Backend
46. Subject-Specific Question Types
44.1 Math Question Types
| Type | question_type Value |
Scoring Logic |
|---|---|---|
| Numerical | numerical |
Compare answer to correct_answer.value within tolerance |
| Expression | expression |
Symbolic comparison using sympy (e.g., x^2 + 2x + 1 == (x+1)^2) |
| Matrix | matrix |
Element-wise comparison |
| Graph | graph |
Compare function definition string |
Dependency for symbolic math: pip install sympy
44.2 IT Question Types
| Type | question_type Value |
Scoring Logic |
|---|---|---|
| Code Completion | code_completion |
String match or AST comparison |
| Code Output | code_output |
Compare predicted output to actual |
| SQL Query | sql_query |
Execute against test DB, compare result sets |
44.3 Code Execution Sandbox (Phase 2+)
For IT exercises requiring code execution, use a sandboxed execution environment. Options:
- Docker container with restricted permissions
- WebAssembly-based execution (Pyodide for Python)
- External code execution API (e.g., Judge0)
Initial implementation can use output prediction (no execution) and add execution in Phase 2.
47. Subject Taxonomy Extension
45.1 Math Taxonomy (pre-loaded data)
Mathematics
├── Arithmetic (Pre-A1 to A2)
│ ├── Basic Operations
│ ├── Fractions and Decimals
│ └── Percentages
├── Algebra (B1 to B2)
│ ├── Linear Equations
│ ├── Quadratic Equations
│ └── Functions
├── Geometry (B1 to C1)
│ ├── 2D Shapes
│ ├── 3D Shapes
│ └── Trigonometry
└── Statistics (B2 to C2)
├── Probability
├── Data Analysis
└── Distributions
45.2 IT Taxonomy (pre-loaded data)
Information Technology
├── Computer Basics (Pre-A1 to A2)
│ ├── Hardware
│ ├── Software
│ └── Operating Systems
├── Programming (B1 to C1)
│ ├── Variables and Data Types
│ ├── Control Flow
│ ├── Functions
│ └── Data Structures
├── Networking (B1 to B2)
│ ├── Network Fundamentals
│ ├── IP Addressing
│ └── Security
└── Databases (B2 to C2)
├── Relational Databases
├── SQL
└── Database Design
Part XIII -- API Endpoint Specification
48. Auth and Signup Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
None | Register new user with CAPTCHA |
| POST | /api/auth/check-email |
None | Check if email exists |
| POST | /api/auth/verify-email |
None | Verify OTP code |
| POST | /api/auth/resend-otp |
None | Resend OTP (max 3) |
| GET | /api/onboarding/goals |
User | Get available learning goals |
| POST | /api/onboarding/complete |
User | Save wizard data, activate account |
| GET | /api/config/captcha |
None | Get CAPTCHA site key and provider |
49. Placement Test Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/placement/start |
User | Start CAT session, get first question |
| POST | /api/placement/answer |
User | Submit answer, get next question |
| POST | /api/placement/autosave |
User | Auto-save current state |
| POST | /api/placement/speaking-upload |
User | Upload speaking audio (multipart) |
| GET | /api/placement/speaking-status |
User | Poll speaking evaluation status |
| GET | /api/placement/results |
User | Get placement results |
| GET | /api/placement/learning-path |
User | Get generated learning path preview |
50. Exam Template Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/exam/templates |
Teacher/Admin | List all templates. Query params: type=international|custom, subject_id. International templates always included; custom templates filtered by user's entity. |
| GET | /api/exam/templates/:id |
Teacher/Admin | Get template detail with section structure |
| POST | /api/exam/templates/custom |
Teacher/Admin | Save a custom exam structure as a reusable template |
51. IELTS Exam Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/exam/ielts/create |
Admin/Teacher | Create IELTS exam |
| GET | /api/exam/ielts/:id |
Admin/Teacher | Get exam details |
| PUT | /api/exam/ielts/:id |
Admin/Teacher | Update exam (status, publish) |
| GET | /api/exam/ielts/:id/content-pool-count |
Admin/Teacher | Get available content counts |
| GET | /api/exam/ielts/:id/content-pool |
Admin/Teacher | Get content pool with filters |
| POST | /api/exam/ielts/:id/auto-assemble |
Admin/Teacher | Auto-select questions |
| POST | /api/exam/ielts/:id/suggest |
Admin/Teacher | Get hybrid suggestions |
| PUT | /api/exam/ielts/:id/sections/:sectionId/questions |
Admin/Teacher | Set section questions |
| GET | /api/exam/ielts/:id/validate |
Admin/Teacher | Run validation checks |
| POST | /api/exam/ielts/:id/assign |
Admin/Teacher | Assign to students/batches |
52. Custom Exam Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/exam/custom/create |
Teacher/Admin | Create a custom exam with sections and questions |
| GET | /api/exam/custom/:id |
Teacher/Admin | Get custom exam detail |
| PUT | /api/exam/custom/:id |
Teacher/Admin | Update custom exam (only while draft) |
| DELETE | /api/exam/custom/:id |
Teacher/Admin | Delete custom exam (only while draft) |
| POST | /api/exam/custom/:id/save-template |
Teacher/Admin | Save exam structure as reusable template |
| GET | /api/exam/custom/:id/validate |
Teacher/Admin | Run validation checks on custom exam |
| POST | /api/exam/custom/:id/assign |
Teacher/Admin | Assign custom exam to students/batches |
53. Exam Session Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/exam/:id/session |
Student | Load exam session |
| POST | /api/exam/:id/autosave |
Student | Auto-save answers |
| POST | /api/exam/:id/submit |
Student | Submit exam |
| GET | /api/exam/:id/results |
Student | Get results (if released) |
54. Grading Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/grading/queue |
Admin/Teacher | Get submissions pending grading |
| GET | /api/grading/:attemptId |
Admin/Teacher | Get submission for grading |
| POST | /api/grading/:attemptId/submit |
Admin/Teacher | Submit grade |
| POST | /api/grading/ai-suggest |
Admin/Teacher | Get AI grade suggestion |
55. Course Generation Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/course/gap-analysis |
User | Get gap analysis from exam/placement |
| POST | /api/course/auto-generate |
User | Auto-generate course from gap profile |
| POST | /api/course/create |
Admin/Teacher | Manually create course |
| GET | /api/course/:id |
User | Get course with modules and progress |
| PUT | /api/course/:id |
Admin/Teacher | Update course (publish) |
| POST | /api/course/:id/progress |
Student | Report resource completion |
| POST | /api/course/:id/checkpoint |
Student | Submit checkpoint exercise |
| POST | /api/course/:id/post-test |
System | Assign post-course assessment |
56. AI Course Generation Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/ai-course/english/create |
User | Start AI English course generation |
| POST | /api/ai-course/ielts/create |
User | Start AI IELTS course generation |
| GET | /api/ai-course/:id/quality |
Admin/Teacher | Get quality gate results |
| POST | /api/ai-course/:id/approve |
Admin/Teacher | Approve AI-generated content |
| POST | /api/ai-course/:id/reject |
Admin/Teacher | Reject with notes |
| GET | /api/ai-course/:id/validation |
Admin | Get IELTS validation status |
57. Entity Onboarding Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/entity/students/validate-csv |
Admin | Upload and validate CSV |
| POST | /api/entity/students/bulk-create |
Admin | Create accounts from validated CSV |
| POST | /api/entity/students/send-credentials |
Admin | Send credential emails |
| POST | /api/entity/students/:id/resend-credentials |
Admin | Resend to one student |
| POST | /api/entity/students/resend-all-pending |
Admin | Resend to all pending |
| POST | /api/entity/students/sis-import |
Admin | Import from SIS |
58. Adaptive Engine Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/adaptive/dashboard |
Admin | Get engine dashboard data |
| GET | /api/adaptive/students |
Admin/Teacher | Get students with adaptive profiles |
| GET | /api/adaptive/student/:id/signals |
Admin/Teacher | Get signal timeline for student |
| GET | /api/adaptive/settings |
Teacher | Get current threshold settings |
| PUT | /api/adaptive/settings |
Teacher | Update threshold settings |
59. Entity and Branding Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/entity/:id/level-mapping |
Admin | Get level mapping |
| PUT | /api/entity/:id/level-mapping |
Admin | Update level mapping |
| GET | /api/entity/:id/branding |
Admin | Get branding settings |
| PUT | /api/entity/:id/branding |
Admin | Update branding settings |
| GET | /api/entity/branding |
User | Get branding for current user's entity |
| GET | /api/entity/branding?domain=:subdomain |
None | Get branding by subdomain |
60. Score Release Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/scores/pending |
Admin | Get scores pending approval |
| POST | /api/scores/:attemptId/release |
Admin | Approve and release scores |
| POST | /api/scores/:attemptId/reject |
Admin | Reject with reason |
61. Report and Verification Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/reports/exam/:attemptId/pdf |
User | Generate and download PDF report |
| GET | /api/verify/:hash |
None | Public score verification |
62. Taxonomy Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/taxonomy/subjects |
User | Get available subjects |
| GET | /api/taxonomy/tree |
Admin | Get full taxonomy tree |
| POST | /api/taxonomy/node |
Admin | Create taxonomy node |
| PUT | /api/taxonomy/node/:id |
Admin | Update taxonomy node |
| DELETE | /api/taxonomy/node/:id |
Admin | Delete taxonomy node |
Part XIV -- AI/ML Service Integration
63. OpenAI GPT Integration
59.1 Content Generation Prompts
General English Reading Passage:
Generate a reading passage for English learners at CEFR level {cefr_level}.
Topic: {topic}
Word count: {min_words}-{max_words}
Include {question_count} comprehension questions in these formats: {question_types}
Provide answer keys for all questions.
Return JSON: {"passage": "...", "questions": [...], "answer_keys": [...]}
IELTS Writing Task 1 Prompt (Academic):
Generate an IELTS Academic Writing Task 1 prompt.
Target band: {target_band}
Topic: {topic}
Include:
1. A description of visual data (chart/graph/table/diagram)
2. The prompt text (exactly as it would appear on the IELTS exam)
3. A model answer at band {target_band} level (minimum 150 words)
4. Scoring rubric notes
Return JSON format.
IELTS Speaking Cue Card (Part 2):
Generate an IELTS Speaking Part 2 cue card.
Target band: {target_band}
Topic category: {category}
Include:
1. The main topic
2. 3-4 bullet points for the candidate to address
3. A model response (2 minutes, at band {target_band} level)
4. 3 follow-up questions for Part 3 (abstract discussion linked to the topic)
Return JSON format.
59.2 Configuration
| Parameter | Value |
|---|---|
encoach.openai_api_key |
Stored in ir.config_parameter |
encoach.openai_model_content |
gpt-4o (for content generation) |
encoach.openai_model_grading |
gpt-4o (for AI grading) |
encoach.openai_model_fast |
gpt-3.5-turbo (for tagging, classification) |
64. OpenAI Whisper Integration
60.1 Speech-to-Text
import whisper
class WhisperService:
def __init__(self):
self.model = whisper.load_model("base")
def transcribe(self, audio_path: str) -> str:
result = self.model.transcribe(audio_path)
return result["text"]
The Whisper model runs locally (no API call) to avoid costs and latency for frequent speaking evaluations.
65. Quality Gate Algorithms
61.1 CEFR-to-Flesch-Kincaid Mapping
| CEFR Level | Flesch-Kincaid Grade Range |
|---|---|
| A1 | 1--3 |
| A2 | 3--5 |
| B1 | 5--8 |
| B2 | 8--12 |
| C1 | 12--15 |
| C2 | 15+ |
61.2 Grammar Accuracy Check
Use a grammar checking library (e.g., language_tool_python) to detect grammar errors in AI-generated content:
import language_tool_python
tool = language_tool_python.LanguageTool('en-US')
def check_grammar(text):
matches = tool.check(text)
error_count = len(matches)
return {'passed': error_count == 0, 'errors': [m.message for m in matches]}
Dependency: pip install language_tool_python textstat
66. IRT Mathematical Model
62.1 Three-Parameter Logistic (3PL) Model
The probability that a student with ability theta answers correctly a question with parameters a, b, c:
P(theta) = c + (1 - c) / (1 + exp(-a * (theta - b)))
Where:
a= discrimination (slope at inflection point)b= difficulty (theta value where P = (1+c)/2)c= pseudo-guessing (lower asymptote)
62.2 Maximum Likelihood Estimation
After each response, update theta using Newton-Raphson iteration:
theta_new = theta_old + (sum of a_i * (u_i - P_i)) / (sum of a_i^2 * P_i * Q_i)
Where u_i = response (1=correct, 0=incorrect), P_i = probability at current theta, Q_i = 1 - P_i.
62.3 Standard Error of Measurement
SEM = 1 / sqrt(sum of I_i(theta))
Where I_i(theta) is the Fisher information of item i at the current theta estimate.
End of Document
Document Version: 1.1
New Odoo Modules: 13
Modified Existing Modules: 6
New Database Tables: 18+ (includes encoach.exam.template, encoach.exam.custom, encoach.exam.custom.section)
Modified Existing Tables: 8+
New API Endpoints: ~79 (includes exam template + custom exam endpoints)
AI/ML Components: OpenAI GPT, Whisper, IRT/CAT engine, quality gate algorithms, collaborative filtering, ML models
Adaptive Engine Phases: 4 (Rule-based, IRT-CAT, Collaborative, Full ML)
Exam Template Paths: 2 (International -- locked structure, seeded by developer; Custom -- fully teacher-configurable)
Python Dependencies: numpy, scipy, sympy, textstat, language_tool_python, reportlab, qrcode, whisper, scikit-learn
This document covers the complete backend implementation required by encoach_workflows_v3.pdf. The developer should implement these features alongside the existing 27 custom modules and 14 OpenEduCat modules.