feat: initial backend codebase — EnCoach v3

Complete Odoo 19 backend with 25 custom addons:
- encoach_core: user/entity/role management
- encoach_api: REST API + JWT auth
- encoach_ai: OpenAI integration, AI settings, generation
- encoach_ai_course: AI-powered English & IELTS course generation
- encoach_exam_template/session: exam creation, structures, sessions
- encoach_scoring: AI auto-grading + manual approval
- encoach_vector: pgvector RAG integration
- encoach_adaptive: adaptive learning engine
- encoach_placement: placement testing
- encoach_taxonomy/resources: content taxonomy & resource management
- Plus 14 more modules for courses, branding, portal, etc.

Includes docs: user guide, generation report, developer workflow.

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-11 15:44:20 +04:00
commit 982d4bca30
371 changed files with 35211 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
from . import rubric
from . import exam_template
from . import passage
from . import audio_file
from . import question
from . import writing_prompt
from . import speaking_card
from . import exam_custom
from . import exam_custom_section
from . import exam_assignment
from . import exam_structure

View File

@@ -0,0 +1,30 @@
from odoo import models, fields
class EncoachAudioFile(models.Model):
_name = 'encoach.audio.file'
_description = 'Listening Audio File'
exam_type = fields.Selection([
('academic', 'Academic'),
('general_training', 'General Training'),
('general_english', 'General English'),
], required=True)
part = fields.Integer(required=True)
context_type = fields.Selection([
('conversation', 'Conversation'),
('monologue', 'Monologue'),
], required=True)
topic = fields.Char(size=200, required=True)
audio_url = fields.Char(size=500, required=True)
transcript = fields.Text()
difficulty = fields.Selection([
('easy', 'Easy'),
('medium', 'Medium'),
('hard', 'Hard'),
], required=True)
ai_generated = fields.Boolean(default=False)
approved = fields.Boolean(default=False)
ielts_certified = fields.Boolean(default=False)
generation_brief = fields.Text()
validation_errors = fields.Text()

View File

@@ -0,0 +1,18 @@
from odoo import models, fields
class EncoachExamAssignment(models.Model):
_name = 'encoach.exam.assignment'
_description = 'Exam Assignment'
exam_id = fields.Many2one('encoach.exam.custom', required=True, ondelete='cascade')
student_id = fields.Many2one('res.users', ondelete='cascade')
batch_id = fields.Many2one('op.batch', ondelete='set null')
access_start = fields.Datetime()
access_end = fields.Datetime()
status = fields.Selection([
('assigned', 'Assigned'),
('started', 'Started'),
('completed', 'Completed'),
('expired', 'Expired'),
], default='assigned', required=True)

View File

@@ -0,0 +1,26 @@
from odoo import models, fields
class EncoachExamCustom(models.Model):
_name = 'encoach.exam.custom'
_description = 'Custom Exam'
title = fields.Char(size=200, required=True)
template_id = fields.Many2one('encoach.exam.template', ondelete='set null')
subject_id = fields.Many2one('encoach.subject', ondelete='set null')
entity_id = fields.Many2one('encoach.entity', ondelete='set null')
teacher_id = fields.Many2one('res.users', ondelete='set null')
description = fields.Text()
total_time_min = fields.Integer()
pass_threshold = fields.Float()
results_release_mode = fields.Selection([
('auto', 'Auto'),
('manual_approval', 'Manual Approval'),
], default='auto')
randomize_questions = fields.Boolean(default=False)
status = fields.Selection([
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
], default='draft', required=True)
section_ids = fields.One2many('encoach.exam.custom.section', 'exam_id')

View File

@@ -0,0 +1,24 @@
from odoo import models, fields
class EncoachExamCustomSection(models.Model):
_name = 'encoach.exam.custom.section'
_description = 'Custom Exam Section'
exam_id = fields.Many2one('encoach.exam.custom', required=True, ondelete='cascade')
title = fields.Char(size=200, required=True)
skill = fields.Char(size=100)
question_count = fields.Integer()
time_limit_min = fields.Integer()
scoring_method = fields.Selection([
('auto', 'Auto'),
('rubric', 'Rubric'),
('mixed', 'Mixed'),
], default='auto')
sequence = fields.Integer(default=10)
question_ids = fields.Many2many(
'encoach.question',
'exam_custom_section_question_rel',
'section_id',
'question_id',
)

View File

@@ -0,0 +1,14 @@
from odoo import models, fields
class EncoachExamStructure(models.Model):
_name = 'encoach.exam.structure'
_description = 'Reusable Exam Structure'
_order = 'create_date desc'
name = fields.Char(size=200, required=True)
entity_id = fields.Many2one('encoach.entity', ondelete='set null')
industry = fields.Char(size=100)
modules = fields.Text(help='JSON list of module keys, e.g. ["reading","listening"]')
config = fields.Text(help='JSON config: timer, difficulty, passage counts per module')
active = fields.Boolean(default=True)

View File

@@ -0,0 +1,27 @@
from odoo import models, fields
class EncoachExamTemplate(models.Model):
_name = 'encoach.exam.template'
_description = 'Exam Template'
name = fields.Char(size=200, required=True)
code = fields.Char(size=50)
type = fields.Selection([
('international', 'International'),
('custom', 'Custom'),
], required=True)
editable = fields.Boolean(default=True)
active = fields.Boolean(default=True)
subject_id = fields.Many2one('encoach.subject', ondelete='set null')
entity_id = fields.Many2one('encoach.entity', ondelete='set null')
teacher_id = fields.Many2one('res.users', ondelete='set null')
structure = fields.Text()
total_time_min = fields.Integer()
pass_threshold = fields.Float()
results_release_mode = fields.Selection([
('auto', 'Auto'),
('manual_approval', 'Manual Approval'),
], default='auto')
randomize_questions = fields.Boolean(default=False)
description = fields.Text()

View File

@@ -0,0 +1,46 @@
from odoo import models, fields, api
class EncoachPassage(models.Model):
_name = 'encoach.passage'
_description = 'Reading Passage'
exam_type = fields.Selection([
('academic', 'Academic'),
('general_training', 'General Training'),
('general_english', 'General English'),
], required=True)
section_num = fields.Integer(required=True)
topic_category = fields.Char(size=100, required=True)
body_text = fields.Text(required=True)
difficulty = fields.Selection([
('easy', 'Easy'),
('medium', 'Medium'),
('hard', 'Hard'),
], required=True)
status = fields.Selection([
('draft', 'Draft'),
('active', 'Active'),
('retired', 'Retired'),
('flagged', 'Flagged'),
], default='draft', required=True)
word_count = fields.Integer(compute='_compute_word_count', store=True)
ai_generated = fields.Boolean(default=False)
approved = fields.Boolean(default=False)
ielts_certified = fields.Boolean(default=False)
generation_brief = fields.Text()
validation_errors = fields.Text()
cefr_level = fields.Selection([
('pre_a1', 'Pre-A1'),
('a1', 'A1'),
('a2', 'A2'),
('b1', 'B1'),
('b2', 'B2'),
('c1', 'C1'),
('c2', 'C2'),
])
@api.depends('body_text')
def _compute_word_count(self):
for record in self:
record.word_count = len(record.body_text.split()) if record.body_text else 0

View File

@@ -0,0 +1,69 @@
from odoo import models, fields
class EncoachQuestion(models.Model):
_name = 'encoach.question'
_description = 'Question Item'
skill = fields.Selection([
('listening', 'Listening'),
('reading', 'Reading'),
('writing', 'Writing'),
('speaking', 'Speaking'),
('grammar', 'Grammar'),
('vocabulary', 'Vocabulary'),
('math', 'Mathematics'),
('it', 'Information Technology'),
], required=True)
source_type = fields.Selection([
('passage', 'Passage'),
('audio', 'Audio'),
('writing_prompt', 'Writing Prompt'),
('speaking_card', 'Speaking Card'),
])
source_id = fields.Integer()
question_type = fields.Selection([
('mcq', 'MCQ'),
('mcq_multi', 'MCQ Multiple'),
('tfng', 'True/False/Not Given'),
('ynng', 'Yes/No/Not Given'),
('gap_fill', 'Gap Fill'),
('short_answer', 'Short Answer'),
('form_completion', 'Form Completion'),
('note_completion', 'Note Completion'),
('map_labelling', 'Map Labelling'),
('matching', 'Matching'),
('summary_completion', 'Summary Completion'),
('heading_matching', 'Heading Matching'),
('matching_features', 'Matching Features'),
('numerical', 'Numerical'),
('expression', 'Expression'),
('code_completion', 'Code Completion'),
('code_output', 'Code Output'),
('sql_query', 'SQL Query'),
('matrix', 'Matrix'),
('graph', 'Graph'),
], required=True)
stem = fields.Text(required=True)
options = fields.Text()
correct_answer = fields.Text()
marks = fields.Float(default=1.0)
difficulty = fields.Selection([
('easy', 'Easy'),
('medium', 'Medium'),
('hard', 'Hard'),
], required=True)
status = fields.Selection([
('draft', 'Draft'),
('active', 'Active'),
('retired', 'Retired'),
('flagged', 'Flagged'),
], default='draft')
irt_a = fields.Float(default=1.0)
irt_b = fields.Float(default=0.0)
irt_c = fields.Float(default=0.25)
ai_generated = fields.Boolean(default=False)
ielts_certified = fields.Boolean(default=False)
format_validated = fields.Boolean(default=False)
subject_id = fields.Many2one('encoach.subject', ondelete='set null')
topic_id = fields.Many2one('encoach.topic', ondelete='set null')

View File

@@ -0,0 +1,18 @@
from odoo import models, fields
class EncoachRubric(models.Model):
_name = 'encoach.rubric'
_description = 'Scoring Rubric'
name = fields.Char(size=200, required=True)
skill = fields.Selection([
('writing', 'Writing'),
('speaking', 'Speaking'),
], required=True)
criteria = fields.Text(required=True)
exam_type = fields.Selection([
('academic', 'Academic'),
('general_training', 'General Training'),
('general_english', 'General English'),
])

View File

@@ -0,0 +1,24 @@
from odoo import models, fields
class EncoachSpeakingCard(models.Model):
_name = 'encoach.speaking.card'
_description = 'Speaking Cue Card'
part = fields.Integer(required=True)
topic = fields.Char(size=200, required=True)
questions = fields.Text()
bullet_points = fields.Text()
linked_card_id = fields.Many2one('encoach.speaking.card', ondelete='set null')
rubric_id = fields.Many2one('encoach.rubric', ondelete='set null')
difficulty = fields.Selection([
('easy', 'Easy'),
('medium', 'Medium'),
('hard', 'Hard'),
], required=True)
model_response = fields.Text()
ai_generated = fields.Boolean(default=False)
approved = fields.Boolean(default=False)
ielts_certified = fields.Boolean(default=False)
generation_brief = fields.Text()
validation_errors = fields.Text()

View File

@@ -0,0 +1,27 @@
from odoo import models, fields
class EncoachWritingPrompt(models.Model):
_name = 'encoach.writing.prompt'
_description = 'Writing Prompt'
exam_type = fields.Selection([
('academic', 'Academic'),
('general_training', 'General Training'),
('general_english', 'General English'),
], required=True)
task = fields.Selection([
('task1', 'Task 1'),
('task2', 'Task 2'),
], required=True)
writing_type = fields.Char(size=100)
prompt_text = fields.Text(required=True)
visual_url = fields.Char(size=500)
rubric_id = fields.Many2one('encoach.rubric', ondelete='set null')
min_words = fields.Integer()
model_answer = fields.Text()
ai_generated = fields.Boolean(default=False)
approved = fields.Boolean(default=False)
ielts_certified = fields.Boolean(default=False)
generation_brief = fields.Text()
validation_errors = fields.Text()