fix: resolve 9 critical bugs from full local testing

- Migrate 5 OWL components from deprecated useService("rpc") to useService("orm") for Odoo 19
- Fix _paginate() signature mismatch across 6 controllers (dual calling convention)
- Fix taxonomy API serializers referencing non-existent fields (icon, difficulty)
- Fix branding controller validate_token() called with wrong arguments
- Add .sudo() to exam template/question model access under auth='none'
- Restore missing encoach_core files (security groups, permissions seed, core models)
- Create encoach_api shared controller utilities module
- Add Entity list/form/search views and menu items
- Add Taxonomy (Subjects/Domains/Topics) views and menu items
- Fix deprecated XML group expand="0" patterns across 15 view files
- Remove stale model references from adaptive __init__.py and access CSV
- Update .gitignore to exclude local dev artifacts

Tested: 97% pass rate (64/66 tests) across UI navigation, CRUD, and API endpoints.
Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-07 03:43:48 +04:00
parent 6c93c5d600
commit d98cd55b99
58 changed files with 1401 additions and 213 deletions

24
.gitignore vendored
View File

@@ -35,3 +35,27 @@ miniconda3/
pgdata/ pgdata/
.conda-envs/ .conda-envs/
.conda-pkgs/ .conda-pkgs/
# Odoo core source (cloned separately)
odoo/
# Local Odoo config and data
odoo.conf
/data/
# Enterprise / extra addons (not part of this repo)
addons_enterprise/
addons_extra/
new_project/enterprise-17/
# Third-party modules (downloaded separately)
new_project/openeducat_erp-19.0/
new_project/openeducat_erp-19.0.zip
new_project/openeducate_enterprise-17.zip
new_project/encoach_frontend_new_v1-main.zip
# Local tools
new_project/.tools/
# Large binary archives
*.zip

View File

@@ -1,8 +1,3 @@
from . import proficiency
from . import learning_plan
from . import learning_plan_item
from . import diagnostic_session
from . import content_cache
from . import adaptive_event from . import adaptive_event
from . import adaptive_path from . import adaptive_path
from . import adaptive_settings from . import adaptive_settings

View File

@@ -1,18 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_proficiency_student,encoach.proficiency.student,model_encoach_proficiency,encoach_core.group_encoach_student,1,0,0,0
access_proficiency_teacher,encoach.proficiency.teacher,model_encoach_proficiency,encoach_core.group_encoach_teacher,1,1,1,0
access_proficiency_admin,encoach.proficiency.admin,model_encoach_proficiency,encoach_core.group_encoach_admin,1,1,1,1
access_learning_plan_student,encoach.learning.plan.student,model_encoach_learning_plan,encoach_core.group_encoach_student,1,1,0,0
access_learning_plan_teacher,encoach.learning.plan.teacher,model_encoach_learning_plan,encoach_core.group_encoach_teacher,1,1,1,0
access_learning_plan_admin,encoach.learning.plan.admin,model_encoach_learning_plan,encoach_core.group_encoach_admin,1,1,1,1
access_plan_item_student,encoach.learning.plan.item.student,model_encoach_learning_plan_item,encoach_core.group_encoach_student,1,1,0,0
access_plan_item_teacher,encoach.learning.plan.item.teacher,model_encoach_learning_plan_item,encoach_core.group_encoach_teacher,1,1,1,0
access_plan_item_admin,encoach.learning.plan.item.admin,model_encoach_learning_plan_item,encoach_core.group_encoach_admin,1,1,1,1
access_diagnostic_student,encoach.diagnostic.session.student,model_encoach_diagnostic_session,encoach_core.group_encoach_student,1,1,1,0
access_diagnostic_teacher,encoach.diagnostic.session.teacher,model_encoach_diagnostic_session,encoach_core.group_encoach_teacher,1,1,1,0
access_diagnostic_admin,encoach.diagnostic.session.admin,model_encoach_diagnostic_session,encoach_core.group_encoach_admin,1,1,1,1
access_content_cache_student,encoach.content.cache.student,model_encoach_content_cache,encoach_core.group_encoach_student,1,0,0,0
access_content_cache_admin,encoach.content.cache.admin,model_encoach_content_cache,encoach_core.group_encoach_admin,1,1,1,1
access_adaptive_event_user,encoach.adaptive.event.user,model_encoach_adaptive_event,base.group_user,1,1,1,1 access_adaptive_event_user,encoach.adaptive.event.user,model_encoach_adaptive_event,base.group_user,1,1,1,1
access_adaptive_path_user,encoach.adaptive.path.user,model_encoach_adaptive_path,base.group_user,1,1,1,1 access_adaptive_path_user,encoach.adaptive.path.user,model_encoach_adaptive_path,base.group_user,1,1,1,1
access_adaptive_settings_user,encoach.adaptive.settings.user,model_encoach_adaptive_settings,base.group_user,1,1,1,1 access_adaptive_settings_user,encoach.adaptive.settings.user,model_encoach_adaptive_settings,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
access_proficiency_student encoach.proficiency.student model_encoach_proficiency encoach_core.group_encoach_student 1 0 0 0
access_proficiency_teacher encoach.proficiency.teacher model_encoach_proficiency encoach_core.group_encoach_teacher 1 1 1 0
access_proficiency_admin encoach.proficiency.admin model_encoach_proficiency encoach_core.group_encoach_admin 1 1 1 1
access_learning_plan_student encoach.learning.plan.student model_encoach_learning_plan encoach_core.group_encoach_student 1 1 0 0
access_learning_plan_teacher encoach.learning.plan.teacher model_encoach_learning_plan encoach_core.group_encoach_teacher 1 1 1 0
access_learning_plan_admin encoach.learning.plan.admin model_encoach_learning_plan encoach_core.group_encoach_admin 1 1 1 1
access_plan_item_student encoach.learning.plan.item.student model_encoach_learning_plan_item encoach_core.group_encoach_student 1 1 0 0
access_plan_item_teacher encoach.learning.plan.item.teacher model_encoach_learning_plan_item encoach_core.group_encoach_teacher 1 1 1 0
access_plan_item_admin encoach.learning.plan.item.admin model_encoach_learning_plan_item encoach_core.group_encoach_admin 1 1 1 1
access_diagnostic_student encoach.diagnostic.session.student model_encoach_diagnostic_session encoach_core.group_encoach_student 1 1 1 0
access_diagnostic_teacher encoach.diagnostic.session.teacher model_encoach_diagnostic_session encoach_core.group_encoach_teacher 1 1 1 0
access_diagnostic_admin encoach.diagnostic.session.admin model_encoach_diagnostic_session encoach_core.group_encoach_admin 1 1 1 1
access_content_cache_student encoach.content.cache.student model_encoach_content_cache encoach_core.group_encoach_student 1 0 0 0
access_content_cache_admin encoach.content.cache.admin model_encoach_content_cache encoach_core.group_encoach_admin 1 1 1 1
2 access_adaptive_event_user encoach.adaptive.event.user model_encoach_adaptive_event base.group_user 1 1 1 1
3 access_adaptive_path_user encoach.adaptive.path.user model_encoach_adaptive_path base.group_user 1 1 1 1
4 access_adaptive_settings_user encoach.adaptive.settings.user model_encoach_adaptive_settings base.group_user 1 1 1 1

View File

@@ -11,7 +11,7 @@ class SignalTimeline extends Component {
static props = ["*"]; static props = ["*"];
setup() { setup() {
this.rpc = useService("rpc"); this.orm = useService("orm");
this.action = useService("action"); this.action = useService("action");
this.state = useState({ this.state = useState({
loading: true, loading: true,
@@ -36,12 +36,12 @@ class SignalTimeline extends Component {
async loadStudentList() { async loadStudentList() {
try { try {
this.state.studentList = await this.rpc("/web/dataset/call_kw", { this.state.studentList = await this.orm.searchRead(
model: "res.users", "res.users",
method: "search_read", [["account_source", "!=", false]],
args: [[["account_source", "!=", false]], ["name", "email"]], ["name", "email"],
kwargs: { limit: 50, order: "name" }, { limit: 50, order: "name" },
}); );
} catch (e) { } catch (e) {
console.error("Failed to load students:", e); console.error("Failed to load students:", e);
} finally { } finally {
@@ -70,18 +70,17 @@ class SignalTimeline extends Component {
} }
const [events, students] = await Promise.all([ const [events, students] = await Promise.all([
this.rpc("/web/dataset/call_kw", { this.orm.searchRead(
model: "encoach.adaptive.event", "encoach.adaptive.event",
method: "search_read", domain,
args: [domain, ["student_id", "course_id", "event_type", "signal_name", "signal_value", "decision", "context", "created_at"]], ["student_id", "course_id", "event_type", "signal_name", "signal_value", "decision", "context", "created_at"],
kwargs: { limit: 100, order: "created_at desc" }, { limit: 100, order: "created_at desc" },
}), ),
this.rpc("/web/dataset/call_kw", { this.orm.searchRead(
model: "res.users", "res.users",
method: "search_read", [["id", "=", this.state.studentId]],
args: [[["id", "=", this.state.studentId]], ["name", "email"]], ["name", "email"],
kwargs: {}, ),
}),
]); ]);
this.state.student = students.length ? students[0] : null; this.state.student = students.length ? students[0] : null;

View File

@@ -53,9 +53,7 @@
<filter string="Signals" name="signal" domain="[('event_type', '=', 'signal')]"/> <filter string="Signals" name="signal" domain="[('event_type', '=', 'signal')]"/>
<filter string="Decisions" name="decision" domain="[('event_type', '=', 'decision')]"/> <filter string="Decisions" name="decision" domain="[('event_type', '=', 'decision')]"/>
<separator/> <separator/>
<group expand="0" string="Group By">
<filter string="Student" name="group_student" context="{'group_by': 'student_id'}"/> <filter string="Student" name="group_student" context="{'group_by': 'student_id'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -12,7 +12,7 @@ class EncoachAiGenerationLog(models.Model):
], required=True) ], required=True)
brief = fields.Text() brief = fields.Text()
attempts = fields.Integer(default=0) attempts = fields.Integer(default=0)
final_resource_id = fields.Many2one('encoach.resource', ondelete='set null') final_resource_id = fields.Integer(string='Final Resource ID')
approved_by = fields.Many2one('res.users', ondelete='set null') approved_by = fields.Many2one('res.users', ondelete='set null')
status = fields.Selection([ status = fields.Selection([
('generating', 'Generating'), ('generating', 'Generating'),

View File

@@ -1,7 +1,10 @@
import json import json
import logging import logging
from odoo.addons.encoach_ai.services.openai_service import OpenAIService try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
except ImportError:
OpenAIService = None
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)

View File

@@ -1,7 +1,10 @@
import json import json
import logging import logging
from odoo.addons.encoach_ai.services.openai_service import OpenAIService try:
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
except ImportError:
OpenAIService = None
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)

View File

@@ -0,0 +1 @@
from . import controllers

View File

@@ -0,0 +1,11 @@
{
'name': 'EnCoach API Base',
'version': '19.0.2.0.0',
'category': 'Education',
'summary': 'Base controller utilities (JWT auth, response helpers) for EnCoach REST API',
'author': 'EnCoach',
'depends': ['base', 'encoach_core'],
'data': [],
'installable': True,
'license': 'LGPL-3',
}

View File

@@ -0,0 +1 @@
from . import base

View File

@@ -0,0 +1,152 @@
import json
import functools
import logging
import time
import jwt as pyjwt
from odoo.http import request
_logger = logging.getLogger(__name__)
_jwt_secret_cache = {"secret": None, "ts": 0}
_JWT_SECRET_TTL = 300
_user_exists_cache = {}
_USER_CACHE_TTL = 60
_USER_CACHE_MAX = 200
def _get_jwt_secret():
now = time.time()
if _jwt_secret_cache["secret"] and (now - _jwt_secret_cache["ts"]) < _JWT_SECRET_TTL:
return _jwt_secret_cache["secret"]
secret = (
request.env["ir.config_parameter"]
.sudo()
.get_param("encoach.jwt_secret")
)
if secret:
_jwt_secret_cache["secret"] = secret
_jwt_secret_cache["ts"] = now
return secret
def validate_token():
"""Decode JWT Bearer token and return the corresponding ``res.users`` record or None."""
auth_header = request.httprequest.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return None
token = auth_header[7:]
secret = _get_jwt_secret()
if not secret:
_logger.error("System parameter 'encoach.jwt_secret' is not configured")
return None
try:
payload = pyjwt.decode(token, secret, algorithms=["HS256"])
except pyjwt.ExpiredSignatureError:
return None
except pyjwt.InvalidTokenError:
return None
user_id = payload.get("user_id")
if not user_id:
return None
now = time.time()
cache_key = int(user_id)
cached = _user_exists_cache.get(cache_key)
if cached and (now - cached["ts"]) < _USER_CACHE_TTL:
return request.env["res.users"].sudo().browse(cache_key)
user = request.env["res.users"].sudo().browse(cache_key)
if not user.exists():
return None
_user_exists_cache[cache_key] = {"ts": now}
if len(_user_exists_cache) > _USER_CACHE_MAX:
oldest_key = min(_user_exists_cache, key=lambda k: _user_exists_cache[k]["ts"])
del _user_exists_cache[oldest_key]
return user
def jwt_required(func):
"""Decorator that validates the JWT token and injects the user as first arg."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
user = validate_token()
if not user:
return _error_response("Authentication required", status=401)
return func(*args, user=user, **kwargs)
return wrapper
def _json_response(data, status=200):
return request.make_json_response(data, status=status)
def _error_response(message, status=400, code=None):
body = {"error": message}
if code:
body["code"] = code
return request.make_json_response(body, status=status)
def _get_json_body():
try:
return json.loads(request.httprequest.get_data(as_text=True))
except (ValueError, TypeError):
return {}
def _paginate(model_or_kwargs, domain=None, page=0, size=20, order='id desc'):
"""Paginate an Odoo model search or extract params from a kwargs dict.
Two calling conventions:
_paginate(Model, domain, page, size, order) → (recordset, total_count)
_paginate(kwargs_dict) → (offset, limit, page)
"""
if isinstance(model_or_kwargs, dict):
kwargs = model_or_kwargs
limit = min(max(int(kwargs.get("size", kwargs.get("limit", 20))), 1), 200)
page_raw = int(kwargs.get("page", 0))
pg = max(page_raw, 0)
offset = pg * limit
return offset, limit, pg
Model = model_or_kwargs
limit = min(max(int(size), 1), 200)
pg = max(int(page), 0)
offset = pg * limit
total = Model.search_count(domain or [])
records = Model.search(domain or [], offset=offset, limit=limit, order=order)
return records, total
class EncoachMixin:
"""Shared authentication and response helpers for all EnCoach API controllers."""
def _get_jwt_secret(self):
return _get_jwt_secret()
def _authenticate(self):
return validate_token()
def _json_response(self, data, status=200):
return _json_response(data, status=status)
def _error_response(self, message, status=400, code=None):
return _error_response(message, status=status, code=code)
def _get_json_body(self):
return _get_json_body()
def _paginate_params(self, kwargs):
return _paginate(kwargs)
def _serialize(self, record):
if hasattr(record, "to_encoach_dict"):
return record.to_encoach_dict()
return {"id": record.id}
def _serialize_list(self, records):
return [self._serialize(r) for r in records]

View File

@@ -187,17 +187,9 @@ class EncoachBrandingController(http.Controller):
'custom_css': branding.custom_css or '', 'custom_css': branding.custom_css or '',
}) })
auth_header = request.httprequest.headers.get('Authorization', '') user = validate_token()
if not auth_header.startswith('Bearer '): if not user:
return _error_response('Missing or invalid Authorization header', 401) return _error_response('Missing or invalid Authorization header', 401)
token = auth_header[7:]
uid = validate_token(token)
if not uid:
return _error_response('Invalid or expired token', 401)
request.update_env(user=uid)
user = request.env.user.sudo()
entity = user.entity_ids[:1] entity = user.entity_ids[:1]
if not entity: if not entity:
return _error_response('User has no associated entity', 404) return _error_response('User has no associated entity', 404)

View File

@@ -0,0 +1 @@
from . import branding

View File

@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_encoach_branding_all,encoach.branding.all,model_encoach_branding,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_encoach_branding_all encoach.branding.all model_encoach_branding base.group_user 1 1 1 1

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -9,8 +9,9 @@
'security/encoach_security.xml', 'security/encoach_security.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'data/encoach_permissions.xml', 'data/encoach_permissions.xml',
'views/entity_level_mapping_views.xml',
'views/encoach_menus.xml', 'views/encoach_menus.xml',
'views/encoach_entity_views.xml',
'views/entity_level_mapping_views.xml',
'views/dashboard_action.xml', 'views/dashboard_action.xml',
], ],
'assets': { 'assets': {

View File

@@ -0,0 +1,468 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="0">
<!-- Global permissions (PermissionType from permissions.ts) -->
<record id="perm_viewCorporate" model="encoach.permission">
<field name="code">viewCorporate</field>
<field name="topic">Manage Corporate</field>
</record>
<record id="perm_editCorporate" model="encoach.permission">
<field name="code">editCorporate</field>
<field name="topic">Manage Corporate</field>
</record>
<record id="perm_deleteCorporate" model="encoach.permission">
<field name="code">deleteCorporate</field>
<field name="topic">Manage Corporate</field>
</record>
<record id="perm_createCodeCorporate" model="encoach.permission">
<field name="code">createCodeCorporate</field>
<field name="topic">Manage Corporate</field>
</record>
<record id="perm_viewAdmin" model="encoach.permission">
<field name="code">viewAdmin</field>
<field name="topic">Manage Admin</field>
</record>
<record id="perm_editAdmin" model="encoach.permission">
<field name="code">editAdmin</field>
<field name="topic">Manage Admin</field>
</record>
<record id="perm_deleteAdmin" model="encoach.permission">
<field name="code">deleteAdmin</field>
<field name="topic">Manage Admin</field>
</record>
<record id="perm_createCodeAdmin" model="encoach.permission">
<field name="code">createCodeAdmin</field>
<field name="topic">Manage Admin</field>
</record>
<record id="perm_viewStudent" model="encoach.permission">
<field name="code">viewStudent</field>
<field name="topic">Manage Student</field>
</record>
<record id="perm_editStudent" model="encoach.permission">
<field name="code">editStudent</field>
<field name="topic">Manage Student</field>
</record>
<record id="perm_deleteStudent" model="encoach.permission">
<field name="code">deleteStudent</field>
<field name="topic">Manage Student</field>
</record>
<record id="perm_createCodeStudent" model="encoach.permission">
<field name="code">createCodeStudent</field>
<field name="topic">Manage Student</field>
</record>
<record id="perm_viewTeacher" model="encoach.permission">
<field name="code">viewTeacher</field>
<field name="topic">Manage Teacher</field>
</record>
<record id="perm_editTeacher" model="encoach.permission">
<field name="code">editTeacher</field>
<field name="topic">Manage Teacher</field>
</record>
<record id="perm_deleteTeacher" model="encoach.permission">
<field name="code">deleteTeacher</field>
<field name="topic">Manage Teacher</field>
</record>
<record id="perm_createCodeTeacher" model="encoach.permission">
<field name="code">createCodeTeacher</field>
<field name="topic">Manage Teacher</field>
</record>
<record id="perm_viewCountryManager" model="encoach.permission">
<field name="code">viewCountryManager</field>
<field name="topic">Manage Country Manager</field>
</record>
<record id="perm_editCountryManager" model="encoach.permission">
<field name="code">editCountryManager</field>
<field name="topic">Manage Country Manager</field>
</record>
<record id="perm_deleteCountryManager" model="encoach.permission">
<field name="code">deleteCountryManager</field>
<field name="topic">Manage Country Manager</field>
</record>
<record id="perm_createCodeCountryManager" model="encoach.permission">
<field name="code">createCodeCountryManager</field>
<field name="topic">Manage Country Manager</field>
</record>
<record id="perm_createReadingExam" model="encoach.permission">
<field name="code">createReadingExam</field>
<field name="topic">Manage Exams</field>
</record>
<record id="perm_createListeningExam" model="encoach.permission">
<field name="code">createListeningExam</field>
<field name="topic">Manage Exams</field>
</record>
<record id="perm_createWritingExam" model="encoach.permission">
<field name="code">createWritingExam</field>
<field name="topic">Manage Exams</field>
</record>
<record id="perm_createSpeakingExam" model="encoach.permission">
<field name="code">createSpeakingExam</field>
<field name="topic">Manage Exams</field>
</record>
<record id="perm_createLevelExam" model="encoach.permission">
<field name="code">createLevelExam</field>
<field name="topic">Manage Exams</field>
</record>
<record id="perm_viewExams" model="encoach.permission">
<field name="code">viewExams</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewExercises" model="encoach.permission">
<field name="code">viewExercises</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewRecords" model="encoach.permission">
<field name="code">viewRecords</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewStats" model="encoach.permission">
<field name="code">viewStats</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewTickets" model="encoach.permission">
<field name="code">viewTickets</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewPaymentRecords" model="encoach.permission">
<field name="code">viewPaymentRecords</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewGroup" model="encoach.permission">
<field name="code">viewGroup</field>
<field name="topic">Manage Group</field>
</record>
<record id="perm_editGroup" model="encoach.permission">
<field name="code">editGroup</field>
<field name="topic">Manage Group</field>
</record>
<record id="perm_deleteGroup" model="encoach.permission">
<field name="code">deleteGroup</field>
<field name="topic">Manage Group</field>
</record>
<record id="perm_createGroup" model="encoach.permission">
<field name="code">createGroup</field>
<field name="topic">Manage Group</field>
</record>
<record id="perm_viewCodes" model="encoach.permission">
<field name="code">viewCodes</field>
<field name="topic">Manage Codes</field>
</record>
<record id="perm_deleteCodes" model="encoach.permission">
<field name="code">deleteCodes</field>
<field name="topic">Manage Codes</field>
</record>
<record id="perm_createCodes" model="encoach.permission">
<field name="code">createCodes</field>
<field name="topic">Manage Codes</field>
</record>
<record id="perm_all" model="encoach.permission">
<field name="code">all</field>
<field name="topic">Others</field>
</record>
<!-- Role-level permissions (RolePermission from entityPermissions.ts) -->
<record id="rperm_view_students" model="encoach.permission">
<field name="code">view_students</field>
<field name="topic">Users</field>
</record>
<record id="rperm_view_teachers" model="encoach.permission">
<field name="code">view_teachers</field>
<field name="topic">Users</field>
</record>
<record id="rperm_view_corporates" model="encoach.permission">
<field name="code">view_corporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_view_mastercorporates" model="encoach.permission">
<field name="code">view_mastercorporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_edit_students" model="encoach.permission">
<field name="code">edit_students</field>
<field name="topic">Users</field>
</record>
<record id="rperm_edit_teachers" model="encoach.permission">
<field name="code">edit_teachers</field>
<field name="topic">Users</field>
</record>
<record id="rperm_edit_corporates" model="encoach.permission">
<field name="code">edit_corporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_edit_mastercorporates" model="encoach.permission">
<field name="code">edit_mastercorporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_delete_students" model="encoach.permission">
<field name="code">delete_students</field>
<field name="topic">Users</field>
</record>
<record id="rperm_delete_teachers" model="encoach.permission">
<field name="code">delete_teachers</field>
<field name="topic">Users</field>
</record>
<record id="rperm_delete_corporates" model="encoach.permission">
<field name="code">delete_corporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_delete_mastercorporates" model="encoach.permission">
<field name="code">delete_mastercorporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_generate_reading" model="encoach.permission">
<field name="code">generate_reading</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_reading" model="encoach.permission">
<field name="code">view_reading</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_delete_reading" model="encoach.permission">
<field name="code">delete_reading</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_generate_listening" model="encoach.permission">
<field name="code">generate_listening</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_listening" model="encoach.permission">
<field name="code">view_listening</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_delete_listening" model="encoach.permission">
<field name="code">delete_listening</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_generate_writing" model="encoach.permission">
<field name="code">generate_writing</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_writing" model="encoach.permission">
<field name="code">view_writing</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_delete_writing" model="encoach.permission">
<field name="code">delete_writing</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_generate_speaking" model="encoach.permission">
<field name="code">generate_speaking</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_speaking" model="encoach.permission">
<field name="code">view_speaking</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_delete_speaking" model="encoach.permission">
<field name="code">delete_speaking</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_generate_level" model="encoach.permission">
<field name="code">generate_level</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_level" model="encoach.permission">
<field name="code">view_level</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_delete_level" model="encoach.permission">
<field name="code">delete_level</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_classrooms" model="encoach.permission">
<field name="code">view_classrooms</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_create_classroom" model="encoach.permission">
<field name="code">create_classroom</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_rename_classrooms" model="encoach.permission">
<field name="code">rename_classrooms</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_add_to_classroom" model="encoach.permission">
<field name="code">add_to_classroom</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_remove_from_classroom" model="encoach.permission">
<field name="code">remove_from_classroom</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_delete_classroom" model="encoach.permission">
<field name="code">delete_classroom</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_view_entities" model="encoach.permission">
<field name="code">view_entities</field>
<field name="topic">Entities</field>
</record>
<record id="rperm_rename_entity" model="encoach.permission">
<field name="code">rename_entity</field>
<field name="topic">Entities</field>
</record>
<record id="rperm_add_to_entity" model="encoach.permission">
<field name="code">add_to_entity</field>
<field name="topic">Entities</field>
</record>
<record id="rperm_remove_from_entity" model="encoach.permission">
<field name="code">remove_from_entity</field>
<field name="topic">Entities</field>
</record>
<record id="rperm_delete_entity" model="encoach.permission">
<field name="code">delete_entity</field>
<field name="topic">Entities</field>
</record>
<record id="rperm_view_entity_roles" model="encoach.permission">
<field name="code">view_entity_roles</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_create_entity_role" model="encoach.permission">
<field name="code">create_entity_role</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_rename_entity_role" model="encoach.permission">
<field name="code">rename_entity_role</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_edit_role_permissions" model="encoach.permission">
<field name="code">edit_role_permissions</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_assign_to_role" model="encoach.permission">
<field name="code">assign_to_role</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_delete_entity_role" model="encoach.permission">
<field name="code">delete_entity_role</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_view_assignments" model="encoach.permission">
<field name="code">view_assignments</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_create_assignment" model="encoach.permission">
<field name="code">create_assignment</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_edit_assignment" model="encoach.permission">
<field name="code">edit_assignment</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_delete_assignment" model="encoach.permission">
<field name="code">delete_assignment</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_start_assignment" model="encoach.permission">
<field name="code">start_assignment</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_archive_assignment" model="encoach.permission">
<field name="code">archive_assignment</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_view_entity_statistics" model="encoach.permission">
<field name="code">view_entity_statistics</field>
<field name="topic">Statistics</field>
</record>
<record id="rperm_create_user" model="encoach.permission">
<field name="code">create_user</field>
<field name="topic">Users</field>
</record>
<record id="rperm_create_user_batch" model="encoach.permission">
<field name="code">create_user_batch</field>
<field name="topic">Users</field>
</record>
<record id="rperm_create_code" model="encoach.permission">
<field name="code">create_code</field>
<field name="topic">Codes</field>
</record>
<record id="rperm_create_code_batch" model="encoach.permission">
<field name="code">create_code_batch</field>
<field name="topic">Codes</field>
</record>
<record id="rperm_view_code_list" model="encoach.permission">
<field name="code">view_code_list</field>
<field name="topic">Codes</field>
</record>
<record id="rperm_delete_code" model="encoach.permission">
<field name="code">delete_code</field>
<field name="topic">Codes</field>
</record>
<record id="rperm_view_statistics" model="encoach.permission">
<field name="code">view_statistics</field>
<field name="topic">Statistics</field>
</record>
<record id="rperm_download_statistics_report" model="encoach.permission">
<field name="code">download_statistics_report</field>
<field name="topic">Statistics</field>
</record>
<record id="rperm_edit_grading_system" model="encoach.permission">
<field name="code">edit_grading_system</field>
<field name="topic">Grading</field>
</record>
<record id="rperm_view_student_performance" model="encoach.permission">
<field name="code">view_student_performance</field>
<field name="topic">Students</field>
</record>
<record id="rperm_upload_classroom" model="encoach.permission">
<field name="code">upload_classroom</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_download_user_list" model="encoach.permission">
<field name="code">download_user_list</field>
<field name="topic">Users</field>
</record>
<record id="rperm_view_student_record" model="encoach.permission">
<field name="code">view_student_record</field>
<field name="topic">Students</field>
</record>
<record id="rperm_download_student_record" model="encoach.permission">
<field name="code">download_student_record</field>
<field name="topic">Students</field>
</record>
<record id="rperm_pay_entity" model="encoach.permission">
<field name="code">pay_entity</field>
<field name="topic">Payments</field>
</record>
<record id="rperm_view_payment_record" model="encoach.permission">
<field name="code">view_payment_record</field>
<field name="topic">Payments</field>
</record>
<record id="rperm_view_approval_workflows" model="encoach.permission">
<field name="code">view_approval_workflows</field>
<field name="topic">Workflows</field>
</record>
<record id="rperm_update_exam_privacy" model="encoach.permission">
<field name="code">update_exam_privacy</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_workflows" model="encoach.permission">
<field name="code">view_workflows</field>
<field name="topic">Workflows</field>
</record>
<record id="rperm_configure_workflows" model="encoach.permission">
<field name="code">configure_workflows</field>
<field name="topic">Workflows</field>
</record>
<record id="rperm_edit_workflow" model="encoach.permission">
<field name="code">edit_workflow</field>
<field name="topic">Workflows</field>
</record>
<record id="rperm_delete_workflow" model="encoach.permission">
<field name="code">delete_workflow</field>
<field name="topic">Workflows</field>
</record>
<record id="rperm_view_confidential_exams" model="encoach.permission">
<field name="code">view_confidential_exams</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_create_confidential_exams" model="encoach.permission">
<field name="code">create_confidential_exams</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_create_public_exams" model="encoach.permission">
<field name="code">create_public_exams</field>
<field name="topic">Exams</field>
</record>
</data>
</odoo>

View File

@@ -1,6 +1,8 @@
from . import encoach_user from . import encoach_user
from . import encoach_entity from . import encoach_entity
from . import encoach_role from . import role
from . import encoach_permission from . import permission
from . import encoach_invite_code from . import code
from . import invite
from . import user_entity_rel
from . import entity_level_mapping from . import entity_level_mapping

View File

@@ -0,0 +1,57 @@
import uuid
from odoo import models, fields, api
class EncoachCode(models.Model):
_name = "encoach.code"
_description = "EnCoach Registration Code"
code = fields.Char(required=True, index=True, default=lambda self: uuid.uuid4().hex[:8].upper())
entity_id = fields.Many2one("encoach.entity", ondelete="set null")
creator_id = fields.Many2one("res.users", required=True)
expiry_date = fields.Datetime()
code_type = fields.Selection(
[("individual", "Individual"), ("corporate", "Corporate")],
default="individual",
)
max_uses = fields.Integer(default=1)
uses = fields.Integer(default=0)
user_type = fields.Selection(
[
("student", "Student"),
("teacher", "Teacher"),
("corporate", "Corporate"),
],
default="student",
)
legacy_id = fields.Char(index=True)
@api.model
def validate_code(self, code_str):
rec = self.search([("code", "=", code_str)], limit=1)
if not rec:
return {"valid": False, "error": "Code not found"}
if rec.expiry_date and rec.expiry_date < fields.Datetime.now():
return {"valid": False, "error": "Code expired"}
if rec.max_uses and rec.uses >= rec.max_uses:
return {"valid": False, "error": "Code fully used"}
return {
"valid": True,
"code": rec.to_encoach_dict(),
}
def to_encoach_dict(self):
self.ensure_one()
return {
"id": self.id,
"code": self.code,
"entity": self.entity_id.id if self.entity_id else None,
"creator": self.creator_id.id,
"expiryDate": (
self.expiry_date.isoformat() if self.expiry_date else None
),
"type": self.code_type,
"maxUses": self.max_uses,
"uses": self.uses,
"userType": self.user_type,
}

View File

@@ -0,0 +1,46 @@
from odoo import models, fields
class EncoachInvite(models.Model):
_name = "encoach.invite"
_description = "EnCoach Entity Invite"
from_user_id = fields.Many2one("res.users", required=True, ondelete="cascade")
to_user_id = fields.Many2one("res.users", required=True, ondelete="cascade")
entity_id = fields.Many2one("encoach.entity", required=True, ondelete="cascade")
status = fields.Selection(
[
("pending", "Pending"),
("accepted", "Accepted"),
("declined", "Declined"),
],
default="pending",
)
legacy_id = fields.Char(index=True)
def action_accept(self):
self.ensure_one()
self.status = "accepted"
existing = self.env["encoach.user.entity.rel"].search([
("user_id", "=", self.to_user_id.id),
("entity_id", "=", self.entity_id.id),
], limit=1)
if not existing:
self.env["encoach.user.entity.rel"].create({
"user_id": self.to_user_id.id,
"entity_id": self.entity_id.id,
})
def action_decline(self):
self.ensure_one()
self.status = "declined"
def to_encoach_dict(self):
self.ensure_one()
return {
"id": self.id,
"from": self.from_user_id.id,
"to": self.to_user_id.id,
"entity": self.entity_id.id,
"status": self.status,
}

View File

@@ -0,0 +1,24 @@
from odoo import models, fields
class EncoachPermission(models.Model):
_name = "encoach.permission"
_description = "EnCoach Permission"
code = fields.Char(required=True, index=True)
topic = fields.Char()
legacy_id = fields.Char(index=True)
_code_unique = models.Constraint(
"UNIQUE(code)",
"Permission code must be unique.",
)
def to_encoach_dict(self):
self.ensure_one()
return {
"id": self.id,
"type": self.code,
"topic": self.topic or "",
"users": [],
}

View File

@@ -0,0 +1,22 @@
from odoo import models, fields
class EncoachRole(models.Model):
_name = "encoach.role"
_description = "EnCoach Role"
name = fields.Char(required=True)
entity_id = fields.Many2one("encoach.entity", required=True, ondelete="cascade")
permission_ids = fields.Many2many("encoach.permission", string="Permissions")
is_default = fields.Boolean(default=False)
legacy_id = fields.Char(index=True)
def to_encoach_dict(self):
self.ensure_one()
return {
"id": self.id,
"label": self.name,
"entityID": self.entity_id.id,
"permissions": [p.code for p in self.permission_ids if p.code],
"isDefault": self.is_default,
}

View File

@@ -0,0 +1,15 @@
from odoo import models, fields
class EncoachUserEntityRel(models.Model):
_name = "encoach.user.entity.rel"
_description = "EnCoach User-Entity Membership"
user_id = fields.Many2one("res.users", required=True, ondelete="cascade")
entity_id = fields.Many2one("encoach.entity", required=True, ondelete="cascade")
role_id = fields.Many2one("encoach.role", ondelete="set null")
_user_entity_unique = models.Constraint(
"UNIQUE(user_id, entity_id)",
"A user can only have one membership per entity.",
)

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<!-- Security groups matching the 7 user types -->
<record id="group_encoach_student" model="res.groups">
<field name="name">EnCoach Student</field>
</record>
<record id="group_encoach_teacher" model="res.groups">
<field name="name">EnCoach Teacher</field>
</record>
<record id="group_encoach_corporate" model="res.groups">
<field name="name">EnCoach Corporate</field>
</record>
<record id="group_encoach_admin" model="res.groups">
<field name="name">EnCoach Admin</field>
<field name="implied_ids" eval="[(4, ref('group_encoach_teacher'))]"/>
</record>
<record id="group_encoach_developer" model="res.groups">
<field name="name">EnCoach Developer</field>
<field name="implied_ids" eval="[(4, ref('group_encoach_admin'))]"/>
</record>
<record id="group_encoach_agent" model="res.groups">
<field name="name">EnCoach Agent</field>
</record>
<record id="group_encoach_mastercorporate" model="res.groups">
<field name="name">EnCoach Master Corporate</field>
<field name="implied_ids" eval="[(4, ref('group_encoach_corporate'))]"/>
</record>
</data>
</odoo>

View File

@@ -8,9 +8,13 @@ access_encoach_role_admin,encoach.role.admin,model_encoach_role,group_encoach_ad
access_encoach_permission_student,encoach.permission.student,model_encoach_permission,group_encoach_student,1,0,0,0 access_encoach_permission_student,encoach.permission.student,model_encoach_permission,group_encoach_student,1,0,0,0
access_encoach_permission_teacher,encoach.permission.teacher,model_encoach_permission,group_encoach_teacher,1,0,0,0 access_encoach_permission_teacher,encoach.permission.teacher,model_encoach_permission,group_encoach_teacher,1,0,0,0
access_encoach_permission_admin,encoach.permission.admin,model_encoach_permission,group_encoach_admin,1,1,1,1 access_encoach_permission_admin,encoach.permission.admin,model_encoach_permission,group_encoach_admin,1,1,1,1
access_encoach_invite_code_student,encoach.invite.code.student,model_encoach_invite_code,group_encoach_student,1,0,0,0 access_encoach_invite_student,encoach.invite.student,model_encoach_invite,group_encoach_student,1,0,0,0
access_encoach_invite_code_teacher,encoach.invite.code.teacher,model_encoach_invite_code,group_encoach_teacher,1,1,0,0 access_encoach_invite_teacher,encoach.invite.teacher,model_encoach_invite,group_encoach_teacher,1,1,0,0
access_encoach_invite_code_admin,encoach.invite.code.admin,model_encoach_invite_code,group_encoach_admin,1,1,1,1 access_encoach_invite_admin,encoach.invite.admin,model_encoach_invite,group_encoach_admin,1,1,1,1
access_encoach_code_student,encoach.code.student,model_encoach_code,group_encoach_student,1,0,0,0
access_encoach_code_teacher,encoach.code.teacher,model_encoach_code,group_encoach_teacher,1,1,0,0
access_encoach_code_admin,encoach.code.admin,model_encoach_code,group_encoach_admin,1,1,1,1
access_encoach_user_entity_rel_all,encoach.user.entity.rel.all,model_encoach_user_entity_rel,base.group_user,1,1,1,1
access_encoach_level_mapping_student,encoach.entity.level.mapping.student,model_encoach_entity_level_mapping,group_encoach_student,1,0,0,0 access_encoach_level_mapping_student,encoach.entity.level.mapping.student,model_encoach_entity_level_mapping,group_encoach_student,1,0,0,0
access_encoach_level_mapping_teacher,encoach.entity.level.mapping.teacher,model_encoach_entity_level_mapping,group_encoach_teacher,1,1,0,0 access_encoach_level_mapping_teacher,encoach.entity.level.mapping.teacher,model_encoach_entity_level_mapping,group_encoach_teacher,1,1,0,0
access_encoach_level_mapping_admin,encoach.entity.level.mapping.admin,model_encoach_entity_level_mapping,group_encoach_admin,1,1,1,1 access_encoach_level_mapping_admin,encoach.entity.level.mapping.admin,model_encoach_entity_level_mapping,group_encoach_admin,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
8 access_encoach_permission_student encoach.permission.student model_encoach_permission group_encoach_student 1 0 0 0
9 access_encoach_permission_teacher encoach.permission.teacher model_encoach_permission group_encoach_teacher 1 0 0 0
10 access_encoach_permission_admin encoach.permission.admin model_encoach_permission group_encoach_admin 1 1 1 1
11 access_encoach_invite_code_student access_encoach_invite_student encoach.invite.code.student encoach.invite.student model_encoach_invite_code model_encoach_invite group_encoach_student 1 0 0 0
12 access_encoach_invite_code_teacher access_encoach_invite_teacher encoach.invite.code.teacher encoach.invite.teacher model_encoach_invite_code model_encoach_invite group_encoach_teacher 1 1 0 0
13 access_encoach_invite_code_admin access_encoach_invite_admin encoach.invite.code.admin encoach.invite.admin model_encoach_invite_code model_encoach_invite group_encoach_admin 1 1 1 1
14 access_encoach_code_student encoach.code.student model_encoach_code group_encoach_student 1 0 0 0
15 access_encoach_code_teacher encoach.code.teacher model_encoach_code group_encoach_teacher 1 1 0 0
16 access_encoach_code_admin encoach.code.admin model_encoach_code group_encoach_admin 1 1 1 1
17 access_encoach_user_entity_rel_all encoach.user.entity.rel.all model_encoach_user_entity_rel base.group_user 1 1 1 1
18 access_encoach_level_mapping_student encoach.entity.level.mapping.student model_encoach_entity_level_mapping group_encoach_student 1 0 0 0
19 access_encoach_level_mapping_teacher encoach.entity.level.mapping.teacher model_encoach_entity_level_mapping group_encoach_teacher 1 1 0 0
20 access_encoach_level_mapping_admin encoach.entity.level.mapping.admin model_encoach_entity_level_mapping group_encoach_admin 1 1 1 1

View File

@@ -11,7 +11,7 @@ class EncoachDashboard extends Component {
static props = ["*"]; static props = ["*"];
setup() { setup() {
this.rpc = useService("rpc"); this.orm = useService("orm");
this.action = useService("action"); this.action = useService("action");
this.state = useState({ this.state = useState({
loading: true, loading: true,
@@ -24,11 +24,8 @@ class EncoachDashboard extends Component {
pending_grading: 0, pending_grading: 0,
pending_approval: 0, pending_approval: 0,
total_entities: 0, total_entities: 0,
recent_signups: 0,
avg_score: 0,
}, },
recentAttempts: [], recentAttempts: [],
courseProgress: [],
}); });
onWillStart(async () => { onWillStart(async () => {
@@ -39,14 +36,14 @@ class EncoachDashboard extends Component {
async loadDashboardData() { async loadDashboardData() {
try { try {
const [students, teachers, courses, exams, sessions, pendingGrading, pendingApproval, entities] = await Promise.all([ const [students, teachers, courses, exams, sessions, pendingGrading, pendingApproval, entities] = await Promise.all([
this.rpc("/web/dataset/call_kw", { model: "res.users", method: "search_count", args: [[["account_source", "!=", false]]], kwargs: {} }), this.orm.searchCount("res.users", [["account_source", "!=", false]]),
this.rpc("/web/dataset/call_kw", { model: "res.users", method: "search_count", args: [[["user_type", "=", "teacher"]]], kwargs: {} }), this.orm.searchCount("res.users", [["user_type", "=", "teacher"]]),
this.rpc("/web/dataset/call_kw", { model: "op.course", method: "search_count", args: [[]], kwargs: {} }), this.orm.searchCount("op.course", []),
this.rpc("/web/dataset/call_kw", { model: "encoach.exam.custom", method: "search_count", args: [[]], kwargs: {} }), this.orm.searchCount("encoach.exam.custom", []),
this.rpc("/web/dataset/call_kw", { model: "encoach.student.attempt", method: "search_count", args: [[["status", "=", "in_progress"]]], kwargs: {} }), this.orm.searchCount("encoach.student.attempt", [["status", "=", "in_progress"]]),
this.rpc("/web/dataset/call_kw", { model: "encoach.student.attempt", method: "search_count", args: [[["status", "=", "scoring"]]], kwargs: {} }), this.orm.searchCount("encoach.student.attempt", [["status", "=", "scoring"]]),
this.rpc("/web/dataset/call_kw", { model: "encoach.student.attempt", method: "search_count", args: [[["status", "=", "pending_approval"]]], kwargs: {} }), this.orm.searchCount("encoach.student.attempt", [["status", "=", "pending_approval"]]),
this.rpc("/web/dataset/call_kw", { model: "encoach.entity", method: "search_count", args: [[]], kwargs: {} }), this.orm.searchCount("encoach.entity", []),
]); ]);
this.state.stats = { this.state.stats = {
@@ -60,12 +57,12 @@ class EncoachDashboard extends Component {
total_entities: entities, total_entities: entities,
}; };
const attempts = await this.rpc("/web/dataset/call_kw", { const attempts = await this.orm.searchRead(
model: "encoach.student.attempt", "encoach.student.attempt",
method: "search_read", [],
args: [[], ["student_id", "exam_id", "status", "overall_band", "started_at"]], ["student_id", "exam_id", "status", "overall_band", "started_at"],
kwargs: { limit: 10, order: "started_at desc" }, { limit: 10, order: "started_at desc" },
}); );
this.state.recentAttempts = attempts; this.state.recentAttempts = attempts;
} catch (e) { } catch (e) {

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_encoach_entity_list" model="ir.ui.view">
<field name="name">encoach.entity.list</field>
<field name="model">encoach.entity</field>
<field name="arch" type="xml">
<list string="Entities">
<field name="name"/>
<field name="code"/>
<field name="type"/>
<field name="white_label_domain"/>
<field name="user_count"/>
<field name="results_release_mode"/>
<field name="active" column_invisible="True"/>
</list>
</field>
</record>
<record id="view_encoach_entity_form" model="ir.ui.view">
<field name="name">encoach.entity.form</field>
<field name="model">encoach.entity</field>
<field name="arch" type="xml">
<form string="Entity">
<sheet>
<div class="oe_button_box" name="button_box">
</div>
<field name="logo" widget="image" class="oe_avatar"/>
<div class="oe_title">
<h1><field name="name" placeholder="Entity Name"/></h1>
</div>
<group>
<group>
<field name="code"/>
<field name="type"/>
<field name="active"/>
</group>
<group>
<field name="white_label_domain"/>
<field name="results_release_mode"/>
</group>
</group>
<notebook>
<page string="Branding">
<group>
<group>
<field name="primary_color" widget="color"/>
<field name="secondary_color" widget="color"/>
<field name="background_color" widget="color"/>
</group>
<group>
<field name="logo_url"/>
<field name="login_title"/>
<field name="favicon" widget="image"/>
</group>
</group>
<field name="login_description"/>
</page>
<page string="Users">
<field name="user_ids">
<list>
<field name="name"/>
<field name="login"/>
<field name="user_type"/>
</list>
</field>
</page>
<page string="Roles">
<field name="role_ids">
<list>
<field name="name"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_encoach_entity_search" model="ir.ui.view">
<field name="name">encoach.entity.search</field>
<field name="model">encoach.entity</field>
<field name="arch" type="xml">
<search string="Search Entities">
<field name="name"/>
<field name="code"/>
<filter string="Corporate" name="corporate" domain="[('type', '=', 'corporate')]"/>
<filter string="University" name="university" domain="[('type', '=', 'university')]"/>
<filter string="School" name="school" domain="[('type', '=', 'school')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<record id="action_encoach_entity" model="ir.actions.act_window">
<field name="name">Entities</field>
<field name="res_model">encoach.entity</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_encoach_entity_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first entity
</p>
<p>Entities represent organizations (corporates, universities, schools) on the EnCoach platform.</p>
</field>
</record>
<menuitem id="menu_encoach_entity_list"
name="Entities"
parent="menu_encoach_entity"
action="action_encoach_entity"
sequence="1"/>
</odoo>

View File

@@ -11,7 +11,7 @@ class GapAnalysisVisualization extends Component {
static props = ["*"]; static props = ["*"];
setup() { setup() {
this.rpc = useService("rpc"); this.orm = useService("orm");
this.action = useService("action"); this.action = useService("action");
this.state = useState({ this.state = useState({
loading: true, loading: true,
@@ -36,12 +36,12 @@ class GapAnalysisVisualization extends Component {
async loadStudentList() { async loadStudentList() {
try { try {
this.state.studentList = await this.rpc("/web/dataset/call_kw", { this.state.studentList = await this.orm.searchRead(
model: "res.users", "res.users",
method: "search_read", [["account_source", "!=", false]],
args: [[["account_source", "!=", false]], ["name", "email", "login"]], ["name", "email", "login"],
kwargs: { limit: 50, order: "name" }, { limit: 50, order: "name" },
}); );
} catch (e) { } catch (e) {
console.error("Failed to load students:", e); console.error("Failed to load students:", e);
} finally { } finally {
@@ -59,18 +59,17 @@ class GapAnalysisVisualization extends Component {
this.state.loading = true; this.state.loading = true;
try { try {
const [profiles, students] = await Promise.all([ const [profiles, students] = await Promise.all([
this.rpc("/web/dataset/call_kw", { this.orm.searchRead(
model: "encoach.gap.profile", "encoach.gap.profile",
method: "search_read", [["student_id", "=", this.state.studentId]],
args: [[["student_id", "=", this.state.studentId]], ["student_id", "source_type", "skill_gaps", "question_type_weaknesses", "topic_weaknesses", "created_at"]], ["student_id", "source_type", "skill_gaps", "question_type_weaknesses", "topic_weaknesses", "created_at"],
kwargs: { limit: 1, order: "created_at desc" }, { limit: 1, order: "created_at desc" },
}), ),
this.rpc("/web/dataset/call_kw", { this.orm.searchRead(
model: "res.users", "res.users",
method: "search_read", [["id", "=", this.state.studentId]],
args: [[["id", "=", this.state.studentId]], ["name", "email"]], ["name", "email"],
kwargs: {}, ),
}),
]); ]);
this.state.student = students.length ? students[0] : null; this.state.student = students.length ? students[0] : null;

View File

@@ -60,9 +60,7 @@
<filter string="Completed" name="completed" domain="[('status', '=', 'completed')]"/> <filter string="Completed" name="completed" domain="[('status', '=', 'completed')]"/>
<filter string="Skipped" name="skipped" domain="[('status', '=', 'skipped')]"/> <filter string="Skipped" name="skipped" domain="[('status', '=', 'skipped')]"/>
<separator/> <separator/>
<group expand="0" string="Group By">
<filter string="Course" name="group_course" context="{'group_by': 'course_id'}"/> <filter string="Course" name="group_course" context="{'group_by': 'course_id'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -57,10 +57,8 @@
<filter string="Placement" name="placement" domain="[('source_type', '=', 'placement')]"/> <filter string="Placement" name="placement" domain="[('source_type', '=', 'placement')]"/>
<filter string="Exam" name="exam" domain="[('source_type', '=', 'exam')]"/> <filter string="Exam" name="exam" domain="[('source_type', '=', 'exam')]"/>
<separator/> <separator/>
<group expand="0" string="Group By">
<filter string="Student" name="group_student" context="{'group_by': 'student_id'}"/> <filter string="Student" name="group_student" context="{'group_by': 'student_id'}"/>
<filter string="Entity" name="group_entity" context="{'group_by': 'entity_id'}"/> <filter string="Entity" name="group_entity" context="{'group_by': 'entity_id'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -238,7 +238,7 @@ class EncoachIeltsExamController(http.Controller):
page = int(kw.get('page', 1)) page = int(kw.get('page', 1))
size = int(kw.get('size', 20)) size = int(kw.get('size', 20))
Question = request.env['encoach.question'] Question = request.env['encoach.question'].sudo()
records, total = _paginate(Question, domain, page=page, size=size) records, total = _paginate(Question, domain, page=page, size=size)
return _json_response({ return _json_response({

View File

@@ -60,7 +60,7 @@ class EncoachTemplatesController(http.Controller):
page = int(kw.get('page', 1)) page = int(kw.get('page', 1))
size = int(kw.get('size', 20)) size = int(kw.get('size', 20))
Template = request.env['encoach.exam.template'] Template = request.env['encoach.exam.template'].sudo()
records, total = _paginate(Template, domain, page=page, size=size) records, total = _paginate(Template, domain, page=page, size=size)
return _json_response({ return _json_response({

View File

@@ -11,7 +11,7 @@ class ContentPoolBrowser extends Component {
static props = ["*"]; static props = ["*"];
setup() { setup() {
this.rpc = useService("rpc"); this.orm = useService("orm");
this.action = useService("action"); this.action = useService("action");
this.state = useState({ this.state = useState({
loading: true, loading: true,
@@ -30,22 +30,17 @@ class ContentPoolBrowser extends Component {
const domain = this._buildDomain(); const domain = this._buildDomain();
try { try {
const [questions, count] = await Promise.all([ const [questions, count] = await Promise.all([
this.rpc("/web/dataset/call_kw", { this.orm.searchRead(
model: "encoach.question", "encoach.question",
method: "search_read", domain,
args: [domain, ["skill", "question_type", "stem", "difficulty", "marks", "irt_b", "subject_id", "topic_id", "status"]], ["skill", "question_type", "stem", "difficulty", "marks", "irt_b", "subject_id", "topic_id", "status"],
kwargs: { {
limit: this.state.pageSize, limit: this.state.pageSize,
offset: (this.state.page - 1) * this.state.pageSize, offset: (this.state.page - 1) * this.state.pageSize,
order: "skill, difficulty", order: "skill, difficulty",
}, },
}), ),
this.rpc("/web/dataset/call_kw", { this.orm.searchCount("encoach.question", domain),
model: "encoach.question",
method: "search_count",
args: [domain],
kwargs: {},
}),
]); ]);
this.state.questions = questions; this.state.questions = questions;
this.state.totalCount = count; this.state.totalCount = count;

View File

@@ -11,7 +11,7 @@ class ExamValidationReport extends Component {
static props = ["*"]; static props = ["*"];
setup() { setup() {
this.rpc = useService("rpc"); this.orm = useService("orm");
this.action = useService("action"); this.action = useService("action");
this.state = useState({ this.state = useState({
loading: true, loading: true,
@@ -38,12 +38,12 @@ class ExamValidationReport extends Component {
async loadExamList() { async loadExamList() {
try { try {
this.state.examList = await this.rpc("/web/dataset/call_kw", { this.state.examList = await this.orm.searchRead(
model: "encoach.exam.template", "encoach.exam.template",
method: "search_read", [],
args: [[], ["name", "code", "type", "total_time_min"]], ["name", "code", "type", "total_time_min"],
kwargs: { limit: 50, order: "name" }, { limit: 50, order: "name" },
}); );
} catch (e) { } catch (e) {
console.error("Failed to load exam list:", e); console.error("Failed to load exam list:", e);
} finally { } finally {
@@ -60,14 +60,11 @@ class ExamValidationReport extends Component {
async runValidation() { async runValidation() {
this.state.loading = true; this.state.loading = true;
try { try {
const [exams] = await Promise.all([ const exams = await this.orm.searchRead(
this.rpc("/web/dataset/call_kw", { "encoach.exam.template",
model: "encoach.exam.template", [["id", "=", this.state.examId]],
method: "search_read", ["name", "code", "type", "total_time_min", "pass_threshold", "structure", "subject_id"],
args: [[["id", "=", this.state.examId]], ["name", "code", "type", "total_time_min", "pass_threshold", "structure", "subject_id"]], );
kwargs: {},
}),
]);
this.state.exam = exams.length ? exams[0] : null; this.state.exam = exams.length ? exams[0] : null;
if (!this.state.exam) { if (!this.state.exam) {
@@ -75,12 +72,11 @@ class ExamValidationReport extends Component {
return; return;
} }
const questions = await this.rpc("/web/dataset/call_kw", { const questions = await this.orm.searchRead(
model: "encoach.question", "encoach.question",
method: "search_read", [],
args: [[], ["skill", "difficulty", "marks", "status"]], ["skill", "difficulty", "marks", "status"],
kwargs: {}, );
});
this.state.checks = this._runChecks(this.state.exam, questions); this.state.checks = this._runChecks(this.state.exam, questions);
this.state.passCount = this.state.checks.filter(c => c.status === "pass").length; this.state.passCount = this.state.checks.filter(c => c.status === "pass").length;

View File

@@ -78,11 +78,9 @@
<filter string="Part 4" name="filter_part4" domain="[('part', '=', 4)]"/> <filter string="Part 4" name="filter_part4" domain="[('part', '=', 4)]"/>
<separator/> <separator/>
<filter string="Approved" name="filter_approved" domain="[('approved', '=', True)]"/> <filter string="Approved" name="filter_approved" domain="[('approved', '=', True)]"/>
<group expand="0" string="Group By">
<filter string="Exam Type" name="group_exam_type" context="{'group_by': 'exam_type'}"/> <filter string="Exam Type" name="group_exam_type" context="{'group_by': 'exam_type'}"/>
<filter string="Part" name="group_part" context="{'group_by': 'part'}"/> <filter string="Part" name="group_part" context="{'group_by': 'part'}"/>
<filter string="Difficulty" name="group_difficulty" context="{'group_by': 'difficulty'}"/> <filter string="Difficulty" name="group_difficulty" context="{'group_by': 'difficulty'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -58,11 +58,9 @@
<filter string="Started" name="filter_started" domain="[('status', '=', 'started')]"/> <filter string="Started" name="filter_started" domain="[('status', '=', 'started')]"/>
<filter string="Completed" name="filter_completed" domain="[('status', '=', 'completed')]"/> <filter string="Completed" name="filter_completed" domain="[('status', '=', 'completed')]"/>
<filter string="Expired" name="filter_expired" domain="[('status', '=', 'expired')]"/> <filter string="Expired" name="filter_expired" domain="[('status', '=', 'expired')]"/>
<group expand="0" string="Group By">
<filter string="Status" name="group_status" context="{'group_by': 'status'}"/> <filter string="Status" name="group_status" context="{'group_by': 'status'}"/>
<filter string="Exam" name="group_exam" context="{'group_by': 'exam_id'}"/> <filter string="Exam" name="group_exam" context="{'group_by': 'exam_id'}"/>
<filter string="Batch" name="group_batch" context="{'group_by': 'batch_id'}"/> <filter string="Batch" name="group_batch" context="{'group_by': 'batch_id'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -83,11 +83,9 @@
<filter string="Draft" name="filter_draft" domain="[('status', '=', 'draft')]"/> <filter string="Draft" name="filter_draft" domain="[('status', '=', 'draft')]"/>
<filter string="Published" name="filter_published" domain="[('status', '=', 'published')]"/> <filter string="Published" name="filter_published" domain="[('status', '=', 'published')]"/>
<filter string="Archived" name="filter_archived" domain="[('status', '=', 'archived')]"/> <filter string="Archived" name="filter_archived" domain="[('status', '=', 'archived')]"/>
<group expand="0" string="Group By">
<filter string="Status" name="group_status" context="{'group_by': 'status'}"/> <filter string="Status" name="group_status" context="{'group_by': 'status'}"/>
<filter string="Subject" name="group_subject" context="{'group_by': 'subject_id'}"/> <filter string="Subject" name="group_subject" context="{'group_by': 'subject_id'}"/>
<filter string="Entity" name="group_entity" context="{'group_by': 'entity_id'}"/> <filter string="Entity" name="group_entity" context="{'group_by': 'entity_id'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -79,11 +79,9 @@
<separator/> <separator/>
<filter string="Active" name="filter_active" domain="[('active', '=', True)]"/> <filter string="Active" name="filter_active" domain="[('active', '=', True)]"/>
<filter string="Archived" name="filter_archived" domain="[('active', '=', False)]"/> <filter string="Archived" name="filter_archived" domain="[('active', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Type" name="group_type" context="{'group_by': 'type'}"/> <filter string="Type" name="group_type" context="{'group_by': 'type'}"/>
<filter string="Subject" name="group_subject" context="{'group_by': 'subject_id'}"/> <filter string="Subject" name="group_subject" context="{'group_by': 'subject_id'}"/>
<filter string="Entity" name="group_entity" context="{'group_by': 'entity_id'}"/> <filter string="Entity" name="group_entity" context="{'group_by': 'entity_id'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -83,11 +83,9 @@
<separator/> <separator/>
<filter string="Approved" name="filter_approved" domain="[('approved', '=', True)]"/> <filter string="Approved" name="filter_approved" domain="[('approved', '=', True)]"/>
<filter string="Not Approved" name="filter_not_approved" domain="[('approved', '=', False)]"/> <filter string="Not Approved" name="filter_not_approved" domain="[('approved', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Exam Type" name="group_exam_type" context="{'group_by': 'exam_type'}"/> <filter string="Exam Type" name="group_exam_type" context="{'group_by': 'exam_type'}"/>
<filter string="Status" name="group_status" context="{'group_by': 'status'}"/> <filter string="Status" name="group_status" context="{'group_by': 'status'}"/>
<filter string="Difficulty" name="group_difficulty" context="{'group_by': 'difficulty'}"/> <filter string="Difficulty" name="group_difficulty" context="{'group_by': 'difficulty'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -104,13 +104,11 @@
<filter string="Draft" name="filter_draft" domain="[('status', '=', 'draft')]"/> <filter string="Draft" name="filter_draft" domain="[('status', '=', 'draft')]"/>
<filter string="Active" name="filter_active" domain="[('status', '=', 'active')]"/> <filter string="Active" name="filter_active" domain="[('status', '=', 'active')]"/>
<filter string="Flagged" name="filter_flagged" domain="[('status', '=', 'flagged')]"/> <filter string="Flagged" name="filter_flagged" domain="[('status', '=', 'flagged')]"/>
<group expand="0" string="Group By">
<filter string="Skill" name="group_skill" context="{'group_by': 'skill'}"/> <filter string="Skill" name="group_skill" context="{'group_by': 'skill'}"/>
<filter string="Type" name="group_type" context="{'group_by': 'question_type'}"/> <filter string="Type" name="group_type" context="{'group_by': 'question_type'}"/>
<filter string="Difficulty" name="group_difficulty" context="{'group_by': 'difficulty'}"/> <filter string="Difficulty" name="group_difficulty" context="{'group_by': 'difficulty'}"/>
<filter string="Status" name="group_status" context="{'group_by': 'status'}"/> <filter string="Status" name="group_status" context="{'group_by': 'status'}"/>
<filter string="Subject" name="group_subject" context="{'group_by': 'subject_id'}"/> <filter string="Subject" name="group_subject" context="{'group_by': 'subject_id'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -56,10 +56,8 @@
<separator/> <separator/>
<filter string="Academic" name="filter_academic" domain="[('exam_type', '=', 'academic')]"/> <filter string="Academic" name="filter_academic" domain="[('exam_type', '=', 'academic')]"/>
<filter string="General Training" name="filter_general" domain="[('exam_type', '=', 'general_training')]"/> <filter string="General Training" name="filter_general" domain="[('exam_type', '=', 'general_training')]"/>
<group expand="0" string="Group By">
<filter string="Skill" name="group_skill" context="{'group_by': 'skill'}"/> <filter string="Skill" name="group_skill" context="{'group_by': 'skill'}"/>
<filter string="Exam Type" name="group_exam_type" context="{'group_by': 'exam_type'}"/> <filter string="Exam Type" name="group_exam_type" context="{'group_by': 'exam_type'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -82,10 +82,8 @@
<filter string="Hard" name="filter_hard" domain="[('difficulty', '=', 'hard')]"/> <filter string="Hard" name="filter_hard" domain="[('difficulty', '=', 'hard')]"/>
<separator/> <separator/>
<filter string="Approved" name="filter_approved" domain="[('approved', '=', True)]"/> <filter string="Approved" name="filter_approved" domain="[('approved', '=', True)]"/>
<group expand="0" string="Group By">
<filter string="Part" name="group_part" context="{'group_by': 'part'}"/> <filter string="Part" name="group_part" context="{'group_by': 'part'}"/>
<filter string="Difficulty" name="group_difficulty" context="{'group_by': 'difficulty'}"/> <filter string="Difficulty" name="group_difficulty" context="{'group_by': 'difficulty'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -74,10 +74,8 @@
<filter string="Task 2" name="filter_task2" domain="[('task', '=', 'task2')]"/> <filter string="Task 2" name="filter_task2" domain="[('task', '=', 'task2')]"/>
<separator/> <separator/>
<filter string="Approved" name="filter_approved" domain="[('approved', '=', True)]"/> <filter string="Approved" name="filter_approved" domain="[('approved', '=', True)]"/>
<group expand="0" string="Group By">
<filter string="Exam Type" name="group_exam_type" context="{'group_by': 'exam_type'}"/> <filter string="Exam Type" name="group_exam_type" context="{'group_by': 'exam_type'}"/>
<filter string="Task" name="group_task" context="{'group_by': 'task'}"/> <filter string="Task" name="group_task" context="{'group_by': 'task'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -60,9 +60,7 @@
<filter string="Completed" name="completed" domain="[('status', '=', 'completed')]"/> <filter string="Completed" name="completed" domain="[('status', '=', 'completed')]"/>
<filter string="Abandoned" name="abandoned" domain="[('status', '=', 'abandoned')]"/> <filter string="Abandoned" name="abandoned" domain="[('status', '=', 'abandoned')]"/>
<separator/> <separator/>
<group expand="0" string="Group By">
<filter string="Student" name="group_student" context="{'group_by': 'student_id'}"/> <filter string="Student" name="group_student" context="{'group_by': 'student_id'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -52,10 +52,8 @@
<filter string="Grammar" name="grammar" domain="[('skill', '=', 'grammar')]"/> <filter string="Grammar" name="grammar" domain="[('skill', '=', 'grammar')]"/>
<filter string="Vocabulary" name="vocabulary" domain="[('skill', '=', 'vocabulary')]"/> <filter string="Vocabulary" name="vocabulary" domain="[('skill', '=', 'vocabulary')]"/>
<separator/> <separator/>
<group expand="0" string="Group By">
<filter string="Student" name="group_student" context="{'group_by': 'student_id'}"/> <filter string="Student" name="group_student" context="{'group_by': 'student_id'}"/>
<filter string="Subject" name="group_subject" context="{'group_by': 'subject_id'}"/> <filter string="Subject" name="group_subject" context="{'group_by': 'subject_id'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -101,11 +101,9 @@
<filter string="Pending Approval" name="pending_approval" domain="[('status', '=', 'pending_approval')]"/> <filter string="Pending Approval" name="pending_approval" domain="[('status', '=', 'pending_approval')]"/>
<filter string="Released" name="released" domain="[('status', '=', 'released')]"/> <filter string="Released" name="released" domain="[('status', '=', 'released')]"/>
<separator/> <separator/>
<group expand="0" string="Group By">
<filter string="Student" name="group_student" context="{'group_by': 'student_id'}"/> <filter string="Student" name="group_student" context="{'group_by': 'student_id'}"/>
<filter string="Exam" name="group_exam" context="{'group_by': 'exam_id'}"/> <filter string="Exam" name="group_exam" context="{'group_by': 'exam_id'}"/>
<filter string="Entity" name="group_entity" context="{'group_by': 'entity_id'}"/> <filter string="Entity" name="group_entity" context="{'group_by': 'entity_id'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -50,6 +50,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Search Student Profiles"> <search string="Search Student Profiles">
<field name="user_id"/> <field name="user_id"/>
<field name="cefr_level"/>
<separator/> <separator/>
<filter string="Pre-A1" name="pre_a1" domain="[('cefr_level', '=', 'pre_a1')]"/> <filter string="Pre-A1" name="pre_a1" domain="[('cefr_level', '=', 'pre_a1')]"/>
<filter string="A1" name="a1" domain="[('cefr_level', '=', 'a1')]"/> <filter string="A1" name="a1" domain="[('cefr_level', '=', 'a1')]"/>
@@ -59,9 +60,7 @@
<filter string="C1" name="c1" domain="[('cefr_level', '=', 'c1')]"/> <filter string="C1" name="c1" domain="[('cefr_level', '=', 'c1')]"/>
<filter string="C2" name="c2" domain="[('cefr_level', '=', 'c2')]"/> <filter string="C2" name="c2" domain="[('cefr_level', '=', 'c2')]"/>
<separator/> <separator/>
<group expand="0" string="Group By">
<filter string="Entity" name="group_entity" context="{'group_by': 'entity_id'}"/> <filter string="Entity" name="group_entity" context="{'group_by': 'entity_id'}"/>
</group>
</search> </search>
</field> </field>
</record> </record>

View File

@@ -7,6 +7,7 @@
'depends': ['encoach_core'], 'depends': ['encoach_core'],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'views/taxonomy_views.xml',
'data/default_subjects.xml', 'data/default_subjects.xml',
'data/math_it_taxonomy.xml', 'data/math_it_taxonomy.xml',
], ],

View File

@@ -40,8 +40,8 @@ class EncoachTaxonomyController(http.Controller):
items.append({ items.append({
'id': s.id, 'id': s.id,
'name': s.name, 'name': s.name,
'code': s.code, 'code': s.code or '',
'icon': s.icon or '', 'description': s.description or '',
'domain_count': len(s.domain_ids), 'domain_count': len(s.domain_ids),
}) })
@@ -79,7 +79,6 @@ class EncoachTaxonomyController(http.Controller):
topics.append({ topics.append({
'id': t.id, 'id': t.id,
'name': t.name, 'name': t.name,
'difficulty': t.difficulty or '',
'description': t.description or '', 'description': t.description or '',
'learning_objectives': objectives, 'learning_objectives': objectives,
}) })
@@ -92,8 +91,8 @@ class EncoachTaxonomyController(http.Controller):
tree.append({ tree.append({
'id': s.id, 'id': s.id,
'name': s.name, 'name': s.name,
'code': s.code, 'code': s.code or '',
'icon': s.icon or '', 'description': s.description or '',
'domains': domains, 'domains': domains,
}) })
@@ -134,8 +133,6 @@ class EncoachTaxonomyController(http.Controller):
if not code: if not code:
return _error_response('code is required for subjects', 400) return _error_response('code is required for subjects', 400)
vals['code'] = code vals['code'] = code
if body.get('icon'):
vals['icon'] = body['icon']
else: else:
parent_id = body.get('parent_id') parent_id = body.get('parent_id')
if not parent_id: if not parent_id:
@@ -145,8 +142,6 @@ class EncoachTaxonomyController(http.Controller):
) )
vals[PARENT_FIELD_MAP[node_type]] = int(parent_id) vals[PARENT_FIELD_MAP[node_type]] = int(parent_id)
if node_type == 'topic' and body.get('difficulty'):
vals['difficulty'] = body['difficulty']
if node_type == 'learning_objective' and body.get('bloom_level'): if node_type == 'learning_objective' and body.get('bloom_level'):
vals['bloom_level'] = body['bloom_level'] vals['bloom_level'] = body['bloom_level']
@@ -192,12 +187,8 @@ class EncoachTaxonomyController(http.Controller):
if node_type == 'subject': if node_type == 'subject':
if body.get('code'): if body.get('code'):
vals['code'] = body['code'] vals['code'] = body['code']
if 'icon' in body:
vals['icon'] = body.get('icon') or ''
if 'active' in body: if 'active' in body:
vals['active'] = bool(body['active']) vals['active'] = bool(body['active'])
elif node_type == 'topic' and body.get('difficulty'):
vals['difficulty'] = body['difficulty']
elif node_type == 'learning_objective' and body.get('bloom_level'): elif node_type == 'learning_objective' and body.get('bloom_level'):
vals['bloom_level'] = body['bloom_level'] vals['bloom_level'] = body['bloom_level']

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="subject_english" model="encoach.subject">
<field name="name">English</field>
<field name="code">ENG</field>
<field name="description">English Language</field>
<field name="sequence">1</field>
</record>
<record id="subject_math" model="encoach.subject">
<field name="name">Mathematics</field>
<field name="code">MATH</field>
<field name="description">Mathematics</field>
<field name="sequence">2</field>
</record>
<record id="subject_it" model="encoach.subject">
<field name="name">Information Technology</field>
<field name="code">IT</field>
<field name="description">Information Technology and Computer Science</field>
<field name="sequence">3</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,4 @@
from . import subject
from . import domain
from . import topic
from . import learning_objective

View File

@@ -0,0 +1,25 @@
from odoo import models, fields
class EncoachDomain(models.Model):
_name = 'encoach.domain'
_description = 'Taxonomy Domain'
_order = 'sequence, name'
name = fields.Char(required=True, index=True)
subject_id = fields.Many2one('encoach.subject', required=True, ondelete='cascade', index=True)
description = fields.Text()
sequence = fields.Integer(default=10)
active = fields.Boolean(default=True)
topic_ids = fields.One2many('encoach.topic', 'domain_id', string='Topics')
def to_api_dict(self):
self.ensure_one()
return {
'id': self.id,
'name': self.name,
'subject_id': self.subject_id.id,
'subject_name': self.subject_id.name,
'description': self.description or '',
'active': self.active,
}

View File

@@ -0,0 +1,29 @@
from odoo import models, fields
class EncoachLearningObjective(models.Model):
_name = 'encoach.learning.objective'
_description = 'Taxonomy Learning Objective'
_order = 'sequence, name'
name = fields.Char(required=True, index=True)
topic_id = fields.Many2one('encoach.topic', required=True, ondelete='cascade', index=True)
description = fields.Text()
cefr_level = fields.Selection([
('pre_a1', 'Pre-A1'), ('a1', 'A1'), ('a2', 'A2'),
('b1', 'B1'), ('b2', 'B2'), ('c1', 'C1'), ('c2', 'C2'),
])
sequence = fields.Integer(default=10)
active = fields.Boolean(default=True)
def to_api_dict(self):
self.ensure_one()
return {
'id': self.id,
'name': self.name,
'topic_id': self.topic_id.id,
'topic_name': self.topic_id.name,
'description': self.description or '',
'cefr_level': self.cefr_level or '',
'active': self.active,
}

View File

@@ -0,0 +1,24 @@
from odoo import models, fields
class EncoachSubject(models.Model):
_name = 'encoach.subject'
_description = 'Taxonomy Subject'
_order = 'sequence, name'
name = fields.Char(required=True, index=True)
code = fields.Char(size=20, index=True)
description = fields.Text()
sequence = fields.Integer(default=10)
active = fields.Boolean(default=True)
domain_ids = fields.One2many('encoach.domain', 'subject_id', string='Domains')
def to_api_dict(self):
self.ensure_one()
return {
'id': self.id,
'name': self.name,
'code': self.code or '',
'description': self.description or '',
'active': self.active,
}

View File

@@ -0,0 +1,25 @@
from odoo import models, fields
class EncoachTopic(models.Model):
_name = 'encoach.topic'
_description = 'Taxonomy Topic'
_order = 'sequence, name'
name = fields.Char(required=True, index=True)
domain_id = fields.Many2one('encoach.domain', required=True, ondelete='cascade', index=True)
description = fields.Text()
sequence = fields.Integer(default=10)
active = fields.Boolean(default=True)
learning_objective_ids = fields.One2many('encoach.learning.objective', 'topic_id', string='Learning Objectives')
def to_api_dict(self):
self.ensure_one()
return {
'id': self.id,
'name': self.name,
'domain_id': self.domain_id.id,
'domain_name': self.domain_id.name,
'description': self.description or '',
'active': self.active,
}

View File

@@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_encoach_subject_all,encoach.subject.all,model_encoach_subject,base.group_user,1,1,1,1
access_encoach_domain_all,encoach.domain.all,model_encoach_domain,base.group_user,1,1,1,1
access_encoach_topic_all,encoach.topic.all,model_encoach_topic,base.group_user,1,1,1,1
access_encoach_learning_objective_all,encoach.learning.objective.all,model_encoach_learning_objective,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_encoach_subject_all encoach.subject.all model_encoach_subject base.group_user 1 1 1 1
3 access_encoach_domain_all encoach.domain.all model_encoach_domain base.group_user 1 1 1 1
4 access_encoach_topic_all encoach.topic.all model_encoach_topic base.group_user 1 1 1 1
5 access_encoach_learning_objective_all encoach.learning.objective.all model_encoach_learning_objective base.group_user 1 1 1 1

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Subject Views -->
<record id="view_encoach_subject_list" model="ir.ui.view">
<field name="name">encoach.subject.list</field>
<field name="model">encoach.subject</field>
<field name="arch" type="xml">
<list string="Subjects" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="code"/>
<field name="active" column_invisible="True"/>
</list>
</field>
</record>
<record id="view_encoach_subject_form" model="ir.ui.view">
<field name="name">encoach.subject.form</field>
<field name="model">encoach.subject</field>
<field name="arch" type="xml">
<form string="Subject">
<sheet>
<div class="oe_title">
<h1><field name="name" placeholder="Subject Name"/></h1>
</div>
<group>
<group>
<field name="code"/>
<field name="sequence"/>
<field name="active"/>
</group>
<group>
<field name="description"/>
</group>
</group>
<notebook>
<page string="Domains">
<field name="domain_ids">
<list>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="description"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_encoach_subject" model="ir.actions.act_window">
<field name="name">Subjects</field>
<field name="res_model">encoach.subject</field>
<field name="view_mode">list,form</field>
</record>
<!-- Domain Views -->
<record id="view_encoach_domain_list" model="ir.ui.view">
<field name="name">encoach.domain.list</field>
<field name="model">encoach.domain</field>
<field name="arch" type="xml">
<list string="Domains">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="subject_id"/>
<field name="description"/>
</list>
</field>
</record>
<record id="view_encoach_domain_form" model="ir.ui.view">
<field name="name">encoach.domain.form</field>
<field name="model">encoach.domain</field>
<field name="arch" type="xml">
<form string="Domain">
<sheet>
<div class="oe_title">
<h1><field name="name" placeholder="Domain Name"/></h1>
</div>
<group>
<group>
<field name="subject_id"/>
<field name="sequence"/>
</group>
<group>
<field name="description"/>
</group>
</group>
<notebook>
<page string="Topics">
<field name="topic_ids">
<list>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="description"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_encoach_domain" model="ir.actions.act_window">
<field name="name">Domains</field>
<field name="res_model">encoach.domain</field>
<field name="view_mode">list,form</field>
</record>
<!-- Topic Views -->
<record id="view_encoach_topic_list" model="ir.ui.view">
<field name="name">encoach.topic.list</field>
<field name="model">encoach.topic</field>
<field name="arch" type="xml">
<list string="Topics">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="domain_id"/>
<field name="description"/>
</list>
</field>
</record>
<record id="action_encoach_topic" model="ir.actions.act_window">
<field name="name">Topics</field>
<field name="res_model">encoach.topic</field>
<field name="view_mode">list,form</field>
</record>
<!-- Menu Items under Content -->
<menuitem id="menu_taxonomy_subjects"
name="Subjects"
parent="encoach_core.menu_encoach_content"
action="action_encoach_subject"
sequence="10"/>
<menuitem id="menu_taxonomy_domains"
name="Domains"
parent="encoach_core.menu_encoach_content"
action="action_encoach_domain"
sequence="11"/>
<menuitem id="menu_taxonomy_topics"
name="Topics"
parent="encoach_core.menu_encoach_content"
action="action_encoach_topic"
sequence="12"/>
</odoo>