diff --git a/.gitignore b/.gitignore index 366475790..9864cb1fb 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,27 @@ miniconda3/ pgdata/ .conda-envs/ .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 diff --git a/new_project/custom_addons/encoach_adaptive/models/__init__.py b/new_project/custom_addons/encoach_adaptive/models/__init__.py index 43532fd71..bec7008f6 100644 --- a/new_project/custom_addons/encoach_adaptive/models/__init__.py +++ b/new_project/custom_addons/encoach_adaptive/models/__init__.py @@ -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_path from . import adaptive_settings diff --git a/new_project/custom_addons/encoach_adaptive/security/ir.model.access.csv b/new_project/custom_addons/encoach_adaptive/security/ir.model.access.csv index c16c70dda..6b8127c0c 100644 --- a/new_project/custom_addons/encoach_adaptive/security/ir.model.access.csv +++ b/new_project/custom_addons/encoach_adaptive/security/ir.model.access.csv @@ -1,18 +1,4 @@ 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_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 diff --git a/new_project/custom_addons/encoach_adaptive/static/src/js/signal_timeline.js b/new_project/custom_addons/encoach_adaptive/static/src/js/signal_timeline.js index ab3e3dd70..c57fbb22f 100644 --- a/new_project/custom_addons/encoach_adaptive/static/src/js/signal_timeline.js +++ b/new_project/custom_addons/encoach_adaptive/static/src/js/signal_timeline.js @@ -11,7 +11,7 @@ class SignalTimeline extends Component { static props = ["*"]; setup() { - this.rpc = useService("rpc"); + this.orm = useService("orm"); this.action = useService("action"); this.state = useState({ loading: true, @@ -36,12 +36,12 @@ class SignalTimeline extends Component { async loadStudentList() { try { - this.state.studentList = await this.rpc("/web/dataset/call_kw", { - model: "res.users", - method: "search_read", - args: [[["account_source", "!=", false]], ["name", "email"]], - kwargs: { limit: 50, order: "name" }, - }); + this.state.studentList = await this.orm.searchRead( + "res.users", + [["account_source", "!=", false]], + ["name", "email"], + { limit: 50, order: "name" }, + ); } catch (e) { console.error("Failed to load students:", e); } finally { @@ -70,18 +70,17 @@ class SignalTimeline extends Component { } const [events, students] = await Promise.all([ - this.rpc("/web/dataset/call_kw", { - model: "encoach.adaptive.event", - method: "search_read", - args: [domain, ["student_id", "course_id", "event_type", "signal_name", "signal_value", "decision", "context", "created_at"]], - kwargs: { limit: 100, order: "created_at desc" }, - }), - this.rpc("/web/dataset/call_kw", { - model: "res.users", - method: "search_read", - args: [[["id", "=", this.state.studentId]], ["name", "email"]], - kwargs: {}, - }), + this.orm.searchRead( + "encoach.adaptive.event", + domain, + ["student_id", "course_id", "event_type", "signal_name", "signal_value", "decision", "context", "created_at"], + { limit: 100, order: "created_at desc" }, + ), + this.orm.searchRead( + "res.users", + [["id", "=", this.state.studentId]], + ["name", "email"], + ), ]); this.state.student = students.length ? students[0] : null; diff --git a/new_project/custom_addons/encoach_adaptive/views/adaptive_event_views.xml b/new_project/custom_addons/encoach_adaptive/views/adaptive_event_views.xml index b9dff46af..93b80c51b 100644 --- a/new_project/custom_addons/encoach_adaptive/views/adaptive_event_views.xml +++ b/new_project/custom_addons/encoach_adaptive/views/adaptive_event_views.xml @@ -53,9 +53,7 @@ - - - + diff --git a/new_project/custom_addons/encoach_ai_course/models/ai_generation_log.py b/new_project/custom_addons/encoach_ai_course/models/ai_generation_log.py index fa66ce569..a45412c3f 100644 --- a/new_project/custom_addons/encoach_ai_course/models/ai_generation_log.py +++ b/new_project/custom_addons/encoach_ai_course/models/ai_generation_log.py @@ -12,7 +12,7 @@ class EncoachAiGenerationLog(models.Model): ], required=True) brief = fields.Text() 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') status = fields.Selection([ ('generating', 'Generating'), diff --git a/new_project/custom_addons/encoach_ai_course/services/english_pipeline.py b/new_project/custom_addons/encoach_ai_course/services/english_pipeline.py index ded44e09a..25f61b80e 100644 --- a/new_project/custom_addons/encoach_ai_course/services/english_pipeline.py +++ b/new_project/custom_addons/encoach_ai_course/services/english_pipeline.py @@ -1,7 +1,10 @@ import json 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__) diff --git a/new_project/custom_addons/encoach_ai_course/services/ielts_pipeline.py b/new_project/custom_addons/encoach_ai_course/services/ielts_pipeline.py index a75740409..59026a7e2 100644 --- a/new_project/custom_addons/encoach_ai_course/services/ielts_pipeline.py +++ b/new_project/custom_addons/encoach_ai_course/services/ielts_pipeline.py @@ -1,7 +1,10 @@ import json 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__) diff --git a/new_project/custom_addons/encoach_api/__init__.py b/new_project/custom_addons/encoach_api/__init__.py new file mode 100644 index 000000000..e046e49fb --- /dev/null +++ b/new_project/custom_addons/encoach_api/__init__.py @@ -0,0 +1 @@ +from . import controllers diff --git a/new_project/custom_addons/encoach_api/__manifest__.py b/new_project/custom_addons/encoach_api/__manifest__.py new file mode 100644 index 000000000..574513ef8 --- /dev/null +++ b/new_project/custom_addons/encoach_api/__manifest__.py @@ -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', +} diff --git a/new_project/custom_addons/encoach_api/controllers/__init__.py b/new_project/custom_addons/encoach_api/controllers/__init__.py new file mode 100644 index 000000000..0e4444933 --- /dev/null +++ b/new_project/custom_addons/encoach_api/controllers/__init__.py @@ -0,0 +1 @@ +from . import base diff --git a/new_project/custom_addons/encoach_api/controllers/base.py b/new_project/custom_addons/encoach_api/controllers/base.py new file mode 100644 index 000000000..e38e22774 --- /dev/null +++ b/new_project/custom_addons/encoach_api/controllers/base.py @@ -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] diff --git a/new_project/custom_addons/encoach_branding/controllers/branding.py b/new_project/custom_addons/encoach_branding/controllers/branding.py index ac1e2e2a2..00c8e1e68 100644 --- a/new_project/custom_addons/encoach_branding/controllers/branding.py +++ b/new_project/custom_addons/encoach_branding/controllers/branding.py @@ -187,17 +187,9 @@ class EncoachBrandingController(http.Controller): 'custom_css': branding.custom_css or '', }) - auth_header = request.httprequest.headers.get('Authorization', '') - if not auth_header.startswith('Bearer '): + user = validate_token() + if not user: 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] if not entity: return _error_response('User has no associated entity', 404) diff --git a/new_project/custom_addons/encoach_branding/models/__init__.py b/new_project/custom_addons/encoach_branding/models/__init__.py new file mode 100644 index 000000000..7fe861f69 --- /dev/null +++ b/new_project/custom_addons/encoach_branding/models/__init__.py @@ -0,0 +1 @@ +from . import branding diff --git a/new_project/custom_addons/encoach_branding/security/ir.model.access.csv b/new_project/custom_addons/encoach_branding/security/ir.model.access.csv new file mode 100644 index 000000000..b98e84cb4 --- /dev/null +++ b/new_project/custom_addons/encoach_branding/security/ir.model.access.csv @@ -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 diff --git a/new_project/custom_addons/encoach_core/__init__.py b/new_project/custom_addons/encoach_core/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/new_project/custom_addons/encoach_core/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/new_project/custom_addons/encoach_core/__manifest__.py b/new_project/custom_addons/encoach_core/__manifest__.py index 4b233bb61..f9237aa58 100644 --- a/new_project/custom_addons/encoach_core/__manifest__.py +++ b/new_project/custom_addons/encoach_core/__manifest__.py @@ -9,8 +9,9 @@ 'security/encoach_security.xml', 'security/ir.model.access.csv', 'data/encoach_permissions.xml', - 'views/entity_level_mapping_views.xml', 'views/encoach_menus.xml', + 'views/encoach_entity_views.xml', + 'views/entity_level_mapping_views.xml', 'views/dashboard_action.xml', ], 'assets': { diff --git a/new_project/custom_addons/encoach_core/data/encoach_permissions.xml b/new_project/custom_addons/encoach_core/data/encoach_permissions.xml new file mode 100644 index 000000000..3986610aa --- /dev/null +++ b/new_project/custom_addons/encoach_core/data/encoach_permissions.xml @@ -0,0 +1,468 @@ + + + + + + viewCorporate + Manage Corporate + + + editCorporate + Manage Corporate + + + deleteCorporate + Manage Corporate + + + createCodeCorporate + Manage Corporate + + + viewAdmin + Manage Admin + + + editAdmin + Manage Admin + + + deleteAdmin + Manage Admin + + + createCodeAdmin + Manage Admin + + + viewStudent + Manage Student + + + editStudent + Manage Student + + + deleteStudent + Manage Student + + + createCodeStudent + Manage Student + + + viewTeacher + Manage Teacher + + + editTeacher + Manage Teacher + + + deleteTeacher + Manage Teacher + + + createCodeTeacher + Manage Teacher + + + viewCountryManager + Manage Country Manager + + + editCountryManager + Manage Country Manager + + + deleteCountryManager + Manage Country Manager + + + createCodeCountryManager + Manage Country Manager + + + createReadingExam + Manage Exams + + + createListeningExam + Manage Exams + + + createWritingExam + Manage Exams + + + createSpeakingExam + Manage Exams + + + createLevelExam + Manage Exams + + + viewExams + View Pages + + + viewExercises + View Pages + + + viewRecords + View Pages + + + viewStats + View Pages + + + viewTickets + View Pages + + + viewPaymentRecords + View Pages + + + viewGroup + Manage Group + + + editGroup + Manage Group + + + deleteGroup + Manage Group + + + createGroup + Manage Group + + + viewCodes + Manage Codes + + + deleteCodes + Manage Codes + + + createCodes + Manage Codes + + + all + Others + + + + + view_students + Users + + + view_teachers + Users + + + view_corporates + Users + + + view_mastercorporates + Users + + + edit_students + Users + + + edit_teachers + Users + + + edit_corporates + Users + + + edit_mastercorporates + Users + + + delete_students + Users + + + delete_teachers + Users + + + delete_corporates + Users + + + delete_mastercorporates + Users + + + generate_reading + Exams + + + view_reading + Exams + + + delete_reading + Exams + + + generate_listening + Exams + + + view_listening + Exams + + + delete_listening + Exams + + + generate_writing + Exams + + + view_writing + Exams + + + delete_writing + Exams + + + generate_speaking + Exams + + + view_speaking + Exams + + + delete_speaking + Exams + + + generate_level + Exams + + + view_level + Exams + + + delete_level + Exams + + + view_classrooms + Classrooms + + + create_classroom + Classrooms + + + rename_classrooms + Classrooms + + + add_to_classroom + Classrooms + + + remove_from_classroom + Classrooms + + + delete_classroom + Classrooms + + + view_entities + Entities + + + rename_entity + Entities + + + add_to_entity + Entities + + + remove_from_entity + Entities + + + delete_entity + Entities + + + view_entity_roles + Roles + + + create_entity_role + Roles + + + rename_entity_role + Roles + + + edit_role_permissions + Roles + + + assign_to_role + Roles + + + delete_entity_role + Roles + + + view_assignments + Assignments + + + create_assignment + Assignments + + + edit_assignment + Assignments + + + delete_assignment + Assignments + + + start_assignment + Assignments + + + archive_assignment + Assignments + + + view_entity_statistics + Statistics + + + create_user + Users + + + create_user_batch + Users + + + create_code + Codes + + + create_code_batch + Codes + + + view_code_list + Codes + + + delete_code + Codes + + + view_statistics + Statistics + + + download_statistics_report + Statistics + + + edit_grading_system + Grading + + + view_student_performance + Students + + + upload_classroom + Classrooms + + + download_user_list + Users + + + view_student_record + Students + + + download_student_record + Students + + + pay_entity + Payments + + + view_payment_record + Payments + + + view_approval_workflows + Workflows + + + update_exam_privacy + Exams + + + view_workflows + Workflows + + + configure_workflows + Workflows + + + edit_workflow + Workflows + + + delete_workflow + Workflows + + + view_confidential_exams + Exams + + + create_confidential_exams + Exams + + + create_public_exams + Exams + + + diff --git a/new_project/custom_addons/encoach_core/models/__init__.py b/new_project/custom_addons/encoach_core/models/__init__.py index 78a3a91eb..8c0d7bc48 100644 --- a/new_project/custom_addons/encoach_core/models/__init__.py +++ b/new_project/custom_addons/encoach_core/models/__init__.py @@ -1,6 +1,8 @@ from . import encoach_user from . import encoach_entity -from . import encoach_role -from . import encoach_permission -from . import encoach_invite_code +from . import role +from . import permission +from . import code +from . import invite +from . import user_entity_rel from . import entity_level_mapping diff --git a/new_project/custom_addons/encoach_core/models/code.py b/new_project/custom_addons/encoach_core/models/code.py new file mode 100644 index 000000000..c3992d3c3 --- /dev/null +++ b/new_project/custom_addons/encoach_core/models/code.py @@ -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, + } diff --git a/new_project/custom_addons/encoach_core/models/invite.py b/new_project/custom_addons/encoach_core/models/invite.py new file mode 100644 index 000000000..cf61e476b --- /dev/null +++ b/new_project/custom_addons/encoach_core/models/invite.py @@ -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, + } diff --git a/new_project/custom_addons/encoach_core/models/permission.py b/new_project/custom_addons/encoach_core/models/permission.py new file mode 100644 index 000000000..a6d6be70b --- /dev/null +++ b/new_project/custom_addons/encoach_core/models/permission.py @@ -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": [], + } diff --git a/new_project/custom_addons/encoach_core/models/role.py b/new_project/custom_addons/encoach_core/models/role.py new file mode 100644 index 000000000..98c7e490c --- /dev/null +++ b/new_project/custom_addons/encoach_core/models/role.py @@ -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, + } diff --git a/new_project/custom_addons/encoach_core/models/user_entity_rel.py b/new_project/custom_addons/encoach_core/models/user_entity_rel.py new file mode 100644 index 000000000..3fce89918 --- /dev/null +++ b/new_project/custom_addons/encoach_core/models/user_entity_rel.py @@ -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.", + ) diff --git a/new_project/custom_addons/encoach_core/security/encoach_security.xml b/new_project/custom_addons/encoach_core/security/encoach_security.xml new file mode 100644 index 000000000..471aaeb44 --- /dev/null +++ b/new_project/custom_addons/encoach_core/security/encoach_security.xml @@ -0,0 +1,30 @@ + + + + + + EnCoach Student + + + EnCoach Teacher + + + EnCoach Corporate + + + EnCoach Admin + + + + EnCoach Developer + + + + EnCoach Agent + + + EnCoach Master Corporate + + + + diff --git a/new_project/custom_addons/encoach_core/security/ir.model.access.csv b/new_project/custom_addons/encoach_core/security/ir.model.access.csv index 38214c3bb..5db3a3e75 100644 --- a/new_project/custom_addons/encoach_core/security/ir.model.access.csv +++ b/new_project/custom_addons/encoach_core/security/ir.model.access.csv @@ -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_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_invite_code_student,encoach.invite.code.student,model_encoach_invite_code,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_code_admin,encoach.invite.code.admin,model_encoach_invite_code,group_encoach_admin,1,1,1,1 +access_encoach_invite_student,encoach.invite.student,model_encoach_invite,group_encoach_student,1,0,0,0 +access_encoach_invite_teacher,encoach.invite.teacher,model_encoach_invite,group_encoach_teacher,1,1,0,0 +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_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 diff --git a/new_project/custom_addons/encoach_core/static/src/js/dashboard.js b/new_project/custom_addons/encoach_core/static/src/js/dashboard.js index 0b23df36e..f593c087b 100644 --- a/new_project/custom_addons/encoach_core/static/src/js/dashboard.js +++ b/new_project/custom_addons/encoach_core/static/src/js/dashboard.js @@ -11,7 +11,7 @@ class EncoachDashboard extends Component { static props = ["*"]; setup() { - this.rpc = useService("rpc"); + this.orm = useService("orm"); this.action = useService("action"); this.state = useState({ loading: true, @@ -24,11 +24,8 @@ class EncoachDashboard extends Component { pending_grading: 0, pending_approval: 0, total_entities: 0, - recent_signups: 0, - avg_score: 0, }, recentAttempts: [], - courseProgress: [], }); onWillStart(async () => { @@ -39,14 +36,14 @@ class EncoachDashboard extends Component { async loadDashboardData() { try { 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.rpc("/web/dataset/call_kw", { model: "res.users", method: "search_count", args: [[["user_type", "=", "teacher"]]], kwargs: {} }), - this.rpc("/web/dataset/call_kw", { model: "op.course", method: "search_count", args: [[]], kwargs: {} }), - this.rpc("/web/dataset/call_kw", { model: "encoach.exam.custom", method: "search_count", args: [[]], kwargs: {} }), - this.rpc("/web/dataset/call_kw", { model: "encoach.student.attempt", method: "search_count", args: [[["status", "=", "in_progress"]]], kwargs: {} }), - this.rpc("/web/dataset/call_kw", { model: "encoach.student.attempt", method: "search_count", args: [[["status", "=", "scoring"]]], kwargs: {} }), - this.rpc("/web/dataset/call_kw", { model: "encoach.student.attempt", method: "search_count", args: [[["status", "=", "pending_approval"]]], kwargs: {} }), - this.rpc("/web/dataset/call_kw", { model: "encoach.entity", method: "search_count", args: [[]], kwargs: {} }), + this.orm.searchCount("res.users", [["account_source", "!=", false]]), + this.orm.searchCount("res.users", [["user_type", "=", "teacher"]]), + this.orm.searchCount("op.course", []), + this.orm.searchCount("encoach.exam.custom", []), + this.orm.searchCount("encoach.student.attempt", [["status", "=", "in_progress"]]), + this.orm.searchCount("encoach.student.attempt", [["status", "=", "scoring"]]), + this.orm.searchCount("encoach.student.attempt", [["status", "=", "pending_approval"]]), + this.orm.searchCount("encoach.entity", []), ]); this.state.stats = { @@ -60,12 +57,12 @@ class EncoachDashboard extends Component { total_entities: entities, }; - const attempts = await this.rpc("/web/dataset/call_kw", { - model: "encoach.student.attempt", - method: "search_read", - args: [[], ["student_id", "exam_id", "status", "overall_band", "started_at"]], - kwargs: { limit: 10, order: "started_at desc" }, - }); + const attempts = await this.orm.searchRead( + "encoach.student.attempt", + [], + ["student_id", "exam_id", "status", "overall_band", "started_at"], + { limit: 10, order: "started_at desc" }, + ); this.state.recentAttempts = attempts; } catch (e) { diff --git a/new_project/custom_addons/encoach_core/views/encoach_entity_views.xml b/new_project/custom_addons/encoach_core/views/encoach_entity_views.xml new file mode 100644 index 000000000..af3ae4e02 --- /dev/null +++ b/new_project/custom_addons/encoach_core/views/encoach_entity_views.xml @@ -0,0 +1,115 @@ + + + + encoach.entity.list + encoach.entity + + + + + + + + + + + + + + + encoach.entity.form + encoach.entity + +
+ +
+
+ +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + encoach.entity.search + encoach.entity + + + + + + + + + + + + + + + Entities + encoach.entity + list,form + + +

+ Create your first entity +

+

Entities represent organizations (corporates, universities, schools) on the EnCoach platform.

+
+
+ + +
diff --git a/new_project/custom_addons/encoach_course_gen/static/src/js/gap_analysis.js b/new_project/custom_addons/encoach_course_gen/static/src/js/gap_analysis.js index d2387fef0..faa31fb96 100644 --- a/new_project/custom_addons/encoach_course_gen/static/src/js/gap_analysis.js +++ b/new_project/custom_addons/encoach_course_gen/static/src/js/gap_analysis.js @@ -11,7 +11,7 @@ class GapAnalysisVisualization extends Component { static props = ["*"]; setup() { - this.rpc = useService("rpc"); + this.orm = useService("orm"); this.action = useService("action"); this.state = useState({ loading: true, @@ -36,12 +36,12 @@ class GapAnalysisVisualization extends Component { async loadStudentList() { try { - this.state.studentList = await this.rpc("/web/dataset/call_kw", { - model: "res.users", - method: "search_read", - args: [[["account_source", "!=", false]], ["name", "email", "login"]], - kwargs: { limit: 50, order: "name" }, - }); + this.state.studentList = await this.orm.searchRead( + "res.users", + [["account_source", "!=", false]], + ["name", "email", "login"], + { limit: 50, order: "name" }, + ); } catch (e) { console.error("Failed to load students:", e); } finally { @@ -59,18 +59,17 @@ class GapAnalysisVisualization extends Component { this.state.loading = true; try { const [profiles, students] = await Promise.all([ - this.rpc("/web/dataset/call_kw", { - model: "encoach.gap.profile", - method: "search_read", - args: [[["student_id", "=", this.state.studentId]], ["student_id", "source_type", "skill_gaps", "question_type_weaknesses", "topic_weaknesses", "created_at"]], - kwargs: { limit: 1, order: "created_at desc" }, - }), - this.rpc("/web/dataset/call_kw", { - model: "res.users", - method: "search_read", - args: [[["id", "=", this.state.studentId]], ["name", "email"]], - kwargs: {}, - }), + this.orm.searchRead( + "encoach.gap.profile", + [["student_id", "=", this.state.studentId]], + ["student_id", "source_type", "skill_gaps", "question_type_weaknesses", "topic_weaknesses", "created_at"], + { limit: 1, order: "created_at desc" }, + ), + this.orm.searchRead( + "res.users", + [["id", "=", this.state.studentId]], + ["name", "email"], + ), ]); this.state.student = students.length ? students[0] : null; diff --git a/new_project/custom_addons/encoach_course_gen/views/course_module_views.xml b/new_project/custom_addons/encoach_course_gen/views/course_module_views.xml index 303d3cae7..4fe5fabaf 100644 --- a/new_project/custom_addons/encoach_course_gen/views/course_module_views.xml +++ b/new_project/custom_addons/encoach_course_gen/views/course_module_views.xml @@ -60,9 +60,7 @@ - - - + diff --git a/new_project/custom_addons/encoach_course_gen/views/gap_profile_views.xml b/new_project/custom_addons/encoach_course_gen/views/gap_profile_views.xml index 647a7af54..fce46286d 100644 --- a/new_project/custom_addons/encoach_course_gen/views/gap_profile_views.xml +++ b/new_project/custom_addons/encoach_course_gen/views/gap_profile_views.xml @@ -57,10 +57,8 @@ - - - - + + diff --git a/new_project/custom_addons/encoach_exam_template/controllers/ielts_exam.py b/new_project/custom_addons/encoach_exam_template/controllers/ielts_exam.py index 1d0758fba..ced95e2de 100644 --- a/new_project/custom_addons/encoach_exam_template/controllers/ielts_exam.py +++ b/new_project/custom_addons/encoach_exam_template/controllers/ielts_exam.py @@ -238,7 +238,7 @@ class EncoachIeltsExamController(http.Controller): page = int(kw.get('page', 1)) 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) return _json_response({ diff --git a/new_project/custom_addons/encoach_exam_template/controllers/templates.py b/new_project/custom_addons/encoach_exam_template/controllers/templates.py index bc50f31e6..2c40fb646 100644 --- a/new_project/custom_addons/encoach_exam_template/controllers/templates.py +++ b/new_project/custom_addons/encoach_exam_template/controllers/templates.py @@ -60,7 +60,7 @@ class EncoachTemplatesController(http.Controller): page = int(kw.get('page', 1)) 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) return _json_response({ diff --git a/new_project/custom_addons/encoach_exam_template/static/src/js/content_pool.js b/new_project/custom_addons/encoach_exam_template/static/src/js/content_pool.js index e5e71e9bb..a5a40a1ff 100644 --- a/new_project/custom_addons/encoach_exam_template/static/src/js/content_pool.js +++ b/new_project/custom_addons/encoach_exam_template/static/src/js/content_pool.js @@ -11,7 +11,7 @@ class ContentPoolBrowser extends Component { static props = ["*"]; setup() { - this.rpc = useService("rpc"); + this.orm = useService("orm"); this.action = useService("action"); this.state = useState({ loading: true, @@ -30,22 +30,17 @@ class ContentPoolBrowser extends Component { const domain = this._buildDomain(); try { const [questions, count] = await Promise.all([ - this.rpc("/web/dataset/call_kw", { - model: "encoach.question", - method: "search_read", - args: [domain, ["skill", "question_type", "stem", "difficulty", "marks", "irt_b", "subject_id", "topic_id", "status"]], - kwargs: { + this.orm.searchRead( + "encoach.question", + domain, + ["skill", "question_type", "stem", "difficulty", "marks", "irt_b", "subject_id", "topic_id", "status"], + { limit: this.state.pageSize, offset: (this.state.page - 1) * this.state.pageSize, order: "skill, difficulty", }, - }), - this.rpc("/web/dataset/call_kw", { - model: "encoach.question", - method: "search_count", - args: [domain], - kwargs: {}, - }), + ), + this.orm.searchCount("encoach.question", domain), ]); this.state.questions = questions; this.state.totalCount = count; diff --git a/new_project/custom_addons/encoach_exam_template/static/src/js/exam_validation.js b/new_project/custom_addons/encoach_exam_template/static/src/js/exam_validation.js index 7753dc931..fe9cdbc18 100644 --- a/new_project/custom_addons/encoach_exam_template/static/src/js/exam_validation.js +++ b/new_project/custom_addons/encoach_exam_template/static/src/js/exam_validation.js @@ -11,7 +11,7 @@ class ExamValidationReport extends Component { static props = ["*"]; setup() { - this.rpc = useService("rpc"); + this.orm = useService("orm"); this.action = useService("action"); this.state = useState({ loading: true, @@ -38,12 +38,12 @@ class ExamValidationReport extends Component { async loadExamList() { try { - this.state.examList = await this.rpc("/web/dataset/call_kw", { - model: "encoach.exam.template", - method: "search_read", - args: [[], ["name", "code", "type", "total_time_min"]], - kwargs: { limit: 50, order: "name" }, - }); + this.state.examList = await this.orm.searchRead( + "encoach.exam.template", + [], + ["name", "code", "type", "total_time_min"], + { limit: 50, order: "name" }, + ); } catch (e) { console.error("Failed to load exam list:", e); } finally { @@ -60,14 +60,11 @@ class ExamValidationReport extends Component { async runValidation() { this.state.loading = true; try { - const [exams] = await Promise.all([ - this.rpc("/web/dataset/call_kw", { - model: "encoach.exam.template", - method: "search_read", - args: [[["id", "=", this.state.examId]], ["name", "code", "type", "total_time_min", "pass_threshold", "structure", "subject_id"]], - kwargs: {}, - }), - ]); + const exams = await this.orm.searchRead( + "encoach.exam.template", + [["id", "=", this.state.examId]], + ["name", "code", "type", "total_time_min", "pass_threshold", "structure", "subject_id"], + ); this.state.exam = exams.length ? exams[0] : null; if (!this.state.exam) { @@ -75,12 +72,11 @@ class ExamValidationReport extends Component { return; } - const questions = await this.rpc("/web/dataset/call_kw", { - model: "encoach.question", - method: "search_read", - args: [[], ["skill", "difficulty", "marks", "status"]], - kwargs: {}, - }); + const questions = await this.orm.searchRead( + "encoach.question", + [], + ["skill", "difficulty", "marks", "status"], + ); this.state.checks = this._runChecks(this.state.exam, questions); this.state.passCount = this.state.checks.filter(c => c.status === "pass").length; diff --git a/new_project/custom_addons/encoach_exam_template/views/audio_file_views.xml b/new_project/custom_addons/encoach_exam_template/views/audio_file_views.xml index 98fd59f96..3886e9a06 100644 --- a/new_project/custom_addons/encoach_exam_template/views/audio_file_views.xml +++ b/new_project/custom_addons/encoach_exam_template/views/audio_file_views.xml @@ -78,11 +78,9 @@ - - - - - + + + diff --git a/new_project/custom_addons/encoach_exam_template/views/exam_assignment_views.xml b/new_project/custom_addons/encoach_exam_template/views/exam_assignment_views.xml index ce441ab4b..9f5086a3f 100644 --- a/new_project/custom_addons/encoach_exam_template/views/exam_assignment_views.xml +++ b/new_project/custom_addons/encoach_exam_template/views/exam_assignment_views.xml @@ -58,11 +58,9 @@ - - - - - + + + diff --git a/new_project/custom_addons/encoach_exam_template/views/exam_custom_views.xml b/new_project/custom_addons/encoach_exam_template/views/exam_custom_views.xml index 4676b2d79..caad339c7 100644 --- a/new_project/custom_addons/encoach_exam_template/views/exam_custom_views.xml +++ b/new_project/custom_addons/encoach_exam_template/views/exam_custom_views.xml @@ -83,11 +83,9 @@ - - - - - + + + diff --git a/new_project/custom_addons/encoach_exam_template/views/exam_template_views.xml b/new_project/custom_addons/encoach_exam_template/views/exam_template_views.xml index a6dc854cb..1e255f09a 100644 --- a/new_project/custom_addons/encoach_exam_template/views/exam_template_views.xml +++ b/new_project/custom_addons/encoach_exam_template/views/exam_template_views.xml @@ -79,11 +79,9 @@ - - - - - + + + diff --git a/new_project/custom_addons/encoach_exam_template/views/passage_views.xml b/new_project/custom_addons/encoach_exam_template/views/passage_views.xml index 4b5b4c3e1..96690fd6c 100644 --- a/new_project/custom_addons/encoach_exam_template/views/passage_views.xml +++ b/new_project/custom_addons/encoach_exam_template/views/passage_views.xml @@ -83,11 +83,9 @@ - - - - - + + + diff --git a/new_project/custom_addons/encoach_exam_template/views/question_views.xml b/new_project/custom_addons/encoach_exam_template/views/question_views.xml index 39f06dd97..7e10ba0f6 100644 --- a/new_project/custom_addons/encoach_exam_template/views/question_views.xml +++ b/new_project/custom_addons/encoach_exam_template/views/question_views.xml @@ -104,13 +104,11 @@ - - - - - - - + + + + + diff --git a/new_project/custom_addons/encoach_exam_template/views/rubric_views.xml b/new_project/custom_addons/encoach_exam_template/views/rubric_views.xml index 904b534a7..abe36875e 100644 --- a/new_project/custom_addons/encoach_exam_template/views/rubric_views.xml +++ b/new_project/custom_addons/encoach_exam_template/views/rubric_views.xml @@ -56,10 +56,8 @@ - - - - + + diff --git a/new_project/custom_addons/encoach_exam_template/views/speaking_card_views.xml b/new_project/custom_addons/encoach_exam_template/views/speaking_card_views.xml index 0c23ce2b7..d4c627843 100644 --- a/new_project/custom_addons/encoach_exam_template/views/speaking_card_views.xml +++ b/new_project/custom_addons/encoach_exam_template/views/speaking_card_views.xml @@ -82,10 +82,8 @@ - - - - + + diff --git a/new_project/custom_addons/encoach_exam_template/views/writing_prompt_views.xml b/new_project/custom_addons/encoach_exam_template/views/writing_prompt_views.xml index bf5d39df6..7ab6e04be 100644 --- a/new_project/custom_addons/encoach_exam_template/views/writing_prompt_views.xml +++ b/new_project/custom_addons/encoach_exam_template/views/writing_prompt_views.xml @@ -74,10 +74,8 @@ - - - - + + diff --git a/new_project/custom_addons/encoach_placement/views/cat_session_views.xml b/new_project/custom_addons/encoach_placement/views/cat_session_views.xml index e1c2dfbd1..8a1214c28 100644 --- a/new_project/custom_addons/encoach_placement/views/cat_session_views.xml +++ b/new_project/custom_addons/encoach_placement/views/cat_session_views.xml @@ -60,9 +60,7 @@ - - - + diff --git a/new_project/custom_addons/encoach_placement/views/student_ability_views.xml b/new_project/custom_addons/encoach_placement/views/student_ability_views.xml index 825f389d1..bca702147 100644 --- a/new_project/custom_addons/encoach_placement/views/student_ability_views.xml +++ b/new_project/custom_addons/encoach_placement/views/student_ability_views.xml @@ -52,10 +52,8 @@ - - - - + + diff --git a/new_project/custom_addons/encoach_scoring/views/student_attempt_views.xml b/new_project/custom_addons/encoach_scoring/views/student_attempt_views.xml index 1e4e099de..33be652ca 100644 --- a/new_project/custom_addons/encoach_scoring/views/student_attempt_views.xml +++ b/new_project/custom_addons/encoach_scoring/views/student_attempt_views.xml @@ -101,11 +101,9 @@ - - - - - + + + diff --git a/new_project/custom_addons/encoach_signup/views/student_profile_views.xml b/new_project/custom_addons/encoach_signup/views/student_profile_views.xml index ed60aab96..8746b32fc 100644 --- a/new_project/custom_addons/encoach_signup/views/student_profile_views.xml +++ b/new_project/custom_addons/encoach_signup/views/student_profile_views.xml @@ -50,6 +50,7 @@ + @@ -59,9 +60,7 @@ - - - + diff --git a/new_project/custom_addons/encoach_taxonomy/__manifest__.py b/new_project/custom_addons/encoach_taxonomy/__manifest__.py index 48afdb71f..9b72aef90 100644 --- a/new_project/custom_addons/encoach_taxonomy/__manifest__.py +++ b/new_project/custom_addons/encoach_taxonomy/__manifest__.py @@ -7,6 +7,7 @@ 'depends': ['encoach_core'], 'data': [ 'security/ir.model.access.csv', + 'views/taxonomy_views.xml', 'data/default_subjects.xml', 'data/math_it_taxonomy.xml', ], diff --git a/new_project/custom_addons/encoach_taxonomy/controllers/taxonomy.py b/new_project/custom_addons/encoach_taxonomy/controllers/taxonomy.py index 1cd93ce0d..5479cd6ca 100644 --- a/new_project/custom_addons/encoach_taxonomy/controllers/taxonomy.py +++ b/new_project/custom_addons/encoach_taxonomy/controllers/taxonomy.py @@ -40,8 +40,8 @@ class EncoachTaxonomyController(http.Controller): items.append({ 'id': s.id, 'name': s.name, - 'code': s.code, - 'icon': s.icon or '', + 'code': s.code or '', + 'description': s.description or '', 'domain_count': len(s.domain_ids), }) @@ -79,7 +79,6 @@ class EncoachTaxonomyController(http.Controller): topics.append({ 'id': t.id, 'name': t.name, - 'difficulty': t.difficulty or '', 'description': t.description or '', 'learning_objectives': objectives, }) @@ -92,8 +91,8 @@ class EncoachTaxonomyController(http.Controller): tree.append({ 'id': s.id, 'name': s.name, - 'code': s.code, - 'icon': s.icon or '', + 'code': s.code or '', + 'description': s.description or '', 'domains': domains, }) @@ -134,8 +133,6 @@ class EncoachTaxonomyController(http.Controller): if not code: return _error_response('code is required for subjects', 400) vals['code'] = code - if body.get('icon'): - vals['icon'] = body['icon'] else: parent_id = body.get('parent_id') if not parent_id: @@ -145,8 +142,6 @@ class EncoachTaxonomyController(http.Controller): ) 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'): vals['bloom_level'] = body['bloom_level'] @@ -192,12 +187,8 @@ class EncoachTaxonomyController(http.Controller): if node_type == 'subject': if body.get('code'): vals['code'] = body['code'] - if 'icon' in body: - vals['icon'] = body.get('icon') or '' if 'active' in body: 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'): vals['bloom_level'] = body['bloom_level'] diff --git a/new_project/custom_addons/encoach_taxonomy/data/default_subjects.xml b/new_project/custom_addons/encoach_taxonomy/data/default_subjects.xml new file mode 100644 index 000000000..19971d9d9 --- /dev/null +++ b/new_project/custom_addons/encoach_taxonomy/data/default_subjects.xml @@ -0,0 +1,23 @@ + + + + + English + ENG + English Language + 1 + + + Mathematics + MATH + Mathematics + 2 + + + Information Technology + IT + Information Technology and Computer Science + 3 + + + diff --git a/new_project/custom_addons/encoach_taxonomy/models/__init__.py b/new_project/custom_addons/encoach_taxonomy/models/__init__.py new file mode 100644 index 000000000..9008366d9 --- /dev/null +++ b/new_project/custom_addons/encoach_taxonomy/models/__init__.py @@ -0,0 +1,4 @@ +from . import subject +from . import domain +from . import topic +from . import learning_objective diff --git a/new_project/custom_addons/encoach_taxonomy/models/domain.py b/new_project/custom_addons/encoach_taxonomy/models/domain.py new file mode 100644 index 000000000..6bb908f4f --- /dev/null +++ b/new_project/custom_addons/encoach_taxonomy/models/domain.py @@ -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, + } diff --git a/new_project/custom_addons/encoach_taxonomy/models/learning_objective.py b/new_project/custom_addons/encoach_taxonomy/models/learning_objective.py new file mode 100644 index 000000000..23a1e0e48 --- /dev/null +++ b/new_project/custom_addons/encoach_taxonomy/models/learning_objective.py @@ -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, + } diff --git a/new_project/custom_addons/encoach_taxonomy/models/subject.py b/new_project/custom_addons/encoach_taxonomy/models/subject.py new file mode 100644 index 000000000..ed3eb8bdc --- /dev/null +++ b/new_project/custom_addons/encoach_taxonomy/models/subject.py @@ -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, + } diff --git a/new_project/custom_addons/encoach_taxonomy/models/topic.py b/new_project/custom_addons/encoach_taxonomy/models/topic.py new file mode 100644 index 000000000..00fd7cf95 --- /dev/null +++ b/new_project/custom_addons/encoach_taxonomy/models/topic.py @@ -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, + } diff --git a/new_project/custom_addons/encoach_taxonomy/security/ir.model.access.csv b/new_project/custom_addons/encoach_taxonomy/security/ir.model.access.csv new file mode 100644 index 000000000..e471f5bd1 --- /dev/null +++ b/new_project/custom_addons/encoach_taxonomy/security/ir.model.access.csv @@ -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 diff --git a/new_project/custom_addons/encoach_taxonomy/views/taxonomy_views.xml b/new_project/custom_addons/encoach_taxonomy/views/taxonomy_views.xml new file mode 100644 index 000000000..9824f5947 --- /dev/null +++ b/new_project/custom_addons/encoach_taxonomy/views/taxonomy_views.xml @@ -0,0 +1,150 @@ + + + + + encoach.subject.list + encoach.subject + + + + + + + + + + + + encoach.subject.form + encoach.subject + +
+ +
+

+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + Subjects + encoach.subject + list,form + + + + + encoach.domain.list + encoach.domain + + + + + + + + + + + + encoach.domain.form + encoach.domain + +
+ +
+

+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + Domains + encoach.domain + list,form + + + + + encoach.topic.list + encoach.topic + + + + + + + + + + + + Topics + encoach.topic + list,form + + + + + + + + +