# OdooshCN Lab Environments Syncs the users of this Odoo instance to an OdooshCN platform and gives every user a one-click entry to their own lab environment. ## Install 1. Add the parent folder to `addons_path`: ``` addons_path = ...,D:\odoo_project\odoo18-2\odoo\custom_project\school-python ``` 2. Restart Odoo, update the app list and install **OdooshCN Lab Environments**. ## Configure ### 1. Create an access token on OdooshCN Sign in as a super administrator, open the user menu in the top right corner and choose **Access tokens**, then create one: | Field | Suggested value | |---|---| | Name | Odoo course system | | Source | `school` (synced users are recorded under this source) | | Organisation | where the users should land | | Scopes | tick all four | | Maximum grantable role | Organisation manager, if any user needs that role | | Instances per user | 1 | | Allow deleting users | on, if deleting a user here should delete the platform account | | Allow other organisations / platform administrator | off | The token is shown once, so copy it immediately. Scopes can be edited later without changing the token value. ### 2. Fill it in here Settings > General Settings > OdooshCN Lab Environments: 1. Turn on **Enable sync**. 2. Enter the platform URL and the token, then press **Test connection**. It validates the URL and token only, reports the organisation, the scopes and the limits, and warns when a scope is missing. For local testing use `http://127.0.0.1:port` rather than `localhost`: on Windows `localhost` resolves to IPv6 first while WSL2 and Docker only forward IPv4, and every request would wait out a 20 second timeout. The module rewrites localhost anyway, but an explicit address is safer. 3. Adjust the environment defaults if needed and save. That is all. There is nothing to configure per user. ## Daily use New users are synced automatically. Everything the platform knows about a user sits on the **Lab Environment** tab of the user form: | Field | Meaning | Default | |---|---|---| | Sync to lab platform | Master switch for this user | On | | Platform username override | Empty derives it from the login | Empty | | Organisation | Platform organisation slug | Empty (default) | | Platform role | Regular user or platform administrator | Regular user | | Role in organisation | Tester / Developer / Organisation manager | Developer | | Web Shell, Enterprise edition, Auto-provision | Switches | On / Off / On | | Instance limit, Extra database limit | 0 means unlimited | 1 / 2 | | Platform menus | Overview / Git / AI / Backups | Off / Off / On / Off | Select several users in the list and use the **Actions** menu to sync, enable or disable them in bulk. Users open their environment from the **Lab Environment** menu. The platform console - the instance list and the instance detail page - is embedded in a frame on that same page, so nobody leaves the course system, with an *Open in a new tab* button for a full window. The instance's own Odoo always opens in a new tab: nesting a whole Odoo inside another one stacks three navigation bars and two sidebars. For the frame to work the platform has to allow being embedded by this Odoo. Its console answers `X-Frame-Options: SAMEORIGIN` by default and the frame stays blank; the platform administrator drops a file `console-frame-odoo.inc` into `/nginx/` naming this server, then reloads the platform's web container: ``` add_header Content-Security-Policy "frame-ancestors 'self' http://school.example.com:8069" always; ``` The value has to match what users actually have in the address bar, scheme and port included: `http://localhost:8069` does not cover `http://the-machine-name:8069`. ## What happens when | In Odoo | On the platform | |---|---| | Create a user | Account created, sync switch turned on, environment provisioned | | Update a user | Pushed when the switch is on, ignored when it is off | | Archive a user | Account deactivated, instances stopped, nothing deleted | | Restore an archived user | Account activated again | | Delete a user | Account deleted; the instances move to the platform recycle bin, where the data stays recoverable for the retention period | Deleting requires the access token to allow it. Without that permission the platform refuses and the queue row shows `delete_not_allowed`; the account is then only deactivated. Portal users and built-in Odoo accounts (OdooBot, the public user, the portal template and the new-user default template) are never synced. ## How syncing works The hooks only write a row into `odoosh.sync.log`; **no HTTP happens inside a user request**. A dedicated thread pushes right after the transaction commits, using its own database connection, so a failure there can never affect what was already saved. Three paths back each other up: | Path | Trigger | Purpose | |---|---|---| | Background thread | Right after commit | The user barely notices a delay | | Queue cron | Every minute | Takes over when the thread was killed with the worker, and drives the retry back-off | | User scan | Inside that same cron, every 30 minutes | Walks users changed since the last scan and compares fingerprints | The scan has no cron of its own: it is cheap, but Odoo's cron pool is shared by the whole database (`max_cron_threads`, 2 by default), so a slot costs more than the scan does. Set the system parameter `odoosh.scan_interval_minutes` to change the interval, or to `0` to turn the scan off and rely on the hooks alone. Cost control: only `login`, `name`, `active`, the sync switch and the platform permission fields trigger a push, so changing an avatar or a preference does not; the scan reads at most 200 users per round through an index; identical content is never pushed twice. A daily reconciliation sends the full list of users so the platform can deactivate accounts that disappeared. An empty local list is skipped, and the platform refuses a list that would deactivate an implausible share of the accounts. ## Security * The token is stored in a system parameter, readable by system administrators only, and the settings page shows only a mask plus the last four characters. * The identity sent to the platform always comes from the current session, never from a request parameter. Otherwise a user could edit a URL and land in somebody else's environment. * Entry URLs are short lived and single use: they are redirected to immediately, never stored and never logged. ## Adjusting the code Everything a user gets is a field, so day-to-day changes need no code. If the mapping itself has to change, it lives in `models/res_users.py`: * `_odoosh_payload()` builds the user payload from the fields. * `_odoosh_env_spec()` builds the environment specification.