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

@@ -369,6 +369,41 @@ class EncoachExamSessionController(http.Controller):
_logger.exception('submit failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/exam/<int:exam_id>/status
# ------------------------------------------------------------------
@http.route('/api/exam/<int:exam_id>/status', type='http', auth='none',
methods=['GET'], csrf=False)
@jwt_required
def get_status(self, exam_id, **kw):
try:
uid = request.env.user.id
Attempt = request.env['encoach.student.attempt'].sudo()
attempt = Attempt.search([
('student_id', '=', uid),
('exam_id', '=', int(exam_id)),
], limit=1, order='id desc')
if not attempt:
return _json_response({
'status': 'not_started',
'scores_available': False,
})
scores_available = attempt.status in ('released', 'scored')
return _json_response({
'status': attempt.status,
'scores_available': scores_available,
'attempt_id': attempt.id,
'completed_at': attempt.completed_at,
'released_at': attempt.released_at,
})
except Exception as e:
_logger.exception('get_status failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/exam/<int:exam_id>/results
# ------------------------------------------------------------------

View File

@@ -24,9 +24,10 @@ def _band_to_cefr(band):
def _recompute_bands(attempt):
"""Recompute skill and overall bands after grading updates."""
"""Recompute skill and overall bands after grading updates, incorporating rubric sub-scores."""
Answer = request.env['encoach.student.answer'].sudo()
Score = request.env['encoach.score'].sudo()
Feedback = request.env['encoach.feedback'].sudo()
skill_totals = {}
skill_max = {}
@@ -36,6 +37,24 @@ def _recompute_bands(attempt):
skill = q.skill or 'general'
skill_totals.setdefault(skill, 0.0)
skill_max.setdefault(skill, 0.0)
fb = Feedback.search([
('attempt_id', '=', attempt.id),
('question_id', '=', q.id),
], limit=1, order='id desc')
if fb and fb.rubric_scores:
try:
rubric_data = json.loads(fb.rubric_scores)
if isinstance(rubric_data, dict) and rubric_data:
rubric_avg = sum(float(v) for v in rubric_data.values()) / len(rubric_data)
rubric_score = (rubric_avg / 9.0) * (q.marks or 1.0)
skill_totals[skill] += rubric_score
skill_max[skill] += q.marks or 1.0
continue
except (json.JSONDecodeError, TypeError, ValueError):
pass
skill_totals[skill] += ans.score or 0.0
skill_max[skill] += q.marks or 1.0