diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e49cb316..641fbe90 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,24 +8,23 @@ jobs:
services:
postgres:
- image: postgres:16
+ image: ${{ matrix.database == 'pgsql' && 'postgres:17' || '' }}
env:
POSTGRES_USER: 'postgres'
POSTGRES_HOST_AUTH_METHOD: 'trust'
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3
+
mariadb:
- image: mariadb:10
+ image: ${{ matrix.database == 'mariadb' && 'mariadb:11' || '' }}
env:
- MYSQL_USER: 'root'
- MYSQL_ALLOW_EMPTY_PASSWORD: "true"
+ MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: '1'
MYSQL_CHARACTER_SET_SERVER: "utf8mb4"
MYSQL_COLLATION_SERVER: "utf8mb4_unicode_ci"
-
ports:
- 3306:3306
- options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3
+ options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval 10s --health-timeout 5s --health-retries 3
strategy:
fail-fast: false
@@ -37,6 +36,12 @@ jobs:
- php: '8.4'
moodle-branch: 'main'
database: 'pgsql'
+ - php: '8.4'
+ moodle-branch: 'MOODLE_502_STABLE'
+ database: 'mariadb'
+ - php: '8.4'
+ moodle-branch: 'MOODLE_502_STABLE'
+ database: 'pgsql'
- php: '8.4'
moodle-branch: 'MOODLE_501_STABLE'
database: 'mariadb'
diff --git a/classes/external/get_cm.php b/classes/external/get_cm.php
index 30ba28c0..084b3dba 100644
--- a/classes/external/get_cm.php
+++ b/classes/external/get_cm.php
@@ -20,7 +20,7 @@
use core_external\external_value;
use core_external\external_single_structure;
use core_external\external_function_parameters;
-use core_courseformat\output\local\content\cm\completion;
+use mod_learningmap\helper;
/**
* Class get_cm
*
@@ -91,10 +91,8 @@ public static function execute(int $cmid): array {
$PAGE->activityheader->set_description('');
}
- $data['completion'] = $OUTPUT->render_from_template(
- 'core/activity_header',
- $PAGE->activityheader->export_for_template($OUTPUT)
- );
+ $activityheaderdata = $PAGE->activityheader->export_for_template($OUTPUT);
+ $data['completion'] = helper::render_activity_header_for_modal($activityheaderdata);
$data['name'] = format_string($cm->name, true, ['context' => $context]);
diff --git a/classes/external/get_learningmap.php b/classes/external/get_learningmap.php
index 0d7b5874..68caf943 100644
--- a/classes/external/get_learningmap.php
+++ b/classes/external/get_learningmap.php
@@ -37,6 +37,7 @@
use external_value;
use invalid_parameter_exception;
use moodle_exception;
+use mod_learningmap\helper;
use required_capability_exception;
use restricted_context_exception;
@@ -104,10 +105,8 @@ public static function execute(int $cmid): array {
$PAGE->set_cm($cminfo);
$PAGE->set_pagelayout('embedded');
- $completion = $OUTPUT->render_from_template(
- 'core/activity_header',
- $PAGE->activityheader->export_for_template($OUTPUT)
- );
+ $activityheaderdata = $PAGE->activityheader->export_for_template($OUTPUT);
+ $completion = helper::render_activity_header_for_modal($activityheaderdata);
return [
'content' => learningmap_get_learningmap($cminfo),
diff --git a/classes/helper.php b/classes/helper.php
index e473b4c5..d8237010 100644
--- a/classes/helper.php
+++ b/classes/helper.php
@@ -24,6 +24,79 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
+ /**
+ * Get the current default activity header description without rendering the full header.
+ *
+ * Calling activity_header::export_for_template() here would add completion actions as a side effect.
+ *
+ * @return string
+ */
+ public static function get_activity_header_description(): string {
+ global $PAGE;
+
+ $layoutoptions = $PAGE->layout_options['activityheader'] ?? [];
+ if (
+ empty($layoutoptions['nodescription'])
+ && !empty($PAGE->activityrecord->intro)
+ && trim($PAGE->activityrecord->intro)
+ ) {
+ return format_module_intro($PAGE->activityname, $PAGE->activityrecord, $PAGE->cm->id);
+ }
+
+ return '';
+ }
+
+ /**
+ * Render the activity header for learningmap modals.
+ *
+ * Since Moodle 5.2 the manual completion UI is rendered via the activity header.
+ * With linear navigation (introduced in Moodle 5.3), the interactive toggle may be
+ * moved to the sticky footer. Modals have no sticky footer, so we inject a fallback
+ * toggle into the header actions when needed.
+ *
+ * @param array $activityheaderdata Exported activity header template data.
+ * @return string
+ */
+ public static function render_activity_header_for_modal(array $activityheaderdata): string {
+ global $PAGE, $OUTPUT;
+
+ $headerhtml = $OUTPUT->render_from_template('core/activity_header', $activityheaderdata);
+
+ $headeractions = $PAGE->get_header_actions();
+ if (self::should_add_manual_completion_fallback($activityheaderdata, $headeractions)) {
+ $headeractions[] = $OUTPUT->render_from_template('core_course/completion_manual', $activityheaderdata);
+ }
+
+ if (!empty($headeractions)) {
+ $headerhtml .= $OUTPUT->render_from_template('mod_learningmap/modal_header_actions', [
+ 'headeractions' => $headeractions,
+ ]);
+ }
+
+ return $headerhtml;
+ }
+
+ /**
+ * Check whether a manual completion fallback button should be rendered.
+ *
+ * @param array $activityheaderdata Exported activity header template data.
+ * @param array $headeractions Collected page header actions.
+ * @return bool
+ */
+ private static function should_add_manual_completion_fallback(array $activityheaderdata, array $headeractions): bool {
+ if (empty($activityheaderdata['showmanualcompletion']) || empty($activityheaderdata['istrackeduser'])) {
+ return false;
+ }
+
+ foreach ($headeractions as $headeraction) {
+ if (str_contains($headeraction, 'data-action="toggle-manual-completion"')) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
/**
* Returns whether the map should be shown on the course page.
*
diff --git a/classes/local/hook_callbacks.php b/classes/local/hook_callbacks.php
index fc19e720..54df7f58 100644
--- a/classes/local/hook_callbacks.php
+++ b/classes/local/hook_callbacks.php
@@ -29,6 +29,7 @@
use core\hook\output\before_http_headers;
use Exception;
use mod_learningmap\cachemanager;
+use mod_learningmap\helper;
/**
* Hook callbacks for mod_learningmap.
@@ -48,7 +49,7 @@ class hook_callbacks {
* @param before_http_headers $beforehttpheadershook the hook object
*/
public static function inject_backlinks_into_activity_header(before_http_headers $beforehttpheadershook): void {
- global $OUTPUT, $PAGE;
+ global $PAGE;
if (defined('LEARNINGMAP_NO_BACKLINK')) {
return;
@@ -99,8 +100,8 @@ public static function inject_backlinks_into_activity_header(before_http_headers
}
if ($backlinktext) {
- $activityheader = $PAGE->activityheader->export_for_template($OUTPUT);
- $PAGE->activityheader->set_description(($activityheader['description'] ?? '') . $backlinktext);
+ $description = helper::get_activity_header_description();
+ $PAGE->activityheader->set_description($description . $backlinktext);
}
} catch (Exception $e) {
debugging($e->getMessage());
diff --git a/lib.php b/lib.php
index 819d45b8..7cfec58c 100644
--- a/lib.php
+++ b/lib.php
@@ -440,8 +440,8 @@ function learningmap_before_http_headers() {
}
if ($backlinktext) {
- $activityheader = $PAGE->activityheader->export_for_template($OUTPUT);
- $PAGE->activityheader->set_description($activityheader['description'] . $backlinktext);
+ $description = helper::get_activity_header_description();
+ $PAGE->activityheader->set_description($description . $backlinktext);
}
} catch (Exception $e) {
debugging($e->getMessage());
diff --git a/templates/modal_header_actions.mustache b/templates/modal_header_actions.mustache
new file mode 100644
index 00000000..2bbcab1f
--- /dev/null
+++ b/templates/modal_header_actions.mustache
@@ -0,0 +1,36 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see