Files
Yamen Ahmad 3e83d8d7d5 feat: add complete EnCoach backend — Odoo 19 + all addons
Includes:
- Odoo 19 framework (odoo/)
- 27 custom EnCoach addons (new_project/custom_addons/)
  - encoach_core, encoach_api, encoach_lms_api, encoach_adaptive_api
  - encoach_exam, encoach_taxonomy, encoach_adaptive, encoach_assignment
  - encoach_ai, encoach_ai_grading, encoach_ai_generation, encoach_ai_media
  - encoach_courseware, encoach_communication, encoach_subscription
  - encoach_notification, encoach_approval, encoach_branding
  - encoach_classroom, encoach_registration, encoach_stats
  - encoach_faq, encoach_ticket, encoach_training, encoach_resources
  - encoach_adaptive_ai, encoach_sis
- 21 OpenEduCat Enterprise modules (new_project/enterprise-19/)
- 14 OpenEduCat Community modules (new_project/openeducat_erp-19.0/)
- Configuration: odoo.conf, requirements.txt, scripts
- 200+ REST API endpoints with JWT authentication
- SRS and test documentation

Made-with: Cursor
2026-04-01 17:10:04 +04:00

251 lines
8.3 KiB
JavaScript

import { App, EventBus } from "@odoo/owl";
import { SERVICES_METADATA } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { getTemplate } from "@web/core/templates";
import { appTranslateFn } from "@web/core/l10n/translation";
import { session } from "@web/session";
import { isMacOS } from "@web/core/browser/feature_detection";
// -----------------------------------------------------------------------------
// Types
// -----------------------------------------------------------------------------
/**
* @typedef {{
* bus: EventBus;
* debug: string;
* services: import("services").Services;
* readonly isSmall: boolean;
* }} OdooEnv
*/
// -----------------------------------------------------------------------------
// makeEnv
// -----------------------------------------------------------------------------
/**
* Return a value Odoo Env object
*
* @returns {OdooEnv}
*/
export function makeEnv() {
const bus = new EventBus();
const prom = new Promise((resolve) => {
bus.addEventListener("SERVICES-LOADED", resolve, { once: true });
});
return {
bus,
isReady: prom,
services: {},
debug: odoo.debug,
get isSmall() {
throw new Error("UI service not initialized!");
},
};
}
// -----------------------------------------------------------------------------
// Service Launcher
// -----------------------------------------------------------------------------
const serviceRegistry = registry.category("services");
serviceRegistry.addValidation({
start: Function,
dependencies: { type: Array, element: String, optional: true },
async: { type: [{ type: Array, element: String }, { value: true }], optional: true },
"*": true,
});
let startServicesPromise = null;
/**
* Start all services registered in the service registry, while making sure
* each service dependencies are properly fulfilled.
*
* @param {OdooEnv} env
* @returns {Promise<void>}
*/
export async function startServices(env) {
// Wait for all synchronous code so that if new services that depend on
// one another are added to the registry, they're all present before we
// start them regardless of the order they're added to the registry.
await Promise.resolve();
const toStart = new Map();
serviceRegistry.addEventListener("UPDATE", async (ev) => {
// Wait for all synchronous code so that if new services that depend on
// one another are added to the registry, they're all present before we
// start them regardless of the order they're added to the registry.
await Promise.resolve();
const { operation, key: name, value: service } = ev.detail;
if (operation === "delete") {
// We hardly see why it would be usefull to remove a service.
// Furthermore we could encounter problems with dependencies.
// Keep it simple!
return;
}
if (toStart.size) {
const namedService = Object.assign(Object.create(service), { name });
toStart.set(name, namedService);
} else {
await _startServices(env, toStart);
}
});
await _startServices(env, toStart);
}
async function _startServices(env, toStart) {
if (startServicesPromise) {
return startServicesPromise.then(() => _startServices(env, toStart));
}
const services = env.services;
for (const [name, service] of serviceRegistry.getEntries()) {
if (!(name in services)) {
const namedService = Object.assign(Object.create(service), { name });
toStart.set(name, namedService);
}
}
// start as many services in parallel as possible
async function start() {
let service = null;
const proms = [];
while ((service = findNext())) {
const name = service.name;
toStart.delete(name);
const entries = (service.dependencies || []).map((dep) => [dep, services[dep]]);
const dependencies = Object.fromEntries(entries);
if (name in services) {
continue;
}
const value = service.start(env, dependencies);
if ("async" in service) {
SERVICES_METADATA[name] = service.async;
}
proms.push(
Promise.resolve(value).then((val) => {
services[name] = val || null;
})
);
}
await Promise.all(proms);
if (proms.length) {
return start();
}
}
startServicesPromise = start().finally(() => {
startServicesPromise = null;
});
await startServicesPromise;
env.bus.trigger("SERVICES-LOADED");
if (toStart.size) {
const missingDeps = new Set();
for (const service of toStart.values()) {
for (const dependency of service.dependencies) {
if (!(dependency in services) && !toStart.has(dependency)) {
missingDeps.add(dependency);
}
}
}
const depNames = [...missingDeps].join(", ");
throw new Error(
`Some services could not be started: ${[
...toStart.keys(),
]}. Missing dependencies: ${depNames}`
);
}
function findNext() {
for (const s of toStart.values()) {
if (s.dependencies) {
if (s.dependencies.every((d) => d in services)) {
return s;
}
} else {
return s;
}
}
return null;
}
}
export const customDirectives = {
// t-custom-click="handler"
// This custom directive will add two even listeners ("click"; "auxclick") and call the global value "click".
// The global value "click" will call the handler with two parameters :
// - ev (the original event)
// - isMiddleClick (a boolean that says if the user middle clicked, or if he did a ctrl+click)
//
click: (node, value, modifiers) => {
let mods = "";
if (modifiers.includes("synthetic")) {
mods += ".synthetic";
}
if (modifiers.includes("capture")) {
mods += ".capture";
}
const handlerFunction = `(ev) => __globals__.click(ev, (${value}).bind(this), '${JSON.stringify(
modifiers
)}')`;
node.setAttribute(`t-on-click${mods}`, handlerFunction);
node.setAttribute(`t-on-auxclick${mods}`, handlerFunction);
},
};
export const globalValues = {
click: (ev, value, modifiers) => {
if (ev.button === 0 || ev.button === 1) {
modifiers = JSON.parse(modifiers);
for (const modifier of modifiers) {
if (modifier === "stop") {
ev.stopPropagation();
}
if (modifier === "prevent") {
ev.preventDefault();
}
}
const ctrlKey = isMacOS() ? ev.metaKey : ev.ctrlKey;
const isMiddleClick = (ctrlKey && ev.button === 0) || ev.button === 1;
value(ev, isMiddleClick);
}
},
};
/**
* Create an application with a given component as root and mount it. If no env
* is provided, the application will be treated as a "root": an env will be
* created and the services will be started, it will also be set as the root
* in `__WOWL_DEBUG__`
*
* @param {import("@odoo/owl").Component} component the component to mount
* @param {HTMLElement} target the HTML element in which to mount the app
* @param {Partial<ConstructorParameters<typeof App>[1]>} [appConfig] object
* containing a (partial) config for the app.
*/
export async function mountComponent(component, target, appConfig = {}) {
let { env } = appConfig;
const isRoot = !env;
if (isRoot) {
env = await makeEnv();
await startServices(env);
}
const app = new App(component, {
env,
getTemplate,
dev: env.debug || session.test_mode,
warnIfNoStaticProps: !session.test_mode,
name: component.constructor.name,
translatableAttributes: ["data-tooltip"],
translateFn: appTranslateFn,
customDirectives,
globalValues,
...appConfig,
});
const root = await app.mount(target);
if (isRoot) {
odoo.__WOWL_DEBUG__ = { root };
}
return app;
}