feat: initial backend codebase — EnCoach v3
Complete Odoo 19 backend with 25 custom addons: - encoach_core: user/entity/role management - encoach_api: REST API + JWT auth - encoach_ai: OpenAI integration, AI settings, generation - encoach_ai_course: AI-powered English & IELTS course generation - encoach_exam_template/session: exam creation, structures, sessions - encoach_scoring: AI auto-grading + manual approval - encoach_vector: pgvector RAG integration - encoach_adaptive: adaptive learning engine - encoach_placement: placement testing - encoach_taxonomy/resources: content taxonomy & resource management - Plus 14 more modules for courses, branding, portal, etc. Includes docs: user guide, generation report, developer workflow. Made-with: Cursor
This commit is contained in:
2
custom_addons/encoach_branding/__init__.py
Normal file
2
custom_addons/encoach_branding/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import controllers
|
||||
15
custom_addons/encoach_branding/__manifest__.py
Normal file
15
custom_addons/encoach_branding/__manifest__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
'name': 'EnCoach Branding',
|
||||
'version': '19.0.1.0',
|
||||
'category': 'Education',
|
||||
'summary': 'Whitelabeling and custom branding per entity',
|
||||
'author': 'EnCoach',
|
||||
'depends': ['encoach_core'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/branding_views.xml',
|
||||
'views/branding_menus.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
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)
|
||||
1
custom_addons/encoach_branding/models/__init__.py
Normal file
1
custom_addons/encoach_branding/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import branding
|
||||
40
custom_addons/encoach_branding/models/branding.py
Normal file
40
custom_addons/encoach_branding/models/branding.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class EncoachBranding(models.Model):
|
||||
_name = 'encoach.branding'
|
||||
_description = 'Entity Branding'
|
||||
|
||||
entity_id = fields.Many2one('encoach.entity', required=True, ondelete='cascade')
|
||||
primary_color = fields.Char(default='#4F46E5')
|
||||
secondary_color = fields.Char(default='#7C3AED')
|
||||
background_color = fields.Char(default='#FFFFFF')
|
||||
logo = fields.Binary(attachment=True)
|
||||
logo_url = fields.Char(string='Logo URL')
|
||||
favicon = fields.Binary(attachment=True)
|
||||
app_name = fields.Char(default='EnCoach')
|
||||
white_label_domain = fields.Char(string='Subdomain')
|
||||
login_title = fields.Char(default='Welcome to EnCoach')
|
||||
login_description = fields.Text()
|
||||
custom_css = fields.Text()
|
||||
|
||||
_entity_unique = models.Constraint('UNIQUE(entity_id)', 'Branding record must be unique per entity.')
|
||||
|
||||
def to_api_dict(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'id': self.id,
|
||||
'entity_id': self.entity_id.id,
|
||||
'entity_name': self.entity_id.name,
|
||||
'primary_color': self.primary_color,
|
||||
'secondary_color': self.secondary_color,
|
||||
'background_color': self.background_color or '#FFFFFF',
|
||||
'has_logo': bool(self.logo),
|
||||
'logo_url': self.logo_url or '',
|
||||
'has_favicon': bool(self.favicon),
|
||||
'app_name': self.app_name,
|
||||
'white_label_domain': self.white_label_domain or '',
|
||||
'login_title': self.login_title or '',
|
||||
'login_description': self.login_description or '',
|
||||
'custom_css': self.custom_css or '',
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_encoach_branding_all,encoach.branding.all,model_encoach_branding,base.group_user,1,1,1,1
|
||||
|
10
custom_addons/encoach_branding/views/branding_menus.xml
Normal file
10
custom_addons/encoach_branding/views/branding_menus.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<menuitem id="menu_branding"
|
||||
name="White-Label Branding"
|
||||
parent="encoach_core.menu_encoach_entity"
|
||||
action="encoach_branding.action_branding"
|
||||
sequence="20"/>
|
||||
|
||||
</odoo>
|
||||
61
custom_addons/encoach_branding/views/branding_views.xml
Normal file
61
custom_addons/encoach_branding/views/branding_views.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_branding_form" model="ir.ui.view">
|
||||
<field name="name">encoach.branding.form</field>
|
||||
<field name="model">encoach.branding</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Branding">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="entity_id"/>
|
||||
<field name="app_name"/>
|
||||
<field name="white_label_domain"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="logo" widget="image" class="oe_avatar"/>
|
||||
<field name="logo_url"/>
|
||||
<field name="favicon" widget="image" class="oe_avatar"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Colors">
|
||||
<group>
|
||||
<field name="primary_color" widget="color"/>
|
||||
<field name="secondary_color" widget="color"/>
|
||||
<field name="background_color" widget="color"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Login Page">
|
||||
<field name="login_title"/>
|
||||
<field name="login_description"/>
|
||||
</group>
|
||||
<group string="Custom CSS">
|
||||
<field name="custom_css"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_branding_list" model="ir.ui.view">
|
||||
<field name="name">encoach.branding.list</field>
|
||||
<field name="model">encoach.branding</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Branding">
|
||||
<field name="entity_id"/>
|
||||
<field name="app_name"/>
|
||||
<field name="primary_color"/>
|
||||
<field name="secondary_color"/>
|
||||
<field name="white_label_domain"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_branding" model="ir.actions.act_window">
|
||||
<field name="name">White-Label Branding</field>
|
||||
<field name="res_model">encoach.branding</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user