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,2 @@
from . import csv_parser
from . import credential_service

View File

@@ -0,0 +1,72 @@
import logging
_logger = logging.getLogger(__name__)
class CredentialService:
def create_accounts(self, env, valid_rows, entity_id):
"""Create res.users records from validated CSV rows.
Each user gets password=national_id, account_source='entity_bulk_upload',
and account_status='activated'.
Args:
env: Odoo environment.
valid_rows: list of dicts with keys name, email, national_id, phone.
entity_id: int, the entity that owns these students.
Returns:
recordset of created res.users.
"""
User = env['res.users']
created_users = User
for row in valid_rows:
try:
user = User.sudo().create({
'name': row['name'],
'login': row['email'],
'email': row['email'],
'phone': row.get('phone', ''),
'password': row['national_id'],
'account_source': 'entity_bulk_upload',
'account_status': 'activated',
'entity_id': entity_id,
})
created_users |= user
except Exception:
_logger.exception("Failed to create account for %s", row.get('email'))
return created_users
def send_credentials(self, env, user_ids):
"""Send credential emails to newly created users.
Args:
env: Odoo environment.
user_ids: list of int, res.users IDs.
"""
Mail = env['mail.mail']
for user in env['res.users'].sudo().browse(user_ids):
mail_values = {
'subject': 'Your EnCoach Account Credentials',
'email_from': env.company.email or 'noreply@encoach.com',
'email_to': user.email,
'body_html': (
f'<p>Dear {user.name},</p>'
f'<p>Your EnCoach account has been created.</p>'
f'<p>Login: {user.login}</p>'
f'<p>Password: Your National ID</p>'
f'<p>Please change your password after first login.</p>'
),
}
mail = Mail.sudo().create(mail_values)
mail.send()
def resend_credentials(self, env, user_id):
"""Resend credential email for a single user.
Args:
env: Odoo environment.
user_id: int, res.users ID.
"""
self.send_credentials(env, [user_id])

View File

@@ -0,0 +1,69 @@
import csv
import io
REQUIRED_COLUMNS = {'name', 'email', 'national_id', 'phone'}
class CsvParser:
def validate(self, file_content):
"""Parse CSV content and validate required columns.
Args:
file_content: raw CSV string or bytes.
Returns:
dict with 'valid_rows' (list of dicts) and 'errors' (list of str).
"""
if isinstance(file_content, bytes):
file_content = file_content.decode('utf-8-sig')
errors = []
valid_rows = []
reader = csv.DictReader(io.StringIO(file_content))
if not reader.fieldnames:
return {'valid_rows': [], 'errors': ['Empty CSV file or missing header row']}
header_set = {col.strip().lower() for col in reader.fieldnames}
missing = REQUIRED_COLUMNS - header_set
if missing:
return {
'valid_rows': [],
'errors': [f"Missing required columns: {', '.join(sorted(missing))}"],
}
for idx, row in enumerate(reader, start=2):
parsed = self.parse_row(row)
row_errors = []
if not parsed.get('name'):
row_errors.append(f"Row {idx}: 'name' is required")
if not parsed.get('email'):
row_errors.append(f"Row {idx}: 'email' is required")
if not parsed.get('national_id'):
row_errors.append(f"Row {idx}: 'national_id' is required")
if row_errors:
errors.extend(row_errors)
else:
valid_rows.append(parsed)
return {'valid_rows': valid_rows, 'errors': errors}
def parse_row(self, row):
"""Normalize a single CSV row.
Args:
row: dict from csv.DictReader.
Returns:
dict with trimmed, normalized values.
"""
normalized = {}
for key, value in row.items():
clean_key = key.strip().lower()
normalized[clean_key] = value.strip() if value else ''
return normalized