Files
full_encoach_platform/seed_reports.py
Yamen Ahmad c016a52200 feat(reports): replace mock Reports pages with real backend aggregates
Wire the three admin Reports pages (/admin/student-performance,
/admin/stats-corporate, /admin/record) to a new
encoach_lms_api/controllers/reports.py that aggregates from
encoach.student.attempt.

* /api/reports/student-performance: per-student band averages + CEFR,
  with entity / level / search filters.
* /api/reports/stats-corporate: by_module bar, N-month trend line,
  CEFR distribution pie, entity comparison bar, threshold + entity
  filters and meta.attempts_considered for UI.
* /api/reports/record: paginated attempt history with entity / user /
  period filters, EX-### exam codes, duration derived from start/end.
* /api/reports/filters: shared picker returning only entities /
  students that actually have attempts.

Frontend: new reports.service.ts, all three pages rewritten to hit
these endpoints; Recharts graphs now read live data, CSV export added
on Student Performance and Record.

Seeding: seed_reports.py completes any in_progress attempts and
backfills 6 months of historical attempts across 3 entities so the
trend / distribution / KPI panels have meaningful data. Idempotent.

Tests: test_reports_flows.py (25/25 PASS) covers shape, filters,
pagination. Regressions still green: Configuration 24/24, Support
29/29, Training 26/26. Browser-verified on localhost:8080 with admin
login — all 4 tabs in Stats Corporate render, Student Performance
shows real students + KPIs, Record shows 28 attempts with filter +
CSV export working.

Docs: new §20 in PROJECT_SUMMARY.md documenting scope, artifacts,
seeding, test results, and gotchas (encoach.exam.custom uses `title`
not `name`; encoach.entity requires `code`; in_progress attempts are
excluded from aggregates).

Made-with: Cursor
2026-04-19 11:28:26 +04:00

168 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Seed demo data for the admin "Reports" section.
Creates / completes a spread of ``encoach.student.attempt`` records across two
entities so that:
* /admin/student-performance shows multiple rows with different CEFR levels
* /admin/stats-corporate has attempts in multiple months + entities
* /admin/record shows per-user attempt history
Idempotent: re-running only fills in missing attempts — it never wipes data.
Usage:
BACKEND_DIR=$PWD/backend \\
/path/to/python backend/odoo/odoo-bin shell \\
-c backend/custom_addons/encoach_demo.conf \\
--no-http --stop-after-init < seed_reports.py
In practice this repo has a helper ``run.sh seed seed_reports.py``.
"""
# pylint: disable=undefined-variable
import random
from datetime import datetime, timedelta
try:
env # noqa: F821 - injected by odoo-bin shell
except NameError:
raise SystemExit('Run this inside odoo-bin shell (env not available).')
def _or_create_entity(name, code):
rec = env['encoach.entity'].search([('name', '=', name)], limit=1)
if rec:
return rec
rec = env['encoach.entity'].search([('code', '=', code)], limit=1)
if rec:
return rec
return env['encoach.entity'].create({'name': name, 'code': code})
def _students():
Users = env['res.users'].sudo()
logins = ['sarah@encoach.test', 'omar@encoach.test',
'layla@encoach.test', 'testuser@writeflow.test']
out = []
for login in logins:
u = Users.search([('login', '=', login)], limit=1)
if u:
out.append(u)
return out
def _default_exam():
exam = env['encoach.exam.custom'].sudo().search([], order='id', limit=1)
if exam:
return exam
# Fallback: create a minimal exam shell if none exists.
return env['encoach.exam.custom'].sudo().create({
'title': 'Seeded Demo Exam',
'status': 'active',
})
BAND_MATRIX = [
# listening, reading, writing, speaking -> overall auto (avg)
(7.5, 8.0, 7.0, 7.5, 'C1'),
(6.0, 6.5, 5.5, 6.0, 'B2'),
(5.0, 5.5, 4.5, 5.0, 'B1'),
(4.0, 4.5, 4.0, 4.0, 'A2'),
(8.0, 8.5, 8.0, 8.5, 'C1'),
(6.5, 7.0, 6.5, 6.5, 'B2'),
]
def _avg(l, r, w, s):
return round((l + r + w + s) / 4.0, 1)
def seed():
acme = _or_create_entity('Acme Corp', 'ACME')
globe = _or_create_entity('Global Ltd', 'GLOBAL')
tech = _or_create_entity('Tech Co', 'TECHCO')
entities = [acme, globe, tech]
students = _students()
if not students:
print('[seed_reports] No students found — nothing to seed.')
return
exam = _default_exam()
print(f'[seed_reports] Using exam id={exam.id} title={exam.title!r}')
Att = env['encoach.student.attempt'].sudo()
# 1. Fill in bands for any existing in_progress attempts that have no scores.
stale = Att.search([('status', '=', 'in_progress')])
for idx, att in enumerate(stale):
vals = BAND_MATRIX[idx % len(BAND_MATRIX)]
l, r, w, s, cefr = vals
started = att.started_at or (datetime.now() - timedelta(days=5))
att.write({
'status': 'completed',
'listening_band': l,
'reading_band': r,
'writing_band': w,
'speaking_band': s,
'overall_band': _avg(l, r, w, s),
'cefr_level': cefr.lower().replace('-', '_'),
'completed_at': started + timedelta(minutes=120),
'entity_id': att.entity_id.id or random.choice(entities).id,
})
print(f'[seed_reports] Completed {len(stale)} existing in_progress attempts.')
# 2. Add historical attempts across the last 6 months so the trend chart
# has enough data points.
now = datetime.now()
created = 0
for month_ago in range(5, -1, -1):
ref = now - timedelta(days=30 * month_ago + random.randint(0, 15))
for i, student in enumerate(students):
# Skip ~30% of (student, month) pairs so the trend varies.
if random.random() < 0.3 and month_ago > 0:
continue
# Slight progression over time so trend line rises.
base = BAND_MATRIX[(i + month_ago) % len(BAND_MATRIX)]
l, r, w, s, cefr = base
delta = max(0.0, (6 - month_ago) * 0.2)
l = min(9.0, l + delta)
r = min(9.0, r + delta)
w = min(9.0, w + delta)
s = min(9.0, s + delta)
entity = entities[(i + month_ago) % len(entities)]
existing = Att.search([
('student_id', '=', student.id),
('exam_id', '=', exam.id),
('started_at', '>=', ref - timedelta(days=3)),
('started_at', '<=', ref + timedelta(days=3)),
], limit=1)
if existing:
continue
vals = {
'student_id': student.id,
'exam_id': exam.id,
'entity_id': entity.id,
'status': 'completed',
'started_at': ref,
'completed_at': ref + timedelta(minutes=100 + i * 15),
'listening_band': l,
'reading_band': r,
'writing_band': w,
'speaking_band': s,
'overall_band': _avg(l, r, w, s),
'cefr_level': cefr.lower().replace('-', '_'),
}
Att.create(vals)
created += 1
print(f'[seed_reports] Created {created} historical attempts.')
env.cr.commit()
total_reportable = Att.search_count([('status', 'in',
['completed', 'scored',
'released', 'scoring'])])
print(f'[seed_reports] OK. Reportable attempts now: {total_reportable}')
seed()