feat: QA fixes, new APIs (assignments, rubrics, custom exams), Generation page enhancements

- Fix ELAI video generation (correct render endpoint, script splitting for 60s limit)
- Fix speaking script generation error handling and empty response display
- Add custom exam list API (GET /api/exam/custom/list)
- Add assignments REST API (list, create, get)
- Add rubrics REST API (list, create)
- Enhance Generation page: dynamic exam structures, auto-module selection, preview dialog, audio player
- Improve submit feedback with exam ID and status in toast notifications
- Fix ExamsListPage to show both custom exams and exam sessions
- Connect RubricsPage to backend API with fallback data
- Add Dockerfile, docker-compose.yml, requirements.txt for deployment
- Fix placement, grading, scoring, and auth controllers
- Add ErrorBoundary component for frontend resilience
- Add QA report and credentials documentation

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-12 14:26:39 +04:00
parent 6a62a43d61
commit ca91544acd
20 changed files with 682 additions and 75 deletions

View File

@@ -101,6 +101,24 @@ class EncoachSignupController(http.Controller):
_logger.info('Registration OTP for %s: %s', email, otp_code)
try:
template = request.env.ref('encoach_signup.email_template_otp', raise_if_not_found=False)
if template:
template.sudo().with_context(otp_code=otp_code).send_mail(user.id, force_send=True)
else:
mail_values = {
'subject': 'EnCoach - Verify Your Email',
'email_from': request.env['ir.config_parameter'].sudo().get_param(
'mail.catchall.email', 'noreply@encoach.com'),
'email_to': email,
'body_html': f'<p>Welcome to EnCoach!</p>'
f'<p>Your verification code is: <strong>{otp_code}</strong></p>'
f'<p>This code expires in 15 minutes.</p>',
}
request.env['mail.mail'].sudo().create(mail_values).send()
except Exception as mail_err:
_logger.warning('OTP email failed for %s: %s', email, mail_err)
return _json_response({
'message': 'Registration successful. Please verify your email.',
'user_id': user.id,
@@ -205,12 +223,116 @@ class EncoachSignupController(http.Controller):
_logger.info('Resend OTP for %s: %s', email, otp_code)
try:
user = request.env['res.users'].sudo().search(
[('login', '=', email)], limit=1)
if user:
mail_values = {
'subject': 'EnCoach - Your New Verification Code',
'email_from': request.env['ir.config_parameter'].sudo().get_param(
'mail.catchall.email', 'noreply@encoach.com'),
'email_to': email,
'body_html': f'<p>Your new verification code is: <strong>{otp_code}</strong></p>'
f'<p>This code expires in 15 minutes.</p>',
}
request.env['mail.mail'].sudo().create(mail_values).send()
except Exception as mail_err:
_logger.warning('Resend OTP email failed for %s: %s', email, mail_err)
return _json_response({'message': 'OTP resent'})
except Exception as e:
_logger.exception('resend_otp failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# POST /api/reset/sendVerification
# ------------------------------------------------------------------
@http.route('/api/reset/sendVerification', type='http', auth='public',
methods=['POST'], csrf=False)
def reset_send_verification(self, **kw):
try:
body = _get_json_body()
email = (body.get('email') or '').strip().lower()
if not email:
return _error_response('email is required', 400)
user = request.env['res.users'].sudo().search(
[('login', '=', email)], limit=1)
if not user:
return _json_response({'message': 'If the email exists, a reset code has been sent.'})
otp_code = ''.join(random.choices(string.digits, k=6))
otp_hash = hashlib.sha256(otp_code.encode()).hexdigest()
expires_at = fields.Datetime.now() + timedelta(minutes=15)
request.env['encoach.otp'].sudo().create({
'email': email,
'otp_hash': otp_hash,
'expires_at': expires_at,
})
try:
template = request.env.ref('encoach_signup.email_template_password_reset', raise_if_not_found=False)
if template:
template.sudo().with_context(otp_code=otp_code).send_mail(user.id, force_send=True)
else:
mail_values = {
'subject': 'EnCoach Password Reset Code',
'email_from': request.env['ir.config_parameter'].sudo().get_param(
'mail.catchall.email', 'noreply@encoach.com'),
'email_to': email,
'body_html': f'<p>Your password reset code is: <strong>{otp_code}</strong></p>'
f'<p>This code expires in 15 minutes.</p>',
}
request.env['mail.mail'].sudo().create(mail_values).send()
except Exception as mail_err:
_logger.warning('Password reset email failed for %s: %s', email, mail_err)
_logger.info('Password reset OTP for %s: %s', email, otp_code)
return _json_response({'message': 'If the email exists, a reset code has been sent.'})
except Exception as e:
_logger.exception('reset_send_verification failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# POST /api/auth/invite/set-password
# ------------------------------------------------------------------
@http.route('/api/auth/invite/set-password', type='http', auth='public',
methods=['POST'], csrf=False)
def invite_set_password(self, **kw):
try:
body = _get_json_body()
token = body.get('token', '').strip()
password = body.get('password', '')
if not token or not password:
return _error_response('token and password are required', 400)
Invite = request.env['encoach.invite'].sudo()
invite = Invite.search([('token', '=', token), ('used', '=', False)], limit=1)
if not invite:
return _error_response('Invalid or expired invitation token', 400)
user = invite.user_id
if not user:
return _error_response('No user associated with this invitation', 400)
user.sudo().write({'password': password})
invite.write({'used': True})
user.sudo().write({
'account_status': 'activated',
'first_login': False,
})
return _json_response({'message': 'Password set successfully'})
except Exception as e:
_logger.exception('invite_set_password failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/onboarding/goals
# ------------------------------------------------------------------
@@ -342,3 +464,32 @@ class EncoachSignupController(http.Controller):
except Exception as e:
_logger.exception('captcha_config failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# POST /api/subscription/trial
# ------------------------------------------------------------------
@http.route('/api/subscription/trial', type='http', auth='none',
methods=['POST'], csrf=False)
@jwt_required
def start_trial(self, **kw):
try:
user = request.env.user
ICP = request.env['ir.config_parameter'].sudo()
trial_days = int(ICP.get_param('encoach.trial_duration_days', '14'))
from datetime import timedelta
trial_end = fields.Datetime.now() + timedelta(days=trial_days)
user.sudo().write({
'account_status': 'trial',
})
return _json_response({
'message': f'Trial activated for {trial_days} days',
'trial_end': str(trial_end),
'status': 'trial',
})
except Exception as e:
_logger.exception('start_trial failed')
return _error_response(str(e), 500)