Roadmap P0 — platform safety & ops
- Merge duplicate encoach.student.attempt/answer models into encoach_scoring
and drop the stale encoach_exam_template copies.
- Remove duplicate /api/exam/* routes; canonicalize on one controller tree.
- Gate raw-SQL seeds in seed_demo_data.py behind an explicit env flag.
- Add /api/health and /api/health/ready (DB + LLM reachability) endpoints.
- Fix docker-compose + ship odoo-docker.conf for container-local runs.
- Enforce OpenAI request_timeout=30s and @jwt_required on all AI/coach routes.
- Promote canonical cefr_mapper to encoach_ai.services.cefr_mapper.
- JWT cache TTL=30s + invalidation hook on user mutation.
Roadmap P1 — exam correctness & data provenance
- Wire QualityChecker + IeltsValidator into exam submit with a
pending_review gate (encoach_ai.services.question_validator).
- Populate RAG metadata (course_id, subject_id, entity_id, taxonomy) on
encoach_vector embeddings and add a chunking pipeline (>2000 chars).
- Add provenance fields on encoach.question (model, prompt_hash, log_id)
and validate LLM output with schema before DB insert.
- Unify response envelope to {items,total,page,size}.
- Approval reject rollback with savepoint atomicity.
- Ticket notifications on status/assignee change.
Roadmap P2 — performance & observability
- Reports: replace Python loops with SQL read_group aggregations.
- X-Request-ID middleware + structured JSON logs.
- In-process/Prometheus counters and openapi.py controller exporting a
spec by scanning @http.route decorators.
- Paymob real checkout + HMAC-SHA512 webhook verification, backed by a
new encoach.paymob.order model and ir.config_parameter credentials.
- JWT refresh tokens + revocation table.
- Composite DB indexes on hot report/ticket/attempt paths.
Roadmap P3 — human-in-the-loop & compliance
- Human-in-the-loop exam review workflow (pending_review → publish) with
new review controller and status transitions.
- encoach.ai.prompt model + versioning + admin editor endpoints (one
active version per key, render-preview dry run).
- Student feedback loop → encoach.ai.feedback (upsert per user/subject,
admin triage + resolve endpoints).
- GDPR export (/api/gdpr/export) and right-to-erasure (/api/gdpr/delete)
with anonymization, tombstone record, and admin-self-erasure guard.
- HttpCase smoke tests for /api/health and /api/health/ready.
Made-with: Cursor
658 lines
44 KiB
Python
658 lines
44 KiB
Python
#!/usr/bin/env python3
|
|
"""Legacy raw-SQL demo data seeder for EnCoach platform.
|
|
|
|
⚠️ DANGER ⚠️ — this script talks to Postgres directly with a hardcoded DB user
|
|
and bypasses all Odoo ORM validation, constraints, and record rules. It exists
|
|
only for historical/bootstrapping reasons and MUST NOT be run against any
|
|
shared or production database.
|
|
|
|
Running it requires:
|
|
|
|
1. ``ENCOACH_ALLOW_RAW_SEED=1`` in the environment (explicit opt-in).
|
|
2. Connection parameters via environment variables, e.g.
|
|
``PGDATABASE``, ``PGHOST``, ``PGUSER``, ``PGPASSWORD``.
|
|
|
|
The **preferred** seeder for a fresh dev environment is ``seed_institutional.py``
|
|
(Odoo-shell, ORM-based, idempotent). See P0.6 in docs/PROJECT_SUMMARY.md §21.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
|
|
if os.environ.get("ENCOACH_ALLOW_RAW_SEED") != "1":
|
|
sys.stderr.write(
|
|
"\nseed_demo_data.py refused to run: set ENCOACH_ALLOW_RAW_SEED=1 to "
|
|
"explicitly opt in.\n"
|
|
"This script writes raw SQL against PostgreSQL and bypasses the "
|
|
"Odoo ORM — prefer seed_institutional.py for everyday seeding.\n"
|
|
)
|
|
sys.exit(2)
|
|
|
|
import psycopg2
|
|
|
|
conn = psycopg2.connect(
|
|
dbname=os.environ.get("PGDATABASE", "encoach_v2"),
|
|
host=os.environ.get("PGHOST", "localhost"),
|
|
port=int(os.environ.get("PGPORT", "5432")),
|
|
user=os.environ.get("PGUSER", os.environ.get("USER", "odoo")),
|
|
password=os.environ.get("PGPASSWORD", ""),
|
|
)
|
|
conn.autocommit = True
|
|
cur = conn.cursor()
|
|
|
|
NOW = datetime.now()
|
|
UID = 2 # admin
|
|
|
|
def ins(table, cols, vals, returning='id'):
|
|
placeholders = ','.join(['%s'] * len(cols))
|
|
col_str = ','.join(cols)
|
|
cur.execute(f"INSERT INTO {table} ({col_str}) VALUES ({placeholders}) RETURNING {returning}", vals)
|
|
return cur.fetchone()[0]
|
|
|
|
def ensure_col(table, col, dtype):
|
|
cur.execute(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col} {dtype}")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 1. RUBRICS — encoach_exam_rubric (for generation/grading)
|
|
# ─────────────────────────────────────────────────────────
|
|
print("── Seeding Exam Rubrics ──")
|
|
exam_rubric_data = [
|
|
("IELTS Reading Band Descriptors", "reading", json.dumps([
|
|
{"name": "Comprehension", "weight": 40, "levels": {"9": "Expert", "7": "Good", "5": "Modest", "3": "Limited"}},
|
|
{"name": "Vocabulary Understanding", "weight": 30, "levels": {"9": "Wide range", "7": "Sufficient", "5": "Basic", "3": "Very limited"}},
|
|
{"name": "Inference & Analysis", "weight": 30, "levels": {"9": "Excellent", "7": "Good", "5": "Developing", "3": "Weak"}}
|
|
]), 9.0),
|
|
("IELTS Listening Band Descriptors", "listening", json.dumps([
|
|
{"name": "Understanding Main Ideas", "weight": 35, "levels": {"9": "Expert", "7": "Good", "5": "Modest", "3": "Limited"}},
|
|
{"name": "Detail Recognition", "weight": 35, "levels": {"9": "Expert", "7": "Good", "5": "Modest", "3": "Limited"}},
|
|
{"name": "Speaker Intent", "weight": 30, "levels": {"9": "Expert", "7": "Good", "5": "Developing", "3": "Weak"}}
|
|
]), 9.0),
|
|
("IELTS Writing Task 1 Rubric", "writing", json.dumps([
|
|
{"name": "Task Achievement", "weight": 25, "levels": {"9": "Fully satisfies", "7": "Covers requirements", "5": "Partially addresses", "3": "Fails to address"}},
|
|
{"name": "Coherence & Cohesion", "weight": 25, "levels": {"9": "Seamless", "7": "Clear progression", "5": "Some organization", "3": "No clear arrangement"}},
|
|
{"name": "Lexical Resource", "weight": 25, "levels": {"9": "Wide range", "7": "Sufficient", "5": "Limited", "3": "Very limited"}},
|
|
{"name": "Grammar Range & Accuracy", "weight": 25, "levels": {"9": "Wide range", "7": "Good control", "5": "Limited range", "3": "Frequent errors"}}
|
|
]), 9.0),
|
|
("IELTS Speaking Rubric", "speaking", json.dumps([
|
|
{"name": "Fluency & Coherence", "weight": 25, "levels": {"9": "Effortless", "7": "Speaks at length", "5": "Maintains flow", "3": "Long pauses"}},
|
|
{"name": "Pronunciation", "weight": 25, "levels": {"9": "Effortless", "7": "Good control", "5": "Generally understood", "3": "Mispronunciations"}}
|
|
]), 9.0),
|
|
]
|
|
|
|
for name, module, criteria, max_score in exam_rubric_data:
|
|
cur.execute("SELECT id FROM encoach_exam_rubric WHERE name=%s", (name,))
|
|
if not cur.fetchone():
|
|
rid = ins('encoach_exam_rubric',
|
|
['name', 'module', 'criteria', 'max_score', 'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[name, module, criteria, max_score, UID, UID, NOW, NOW])
|
|
print(f" Created exam rubric: {name} (id={rid})")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 1b. RUBRICS — encoach_rubric (for rubric groups)
|
|
# ─────────────────────────────────────────────────────────
|
|
print("── Seeding Rubrics (for groups) ──")
|
|
rubric_data = [
|
|
("IELTS Reading Assessment", "reading", "ielts",
|
|
json.dumps([{"name": "Comprehension", "weight": 50}, {"name": "Inference", "weight": 50}]),
|
|
json.dumps(["Band 9","Band 7","Band 5","Band 3"])),
|
|
("IELTS Listening Assessment", "listening", "ielts",
|
|
json.dumps([{"name": "Main Ideas", "weight": 50}, {"name": "Detail Recognition", "weight": 50}]),
|
|
json.dumps(["Band 9","Band 7","Band 5","Band 3"])),
|
|
("IELTS Speaking Assessment", "speaking", "ielts",
|
|
json.dumps([{"name": "Fluency", "weight": 25}, {"name": "Lexical Resource", "weight": 25}, {"name": "Grammar", "weight": 25}, {"name": "Pronunciation", "weight": 25}]),
|
|
json.dumps(["Band 9","Band 7","Band 5","Band 3"])),
|
|
("General English Grammar", "grammar", "general",
|
|
json.dumps([{"name": "Accuracy", "weight": 60}, {"name": "Complexity", "weight": 40}]),
|
|
json.dumps(["A1","A2","B1","B2","C1","C2"])),
|
|
]
|
|
|
|
rubric_ids = {}
|
|
for name, skill, exam_type, criteria, levels in rubric_data:
|
|
cur.execute("SELECT id FROM encoach_rubric WHERE name=%s", (name,))
|
|
row = cur.fetchone()
|
|
if row:
|
|
rubric_ids[skill] = row[0]
|
|
else:
|
|
rid = ins('encoach_rubric',
|
|
['name', 'skill', 'exam_type', 'criteria', 'levels',
|
|
'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[name, skill, exam_type, criteria, levels, UID, UID, NOW, NOW])
|
|
rubric_ids[skill] = rid
|
|
print(f" Created rubric: {name} (id={rid})")
|
|
|
|
# Also map existing rubrics
|
|
cur.execute("SELECT id, skill FROM encoach_rubric")
|
|
for row in cur.fetchall():
|
|
if row[1] and row[1] not in rubric_ids:
|
|
rubric_ids[row[1]] = row[0]
|
|
|
|
# Rubric groups
|
|
print("── Seeding Rubric Groups ──")
|
|
groups = [
|
|
("IELTS Full Exam Rubrics", ['reading', 'listening', 'writing', 'speaking']),
|
|
("Academic Writing Rubrics", ['writing']),
|
|
("Receptive Skills Rubrics", ['reading', 'listening']),
|
|
]
|
|
for gname, modules in groups:
|
|
cur.execute("SELECT id FROM encoach_rubric_group WHERE name=%s", (gname,))
|
|
row = cur.fetchone()
|
|
if not row:
|
|
gid = ins('encoach_rubric_group',
|
|
['name', 'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[gname, UID, UID, NOW, NOW])
|
|
for m in modules:
|
|
if m in rubric_ids:
|
|
cur.execute("INSERT INTO encoach_rubric_group_rel (group_id, rubric_id) VALUES (%s,%s) ON CONFLICT DO NOTHING", (gid, rubric_ids[m]))
|
|
print(f" Created rubric group: {gname} (id={gid})")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 2. EXAM STRUCTURES
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Exam Structures ──")
|
|
structures = [
|
|
("IELTS Academic Reading", "reading", json.dumps({
|
|
"parts": [
|
|
{"name": "Part 1", "passages": [{"topic": "Academic text", "word_count": 700, "exercises": [
|
|
{"type": "true_false_ng", "count": 5}, {"type": "matching_headings", "count": 4}
|
|
]}]},
|
|
{"name": "Part 2", "passages": [{"topic": "Scientific article", "word_count": 800, "exercises": [
|
|
{"type": "fill_blanks", "count": 6}, {"type": "multiple_choice", "count": 4}
|
|
]}]},
|
|
{"name": "Part 3", "passages": [{"topic": "Complex argument", "word_count": 900, "exercises": [
|
|
{"type": "matching_information", "count": 5}, {"type": "short_answer", "count": 5}
|
|
]}]}
|
|
], "total_time": 60, "total_questions": 40
|
|
})),
|
|
("IELTS Academic Writing", "writing", json.dumps({
|
|
"tasks": [
|
|
{"name": "Task 1", "type": "report", "min_words": 150, "time_suggested": 20,
|
|
"prompt_types": ["graph", "chart", "table", "diagram"]},
|
|
{"name": "Task 2", "type": "essay", "min_words": 250, "time_suggested": 40,
|
|
"prompt_types": ["opinion", "discussion", "problem_solution", "advantages_disadvantages"]}
|
|
], "total_time": 60
|
|
})),
|
|
("IELTS Listening Standard", "listening", json.dumps({
|
|
"sections": [
|
|
{"name": "Section 1", "context": "Social conversation", "speakers": 2, "exercises": [
|
|
{"type": "form_completion", "count": 5}, {"type": "multiple_choice", "count": 5}
|
|
]},
|
|
{"name": "Section 2", "context": "Monologue social", "speakers": 1, "exercises": [
|
|
{"type": "matching", "count": 5}, {"type": "plan_map_labelling", "count": 5}
|
|
]},
|
|
{"name": "Section 3", "context": "Academic discussion", "speakers": 3, "exercises": [
|
|
{"type": "multiple_choice", "count": 5}, {"type": "sentence_completion", "count": 5}
|
|
]},
|
|
{"name": "Section 4", "context": "Academic lecture", "speakers": 1, "exercises": [
|
|
{"type": "note_completion", "count": 5}, {"type": "summary_completion", "count": 5}
|
|
]}
|
|
], "total_time": 30, "total_questions": 40
|
|
})),
|
|
("IELTS Speaking Standard", "speaking", json.dumps({
|
|
"parts": [
|
|
{"name": "Part 1 - Introduction", "duration_min": 5, "questions": 4,
|
|
"topics": ["work", "study", "hometown", "hobbies"]},
|
|
{"name": "Part 2 - Long Turn", "duration_min": 4, "prep_time": 1,
|
|
"cue_card": True},
|
|
{"name": "Part 3 - Discussion", "duration_min": 5, "questions": 4,
|
|
"abstract_topics": True}
|
|
], "total_time": 14
|
|
})),
|
|
("Business English Assessment", "industry", json.dumps({
|
|
"modules": ["reading", "writing", "listening"],
|
|
"industry": "business",
|
|
"sections": [
|
|
{"module": "reading", "passages": 2, "questions": 20, "time": 30},
|
|
{"module": "writing", "tasks": 1, "min_words": 200, "time": 30},
|
|
{"module": "listening", "sections": 2, "questions": 20, "time": 20}
|
|
], "total_time": 80
|
|
})),
|
|
("Quick Level Check", "level", json.dumps({
|
|
"adaptive": True,
|
|
"question_pool_size": 50,
|
|
"max_questions": 25,
|
|
"cefr_levels": ["A1", "A2", "B1", "B2", "C1", "C2"],
|
|
"skills": ["grammar", "vocabulary", "reading"],
|
|
"time_limit": 30
|
|
})),
|
|
]
|
|
|
|
for name, module, config in structures:
|
|
cur.execute("SELECT id FROM encoach_exam_structure WHERE name=%s", (name,))
|
|
if not cur.fetchone():
|
|
sid = ins('encoach_exam_structure',
|
|
['name', 'module', 'config', 'modules', 'active', 'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[name, module, config, module, True, UID, UID, NOW, NOW])
|
|
print(f" Created structure: {name} (id={sid})")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 3. PASSAGES (reading content)
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Passages ──")
|
|
passage_texts = [
|
|
("academic", "B2", 1, """The Impact of Artificial Intelligence on Modern Education
|
|
|
|
Artificial intelligence has rapidly transformed various sectors of society, and education is no exception. In recent years, educational institutions worldwide have begun integrating AI-powered tools into their curricula, fundamentally changing how students learn and how teachers instruct.
|
|
|
|
One of the most significant applications of AI in education is personalized learning. Traditional classroom settings often follow a one-size-fits-all approach, where all students receive the same instruction regardless of their individual learning styles, paces, or abilities. AI-powered adaptive learning platforms, however, can analyze a student's performance in real-time and adjust the difficulty, pace, and content accordingly.
|
|
|
|
Furthermore, AI has revolutionized assessment methods. Automated grading systems can now evaluate not only multiple-choice answers but also essays and open-ended responses with increasing accuracy. Natural language processing algorithms can assess writing quality, coherence, and even creativity, providing instant feedback that would take human teachers significantly longer to deliver.
|
|
|
|
However, the integration of AI in education is not without challenges. Privacy concerns regarding student data collection are paramount. Additionally, there is the risk of over-reliance on technology, which may diminish critical thinking skills and human interaction — both essential components of a well-rounded education.
|
|
|
|
Despite these concerns, the trajectory of AI in education continues upward. Research suggests that when AI tools are used as supplements to, rather than replacements for, traditional teaching methods, student outcomes improve significantly. The key lies in finding the right balance between technological innovation and the irreplaceable human elements of education."""),
|
|
|
|
("scientific", "C1", 2, """Neuroplasticity and Language Acquisition in Adults
|
|
|
|
For decades, the prevailing scientific consensus held that the human brain's capacity for language acquisition diminished significantly after the so-called 'critical period' — typically considered to end around puberty. This view, largely influenced by Lenneberg's Critical Period Hypothesis (1967), suggested that adults who attempted to learn new languages would inevitably fall short of native-like proficiency.
|
|
|
|
Recent neuroscientific research, however, has challenged this deterministic view. Studies utilizing functional magnetic resonance imaging (fMRI) have demonstrated that the adult brain retains remarkable neuroplasticity — the ability to form new neural connections and reorganize existing ones. When adults engage in intensive language learning, observable changes occur in brain structure, particularly in the left inferior frontal gyrus (Broca's area) and the superior temporal gyrus (Wernicke's area).
|
|
|
|
A groundbreaking 2019 study published in the Journal of Neurolinguistics tracked 45 adult learners of Mandarin Chinese over an 18-month period. The researchers found that participants who received immersive instruction showed significant cortical thickening in language-related brain regions, comparable to changes observed in younger learners. Moreover, 23% of participants achieved scores on standardized proficiency tests that were statistically indistinguishable from native speakers of similar educational backgrounds.
|
|
|
|
These findings have profound implications for language education policy. If the critical period is less rigid than previously thought, then the investment in adult language programs — particularly for immigrants and professionals requiring multilingual competencies — may yield higher returns than traditional models predicted. The research suggests that intensity and immersion quality, rather than age alone, are the primary determinants of ultimate attainment in second language acquisition."""),
|
|
|
|
("argumentative", "B1", 3, """Remote Work: Reshaping the Modern Workplace
|
|
|
|
The global pandemic of 2020 forced millions of workers to transition from traditional office environments to remote work arrangements virtually overnight. What began as a temporary measure has since evolved into a permanent feature of the modern workplace, with far-reaching consequences for both employers and employees.
|
|
|
|
Proponents of remote work point to numerous advantages. Employees report higher job satisfaction, citing the elimination of lengthy commutes, greater flexibility in managing personal responsibilities, and improved work-life balance. Studies from Stanford University found that remote workers are 13% more productive than their office-based counterparts, attributed to fewer distractions and more comfortable working environments.
|
|
|
|
For employers, remote work offers the tantalizing prospect of reduced overhead costs. Office space, utilities, and in-person perks can be minimized or eliminated entirely. Furthermore, companies can tap into a global talent pool, no longer limited by geographical constraints when hiring.
|
|
|
|
Nevertheless, remote work presents its own set of challenges. Many employees report feelings of isolation and difficulty disconnecting from work, leading to burnout. Collaboration and spontaneous innovation — often sparked by casual conversations in office hallways — can be harder to replicate through video calls and messaging platforms.
|
|
|
|
The emerging consensus among workplace experts is that a hybrid model, combining remote and in-office work, represents the optimal solution. Such arrangements preserve the benefits of flexibility while maintaining the social connections and collaborative opportunities that physical workplaces provide."""),
|
|
]
|
|
|
|
passage_ids = []
|
|
for category, cefr, section_num, text in passage_texts:
|
|
cur.execute("SELECT id FROM encoach_passage WHERE topic_category=%s AND section_num=%s LIMIT 1", (category, section_num))
|
|
row = cur.fetchone()
|
|
if row:
|
|
passage_ids.append(row[0])
|
|
else:
|
|
pid = ins('encoach_passage',
|
|
['exam_type', 'topic_category', 'difficulty', 'cefr_level', 'section_num', 'word_count',
|
|
'body_text', 'status', 'ai_generated', 'approved', 'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
['ielts', category, 'medium', cefr, section_num, len(text.split()),
|
|
text, 'approved', True, True, UID, UID, NOW, NOW])
|
|
passage_ids.append(pid)
|
|
print(f" Created passage: {category} section {section_num} (id={pid})")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 4. QUESTIONS
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Questions ──")
|
|
questions = [
|
|
# Reading questions
|
|
("reading", "true_false_ng", "medium", "AI-powered adaptive learning platforms provide the same instruction to all students.", json.dumps(["True","False","Not Given"]), "False", 1),
|
|
("reading", "true_false_ng", "medium", "Automated grading systems can only evaluate multiple-choice answers.", json.dumps(["True","False","Not Given"]), "False", 1),
|
|
("reading", "true_false_ng", "medium", "Privacy concerns about student data are considered very important.", json.dumps(["True","False","Not Given"]), "True", 1),
|
|
("reading", "true_false_ng", "hard", "AI tools should completely replace traditional teaching methods.", json.dumps(["True","False","Not Given"]), "False", 1),
|
|
("reading", "multiple_choice", "medium", "What is the main advantage of AI-powered adaptive learning?", json.dumps(["A) Lower costs","B) Personalized instruction","C) Faster grading","D) Better technology"]), "B) Personalized instruction", 1),
|
|
("reading", "multiple_choice", "hard", "According to the passage, what is the key to successful AI integration in education?", json.dumps(["A) Using only AI tools","B) Eliminating human teachers","C) Balancing technology with human elements","D) Collecting more student data"]), "C) Balancing technology with human elements", 1),
|
|
("reading", "fill_blanks", "medium", "AI-powered _____ learning platforms can analyze student performance in real-time.", json.dumps([]), "adaptive", 1),
|
|
("reading", "short_answer", "hard", "What two essential components of education might be at risk from over-reliance on AI?", json.dumps([]), "critical thinking skills and human interaction", 1),
|
|
# Listening questions
|
|
("listening", "multiple_choice", "easy", "Where does the speaker suggest meeting?", json.dumps(["A) Library","B) Coffee shop","C) Classroom","D) Online"]), "B) Coffee shop", 1),
|
|
("listening", "multiple_choice", "medium", "What is the main topic of the lecture?", json.dumps(["A) Marine biology","B) Climate change","C) Economics","D) History"]), "B) Climate change", 1),
|
|
("listening", "form_completion", "easy", "The student's ID number is _____.", json.dumps([]), "SN20264823", 1),
|
|
("listening", "form_completion", "medium", "The conference is held at _____ University.", json.dumps([]), "Cambridge", 1),
|
|
("listening", "sentence_completion", "medium", "The deadline for applications is the _____ of next month.", json.dumps([]), "15th", 1),
|
|
# Level assessment questions
|
|
("level", "multiple_choice", "easy", "She _____ to the store yesterday.", json.dumps(["A) go","B) goes","C) went","D) going"]), "C) went", 1),
|
|
("level", "multiple_choice", "easy", "I have _____ been to Paris.", json.dumps(["A) ever","B) never","C) already","D) yet"]), "B) never", 1),
|
|
("level", "multiple_choice", "medium", "If I _____ more time, I would travel more.", json.dumps(["A) have","B) had","C) having","D) has"]), "B) had", 1),
|
|
("level", "multiple_choice", "medium", "The report _____ by the team last week.", json.dumps(["A) was written","B) wrote","C) is written","D) writing"]), "A) was written", 1),
|
|
("level", "multiple_choice", "hard", "Not until the meeting ended _____ the true extent of the problem.", json.dumps(["A) they realized","B) did they realize","C) they did realize","D) realized they"]), "B) did they realize", 1),
|
|
("level", "multiple_choice", "hard", "The scientist, whose research _____ controversial, received the award.", json.dumps(["A) has been","B) was being","C) had been","D) being"]), "C) had been", 1),
|
|
]
|
|
|
|
for skill, qtype, diff, stem, options, answer, marks in questions:
|
|
cur.execute("SELECT id FROM encoach_question WHERE stem=%s LIMIT 1", (stem,))
|
|
if not cur.fetchone():
|
|
ins('encoach_question',
|
|
['skill', 'question_type', 'difficulty', 'stem', 'options', 'correct_answer', 'marks',
|
|
'status', 'ai_generated', 'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[skill, qtype, diff, stem, options, answer, marks,
|
|
'approved', True, UID, UID, NOW, NOW])
|
|
|
|
print(f" Seeded {len(questions)} questions")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 5. WRITING PROMPTS
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Writing Prompts ──")
|
|
writing_prompts = [
|
|
("ielts", "1", "report", "The chart below shows the percentage of households with internet access in five countries between 2000 and 2020.\n\nSummarise the information by selecting and reporting the main features, and make comparisons where relevant.\n\nWrite at least 150 words.", 150),
|
|
("ielts", "2", "essay", "Some people believe that universities should focus on providing academic knowledge, while others think they should prepare students for the job market.\n\nDiscuss both views and give your own opinion.\n\nWrite at least 250 words.", 250),
|
|
("ielts", "2", "essay", "In many countries, the gap between the rich and the poor is increasing. What problems does this cause? What solutions can you suggest?\n\nWrite at least 250 words.", 250),
|
|
("ielts", "1", "report", "The two maps below show an island before and after the construction of some tourist facilities.\n\nSummarise the information by selecting and reporting the main features, and make comparisons where relevant.\n\nWrite at least 150 words.", 150),
|
|
]
|
|
|
|
for exam_type, task, wtype, prompt_text, min_words in writing_prompts:
|
|
cur.execute("SELECT id FROM encoach_writing_prompt WHERE prompt_text=%s LIMIT 1", (prompt_text[:100],))
|
|
if not cur.fetchone():
|
|
ins('encoach_writing_prompt',
|
|
['exam_type', 'task', 'writing_type', 'prompt_text', 'min_words',
|
|
'ai_generated', 'approved', 'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[exam_type, task, wtype, prompt_text, min_words,
|
|
True, True, UID, UID, NOW, NOW])
|
|
|
|
print(f" Seeded {len(writing_prompts)} writing prompts")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 6. SPEAKING CARDS
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Speaking Cards ──")
|
|
speaking_cards = [
|
|
(1, "Describe a book that you recently read", "medium",
|
|
json.dumps(["What book it was", "When you read it", "What it was about", "Why you enjoyed it"]),
|
|
"You should speak for 1-2 minutes."),
|
|
(2, "Describe a place you would like to visit", "medium",
|
|
json.dumps(["Where the place is", "How you know about it", "What you would do there", "Why you want to visit"]),
|
|
"You should speak for 1-2 minutes."),
|
|
(3, "Describe an achievement you are proud of", "hard",
|
|
json.dumps(["What you achieved", "When it happened", "How you achieved it", "Why you are proud of it"]),
|
|
"You should speak for 1-2 minutes."),
|
|
(1, "Hometown and living", "easy",
|
|
json.dumps(["Where is your hometown?", "What do you like about it?", "Has it changed much?", "Would you recommend visiting?"]),
|
|
"Part 1 questions"),
|
|
(3, "Technology and society", "hard",
|
|
json.dumps(["How has technology changed communication?", "Do you think we rely too much on technology?", "What might technology look like in 50 years?", "Should governments regulate technology more?"]),
|
|
"Part 3 discussion questions"),
|
|
]
|
|
|
|
for part, topic, diff, questions_json, bullet in speaking_cards:
|
|
cur.execute("SELECT id FROM encoach_speaking_card WHERE topic=%s AND part=%s LIMIT 1", (topic, part))
|
|
if not cur.fetchone():
|
|
ins('encoach_speaking_card',
|
|
['part', 'topic', 'difficulty', 'questions', 'bullet_points',
|
|
'ai_generated', 'approved', 'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[part, topic, diff, questions_json, bullet,
|
|
True, True, UID, UID, NOW, NOW])
|
|
|
|
print(f" Seeded {len(speaking_cards)} speaking cards")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 7. CUSTOM EXAMS (with sections)
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Custom Exams ──")
|
|
|
|
# Clean old stub exam
|
|
cur.execute("DELETE FROM encoach_exam_assignment WHERE exam_id IN (SELECT id FROM encoach_exam_custom WHERE title='IELTS Reading Practice Test 1')")
|
|
cur.execute("DELETE FROM encoach_exam_schedule WHERE exam_id IN (SELECT id FROM encoach_exam_custom WHERE title='IELTS Reading Practice Test 1')")
|
|
|
|
custom_exams = [
|
|
("IELTS Academic Reading Practice", "published", 60, "Complete IELTS Academic Reading practice test with 3 passages and 40 questions.", 50.0),
|
|
("IELTS Writing Task 1 & 2", "published", 60, "Full IELTS Academic Writing test covering Task 1 (report) and Task 2 (essay).", 50.0),
|
|
("IELTS Listening Full Test", "published", 30, "Complete IELTS Listening test with 4 sections and 40 questions.", 50.0),
|
|
("IELTS Speaking Mock", "published", 15, "Full IELTS Speaking test simulation with Parts 1, 2, and 3.", 50.0),
|
|
("Quick Grammar Level Check", "published", 20, "Quick assessment to determine your English grammar level (A1-C2).", 60.0),
|
|
("Business English Reading", "published", 45, "Reading comprehension test focused on business English contexts.", 50.0),
|
|
("Academic Vocabulary Builder", "draft", 30, "Vocabulary assessment targeting academic word list proficiency.", 60.0),
|
|
("IELTS Full Practice Exam", "published", 165, "Complete IELTS practice covering all four skills: Listening, Reading, Writing, Speaking.", 50.0),
|
|
]
|
|
|
|
exam_ids = {}
|
|
for title, status, time_min, desc, threshold in custom_exams:
|
|
cur.execute("SELECT id FROM encoach_exam_custom WHERE title=%s", (title,))
|
|
row = cur.fetchone()
|
|
if row:
|
|
exam_ids[title] = row[0]
|
|
else:
|
|
eid = ins('encoach_exam_custom',
|
|
['title', 'status', 'total_time_min', 'description', 'pass_threshold',
|
|
'entity_id', 'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[title, status, time_min, desc, threshold,
|
|
2, UID, UID, NOW, NOW])
|
|
exam_ids[title] = eid
|
|
print(f" Created exam: {title} (id={eid})")
|
|
|
|
# Sections for exams
|
|
sections_data = {
|
|
"IELTS Academic Reading Practice": [
|
|
("Passage 1 — Education & AI", "reading", 13, 20, 1),
|
|
("Passage 2 — Neuroplasticity", "reading", 13, 20, 2),
|
|
("Passage 3 — Remote Work", "reading", 14, 20, 3),
|
|
],
|
|
"IELTS Writing Task 1 & 2": [
|
|
("Task 1 — Data Report", "writing", 0, 20, 1),
|
|
("Task 2 — Essay", "writing", 0, 40, 2),
|
|
],
|
|
"IELTS Listening Full Test": [
|
|
("Section 1 — Social Conversation", "listening", 10, 8, 1),
|
|
("Section 2 — Monologue", "listening", 10, 8, 2),
|
|
("Section 3 — Academic Discussion", "listening", 10, 7, 3),
|
|
("Section 4 — Lecture", "listening", 10, 7, 4),
|
|
],
|
|
"IELTS Speaking Mock": [
|
|
("Part 1 — Introduction", "speaking", 4, 5, 1),
|
|
("Part 2 — Long Turn", "speaking", 0, 4, 2),
|
|
("Part 3 — Discussion", "speaking", 4, 5, 3),
|
|
],
|
|
"IELTS Full Practice Exam": [
|
|
("Listening", "listening", 40, 30, 1),
|
|
("Reading", "reading", 40, 60, 2),
|
|
("Writing", "writing", 0, 60, 3),
|
|
("Speaking", "speaking", 0, 15, 4),
|
|
],
|
|
}
|
|
|
|
for exam_title, sections in sections_data.items():
|
|
if exam_title in exam_ids:
|
|
eid = exam_ids[exam_title]
|
|
for title, skill, qcount, time_lim, seq in sections:
|
|
cur.execute("SELECT id FROM encoach_exam_custom_section WHERE exam_id=%s AND title=%s", (eid, title))
|
|
if not cur.fetchone():
|
|
ins('encoach_exam_custom_section',
|
|
['exam_id', 'title', 'skill', 'question_count', 'time_limit_min', 'sequence', 'scoring_method',
|
|
'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[eid, title, skill, qcount, time_lim, seq, 'weighted',
|
|
UID, UID, NOW, NOW])
|
|
|
|
print(" Sections created for all exams")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 8. EXAM SCHEDULES (all lifecycle states)
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Exam Schedules ──")
|
|
|
|
# Clean existing
|
|
cur.execute("DELETE FROM encoach_exam_assignment")
|
|
cur.execute("DELETE FROM encoach_exam_schedule")
|
|
|
|
published_exams = [k for k, v in exam_ids.items() if "published" in k.lower() or True]
|
|
pub_ids = list(exam_ids.values())[:6]
|
|
|
|
students_5 = [6, 39, 40, 41, 42] # alice + 4 others
|
|
|
|
schedules = [
|
|
# Active schedules
|
|
("Spring 2026 — IELTS Reading", pub_ids[0], "active", NOW - timedelta(days=2), NOW + timedelta(days=14), 2, "individual", True, True, True),
|
|
("April Writing Practice", pub_ids[1], "active", NOW - timedelta(hours=6), NOW + timedelta(days=7), 2, "individual", True, False, False),
|
|
("Listening Marathon", pub_ids[2], "active", NOW - timedelta(days=1), NOW + timedelta(days=5), 2, "batch", False, True, False),
|
|
# Planned schedules
|
|
("May Reading Exam", pub_ids[0], "planned", NOW + timedelta(days=15), NOW + timedelta(days=30), 2, "individual", True, True, True),
|
|
("Summer Speaking Test", pub_ids[3], "planned", NOW + timedelta(days=30), NOW + timedelta(days=45), 2, "entity", False, False, False),
|
|
# Past schedules
|
|
("March Grammar Check", pub_ids[4], "past", NOW - timedelta(days=30), NOW - timedelta(days=15), 2, "individual", False, True, False),
|
|
("Winter Reading Final", pub_ids[0], "past", NOW - timedelta(days=60), NOW - timedelta(days=45), 2, "batch", True, True, True),
|
|
# Start expired
|
|
("Expired Business English", pub_ids[5], "start_expired", NOW - timedelta(days=10), NOW - timedelta(days=3), 2, "individual", False, False, False),
|
|
]
|
|
|
|
schedule_ids = []
|
|
for name, eid, state, start, end, entity_id, mode, auto_rel, auto_start, official in schedules:
|
|
sid = ins('encoach_exam_schedule',
|
|
['name', 'exam_id', 'entity_id', 'state', 'start_date', 'end_date', 'assign_mode',
|
|
'auto_release_results', 'auto_start', 'official_exam', 'full_length',
|
|
'assignee_count', 'completed_count',
|
|
'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[name, eid, entity_id, state, start, end, mode,
|
|
auto_rel, auto_start, official, True,
|
|
len(students_5), 0,
|
|
UID, UID, NOW, NOW])
|
|
schedule_ids.append((sid, state, eid, start, end))
|
|
print(f" Schedule: {name} [{state}] (id={sid})")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 9. EXAM ASSIGNMENTS for students
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Exam Assignments ──")
|
|
assignment_count = 0
|
|
for sid, state, eid, start, end in schedule_ids:
|
|
for uid in students_5:
|
|
status = 'assigned'
|
|
if state == 'past':
|
|
status = 'completed'
|
|
elif state == 'start_expired':
|
|
status = 'expired'
|
|
ins('encoach_exam_assignment',
|
|
['exam_id', 'schedule_id', 'student_id', 'status', 'access_start', 'access_end',
|
|
'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[eid, sid, uid, status, start, end,
|
|
UID, UID, NOW, NOW])
|
|
assignment_count += 1
|
|
|
|
# Update assignee counts
|
|
for sid, _, _, _, _ in schedule_ids:
|
|
cur.execute("UPDATE encoach_exam_schedule SET assignee_count=(SELECT count(*) FROM encoach_exam_assignment WHERE schedule_id=%s) WHERE id=%s", (sid, sid))
|
|
if state == 'past':
|
|
cur.execute("UPDATE encoach_exam_schedule SET completed_count=assignee_count WHERE id=%s", (sid,))
|
|
|
|
print(f" Created {assignment_count} assignments across {len(schedule_ids)} schedules")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 10. EXAM SESSIONS (some completed for past schedules)
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Exam Sessions ──")
|
|
import random
|
|
session_count = 0
|
|
for sid, state, eid, start, end in schedule_ids:
|
|
if state in ('past', 'active'):
|
|
for uid in students_5[:3]:
|
|
score = round(random.uniform(4.0, 8.5), 1) if state == 'past' else None
|
|
band = score if score else None
|
|
sess_status = 'completed' if state == 'past' else 'started'
|
|
started_at = start + timedelta(hours=random.randint(1, 24))
|
|
completed_at = started_at + timedelta(minutes=random.randint(30, 90)) if state == 'past' else None
|
|
time_spent = int((completed_at - started_at).total_seconds()) if completed_at else random.randint(300, 1800)
|
|
|
|
ins('encoach_exam_session',
|
|
['user_id', 'exam_id', 'status', 'score', 'band_score', 'time_spent',
|
|
'started_at', 'completed_at', 'answers', 'feedback',
|
|
'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[uid, eid, sess_status, score, band, time_spent,
|
|
started_at, completed_at,
|
|
json.dumps({"responses": []}), json.dumps({"summary": "Demo session"}),
|
|
UID, UID, NOW, NOW])
|
|
session_count += 1
|
|
|
|
print(f" Created {session_count} exam sessions")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 11. ANNOUNCEMENTS
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Announcements ──")
|
|
announcements = [
|
|
("IELTS Mock Test Schedule Released", "all", "normal",
|
|
"The schedule for April IELTS mock tests has been published. Check your assignments page for details."),
|
|
("System Maintenance — April 20", "all", "high",
|
|
"The platform will be undergoing maintenance on April 20, 2026 from 2:00 AM to 5:00 AM UTC. Please save your work."),
|
|
("New Speaking Practice Materials", "student", "normal",
|
|
"We've added 15 new IELTS Speaking Part 2 cue cards. Practice with them in the Speaking module!"),
|
|
("Grading Update for Writing Module", "teacher", "normal",
|
|
"The AI-assisted grading rubric for Writing Task 2 has been updated. Please review the new criteria."),
|
|
]
|
|
|
|
for title, target, priority, content in announcements:
|
|
cur.execute("SELECT id FROM encoach_announcement WHERE title=%s", (title,))
|
|
if not cur.fetchone():
|
|
ins('encoach_announcement',
|
|
['title', 'target_type', 'priority', 'content', 'author_id', 'entity_id', 'active',
|
|
'published_at', 'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[title, target, priority, content, UID, 2, True,
|
|
NOW, UID, UID, NOW, NOW])
|
|
|
|
print(f" Seeded {len(announcements)} announcements")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 12. NOTIFICATIONS for alice
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Notifications ──")
|
|
notifications = [
|
|
("New Exam Assigned", "exam", "assignment", "/student/assignments", "You have been assigned 'IELTS Academic Reading Practice'. Check your dashboard.", False),
|
|
("Writing Results Available", "result", "exam", "/student/grades", "Your Writing Task 1 & 2 results are now available. Band score: 6.5", True),
|
|
("Upcoming Exam Reminder", "reminder", "exam", "/student/dashboard", "Your IELTS Reading Practice starts in 2 days. Make sure to prepare!", False),
|
|
("New Course Material", "course", "learning", "/student/courses", "New vocabulary exercises have been added to your English course.", True),
|
|
("Achievement Unlocked!", "achievement", "general", "/student/journey", "Congratulations! You completed 10 practice tests this month.", False),
|
|
]
|
|
|
|
for title, ntype, category, link, message, is_read in notifications:
|
|
cur.execute("SELECT id FROM encoach_notification WHERE title=%s AND user_id=6", (title,))
|
|
if not cur.fetchone():
|
|
ins('encoach_notification',
|
|
['user_id', 'title', 'type', 'category', 'link', 'message', 'is_read',
|
|
'created_at', 'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[6, title, ntype, category, link, message, is_read,
|
|
NOW - timedelta(hours=random.randint(1, 72)), UID, UID, NOW, NOW])
|
|
|
|
print(f" Seeded {len(notifications)} notifications for alice")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 13. FAQ ITEMS (ensure some exist)
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding FAQ Items ──")
|
|
faqs = [
|
|
("How do I start a practice test?", "Go to your Student Dashboard and click on 'Assignments'. Find the exam you want to take and click 'Start Exam' when it becomes active.", "student"),
|
|
("How is the IELTS band score calculated?", "IELTS scores range from 0-9 in 0.5 increments. Each module (Reading, Writing, Listening, Speaking) receives an individual band score, and the overall band score is the average of all four.", "student"),
|
|
("How do I assign an exam to students?", "Navigate to Admin > Assignments, click 'Schedule Exam', select the exam, set dates, choose your assignment mode (entity, batch, or individual), and click Schedule.", "admin"),
|
|
("What exam structures are available?", "The platform supports IELTS Academic, IELTS General, Business English, and custom structures. You can create new structures from Admin > Exam Structures.", "admin"),
|
|
]
|
|
|
|
cur.execute("SELECT id FROM encoach_faq_category LIMIT 1")
|
|
fcat = cur.fetchone()
|
|
fcat_id = fcat[0] if fcat else None
|
|
|
|
if fcat_id:
|
|
for q, a, roles in faqs:
|
|
cur.execute("SELECT id FROM encoach_faq_item WHERE question=%s", (q,))
|
|
if not cur.fetchone():
|
|
ins('encoach_faq_item',
|
|
['category_id', 'question', 'answer', 'roles', 'sequence', 'active',
|
|
'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[fcat_id, q, a, roles, 99, True, UID, UID, NOW, NOW])
|
|
|
|
print(f" Seeded {len(faqs)} FAQ items")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 14. TICKETS
|
|
# ─────────────────────────────────────────────────────────
|
|
print("\n── Seeding Support Tickets ──")
|
|
tickets = [
|
|
(6, "Cannot access writing section", "bug", "open", "When I try to open Writing Task 2, the page shows a blank screen."),
|
|
(39, "Request for extra time", "feature", "open", "I have a medical condition and need extra time for my IELTS mock test."),
|
|
(40, "Score discrepancy", "complaint", "in_progress", "My listening score shows 5.0 but I believe I answered more correctly."),
|
|
(41, "Cannot hear audio", "bug", "resolved", "The audio for Listening Section 3 was not playing. Issue has been fixed now."),
|
|
]
|
|
|
|
for uid, subject, ttype, status, desc in tickets:
|
|
cur.execute("SELECT id FROM encoach_ticket WHERE subject=%s AND reporter_id=%s", (subject, uid))
|
|
if not cur.fetchone():
|
|
ins('encoach_ticket',
|
|
['reporter_id', 'entity_id', 'subject', 'type', 'status', 'description',
|
|
'created_at', 'create_uid', 'write_uid', 'create_date', 'write_date'],
|
|
[uid, 2, subject, ttype, status, desc,
|
|
NOW - timedelta(days=random.randint(1, 14)), UID, UID, NOW, NOW])
|
|
|
|
print(f" Seeded {len(tickets)} tickets")
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# DONE
|
|
# ─────────────────────────────────────────────────────────
|
|
conn.close()
|
|
print("\n✓ All demo data seeded successfully!")
|