# EnCoach Platform — Project Summary > Last updated: 2026-04-19 | **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-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 → `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/` 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 Admin/teacher users use password **`admin`**. Student passwords were reset to **`student123`** during end-to-end testing. | ID | Login | Name | Type | Password | |----|-------|------|------|----------| | 2 | `admin` | Administrator | superadmin | `admin` | | 5 | `admin@encoach.test` | Admin User | admin | `admin` | | 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 | `admin` | | 10 | `fatima@encoach.test` | Ms. Fatima | teacher | `admin` | > **Note:** Student passwords were bulk-reset to `student123` via a direct `psycopg2` + `passlib` script during testing. If a student can't login, reset their password in psql using `passlib.context.CryptContext(['pbkdf2_sha512'])`. ### Login API ```bash # Admin login curl -X POST http://localhost:8069/api/login \ -H "Content-Type: application/json" \ -d '{"email":"admin","password":"admin"}' # Student login curl -X POST http://localhost:8069/api/login \ -H "Content-Type: application/json" \ -d '{"email":"sarah@encoach.test","password":"student123"}' ``` Returns `{ token, user, permissions }`. The `token` (JWT) is used as `Authorization: Bearer ` 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 either a secondary mirror or legacy. | Remote | URL | Role | |--------|-----|------| | **`backend-v4`** | `https://git.albousalh.com/devops/encoach_backend_v4.git` | **Canonical backend** — branch `main`. VPS deploy target. | | **`frontend-v4`** | `https://git.albousalh.com/devops/encoach_frontend_v4.git` | **Canonical frontend** — branch `main`. VPS deploy target. | | `origin` | `https://git.albousalh.com/devops/encoach_backend_new_v2.git` | Secondary monorepo mirror (branch `v4`). Handy for a full-tree backup but **not** the source of truth. | | `ip-origin` | `https://5.189.151.117/devops/encoach_backend_new_v2.git` | IP fallback for `origin` 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. | > The remote names `backend-v4` / `frontend-v4` pre-date this reorg; conceptually > treat them as `origin-backend` / `origin-frontend`. Feel free to add aliases > with `git remote rename` if you prefer different names. ### 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 clean workspace synced with canonical git checkout v4 git pull origin v4 # optional: keep monorepo mirror in sync # 2. Create a feature branch git checkout -b feature/ # 3. Make changes, commit small and often git add git commit -m "feat(): " # 4. Merge back onto v4 (or open a PR if branch protection is enabled) git checkout v4 git merge --no-ff feature/ # 5. Publish to the CANONICAL repos (mandatory — see §6.4) # This is what the team lead / CI will see. # 6. (Optional) push the monorepo mirror for a full-tree backup git push origin 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 backend-v4 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 frontend-v4 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 backend-v4 frontend-v4 git log --oneline backend-v4/main..split-backend-v4 # what we're adding git log --oneline split-backend-v4..backend-v4/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//generate` | AI-generate content for a module (reading/listening/writing/speaking) | | POST | `/api/exam/generation/submit` | Submit generated exam, create questions | | POST | `/api/exam//generate/save` | Save generated content | ### Exam Session (`encoach_exam_template`) | Method | Route | Description | |--------|-------|-------------| | GET | `/api/exam//session` | Get exam session (creates attempt if needed) | | POST | `/api/exam//autosave` | Autosave student answers | | POST | `/api/exam//submit` | Submit exam for scoring | | GET | `/api/exam//status` | Get attempt status | | GET | `/api/exam//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/` | 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//approve` | Approve request | | POST | `/api/approval-requests//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/` | Get / update / delete a single course | | GET/POST | `/api/students` | List students / create student | | GET/PUT/DELETE | `/api/students/` | Get / update / delete a student | | GET | `/api/student/my-courses` | Enrolled courses for logged-in student (with progress) | | POST | `/api/students//enroll` | Enroll a student in one or more courses | | POST | `/api/courses//enroll` | Bulk-enroll students in a course | | GET/POST | `/api/teachers` | List / create teachers | | GET/PUT/DELETE | `/api/teachers/` | Get / update / delete a teacher | | GET/POST | `/api/batches` | List / create batches | | GET/PUT/DELETE | `/api/batches/` | Get / update / delete a batch | ### LMS Courseware (`encoach_lms_api` — `courseware.py`) | Method | Route | Description | |--------|-------|-------------| | GET/POST | `/api/courses//chapters` | List / create chapters for a course | | GET/PUT/DELETE | `/api/chapters/` | Get / update / delete a chapter | | POST | `/api/chapters//materials` | Upload material (JSON or FormData) | | GET/PUT/DELETE | `/api/materials/` | Get / update / delete a material | | GET | `/api/materials//download` | Download material file | | GET | `/api/materials//content` | Serve material file inline (for iframe embed, accepts ?token= param) | | POST | `/api/materials//view` | Mark material as viewed | | POST | `/api/chapters//progress/complete` | Mark chapter as completed | | GET | `/api/courses//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/` | Get / update / delete year (DELETE cascades terms) | | POST | `/api/academic-years//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/` | Term CRUD | | GET / POST | `/api/departments` | List / create departments | | GET / PATCH / DELETE | `/api/departments/` | Department CRUD | **Admissions** | Method | Route | Description | |--------|-------|-------------| | GET / POST | `/api/admission-registers` | List / create admission registers | | GET / PATCH / DELETE | `/api/admission-registers/` | Register CRUD (DELETE supported) | | POST | `/api/admission-registers//confirm` | Confirm register | | GET / POST | `/api/admissions` | List / create admissions (application_date **must** fall inside register window) | | GET / PATCH / DELETE | `/api/admissions/` | 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/` | Session CRUD | | POST | `/api/inst-exam-sessions//schedule` | Schedule session | | GET / POST | `/api/result-templates` | List / create result templates | | GET / PATCH / DELETE | `/api/result-templates/` | Template CRUD (DELETE cascades `op.marksheet.register`) | | POST | `/api/result-templates//generate` (alias: `/generate-marksheets`) | Generate marksheet registers (tries `generate_result` then `action_generate_marksheet`) | | GET | `/api/marksheets` | List marksheet registers | | POST | `/api/marksheets//validate` | Validate marksheet | **Facilities, Assets, Activities** | Method | Route | Description | |--------|-------|-------------| | GET / POST | `/api/facilities` | List / create facilities | | GET / PATCH / DELETE | `/api/facilities/` | Facility CRUD | | GET / POST | `/api/assets` | List / create assets (backed by custom `encoach.asset` model) | | GET / PATCH / DELETE | `/api/assets/` | Asset CRUD | | GET / POST / PATCH / DELETE | `/api/activity-types` + `/` | Activity-type CRUD | | GET / POST / PATCH / DELETE | `/api/activities` + `/` | 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/` | Media CRUD — detail endpoint returns `author_names` + `author_ids` | | GET / POST | `/api/lessons` | List / create lessons | | GET / PATCH / DELETE | `/api/lessons/` | Lesson CRUD — detail endpoint preserves `course_id` / `batch_id` on edits | | GET / POST / DELETE | `/api/grading-assignments` + `/` | Grading assignment CRUD (auto-resolves `assignment_type` and default `faculty_id`) | | GET | `/api/gradebooks` + `/api/gradebook-lines?gradebook_id=` | Gradebook drill-down | | GET | `/api/student-progress` + `/` | Student progress list/detail | | GET / POST | `/api/student-leaves` | List / create leave requests | | POST | `/api/student-leaves//approve` | Approve leave | | DELETE | `/api/student-leaves/` | Delete leave | | GET / POST | `/api/fees-plans` | List / create fee plans | | POST | `/api/fees-plans//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//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//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//assign or /api/exam-schedules 5. Student → /student/my-exams → clicks exam → /student/exam/ ├── GET /api/exam//session → loads sections, questions, passage_text ├── POST /api/exam//autosave → saves answers periodically └── POST /api/exam//submit → auto-scores objective questions 6. Student → /student/exam//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//materials (JSON or FormData) 3. Admin → /admin/courses → "Enroll Students" ├── Bulk-enroll students via POST /api/courses//enroll └── Or enroll individually via POST /api/students//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//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//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 = '' 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 --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//roles` | ✅ | | LMS Core | `/api/courses`, `/api/students`, `/api/teachers`, `/api/batches`, `/api/batches//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//chapters`, `/api/courses//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/` 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/` - `DELETE /api/admissions/` - `DELETE /api/result-templates/` (cascades `op.marksheet.register` rows created by `generate_result`) - `DELETE /api/student-leaves/` - `DELETE /api/academic-years/` 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/` — exposes full lesson incl. `course_id`, `batch_id` so EDIT can verify those are preserved. - `GET /api/library/media/` — 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/` 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//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/`, `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/` | `GET` | Fetch a single item with the caller's progress merged in. | | `/api/training/vocabulary/` | `PATCH` | Update any mutable field. | | `/api/training/vocabulary/` | `DELETE` | Delete (cascades progress). | | `/api/training/vocabulary//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///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.