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:
@@ -0,0 +1,2 @@
|
||||
from . import csv_parser
|
||||
from . import credential_service
|
||||
@@ -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])
|
||||
69
custom_addons/encoach_entity_onboard/services/csv_parser.py
Normal file
69
custom_addons/encoach_entity_onboard/services/csv_parser.py
Normal 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
|
||||
Reference in New Issue
Block a user