feat(v3): restructure project + add complete frontend

- 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
This commit is contained in:
Yamen Ahmad
2026-04-10 17:26:42 +04:00
commit 907a5c0e92
331 changed files with 23511 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,230 @@
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 or '',
'description': s.description 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,
'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 or '',
'description': s.description 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
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 == '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 'active' in body:
vals['active'] = bool(body['active'])
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)