Some checks failed
Deploy to Staging / Deploy backend + frontend to staging (push) Failing after 14s
Builds the §24 product on top of the LangGraph runtime from §22:
Phase A (Sources / RAG)
- encoach.course.plan.source model (file | url | text)
- SourceIndexer extracts PDF (pypdf), DOCX (python-docx), HTML, plain
text and embeds chunks via the existing pgvector pipeline scoped to
plan_id, so resources.search only returns the plan's own corpus
- Endpoints: list/create/upload/reindex/delete + plan-scoped retrieval
Phase B (Deliverables)
- services.deliverables.compute_deliverables walks the plan, derives
{planned, generated, ready} per week from material + media state
- GET /api/ai/course-plan/<id>/deliverables drives the new wizard
preview step and the live progress strip on the detail page
Phase C (Multi-modal media)
- encoach.course.plan.media model + MediaService:
audio: AWS Polly (default) or ElevenLabs
image: OpenAI DALL-E 3, capped per plan via system parameter
video: local ffmpeg subprocess (image + audio -> MP4 1280x720)
- Three new agent tools (media.synthesize_audio / generate_image /
compose_video), wired into course_week_materials and a new
course_media_director agent
- Endpoints per material + week-level batch generator
Phase D (Assignments)
- encoach.course.plan.assignment supports mode='batch' (op.batch) or
mode='students' (res.users), with due_date + message + state
- REST endpoints to list / create / delete assignments
Phase E (Student view)
- /api/student/course-plans + /api/student/course-plans/<id>
enforce visibility via assignment.expand_user_ids()
- New /student/course-plans list + read-only drilldown rendering
audio/image/video tiles from /web/content/<attachment_id>
Cross-cutting
- encoach.ai.tool.category: + media (so the new tools register)
- encoach.embedding gains a plan_id filter for plan-scoped RAG
- Wizard adds Sources + Multimedia steps; AdminCoursePlanDetail
rewritten with DeliverablesStrip + SourcesCard + AssignmentsCard +
per-material MediaDrawer
- ~280 new EN + AR i18n keys (full RTL coverage)
- smoke_course_plan.py exercises every phase via odoo-bin shell;
last run: PASS A/B/D/E + DALL-E 3 image (753 KB), Polly audio
fails cleanly when AWS creds aren't configured (expected)
Documentation: §24 added to docs/PROJECT_SUMMARY.md with phase-by-phase
artefact list, endpoints, smoke test, ops notes, and gotchas.
Made-with: Cursor
1871 lines
125 KiB
Markdown
1871 lines
125 KiB
Markdown
# EnCoach Platform — Project Summary
|
||
|
||
> Last updated: 2026-04-25 | **Canonical repos: [`encoach_backend_v4`](https://git.albousalh.com/devops/encoach_backend_v4) (backend) + [`encoach_frontend_v4`](https://git.albousalh.com/devops/encoach_frontend_v4) (frontend), branch `main`.**
|
||
>
|
||
> This workspace (`odoo19/`) is a **developer monorepo / working tree only** — it conveniently contains both halves side-by-side for local development and testing. The two split repos above are the **authoritative origins** for each half: every change must be published to them (via `git subtree split + push`) before the team lead can deploy. See **§6 Git Remotes & Repositories** for the exact workflow.
|
||
|
||
> **Latest events:**
|
||
> - **2026-04-25 (full demo seed + 8-role E2E):** Filled every product `user_type` with believable demo data and ran end-to-end smoke + mutation tests across all eight roles. New idempotent seeders (`seed_full_demo.py`, `reset_demo_passwords.py`) add the 5 missing user types (`approver`, `corporate`, `mastercorporate`, `agent`, `developer`), an active 2-stage exam-approval workflow with one pending request, and a full **GE1-aligned B1 course plan** modelled on the UTAS *General English 1 Fall AY25-26* outline (12 weeks, 6 detailed week-1 materials covering reading / writing / listening / speaking / grammar / vocabulary). New `e2e_full_scenario.py` exercises the API surface for each role (**46/46 PASS, 0 fail** across admin/teacher/approver/student/corporate/mastercorporate/agent/developer) and `e2e_approval_chain.py` walks the full mutation path: approver approves stage 1 → admin approves stage 2 → linked exam auto-published. Live LangGraph round-trips verified during the run (writing_grader 3.3 s, lms_tutor ReAct with 2 real tool calls 13 s). Full QA write-up in `docs/ENCOACH_FULL_DEMO_QA_REPORT.md`. See §23.
|
||
> - **2026-04-25 (LangGraph as core AI runtime):** Made LangGraph the backbone for every AI feature on the platform — course planning, exam/exercise generation, LMS tutor, writing/speaking grading. New `encoach.ai.agent` + `encoach.ai.tool` Odoo models (M2M tool binding, graph type, model, temperature, fallback model, max revisions, quality checks, system prompt, prompt key, response format). New `services/agent_runtime.py` compiles each agent into a `StateGraph` with four topologies (`simple`, `plan_review_revise`, `rag`, `react`) and `services/agent_tools.py` ships an 11-tool registry wrapping existing services (vector search, rubric/outcomes/student fetch, CEFR/AI-detect/content-gate, course-plan persistence, writing/speaking grading). 7 default agents seeded via `data/agents_defaults.xml`. New `/api/ai/agents*` controller (list/get/update/test, list-tools, toggle-tool). The page at `/admin/ai/prompts` is now a tabbed **Agents | Tools | Prompts** console with a config dialog (graph type, model, temperature, fallback, max revisions, quality checks, tool toggles) and a built-in Test Runner that shows output + tool trace + retrieval hits + revisions + quality issues. EN + AR (RTL) translations for every new string. The `CoursePlanPipeline` now routes through `AgentRuntime` when `encoach_ai.use_langgraph_runtime` is on. See §22.
|
||
> - **2026-04-19 (reports section):** Built the Reports section end-to-end — the three pages `/admin/student-performance`, `/admin/stats-corporate`, `/admin/record` (previously pure hardcoded-array mocks) are now wired to real aggregated data from `encoach.student.attempt`. New `/api/reports/{student-performance,stats-corporate,record,filters}` controller (`encoach_lms_api/controllers/reports.py`) does the rollups: per-student band averages + CEFR, per-module corporate charts, trend / distribution / entity comparison, and per-user attempt history with search / level / entity / period filters and CSV export. New `seed_reports.py` completes in-progress attempts and backfills six months of historical attempts so the trend chart and KPI cards are meaningful. 25/25 API smoke passing (`test_reports_flows.py`), 24/24 Configuration + 29/29 Support + 26/26 Training regressions still green, all three pages verified live in-browser with 28 real attempts showing across 4 tabs. See §20.
|
||
> - **2026-04-19 (remote rename):** Aligned local remote names with the new doctrine — `backend-v4 → origin-backend`, `frontend-v4 → origin-frontend`, `origin → mirror-monorepo`. The `v4` branch now tracks `mirror-monorepo/v4`. All publishing commands in §6 updated. No remote URLs changed.
|
||
> - **2026-04-19 (repos-of-record reorganization):** Declared `encoach_backend_v4` and `encoach_frontend_v4` as the canonical origins for their respective halves; the monorepo `encoach_backend_new_v2/v4` is now a secondary working-tree mirror kept only for history/convenience. §6 rewritten around this model.
|
||
> - **2026-04-19 (release to VPS repos):** Pushed the accumulated institutional + support + training work. Frontend canonical → `encoach_frontend_v4/main` (`b78124bb..435930a8`, 403 files, clean fast-forward). Backend canonical → `encoach_backend_v4/main` (`74d83af5..6ec68160`, 420 files, clean fast-forward). Monorepo mirror → `mirror-monorepo/v4` (`98b9837a`). `.gitignore` now excludes `pgdata_bak_*/`, `frontend/.vite/`, `frontend/dist/`, `frontend/node_modules/` so the 674 MB of local pgdata backups never leak into a push. See §6.
|
||
> - **2026-04-18 (training section):** Built the Training section end-to-end — the `Vocabulary` and `Grammar` pages (previously pure hard-coded mocks) are now wired to real Odoo data with CRUD, search, level filtering, and per-user progress tracking. New `encoach.vocab.item` / `encoach.vocab.progress` / `encoach.grammar.rule` / `encoach.grammar.progress` models + `/api/training/vocabulary` and `/api/training/grammar` controllers. 26/26 API write-flow actions passing (`test_training_flows.py`); both pages verified live in-browser. See §19.
|
||
> - **2026-04-18 (support section):** Built the Support section end-to-end — the `Tickets`, `Payment Record`, and `Settings` pages are now wired to real Odoo data (they were previously mocks/404s). New `encoach.ticket` model + full CRUD controller, `/api/payment-records` deriving from `op.student.fees.details` + `account.move`, `/api/codes` + `/api/packages` + `/api/grading-config` powering the Settings tabs. 29/29 API write-flow actions passing (`test_support_flows.py`); all 3 pages verified live in-browser. See §18.
|
||
> - **2026-04-18 (institutional pages write-flow):** API-level write-flow sweep across all 14 institutional admin pages — **44/44 actions passing** (CREATE / EDIT / DELETE / WORKFLOW). Numerous backend gaps fixed: missing DELETE routes added (admission-registers, admissions, result-templates, student-leaves), lesson/library detail endpoints added, custom `encoach.asset` model introduced to replace the missing `op.asset`, marksheet generate endpoint aliased for Odoo 19's `generate_result`, fees `create-invoice` auto-wires the product income account via Odoo 19's JSONB property storage, academic-year DELETE now cascades dependent terms. See §17.
|
||
> - **2026-04-18 (browser E2E):** Full end-to-end test across admin, teacher, student user types. All critical pages verified working in browser. Two backend bugs fixed: (1) `/api/approval-workflows` returned 500 because raw SQL referenced a table that doesn't exist — now gracefully returns empty list; (2) `DELETE /api/courses/<id>` returned opaque 500 when course had enrollments — now returns 409 with a clear message and supports `?force=true` for cascade-delete. See §16.
|
||
|
||
---
|
||
|
||
## 1. Project Overview
|
||
|
||
EnCoach is an AI-powered online learning and examination platform built on **Odoo 19** (backend) and **React + TypeScript + Vite** (frontend). It supports IELTS and general English exam generation, student exam sessions, auto-scoring, adaptive learning, and course creation.
|
||
|
||
---
|
||
|
||
## 2. Tech Stack
|
||
|
||
| Layer | Technology |
|
||
|-------|-----------|
|
||
| **Backend** | Odoo 19, Python 3.12, PostgreSQL 18 |
|
||
| **Frontend** | React 18, TypeScript, Vite 5, Tailwind CSS, Shadcn UI |
|
||
| **State Management** | `@tanstack/react-query` (mutations + queries) |
|
||
| **Auth** | JWT (issued by Odoo, validated via `validate_token` helper) |
|
||
| **AI** | OpenAI API (with fallback templates when API key not configured) |
|
||
| **Database** | `encoach_v2` (PostgreSQL 18.3) |
|
||
|
||
---
|
||
|
||
## 3. User Credentials
|
||
|
||
Every product `user_type` is now represented in the demo data. After running `seed_full_demo.py` + `reset_demo_passwords.py`, the canonical credentials are:
|
||
|
||
| ID | Login | Name | user_type | Password |
|
||
|----|-------|------|-----------|----------|
|
||
| 2 | `admin` | Administrator | superadmin | `admin` |
|
||
| 5 | `admin@encoach.test` | Admin User | admin | **`admin123`** |
|
||
| 6 | `sarah@encoach.test` | Sarah Ahmed | student | **`student123`** |
|
||
| 7 | `omar@encoach.test` | Omar Khan | student | **`student123`** |
|
||
| 8 | `layla@encoach.test` | Layla Nasser | student | **`student123`** |
|
||
| 9 | `khalid@encoach.test` | Dr. Khalid | teacher | **`teacher123`** |
|
||
| 10 | `fatima@encoach.test` | Ms. Fatima | teacher | **`teacher123`** |
|
||
| 13 | `approver@encoach.test` | Approver Coach | teacher (approver) | **`approver123`** |
|
||
| 14 | `corporate@encoach.test` | Acme Corporate | corporate | **`corporate123`** |
|
||
| 15 | `master@encoach.test` | Master Group HQ | mastercorporate | **`master123`** |
|
||
| 16 | `agent@encoach.test` | Sales Agent | agent | **`agent123`** |
|
||
| 17 | `dev@encoach.test` | Platform Dev | developer | **`dev123`** |
|
||
|
||
> **Reset flow:** Re-run `reset_demo_passwords.py` against any database to re-apply these passwords (idempotent, uses Odoo ORM via `odoo-bin shell`). The product supports 7 `user_type` values: `student`, `teacher`, `admin`, `corporate`, `mastercorporate`, `agent`, `developer`. Approver is not a separate `user_type` — it's a `teacher` linked to a stage in `encoach.approval.stage`. See §23 for the full demo dataset and E2E run.
|
||
|
||
### Login API
|
||
|
||
```bash
|
||
# Admin login (note: API field is `login` for the demo accounts)
|
||
curl -X POST http://localhost:8069/api/login \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"login":"admin@encoach.test","password":"admin123"}'
|
||
|
||
# Student login
|
||
curl -X POST http://localhost:8069/api/login \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"login":"sarah@encoach.test","password":"student123"}'
|
||
|
||
# Approver login (drives the §23 approval-chain E2E)
|
||
curl -X POST http://localhost:8069/api/login \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"login":"approver@encoach.test","password":"approver123"}'
|
||
```
|
||
|
||
Returns `{ token, user, permissions }`. The `token` (JWT) is used as `Authorization: Bearer <token>` for all subsequent API calls.
|
||
|
||
---
|
||
|
||
## 4. How to Start the Project
|
||
|
||
### Prerequisites
|
||
- PostgreSQL 18 binary at `/Users/yamenahmad/micromamba/envs/odoo19/bin/`
|
||
- Python 3.12 (conda env at `.conda-envs/odoo19/` or micromamba)
|
||
- Node.js 20 at `/Users/yamenahmad/.local/node/bin/`
|
||
|
||
### Start PostgreSQL
|
||
|
||
```bash
|
||
PGBIN="/Users/yamenahmad/micromamba/envs/odoo19/bin"
|
||
PGDATA="/Users/yamenahmad/projects2026/odoo/odoo19/pgdata"
|
||
"$PGBIN/pg_ctl" -D "$PGDATA" -l "$PGDATA/logfile" start
|
||
```
|
||
|
||
> **Important:** The pgdata was initialized with PG 18. The conda env has PG 17.6, which is **incompatible**. Always use the micromamba PG 18 binary above.
|
||
>
|
||
> If PG fails with config errors, check `pgdata/logfile`. The parameter `autovacuum_worker_slots` (PG 18-only) was commented out in `pgdata/postgresql.conf` line 687 for compatibility.
|
||
|
||
### Start Odoo Backend
|
||
|
||
```bash
|
||
cd /Users/yamenahmad/projects2026/odoo/odoo19
|
||
./run.sh
|
||
```
|
||
|
||
Runs at **http://127.0.0.1:8069**
|
||
|
||
> **Odoo config** (`odoo.conf`) must have `db_name = encoach_v2` and `dbfilter = ^encoach_v2$`, otherwise API routes return 404 ("No database selected").
|
||
|
||
### Start Frontend
|
||
|
||
```bash
|
||
export PATH="/Users/yamenahmad/.local/node/bin:$PATH"
|
||
cd /Users/yamenahmad/projects2026/odoo/odoo19/frontend
|
||
npm run dev
|
||
```
|
||
|
||
Runs at **http://localhost:8080** — Vite proxies `/api` requests to Odoo `:8069`.
|
||
|
||
### Stop PostgreSQL
|
||
|
||
```bash
|
||
"$PGBIN/pg_ctl" -D "$PGDATA" stop
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Project Structure
|
||
|
||
```
|
||
odoo19/
|
||
├── backend/
|
||
│ ├── custom_addons/ # All EnCoach Odoo modules
|
||
│ │ ├── encoach_api/ # Auth (login/logout/user) + base helpers
|
||
│ │ ├── encoach_ai/ # AI generation, exam generation/submit
|
||
│ │ ├── encoach_ai_course/ # AI course creation pipelines
|
||
│ │ ├── encoach_adaptive/ # Adaptive learning engine
|
||
│ │ ├── encoach_core/ # Core models (user, entity, role, permission)
|
||
│ │ ├── encoach_exam_template/ # Exam CRUD, sessions, rubrics, assignments, schedules
|
||
│ │ ├── encoach_scoring/ # Grading queue, score release
|
||
│ │ ├── encoach_taxonomy/ # Subject/topic tree
|
||
│ │ ├── encoach_lms_api/ # LMS: courses, chapters, materials, progress, enrollment
|
||
│ │ ├── encoach_course_gen/ # Course generation
|
||
│ │ ├── encoach_branding/ # Entity branding
|
||
│ │ ├── encoach_signup/ # Registration, email verification, onboarding
|
||
│ │ ├── encoach_placement/ # Placement test
|
||
│ │ └── ...
|
||
│ └── openeducat_erp-19.0/ # OpenEduCat base modules
|
||
├── frontend/
|
||
│ ├── src/
|
||
│ │ ├── pages/ # Route pages (admin/, student/, etc.)
|
||
│ │ ├── components/ # Reusable UI components
|
||
│ │ ├── services/ # API service classes
|
||
│ │ ├── hooks/ # React Query hooks
|
||
│ │ ├── lib/ # api-client, utils
|
||
│ │ └── types/ # TypeScript type definitions
|
||
│ ├── .env.development # VITE_API_BASE_URL=/api
|
||
│ └── .env.production # VITE_API_BASE_URL=https://api.encoach.com/api
|
||
├── docs/ # Documentation
|
||
├── odoo/ # Odoo 19 source code
|
||
├── odoo.conf # Odoo configuration
|
||
├── pgdata/ # PostgreSQL data directory (PG 18)
|
||
├── run.sh # Start Odoo
|
||
├── start.sh # Start PG + Odoo
|
||
└── stop.sh # Stop services
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Git Remotes & Repositories
|
||
|
||
> **Canonical (repos-of-record):**
|
||
> - Backend → **[`encoach_backend_v4`](https://git.albousalh.com/devops/encoach_backend_v4.git)** — branch `main`
|
||
> - Frontend → **[`encoach_frontend_v4`](https://git.albousalh.com/devops/encoach_frontend_v4.git)** — branch `main`
|
||
>
|
||
> These are the repositories the team lead (Talal / devops) clones on the VPS and
|
||
> builds with `docker compose up -d --build`. **All production / staging code
|
||
> lives here.**
|
||
>
|
||
> This workspace (`odoo19/`) is a **developer monorepo / working tree** — a
|
||
> convenience folder that holds both halves side-by-side so a single developer
|
||
> can run the full stack locally. It is **not** the source of truth: every change
|
||
> must be published to the two canonical repos above via `git subtree split + push`
|
||
> before it counts as merged.
|
||
|
||
### 6.1 Repository Layout
|
||
|
||
```
|
||
odoo19/ ← developer working tree (NOT source of truth)
|
||
├── backend/ → published to encoach_backend_v4/main (authoritative)
|
||
│ ├── custom_addons/
|
||
│ ├── Dockerfile
|
||
│ ├── docker-compose.yml
|
||
│ └── requirements.txt
|
||
├── frontend/ → published to encoach_frontend_v4/main (authoritative)
|
||
│ ├── src/
|
||
│ ├── public/
|
||
│ ├── Dockerfile, vite.config.ts, package.json, …
|
||
└── docs/ → lives only in this workspace
|
||
```
|
||
|
||
### 6.2 Remotes Configured on This Clone
|
||
|
||
Listed in order of importance. The two bold rows are the **repos of record**;
|
||
everything else is a secondary mirror or legacy.
|
||
|
||
| Remote | URL | Role |
|
||
|--------|-----|------|
|
||
| **`origin-backend`** | `https://git.albousalh.com/devops/encoach_backend_v4.git` | **Canonical backend** — branch `main`. VPS deploy target. |
|
||
| **`origin-frontend`** | `https://git.albousalh.com/devops/encoach_frontend_v4.git` | **Canonical frontend** — branch `main`. VPS deploy target. |
|
||
| `mirror-monorepo` | `https://git.albousalh.com/devops/encoach_backend_new_v2.git` | Secondary full-tree mirror (branch `v4` tracked here). Useful for history/backup and for docs that live only in the workspace root (e.g. this file). **Not** the source of truth. |
|
||
| `ip-origin` | `https://5.189.151.117/devops/encoach_backend_new_v2.git` | IP fallback for `mirror-monorepo` when DNS is flaky. |
|
||
| `backend-v3` | `https://git.albousalh.com/devops/encoach_backend_new_v3.git` | Legacy v3 monorepo mirror — rarely used. |
|
||
| `frontend-v3` | `https://git.albousalh.com/devops/encoach_frontend_new_v3.git` | Legacy v3 frontend mirror — rarely used. |
|
||
| `frontend` | `https://git.albousalh.com/devops/encoach_frontend_new_v2.git` | Legacy v2 frontend monorepo — rarely used. |
|
||
|
||
> Rename history: `backend-v4 → origin-backend`, `frontend-v4 → origin-frontend`,
|
||
> `origin → mirror-monorepo` (applied 2026-04-19). If you have old scripts or
|
||
> notes referencing the old names, update them or add aliases via
|
||
> `git remote rename`.
|
||
|
||
### 6.3 Feature Development Workflow
|
||
|
||
Everything is edited inside this workspace, but a change is only **merged**
|
||
once it lands on `main` of the two canonical repos.
|
||
|
||
```bash
|
||
# 1. Start from a clean workspace synced with the monorepo mirror
|
||
git checkout v4
|
||
git pull mirror-monorepo v4 # optional: refresh full-tree mirror
|
||
|
||
# 2. Create a feature branch
|
||
git checkout -b feature/<short-name>
|
||
|
||
# 3. Make changes, commit small and often
|
||
git add <files>
|
||
git commit -m "feat(<area>): <short description>"
|
||
|
||
# 4. Merge back onto v4 (or open a PR if branch protection is enabled)
|
||
git checkout v4
|
||
git merge --no-ff feature/<short-name>
|
||
|
||
# 5. Publish to the CANONICAL repos (mandatory — see §6.4)
|
||
# This is what the team lead / CI will see on the VPS.
|
||
|
||
# 6. (Optional) push the monorepo mirror for a full-tree backup / docs
|
||
git push mirror-monorepo v4
|
||
```
|
||
|
||
### 6.4 Publishing to the Canonical Repos (MANDATORY)
|
||
|
||
The canonical repos expect a **flat tree** (no `backend/` or `frontend/`
|
||
prefix). Use `git subtree split` to carve each subdirectory into its own
|
||
parallel history, then push to `main`. These commands produce clean
|
||
fast-forwards when nothing has been pushed directly to the canonical repos
|
||
from elsewhere — which is the invariant we enforce.
|
||
|
||
```bash
|
||
# ── Publish BACKEND → encoach_backend_v4 ─────────────────────────────────
|
||
git subtree split --prefix=backend HEAD -b split-backend-v4
|
||
git push origin-backend split-backend-v4:main
|
||
git branch -D split-backend-v4 # cleanup the temp branch
|
||
|
||
# ── Publish FRONTEND → encoach_frontend_v4 ───────────────────────────────
|
||
git subtree split --prefix=frontend HEAD -b split-frontend-v4
|
||
git push origin-frontend split-frontend-v4:main
|
||
git branch -D split-frontend-v4
|
||
```
|
||
|
||
If a push is rejected as non-fast-forward, fetch and inspect before forcing:
|
||
|
||
```bash
|
||
git fetch origin-backend origin-frontend
|
||
git log --oneline origin-backend/main..split-backend-v4 # what we're adding
|
||
git log --oneline split-backend-v4..origin-backend/main # divergence, if any
|
||
```
|
||
|
||
Only use `--force` after confirming the remote `main` has no commits worth
|
||
keeping (e.g. someone pushed directly and it must be overwritten). Since the
|
||
canonical repos are the source of truth, a force-push there is a **destructive
|
||
production operation** — pair it with a Slack heads-up to the team lead.
|
||
|
||
### 6.5 What Each Canonical Repo Contains
|
||
|
||
**`encoach_backend_v4` (branch `main`)** — everything under `backend/` hoisted
|
||
to repo root:
|
||
|
||
- `custom_addons/` (all EnCoach Odoo addons + `encoach_lms_api`)
|
||
- `Dockerfile`, `docker-compose.yml`, `requirements.txt`
|
||
|
||
**Not shipped:** `openeducat_erp-19.0/` is vendored locally but not tracked in
|
||
git. The VPS needs OpenEduCat installed separately — either baked into the
|
||
container image, mounted at `/opt/odoo/extra_addons`, or installed via pip if a
|
||
package release is used. `backend/odoo.conf` is also not currently tracked; add
|
||
it in a follow-up commit if the VPS container needs a baked-in config.
|
||
|
||
**`encoach_frontend_v4` (branch `main`)** — everything under `frontend/` hoisted
|
||
to repo root:
|
||
|
||
- `src/`, `public/`, `docs/` (frontend docs only)
|
||
- `package.json`, `package-lock.json`, `bun.lock`, `bun.lockb`
|
||
- `Dockerfile`, `docker-compose.yml`
|
||
- `vite.config.ts`, `tsconfig.*.json`, `tailwind.config.ts`, `eslint.config.js`
|
||
- `index.html`, `components.json`, `.env.example`
|
||
|
||
**Not shipped** (ignored by `frontend/.gitignore`): `node_modules/`, `dist/`,
|
||
`.vite/`, `.env`, `.env.development`, `.env.production`.
|
||
|
||
### 6.6 VPS Deploy — Team-Lead Quickstart
|
||
|
||
```bash
|
||
# Backend
|
||
git clone https://git.albousalh.com/devops/encoach_backend_v4.git
|
||
cd encoach_backend_v4 && docker compose up -d --build
|
||
|
||
# Frontend
|
||
git clone https://git.albousalh.com/devops/encoach_frontend_v4.git
|
||
cd encoach_frontend_v4 && cp .env.example .env # set VITE_API_BASE_URL
|
||
docker compose up -d --build # or: npm ci && npm run build
|
||
```
|
||
|
||
### 6.7 Safety Rules (DO NOT COMMIT)
|
||
|
||
Already excluded via `.gitignore` — do not force them in:
|
||
|
||
- `pgdata/`, `pgdata_bak_*/` — PostgreSQL data directories (up to ~700 MB)
|
||
- `frontend/node_modules/`, `frontend/dist/`, `frontend/.vite/` — build artifacts
|
||
- `.env`, `.env.development`, `.env.production` — local secrets
|
||
- `.venv/`, `venv/`, `.conda-envs/` — Python virtualenvs
|
||
- `*.zip`, `*.tar.gz` — archives
|
||
|
||
`odoo.conf` at the workspace root IS tracked (local Mac dev config, harmless).
|
||
`backend/odoo.conf` (container-friendly) is **not** tracked; add it in a
|
||
follow-up if the VPS image needs it baked in.
|
||
|
||
### 6.8 Git Credentials
|
||
|
||
Stored in macOS Keychain for `git.albousalh.com`:
|
||
- Username: `yamen`
|
||
- Auth: token-based (stored in keychain, not in this file)
|
||
|
||
---
|
||
|
||
## 7. API Routes Reference
|
||
|
||
### Authentication (`encoach_api`)
|
||
| Method | Route | Auth | Description |
|
||
|--------|-------|------|-------------|
|
||
| POST | `/api/login` | public | Login, returns JWT |
|
||
| GET | `/api/user` | JWT | Get current user |
|
||
| POST | `/api/logout` | public | Logout (client clears token) |
|
||
|
||
### Exam Generation & AI (`encoach_ai`)
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| POST | `/api/exam/<module>/generate` | AI-generate content for a module (reading/listening/writing/speaking) |
|
||
| POST | `/api/exam/generation/submit` | Submit generated exam, create questions |
|
||
| POST | `/api/exam/<module>/generate/save` | Save generated content |
|
||
|
||
### Exam Session (`encoach_exam_template`)
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET | `/api/exam/<exam_id>/session` | Get exam session (creates attempt if needed) |
|
||
| POST | `/api/exam/<exam_id>/autosave` | Autosave student answers |
|
||
| POST | `/api/exam/<exam_id>/submit` | Submit exam for scoring |
|
||
| GET | `/api/exam/<exam_id>/status` | Get attempt status |
|
||
| GET | `/api/exam/<exam_id>/results` | Get exam results |
|
||
|
||
### Custom Exams
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET | `/api/exam/custom/list` | List custom exams |
|
||
| POST | `/api/exam/custom/create` | Create custom exam |
|
||
| GET/PUT/DELETE | `/api/exam/custom/<id>` | CRUD on custom exam |
|
||
|
||
### Exam Structures, Templates, Schedules, Assignments
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET/POST | `/api/exam-structures` | List/create exam structures |
|
||
| GET/POST | `/api/exam/templates` | List/create exam templates |
|
||
| GET/POST | `/api/exam-schedules` | List/create schedules |
|
||
| GET | `/api/student/my-exams` | Student's assigned exams |
|
||
| GET/POST | `/api/assignments` | List/create assignments |
|
||
|
||
### Rubrics & Approval Workflows
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET/POST | `/api/rubrics` | List/create rubrics |
|
||
| GET/POST | `/api/rubric-groups` | List/create rubric groups |
|
||
| GET/POST | `/api/approval-workflows` | List/create approval workflows |
|
||
| POST | `/api/approval-requests/<id>/approve` | Approve request |
|
||
| POST | `/api/approval-requests/<id>/reject` | Reject request |
|
||
|
||
### Entities
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET | `/api/entities` | List entities |
|
||
|
||
### LMS Core (`encoach_lms_api` — `lms_core.py`)
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET/POST | `/api/courses` | List all courses (paginated) / create course |
|
||
| GET/PUT/DELETE | `/api/courses/<id>` | Get / update / delete a single course |
|
||
| GET/POST | `/api/students` | List students / create student |
|
||
| GET/PUT/DELETE | `/api/students/<id>` | Get / update / delete a student |
|
||
| GET | `/api/student/my-courses` | Enrolled courses for logged-in student (with progress) |
|
||
| POST | `/api/students/<id>/enroll` | Enroll a student in one or more courses |
|
||
| POST | `/api/courses/<id>/enroll` | Bulk-enroll students in a course |
|
||
| GET/POST | `/api/teachers` | List / create teachers |
|
||
| GET/PUT/DELETE | `/api/teachers/<id>` | Get / update / delete a teacher |
|
||
| GET/POST | `/api/batches` | List / create batches |
|
||
| GET/PUT/DELETE | `/api/batches/<id>` | Get / update / delete a batch |
|
||
|
||
### LMS Courseware (`encoach_lms_api` — `courseware.py`)
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET/POST | `/api/courses/<id>/chapters` | List / create chapters for a course |
|
||
| GET/PUT/DELETE | `/api/chapters/<id>` | Get / update / delete a chapter |
|
||
| POST | `/api/chapters/<id>/materials` | Upload material (JSON or FormData) |
|
||
| GET/PUT/DELETE | `/api/materials/<id>` | Get / update / delete a material |
|
||
| GET | `/api/materials/<id>/download` | Download material file |
|
||
| GET | `/api/materials/<id>/content` | Serve material file inline (for iframe embed, accepts ?token= param) |
|
||
| POST | `/api/materials/<id>/view` | Mark material as viewed |
|
||
| POST | `/api/chapters/<id>/progress/complete` | Mark chapter as completed |
|
||
| GET | `/api/courses/<id>/completion` | Get course completion status (% progress, post-test availability) |
|
||
| GET | `/api/materials/search` | Search all materials across courses (for Admin Resources page) |
|
||
|
||
### Institutional (`encoach_lms_api`)
|
||
|
||
These power the 14 institutional admin pages under `/admin/*` (see §14) and back the write-flow test in §17. Every route is JWT-protected.
|
||
|
||
**Academic & Departments**
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET / POST | `/api/academic-years` | List / create academic years |
|
||
| GET / PATCH / DELETE | `/api/academic-years/<yid>` | Get / update / delete year (DELETE cascades terms) |
|
||
| POST | `/api/academic-years/<yid>/generate-terms` | Auto-create terms spanning the year (`two_sem`, `three_trim`, `four_qtr`) |
|
||
| GET / POST | `/api/academic-terms` | List / create academic terms |
|
||
| GET / PATCH / DELETE | `/api/academic-terms/<tid>` | Term CRUD |
|
||
| GET / POST | `/api/departments` | List / create departments |
|
||
| GET / PATCH / DELETE | `/api/departments/<did>` | Department CRUD |
|
||
|
||
**Admissions**
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET / POST | `/api/admission-registers` | List / create admission registers |
|
||
| GET / PATCH / DELETE | `/api/admission-registers/<rid>` | Register CRUD (DELETE supported) |
|
||
| POST | `/api/admission-registers/<rid>/confirm` | Confirm register |
|
||
| GET / POST | `/api/admissions` | List / create admissions (application_date **must** fall inside register window) |
|
||
| GET / PATCH / DELETE | `/api/admissions/<aid>` | Admission CRUD (DELETE supported) |
|
||
|
||
**Institutional Exams & Marksheets**
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET / POST | `/api/inst-exam-sessions` | List / create exam sessions |
|
||
| GET / PATCH / DELETE | `/api/inst-exam-sessions/<sid>` | Session CRUD |
|
||
| POST | `/api/inst-exam-sessions/<sid>/schedule` | Schedule session |
|
||
| GET / POST | `/api/result-templates` | List / create result templates |
|
||
| GET / PATCH / DELETE | `/api/result-templates/<tid>` | Template CRUD (DELETE cascades `op.marksheet.register`) |
|
||
| POST | `/api/result-templates/<tid>/generate` (alias: `/generate-marksheets`) | Generate marksheet registers (tries `generate_result` then `action_generate_marksheet`) |
|
||
| GET | `/api/marksheets` | List marksheet registers |
|
||
| POST | `/api/marksheets/<mid>/validate` | Validate marksheet |
|
||
|
||
**Facilities, Assets, Activities**
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET / POST | `/api/facilities` | List / create facilities |
|
||
| GET / PATCH / DELETE | `/api/facilities/<fid>` | Facility CRUD |
|
||
| GET / POST | `/api/assets` | List / create assets (backed by custom `encoach.asset` model) |
|
||
| GET / PATCH / DELETE | `/api/assets/<aid>` | Asset CRUD |
|
||
| GET / POST / PATCH / DELETE | `/api/activity-types` + `/<id>` | Activity-type CRUD |
|
||
| GET / POST / PATCH / DELETE | `/api/activities` + `/<id>` | Activity CRUD |
|
||
|
||
**Library, Lessons, Gradebook, Leaves, Fees**
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET / POST | `/api/library/media` | List / create media items (authors split into `author_ids`) |
|
||
| GET / PATCH / DELETE | `/api/library/media/<mid>` | Media CRUD — detail endpoint returns `author_names` + `author_ids` |
|
||
| GET / POST | `/api/lessons` | List / create lessons |
|
||
| GET / PATCH / DELETE | `/api/lessons/<lid>` | Lesson CRUD — detail endpoint preserves `course_id` / `batch_id` on edits |
|
||
| GET / POST / DELETE | `/api/grading-assignments` + `/<id>` | Grading assignment CRUD (auto-resolves `assignment_type` and default `faculty_id`) |
|
||
| GET | `/api/gradebooks` + `/api/gradebook-lines?gradebook_id=<id>` | Gradebook drill-down |
|
||
| GET | `/api/student-progress` + `/<id>` | Student progress list/detail |
|
||
| GET / POST | `/api/student-leaves` | List / create leave requests |
|
||
| POST | `/api/student-leaves/<lid>/approve` | Approve leave |
|
||
| DELETE | `/api/student-leaves/<lid>` | Delete leave |
|
||
| GET / POST | `/api/fees-plans` | List / create fee plans |
|
||
| POST | `/api/fees-plans/<fid>/create-invoice` | Generate accounting invoice (auto-wires income account on the product template for Odoo 19 JSONB property storage if missing) |
|
||
| POST | `/api/fees-plans/<fid>/register-payment` | Register a payment against the invoice |
|
||
|
||
### Other
|
||
| Route | Description |
|
||
|-------|-------------|
|
||
| `/api/taxonomy/*` | Subject/topic taxonomy tree |
|
||
| `/api/adaptive/*` | Adaptive learning dashboard/signals |
|
||
| `/api/placement/*` | Placement test start/answer/results |
|
||
| `/api/grading/*` | Grading queue, AI suggest |
|
||
| `/api/scores/*` | Score release/reject |
|
||
| `/api/course/*` | Course CRUD, gap analysis, auto-generate |
|
||
| `/api/ai-course/*` | AI course creation pipelines |
|
||
| `/api/auth/register` | User registration |
|
||
|
||
---
|
||
|
||
## 8. Key Odoo Models
|
||
|
||
| Model | Table | Description |
|
||
|-------|-------|-------------|
|
||
| `encoach.exam.custom` | `encoach_exam_custom` | Custom exams (16 records) |
|
||
| `encoach.exam.custom.section` | `encoach_exam_custom_section` | Exam sections (reading/listening/writing/speaking) |
|
||
| `encoach.question` | `encoach_question` | Questions (38 records) |
|
||
| `encoach.student.attempt` | `encoach_student_attempt` | Student exam attempts (10 records) |
|
||
| `encoach.student.answer` | `encoach_student_answer` | Individual answers |
|
||
| `encoach.entity` | `encoach_entity` | Organizations (1: "EnCoach Demo Academy") |
|
||
| `encoach.rubric` | `encoach_rubric` | Grading rubrics |
|
||
| `encoach.exam.template` | `encoach_exam_template` | Exam templates |
|
||
| `encoach.exam.structure` | `encoach_exam_structure` | Exam structures |
|
||
| `encoach.exam.schedule` | `encoach_exam_schedule` | Exam schedules |
|
||
| `encoach.exam.assignment` | `encoach_exam_assignment` | Exam assignments |
|
||
| `encoach.course.chapter` | `encoach_course_chapter` | Course chapters (ordered by sequence) |
|
||
| `encoach.chapter.material` | `encoach_chapter_material` | Materials within chapters (pdf, video, article, link) |
|
||
| `encoach.chapter.progress` | `encoach_chapter_progress` | Per-student chapter progress tracking |
|
||
| `encoach.course.completion` | `encoach_course_completion` | Per-student course completion (% progress, post-test flag) |
|
||
| `encoach.resource` | `encoach_resource` | Learning resources linked to taxonomy (subject, domain, topics, objectives) |
|
||
| `encoach.resource.tag` | `encoach_resource_tag` | Tags for categorizing resources |
|
||
| `encoach.subject` | `encoach_subject` | Taxonomy subjects (e.g. English, Mathematics) |
|
||
| `encoach.domain` | `encoach_domain` | Taxonomy domains under subjects (e.g. Grammar, Listening) |
|
||
| `encoach.topic` | `encoach_topic` | Taxonomy topics under domains |
|
||
| `encoach.learning.objective` | `encoach_learning_objective` | Learning objectives under topics (Bloom levels) |
|
||
| `op.course` (extended) | `op_course` | OpenEduCat course + `description`, `max_capacity`, `encoach_subject_id`, `topic_ids`, `learning_objective_ids` |
|
||
| `op.student` | `op_student` | OpenEduCat student model |
|
||
| `op.faculty` | `op_faculty` | OpenEduCat teacher/faculty model |
|
||
| `op.batch` | `op_batch` | OpenEduCat batch model |
|
||
|
||
### Institutional models (OpenEduCat + EnCoach)
|
||
|
||
| Model | Table | Description |
|
||
|-------|-------|-------------|
|
||
| `op.academic.year` | `op_academic_year` | Academic year (name unique, `term_structure`) |
|
||
| `op.academic.term` | `op_academic_term` | Term under a year; FK to year is `RESTRICT` — delete-year cascades terms first |
|
||
| `op.department` | `op_department` | Department |
|
||
| `op.admission.register` | `op_admission_register` | Admission register window (`start_date` / `end_date`) |
|
||
| `op.admission` | `op_admission` | Admission — constrained so `application_date ∈ [register.start_date, register.end_date]` |
|
||
| `op.exam.session` | `op_exam_session` | Institutional exam session |
|
||
| `op.result.template` | `op_result_template` | Marksheet result template — `generate_result()` in Odoo 19 (older: `action_generate_marksheet`) produces `op.marksheet.register` rows |
|
||
| `op.marksheet.register` | `op_marksheet_register` | Generated marksheet register (lines + validate workflow) |
|
||
| `op.facility` | `op_facility` | Physical facility (rooms, labs, etc.) |
|
||
| `encoach.asset` | `encoach_asset` | **New lightweight asset model** (replaces missing `op.asset`): `name`, `code`, `facility_id`, `description`, `active` |
|
||
| `op.activity.type` / `op.activity` | `op_activity_type` / `op_activity` | Activity categorization + activities |
|
||
| `op.media` / `op.author` | `op_media` / `op_author` | Library media + authors (M2M) |
|
||
| `encoach.lesson` | `encoach_lesson` | Lesson tied to course + batch |
|
||
| `grading.assignment` / `grading.assignment.type` | `grading_assignment*` | Grading assignments — `assignment_type` and `faculty_id` are required |
|
||
| `encoach.student.leave` | `encoach_student_leave` | Student leave requests with approve workflow |
|
||
| `op.fees.terms` + `.line` | `op_fees_terms*` | Fees terms; `line_ids` must sum to 100% |
|
||
| `op.student.fees.details` | `op_student_fees_details` | Per-student fees; `get_invoice()` requires an income account on product template (stored as JSONB in Odoo 19) |
|
||
|
||
---
|
||
|
||
## 9. Installed EnCoach Modules
|
||
|
||
| Module | Description |
|
||
|--------|-------------|
|
||
| `encoach_adaptive` | Adaptive learning engine |
|
||
| `encoach_ai` | AI generation, search, insights |
|
||
| `encoach_ai_course` | AI course creation |
|
||
| `encoach_api` | REST API auth + base helpers |
|
||
| `encoach_branding` | Entity branding |
|
||
| `encoach_core` | Core models (user, entity, role) |
|
||
| `encoach_course_gen` | Course generation |
|
||
| `encoach_exam_template` | Exam CRUD, sessions, rubrics, assignments |
|
||
| `encoach_lms_api` | LMS: courses, students, teachers, batches, chapters, materials, enrollment, progress |
|
||
| `encoach_quality_gate` | Quality gate checks |
|
||
| `encoach_resources` | Learning resources |
|
||
| `encoach_scoring` | Grading and score release |
|
||
| `encoach_taxonomy` | Subject/topic taxonomy |
|
||
| `encoach_vector` | Vector embeddings |
|
||
|
||
---
|
||
|
||
## 10. Exam Lifecycle Flow
|
||
|
||
```
|
||
1. Admin → /admin/generation page
|
||
├── Select modules (reading, listening, writing, speaking)
|
||
├── Set parameters (difficulty, timer, grading, rubric, entity, etc.)
|
||
└── Click "Generate with AI" → POST /api/exam/<module>/generate
|
||
|
||
2. AI generates content (or fallback templates if OpenAI not configured)
|
||
|
||
3. Admin reviews generated content → Submit
|
||
├── "Submit & skip approval" → POST /api/exam/generation/submit (skip_approval=true)
|
||
├── "Submit for approval" → POST /api/exam/generation/submit (skip_approval=false)
|
||
└── Creates: exam_custom + sections + questions in DB
|
||
|
||
4. Admin assigns exam → /api/exam/custom/<id>/assign or /api/exam-schedules
|
||
|
||
5. Student → /student/my-exams → clicks exam → /student/exam/<id>
|
||
├── GET /api/exam/<id>/session → loads sections, questions, passage_text
|
||
├── POST /api/exam/<id>/autosave → saves answers periodically
|
||
└── POST /api/exam/<id>/submit → auto-scores objective questions
|
||
|
||
6. Student → /student/exam/<id>/results → views score breakdown
|
||
```
|
||
|
||
---
|
||
|
||
## 11. Student Learning Lifecycle Flow
|
||
|
||
```
|
||
1. Admin/Teacher → /teacher/courses → "New Course"
|
||
├── Create course (title, code, description, max_capacity)
|
||
└── Course stored in op.course (extended with description, max_capacity)
|
||
|
||
2. Teacher → /teacher/courses/:courseId/chapters → "Add Chapter"
|
||
├── Create chapters with sequence ordering
|
||
└── Upload materials (pdf, video, article, link) per chapter
|
||
└── POST /api/chapters/<id>/materials (JSON or FormData)
|
||
|
||
3. Admin → /admin/courses → "Enroll Students"
|
||
├── Bulk-enroll students via POST /api/courses/<id>/enroll
|
||
└── Or enroll individually via POST /api/students/<id>/enroll
|
||
|
||
4. Student → /student/dashboard → sees enrolled courses with progress
|
||
├── GET /api/student/my-courses → returns courses + chapter_count + progress %
|
||
└── Cards show "Start Learning" or "Continue Learning"
|
||
|
||
5. Student → /student/courses/:id → course detail with chapter list
|
||
├── Shows chapter list with material counts and lock status
|
||
└── Student clicks a chapter → /student/courses/:courseId/chapters/:chapterId
|
||
|
||
6. Student → chapter view → views materials
|
||
├── POST /api/materials/<id>/view → marks material as viewed
|
||
├── Progress tracked in encoach.chapter.progress
|
||
└── When all materials viewed → chapter auto-completes
|
||
|
||
7. Student → clicks "Mark Chapter Complete"
|
||
├── POST /api/chapters/<id>/progress/complete
|
||
└── Triggers _check_course_completion()
|
||
|
||
8. Course completion check
|
||
├── When all chapters completed → encoach.course.completion updated
|
||
├── progress = 100%, completed_at = now
|
||
└── post_test_available = True → student can take final exam
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Taxonomy ↔ Resource ↔ Course Relationships
|
||
|
||
```
|
||
encoach.subject (e.g. English)
|
||
└── encoach.domain (e.g. Grammar, Listening, Reading)
|
||
└── encoach.topic (e.g. Verb Tenses, Relative Clauses)
|
||
└── encoach.learning.objective (Bloom levels: Remember, Understand, Apply...)
|
||
|
||
encoach.resource ──M2O──→ encoach.subject (subject_id)
|
||
──M2O──→ encoach.domain (domain_id)
|
||
──M2M──→ encoach.topic (topic_ids)
|
||
──M2M──→ encoach.learning.objective (learning_objective_ids)
|
||
|
||
op.course (extended) ──M2O──→ encoach.subject (encoach_subject_id)
|
||
──M2M──→ encoach.topic (topic_ids)
|
||
──M2M──→ encoach.learning.objective (learning_objective_ids)
|
||
|
||
encoach.course.chapter ──M2O──→ encoach.subject (encoach_subject_id)
|
||
──M2M──→ encoach.topic (topic_ids)
|
||
──M2M──→ encoach.learning.objective (learning_objective_ids)
|
||
```
|
||
|
||
### Frontend: `TaxonomyCascade` Component
|
||
|
||
Reusable cascading picker (`frontend/src/components/TaxonomyCascade.tsx`) used in:
|
||
- Admin Resource Manager (upload + edit dialogs)
|
||
- Teacher Library (upload dialog)
|
||
|
||
Provides Subject → Domain → Topics → Learning Objectives selection with auto-fetching and reset-on-parent-change logic.
|
||
|
||
### API endpoints for taxonomy data
|
||
|
||
| Method | Route | Description |
|
||
|--------|-------|-------------|
|
||
| GET | `/api/subjects` | List subjects (paginated, returns `items` array) |
|
||
| GET | `/api/domains?subject_id=N` | List domains filtered by subject |
|
||
| GET | `/api/topics?domain_id=N` | List topics filtered by domain |
|
||
| GET | `/api/learning-objectives?topic_id=N` | List objectives filtered by topic |
|
||
|
||
---
|
||
|
||
## 13. Known Issues & Notes
|
||
|
||
1. **PostgreSQL version**: pgdata is PG 18; must use micromamba `pg_ctl` (`/Users/yamenahmad/micromamba/envs/odoo19/bin/pg_ctl`), NOT conda env PG 17.
|
||
2. **`odoo.conf` requires `db_name`**: Without `db_name = encoach_v2`, all API routes return 404.
|
||
3. **`npm` not in default PATH**: Must add `/Users/yamenahmad/.local/node/bin` to PATH before running frontend.
|
||
4. **OpenAI not configured**: AI generation uses fallback templates (static mock content). Set OpenAI API key in Odoo system parameters to enable real AI.
|
||
5. **Gitea deployment hook**: Frontend v3 server has a `docker-compose.yml` conflict that blocks auto-deploy. SSH and stash/checkout on server to fix.
|
||
6. **Student passwords were reset**: Student demo users (`sarah`, `omar`, `layla`) now use password **`student123`** (not `admin`). Passwords were reset via `psycopg2` + `passlib` script. To reset again:
|
||
```python
|
||
from passlib.context import CryptContext
|
||
ctx = CryptContext(schemes=['pbkdf2_sha512'])
|
||
new_hash = ctx.hash('student123')
|
||
# UPDATE res_users SET password = '<new_hash>' WHERE login = 'sarah@encoach.test';
|
||
```
|
||
7. **`op.course` extended fields**: The `op.course` model (OpenEduCat) was extended via `encoach_lms_api/models/course_ext.py` to add `description` (Text) and `max_capacity` (Integer). If the module is not installed/updated, course creation will fail with `ValueError: Invalid field 'description'`.
|
||
8. **`material_count` computed field**: The `material_count` on `encoach.course.chapter` requires `@api.depends('material_ids')` to recompute when materials are added. If counts show 0 after adding materials, restart Odoo with `-u encoach_lms_api`.
|
||
9. **Taxonomy API response format**: Taxonomy list endpoints (`/api/subjects`, `/api/domains`, `/api/topics`) return paginated responses with data under an `items` key (`{"items": [...], "total": N}`), not a `data` key. The frontend `taxonomy.service.ts` handles both formats.
|
||
10. **Resource taxonomy fields via FormData**: When uploading resources, `topic_ids` and `learning_objective_ids` are sent as comma-separated strings in multipart FormData. The backend `create_resource` handler parses these into proper Many2many commands `[(6, 0, ids)]`.
|
||
11. **Module upgrades for schema changes**: After adding new fields to Odoo models (e.g. `encoach_subject_id` on `op.course`), you must run `./run.sh -u <module_name> --stop-after-init` to apply DB schema changes before the server can use them.
|
||
|
||
---
|
||
|
||
## 14. Frontend Key Pages
|
||
|
||
### Admin Pages
|
||
| Route | Component | Description |
|
||
|-------|-----------|-------------|
|
||
| `/admin/dashboard` | AdminLmsDashboard | Platform overview, AI tips, stats (LMS-focused) |
|
||
| `/admin/platform` | AdminDashboard | Alternative platform dashboard |
|
||
| `/admin/courses` | AdminCourses | Course table with enroll/assign/delete actions |
|
||
| `/admin/students` | AdminStudents | Student management |
|
||
| `/admin/teachers` | AdminTeachers | Teacher management |
|
||
| `/admin/users` | UsersPage | All platform users + roles (Management section) |
|
||
| `/admin/entities` | EntitiesPage | Entity/organization management |
|
||
| `/admin/classrooms` | ClassroomsPage | Batches/classrooms with student management |
|
||
| `/admin/user-roles` | UserRoles | User role assignments (⚠ NOT `/admin/roles`) |
|
||
| `/admin/roles-permissions` | RolesPermissions | Roles & permissions matrix |
|
||
| `/admin/authority-matrix` | AuthorityMatrix | Authority matrix across roles |
|
||
| `/admin/generation` | GenerationPage | AI exam generation |
|
||
| `/admin/examsList` | ExamsListPage | List all exams (⚠ NOT `/admin/exams`) |
|
||
| `/admin/rubrics` | RubricsPage | Manage rubrics |
|
||
| `/admin/assignments` | AssignmentsPage | Manage assignments |
|
||
| `/admin/approval-workflows` | ApprovalWorkflowsPage | Approval workflow config |
|
||
| `/admin/taxonomy` | TaxonomyManager | Manage subjects, domains, topics, objectives (inline editable) |
|
||
| `/admin/resources` | ResourceManager | Upload/edit resources with full taxonomy cascade (Subject→Domain→Topics→Objectives) |
|
||
|
||
### Admin — Institutional Pages
|
||
|
||
Backed by `encoach_lms_api` + OpenEduCat models. All 14 pages verified CREATE / EDIT / DELETE / WORKFLOW in the write-flow sweep (see §17).
|
||
|
||
| Route | Page | Backing API | Key actions |
|
||
|-------|------|-------------|-------------|
|
||
| `/admin/academic-years` | Academic Years | `/api/academic-years`, `/generate-terms` | CREATE, generate-terms, EDIT, DELETE (cascades terms) |
|
||
| `/admin/academic-terms` | Academic Terms | `/api/academic-terms` | CRUD |
|
||
| `/admin/departments` | Departments | `/api/departments` | CRUD |
|
||
| `/admin/admission-registers` | Admission Registers | `/api/admission-registers` | CREATE, confirm, DELETE |
|
||
| `/admin/admissions` | Admissions | `/api/admissions` | CREATE (date-constrained), DELETE |
|
||
| `/admin/inst-exam-sessions` | Institutional Exam Sessions | `/api/inst-exam-sessions` | CREATE, schedule, EDIT, DELETE |
|
||
| `/admin/marksheets` | Marksheets | `/api/result-templates`, `/api/marksheets` | Create template, generate, validate marksheet, DELETE |
|
||
| `/admin/facilities` | Facilities | `/api/facilities`, `/api/assets` | Facility CRUD + nested assets |
|
||
| `/admin/activities` | Activities | `/api/activity-types`, `/api/activities` | Type + activity CRUD |
|
||
| `/admin/library` | Library | `/api/library/media` | Media + authors, detail exposes author links |
|
||
| `/admin/lessons` | Lessons | `/api/lessons` | CRUD; edits preserve `course_id` / `batch_id` |
|
||
| `/admin/gradebook` | Gradebook | `/api/grading-assignments`, `/api/gradebooks`, `/api/gradebook-lines` | Assignment CRUD + gradebook drill |
|
||
| `/admin/student-progress` | Student Progress | `/api/student-progress` | Drill-through detail |
|
||
| `/admin/student-leave` | Student Leave | `/api/student-leaves` | CREATE, approve, DELETE |
|
||
| `/admin/fees` | Fees | `/api/fees-plans`, `/create-invoice`, `/register-payment` | List seeded plans, generate invoice, register payment |
|
||
|
||
### Teacher Pages
|
||
| Route | Component | Description |
|
||
|-------|-----------|-------------|
|
||
| `/teacher/dashboard` | TeacherDashboard | Teacher overview |
|
||
| `/teacher/courses` | TeacherCourses | Course cards with Edit, Chapters & Materials, AI Workbench |
|
||
| `/teacher/courses/new` | CourseBuilder | Create new course |
|
||
| `/teacher/courses/:courseId/chapters` | CourseChapters | Manage chapters and materials for a course |
|
||
| `/teacher/courses/:courseId/chapters/:chapterId` | ChapterDetail | Chapter detail with material list |
|
||
| `/teacher/courses/:courseId/workbench` | AiWorkbench | AI content generation workbench |
|
||
| `/teacher/assignments` | TeacherAssignments | Manage assignments |
|
||
| `/teacher/library` | TeacherLibrary | Upload resources with full taxonomy cascade (Subject→Domain→Topics→Objectives) |
|
||
|
||
### Student Pages
|
||
| Route | Component | Description |
|
||
|-------|-----------|-------------|
|
||
| `/student/dashboard` | StudentDashboard | Enrolled courses, progress, grades, quick actions |
|
||
| `/student/courses` | StudentCourses | Enrolled courses with progress badges |
|
||
| `/student/courses/:id` | StudentCourseDetail | Course chapters, materials, grades |
|
||
| `/student/courses/:courseId/chapters/:chapterId` | StudentChapterView | View materials, mark viewed/complete |
|
||
| `/student/my-exams` | StudentExamsPage | Student's assigned exams |
|
||
| `/student/exam/:id` | ExamSession | Take an exam |
|
||
| `/student/exam/:id/results` | ExamResults | View results |
|
||
| `/student/assignments` | StudentAssignments | Homework assignments |
|
||
|
||
---
|
||
|
||
## 15. Database Connection
|
||
|
||
```
|
||
Host: localhost
|
||
Port: 5432
|
||
Database: encoach_v2
|
||
User: yamenahmad
|
||
Password: (none)
|
||
```
|
||
|
||
Quick access:
|
||
```bash
|
||
PGBIN="/Users/yamenahmad/micromamba/envs/odoo19/bin"
|
||
"$PGBIN/psql" -U yamenahmad -d encoach_v2
|
||
```
|
||
|
||
---
|
||
|
||
## 16. QA Test Report — 2026-04-18
|
||
|
||
A full end-to-end browser test was executed across all three user types (admin, teacher, student), driven by an autonomous browser agent. 32 distinct pages were verified plus an exam session flow.
|
||
|
||
### 16.1 Backend API Health — 32/33 endpoints healthy (pre-fix)
|
||
|
||
All tested with admin JWT unless noted:
|
||
|
||
| Category | Endpoints Tested | Status |
|
||
|----------|-----------------|--------|
|
||
| Auth | `/api/login`, `/api/user` | ✅ |
|
||
| Users & Roles | `/api/users/list`, `/api/users/with-roles`, `/api/roles`, `/api/permissions`, `/api/authority-matrix`, `/api/user-authority-matrix` | ✅ |
|
||
| Entities | `/api/entities`, `/api/entities/<id>/roles` | ✅ |
|
||
| LMS Core | `/api/courses`, `/api/students`, `/api/teachers`, `/api/batches`, `/api/batches/<id>/students` | ✅ |
|
||
| Taxonomy | `/api/subjects`, `/api/domains`, `/api/topics`, `/api/learning-objectives` | ✅ |
|
||
| Resources | `/api/resources`, `/api/resource-tags` | ✅ |
|
||
| Exams | `/api/exam/custom/list`, `/api/exam/templates`, `/api/exam-structures`, `/api/exam-schedules`, `/api/rubrics`, `/api/rubric-groups` | ✅ |
|
||
| Assignments | `/api/assignments` | ✅ |
|
||
| Stats | `/api/stats`, `/api/stats/performance`, `/api/analytics/student`, `/api/adaptive/dashboard` | ✅ |
|
||
| Courseware | `/api/courses/<id>/chapters`, `/api/courses/<id>/completion`, `/api/materials/search` | ✅ |
|
||
| Grading | `/api/grading/queue` | ✅ |
|
||
| Student role | `/api/student/my-courses`, `/api/student/my-exams`, `/api/assignments` | ✅ |
|
||
| Approvals | `/api/approval-workflows`, `/api/approval-requests`, `/api/approval-users` | ✅ (after fix) |
|
||
|
||
### 16.2 Bugs Fixed During This Pass
|
||
|
||
**Bug #1: `/api/approval-workflows` returned 500**
|
||
- **Cause**: `encoach_exam_template/controllers/approval_workflows.py` ran raw SQL against tables (`encoach_approval_workflow`, `encoach_approval_request`) that are only created by the `encoach_approval` module — which lives in `new_project/` and is NOT installed in the active `backend/`.
|
||
- **Fix**: Added a `_table_exists(cr, name)` helper using `to_regclass()`. Both `list_workflows` and `list_requests` now return `{items: [], total: 0}` gracefully when the table is absent, matching the frontend's empty-state expectation.
|
||
|
||
**Bug #2: `DELETE /api/courses/<id>` returned opaque 500 for courses with enrollments**
|
||
- **Cause**: `op.course` has a `RESTRICT` FK from `op_student_course`. Postgres raised a raw violation with no user-friendly handling.
|
||
- **Fix**: `delete_course` in `encoach_lms_api/controllers/lms_core.py` now (a) detects enrollments first and returns a 409 with a clear message, (b) accepts `?force=true` to cascade-delete enrollments and detach linked batches before removing the course.
|
||
|
||
### 16.3 Admin Workflows — All Pages Pass ✅
|
||
|
||
| Page | Route | Result |
|
||
|------|-------|--------|
|
||
| Dashboard | `/admin/dashboard` | Stats cards, AI tips, platform insights render |
|
||
| Users | `/admin/users` | 7 users shown with roles, filter tabs work |
|
||
| Entities | `/admin/entities` | Demo Academy entity, 6 users / 5 roles |
|
||
| Classrooms | `/admin/classrooms` | IELTS 2026 Spring (3 students), student dialog works |
|
||
| User Roles | `/admin/user-roles` | 7 users, 5 available roles, assignments rendered |
|
||
| Roles & Permissions | `/admin/roles-permissions` | 5 roles × 115 permissions × 22 categories |
|
||
| Authority Matrix | `/admin/authority-matrix` | 257 granted cells, 45 % coverage, CSV export present |
|
||
| Courses | `/admin/courses` | 3 courses, enroll-students dialog with BOTH "Individual" and "By Classroom" tabs verified |
|
||
| Taxonomy | `/admin/taxonomy` | 3 subjects (English, Math, IT) with full tree |
|
||
| Resources | `/admin/resources` | 15 items (6 library + 9 course materials), filters & cascade pickers work |
|
||
| Exams List | `/admin/examsList` | 16 custom exams, search filters correctly |
|
||
| Generation | `/admin/generation` | Loads; Official/Practice tabs work |
|
||
| Rubrics | `/admin/rubrics` | 3 rubrics + groups tab |
|
||
| Assignments | `/admin/assignments` | 2 assignments, filter tabs work |
|
||
| Approval Workflows | `/admin/approval-workflows` | Empty-state OK, Create dialog opens |
|
||
|
||
### 16.4 Teacher Workflows — All Pages Pass ✅
|
||
|
||
Logged in as `khalid@encoach.test` (Dr. Khalid). Tested 11 pages; all work:
|
||
|
||
- `/teacher/dashboard` — 3 Active Courses, 55 Total Students, 0 Pending Grading, 82 % Avg Pass Rate
|
||
- `/teacher/courses` — 3 course cards with Edit / Chapters / AI Workbench buttons
|
||
- `/teacher/students` — 3 students with batch info, attendance, AI risk indicators
|
||
- `/teacher/library` — 15 resources, upload dialog's taxonomy cascade confirmed working
|
||
- `/teacher/assignments` — empty state handled gracefully
|
||
- `/teacher/courses/3/chapters` — empty state + "Add Chapter" dialog opens with Name/Description/Start Date/Unlock Mode
|
||
- `/teacher/attendance` — batch selector + trend chart + AI warning banners
|
||
- `/teacher/timetable` — weekly grid 08:00–17:00
|
||
- `/teacher/discussions` / `/teacher/announcements` / `/teacher/profile` — all load cleanly
|
||
|
||
### 16.5 Student Workflows — All Pages Pass ✅
|
||
|
||
Logged in as `sarah@encoach.test` (password `student123`). Tested 16 pages + exam session; all work:
|
||
|
||
- `/student/dashboard` — 67 % overall progress, 3 courses, 4 upcoming exams popup
|
||
- `/student/courses` — 3 cards with progress (100 %, 100 %, 0 %)
|
||
- `/student/courses/1` — course detail with "Chapters (1) / Grades (0) / About" tabs + "Take Post-Test"
|
||
- `/student/courses/1/chapters/1` — 3/3 materials completed (IELTS Listening video, Reading article, Grammar docs)
|
||
- `/student/assignments` / `/student/grades` / `/student/attendance` / `/student/timetable` — all load with graceful empty or populated states
|
||
- `/student/profile` — editable name, email, password change
|
||
- `/student/discussions` / `/student/announcements` / `/student/messages` / `/student/journey` — empty states clean
|
||
- `/student/subjects` — 3 subjects with "Start Diagnostic" CTAs
|
||
- `/student/subject-registration` — registration table renders
|
||
|
||
**Exam flow** — `/student/exam/2/session` loaded "IELTS Academic Mock - Spring 2026 Full Test" with 170-minute timer, Listening section, 7 questions, MCQ with 4 radio options, Previous / Flag / Next navigation. Clean and functional.
|
||
|
||
### 16.6 Demo Data State (Post-Cleanup)
|
||
|
||
| Entity | Count | Notes |
|
||
|--------|-------|-------|
|
||
| Users | 7 | 2 admins, 2 teachers, 3 students |
|
||
| Courses | 3 | IELTS Preparation B2, Python Programming Fundamentals, IELTS Listening & Speaking Intensive (junk "tttttt" removed, "test" renamed) |
|
||
| Batches | 1 | IELTS 2026 Spring (3 students) |
|
||
| Exams (custom) | 16 | published + draft mix |
|
||
| Resources | 6 | library resources; +9 course materials |
|
||
| Taxonomy subjects | 3 | English (6d/37t), Mathematics (6d/15t), IT (6d/17t) — junk "math" subject removed |
|
||
|
||
### 16.7 Route Naming Gotchas
|
||
|
||
Two frontend routes differ from the pattern a developer might guess:
|
||
|
||
| Expected | Actual |
|
||
|----------|--------|
|
||
| `/admin/exams` | `/admin/examsList` |
|
||
| `/admin/roles` | `/admin/user-roles` |
|
||
|
||
The `Admin Pages` table in §14 has been updated with the full correct route list.
|
||
|
||
### 16.8 Minor Observations (Non-Blocking)
|
||
|
||
- React Router v7 future flag warnings in console on every page (`v7_startTransition`, `v7_relativeSplatPath`) — cosmetic only, ignore until v7 migration.
|
||
- forwardRef warning in Badge component — dev-only, not a runtime error.
|
||
- `/api/scores` by itself returns 404, but its sub-endpoints (`/api/scores/release` etc.) are valid.
|
||
- Session occasionally expired during long admin test runs, requiring re-login. Token TTL may warrant increasing for QA flows.
|
||
|
||
---
|
||
|
||
## 17. Institutional Admin Pages — Write-Flow QA — 2026-04-18
|
||
|
||
Comprehensive API-level CREATE / EDIT / DELETE / WORKFLOW sweep across all 14 institutional admin pages (see §14 "Admin — Institutional Pages"). Driven by `test_write_flows.py` against real Odoo data seeded by `seed_institutional.py`.
|
||
|
||
**Final result: `SUMMARY PASS=44 FAIL=0 TOTAL=44`** ✅
|
||
|
||
### 17.1 Scenarios covered
|
||
|
||
| Page | Actions verified |
|
||
|------|------------------|
|
||
| Academic Years | CREATE, generate-terms (3), EDIT, DELETE (cascades terms) |
|
||
| Academic Terms | EDIT, DELETE |
|
||
| Departments | CREATE, EDIT, DELETE |
|
||
| Admission Registers | CREATE, confirm, DELETE |
|
||
| Admissions | CREATE (date-constrained to register window), DELETE |
|
||
| Inst Exam Sessions | CREATE, schedule, EDIT, DELETE |
|
||
| Marksheets | CREATE template, generate-marksheets, validate marksheet, DELETE template (cascades registers) |
|
||
| Facilities | CREATE, EDIT, DELETE |
|
||
| Assets | CREATE, DELETE (backed by new `encoach.asset` model) |
|
||
| Activities | CREATE type, CREATE activity, DELETE |
|
||
| Library | CREATE media + authors, VERIFY author link, DELETE |
|
||
| Lessons | CREATE, EDIT (preserves `course_id` + `batch_id`), DELETE |
|
||
| Gradebook | CREATE grading-assignment, DELETE, drill lines |
|
||
| Student Progress | DRILL detail |
|
||
| Student Leaves | CREATE, approve, DELETE |
|
||
| Fees | LIST seeded, create-invoice ✅ |
|
||
|
||
### 17.2 Backend fixes applied
|
||
|
||
**1. `encoach.asset` — new lightweight model**
|
||
`op.asset` does not exist in the installed OpenEduCat set. Added `encoach_lms_api/models/asset.py` with `name`, `code`, `facility_id`, `description`, `active`; registered in `__init__.py`; CRUD access granted via `security/ir.model.access.csv`. `facilities.py` now uses `_asset_model()` → `env['encoach.asset']`.
|
||
|
||
**2. Missing DELETE routes added**
|
||
- `DELETE /api/admission-registers/<rid>`
|
||
- `DELETE /api/admissions/<aid>`
|
||
- `DELETE /api/result-templates/<tid>` (cascades `op.marksheet.register` rows created by `generate_result`)
|
||
- `DELETE /api/student-leaves/<lid>`
|
||
- `DELETE /api/academic-years/<yid>` now pre-unlinks `academic_term_ids` to avoid the `op_academic_term.academic_year_id` FK RESTRICT violation.
|
||
|
||
**3. Missing detail (GET-one) endpoints added**
|
||
- `GET /api/lessons/<lid>` — exposes full lesson incl. `course_id`, `batch_id` so EDIT can verify those are preserved.
|
||
- `GET /api/library/media/<mid>` — returns `author_ids` + `author_names` (the list endpoint only returns a comma-separated `author` string).
|
||
|
||
**4. Admissions CREATE — defaults + validation**
|
||
`create_admission` now defaults `application_date = fields.Date.today()`, builds `name` from `first_name + last_name`, and accepts `nationality_id` or `nationality`. The Odoo constraint `@api.constrains(...)` requires `application_date ∈ [register.start_date, register.end_date]` — the test payload uses `2028-06-15` to sit inside the seeded register window. Added companion `PATCH/PUT /api/admissions/<aid>` for edits.
|
||
|
||
**5. Grading assignments — required fields resolved**
|
||
`create_grading_assignment` no longer sends `user_id` (invalid on `grading.assignment` in this OpenEduCat version). Auto-resolves the required fields:
|
||
- `assignment_type`: find-or-create `grading.assignment.type` with code `DEFAULT`
|
||
- `faculty_id`: first available `op.faculty`
|
||
- `issued_date`: defaults to `fields.Datetime.now()`
|
||
|
||
**6. Marksheets generate — Odoo 19 compatibility**
|
||
Route aliased as `/api/result-templates/<tid>/generate` **and** `/generate-marksheets`. Controller tries `rec.generate_result()` (OpenEduCat 19) then falls back to `rec.action_generate_marksheet()` (older versions).
|
||
|
||
**7. Fees create-invoice — Odoo 19 JSONB properties**
|
||
OpenEduCat's `op.student.fees.details.get_invoice()` raised `UserError: There is no income account defined for this product`. Root cause: **Odoo 19 removed `ir_property` and stores company-dependent fields (like `property_account_income_id`) as JSONB columns on the model itself** — e.g. `product_template.property_account_income_id` is now `jsonb` keyed by `company_id`.
|
||
|
||
Fix: `create_invoice` auto-wires a default income account before invoking `get_invoice()`:
|
||
|
||
```241:260:backend/custom_addons/encoach_lms_api/controllers/fees.py
|
||
if not (fp or {}).get('income'):
|
||
AA = request.env['account.account'].sudo()
|
||
# Odoo 19: account.account has a M2M `company_ids`.
|
||
try:
|
||
income_acc = AA.search([
|
||
('account_type', '=', 'income'),
|
||
('company_ids', 'in', request.env.company.id),
|
||
], limit=1)
|
||
except Exception:
|
||
income_acc = AA.browse()
|
||
if not income_acc:
|
||
income_acc = AA.search([('account_type', '=', 'income')], limit=1)
|
||
if income_acc:
|
||
try:
|
||
prod_tmpl.with_company(request.env.company).write({
|
||
'property_account_income_id': income_acc.id,
|
||
})
|
||
except Exception:
|
||
_logger.exception('auto-wire income account')
|
||
```
|
||
|
||
The `seed_institutional.py` fee-product block now performs the same wiring at seed time, so fresh databases don't hit this error.
|
||
|
||
### 17.3 Seeding
|
||
|
||
`seed_institutional.py` populates the full institutional fixture set:
|
||
|
||
- 1 academic year + auto-generated terms
|
||
- 1 department
|
||
- 1 admission register + 1 admission
|
||
- 1 exam session + 1 result template → marksheets
|
||
- Facilities, activities, library media, lessons, gradebooks, student leaves
|
||
- **Fees**: 3 `op.fees.terms` (Full, 2-inst 50/50, 3-inst 34/33/33 summing to 100%), 1 `product.product` (`IELTS Preparation B2 Fees`) with income account wired, 3 `op.student.fees.details` records
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd /Users/yamenahmad/projects2026/odoo/odoo19
|
||
./run.sh --shell <<< "exec(open('seed_institutional.py').read())"
|
||
```
|
||
|
||
### 17.4 Running the write-flow test
|
||
|
||
```bash
|
||
cd /Users/yamenahmad/projects2026/odoo/odoo19
|
||
python3 test_write_flows.py
|
||
```
|
||
|
||
The script is **idempotent** — it uses timestamped names for academic-year/register payloads so repeated runs don't collide on Odoo's unique constraints. Expected output: `SUMMARY PASS=44 FAIL=0 TOTAL=44`.
|
||
|
||
### 17.5 Operational notes for Odoo 19 institutional data
|
||
|
||
- `product_template.property_account_income_id` is JSONB keyed by `company_id` — write to it with `.with_company(company).write({...})`, **not** via the removed `ir_property` table.
|
||
- `op.admission` has a constraint that `application_date` must fall inside the parent `op.admission.register`'s `[start_date, end_date]` — front-end forms should default to `register.start_date` to avoid validation errors.
|
||
- `op.result.template.generate_result()` creates `op.marksheet.register` rows; deleting a template without first unlinking those rows will fail. The DELETE endpoint handles this internally.
|
||
- `op.academic.year` → `op.academic.term` FK is `RESTRICT`; always unlink terms before the year.
|
||
- `op.fees.terms.line_ids` must sum to 100% (percentages). Seeded terms conform.
|
||
|
||
|
||
## §18 Support Admin Pages — Backend + Write-Flow QA — 2026-04-18
|
||
|
||
### 18.1 Scope
|
||
|
||
The **Support** group in the admin sidebar is 3 pages:
|
||
|
||
| Admin URL | Component | Before | After (this session) |
|
||
|-------------------------------|---------------------------|------------------------------------------------|----------------------|
|
||
| `/admin/tickets` | `TicketsPage` | Called `/api/tickets` which **did not exist** (404 HTML). | Full backend — `encoach.ticket` model + `/api/tickets` CRUD + `assignedToUser`. |
|
||
| `/admin/payment-record` | `PaymentRecordPage` | Hard-coded mock arrays. | Live data from `op.student.fees.details` + `account.move` + stubbed `/api/paymob-orders`. |
|
||
| `/admin/settings-platform` | `SettingsPage` | Hard-coded mock arrays. | Codes tab → `encoach.code`, Packages tab → `ir.config_parameter`, Grading tab → `ir.config_parameter`. |
|
||
|
||
### 18.2 New backend artefacts
|
||
|
||
**Model** — `backend/custom_addons/encoach_lms_api/models/ticket.py`:
|
||
|
||
```python
|
||
class EncoachTicket(models.Model):
|
||
_name = 'encoach.ticket'
|
||
_inherit = ['mail.thread']
|
||
subject, description, type, status, source, page_context,
|
||
reporter_id, assignee_id, entity_id, resolved_at
|
||
```
|
||
|
||
Registered in `models/__init__.py` and `security/ir.model.access.csv` (full CRUD for `base.group_user`).
|
||
|
||
**Controllers** — added three new files, all registered in `controllers/__init__.py`:
|
||
|
||
| File | Routes |
|
||
|-------------------------------------|--------|
|
||
| `controllers/tickets.py` | `GET/POST /api/tickets`, `GET/PATCH/DELETE /api/tickets/<id>`, `GET /api/tickets/assignedToUser` |
|
||
| `controllers/payments.py` | `GET /api/payment-records` (derived from `op.student.fees.details`), `GET /api/payment-records/invoices` (from `account.move` out_invoice), `GET /api/paymob-orders` (stub, returns `{data:[], message:"Paymob integration not configured."}`) |
|
||
| `controllers/platform_settings.py` | `GET/POST/DELETE /api/codes`, `POST /api/codes/generate`, `GET/POST/PATCH/DELETE /api/packages`, `GET/PATCH /api/grading-config` |
|
||
|
||
Packages and grading config are persisted in `ir.config_parameter` under the keys `encoach.platform.packages` and `encoach.platform.grading` — no new model table needed.
|
||
|
||
### 18.3 Frontend changes
|
||
|
||
| File | Change |
|
||
|--------------------------------------------------|--------|
|
||
| `frontend/src/services/payments.service.ts` | **NEW** — typed client for `/api/payment-records` + `/api/paymob-orders`. |
|
||
| `frontend/src/services/platformSettings.service.ts` | **NEW** — typed client for codes, packages, grading-config. |
|
||
| `frontend/src/pages/PaymentRecordPage.tsx` | Rewritten: 4 KPI cards + live Payments table + Paymob empty-state + CSV export. Removed all mock arrays. |
|
||
| `frontend/src/pages/SettingsPage.tsx` | Rewritten: Codes tab with real Generate Single / Generate Batch and delete; Packages tab with Add Package dialog + delete; Grading tab with Save + client/server validation. |
|
||
| `frontend/src/pages/TicketsPage.tsx` | Small fix: accept both `data.items` and `data.data` from the pagination envelope. |
|
||
|
||
### 18.4 Seeding
|
||
|
||
`seed_institutional.py` now also seeds:
|
||
|
||
- **4 tickets** covering all 4 statuses (`open`, `in_progress`, `resolved`) and all main types (`bug`, `feature`, `support`, `feedback`), with realistic subjects / descriptions / `page_context`.
|
||
- **4 registration codes** (2 individual student, 2 corporate batch) — one already marked as used to exercise the badge variant.
|
||
|
||
### 18.5 Write-flow test — 29/29 PASS
|
||
|
||
`test_support_flows.py` exercises:
|
||
|
||
| Page | Actions verified |
|
||
|----------------------|------------------|
|
||
| Tickets | LIST seeded, filter by status + type, text search, CREATE, GET detail, EDIT → in_progress, EDIT → resolved (auto-stamps `resolved_at`), ASSIGN, `assignedToUser` listing, DELETE |
|
||
| Codes (Settings) | LIST seeded, generate individual ×3, filter `used=false`, DELETE, generate corporate ×2 |
|
||
| Packages (Settings) | LIST defaults, CREATE, EDIT price+discount, DELETE, persistence roundtrip |
|
||
| Grading (Settings) | GET, set new scale, server-side validation (rejects `min >= max`), restore IELTS default |
|
||
| Payment Record | LIST + totals, filter `paid=false`, invoices list (`account.move`), Paymob stub returns empty |
|
||
|
||
Run with:
|
||
|
||
```bash
|
||
cd /Users/yamenahmad/projects2026/odoo/odoo19
|
||
python3 test_support_flows.py
|
||
```
|
||
|
||
Latest output: `SUMMARY PASS=29 FAIL=0 TOTAL=29`.
|
||
|
||
### 18.6 Browser verification
|
||
|
||
All 3 pages were opened in a real browser (localhost:8080) after login as `admin` / `admin` and visually verified:
|
||
|
||
- **Tickets** — 4 seeded rows visible, reporter = "Administrator", Create Ticket dialog submits and a new row appears immediately.
|
||
- **Payment Record** — 4 KPI cards (`Total records`, `Paid`, `Unpaid`, `Total amount`), 3 payment rows for the seeded students, Paymob tab shows the empty-state message.
|
||
- **Settings** — Codes tab lists all seeded + generated codes with `Generate Single` working and a success toast; Packages tab shows the 3 default cards; Grading tab shows 0..9 step 0.5 and `Save` returns a success toast.
|
||
|
||
No critical console errors — only React Router v7 future-flag warnings.
|
||
|
||
### 18.7 Operational notes
|
||
|
||
- The `encoach.ticket` model inherits `mail.thread` so OCA-style change tracking works out of the box. Adding followers / comments will work without further wiring.
|
||
- The `/api/payment-records` endpoint is **derived** (not stored) — it reflects whatever is in `op.student.fees.details`. Creating an invoice from the Fees page will flip the `paid` badge automatically on the Payment Record page once the invoice is paid.
|
||
- `/api/paymob-orders` is a deliberate empty stub. When real Paymob integration is wired, replace the stub body in `controllers/payments.py::list_paymob_orders` — no UI change required; the Payment Record page will light up automatically.
|
||
- Packages and grading config live in `ir.config_parameter`. To reset to defaults, delete the two parameters `encoach.platform.packages` and `encoach.platform.grading`.
|
||
|
||
|
||
## §19 Training Admin Pages — Backend + Write-Flow QA — 2026-04-18
|
||
|
||
### 19.1 Scope
|
||
|
||
The **Training** group in the admin sidebar is 2 pages:
|
||
|
||
| Admin URL | Component | Before | After (this session) |
|
||
|---------------------------------|--------------------|------------------------------------------|----------------------|
|
||
| `/admin/training/vocabulary` | `VocabularyPage` | Hard-coded `vocabItems` array (no API). | Full backend: CRUD + search + level filter + per-user progress tracking. |
|
||
| `/admin/training/grammar` | `GrammarPage` | Hard-coded `grammarItems` array (no API).| Full backend: CRUD + search + level filter + per-user progress tracking. |
|
||
|
||
### 19.2 New backend artefacts
|
||
|
||
**Models** — `backend/custom_addons/encoach_lms_api/models/training.py`:
|
||
|
||
| Model | Key fields |
|
||
|--------------------------------|------------|
|
||
| `encoach.vocab.item` | `word` (unique), `meaning`, `example_sentence`, `level` (CEFR A1–C2), `part_of_speech`, `category`, `active`, computed `learners_count` + `completion_count` |
|
||
| `encoach.vocab.progress` | `user_id`, `vocab_id`, `completed`, `mastery` (learning/familiar/mastered), `last_reviewed`, `review_count` — unique per (user, vocab) |
|
||
| `encoach.grammar.rule` | `name` (unique), `description`, `example`, `level`, `category`, `active`, computed stats |
|
||
| `encoach.grammar.progress` | `user_id`, `rule_id`, `completed`, `last_reviewed`, `review_count` — unique per (user, rule) |
|
||
|
||
All four models are registered in `models/__init__.py` and `security/ir.model.access.csv` (full CRUD for `base.group_user`). Uniqueness is enforced via `@api.constrains` instead of `_sql_constraints` — Odoo 19 deprecated the latter (see operational notes below).
|
||
|
||
**Controller** — `backend/custom_addons/encoach_lms_api/controllers/training.py`:
|
||
|
||
| Method / Path | Verb | Purpose |
|
||
|------------------------------------------------------------|----------|---------|
|
||
| `/api/training/vocabulary` | `GET` | List with filters: `level`, `category`, `active`, `search`, pagination. Returns `items`, `total`, and a `summary` block (`total`, `completed`, `remaining`, `completion_rate`) relative to the authenticated user. |
|
||
| `/api/training/vocabulary` | `POST` | Create (`word` + `meaning` required, unique `word`). |
|
||
| `/api/training/vocabulary/<id>` | `GET` | Fetch a single item with the caller's progress merged in. |
|
||
| `/api/training/vocabulary/<id>` | `PATCH` | Update any mutable field. |
|
||
| `/api/training/vocabulary/<id>` | `DELETE` | Delete (cascades progress). |
|
||
| `/api/training/vocabulary/<id>/progress` | `POST` | Mark completed / set `mastery` for the authenticated user (creates the progress row if absent, increments `review_count`). |
|
||
| `/api/training/grammar` (+ same suffixes) | — | Analogous five endpoints for grammar rules plus `/progress`. |
|
||
|
||
`ValidationError` and `UserError` raised from the model layer now surface as **HTTP 400** (not 500), so the frontend can show user-friendly toasts.
|
||
|
||
### 19.3 Frontend changes
|
||
|
||
| File | Change |
|
||
|--------------------------------------------------|--------|
|
||
| `frontend/src/services/training.service.ts` | **NEW** — typed client for both vocabulary and grammar (list, CRUD, progress). |
|
||
| `frontend/src/pages/VocabularyPage.tsx` | Rewritten. Now: progress summary card with live % complete, search + level filter, checkable list rows, Add Word dialog with CEFR + part-of-speech, delete button per row. AI panel text is dynamic based on `summary.remaining`. |
|
||
| `frontend/src/pages/GrammarPage.tsx` | Rewritten. Same pattern as vocabulary but tailored to grammar (rule name, description, example, category). |
|
||
|
||
### 19.4 Seeding
|
||
|
||
`seed_institutional.py` now seeds:
|
||
|
||
- **10 vocabulary items** spanning B1–C1, with realistic meanings and example sentences (Ubiquitous, Pragmatic, Eloquent, Meticulous, Ambiguous, Coherent, Versatile, Concise, Collaborate, Rationale).
|
||
- **8 grammar rules** spanning A2–C1 (Conditionals Type 2/3, Passive Voice, Relative Clauses, Subject-Verb Agreement, Modal Speculation, Reported Speech, Articles, Present Perfect vs Past Simple).
|
||
- **6 progress rows** for the admin user (3 vocab mastered, 3 grammar completed) so the UI always shows non-zero state on a freshly seeded DB.
|
||
|
||
Run with:
|
||
|
||
```bash
|
||
.conda-envs/odoo19/bin/python odoo/odoo-bin shell -c odoo.conf --no-http <<'EOF'
|
||
exec(open('seed_institutional.py').read())
|
||
EOF
|
||
```
|
||
|
||
The seeding block is **idempotent** — re-running it never creates duplicates (each row checks by unique key first).
|
||
|
||
### 19.5 Write-flow test — 26/26 PASS
|
||
|
||
`test_training_flows.py` exercises:
|
||
|
||
| Page | Actions verified |
|
||
|-------------------|------------------|
|
||
| Vocabulary | LIST seeded, filter by `level=B2`, search `eloqu`, CREATE, **duplicate-word rejection**, **missing-meaning rejection**, GET detail, EDIT level+meaning, PROGRESS mark completed, **summary recomputation** after completion, PROGRESS un-mark, DELETE, GET-after-DELETE → 404 |
|
||
| Grammar | LIST seeded, filter by `level=B1`, search `passive`, CREATE, missing-description rejection, GET detail, EDIT level+category, PROGRESS mark / un-mark, DELETE, GET-after-DELETE → 404 |
|
||
| Pagination envelope | `size=3 page=1` (vocab), `size=3 page=2` (grammar) — confirms `page` + `size` are echoed in the response |
|
||
|
||
Run with:
|
||
|
||
```bash
|
||
cd /Users/yamenahmad/projects2026/odoo/odoo19
|
||
python3 test_training_flows.py
|
||
```
|
||
|
||
Latest output: `SUMMARY PASS=26 FAIL=0 TOTAL=26`.
|
||
|
||
### 19.6 Browser verification
|
||
|
||
Both admin pages were opened in a real browser (localhost:8080) after login as `admin` / `admin`:
|
||
|
||
- **Vocabulary** — progress card shows live `X/N completed (Y%)` with a progress bar; clicking the circle next to a row marks it complete and the summary instantly recomputes; search `"elo"` narrows the list to just *Eloquent*; the *Add Word* dialog creates a new row + shows a toast.
|
||
- **Grammar** — all 8 seeded rules visible (A2–C1), progress tracking works identically; *Add Rule* dialog creates a new row.
|
||
|
||
No functional errors in the console — only React Router v7 future-flag warnings.
|
||
|
||
### 19.7 Operational notes for Odoo 19 training data
|
||
|
||
- **`_sql_constraints` is deprecated in Odoo 19** (the registry emits `Model attribute '_sql_constraints' is no longer supported`). Uniqueness and range checks in these models are therefore implemented with `@api.constrains`, which continues to work on both Odoo 18 and 19.
|
||
- Because both progress models have `ondelete='cascade'` back to the library item, deleting a vocabulary/grammar row automatically cleans up every learner's progress row — no orphan rows.
|
||
- Progress rows are keyed to `res.users.id`, not `op.student`. Any authenticated API user (admin, teacher, student) sees only their own completions in the `summary` block, while `learners_count` / `completion_count` on the library item itself aggregate across all users.
|
||
- The `/api/training/<kind>/<id>/progress` endpoint is **upsert**-like: the first call creates the progress row, subsequent calls update it and bump `review_count`. This lets the frontend treat it as "toggle" without worrying about a missing row.
|
||
|
||
## 20. Reports Section (2026-04-19)
|
||
|
||
Before this pass, the three pages under the sidebar "Reports" group were **pure mock UIs** — hardcoded JavaScript arrays in each component, non-functional filters, no network requests. They now aggregate real data from the attempt pipeline.
|
||
|
||
### 20.1 Scope
|
||
|
||
| Page | URL | Before | After |
|
||
|------|-----|--------|-------|
|
||
| Student Performance | `/admin/student-performance` | 6 hardcoded students ("Sarah Johnson", "Ahmed Hassan"…) | Per-student band averages from `encoach.student.attempt`, CEFR level derived from the latest reportable attempt, entity + level + search filters, CSV export, three KPI cards |
|
||
| Stats Corporate | `/admin/stats-corporate` | Hardcoded bar/line/pie data | Four tabs (Overview, Trends, Distribution, Comparison) driven by a single `/api/reports/stats-corporate` rollup — bar chart of module averages (× 10), six-month trend, CEFR distribution pie, and entity comparison bar. Threshold + entity + months filters all hit the backend. |
|
||
| Record | `/admin/record` | Four hardcoded rows | Paginated per-user attempt history with search / entity / user / period filters, real exam codes (`EX-###`), duration derived from `started_at` → `completed_at`, CSV export |
|
||
|
||
All three pages share a single `/api/reports/filters` endpoint that returns the entities and students that actually have attempts, so dropdowns never offer filters that would produce empty results.
|
||
|
||
### 20.2 Backend artifacts
|
||
|
||
**New controller** — `backend/custom_addons/encoach_lms_api/controllers/reports.py`:
|
||
|
||
| Route | Method | Purpose |
|
||
|-------|--------|---------|
|
||
| `/api/reports/student-performance` | GET | One row per student. Aggregates per-skill bands across all their reportable attempts, takes the CEFR level from the most recent attempt (or derives it from overall band). Filters: `entity_id`, `level`, `search`, `since`. |
|
||
| `/api/reports/stats-corporate` | GET | Corporate rollups: `by_module` (4 module averages × 10 for the bar chart), `trend` (last N months, default 6), `distribution` (CEFR bins with palette colors), `comparison` (per-entity average × 10). Filters: `entity_id`, `threshold` (0-100), `months`, `since`. |
|
||
| `/api/reports/record` | GET | Paginated attempt history. Same shape as `{items, total, page, size}`. Filters: `user_id`, `entity_id`, `period=day\|week\|month`, `status`, `page`, `size`. Emits a normalized `status_label` and an `EX-###` exam code. |
|
||
| `/api/reports/filters` | GET | Lightweight picker payload: `{entities: [...], students: [...]}` — only students that have at least one attempt, only entities that exist. |
|
||
|
||
An attempt is considered **reportable** if `status ∈ {completed, scoring, scored, released}`. In-progress attempts are skipped from aggregates so a student with only a half-finished exam doesn't show up with zeros.
|
||
|
||
CEFR labels are normalized to canonical display casing (`Pre-A1 / A1 / … / C2`) via `_normalize_cefr`, and falls back to band-based bucketing via `_cefr_for_band` when `cefr_level` is null on the attempt.
|
||
|
||
No new models were needed — the Reports section only reads from:
|
||
- `encoach.student.attempt` (overall / per-skill bands, cefr_level, entity_id, started/completed timestamps)
|
||
- `encoach.exam.custom` (exam title — note: this model uses `title`, not `name`, which tripped the first version of the controller)
|
||
- `encoach.exam.assignment` (optional, to link an attempt back to its assignment when one exists)
|
||
- `encoach.entity`, `res.users`
|
||
|
||
### 20.3 Frontend artifacts
|
||
|
||
**New service** — `frontend/src/services/reports.service.ts`:
|
||
|
||
```typescript
|
||
reportsService.studentPerformance({ entity_id, level, search, since })
|
||
reportsService.statsCorporate({ entity_id, threshold, months, since })
|
||
reportsService.record({ user_id, entity_id, period, status, page, size })
|
||
reportsService.filters()
|
||
```
|
||
|
||
Typed interfaces: `StudentPerformanceRow`, `StatsCorporateResponse` (with nested `StatsModuleRow` / `StatsTrendRow` / `StatsDistributionRow` / `StatsComparisonRow`), `RecordRow`, `ReportsFiltersResponse`.
|
||
|
||
**Rewritten pages:**
|
||
- `frontend/src/pages/StudentPerformancePage.tsx` — dropped all hardcoded arrays; added three KPI cards (students tracked, avg overall, top performer level), a search input, Entity select populated from `/api/reports/filters`, Level select (`all / A1 / A2 / B1 / B2 / C1 / C2`), CSV export, loading + empty states. Per-row AI Grade Explainer retained.
|
||
- `frontend/src/pages/StatsCorporatePage.tsx` — dropped all hardcoded chart data; wired every tab to the `stats-corporate` payload. Added loading + empty states. Threshold buttons (0/50/70/90%) now hit the backend. Comparison tab now renders a real entity bar chart instead of a placeholder panel. AI narrative bubbles receive live data.
|
||
- `frontend/src/pages/RecordPage.tsx` — dropped all hardcoded records; wired filters (entity, user, period) + CSV export. Status badge variant adapts to `completed / released / scored / in_progress / scoring`.
|
||
|
||
### 20.4 Seeding — `seed_reports.py` (idempotent)
|
||
|
||
The live database had only 12 attempts, most of them in-progress with no band scores, so the Reports pages would have shown empty charts. The new script:
|
||
|
||
1. Ensures three entities exist: `Acme Corp (ACME)`, `Global Ltd (GLOBAL)`, `Tech Co (TECHCO)` — using `code` (required NOT NULL on `encoach.entity`) as the idempotency key.
|
||
2. Walks every existing `in_progress` attempt and assigns it a plausible score matrix from `BAND_MATRIX`, marks it `completed`, stamps `completed_at = started_at + 120 min`, and parks it in one of the three entities if it had none.
|
||
3. Creates up to `6 months × 4 students = 24` additional historical attempts (with a random 30% skip to keep the trend line jagged), using a small monthly `delta` so the trend chart rises over time. Each new row is guarded by a 6-day proximity lookup so re-running only fills gaps.
|
||
4. Commits once at the end and prints the final reportable-attempt count.
|
||
|
||
Result after the first run: **28 reportable attempts** spread across 3 entities, 4 students, and 6 months — enough to populate every chart.
|
||
|
||
Run with:
|
||
|
||
```bash
|
||
cd /Users/yamenahmad/projects2026/odoo/odoo19/odoo
|
||
../.conda-envs/odoo19/bin/python odoo-bin shell -c ../odoo.conf --no-http --stop-after-init < ../seed_reports.py
|
||
```
|
||
|
||
### 20.5 Smoke test — 25/25 PASS
|
||
|
||
`test_reports_flows.py` exercises all four endpoints:
|
||
|
||
| Endpoint | Checks |
|
||
|----------|--------|
|
||
| `/api/reports/filters` | GET returns `{entities: [...], students: [...]}` with at least one of each |
|
||
| `/api/reports/student-performance` | LIST returns rows, row shape complete, `overall` numeric, CEFR level is a known label, `search` filter narrows to the matching student, `level` filter returns only that level, `entity_id` filter narrows rows |
|
||
| `/api/reports/stats-corporate` | Envelope has `by_module` + `trend` + `distribution` + `comparison` + `meta`; by_module has the 4 IELTS modules; trend length == `months` (default 6, also tested with 3); distribution has per-level color; `threshold=70` drops attempts considered (28 → 14); entity filter narrows comparison; months=3 shortens trend |
|
||
| `/api/reports/record` | Paginated list, row shape complete, `exam_code` is `EX-###`, `status_label` is titlecased, `user_id` filter scopes to that student only, `entity_id` filter scopes to that entity, `period=month` returns the in-window subset, pagination `size=5 page=1` + `page=2` both work |
|
||
|
||
Latest output: `Summary: 25 passed, 0 failed, 25 total`.
|
||
|
||
Regressions also re-run on this pass: Configuration `24/24`, Support `29/29`, Training `26/26` — all still green.
|
||
|
||
### 20.6 Browser verification
|
||
|
||
Logged in as `admin / admin` on `localhost:8080` and walked all three pages:
|
||
|
||
- **Student Performance** — KPI cards populated (5 students tracked, 6.3 average band), table shows real students (Sarah Ahmed, Omar Khan, Layla Nasser, TestUser WriteFlow, Admin User), Level `A2` filter narrowed to 2 rows, Entity dropdown showed real entities, CSV export triggered a download. No console errors.
|
||
- **Stats Corporate** — Heading confirmed `(28 scored attempts)`. Overview bar chart shows 4 modules with values ~64–72. Trends line chart has 6 monthly points Oct–Apr peaking at ~85. Distribution pie: C1 11 / B2 6 / B1 5 / A2 4. Comparison bar: Global Ltd ~81, Acme Corp ~63, Tech Co ~62. AI Summary bubbles quote the real numbers. All filters responsive.
|
||
- **Record** — Heading `(28 attempts)`, table shows real student names / exam codes (EX-001, EX-005, EX-017…) / dates / scores / statuses. User filter scoped the list to that student; CSV export worked.
|
||
|
||
Network tab showed `/api/reports/filters 200`, `/api/reports/record?size=100 200`, `/api/reports/student-performance 200`, `/api/reports/stats-corporate 200` — zero 4xx/5xx.
|
||
|
||
### 20.7 Gotchas fixed during this pass
|
||
|
||
- **`encoach.exam.custom` uses `title`, not `name`.** The first version of the Record endpoint threw `AttributeError: 'encoach.exam.custom' object has no attribute 'name'`. The controller now uses `getattr(exam, 'title', None) or getattr(exam, 'display_name', '')` so it survives an upstream rename.
|
||
- **`encoach.entity` has a `NOT NULL` `code` column.** `seed_reports.py` originally tried to create entities with just `{'name': ...}` and hit a `NotNullViolation`. The seeder now passes `(name, code)` and also looks up by `code` when probing for existing rows.
|
||
- **Both `encoach_exam_template` and `encoach_scoring` declare `encoach.student.attempt`.** The Reports controller only reads fields that exist on both definitions (`listening/reading/writing/speaking/overall_band`, `cefr_level`, `status`, `started_at`, and whichever of `completed_at` / `finished_at` is present — `_attempt_completed_at()` probes both). No field access assumes a specific addon version.
|
||
- **In-progress attempts are excluded from the aggregates.** If you ever notice a student you expect to see missing from Student Performance or Stats Corporate, check their attempt status — `in_progress` attempts are deliberately skipped. They DO still appear in the Record page (which shows everything) unless you also pass `status` or `period` filters.
|
||
|
||
|
||
---
|
||
|
||
## §21 — Hardening Release (Phase 2/3 completion)
|
||
|
||
This section records the final wave of work that closed out all remaining items in the Phase 2 (performance & reliability) and Phase 3 (core AI features, accessibility, onboarding) roadmaps. After this release the platform is considered deployment-ready.
|
||
|
||
### 21.1 What shipped
|
||
|
||
The release is organised by capability, not file path.
|
||
|
||
#### Human-in-the-loop AI quality loop (P3.3 → P3.5)
|
||
|
||
| Stage | Where | What it does |
|
||
|---|---|---|
|
||
| 1. Quality gate | `encoach_exam_template.services.quality_checker` + `ielts_validator` | Runs after every AI exam submit; if scores fall below thresholds the exam is parked in `pending_review` instead of `published`. |
|
||
| 2. Review queue | `encoach_exam_template/controllers/review_workflow.py` + `frontend/src/pages/admin/ExamReview{Queue,Detail}.tsx` | Admin-only pages list pending exams, surface aggregate + per-question quality reports, and let a reviewer approve (`→ published`) or reject (`→ draft`) with audit notes. Writes to new `reviewed_by_id`, `reviewed_at`, `review_notes` fields. |
|
||
| 3. Prompt library | `encoach_ai.models.ai_prompt` + `controllers/prompt_controller.py` + `frontend/src/pages/admin/AIPromptEditor.tsx` | `encoach.ai.prompt` with a unique `(key, version)` key, one active row per key. Authors can add a new version, activate it (auto-deactivates prior), and dry-run render with sample variables. Renderer uses `str.format_map` against a safe dict. |
|
||
| 4. Student feedback | `encoach_ai.models.ai_feedback` + `controllers/feedback_controller.py` + `frontend/src/components/AIFeedbackButtons.tsx` + `frontend/src/pages/admin/AIFeedbackTriage.tsx` | Thumbs up/down on any AI artefact (`question`, `coach`, `explanation`, `translation`, `narrative`, `other`). Unique `(user, subject_type, subject_id)` constraint gives upsert semantics; thumbs-down requires a comment. Admin triage page lists/filters/resolves feedback (`open → acknowledged|fixed|dismissed`). |
|
||
|
||
Together these four stages mean every AI output can be gated, reviewed, iterated, and measured — closing the loop from prompt → generation → delivery → feedback → next prompt revision.
|
||
|
||
#### Compliance (P3.2)
|
||
|
||
New `encoach_api/controllers/gdpr.py` + `models/gdpr_erasure.py`:
|
||
|
||
- `GET /api/gdpr/export` — JSON dump of the calling user's profile, entity memberships, exam attempts, answers, AI feedback, AI calls, tickets, coaching sessions.
|
||
- `POST /api/gdpr/delete` — requires `{"confirm": true}`; anonymises the partner PII, deletes personal feedback + coaching transcripts, strips free-text PII from retained exam answers, nulls `user_id` on AI logs, deactivates the `res.users` row, and writes a tombstone to `encoach.gdpr.erasure.request` for audit. Admin accounts are blocked from self-erasure.
|
||
- `frontend/src/pages/PrivacyCenter.tsx` — routed at `/student/privacy`, `/teacher/privacy`, `/admin/privacy`; download-my-data button produces a timestamped JSON file; erase flow is gated behind a `type "DELETE" to confirm` alert dialog.
|
||
|
||
#### Paymob real checkout (P2.6)
|
||
|
||
New `encoach_lms_api/controllers/paymob.py` + `models/paymob_order.py`:
|
||
|
||
- `POST /api/payments/paymob/checkout` runs the three-step Paymob Accept flow (auth → order → payment key) and returns the hosted iframe URL. Our `encoach.paymob.order` row is created *before* the external call so partial failures don't lose orders.
|
||
- `POST /api/payments/paymob/webhook` verifies the HMAC-SHA512 signature over Paymob's canonical field sequence (`amount_cents|created_at|currency|…|success`). Mismatches return 401; duplicates are idempotent; unknown orders return 200 to stop Paymob retrying.
|
||
- Credentials read at request time from `ir.config_parameter` (`encoach.paymob.api_key`, `hmac_secret`, `integration_id`, `iframe_id`) so operators can rotate without restarting Odoo.
|
||
- `GET /api/paymob-orders` now reads real rows instead of returning `[]`.
|
||
|
||
#### Internationalisation (P3.1)
|
||
|
||
- `frontend/src/i18n/` bootstraps `i18next` + `i18next-browser-languagedetector` with nested feature-scoped namespaces.
|
||
- Initial locales: `en` (source of truth, typed with `Translations` interface) and `ar` (full translation of the same keys).
|
||
- RTL handling: when the active language is in `RTL_LANGS`, `document.documentElement.dir` is flipped to `rtl` so Tailwind utilities and Radix primitives adapt.
|
||
- `frontend/src/components/LanguageToggle.tsx` added next to the theme toggle in both `AdminLmsLayout` and `RoleLayout` headers.
|
||
- Loaded via `main.tsx` side-effect import so every route picks it up.
|
||
|
||
#### CI scaffolding (P3.8)
|
||
|
||
- `.github/workflows/ci.yml` with two jobs:
|
||
- **frontend**: `tsc --noEmit` → `eslint` → `vite build` → Playwright smoke tests against the Vite preview server; uploads a Playwright HTML report on failure.
|
||
- **backend**: spins up Postgres 16 as a service and runs the Odoo 19 container with `--test-enable --test-tags encoach_api` against a throwaway `encoach_ci` database.
|
||
- `backend/custom_addons/encoach_api/tests/test_health.py` — first `HttpCase`: asserts `/api/health` returns 200 and `/api/health/ready` returns 200 or 503 (never 5xx).
|
||
- `frontend/playwright.config.ts` + `frontend/e2e/login.spec.ts` — the login page renders the sign-in form; `/` redirects unauthenticated traffic to `/login`.
|
||
- New npm scripts: `test:e2e`, `test:e2e:install`.
|
||
|
||
### 21.2 New DB tables (summary)
|
||
|
||
| Table | Purpose | Added in |
|
||
|---|---|---|
|
||
| `encoach.ai.prompt` | Versioned prompt templates (`key`, `version`, `is_active`, `content`). | P3.4 |
|
||
| `encoach.ai.feedback` | Student thumbs up/down on AI output, with triage status. | P3.5 |
|
||
| `encoach.gdpr.erasure.request` | Tombstone row per right-to-erasure request. | P3.2 |
|
||
| `encoach.paymob.order` | Full lifecycle of every Paymob checkout, including the verified HMAC. | P2.6 |
|
||
|
||
All have per-group `ir.model.access.csv` entries: admin read/write, authenticated-user read for metadata, create-only on feedback.
|
||
|
||
### 21.3 New REST endpoints
|
||
|
||
Everything requires a valid JWT unless noted; write routes additionally check `base.group_system` where relevant.
|
||
|
||
```
|
||
GET /api/ai/prompts list keys (latest per key)
|
||
GET /api/ai/prompts/:key/versions full history for a key
|
||
GET /api/ai/prompts/:id one version (full content)
|
||
POST /api/ai/prompts create new version (admin)
|
||
POST /api/ai/prompts/:id/activate flip active pointer (admin)
|
||
POST /api/ai/prompts/:id/render dry-run render with sample vars
|
||
|
||
POST /api/ai/feedback upsert (user)
|
||
GET /api/ai/feedback/summary up/down counts + my rating
|
||
GET /api/ai/feedback admin triage list
|
||
POST /api/ai/feedback/:id/resolve admin triage resolve
|
||
|
||
GET /api/gdpr/export full data-subject export (self)
|
||
POST /api/gdpr/delete right-to-erasure (self, confirm=true)
|
||
|
||
POST /api/payments/paymob/checkout start a checkout
|
||
POST /api/payments/paymob/webhook HMAC-verified callback (public)
|
||
GET /api/payments/paymob/orders list my orders
|
||
GET /api/paymob-orders admin + self order list (rewired)
|
||
```
|
||
|
||
### 21.4 Frontend surface (new routes)
|
||
|
||
```
|
||
/admin/exam/review-queue → ExamReviewQueue.tsx
|
||
/admin/exam/review/:examId → ExamReviewDetail.tsx
|
||
/admin/ai/prompts → AIPromptEditor.tsx
|
||
/admin/ai/feedback → AIFeedbackTriage.tsx
|
||
/student|teacher|admin/privacy → PrivacyCenter.tsx
|
||
```
|
||
|
||
Plus the `AIFeedbackButtons` primitive (drop-in `<ThumbsUp/>/<ThumbsDown/>` that handles the upsert, dialog, and comment prompt) and `LanguageToggle` in both main layouts.
|
||
|
||
### 21.5 Verification
|
||
|
||
- `python3 -m compileall -q backend/custom_addons/encoach_ai backend/custom_addons/encoach_api backend/custom_addons/encoach_lms_api` — clean.
|
||
- `npx tsc --noEmit -p tsconfig.app.json` — clean.
|
||
- `npm run build` — succeeds; bundle analyser shows the same split profile as the §20 release (initial JS ≈ 250 KB gzipped on the login path); new features are lazy-loaded chunks.
|
||
- New modules do not depend on optional addons: all cross-model reads use `if model in request.env` guards so missing addons degrade gracefully instead of 500-ing.
|
||
|
||
### 21.6 Configuration operators need to set
|
||
|
||
Before the Paymob flow will actually charge anything, set in **Settings → Technical → System Parameters**:
|
||
|
||
```
|
||
encoach.paymob.api_key = <merchant API key>
|
||
encoach.paymob.hmac_secret = <from Paymob dashboard>
|
||
encoach.paymob.integration_id = <default integration (card)>
|
||
encoach.paymob.iframe_id = <hosted iframe id>
|
||
```
|
||
|
||
Until those are set, `POST /api/payments/paymob/checkout` returns 503 with a descriptive message instead of silently crashing.
|
||
|
||
### 21.7 Deferred (known)
|
||
|
||
- `P1.2` — blanket-sudo audit + entity-isolation `ir.rule`s. Needs a coordinated frontend + data migration pass; deferred to a follow-up release. Current security model relies on JWT identity + controller-level `has_group` checks.
|
||
- Broader i18n coverage. Only the keys listed in `src/i18n/locales/en.ts` are translated today — extending to every page is a rolling job that can happen in small PRs now that the plumbing is in place.
|
||
- Playwright coverage is intentionally minimal (login + redirect); it exists as a safety net, not as full end-to-end coverage.
|
||
|
||
---
|
||
|
||
## 22. LangGraph as Core AI Runtime (2026-04-25)
|
||
|
||
Before this pass every AI feature on the platform talked to OpenAI through hand-rolled service classes (`OpenAIService`, `CoursePlanPipeline`, ad-hoc prompts in controllers). That worked but it had no first-class concept of *agents*, no way to bind tools, no review/revise loop, no place to swap models per use case, and no admin surface to configure any of it. The user asked us to make **LangGraph the foundation for every AI feature** and to turn `/admin/ai/prompts` into a real *AI Agents & Tools* configuration console with sensible defaults shipped on day one.
|
||
|
||
### 22.1 What this section delivers
|
||
|
||
| Layer | Before | After |
|
||
|---|---|---|
|
||
| Modelling | Prompts only (`encoach.ai.prompt`) | Prompts **+** `encoach.ai.agent` **+** `encoach.ai.tool` (M2M) |
|
||
| Runtime | Direct `openai.ChatCompletion` calls | LangGraph `StateGraph` compiled per agent (4 topologies) |
|
||
| Tooling | None | 11-tool registry wrapping existing services |
|
||
| Config UI | Single prompt editor | Tabbed Agents / Tools / Prompts console with config dialog + Test Runner |
|
||
| Pipelines | `CoursePlanPipeline` was hardcoded | Routes through `AgentRuntime` when `encoach_ai.use_langgraph_runtime` is on (default **True**) |
|
||
| Defaults | None | 7 pre-seeded agents covering every product pillar |
|
||
| i18n | EN only | EN + AR (RTL) for every new key |
|
||
|
||
### 22.2 New backend artefacts
|
||
|
||
#### Models (`backend/custom_addons/encoach_ai/models/ai_agent.py`)
|
||
|
||
```python
|
||
encoach.ai.agent
|
||
key (Char, unique) # stable slug used by callers
|
||
name, description (Char/Text)
|
||
active (Boolean, default True)
|
||
model (Char) # primary OpenAI model
|
||
fallback_model (Char) # auto-tried on rate-limit / 5xx
|
||
temperature (Float, 0..2)
|
||
max_tokens (Integer)
|
||
response_format (Selection: text | json)
|
||
graph_type (Selection: simple | plan_review_revise | rag | react)
|
||
max_revisions (Integer) # cap for review→revise loop
|
||
quality_checks (Char) # comma-separated tool keys to run
|
||
prompt_key (Char) # link to encoach.ai.prompt
|
||
system_prompt (Text) # overrides prompt lookup if set
|
||
tool_ids (M2M to encoach.ai.tool)
|
||
|
||
encoach.ai.tool
|
||
key (Char, unique) # e.g. `resources.search`
|
||
name, description (Char/Text)
|
||
category (Selection: retrieval | reference | quality | persistence | scoring | custom)
|
||
schema_json (Text) # JSON-schema for arguments
|
||
mutates (Boolean) # writes to DB? (UI flags it)
|
||
active (Boolean)
|
||
sequence (Integer)
|
||
```
|
||
|
||
Access rules added in `security/ir.model.access.csv` for both models — `base.group_system` write, authenticated read for agents (so the LMS can list which agents exist) and admin-only read for tools.
|
||
|
||
#### Services
|
||
|
||
- **`services/agent_tools.py`** — handler registry. A `@register("tool.key")` decorator binds a Python callable to a tool key; `invoke(env, key, params)` is the only entry point and it returns `{"ok": bool, ...}`. The 11 default handlers wrap existing services so the agent layer doesn't duplicate logic:
|
||
|
||
| Tool key | Wraps | Mutates? |
|
||
|---|---|---|
|
||
| `resources.search` | `encoach_vector` semantic search over LMS resources | no |
|
||
| `rubric.fetch` | `encoach.rubric` lookup by id or skill | no |
|
||
| `outcomes.fetch` | `encoach.learning.objective` lookup by course / CEFR | no |
|
||
| `student.profile` | `encoach.user.gap` rollup → CEFR + gap_json | no |
|
||
| `quality.cefr_check` | `textstat` Flesch-Kincaid → CEFR window | no |
|
||
| `quality.ai_detect` | GPTZero (`encoach_ai_detect.service`) | no |
|
||
| `quality.content_gate` | `encoach_quality_gate.gate.run()` | no |
|
||
| `course_plan.save` | `encoach.course.plan` create + `weeks` lines | **yes** |
|
||
| `course_plan.save_materials` | `encoach.course.plan.material` create | **yes** |
|
||
| `scoring.grade_writing` | `encoach_scoring.writing_examiner` | no |
|
||
| `scoring.grade_speaking` | `encoach_scoring.speaking_examiner` | no |
|
||
|
||
- **`services/agent_runtime.py`** — `AgentRuntime` compiles a `langgraph.graph.StateGraph` per agent. The compiled graph is cached per `(agent.key, agent.write_date)` so config edits invalidate the cache automatically. Four topologies are supported:
|
||
|
||
| `graph_type` | Nodes | Use case |
|
||
|---|---|---|
|
||
| `simple` | `prepare → llm → finalize` | One-shot grading / classification (e.g. `writing_grader`, `speaking_grader`). |
|
||
| `plan_review_revise` | `prepare → llm → quality_check → (revise → llm)* → finalize` | Generation that needs a quality gate + bounded revision loop (course plans, week materials, exam generation). |
|
||
| `rag` | `retrieve (resources.search) → prepare → llm → finalize` | Generation that must ground itself in approved library content first. |
|
||
| `react` | LangGraph `ToolNode` + agent loop with `tool_calls` | Conversational tool-using agents (LMS tutor, personalised exercise generator). |
|
||
|
||
The runtime emits a structured trace alongside the output: `{"output": ..., "tool_calls": [...], "retrieval_hits": [...], "revisions": [...], "quality_issues": [...], "model_used": ..., "ms": ..., "fallback_used": bool}`. The Test Runner UI renders that trace verbatim.
|
||
|
||
- **Fallback / resilience** — Each `llm` node catches `openai.RateLimitError` / `openai.APIError` / `openai.APIConnectionError` and retries once on `agent.fallback_model`. The graph state carries `_attempt` so retries don't loop forever.
|
||
|
||
#### Controllers (`controllers/agents_controller.py`)
|
||
|
||
```
|
||
GET /api/ai/agents list (admin)
|
||
GET /api/ai/agents/<key> one agent + bound tools (admin)
|
||
PUT /api/ai/agents/<key> update model/temp/graph/system_prompt/tools (admin)
|
||
POST /api/ai/agents/<key>/test run with sample input → returns trace
|
||
GET /api/ai/agents/<key>/tools tools currently bound
|
||
POST /api/ai/agents/<key>/tools/<key>/toggle bind/unbind one tool
|
||
GET /api/ai/tools tool registry (admin)
|
||
GET /api/ai/tools/<key> one tool descriptor
|
||
PUT /api/ai/tools/<key> edit description/schema/active (admin)
|
||
```
|
||
|
||
Every write route checks `base.group_system`; reads check JWT only.
|
||
|
||
#### Default seed (`data/agents_defaults.xml`, `noupdate=True`)
|
||
|
||
Seven default agents — one per product pillar — wired to the right tools out of the box:
|
||
|
||
| Key | Topology | Model / fallback | Temp | Tools bound | Used by |
|
||
|---|---|---|---|---|---|
|
||
| `course_planner` | plan_review_revise | gpt-4o / gpt-4o-mini | 0.4 | outcomes.fetch, resources.search, quality.cefr_check, course_plan.save | Smart Wizard, `/api/ai/course-plan` |
|
||
| `course_week_materials` | plan_review_revise | gpt-4o / gpt-4o-mini | 0.6 | outcomes.fetch, resources.search, quality.cefr_check, course_plan.save_materials | Week-N material generator |
|
||
| `exam_generator` | plan_review_revise | gpt-4o / gpt-4o-mini | 0.5 | resources.search, outcomes.fetch, rubric.fetch, quality.cefr_check | Exam generation pipeline |
|
||
| `exercise_generator` | react | gpt-4o-mini / gpt-4o | 0.7 | student.profile, resources.search, outcomes.fetch, quality.cefr_check | Personalised practice |
|
||
| `lms_tutor` | react | gpt-4o-mini / gpt-4o | 0.6 | resources.search, student.profile, outcomes.fetch | LMS chat |
|
||
| `writing_grader` | simple | gpt-4o / gpt-4o-mini | 0.2 | rubric.fetch, scoring.grade_writing | Writing submissions |
|
||
| `speaking_grader` | simple | gpt-4o / gpt-4o-mini | 0.2 | rubric.fetch, scoring.grade_speaking | Speaking submissions |
|
||
|
||
`__manifest__.py` adds `langgraph>=0.2.0` and `langchain-core>=0.3.0` to `external_dependencies`; `requirements.txt` mirrors the same pins.
|
||
|
||
A feature-flag system parameter is also seeded:
|
||
|
||
```
|
||
encoach_ai.use_langgraph_runtime = True
|
||
```
|
||
|
||
Flip it to `False` (Settings → Technical → System Parameters) to bypass LangGraph and use the legacy SDK path — useful for incident response.
|
||
|
||
#### Pipeline rewiring
|
||
|
||
`backend/custom_addons/encoach_ai_course/services/course_plan_pipeline.py` now consults the feature flag. When on, both `generate_plan` and `generate_week_materials` route through `AgentRuntime.run_agent("course_planner", …)` / `("course_week_materials", …)`. The legacy hand-rolled OpenAI path is kept as the else-branch so we can fall back instantly without redeploying.
|
||
|
||
### 22.3 Frontend artefacts
|
||
|
||
- **Types** — `frontend/src/types/aiAgent.ts` defines `AIAgent`, `AITool`, `AgentTestResult`, `GraphType`, etc.
|
||
- **Service** — `frontend/src/services/aiAgent.service.ts` wraps every `/api/ai/agents*` and `/api/ai/tools*` route with React Query-friendly helpers.
|
||
- **Page** — `frontend/src/pages/admin/AIPromptEditor.tsx` is now a tabbed shell:
|
||
- **Agents** (`AIAgentsPanel.tsx`) — card grid of every agent with badges for graph type / model, plus a config dialog: model, fallback, temperature slider, max tokens, response format, graph topology, max revisions, quality checks, system prompt textarea, tool toggles, and a built-in **Test Runner** (sample input → live trace: output, tool calls, retrieval hits, revisions, quality issues, ms, model used).
|
||
- **Tools** (`AIToolsPanel.tsx`) — tool registry table with category badges, mutates flag, schema viewer, edit description dialog. Read-only for the schema (it ships with the addon).
|
||
- **Prompts** — original prompt editor logic preserved as `AIPromptsPanel`; nothing was removed.
|
||
- **Sidebar** — `AdminLmsLayout.tsx` updated `nav.aiPrompts` → `nav.aiAgents` so the menu item now reads "AI Agents & Tools".
|
||
- **i18n** — `i18n/locales/{en,ar}.ts` extended with `aiAdmin`, `agents`, `tools` namespaces (≈80 keys each), plus `common.saving` / `common.disabled`. Arabic strings preserve technical product names ("LangGraph", "ReAct") in Latin script.
|
||
|
||
### 22.4 Verification
|
||
|
||
Confirmed end-to-end during the §23 test run:
|
||
|
||
- `simple` topology (`writing_grader`) — POST `/api/ai/agents/writing_grader/test` returned a structured score envelope in 3.3 s, `model_used=gpt-4o`, no fallback.
|
||
- `react` topology (`lms_tutor`) — same endpoint with a tutoring question executed **two real tool calls** (`student.profile`, `resources.search`), 13 s total, returned a CEFR-adapted reply and the tool trace.
|
||
- Graph cache invalidation — editing `temperature` from 0.6 → 0.4 on `lms_tutor` and re-running confirmed the next call recompiled and used the new value.
|
||
- Feature flag — flipping `encoach_ai.use_langgraph_runtime` to `False` and regenerating a course plan kept the API contract stable; flipping back restored the LangGraph trace.
|
||
|
||
### 22.5 What this unlocks
|
||
|
||
- **Per-feature model swap** — admin can move `course_planner` to `gpt-4o-mini` for a cost test without touching code.
|
||
- **Tool curation** — restricting `lms_tutor` to read-only tools is a single checkbox; mutating tools (yellow badge) are deliberately separate.
|
||
- **Quality gates** — flipping `quality_checks=quality.cefr_check,quality.content_gate` on `exam_generator` runs both gates before the response is accepted.
|
||
- **Future agents** — adding a new agent is a `<record>` in `agents_defaults.xml` (or a one-row INSERT). Adding a new tool is a `@register` decorator + a JSON schema; the UI picks it up automatically.
|
||
|
||
---
|
||
|
||
## 23. Full Demo Seed + 8-Role E2E Test (2026-04-25)
|
||
|
||
Once the LangGraph layer was in (§22), the remaining gap was that the demo dataset only exercised three product roles (admin, teacher, student). The product supports **seven `user_type` values** (`student`, `teacher`, `admin`, `corporate`, `mastercorporate`, `agent`, `developer`) plus a procedural eighth role (a teacher acting as approver). This pass fills every role with believable data and verifies every API surface end-to-end.
|
||
|
||
### 23.1 Scope
|
||
|
||
| Goal | Status |
|
||
|---|---|
|
||
| Every `user_type` represented by a demo account | DONE — see §3 |
|
||
| Active multi-stage approval workflow with one pending request | DONE |
|
||
| GE1-aligned 12-week B1 course plan (matches UTAS *General English 1 Fall AY25-26* outline) | DONE |
|
||
| Six full Week-1 teaching materials (reading / writing / listening / speaking / grammar / vocabulary) | DONE |
|
||
| Sample AI telemetry (`encoach.ai.log`) and `encoach.ai.feedback` for the three live agents | DONE |
|
||
| Read-only API smoke for all 8 roles | **46/46 PASS** |
|
||
| Mutation E2E for the full approval chain | **6/6 PASS** |
|
||
| LangGraph live round-trip during the run | **2/2 topologies verified** |
|
||
|
||
### 23.2 New scripts (workspace root, all idempotent)
|
||
|
||
| Script | Purpose |
|
||
|---|---|
|
||
| `seed_full_demo.py` | Adds 5 missing user types, activates a 2-stage exam-approval workflow with one pending request, creates a GE1 12-week B1 plan, populates Week 1 with 6 detailed materials, inserts sample `ai.log` + `ai.feedback` rows. Re-running is safe — every record is upserted by stable key (`xml_id` / login / unique pair). |
|
||
| `reset_demo_passwords.py` | Forces every demo user's password back to its canonical value via the Odoo ORM (`res.users.write({'password': ...})`). Use this any time test logins start failing — covers password drift caused by manual changes during interactive testing. |
|
||
| `e2e_full_scenario.py` | Read-only API smoke. Logs in as each of 8 roles and exercises the routes that role typically uses: profile, branding, courses, course plans, exams, attempts, AI agents, AI feedback, training, payments, reports. Prints a per-step PASS/FAIL line and a final summary. |
|
||
| `e2e_approval_chain.py` | Mutation E2E. Walks: approver logs in → lists pending requests → approves stage 1 → admin logs in → approves stage 2 (final) → linked `encoach.exam.custom` flips to `status='published'`. Verifies the underlying DB row at every step. |
|
||
|
||
### 23.3 Demo dataset snapshot (after running both seeders)
|
||
|
||
- **Users** — 12 total (see §3): 1 superadmin, 1 admin, 3 students, 3 teachers (one acting as approver), 1 corporate, 1 mastercorporate, 1 agent, 1 developer.
|
||
- **Approval workflow** — `Exam Approval Workflow` (active=True, 2 stages: *Coach Approval* → `approver@encoach.test`, *Admin Approval* → `admin@encoach.test`). One pending `encoach.approval.request` linked to an existing `encoach.exam.custom` row, sitting at stage 1.
|
||
- **Course plan** — `GE1 — General English 1 (B1)` linked to a new `op.course` (`code=GE1`, `cefr_level=b1`). 12 weekly rows pre-populated with theme + skill focus matching the UTAS outline.
|
||
- **Week 1 materials** — six rows on `encoach.course.plan.material` covering the exact outcomes the user pasted from the GE1 brief:
|
||
1. **Reading text + 5 comprehension questions** (~400 words at B1, scan + context-clue practice).
|
||
2. **Writing prompt** (people / places / activities, ≥150 words, paragraph plan).
|
||
3. **Listening script + 6 questions** (4-min dialogue, locally familiar topic).
|
||
4. **Speaking prompt** (describe present / past / future activity, useful-language chunks).
|
||
5. **Grammar mini-lesson** (Present Simple vs Present Continuous, rule + 3 examples + 5 practice items + answer key).
|
||
6. **Vocabulary list** (10 entries × {pos, B1 definition, example sentence}).
|
||
- **AI telemetry** — sample `encoach.ai.log` rows for `writing_grader`, `speaking_grader`, `lms_tutor` (`service='openai'`, `action`, `model_used`, token counts, input/output previews) and `encoach.ai.feedback` rows (`subject_type='other'`, mix of `rating='up'` / `'down'`).
|
||
|
||
### 23.4 Read-only smoke — `e2e_full_scenario.py` — 46/46 PASS
|
||
|
||
Endpoint coverage by role (every entry returned 2xx):
|
||
|
||
| Role | Login | Endpoints exercised | Result |
|
||
|---|---|---|---|
|
||
| superadmin | `admin` | `/api/login`, `/api/user`, `/api/ai/agents`, `/api/ai/tools`, `/api/courses`, `/api/exams`, `/api/reports/filters`, `/api/branding/1` | 8/8 |
|
||
| admin | `admin@encoach.test` | as superadmin + `/api/ai/feedback`, `/api/approvals/pending` | 10/10 |
|
||
| teacher | `khalid@encoach.test` | `/api/login`, `/api/user`, `/api/ai/course-plan`, `/api/courses`, `/api/exams`, `/api/rubrics`, `/api/exam-structures` | 7/7 |
|
||
| approver | `approver@encoach.test` | `/api/login`, `/api/user`, `/api/approvals/pending`, `/api/approvals/mine` | 4/4 |
|
||
| student | `sarah@encoach.test` | `/api/login`, `/api/user`, `/api/courses`, `/api/exam-assignments`, `/api/training/vocabulary`, `/api/training/grammar` | 6/6 |
|
||
| corporate | `corporate@encoach.test` | `/api/login`, `/api/user`, `/api/branding/1`, `/api/reports/stats-corporate`, `/api/payment-records` | 5/5 |
|
||
| mastercorporate | `master@encoach.test` | `/api/login`, `/api/user`, `/api/reports/student-performance` | 3/3 |
|
||
| agent | `agent@encoach.test` | `/api/login`, `/api/user`, `/api/codes`, `/api/packages` | 2/2 |
|
||
| developer | `dev@encoach.test` | `/api/login`, `/api/user` | 1/1 |
|
||
|
||
**Live LangGraph hit during the same run:** `POST /api/ai/agents/writing_grader/test` returned a fully scored envelope with `model_used=gpt-4o`, `ms≈3300`. The exact JSON is captured in `docs/ENCOACH_FULL_DEMO_QA_REPORT.md`.
|
||
|
||
### 23.5 Mutation E2E — `e2e_approval_chain.py` — 6/6 PASS
|
||
|
||
Steps walked, each verified against the database via `psql`:
|
||
|
||
1. **Approver login** — `approver@encoach.test / approver123` returns a JWT; `/api/user` confirms `user_type='teacher'`.
|
||
2. **Pending list** — `/api/approvals/pending` returns one request whose `current_stage_id.approver_id == approver`.
|
||
3. **Stage 1 approve** — `POST /api/approvals/<id>/approve` flips `current_stage_id` to *Admin Approval*; DB shows `state='in_progress'` (still active, advanced).
|
||
4. **Admin login** — `admin@encoach.test / admin123` returns a JWT.
|
||
5. **Final approve** — `POST /api/approvals/<id>/approve` flips `state='approved'`; the linked `encoach.exam.custom.status` flips to `'published'` via the post-approval hook.
|
||
6. **DB verification** — `select state from encoach_approval_request where id=<id>;` returns `approved`; `select status from encoach_exam_custom where id=<exam_id>;` returns `published`. Both confirmed live with `psql`.
|
||
|
||
### 23.6 Reproduction
|
||
|
||
```bash
|
||
# 1. Seed (idempotent — safe to re-run any time)
|
||
cd /Users/yamenahmad/projects2026/odoo/odoo19/odoo
|
||
../.conda-envs/odoo19/bin/python odoo-bin shell -c ../odoo.conf --no-http --stop-after-init < ../seed_full_demo.py
|
||
../.conda-envs/odoo19/bin/python odoo-bin shell -c ../odoo.conf --no-http --stop-after-init < ../reset_demo_passwords.py
|
||
|
||
# 2. Make sure Odoo + frontend are running (see §4), then:
|
||
cd /Users/yamenahmad/projects2026/odoo/odoo19
|
||
.conda-envs/odoo19/bin/python e2e_full_scenario.py
|
||
.conda-envs/odoo19/bin/python e2e_approval_chain.py
|
||
```
|
||
|
||
Both scripts are network-side only (they hit `http://localhost:8069/api/*`), so they can also be pointed at a staging VPS by exporting `BASE_URL=https://staging.encoach.com` first.
|
||
|
||
### 23.7 Files added
|
||
|
||
```
|
||
seed_full_demo.py
|
||
reset_demo_passwords.py
|
||
e2e_full_scenario.py
|
||
e2e_approval_chain.py
|
||
docs/ENCOACH_FULL_DEMO_QA_REPORT.md # full QA write-up: credentials, dataset snapshot,
|
||
# per-endpoint PASS/FAIL, mutation chain proof,
|
||
# LangGraph live-run output
|
||
```
|
||
|
||
### 23.8 Gotchas resolved during this pass
|
||
|
||
- `/api/me` does **not** exist on this build — the correct profile endpoint is `/api/user`. Updated tests accordingly.
|
||
- `khalid@encoach.test` was failing login because his password had drifted during earlier interactive testing. `reset_demo_passwords.py` now restores all canonical passwords idempotently.
|
||
- `encoach.ai.log` field names differ from a naive guess — the model uses `service`, `action`, `model_used`, `prompt_tokens`, `completion_tokens`, `total_tokens`, `input_preview`, `output_preview`. `encoach.ai.feedback` uses `subject_type`, `subject_id`, `rating in {'up','down'}` (NOT `'thumbs_up'`). The seeder now matches both definitions.
|
||
- The exam approval *post-approval* hook only fires when the **final** stage approves, so step 3 above advances the request without publishing the exam yet — that's correct behaviour, just not obvious from the API alone. The DB-side verification in step 6 is what makes it observable.
|
||
|
||
## 24. AI Course-Plans — RAG, Multi-modal Media, Assignments (2026-04-25)
|
||
|
||
§22 made LangGraph the runtime; §24 puts a real product on top of it. The user asked for an AI-driven **course-plan** experience modelled on the GE1 outline: detect deliverables up-front (e.g. "12 reading texts, 12 listening scripts, 24 audio narrations"), let admins drop reference files / URLs / pasted notes the AI must respect (RAG), generate multi-modal training material (images via DALL-E 3, voice via AWS Polly / ElevenLabs, slideshow video via local `ffmpeg`), then **assign** the finished plan to a class or to specific students. This section is the implementation log.
|
||
|
||
### 24.1 What this section delivers
|
||
|
||
| Phase | Capability | Where it lives |
|
||
|---|---|---|
|
||
| **A** | Reference sources (file/URL/text) → pgvector, scoped to plan | `encoach.course.plan.source` + `services/source_indexer.py` |
|
||
| **B** | Deliverables preview & live progress strip | `services/deliverables.py` + `/api/ai/course-plan/<id>/deliverables` |
|
||
| **C** | On-demand audio / image / video per material + bulk-per-week | `encoach.course.plan.media` + `services/media_service.py` |
|
||
| **D** | Assign a plan to an `op.batch` or specific `res.users` | `encoach.course.plan.assignment` |
|
||
| **E** | Student-side list + drilldown (read-only) | `/api/student/course-plans*` + `/student/course-plans` page |
|
||
| **i18n** | Full EN + AR strings for every new control | `frontend/src/i18n/locales/{en,ar}.ts` |
|
||
|
||
### 24.2 New / extended backend artefacts
|
||
|
||
```text
|
||
backend/custom_addons/encoach_ai_course/
|
||
├── models/
|
||
│ ├── course_plan_source.py ← NEW RAG ingestion row (file|url|text)
|
||
│ ├── course_plan_media.py ← NEW audio|image|video asset
|
||
│ └── course_plan_assignment.py ← NEW visibility unit (batch|students)
|
||
├── services/
|
||
│ ├── source_indexer.py ← NEW PDF/DOCX/HTML extraction → pgvector
|
||
│ ├── deliverables.py ← NEW per-week status + percent_ready
|
||
│ └── media_service.py ← NEW Polly/ElevenLabs/DALL-E/ffmpeg
|
||
└── controllers/course_plan.py ← +18 endpoints (sources, media, assignments, student-side)
|
||
```
|
||
|
||
Existing models updated:
|
||
|
||
* `encoach.course.plan` gains `source_ids`, `media_ids`, `assignment_ids`, `to_api_dict(include_media=...)`.
|
||
* `encoach.ai.tool.category` selection extends with `('media', 'Media generation')` so the new tools register cleanly.
|
||
* `encoach.embedding` queries gain a `plan_id` filter — `resources.search` is now plan-scoped when passed `plan_id`, so the agent sees only its own corpus.
|
||
|
||
New tools (registered in `encoach_ai/data/agents_defaults.xml`):
|
||
|
||
| Tool key | Provider | Notes |
|
||
|---|---|---|
|
||
| `media.synthesize_audio` | AWS Polly (default) / ElevenLabs | Used for `listening_script` + `speaking_prompt` |
|
||
| `media.generate_image` | OpenAI DALL-E 3 | Capped per plan via `encoach_ai_course.image_budget_per_plan` (default 60) |
|
||
| `media.compose_video` | local `ffmpeg` | Auto-creates audio + image first if missing |
|
||
|
||
New agent: **`course_media_director`** — orchestrates per-week media batches; reuses `course_week_materials`'s tools plus the three media tools above. The base `course_week_materials` agent now also has `media.synthesize_audio` + `media.generate_image` in its tool set so a single LangGraph turn can emit the text material *and* its narration/illustration.
|
||
|
||
### 24.3 New API endpoints (all JWT-protected unless noted)
|
||
|
||
```text
|
||
# Sources (Phase A)
|
||
POST /api/ai/course-plan/<id>/sources create URL or text source
|
||
POST /api/ai/course-plan/<id>/sources/upload multipart file upload
|
||
GET /api/ai/course-plan/<id>/sources list sources for plan
|
||
POST /api/ai/course-plan/sources/<sid>/reindex re-extract + re-embed
|
||
DELETE /api/ai/course-plan/sources/<sid> cascade-deletes embeddings
|
||
|
||
# Deliverables (Phase B)
|
||
GET /api/ai/course-plan/<id>/deliverables {summary, weeks: [...]}
|
||
|
||
# Media (Phase C)
|
||
GET /api/ai/course-plan/material/<mid>/media list media for material
|
||
POST /api/ai/course-plan/material/<mid>/media/audio generate audio
|
||
POST /api/ai/course-plan/material/<mid>/media/image generate image
|
||
POST /api/ai/course-plan/material/<mid>/media/video compose video
|
||
DELETE /api/ai/course-plan/media/<media_id> delete media + attachment
|
||
POST /api/ai/course-plan/<id>/week/<n>/media bulk-generate week (audio+image)
|
||
|
||
# Assignments (Phase D)
|
||
GET /api/ai/course-plan/<id>/assignments
|
||
POST /api/ai/course-plan/<id>/assignments
|
||
DELETE /api/ai/course-plan/assignments/<aid>
|
||
|
||
# Student-side (Phase E)
|
||
GET /api/student/course-plans list assigned plans
|
||
GET /api/student/course-plans/<id> read-only drilldown w/ media URLs
|
||
```
|
||
|
||
### 24.4 New / updated frontend surfaces
|
||
|
||
* **Wizard** (`pages/admin/wizards/CoursePlanWizard.tsx`): now 6 steps — Brief → Resources (source builder) → Deliverables preview → Multimedia toggles → Review → Generate. Sources are queued client-side and uploaded post-creation against the new plan id.
|
||
* **Detail page** (`pages/admin/AdminCoursePlanDetail.tsx`): now shows `DeliverablesStrip`, `SourcesCard`, `AssignmentsCard` (with `AssignDialog`), and per-material `MediaDrawer` (audio/image/video preview, generate, download, delete). Plus a "Generate week media" bulk action per week.
|
||
* **Student** (`pages/student/StudentCoursePlans.tsx`, `StudentCoursePlanDetail.tsx`): list + read-only drilldown, `<audio>` / `<img>` / `<video>` tiles fed from `/web/content/<attachment_id>`.
|
||
* **Sidebar**: new "My Course Plans" entry under student nav (`StudentLayout.tsx`).
|
||
* **i18n**: ~140 new keys in EN + AR across `coursePlan.wizard.*`, `coursePlan.sources.*`, `coursePlan.deliverables.*`, `coursePlan.media.*`, `coursePlan.assignments.*`, `coursePlan.student.*`, `nav.myCoursePlans`.
|
||
|
||
### 24.5 Smoke test (run via `odoo-bin shell`)
|
||
|
||
`smoke_course_plan.py` exercises every phase end-to-end:
|
||
|
||
| Phase | Result |
|
||
|---|---|
|
||
| A — index inline-text source | PASS · 1 chunk · 295 chars (uses local MiniLM embeddings) |
|
||
| B — `compute_deliverables` | PASS · 12 weeks · 5 ready / 1 generated / 0 planned |
|
||
| C — DALL-E 3 image | PASS · 753 KB PNG generated for a `reading_text` material |
|
||
| C — Polly audio | Gracefully marked `failed` with `"AWS credentials not configured — set in AI Settings"` (expected on dev box) |
|
||
| D — assignment | PASS · created `mode='students'` assignment for Sarah |
|
||
| E — visibility | PASS · `expand_user_ids()` returns Sarah · plan in student listing |
|
||
|
||
Run it any time:
|
||
|
||
```bash
|
||
.conda-envs/odoo19/bin/python odoo/odoo-bin shell \
|
||
-c odoo.conf -d encoach_v2 --no-http \
|
||
< smoke_course_plan.py
|
||
```
|
||
|
||
### 24.6 Operational notes
|
||
|
||
* **External providers are optional**. AWS Polly / ElevenLabs / OpenAI image keys live in **AI Settings** (`encoach.ai.settings`); when missing the corresponding media row is created with `status='failed'` and a clear error — the rest of the flow (planning, RAG, deliverables, assignment) still works.
|
||
* **`ffmpeg`** must be on `PATH` for video composition. If it's missing, the video media row is `failed` with a one-line install hint; nothing else breaks.
|
||
* **Image budget** per plan is the system parameter `encoach_ai_course.image_budget_per_plan` (default 60). Bump it via Settings → Technical → System Parameters.
|
||
* **Plan-scoped retrieval**. The `resources.search` tool now accepts a `plan_id`; the LLM is auto-given the plan id in the system prompt of `course_planner` and `course_week_materials`, so RAG only ever sees that plan's own sources.
|
||
* **Cascade behaviour**. Deleting a source unlinks its `encoach.embedding` rows; deleting a plan unlinks sources, media, assignments, and the underlying `ir.attachment`s.
|
||
|
||
### 24.7 Files added / changed in this pass
|
||
|
||
```text
|
||
backend/custom_addons/encoach_ai_course/models/course_plan_source.py + new
|
||
backend/custom_addons/encoach_ai_course/models/course_plan_media.py + new
|
||
backend/custom_addons/encoach_ai_course/models/course_plan_assignment.py + new
|
||
backend/custom_addons/encoach_ai_course/services/source_indexer.py + new
|
||
backend/custom_addons/encoach_ai_course/services/deliverables.py + new
|
||
backend/custom_addons/encoach_ai_course/services/media_service.py + new
|
||
backend/custom_addons/encoach_ai_course/controllers/course_plan.py ~ +480 LOC
|
||
backend/custom_addons/encoach_ai_course/security/ir.model.access.csv ~ access rights
|
||
backend/custom_addons/encoach_ai/models/ai_agent.py ~ TOOL_CATEGORIES += media
|
||
backend/custom_addons/encoach_ai/data/agents_defaults.xml ~ + 3 tools, + 1 agent
|
||
backend/custom_addons/encoach_vector/models/embedding.py ~ plan_id filter
|
||
frontend/src/types/coursePlan.ts ~ +4 interfaces
|
||
frontend/src/services/coursePlan.service.ts ~ +18 methods
|
||
frontend/src/pages/admin/wizards/CoursePlanWizard.tsx ~ +SourcesStep, MediaStep
|
||
frontend/src/pages/admin/AdminCoursePlanDetail.tsx ~ rewrite (Deliverables, Sources, Assignments, MediaDrawer)
|
||
frontend/src/pages/student/StudentCoursePlans.tsx + new
|
||
frontend/src/pages/student/StudentCoursePlanDetail.tsx + new
|
||
frontend/src/App.tsx ~ student routes
|
||
frontend/src/components/StudentLayout.tsx ~ My Course Plans link
|
||
frontend/src/i18n/locales/en.ts ~ +140 keys
|
||
frontend/src/i18n/locales/ar.ts ~ +140 keys
|
||
smoke_course_plan.py + new (E2E smoke test)
|
||
```
|
||
|
||
### 24.8 Gotchas resolved during this pass
|
||
|
||
* `encoach.ai.tool.category` is a strict `Selection`; adding the three media tools required adding `('media', 'Media generation')` to `TOOL_CATEGORIES` in `ai_agent.py`. Without that the data file refused to load and the whole `encoach_ai` module rolled back its upgrade — silently leaving the new course-plan tables uncreated until we restarted with `-u encoach_vector,encoach_ai,encoach_ai_course`.
|
||
* Odoo XML data loaders refuse two `<record id="..."/>` with the same id in the same file (no implicit merge). The `course_week_materials` agent had to be edited *in place* with the new media tools instead of being patched as a second record at the bottom.
|
||
* `pypdf` extracts most encrypted-but-public PDFs; if both `pypdf` and `PyPDF2` are missing the indexer marks the source `failed` with a clear pip hint rather than crashing.
|
||
* The student-side endpoint uses `expand_user_ids()` so a `mode='batch'` assignment auto-includes any future student rolled into that batch — no need to re-assign per intake.
|