# Technical documentation — doh_moodle

Evidence date: 2026-10-16.

Source: `main` at `b9db23cf33d9ec93ae8764a160ec98f00f6f0441` (2026-06-24).

This document covers the Overdrive delta and the platform facts needed to run it. It does not describe Moodle core internals.

## Platform

| Item | Value | Evidence |
|---|---|---|
| Product | Moodle LMS plus local plugins | `version.php`, `local/` |
| Release | `4.5.7+ (Build: 20251009)` | `version.php` `$release` |
| Version number | `2024100707.01` | `$version` |
| Branch | `405` | `$branch` |
| Maturity | `MATURITY_STABLE` for core | `$maturity` |
| PHP | `>=8.1.0` | `composer.json` |
| Licence | GPL-3.0-or-later | `composer.json`, `COPYING.txt` |
| Package name | `moodle/moodle` | `composer.json` |
| Git remote | `git@bitbucket.org:overdrive/doh_moodle.git` | clone |
| Default branch inspected | `main` | `git status` at clone |

History in the depth-50 clone starts at `1166e795` (2025-10-24, Rachel Mogano, "Initial commit"). The Moodle tree was added in a series of commits on 2025-12-11. Custom work after that is principally Joshua G-D (`joshgatdew`), with pipeline commits by Jaco Brits and a "Gibbon Sync" commit by rachelmogano (`2b73d6b7`, 2026-02-17).

## Repository layout (custom and operational)

```text
version.php                      Moodle core version
config-dist.php                  upstream sample
configx.php                      committed local config (credentials; do not copy)
azure-pipelines.yml              Azure DevOps deploy
bitbucket-pipelines.yml          SonarQube on main
sonar-project.properties         project key only
local/gibbon_cohort/             cohort, enrolment, credits
local/gibbon_sync/               web services for Gibbon
local/course_startdate/          start-date visibility
local/adminer/                   third-party Adminer
plagiarism/azureai/              plagiarism plugin
gibbon_module/                   Gibbon client, not a Moodle plugin
check_*.php, assign_admin_role*.php, and similar
                                 root diagnostic scripts
```

Themes present: `boost`, `classic`. No custom theme directory.

## Plugins

### local_gibbon_cohort

| Field | Value |
|---|---|
| Component | `local_gibbon_cohort` |
| Version | `2026031903` |
| Release | `1.2.0` |
| Requires | `2022111800` (Moodle 4.1) |
| Maturity | alpha |
| Copyright | 2025 |

Scheduled task:

| Class | Minute | Hour | Day | Month | Day of week | Blocking |
|---|---|---|---|---|---|---|
| `local_gibbon_cohort\task\sync_all` | `*/5` | `*` | `*` | `*` | `*` | 0 |

`sync_all` calls, in order: `sync_cohorts` (if `synccohorts`), `credits_sync::run` (always), `sync_cohort_members` (if `synccohortmembers`), `sync_cohort_enrolments` (if `synccohortenrolments`), `course_group_mode_sync::run` (always).

Hooks (`db/hooks.php`):

- `\core\hook\navigation\primary_extend` → `hook_callbacks::primary_extend`
- `\core_user\hook\extend_user_menu` → `hook_callbacks::extend_user_menu`

Observer: `\core\event\user_graded` → `local_gibbon_cohort\event\observer::user_graded`.

Page: `/local/gibbon_cohort/my_credits.php` (`require_login`, system context, standard layout).

Capability: `local/gibbon_cohort:manage`, write, system context, manager archetype `CAP_ALLOW`.

External database connection is Moodle's `moodle_database` style via `classes/db_helper.php` (ADO-style `Execute`, `db_init`, `db_get_sql`). Drivers offered in settings: MySQLi (default), PDO, PostgreSQL. Setup SQL and Sybase quoting are settings. `debugdb` is a checkbox defaulting off.

Default view and field names are in `settings.php` (`moodleCohort`, `moodleCohortMember`, `moodleCohortCourse`, `moodleCohortCourseCredits`, `moodleYearGroupCredits`, `moodleCourse`, and the field names in the PRD).

### local_gibbon_sync

| Field | Value |
|---|---|
| Component | `local_gibbon_sync` |
| Version | `2025112000` |
| Release | `1.0` |
| Requires | `2022041900` (Moodle 4.0) |
| Maturity | alpha |

External service declaration (`db/services.php`):

| Key | Value |
|---|---|
| Name | Gibbon Sync Service |
| Shortname | `gibbon_sync` |
| Enabled in code | 1 |
| Restricted users | 0 |
| Download / upload files | 0 |

Functions and classes:

| `wsfunction` | Class | Method | Type |
|---|---|---|---|
| `local_gibbon_sync_get_students` | `local_gibbon_sync\api\grades` | `get_students` | read |
| `local_gibbon_sync_get_grades` | `local_gibbon_sync\api\grades` | `get_grades` | read |
| `local_gibbon_sync_sync_grades` | `local_gibbon_sync\api\grades` | `sync_grades` | write |
| `local_gibbon_sync_get_course` | `local_gibbon_sync_external_get_course` | `get_course` | read |
| `local_gibbon_sync_get_courses` | `local_gibbon_sync_external_get_courses` | `get_courses` | read |

`get_course` and `get_courses` set `classpath` because they are global classes, not namespaced. `loginrequired` is true on those two declarations. The `grades` methods do not set `loginrequired` in the array that was read. Moodle still requires a token for REST.

Example base URL from the README (placeholder host):

```text
https://your-moodle.com/webservice/rest/server.php
  ?wstoken=TOKEN
  &wsfunction=local_gibbon_sync_get_grades
  &moodlewsrestformat=json
  &api_key=YOUR_API_KEY
  &course_id=5
```

The `gibbon_module` README example uses `http://localhost/moodle_stable/webservice/rest/server.php`, which matches `configx.php` `$CFG->wwwroot`.

### local_course_startdate

| Field | Value |
|---|---|
| Component | `local_course_startdate` |
| Version | `2026051802` |
| Release | `1.0.0` |
| Requires | `2022111800` |
| Maturity | stable |

Callbacks in `lib.php`: `local_course_startdate_filter_course_visible_to_user` (Moodle course-list filter name derived from the component) and `local_course_startdate_after_require_login`.

Settings: `enabled` default 1, `usegraceperiod` default 1. The grace label text is inlined in `settings.php` rather than only in the language pack. The comment says that avoids a stale string cache.

Tests: `tests/course_access_test.php`, `@covers \local_course_startdate\course_access`.

### local_adminer

Third-party. Version `2023102800`, release `4.8.2 (2023102800)`, requires `2022111800`, maturity stable, author Andreas Grabs. Entry from the README: Site administration → Server → Moodle Adminer.

### plagiarism_azureai

| Field | Value |
|---|---|
| Component | `plagiarism_azureai` |
| Version | `2025101501` |
| Release | `1.0.0` |
| Requires | `2022112800` |
| Maturity | stable |
| Legacy cron | `$plugin->cron = 300` |
| Scheduled tasks | none (`db/tasks.php` explains adhoc processing) |

Adhoc task class: `plagiarism_azureai\task\process_queue`.

Events: assignment online-text and file submission created/updated; quiz attempt submitted. Callbacks are global functions on `plagiarism_azureai_observer` with `includefile`.

Tables named in code and install notes: `plagiarism_azureai_config`, `plagiarism_azureai_results`. The install XML was not copied into this document. Result statuses written by the task include `processing` and `failed`.

Config keys: `enabled`, `api_provider` (default `azure`), `api_endpoint`, `api_key`, `api_timeout` (default 60).

Language-string example endpoint shape: `https://your-app.azurewebsites.net/api/analyze`. Built-in defaults in `settings.php` when the endpoint is empty: GPTZero `https://api.gptzero.me/v2/detect`, check-plagiarism.com `https://www.check-plagiarism.com/apis/checkPlag`, Azure left as an empty string.

The task posts `submissiontext`. File submissions have a docx text extractor on the observer (`extract_text_from_docx`). Other binary formats were not fully traced.

## Database tables added by plugins

Moodle prefix in the local sample is `mdl_`. XMLDB names are unprefixed.

### local_gibbon_cohort (`db/install.xml`, version comment `2026021800`)

| Table | Unique key | Purpose |
|---|---|---|
| `local_gibbon_cohort_credits` | `cohortid` | theoretical_credits, clinical_credits, pass_mark (number 10,5), is_core, timemodified |
| `local_gibbon_cohort_yeargroup` | `idnumber` | name, minimum_theoretical_credits, minimum_clinical_credits |
| `local_gibbon_cohort_course_yeargroup` | `(courseid, yeargroup_idnumber)` | course to Gibbon year group |
| `local_gibbon_cohort_user_credits` | `(userid, courseid)` | awarded credits, is_core, `grade_grade_id` FK to `grade_grades` |

Foreign keys on the user-credits table reference `user`, `course`, and `grade_grades`. The other tables use indexes, not foreign keys, for `cohortid` and `courseid`.

`db/upgrade.php` exists for upgrades after install.

### local_gibbon_sync

`gibbon_sync_course_mapping`: `moodle_course_id` unique, `gibbon_course_id`, optional `gibbon_class_id`, `sync_enabled` default 1, timestamps. No PHP reference to the table name was found.

### Core tables the sync writes

`cohort` (`idnumber`, `component`), `cohort_members`, `enrol` (`enrol = cohort`, `customint1` = cohort id), `user_enrolments`, `groups` / group membership when create-groups is on, `course.groupmode` and `course.groupmodeforce`, `course_modules.groupmode`, `role_assignments`, `grade_grades.feedback` (web service write).

## Gibbon views expected

Moodle does not create these views. Names are defaults and can be changed in plugin settings.

| Default name | Columns the code and README expect |
|---|---|
| `moodleCohort` | `idnumber`, `name`, `description` |
| `moodleCohortMember` | `cohortid`, `username`, `role` |
| `moodleCohortCourse` | `cohortid`, `gibbonCourseID` |
| `moodleCohortCourseCredits` | `cohortid`, `theoretical_credits`, `clinical_credits`, `pass_mark`, `is_core` |
| `moodleYearGroupCredits` | `gibbonYearGroupID`, `name`, `minimum_theoretical_credits`, `minimum_clinical_credits` |
| `moodleCourse` | `gibbonCourseID`, `gibbonYearGroupIDList` (comma-separated) |

Silicon Ark `doh_sims` documents Moodle-facing views including `moodleUser`, `moodleCohort`, `moodleCohortMember`, `moodleCohortCourse`, `moodleCohortCourseCredits`, and `moodleYearGroupCredits`. It does not name `moodleCourse`. This plugin's default course-year-group source is `moodleCourse`. Whether that view exists on the Gibbon database is **Requires confirmation**.

`local_gibbon_cohort` does not read `moodleUser`. User rows are assumed to exist in Moodle.

## Authentication and secrets

- Moodle session for browser pages. My credits calls `require_login()`.
- Web services: Moodle token (`wstoken`) plus plugin setting `local_gibbon_sync/api_key`.
- Gibbon database: plugin settings `dbtype`, `dbhost`, `dbname`, `dbuser`, `dbpass`. Password field is `admin_setting_configpasswordunmask`.
- Plagiarism: `api_key` in plugin config.
- `configx.php` contains a database user and password for database `moodle_stable`. Not reproduced. The file is tracked. `.gitignore` does not ignore it. Commit `57ac79f2` removed `*Config` from `.gitignore`.
- Azure variable group `deploy-secrets-dev` is named as the holder of DB password secrets. The values are not in the YAML.
- Bitbucket `SONAR_TOKEN` is a repository variable reference, not a literal.

No custom auth plugin is in the tree. Cohort removal reads `auth_db` / `removeuser` and `is_enabled_auth('db')`.

Adminer is a database administration UI. Installing it exposes a site-admin path into the Moodle database. That is third-party behaviour documented by its README.

Root scripts such as `assign_admin_role.php` and `check_user_permissions.php` are web-reachable if the vhost serves the Moodle root as PHP. Their inputs were not audited line by line. Treat them as a deployment risk until they are removed or blocked.

## Background work

| Work | Kind | Schedule |
|---|---|---|
| `local_gibbon_cohort\task\sync_all` | scheduled | every 5 minutes |
| `plagiarism_azureai\task\process_queue` | adhoc | queued on submission events; cron must run |
| Moodle core cron | platform | not redefined here |

Cohort task logs with `error_log('local_gibbon_cohort: ...')` and `text_progress_trace`. There is no custom log table for sync statistics.

The cohort README's CLI examples still name `sync_cohorts`, `sync_cohort_members`, and `sync_cohort_enrolments`. Those classes have `execute()` and can be passed to `admin/cli/scheduled_task.php`, but they are not the registered schedule. The registered class is `\local_gibbon_cohort\task\sync_all`.

## Deployment

`azure-pipelines.yml` (header text still says `dob_doh_moodle` and `bootstrap-devops.sh`):

1. Trigger on `main`, `releases/*`, and pull requests to `main`.
2. Pool `selfhosted-dev`. Variable group `deploy-secrets-dev`.
3. Checkout depth 1.
4. `composer install --no-dev --optimize-autoloader --no-interaction --ignore-platform-req=php`.
5. If `vendor/bin/phpunit` exists, run it with `--testdox`. A comment says to add TestSprite. PHPUnit is in `require-dev`, and the install uses `--no-dev`, so the binary is absent after a clean install.
6. Tar the tree, excluding `.git`, `tests`, and `node_modules`, artifact name `moodle`.
7. Deploy stage `moodle-dev`, `runOnce`, depends on build success.
8. `CopyFilesOverSSH@0` to `/home/deploy/_incoming` using endpoint `ssh-moodle`, cleaning the target folder.
9. `SSH@0` runs `APP=moodle ARTIFACT=/home/deploy/_incoming/moodle.tar.gz KEEP=5 bash /home/deploy/release.sh`.

`release.sh` is not in the repository.

`bitbucket-pipelines.yml`: on branch `main` only, pipe `sonarsource/sonarqube-scan:3.0.2`, full clone, cache `~/.sonar`. Project key in `sonar-project.properties`:

```text
sonar.projectKey=overdrive_doh_moodle_a248e458-072c-4039-bb61-72ac3a59b665
```

No other Sonar properties (sources, exclusions) are set in that file.

## Development and test

- Upstream Moodle PHPUnit and Behat tooling is in the tree (`behat.yml.dist`, `composer.json` require-dev: PHPUnit `^9.6.18`, Behat `3.14.*`).
- The only custom PHPUnit test found under the Overdrive plugins is `local/course_startdate/tests/course_access_test.php`.
- `local/adminer/tests/behat/start_adminer.feature` is from the third-party plugin.
- No phpunit.xml override for the Overdrive plugins was searched beyond the core files. Standard Moodle test bootstrap applies.
- `gibbon_module/` includes SQL snippets and troubleshooting markdown (`FIX_FOREIGN_KEY_ERROR.md`, `TROUBLESHOOTING_STILL_ERROR.md`, and others). Those are install notes for the Gibbon copy, not Moodle tests.

## Configuration files Moodle expects at runtime

Moodle loads `config.php` from the root. That file is not in git. `config-dist.php` is the upstream template. `configx.php` is an extra file with local values:

| Key | Value observed (non-secret) |
|---|---|
| `dbtype` | `mariadb` |
| `dbname` | `moodle_stable` |
| `prefix` | `mdl_` |
| `wwwroot` | `http://localhost/moodle_stable` |
| `dataroot` | `C:\moodledata` |

A production wwwroot is **Unknown**.

Plugin settings that must be filled after install, or the related feature does nothing useful:

- `local_gibbon_cohort` database host, name, user, password.
- `local_gibbon_sync` API key (empty key rejects every call).
- Web services enabled, service enabled, token created (README steps). The service record is declared enabled; an upgraded site may still need the admin to enable web services globally.
- `enrol_database` new-course table, if step 4 should run.
- `auth_db` remove-user, if cohort membership should be deleted when Gibbon drops a person.
- Plagiarism endpoint and key, if checks should succeed. The Azure provider default does not fill an endpoint.

## Related Silicon Ark

`doh_sims` is the Gibbon fork. Its PRD and technical notes describe LMS Sync calling `local_gibbon_sync_get_students`, `local_gibbon_sync_get_grades`, and `local_gibbon_sync_get_courses`, and they describe the SQL views Moodle reads. Two disagreements to keep in mind:

1. `doh_sims` calls the API key optional. This code requires it.
2. This plugin reads a `moodleCourse` view by default. The `doh_sims` knowledge set does not name that view.

The `gibbon_module/MoodleSync` tree in this repository is a second client (settings, `MoodleAPI.php`, `SyncStudents.php`, `SyncGrades.php`, standalone script). It is documentation and source for a Gibbon install, not code Moodle executes.
