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 . +}} +{{! + @template mod_learningmap/modal_header_actions + + Renders header actions for learningmap modals. + + Context variables required for this template: + * headeractions - Array of pre-rendered header actions. + + Example context (json): + { + "headeractions": [ + "" + ] + } +}} +
+ {{#headeractions}} +
{{{.}}}
+ {{/headeractions}} +
diff --git a/tests/mod_learningmap_helper_test.php b/tests/mod_learningmap_helper_test.php index 31986b30..bb7108fc 100644 --- a/tests/mod_learningmap_helper_test.php +++ b/tests/mod_learningmap_helper_test.php @@ -26,6 +26,7 @@ */ #[\PHPUnit\Framework\Attributes\CoversClass(helper::class)] #[\PHPUnit\Framework\Attributes\CoversMethod(helper::class, 'repair_learningmap_record')] +#[\PHPUnit\Framework\Attributes\CoversMethod(helper::class, 'render_activity_header_for_modal')] final class mod_learningmap_helper_test extends \advanced_testcase { /** * Tests the repair_learningmap_record method. @@ -62,4 +63,42 @@ public function test_repair_learningmap_record(): void { 'The learning map record should not have changed after trying to repair it with a non-existing course.' ); } + + /** + * Tests that the manual completion button is present in rendered modal header HTML. + * + * There have been changes to the placement of the completion information/manual completion button + * across < Mooodle 5.2, Moodle 5.2 and Moodle > 5.2, so this test should make sure there's always + * a completion button for all supported moodle versions. + */ + public function test_render_activity_header_for_modal_manual_completion_button_presence(): void { + global $PAGE, $OUTPUT; + + $this->resetAfterTest(); + set_config('enablecompletion', 1); + + $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); + $user = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); + $this->setUser($user); + + $label = $this->getDataGenerator()->create_module('label', [ + 'course' => $course->id, + 'completion' => COMPLETION_TRACKING_MANUAL, + ]); + + $modinfo = get_fast_modinfo($course); + $cminfo = $modinfo->get_cm($label->cmid); + $context = \context_module::instance($cminfo->id); + + $PAGE->set_url(new \moodle_url('/mod/label/view.php', ['id' => $cminfo->id])); + $PAGE->set_context($context); + $PAGE->set_cm($cminfo, $course); + $PAGE->set_pagelayout('embedded'); + + $activityheaderdata = $PAGE->activityheader->export_for_template($OUTPUT); + $html = helper::render_activity_header_for_modal($activityheaderdata); + + $this->assertStringContainsString('data-action="toggle-manual-completion"', $html); + } } diff --git a/version.php b/version.php index fde53459..6f81eb6a 100644 --- a/version.php +++ b/version.php @@ -28,5 +28,5 @@ $plugin->release = '0.9.16'; $plugin->version = 2026021900; $plugin->requires = 2022112800; -$plugin->supported = [405, 501]; +$plugin->supported = [405, 502]; $plugin->maturity = MATURITY_STABLE;