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_portal/controllers/__init__.py
Normal file
1
custom_addons/encoach_portal/controllers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import portal
|
||||
225
custom_addons/encoach_portal/controllers/portal.py
Normal file
225
custom_addons/encoach_portal/controllers/portal.py
Normal 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,
|
||||
})
|
||||
Reference in New Issue
Block a user