feat: complete 42/42 user story coverage with tested demo data

- Registration: add role field (academic_student/self_learner/teacher),
  return user_id, fix auth='public' for Odoo 19, add company_id
- Onboarding: add learning_style as JSON array with validation,
  placement_decision field, JSON-RPC routes for OWL wizard
- Student profile: add placement_decision selection, get/set_learning_styles
  helpers for JSON array storage
- Portal templates: fix course.description, mod.description,
  profile.study_preference, profile.entity_id.entity_type field references

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-10 00:26:37 +04:00
parent 5ff9f12de7
commit a3e12f62fa
4 changed files with 153 additions and 13 deletions

View File

@@ -21,7 +21,7 @@ class EncoachSignupController(http.Controller):
# ------------------------------------------------------------------
# POST /api/auth/register
# ------------------------------------------------------------------
@http.route('/api/auth/register', type='http', auth='none',
@http.route('/api/auth/register', type='http', auth='public',
methods=['POST'], csrf=False)
def register(self, **kw):
try:
@@ -29,11 +29,24 @@ class EncoachSignupController(http.Controller):
email = (body.get('email') or '').strip().lower()
password = body.get('password')
name = body.get('name', '').strip()
role = body.get('role', 'student')
captcha_token = body.get('captcha_token')
if not email or not password or not name:
return _error_response('email, password, and name are required', 400)
valid_roles = ('academic_student', 'self_learner', 'teacher', 'student')
if role not in valid_roles:
return _error_response(f'role must be one of: {", ".join(valid_roles)}', 400)
# Map frontend role names to user_type values
role_to_user_type = {
'academic_student': 'student',
'self_learner': 'student',
'teacher': 'teacher',
'student': 'student',
}
ICP = request.env['ir.config_parameter'].sudo()
secret_key = ICP.get_param('encoach.captcha_secret_key', '')
provider = ICP.get_param('encoach.captcha_provider', 'recaptcha')
@@ -61,11 +74,17 @@ class EncoachSignupController(http.Controller):
if existing:
return _error_response('Email already registered', 409)
user = request.env['res.users'].sudo().create({
company = request.env['res.company'].sudo().search([], limit=1)
user = request.env['res.users'].sudo().with_context(
no_reset_password=True,
).create({
'login': email,
'email': email,
'name': name,
'password': password,
'company_id': company.id,
'company_ids': [(4, company.id)],
'user_type': role_to_user_type.get(role, 'student'),
'account_source': 'self_registered',
'account_status': 'unactivated',
})
@@ -84,7 +103,9 @@ class EncoachSignupController(http.Controller):
return _json_response({
'message': 'Registration successful. Please verify your email.',
'user_id': user.id,
'email': email,
'role': role,
}, 201)
except Exception as e:
@@ -253,6 +274,18 @@ class EncoachSignupController(http.Controller):
return _error_response('learning_goal is required', 400)
skip_placement = body.get('skip_placement', False)
placement_decision = 'skip' if skip_placement else 'take_now'
# learning_style: accept as JSON array ["visual","audio"] or string
learning_style_raw = body.get('learning_style')
if isinstance(learning_style_raw, list):
valid_styles = {'visual', 'audio', 'reading', 'mixed'}
learning_style_raw = [s for s in learning_style_raw if s in valid_styles]
learning_style_json = json.dumps(learning_style_raw) if learning_style_raw else None
elif isinstance(learning_style_raw, str):
learning_style_json = json.dumps([learning_style_raw])
else:
learning_style_json = None
profile_vals = {
'user_id': user.id,
@@ -261,8 +294,12 @@ class EncoachSignupController(http.Controller):
'hours_per_week': body.get('hours_per_week', 0),
'study_mode': body.get('study_mode'),
'exam_date': body.get('exam_date'),
'placement_decision': placement_decision,
}
if learning_style_json:
profile_vals['learning_style'] = learning_style_json
# Assign B1 default level when placement test is skipped
if skip_placement:
profile_vals['cefr_level'] = 'b1'
@@ -282,7 +319,7 @@ class EncoachSignupController(http.Controller):
return _json_response({
'message': 'Onboarding complete',
'profile_id': profile.id,
'skip_placement': skip_placement,
'placement_decision': placement_decision,
})
except Exception as e:

View File

@@ -1,4 +1,6 @@
from odoo import models, fields
import json
from odoo import models, fields, api
class EncoachStudentProfile(models.Model):
@@ -16,7 +18,7 @@ class EncoachStudentProfile(models.Model):
('c2', 'C2'),
])
target_band = fields.Float()
learning_style = fields.Text()
learning_style = fields.Text(help='JSON array of styles: visual, audio, reading, mixed')
learning_goal = fields.Char(size=200)
hours_per_week = fields.Integer()
study_mode = fields.Selection([
@@ -25,4 +27,26 @@ class EncoachStudentProfile(models.Model):
])
exam_date = fields.Date()
placement_completed = fields.Boolean(default=False)
placement_decision = fields.Selection([
('take_now', 'Take Now'),
('skip', 'Skip'),
])
entity_id = fields.Many2one('encoach.entity', ondelete='set null')
def get_learning_styles(self):
"""Return learning_style as a Python list."""
self.ensure_one()
if not self.learning_style:
return []
try:
return json.loads(self.learning_style)
except (json.JSONDecodeError, TypeError):
return [self.learning_style] if self.learning_style else []
def set_learning_styles(self, styles):
"""Accept a list of styles and store as JSON."""
self.ensure_one()
if isinstance(styles, list):
self.learning_style = json.dumps(styles)
elif isinstance(styles, str):
self.learning_style = json.dumps([styles])