Files
full_encoach_platform/seed_demo.py
Yamen Ahmad 98b9837a54 feat: institutional + support + training admin sections (backend + frontend)
Ship three fully-wired admin areas end-to-end with APIs, seeds, tests and docs.

Backend (new `encoach_lms_api` addon + existing addons):
- Institutional: academic years/terms, departments, admission registers & admissions,
  courses/batches, lessons, fees (terms + student fees + invoicing with income-account
  auto-wiring), gradebook (assignments/grades), library, facilities (encoach.asset),
  student leave, result templates + marksheets (incl. delete-with-cascade).
- Support: `encoach.ticket` model + CRUD/assignee routes; payment records derived
  from `op.student.fees.details` and `account.move`; platform settings backed by
  `encoach.code` and `ir.config_parameter` (packages + grading config).
- Training: `encoach.vocab.item` + `encoach.grammar.rule` (plus progress models)
  with CRUD, pagination, search/level filters, and upsert-style progress endpoints.
  Odoo 19 compatibility: `_sql_constraints` replaced with `@api.constrains`;
  `ValidationError`/`UserError` mapped to HTTP 400.

Frontend:
- Rewire institutional admin pages (Academic Year Manager, Admissions, Courses,
  Lessons, Fees, Gradebook, Library, Facilities, Student Leave, Marksheets,
  Taxonomy, Resources) to real APIs with React Query invalidation and dialogs.
- New typed services: `payments.service.ts`, `platformSettings.service.ts`,
  `training.service.ts`. Updated `fees/gradebook/lms/courseware/taxonomy/
  resources/student-progress/generation` services + related types.
- Rewrite `VocabularyPage`, `GrammarPage`, `PaymentRecordPage`, `SettingsPage`,
  `TicketsPage` to consume live data with search/filter/progress/CRUD flows.
- New shared components: `TaxonomyCascade`, `MaterialViewer`, `teacher/TeacherLibrary`.
- Favicons/branding assets and misc. UX polish across teacher/student pages.

Tooling & QA:
- Seeders: `seed_demo.py`, `seed_demo_data.py`, `seed_institutional.py` (idempotent,
  covers institutional + support + training fixtures incl. income-account wiring).
- API write-flow test suites: `test_write_flows.py` (institutional),
  `test_support_flows.py` (support), `test_training_flows.py` (training),
  `test_ai_full.py`. All suites pass end-to-end.
- Docs: add `docs/PROJECT_SUMMARY.md` with per-section scope, artifacts and QA.
- `.gitignore`: ignore `pgdata_bak_*/`, `frontend/.vite/`, `frontend/dist/`,
  `frontend/node_modules/`.

Made-with: Cursor
2026-04-19 03:13:23 +04:00

567 lines
30 KiB
Python

#!/usr/bin/env python3
"""
Seed comprehensive demo data for EnCoach.
Run inside Odoo shell:
python3 odoo/odoo-bin shell -c odoo.conf -d encoach_v2 < seed_demo.py
"""
import json, time
# ── 0. JWT secret ─────────────────────────────────────────────────────────
env['ir.config_parameter'].sudo().set_param('encoach.jwt_secret', 'demo-secret-key-for-testing-only')
env['ir.config_parameter'].sudo().set_param('encoach_ai.ai_enabled', 'True')
env['ir.config_parameter'].sudo().set_param('encoach_ai.openai_model', 'gpt-4o')
env.cr.commit()
print("✓ JWT secret + AI config set")
# ── 1. Entity ─────────────────────────────────────────────────────────────
Entity = env['encoach.entity']
entity = Entity.search([('code', '=', 'DEMO_ACADEMY')], limit=1)
if not entity:
entity = Entity.create({
'name': 'EnCoach Demo Academy',
'code': 'DEMO_ACADEMY',
'type': 'university',
'primary_color': '#4F46E5',
'secondary_color': '#7C3AED',
})
env.cr.commit()
print(f"✓ Entity: {entity.name} (id={entity.id})")
# ── 2. Users ──────────────────────────────────────────────────────────────
Users = env['res.users']
demo_users = [
{'name': 'Admin User', 'login': 'admin@encoach.test', 'password': 'admin123', 'user_type': 'admin'},
{'name': 'Sarah Ahmed', 'login': 'sarah@encoach.test', 'password': 'student123', 'user_type': 'student'},
{'name': 'Omar Khan', 'login': 'omar@encoach.test', 'password': 'student123', 'user_type': 'student'},
{'name': 'Layla Nasser', 'login': 'layla@encoach.test', 'password': 'student123', 'user_type': 'student'},
{'name': 'Dr. Khalid', 'login': 'khalid@encoach.test','password': 'teacher123', 'user_type': 'teacher'},
{'name': 'Ms. Fatima', 'login': 'fatima@encoach.test','password': 'teacher123', 'user_type': 'teacher'},
]
created_users = {}
for u in demo_users:
existing = Users.search([('login', '=', u['login'])], limit=1)
if existing:
created_users[u['login']] = existing
continue
user = Users.with_context(no_reset_password=True).create({
'name': u['name'],
'login': u['login'],
'password': u['password'],
'email': u['login'],
'user_type': u['user_type'],
'account_status': 'activated',
'is_verified': True,
'entity_ids': [(4, entity.id)],
})
created_users[u['login']] = user
env.cr.commit()
admin = created_users['admin@encoach.test']
sarah = created_users['sarah@encoach.test']
omar = created_users['omar@encoach.test']
layla = created_users['layla@encoach.test']
khalid = created_users['khalid@encoach.test']
fatima = created_users['fatima@encoach.test']
print(f"✓ Users created: {[u.name for u in created_users.values()]}")
# ── 3. Link students to op.student / faculty ──────────────────────────────
OpStudent = env['op.student']
OpFaculty = env['op.faculty']
OpCourse = env['op.course']
OpBatch = env['op.batch']
# Create a placeholder course first (op.student requires course_id)
placeholder_course = OpCourse.search([('name', '=', 'IELTS Preparation B2')], limit=1)
if not placeholder_course:
placeholder_course = OpCourse.create({'name': 'IELTS Preparation B2', 'code': 'IELTS-B2'})
env.cr.commit()
student_genders = {'sarah@encoach.test': 'f', 'omar@encoach.test': 'm', 'layla@encoach.test': 'f'}
for student_user in [sarah, omar, layla]:
if not student_user.op_student_id:
stud = OpStudent.search([('user_id', '=', student_user.id)], limit=1)
if not stud:
parts = student_user.name.split(' ', 1)
stud = OpStudent.create({
'first_name': parts[0],
'last_name': parts[1] if len(parts) > 1 else parts[0],
'user_id': student_user.id,
'gender': student_genders.get(student_user.login, 'm'),
'birth_date': '2000-05-15',
})
student_user.write({'op_student_id': stud.id})
# Link student to course via op.student.course
OpStudentCourse = env['op.student.course']
stud_rec = student_user.op_student_id
if stud_rec and not OpStudentCourse.search([('student_id', '=', stud_rec.id), ('course_id', '=', placeholder_course.id)], limit=1):
OpStudentCourse.create({'student_id': stud_rec.id, 'course_id': placeholder_course.id})
teacher_genders = {'khalid@encoach.test': 'male', 'fatima@encoach.test': 'female'}
for teacher_user in [khalid, fatima]:
if not teacher_user.op_faculty_id:
fac = OpFaculty.search([('user_id', '=', teacher_user.id)], limit=1)
if not fac:
parts = teacher_user.name.split(' ', 1)
fac = OpFaculty.create({
'first_name': parts[0],
'last_name': parts[1] if len(parts) > 1 else parts[0],
'user_id': teacher_user.id,
'gender': teacher_genders.get(teacher_user.login, 'male'),
'birth_date': '1980-01-15',
})
teacher_user.write({'op_faculty_id': fac.id})
env.cr.commit()
print("✓ op.student / op.faculty linked")
# ── 4. Taxonomy ───────────────────────────────────────────────────────────
Subject = env['encoach.subject']
Domain = env['encoach.domain']
Topic = env['encoach.topic']
LO = env['encoach.learning.objective']
subj_eng = Subject.search([('code', '=', 'ENG')], limit=1)
if not subj_eng:
subj_eng = Subject.create({'name': 'English Language', 'code': 'ENG'})
subj_math = Subject.search([('code', '=', 'MATH')], limit=1)
if not subj_math:
subj_math = Subject.create({'name': 'Mathematics', 'code': 'MATH'})
domains_data = [
{'name': 'Reading', 'subject_id': subj_eng.id},
{'name': 'Writing', 'subject_id': subj_eng.id},
{'name': 'Speaking', 'subject_id': subj_eng.id},
{'name': 'Listening', 'subject_id': subj_eng.id},
{'name': 'Grammar', 'subject_id': subj_eng.id},
{'name': 'Vocabulary','subject_id': subj_eng.id},
]
domains = {}
for d in domains_data:
dom = Domain.search([('name', '=', d['name']), ('subject_id', '=', d['subject_id'])], limit=1)
if not dom:
dom = Domain.create(d)
domains[d['name']] = dom
topics_data = [
('Reading', ['Main Idea', 'Detail Finding', 'Inference', 'Vocabulary in Context']),
('Writing', ['Task Achievement', 'Coherence & Cohesion', 'Lexical Resource', 'Grammar Range']),
('Speaking', ['Fluency', 'Pronunciation', 'Lexical Resource (Speaking)', 'Grammar (Speaking)']),
('Listening', ['Gap Fill', 'Multiple Choice', 'Matching', 'Map Labelling']),
('Grammar', ['Tenses', 'Articles', 'Conditionals', 'Passive Voice']),
('Vocabulary',['Academic Word List', 'Collocations', 'Idioms', 'Phrasal Verbs']),
]
all_topics = {}
for domain_name, topic_names in topics_data:
for tn in topic_names:
t = Topic.search([('name', '=', tn), ('domain_id', '=', domains[domain_name].id)], limit=1)
if not t:
t = Topic.create({'name': tn, 'domain_id': domains[domain_name].id})
all_topics[tn] = t
for topic_name, topic_rec in list(all_topics.items())[:8]:
existing_lo = LO.search([('topic_id', '=', topic_rec.id)], limit=1)
if not existing_lo:
LO.create({
'name': f'Demonstrate mastery of {topic_name}',
'topic_id': topic_rec.id,
'cefr_level': 'b1',
})
env.cr.commit()
print(f"✓ Taxonomy: {len(domains)} domains, {len(all_topics)} topics")
# ── 5. Rubrics ────────────────────────────────────────────────────────────
Rubric = env['encoach.rubric']
rubrics = {}
for skill in ['writing', 'speaking']:
r = Rubric.search([('skill', '=', skill), ('exam_type', '=', 'academic')], limit=1)
if not r:
r = Rubric.create({
'name': f'IELTS {skill.title()} Academic Rubric',
'skill': skill,
'exam_type': 'academic',
'criteria': json.dumps({
'band_9': 'Expert user — fully operational command',
'band_7': 'Good user — operational command with occasional inaccuracies',
'band_5': 'Modest user — partial command, likely to make many mistakes',
}),
})
rubrics[skill] = r
env.cr.commit()
print("✓ Rubrics created")
# ── 6. Passages ───────────────────────────────────────────────────────────
Passage = env['encoach.passage']
passages = []
passage_data = [
{
'exam_type': 'academic', 'section_num': 1, 'topic_category': 'Science',
'body_text': 'The discovery of penicillin by Alexander Fleming in 1928 revolutionised modern medicine. Fleming noticed that a mould called Penicillium notatum had contaminated one of his petri dishes and was killing the surrounding bacteria. This accidental discovery led to the development of antibiotics, which have since saved millions of lives worldwide. The mass production of penicillin began during World War II, when it was used to treat infected wounds of soldiers. Today, antibiotics remain one of the most important tools in medicine, though concerns about antibiotic resistance continue to grow.',
'difficulty': 'medium', 'status': 'active', 'approved': True,
},
{
'exam_type': 'academic', 'section_num': 2, 'topic_category': 'Environment',
'body_text': 'Climate change represents one of the most significant challenges facing humanity in the 21st century. Rising global temperatures are causing widespread environmental disruption, including melting polar ice caps, rising sea levels, and increasingly severe weather events. Scientists have reached a broad consensus that human activities, particularly the burning of fossil fuels, are the primary driver of these changes. International agreements such as the Paris Accord aim to limit global warming to 1.5 degrees Celsius above pre-industrial levels, but achieving this target requires unprecedented cooperation and transformation of energy systems worldwide.',
'difficulty': 'hard', 'status': 'active', 'approved': True,
},
]
for pd in passage_data:
p = Passage.search([('topic_category', '=', pd['topic_category']), ('section_num', '=', pd['section_num'])], limit=1)
if not p:
p = Passage.create(pd)
passages.append(p)
env.cr.commit()
print(f"✓ Passages: {len(passages)}")
# ── 7. Questions ──────────────────────────────────────────────────────────
Question = env['encoach.question']
questions = []
q_data = [
{'skill': 'reading', 'question_type': 'mcq', 'stem': 'What did Fleming discover in 1928?', 'options': json.dumps(['Penicillin', 'Aspirin', 'Insulin', 'Morphine']), 'correct_answer': 'Penicillin', 'marks': 1.0, 'difficulty': 'easy', 'status': 'active', 'source_type': 'passage', 'subject_id': subj_eng.id},
{'skill': 'reading', 'question_type': 'tfng', 'stem': 'Penicillin was mass-produced before World War II.', 'correct_answer': 'False', 'marks': 1.0, 'difficulty': 'medium', 'status': 'active', 'source_type': 'passage', 'subject_id': subj_eng.id},
{'skill': 'reading', 'question_type': 'mcq', 'stem': 'What is the primary driver of climate change according to scientists?', 'options': json.dumps(['Solar activity', 'Volcanic eruptions', 'Human activities', 'Natural cycles']), 'correct_answer': 'Human activities', 'marks': 1.0, 'difficulty': 'medium', 'status': 'active', 'source_type': 'passage', 'subject_id': subj_eng.id},
{'skill': 'reading', 'question_type': 'gap_fill', 'stem': 'The Paris Accord aims to limit global warming to ___ degrees Celsius.', 'correct_answer': '1.5', 'marks': 1.0, 'difficulty': 'medium', 'status': 'active', 'source_type': 'passage', 'subject_id': subj_eng.id},
{'skill': 'listening', 'question_type': 'mcq', 'stem': 'According to the speaker, what was the main outcome of the meeting?', 'options': json.dumps(['A new policy', 'Budget cuts', 'Staff changes', 'No decision']), 'correct_answer': 'A new policy', 'marks': 1.0, 'difficulty': 'easy', 'status': 'active', 'subject_id': subj_eng.id},
{'skill': 'listening', 'question_type': 'gap_fill', 'stem': 'The lecture will take place in Room ___.', 'correct_answer': '204', 'marks': 1.0, 'difficulty': 'easy', 'status': 'active', 'subject_id': subj_eng.id},
{'skill': 'writing', 'question_type': 'short_answer', 'stem': 'Some people believe that technology has made our lives more complex rather than simpler. To what extent do you agree or disagree?', 'correct_answer': '', 'marks': 9.0, 'difficulty': 'hard', 'status': 'active', 'subject_id': subj_eng.id},
{'skill': 'grammar', 'question_type': 'mcq', 'stem': 'She ___ to the store yesterday.', 'options': json.dumps(['go', 'goes', 'went', 'going']), 'correct_answer': 'went', 'marks': 1.0, 'difficulty': 'easy', 'status': 'active', 'subject_id': subj_eng.id},
{'skill': 'vocabulary', 'question_type': 'mcq', 'stem': 'Choose the synonym of "ubiquitous":', 'options': json.dumps(['Rare', 'Omnipresent', 'Hidden', 'Temporary']), 'correct_answer': 'Omnipresent', 'marks': 1.0, 'difficulty': 'hard', 'status': 'active', 'subject_id': subj_eng.id},
{'skill': 'grammar', 'question_type': 'mcq', 'stem': 'If I ___ rich, I would travel the world.', 'options': json.dumps(['am', 'was', 'were', 'be']), 'correct_answer': 'were', 'marks': 1.0, 'difficulty': 'medium', 'status': 'active', 'subject_id': subj_eng.id},
]
for qd in q_data:
q = Question.search([('stem', '=', qd['stem'])], limit=1)
if not q:
q = Question.create(qd)
questions.append(q)
if passages:
for q in questions[:2]:
q.write({'source_id': passages[0].id})
for q in questions[2:4]:
q.write({'source_id': passages[1].id})
env.cr.commit()
print(f"✓ Questions: {len(questions)}")
# ── 8. Exam template + Custom exam ────────────────────────────────────────
Template = env['encoach.exam.template']
ExamCustom = env['encoach.exam.custom']
Section = env['encoach.exam.custom.section']
tmpl = Template.search([('code', '=', 'IELTS_ACADEMIC')], limit=1)
if not tmpl:
tmpl = Template.create({
'name': 'IELTS Academic',
'code': 'IELTS_ACADEMIC',
'type': 'international',
'subject_id': subj_eng.id,
'total_time_min': 170,
'pass_threshold': 5.0,
})
exam = ExamCustom.search([('title', '=', 'IELTS Mock Q2 2026')], limit=1)
if not exam:
exam = ExamCustom.create({
'title': 'IELTS Mock Q2 2026',
'template_id': tmpl.id,
'subject_id': subj_eng.id,
'entity_id': entity.id,
'teacher_id': khalid.id,
'total_time_min': 170,
'pass_threshold': 5.0,
'status': 'published',
})
reading_qs = [q.id for q in questions if q.skill == 'reading']
listening_qs = [q.id for q in questions if q.skill == 'listening']
writing_qs = [q.id for q in questions if q.skill == 'writing']
grammar_qs = [q.id for q in questions if q.skill == 'grammar']
sections_data = [
{'exam_id': exam.id, 'title': 'Reading', 'skill': 'reading', 'question_count': len(reading_qs), 'time_limit_min': 60, 'sequence': 1, 'question_ids': [(6, 0, reading_qs)]},
{'exam_id': exam.id, 'title': 'Listening', 'skill': 'listening', 'question_count': len(listening_qs), 'time_limit_min': 30, 'sequence': 2, 'question_ids': [(6, 0, listening_qs)]},
{'exam_id': exam.id, 'title': 'Writing', 'skill': 'writing', 'question_count': len(writing_qs), 'time_limit_min': 60, 'sequence': 3, 'question_ids': [(6, 0, writing_qs)]},
{'exam_id': exam.id, 'title': 'Grammar & Vocabulary', 'skill': 'grammar', 'question_count': len(grammar_qs), 'time_limit_min': 20, 'sequence': 4, 'question_ids': [(6, 0, grammar_qs)]},
]
for sd in sections_data:
sec = Section.search([('exam_id', '=', exam.id), ('title', '=', sd['title'])], limit=1)
if not sec:
Section.create(sd)
env.cr.commit()
print(f"✓ Exam: {exam.title} with sections")
# ── 9. Course + modules + resources ───────────────────────────────────────
Resource = env['encoach.resource']
resources = []
resource_data = [
{'name': 'IELTS Reading Strategies Guide', 'type': 'pdf', 'subject_id': subj_eng.id, 'difficulty': 'intermediate', 'duration_minutes': 45, 'cefr_level': 'b2', 'url': 'https://example.com/reading-guide.pdf'},
{'name': 'Academic Writing Masterclass', 'type': 'video', 'subject_id': subj_eng.id, 'difficulty': 'advanced', 'duration_minutes': 60, 'cefr_level': 'c1', 'url': 'https://example.com/writing-masterclass.mp4'},
{'name': 'Speaking Practice: Part 2 Topics', 'type': 'interactive', 'subject_id': subj_eng.id, 'difficulty': 'intermediate', 'duration_minutes': 30, 'cefr_level': 'b1', 'url': 'https://example.com/speaking-practice'},
{'name': 'Listening: Note Completion Drills', 'type': 'interactive', 'subject_id': subj_eng.id, 'difficulty': 'beginner', 'duration_minutes': 25, 'cefr_level': 'a2', 'url': 'https://example.com/listening-drills'},
{'name': 'Grammar Fundamentals: Tenses', 'type': 'document', 'subject_id': subj_eng.id, 'difficulty': 'beginner', 'duration_minutes': 40, 'cefr_level': 'a2', 'url': 'https://example.com/grammar-tenses.pdf'},
{'name': 'Advanced Vocabulary for IELTS', 'type': 'pdf', 'subject_id': subj_eng.id, 'difficulty': 'advanced', 'duration_minutes': 50, 'cefr_level': 'c1', 'url': 'https://example.com/vocab-advanced.pdf'},
]
for rd in resource_data:
res = Resource.search([('name', '=', rd['name'])], limit=1)
if not res:
res = Resource.create(rd)
resources.append(res)
env.cr.commit()
course = placeholder_course
course.write({
'entity_id': entity.id,
'generation_source': 'manual',
'target_band': 7.0,
'study_hours_week': 10,
})
CourseSection = env['encoach.course.section']
CourseModule = env['encoach.course.module']
sections_course = [
('Reading Skills', 'reading', [resources[0]]),
('Writing Skills', 'writing', [resources[1]]),
('Speaking Skills', 'speaking', [resources[2]]),
('Listening Skills', 'listening', [resources[3]]),
]
seq = 1
for sec_name, sec_skill, sec_resources in sections_course:
cs = CourseSection.search([('name', '=', sec_name), ('course_id', '=', course.id)], limit=1)
if not cs:
cs = CourseSection.create({'name': sec_name, 'course_id': course.id, 'skill': sec_skill, 'sequence': seq})
cm = CourseModule.search([('name', '=', f'{sec_name} Module 1'), ('course_id', '=', course.id)], limit=1)
if not cm:
CourseModule.create({
'name': f'{sec_name} Module 1',
'course_id': course.id,
'section_id': cs.id,
'cefr_target': 'b2',
'status': 'available' if seq == 1 else 'locked',
'sequence': seq,
'resource_ids': [(6, 0, [r.id for r in sec_resources])],
})
seq += 1
env.cr.commit()
print(f"✓ Course: {course.name} with {seq-1} modules")
# ── 10. Batch + enrol students ────────────────────────────────────────────
batch = OpBatch.search([('name', '=', 'IELTS 2026 Spring')], limit=1)
if not batch:
batch = OpBatch.create({
'name': 'IELTS 2026 Spring',
'code': 'IELTS-SPR-2026',
'course_id': course.id,
'start_date': '2026-03-01',
'end_date': '2026-06-30',
})
env.cr.commit()
print(f"✓ Batch: {batch.name}")
# ── 11. Exam assignments ─────────────────────────────────────────────────
Assignment = env['encoach.exam.assignment']
for student in [sarah, omar, layla]:
asn = Assignment.search([('exam_id', '=', exam.id), ('student_id', '=', student.id)], limit=1)
if not asn:
Assignment.create({
'exam_id': exam.id,
'student_id': student.id,
'batch_id': batch.id,
'status': 'assigned',
})
env.cr.commit()
print("✓ Exam assignments created")
# ── 12. Student attempts + answers + scores ───────────────────────────────
Attempt = env['encoach.student.attempt']
Answer = env['encoach.student.answer']
Score = env['encoach.score']
import random
random.seed(42)
student_profiles = [
(sarah, {'listening': 6.0, 'reading': 6.5, 'writing': 5.5, 'speaking': 6.0, 'overall': 6.0, 'cefr': 'b2'}),
(omar, {'listening': 5.5, 'reading': 5.0, 'writing': 5.0, 'speaking': 5.5, 'overall': 5.25, 'cefr': 'b1'}),
(layla, {'listening': 7.0, 'reading': 7.5, 'writing': 6.5, 'speaking': 7.0, 'overall': 7.0, 'cefr': 'c1'}),
]
for student, bands in student_profiles:
att = Attempt.search([('student_id', '=', student.id), ('exam_id', '=', exam.id)], limit=1)
if not att:
att = Attempt.create({
'student_id': student.id,
'exam_id': exam.id,
'status': 'scored',
'listening_band': bands['listening'],
'reading_band': bands['reading'],
'writing_band': bands['writing'],
'speaking_band': bands['speaking'],
'overall_band': bands['overall'],
'cefr_level': bands['cefr'],
'entity_id': entity.id,
})
for q in questions:
existing_ans = Answer.search([('attempt_id', '=', att.id), ('question_id', '=', q.id)], limit=1)
if not existing_ans:
is_correct = random.random() > 0.3
Answer.create({
'attempt_id': att.id,
'question_id': q.id,
'answer': q.correct_answer if is_correct else 'wrong answer',
'is_correct': is_correct,
'score': q.marks if is_correct else 0,
'time_spent_ms': random.randint(15000, 120000),
})
for skill_name in ['listening', 'reading', 'writing', 'speaking', 'overall']:
existing_score = Score.search([('attempt_id', '=', att.id), ('skill', '=', skill_name)], limit=1)
if not existing_score:
Score.create({
'attempt_id': att.id,
'skill': skill_name,
'band_score': bands.get(skill_name, 0),
'raw_score': bands.get(skill_name, 0) * 10,
'max_score': 90,
'entity_id': entity.id,
})
env.cr.commit()
print("✓ Student attempts, answers, scores created")
# ── 13. Gap profiles ─────────────────────────────────────────────────────
GapProfile = env['encoach.gap.profile']
for student, bands in student_profiles:
gp = GapProfile.search([('student_id', '=', student.id)], limit=1)
if not gp:
GapProfile.create({
'student_id': student.id,
'source_type': 'exam',
'skill_gaps': json.dumps({
'writing': round(7.0 - bands['writing'], 1),
'speaking': round(7.0 - bands['speaking'], 1),
'reading': round(7.0 - bands['reading'], 1),
'listening': round(7.0 - bands['listening'], 1),
}),
'question_type_weaknesses': json.dumps(['tfng', 'gap_fill', 'short_answer']),
'topic_weaknesses': json.dumps(['Coherence & Cohesion', 'Inference']),
'entity_id': entity.id,
})
env.cr.commit()
print("✓ Gap profiles created")
# ── 14. Adaptive events ──────────────────────────────────────────────────
AdaptiveEvent = env['encoach.adaptive.event']
for student, bands in student_profiles:
existing = AdaptiveEvent.search([('student_id', '=', student.id)], limit=1)
if existing:
continue
events = [
{'event_type': 'signal', 'signal_name': 'score_reading', 'signal_value': bands['reading']},
{'event_type': 'signal', 'signal_name': 'score_writing', 'signal_value': bands['writing']},
{'event_type': 'signal', 'signal_name': 'score_listening', 'signal_value': bands['listening']},
{'event_type': 'signal', 'signal_name': 'score_speaking', 'signal_value': bands['speaking']},
{'event_type': 'signal', 'signal_name': 'time_on_task', 'signal_value': random.uniform(20, 80)},
{'event_type': 'decision', 'decision': 'step_up_reading' if bands['reading'] >= 6.5 else 'reinforce_reading'},
{'event_type': 'decision', 'decision': 'micro_lesson_writing' if bands['writing'] < 6.0 else 'advance_writing'},
]
for ev in events:
AdaptiveEvent.create({
'student_id': student.id,
'course_id': course.id,
'event_type': ev['event_type'],
'signal_name': ev.get('signal_name', ''),
'signal_value': ev.get('signal_value', 0),
'decision': ev.get('decision', ''),
})
env.cr.commit()
print("✓ Adaptive events created")
# ── 15. Student progress ─────────────────────────────────────────────────
Progress = env['encoach.student.progress']
modules = CourseModule.search([('course_id', '=', course.id)])
for student, bands in student_profiles:
for mod in modules:
pr = Progress.search([('student_id', '=', student.id), ('module_id', '=', mod.id)], limit=1)
if not pr:
stat = 'completed' if bands['overall'] >= 7.0 else ('in_progress' if bands['overall'] >= 5.5 else 'not_started')
Progress.create({
'student_id': student.id,
'course_id': course.id,
'module_id': mod.id,
'status': stat,
'score': bands['overall'] * 10 + random.uniform(-5, 5),
'entity_id': entity.id,
})
env.cr.commit()
print("✓ Student progress records")
# ── 16. AI generation logs ────────────────────────────────────────────────
GenLog = env['encoach.ai.generation.log']
IeltsGenLog = env['encoach.ai.ielts.generation.log']
for student in [sarah, omar]:
gl = GenLog.search([('student_id', '=', student.id)], limit=1)
if not gl:
GenLog.create({
'student_id': student.id,
'course_type': 'general_english',
'brief': json.dumps({'cefr_level': 'B2', 'focus': 'writing improvement'}),
'status': 'approved',
'attempts': 1,
})
igl = IeltsGenLog.search([], limit=1)
if not igl:
for skill in ['writing', 'speaking', 'reading']:
IeltsGenLog.create({
'skill': skill,
'brief': json.dumps({'target_band': 7.0, 'exam_type': 'academic'}),
'status': 'examiner_review' if skill == 'writing' else 'approved',
'review_status': 'pending_review' if skill == 'writing' else 'approved',
'attempts': 1,
})
env.cr.commit()
print("✓ AI generation logs")
# ── 17. AI service logs ───────────────────────────────────────────────────
AiLog = env['encoach.ai.log']
if not AiLog.search([], limit=1):
for i in range(5):
AiLog.create({
'service': random.choice(['openai', 'coach']),
'action': random.choice(['chat', 'get_tip', 'suggest', 'explain', 'search', 'grade_speaking']),
'model_used': 'gpt-4o',
'prompt_tokens': random.randint(100, 500),
'completion_tokens': random.randint(50, 300),
'total_tokens': random.randint(150, 800),
'latency_ms': random.randint(500, 3000),
'status': 'success',
'user_id': random.choice([sarah.id, omar.id, layla.id]),
})
env.cr.commit()
print("✓ AI logs created")
# ── 18. Adaptive settings ────────────────────────────────────────────────
AdaptiveSettings = env['encoach.adaptive.settings']
if not AdaptiveSettings.search([('entity_id', '=', entity.id)], limit=1):
AdaptiveSettings.create({
'entity_id': entity.id,
'step_up_threshold': 0.85,
'step_down_threshold': 0.50,
'micro_lesson_trigger': 2,
'no_progress_alert_days': 3,
})
env.cr.commit()
print("✓ Adaptive settings")
# ── Done ──────────────────────────────────────────────────────────────────
print("\n" + "="*60)
print(" DEMO DATA SEEDED SUCCESSFULLY!")
print("="*60)
print(f"\nLogin credentials:")
print(f" Admin: admin@encoach.test / admin123")
print(f" Student: sarah@encoach.test / student123")
print(f" Student: omar@encoach.test / student123")
print(f" Student: layla@encoach.test / student123")
print(f" Teacher: khalid@encoach.test/ teacher123")
print(f" Teacher: fatima@encoach.test/ teacher123")
print(f"\nEntity: {entity.name} (code: {entity.code})")
print(f"Course: {course.name}")
print(f"Exam: {exam.title}")