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:
Yamen Ahmad
2026-04-11 15:44:20 +04:00
commit 982d4bca30
371 changed files with 35211 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,225 @@
import json
import logging
from odoo import http, fields
from odoo.http import request
from odoo.addons.portal.controllers.portal import CustomerPortal
_logger = logging.getLogger(__name__)
class EncoachPortal(CustomerPortal):
def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
if 'course_count' in counters:
values['course_count'] = request.env['op.course'].sudo().search_count([])
return values
@http.route('/register', type='http', auth='public', website=True)
def register_page(self, **kw):
captcha_key = request.env['ir.config_parameter'].sudo().get_param(
'encoach.captcha_site_key', '',
)
return request.render('encoach_portal.register', {
'captcha_site_key': captcha_key,
})
@http.route('/verify-email', type='http', auth='public', website=True)
def verify_email_page(self, **kw):
return request.render('encoach_portal.verify_email', {
'email': kw.get('email', ''),
})
@http.route('/onboarding', type='http', auth='user', website=True)
def onboarding_page(self, **kw):
user = request.env.user
profile = request.env['encoach.student.profile'].sudo().search(
[('user_id', '=', user.id)], limit=1,
)
return request.render('encoach_portal.onboarding', {
'user': user,
'profile': profile,
})
# ------------------------------------------------------------------
# JSON-RPC: /encoach/onboarding/goals (called by OWL wizard)
# ------------------------------------------------------------------
@http.route('/encoach/onboarding/goals', type='json', auth='user')
def onboarding_goals_rpc(self, **kw):
Template = request.env['encoach.exam.template'].sudo()
templates = Template.search([('active', '=', True)])
seen = set()
goals = []
icon_map = {
'ielts': 'fa-language', 'general_english': 'fa-comments',
'general english': 'fa-comments', 'academic': 'fa-graduation-cap',
'math': 'fa-calculator', 'mathematics': 'fa-calculator',
'it': 'fa-laptop', 'information technology': 'fa-laptop',
}
for tpl in templates:
subj = tpl.subject_id
if subj and subj.name and subj.name.lower() not in seen:
seen.add(subj.name.lower())
goal_id = subj.name.lower().replace(' ', '_')
goals.append({
'id': goal_id,
'name': subj.name,
'icon': icon_map.get(goal_id, icon_map.get(subj.name.lower(), 'fa-book')),
})
if not goals:
goals = [
{'id': 'ielts', 'name': 'IELTS Preparation', 'icon': 'fa-language'},
{'id': 'general_english', 'name': 'General English', 'icon': 'fa-comments'},
{'id': 'math', 'name': 'Mathematics', 'icon': 'fa-calculator'},
{'id': 'it', 'name': 'Information Technology', 'icon': 'fa-laptop'},
]
return {'goals': goals}
# ------------------------------------------------------------------
# JSON-RPC: /encoach/onboarding/save (called by OWL wizard)
# ------------------------------------------------------------------
@http.route('/encoach/onboarding/save', type='json', auth='user')
def onboarding_save_rpc(self, **kw):
user = request.env.user
goal = kw.get('goal')
target_level = kw.get('target_level')
study_preference = kw.get('study_preference')
skip_placement = kw.get('skip_placement', False)
learning_style = kw.get('learning_style')
hours_per_week = kw.get('hours_per_week', 0)
placement_decision = 'skip' if skip_placement else 'take_now'
profile_vals = {
'user_id': user.id,
'learning_goal': goal,
'study_mode': study_preference,
'hours_per_week': hours_per_week,
'placement_decision': placement_decision,
}
if learning_style:
if isinstance(learning_style, list):
profile_vals['learning_style'] = json.dumps(learning_style)
elif isinstance(learning_style, str):
profile_vals['learning_style'] = json.dumps([learning_style])
if skip_placement:
profile_vals['cefr_level'] = 'b1'
Profile = request.env['encoach.student.profile'].sudo()
profile = Profile.search([('user_id', '=', user.id)], limit=1)
if profile:
profile.write(profile_vals)
else:
profile = Profile.create(profile_vals)
user.sudo().write({
'account_status': 'activated',
'first_login': False,
})
return {'ok': True, 'profile_id': profile.id, 'placement_decision': placement_decision}
@http.route('/my/dashboard', type='http', auth='user', website=True)
def student_dashboard(self, **kw):
user = request.env.user
# Entity students must complete placement test first
if getattr(user, 'account_source', '') == 'entity_bulk_upload':
completed_session = request.env['encoach.cat.session'].sudo().search([
('student_id', '=', user.id),
('status', '=', 'completed'),
], limit=1)
if not completed_session:
return request.redirect('/my/placement')
profile = request.env['encoach.student.profile'].sudo().search(
[('user_id', '=', user.id)], limit=1,
)
courses = request.env['op.course'].sudo().search([], limit=10)
attempts = request.env['encoach.student.attempt'].sudo().search(
[('student_id', '=', user.id)], limit=5, order='started_at desc',
)
return request.render('encoach_portal.student_dashboard', {
'user': user,
'profile': profile,
'courses': courses,
'attempts': attempts,
})
@http.route('/my/placement', type='http', auth='user', website=True)
def placement_page(self, **kw):
user = request.env.user
session = request.env['encoach.cat.session'].sudo().search(
[('student_id', '=', user.id)], limit=1, order='id desc',
)
return request.render('encoach_portal.placement_briefing', {
'session': session,
'user': user,
})
@http.route('/my/placement/results', type='http', auth='user', website=True)
def placement_results_page(self, **kw):
user = request.env.user
session = request.env['encoach.cat.session'].sudo().search(
[('student_id', '=', user.id), ('status', '=', 'completed')],
limit=1, order='id desc',
)
abilities = request.env['encoach.student.ability.model'].sudo().search(
[('student_id', '=', user.id)],
)
return request.render('encoach_portal.placement_results', {
'session': session,
'abilities': abilities,
})
@http.route('/my/course/<int:course_id>', type='http', auth='user', website=True)
def course_detail(self, course_id, **kw):
course = request.env['op.course'].sudo().browse(course_id)
if not course.exists():
return request.not_found()
modules = request.env['encoach.course.module'].sudo().search(
[('course_id', '=', course_id)], order='sequence',
)
return request.render('encoach_portal.course_detail', {
'course': course,
'modules': modules,
})
@http.route('/my/exam/<int:attempt_id>/results', type='http', auth='user', website=True)
def exam_results(self, attempt_id, **kw):
attempt = request.env['encoach.student.attempt'].sudo().browse(attempt_id)
if not attempt.exists() or attempt.student_id.id != request.env.user.id:
return request.not_found()
if attempt.status not in ('released', 'scored'):
return request.render('encoach_portal.results_pending', {
'attempt': attempt,
})
scores = request.env['encoach.score'].sudo().search(
[('attempt_id', '=', attempt_id)],
)
feedback = request.env['encoach.feedback'].sudo().search(
[('attempt_id', '=', attempt_id)],
)
return request.render('encoach_portal.exam_results', {
'attempt': attempt,
'scores': scores,
'feedback': feedback,
})
@http.route('/my/profile', type='http', auth='user', website=True)
def profile_page(self, **kw):
user = request.env.user
profile = request.env['encoach.student.profile'].sudo().search(
[('user_id', '=', user.id)], limit=1,
)
return request.render('encoach_portal.profile', {
'user': user,
'profile': profile,
})
@http.route('/verify/<string:hash_code>', type='http', auth='public', website=True)
def verification_page(self, hash_code, **kw):
return request.render('encoach_portal.score_verification', {
'hash_code': hash_code,
})