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:
1
custom_addons/encoach_branding/controllers/__init__.py
Normal file
1
custom_addons/encoach_branding/controllers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import branding
|
||||
217
custom_addons/encoach_branding/controllers/branding.py
Normal file
217
custom_addons/encoach_branding/controllers/branding.py
Normal file
@@ -0,0 +1,217 @@
|
||||
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,
|
||||
validate_token,
|
||||
)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EncoachBrandingController(http.Controller):
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/entity/<int:entity_id>/level-mapping
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/api/entity/<int:entity_id>/level-mapping', type='http',
|
||||
auth='none', methods=['GET'], csrf=False)
|
||||
@jwt_required
|
||||
def get_level_mapping(self, entity_id, **kw):
|
||||
try:
|
||||
Entity = request.env['encoach.entity'].sudo()
|
||||
entity = Entity.browse(entity_id)
|
||||
if not entity.exists():
|
||||
return _error_response('Entity not found', 404)
|
||||
|
||||
Mapping = request.env['encoach.entity.level.mapping'].sudo()
|
||||
mappings = Mapping.search(
|
||||
[('entity_id', '=', entity_id)],
|
||||
order='min_score asc',
|
||||
)
|
||||
|
||||
items = []
|
||||
for m in mappings:
|
||||
items.append({
|
||||
'id': m.id,
|
||||
'entity_id': m.entity_id.id,
|
||||
'min_score': m.min_score,
|
||||
'max_score': m.max_score,
|
||||
'internal_level_name': m.internal_level_name,
|
||||
'cefr_equivalent': m.cefr_equivalent or '',
|
||||
})
|
||||
|
||||
return _json_response({'mappings': items})
|
||||
|
||||
except Exception as e:
|
||||
_logger.exception('get_level_mapping failed')
|
||||
return _error_response(str(e), 500)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PUT /api/entity/<int:entity_id>/level-mapping
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/api/entity/<int:entity_id>/level-mapping', type='http',
|
||||
auth='none', methods=['PUT'], csrf=False)
|
||||
@jwt_required
|
||||
def update_level_mapping(self, entity_id, **kw):
|
||||
try:
|
||||
body = _get_json_body()
|
||||
mappings_data = body.get('mappings', [])
|
||||
if not mappings_data:
|
||||
return _error_response('mappings list is required', 400)
|
||||
|
||||
Entity = request.env['encoach.entity'].sudo()
|
||||
entity = Entity.browse(entity_id)
|
||||
if not entity.exists():
|
||||
return _error_response('Entity not found', 404)
|
||||
|
||||
Mapping = request.env['encoach.entity.level.mapping'].sudo()
|
||||
Mapping.search([('entity_id', '=', entity_id)]).unlink()
|
||||
|
||||
new_mappings = []
|
||||
for md in mappings_data:
|
||||
if not md.get('internal_level_name'):
|
||||
return _error_response('internal_level_name is required for each mapping', 400)
|
||||
|
||||
rec = Mapping.create({
|
||||
'entity_id': entity_id,
|
||||
'min_score': float(md.get('min_score', 0)),
|
||||
'max_score': float(md.get('max_score', 0)),
|
||||
'internal_level_name': md['internal_level_name'],
|
||||
'cefr_equivalent': md.get('cefr_equivalent') or False,
|
||||
})
|
||||
new_mappings.append({
|
||||
'id': rec.id,
|
||||
'entity_id': rec.entity_id.id,
|
||||
'min_score': rec.min_score,
|
||||
'max_score': rec.max_score,
|
||||
'internal_level_name': rec.internal_level_name,
|
||||
'cefr_equivalent': rec.cefr_equivalent or '',
|
||||
})
|
||||
|
||||
return _json_response({'mappings': new_mappings})
|
||||
|
||||
except Exception as e:
|
||||
_logger.exception('update_level_mapping failed')
|
||||
return _error_response(str(e), 500)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/entity/<int:entity_id>/branding
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/api/entity/<int:entity_id>/branding', type='http',
|
||||
auth='none', methods=['GET'], csrf=False)
|
||||
@jwt_required
|
||||
def get_entity_branding(self, entity_id, **kw):
|
||||
try:
|
||||
Branding = request.env['encoach.branding'].sudo()
|
||||
branding = Branding.search([('entity_id', '=', entity_id)], limit=1)
|
||||
if not branding:
|
||||
return _error_response('Branding not found for this entity', 404)
|
||||
|
||||
return _json_response(branding.to_api_dict())
|
||||
|
||||
except Exception as e:
|
||||
_logger.exception('get_entity_branding failed')
|
||||
return _error_response(str(e), 500)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PUT /api/entity/<int:entity_id>/branding
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/api/entity/<int:entity_id>/branding', type='http',
|
||||
auth='none', methods=['PUT'], csrf=False)
|
||||
@jwt_required
|
||||
def update_entity_branding(self, entity_id, **kw):
|
||||
try:
|
||||
body = _get_json_body()
|
||||
|
||||
Entity = request.env['encoach.entity'].sudo()
|
||||
entity = Entity.browse(entity_id)
|
||||
if not entity.exists():
|
||||
return _error_response('Entity not found', 404)
|
||||
|
||||
Branding = request.env['encoach.branding'].sudo()
|
||||
branding = Branding.search([('entity_id', '=', entity_id)], limit=1)
|
||||
|
||||
allowed = [
|
||||
'primary_color', 'secondary_color', 'background_color',
|
||||
'app_name', 'login_title', 'login_description',
|
||||
'white_label_domain', 'custom_css',
|
||||
]
|
||||
vals = {k: body[k] for k in allowed if k in body}
|
||||
|
||||
if not branding:
|
||||
vals['entity_id'] = entity_id
|
||||
branding = Branding.create(vals)
|
||||
else:
|
||||
branding.write(vals)
|
||||
|
||||
return _json_response(branding.to_api_dict())
|
||||
|
||||
except Exception as e:
|
||||
_logger.exception('update_entity_branding failed')
|
||||
return _error_response(str(e), 500)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/entity/branding (combined public + authenticated)
|
||||
# GET /api/entity/branding/public
|
||||
#
|
||||
# If ?domain=<subdomain> is present → public lookup, no auth needed.
|
||||
# Otherwise → JWT required, return user entity branding.
|
||||
# ------------------------------------------------------------------
|
||||
@http.route(
|
||||
['/api/entity/branding', '/api/entity/branding/public'],
|
||||
type='http', auth='none', methods=['GET'], csrf=False,
|
||||
)
|
||||
def get_branding(self, **kw):
|
||||
try:
|
||||
domain_param = kw.get('domain')
|
||||
|
||||
if domain_param:
|
||||
Branding = request.env(su=True)['encoach.branding']
|
||||
branding = Branding.search(
|
||||
[('white_label_domain', '=', domain_param)], limit=1,
|
||||
)
|
||||
if not branding:
|
||||
return _error_response('Branding not found for this domain', 404)
|
||||
|
||||
return _json_response({
|
||||
'app_name': branding.app_name or 'EnCoach',
|
||||
'primary_color': branding.primary_color or '#4F46E5',
|
||||
'secondary_color': branding.secondary_color or '#7C3AED',
|
||||
'background_color': branding.background_color or '#FFFFFF',
|
||||
'has_logo': bool(branding.logo),
|
||||
'logo_url': branding.logo_url or '',
|
||||
'login_title': branding.login_title or '',
|
||||
'login_description': branding.login_description or '',
|
||||
'custom_css': branding.custom_css or '',
|
||||
})
|
||||
|
||||
user = validate_token()
|
||||
if not user:
|
||||
return _error_response('Missing or invalid Authorization header', 401)
|
||||
entity = user.entity_ids[:1]
|
||||
if not entity:
|
||||
return _error_response('User has no associated entity', 404)
|
||||
|
||||
Branding = request.env['encoach.branding'].sudo()
|
||||
branding = Branding.search([('entity_id', '=', entity.id)], limit=1)
|
||||
if not branding:
|
||||
return _json_response({
|
||||
'entity_id': entity.id,
|
||||
'entity_name': entity.name,
|
||||
'app_name': 'EnCoach',
|
||||
'primary_color': entity.primary_color or '#4F46E5',
|
||||
'secondary_color': entity.secondary_color or '#7C3AED',
|
||||
'background_color': entity.background_color or '#FFFFFF',
|
||||
'has_logo': bool(entity.logo),
|
||||
'logo_url': entity.logo_url or '',
|
||||
'login_title': entity.login_title or '',
|
||||
'login_description': entity.login_description or '',
|
||||
})
|
||||
|
||||
return _json_response(branding.to_api_dict())
|
||||
|
||||
except Exception as e:
|
||||
_logger.exception('get_branding failed')
|
||||
return _error_response(str(e), 500)
|
||||
Reference in New Issue
Block a user