feat(v3): restructure project + add complete frontend
- Restructure: move backend from new_project/ to backend/ - Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks) - Add docs/ with SRS specs, user stories, and workflow documentation - Update .gitignore for new directory layout Workflows implemented: WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration, WF4 General English Exam, WF5 Course Generation, WF6 Entity Student Onboarding, AI Course Generation, Adaptive Learning Engine UI, White-Label Branding, Score Release Made-with: Cursor
This commit is contained in:
10
custom_addons/encoach_exam_template/models/__init__.py
Normal file
10
custom_addons/encoach_exam_template/models/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
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
|
||||
30
custom_addons/encoach_exam_template/models/audio_file.py
Normal file
30
custom_addons/encoach_exam_template/models/audio_file.py
Normal 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()
|
||||
@@ -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)
|
||||
26
custom_addons/encoach_exam_template/models/exam_custom.py
Normal file
26
custom_addons/encoach_exam_template/models/exam_custom.py
Normal 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')
|
||||
@@ -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',
|
||||
)
|
||||
27
custom_addons/encoach_exam_template/models/exam_template.py
Normal file
27
custom_addons/encoach_exam_template/models/exam_template.py
Normal 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()
|
||||
46
custom_addons/encoach_exam_template/models/passage.py
Normal file
46
custom_addons/encoach_exam_template/models/passage.py
Normal 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
|
||||
69
custom_addons/encoach_exam_template/models/question.py
Normal file
69
custom_addons/encoach_exam_template/models/question.py
Normal 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')
|
||||
18
custom_addons/encoach_exam_template/models/rubric.py
Normal file
18
custom_addons/encoach_exam_template/models/rubric.py
Normal 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'),
|
||||
])
|
||||
24
custom_addons/encoach_exam_template/models/speaking_card.py
Normal file
24
custom_addons/encoach_exam_template/models/speaking_card.py
Normal 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()
|
||||
27
custom_addons/encoach_exam_template/models/writing_prompt.py
Normal file
27
custom_addons/encoach_exam_template/models/writing_prompt.py
Normal 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()
|
||||
Reference in New Issue
Block a user