feat: EnCoach V2 — complete OWL refactor with 15 new modules

Full architectural refactor from React to Odoo OWL:

- 15 new Odoo modules: signup, placement, exam_template, scoring,
  course_gen, entity_onboard, ai_course, quality_gate,
  ielts_validation, pdf_report, verification, math, it, portal,
  exam_session
- 6 modified modules: core (new user fields), exam (template types),
  adaptive (events/paths/settings), branding (white-label),
  resources (CEFR/AI fields), taxonomy (Math+IT hierarchies)
- ~79 new REST API endpoints across all controller packages
- ~50 admin backend views (form/list/kanban/search/menu)
- 5 custom OWL components: dashboard, content pool, exam validation,
  gap analysis, adaptive timeline
- POS-inspired full-screen exam session with 9 question renderers
- Student portal with 12 website pages (QWeb + OWL)
- AI/ML services: IRT 3PL CAT engine, CEFR mapper, quality gates,
  IELTS validator, SymPy math scorer, speaking evaluator, adaptive engine
- Seed data: IELTS/TOEFL/STEP/IC3 templates, 30+ sample questions,
  English/Math/IT taxonomy trees with 50+ topics
- Updated requirements.txt with new dependencies

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-07 01:53:06 +04:00
parent a4b9ab62cf
commit 6c93c5d600
233 changed files with 23446 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
from . import models
from . import controllers

View File

@@ -0,0 +1,15 @@
{
'name': 'EnCoach Taxonomy',
'version': '19.0.1.0',
'category': 'Education',
'summary': 'Subject taxonomy: subjects, domains, topics, learning objectives',
'author': 'EnCoach',
'depends': ['encoach_core'],
'data': [
'security/ir.model.access.csv',
'data/default_subjects.xml',
'data/math_it_taxonomy.xml',
],
'installable': True,
'license': 'LGPL-3',
}

View File

@@ -0,0 +1 @@
from . import taxonomy

View File

@@ -0,0 +1,239 @@
import json
import logging
from odoo import http
from odoo.http import request
from odoo.addons.encoach_api.controllers.base import (
jwt_required, _json_response, _error_response, _get_json_body, _paginate
)
_logger = logging.getLogger(__name__)
TAXONOMY_MODELS = {
'subject': 'encoach.subject',
'domain': 'encoach.domain',
'topic': 'encoach.topic',
'learning_objective': 'encoach.learning.objective',
}
PARENT_FIELD_MAP = {
'domain': 'subject_id',
'topic': 'domain_id',
'learning_objective': 'topic_id',
}
class EncoachTaxonomyController(http.Controller):
# ------------------------------------------------------------------
# GET /api/taxonomy/subjects
# ------------------------------------------------------------------
@http.route('/api/taxonomy/subjects', type='http', auth='none',
methods=['GET'], csrf=False)
@jwt_required
def get_subjects(self, **kw):
try:
Subject = request.env['encoach.subject'].sudo()
subjects = Subject.search([('active', '=', True)])
items = []
for s in subjects:
items.append({
'id': s.id,
'name': s.name,
'code': s.code,
'icon': s.icon or '',
'domain_count': len(s.domain_ids),
})
return _json_response({'subjects': items})
except Exception as e:
_logger.exception('get_subjects failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/taxonomy/tree
# ------------------------------------------------------------------
@http.route('/api/taxonomy/tree', type='http', auth='none',
methods=['GET'], csrf=False)
@jwt_required
def get_tree(self, **kw):
try:
Subject = request.env['encoach.subject'].sudo()
subjects = Subject.search([('active', '=', True)])
tree = []
for s in subjects:
domains = []
for d in s.domain_ids:
topics = []
for t in d.topic_ids:
objectives = []
for o in t.learning_objective_ids:
objectives.append({
'id': o.id,
'name': o.name,
'bloom_level': o.bloom_level or '',
'description': o.description or '',
})
topics.append({
'id': t.id,
'name': t.name,
'difficulty': t.difficulty or '',
'description': t.description or '',
'learning_objectives': objectives,
})
domains.append({
'id': d.id,
'name': d.name,
'description': d.description or '',
'topics': topics,
})
tree.append({
'id': s.id,
'name': s.name,
'code': s.code,
'icon': s.icon or '',
'domains': domains,
})
return _json_response({'tree': tree})
except Exception as e:
_logger.exception('get_tree failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# POST /api/taxonomy/node
# ------------------------------------------------------------------
@http.route('/api/taxonomy/node', type='http', auth='none',
methods=['POST'], csrf=False)
@jwt_required
def create_node(self, **kw):
try:
body = _get_json_body()
node_type = body.get('type')
if not node_type or node_type not in TAXONOMY_MODELS:
return _error_response(
f'type must be one of: {", ".join(TAXONOMY_MODELS.keys())}', 400,
)
name = body.get('name')
if not name:
return _error_response('name is required', 400)
model_name = TAXONOMY_MODELS[node_type]
Model = request.env[model_name].sudo()
vals = {'name': name}
if body.get('description'):
vals['description'] = body['description']
if node_type == 'subject':
code = body.get('code')
if not code:
return _error_response('code is required for subjects', 400)
vals['code'] = code
if body.get('icon'):
vals['icon'] = body['icon']
else:
parent_id = body.get('parent_id')
if not parent_id:
parent_field = PARENT_FIELD_MAP[node_type]
return _error_response(
f'parent_id ({parent_field}) is required for {node_type}', 400,
)
vals[PARENT_FIELD_MAP[node_type]] = int(parent_id)
if node_type == 'topic' and body.get('difficulty'):
vals['difficulty'] = body['difficulty']
if node_type == 'learning_objective' and body.get('bloom_level'):
vals['bloom_level'] = body['bloom_level']
record = Model.create(vals)
return _json_response({
'id': record.id,
'type': node_type,
'name': record.name,
}, 201)
except Exception as e:
_logger.exception('create_node failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# PUT /api/taxonomy/node/<int:node_id>
# ------------------------------------------------------------------
@http.route('/api/taxonomy/node/<int:node_id>', type='http', auth='none',
methods=['PUT'], csrf=False)
@jwt_required
def update_node(self, node_id, **kw):
try:
body = _get_json_body()
node_type = body.get('type')
if not node_type or node_type not in TAXONOMY_MODELS:
return _error_response(
f'type must be one of: {", ".join(TAXONOMY_MODELS.keys())}', 400,
)
model_name = TAXONOMY_MODELS[node_type]
Model = request.env[model_name].sudo()
record = Model.browse(node_id)
if not record.exists():
return _error_response(f'{node_type} not found', 404)
vals = {}
if body.get('name'):
vals['name'] = body['name']
if 'description' in body:
vals['description'] = body.get('description') or ''
if node_type == 'subject':
if body.get('code'):
vals['code'] = body['code']
if 'icon' in body:
vals['icon'] = body.get('icon') or ''
if 'active' in body:
vals['active'] = bool(body['active'])
elif node_type == 'topic' and body.get('difficulty'):
vals['difficulty'] = body['difficulty']
elif node_type == 'learning_objective' and body.get('bloom_level'):
vals['bloom_level'] = body['bloom_level']
if vals:
record.write(vals)
return _json_response(record.to_api_dict())
except Exception as e:
_logger.exception('update_node failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# DELETE /api/taxonomy/node/<int:node_id>
# ------------------------------------------------------------------
@http.route('/api/taxonomy/node/<int:node_id>', type='http', auth='none',
methods=['DELETE'], csrf=False)
@jwt_required
def delete_node(self, node_id, **kw):
try:
body = _get_json_body()
node_type = body.get('type') or kw.get('type')
if not node_type or node_type not in TAXONOMY_MODELS:
return _error_response(
f'type must be one of: {", ".join(TAXONOMY_MODELS.keys())}', 400,
)
model_name = TAXONOMY_MODELS[node_type]
Model = request.env[model_name].sudo()
record = Model.browse(node_id)
if not record.exists():
return _error_response(f'{node_type} not found', 404)
record.unlink()
return _json_response({}, 204)
except Exception as e:
_logger.exception('delete_node failed')
return _error_response(str(e), 500)

View File

@@ -0,0 +1,304 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- ===================== ENGLISH DOMAINS & TOPICS ===================== -->
<record id="domain_eng_listening" model="encoach.domain">
<field name="name">Listening</field>
<field name="subject_id" ref="subject_english"/>
<field name="description">Listening comprehension skills</field>
</record>
<record id="domain_eng_reading" model="encoach.domain">
<field name="name">Reading</field>
<field name="subject_id" ref="subject_english"/>
<field name="description">Reading comprehension and analysis</field>
</record>
<record id="domain_eng_writing" model="encoach.domain">
<field name="name">Writing</field>
<field name="subject_id" ref="subject_english"/>
<field name="description">Written communication skills</field>
</record>
<record id="domain_eng_speaking" model="encoach.domain">
<field name="name">Speaking</field>
<field name="subject_id" ref="subject_english"/>
<field name="description">Spoken communication and fluency</field>
</record>
<record id="domain_eng_grammar" model="encoach.domain">
<field name="name">Grammar</field>
<field name="subject_id" ref="subject_english"/>
<field name="description">English grammar rules and usage</field>
</record>
<record id="domain_eng_vocabulary" model="encoach.domain">
<field name="name">Vocabulary</field>
<field name="subject_id" ref="subject_english"/>
<field name="description">Word knowledge and usage</field>
</record>
<!-- English Topics -->
<record id="topic_eng_conversation" model="encoach.topic">
<field name="name">Conversation Comprehension</field>
<field name="domain_id" ref="domain_eng_listening"/>
</record>
<record id="topic_eng_monologue" model="encoach.topic">
<field name="name">Monologue Comprehension</field>
<field name="domain_id" ref="domain_eng_listening"/>
</record>
<record id="topic_eng_note_taking" model="encoach.topic">
<field name="name">Note Taking &amp; Form Completion</field>
<field name="domain_id" ref="domain_eng_listening"/>
</record>
<record id="topic_eng_skimming" model="encoach.topic">
<field name="name">Skimming &amp; Scanning</field>
<field name="domain_id" ref="domain_eng_reading"/>
</record>
<record id="topic_eng_detailed_reading" model="encoach.topic">
<field name="name">Detailed Reading</field>
<field name="domain_id" ref="domain_eng_reading"/>
</record>
<record id="topic_eng_inference" model="encoach.topic">
<field name="name">Inference &amp; Implication</field>
<field name="domain_id" ref="domain_eng_reading"/>
</record>
<record id="topic_eng_task1_writing" model="encoach.topic">
<field name="name">Task 1 — Report/Letter Writing</field>
<field name="domain_id" ref="domain_eng_writing"/>
</record>
<record id="topic_eng_task2_essay" model="encoach.topic">
<field name="name">Task 2 — Essay Writing</field>
<field name="domain_id" ref="domain_eng_writing"/>
</record>
<record id="topic_eng_part1_intro" model="encoach.topic">
<field name="name">Part 1 — Introduction &amp; Interview</field>
<field name="domain_id" ref="domain_eng_speaking"/>
</record>
<record id="topic_eng_part2_cue" model="encoach.topic">
<field name="name">Part 2 — Cue Card</field>
<field name="domain_id" ref="domain_eng_speaking"/>
</record>
<record id="topic_eng_part3_discussion" model="encoach.topic">
<field name="name">Part 3 — Discussion</field>
<field name="domain_id" ref="domain_eng_speaking"/>
</record>
<record id="topic_eng_tenses" model="encoach.topic">
<field name="name">Tenses</field>
<field name="domain_id" ref="domain_eng_grammar"/>
</record>
<record id="topic_eng_conditionals" model="encoach.topic">
<field name="name">Conditionals</field>
<field name="domain_id" ref="domain_eng_grammar"/>
</record>
<record id="topic_eng_passive_voice" model="encoach.topic">
<field name="name">Passive Voice</field>
<field name="domain_id" ref="domain_eng_grammar"/>
</record>
<record id="topic_eng_articles_determiners" model="encoach.topic">
<field name="name">Articles &amp; Determiners</field>
<field name="domain_id" ref="domain_eng_grammar"/>
</record>
<record id="topic_eng_academic_vocab" model="encoach.topic">
<field name="name">Academic Vocabulary</field>
<field name="domain_id" ref="domain_eng_vocabulary"/>
</record>
<record id="topic_eng_collocations" model="encoach.topic">
<field name="name">Collocations &amp; Phrasal Verbs</field>
<field name="domain_id" ref="domain_eng_vocabulary"/>
</record>
<!-- ===================== MATHEMATICS DOMAINS & TOPICS ===================== -->
<record id="domain_math_algebra" model="encoach.domain">
<field name="name">Algebra</field>
<field name="subject_id" ref="subject_math"/>
<field name="description">Algebraic expressions, equations, and functions</field>
</record>
<record id="domain_math_geometry" model="encoach.domain">
<field name="name">Geometry</field>
<field name="subject_id" ref="subject_math"/>
<field name="description">Shapes, areas, volumes, and geometric proofs</field>
</record>
<record id="domain_math_calculus" model="encoach.domain">
<field name="name">Calculus</field>
<field name="subject_id" ref="subject_math"/>
<field name="description">Limits, derivatives, integrals</field>
</record>
<record id="domain_math_statistics" model="encoach.domain">
<field name="name">Statistics &amp; Probability</field>
<field name="subject_id" ref="subject_math"/>
<field name="description">Data analysis, probability, distributions</field>
</record>
<record id="domain_math_linear_algebra" model="encoach.domain">
<field name="name">Linear Algebra</field>
<field name="subject_id" ref="subject_math"/>
<field name="description">Matrices, vectors, linear transformations</field>
</record>
<record id="domain_math_number_theory" model="encoach.domain">
<field name="name">Number Theory</field>
<field name="subject_id" ref="subject_math"/>
<field name="description">Integers, primes, divisibility</field>
</record>
<!-- Math Topics -->
<record id="topic_math_linear_eq" model="encoach.topic">
<field name="name">Linear Equations</field>
<field name="domain_id" ref="domain_math_algebra"/>
</record>
<record id="topic_math_quadratic_eq" model="encoach.topic">
<field name="name">Quadratic Equations</field>
<field name="domain_id" ref="domain_math_algebra"/>
</record>
<record id="topic_math_polynomials" model="encoach.topic">
<field name="name">Polynomials</field>
<field name="domain_id" ref="domain_math_algebra"/>
</record>
<record id="topic_math_functions" model="encoach.topic">
<field name="name">Functions &amp; Graphs</field>
<field name="domain_id" ref="domain_math_algebra"/>
</record>
<record id="topic_math_triangles" model="encoach.topic">
<field name="name">Triangles &amp; Trigonometry</field>
<field name="domain_id" ref="domain_math_geometry"/>
</record>
<record id="topic_math_circles" model="encoach.topic">
<field name="name">Circles</field>
<field name="domain_id" ref="domain_math_geometry"/>
</record>
<record id="topic_math_coordinate" model="encoach.topic">
<field name="name">Coordinate Geometry</field>
<field name="domain_id" ref="domain_math_geometry"/>
</record>
<record id="topic_math_limits" model="encoach.topic">
<field name="name">Limits &amp; Continuity</field>
<field name="domain_id" ref="domain_math_calculus"/>
</record>
<record id="topic_math_derivatives" model="encoach.topic">
<field name="name">Derivatives</field>
<field name="domain_id" ref="domain_math_calculus"/>
</record>
<record id="topic_math_integrals" model="encoach.topic">
<field name="name">Integrals</field>
<field name="domain_id" ref="domain_math_calculus"/>
</record>
<record id="topic_math_descriptive" model="encoach.topic">
<field name="name">Descriptive Statistics</field>
<field name="domain_id" ref="domain_math_statistics"/>
</record>
<record id="topic_math_probability" model="encoach.topic">
<field name="name">Probability</field>
<field name="domain_id" ref="domain_math_statistics"/>
</record>
<record id="topic_math_matrices" model="encoach.topic">
<field name="name">Matrix Operations</field>
<field name="domain_id" ref="domain_math_linear_algebra"/>
</record>
<record id="topic_math_vectors" model="encoach.topic">
<field name="name">Vectors</field>
<field name="domain_id" ref="domain_math_linear_algebra"/>
</record>
<record id="topic_math_primes" model="encoach.topic">
<field name="name">Prime Numbers &amp; Factorization</field>
<field name="domain_id" ref="domain_math_number_theory"/>
</record>
<!-- ===================== IT DOMAINS & TOPICS ===================== -->
<record id="domain_it_programming" model="encoach.domain">
<field name="name">Programming Fundamentals</field>
<field name="subject_id" ref="subject_it"/>
<field name="description">Core programming concepts and languages</field>
</record>
<record id="domain_it_databases" model="encoach.domain">
<field name="name">Databases</field>
<field name="subject_id" ref="subject_it"/>
<field name="description">Database design, SQL, and data management</field>
</record>
<record id="domain_it_networking" model="encoach.domain">
<field name="name">Networking</field>
<field name="subject_id" ref="subject_it"/>
<field name="description">Computer networks, protocols, and security</field>
</record>
<record id="domain_it_web_dev" model="encoach.domain">
<field name="name">Web Development</field>
<field name="subject_id" ref="subject_it"/>
<field name="description">Frontend, backend, and full-stack web development</field>
</record>
<record id="domain_it_os" model="encoach.domain">
<field name="name">Operating Systems</field>
<field name="subject_id" ref="subject_it"/>
<field name="description">OS concepts, Linux, process management</field>
</record>
<record id="domain_it_security" model="encoach.domain">
<field name="name">Cybersecurity</field>
<field name="subject_id" ref="subject_it"/>
<field name="description">Information security principles and practices</field>
</record>
<!-- IT Topics -->
<record id="topic_it_variables" model="encoach.topic">
<field name="name">Variables &amp; Data Types</field>
<field name="domain_id" ref="domain_it_programming"/>
</record>
<record id="topic_it_control_flow" model="encoach.topic">
<field name="name">Control Flow</field>
<field name="domain_id" ref="domain_it_programming"/>
</record>
<record id="topic_it_functions" model="encoach.topic">
<field name="name">Functions &amp; Modules</field>
<field name="domain_id" ref="domain_it_programming"/>
</record>
<record id="topic_it_oop" model="encoach.topic">
<field name="name">Object-Oriented Programming</field>
<field name="domain_id" ref="domain_it_programming"/>
</record>
<record id="topic_it_algorithms" model="encoach.topic">
<field name="name">Algorithms &amp; Data Structures</field>
<field name="domain_id" ref="domain_it_programming"/>
</record>
<record id="topic_it_sql_basics" model="encoach.topic">
<field name="name">SQL Basics</field>
<field name="domain_id" ref="domain_it_databases"/>
</record>
<record id="topic_it_joins" model="encoach.topic">
<field name="name">Joins &amp; Subqueries</field>
<field name="domain_id" ref="domain_it_databases"/>
</record>
<record id="topic_it_db_design" model="encoach.topic">
<field name="name">Database Design &amp; Normalization</field>
<field name="domain_id" ref="domain_it_databases"/>
</record>
<record id="topic_it_tcp_ip" model="encoach.topic">
<field name="name">TCP/IP &amp; OSI Model</field>
<field name="domain_id" ref="domain_it_networking"/>
</record>
<record id="topic_it_routing" model="encoach.topic">
<field name="name">Routing &amp; Switching</field>
<field name="domain_id" ref="domain_it_networking"/>
</record>
<record id="topic_it_html_css" model="encoach.topic">
<field name="name">HTML &amp; CSS</field>
<field name="domain_id" ref="domain_it_web_dev"/>
</record>
<record id="topic_it_javascript" model="encoach.topic">
<field name="name">JavaScript</field>
<field name="domain_id" ref="domain_it_web_dev"/>
</record>
<record id="topic_it_backend" model="encoach.topic">
<field name="name">Backend Development</field>
<field name="domain_id" ref="domain_it_web_dev"/>
</record>
<record id="topic_it_linux" model="encoach.topic">
<field name="name">Linux Administration</field>
<field name="domain_id" ref="domain_it_os"/>
</record>
<record id="topic_it_processes" model="encoach.topic">
<field name="name">Process Management</field>
<field name="domain_id" ref="domain_it_os"/>
</record>
<record id="topic_it_encryption" model="encoach.topic">
<field name="name">Encryption &amp; Cryptography</field>
<field name="domain_id" ref="domain_it_security"/>
</record>
<record id="topic_it_threats" model="encoach.topic">
<field name="name">Threats &amp; Vulnerability Assessment</field>
<field name="domain_id" ref="domain_it_security"/>
</record>
</data>
</odoo>