mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-03-18 19:01:56 -04:00
a
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Hook listener for filter_mediacms navigation hooks (Moodle 4.3+)
|
||||||
|
*
|
||||||
|
* @package filter_mediacms
|
||||||
|
* @copyright 2026 MediaCMS
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace filter_mediacms;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends the primary (top) navigation bar with a My Media link.
|
||||||
|
*/
|
||||||
|
class hook_listener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the \core\hook\navigation\primary_extend hook.
|
||||||
|
* Adds the My Media link to the primary nav bar when placement = 'top'.
|
||||||
|
*/
|
||||||
|
public static function extend_primary_navigation(
|
||||||
|
\core\hook\navigation\primary_extend $hook
|
||||||
|
): void {
|
||||||
|
$placement = get_config('filter_mediacms', 'mymedia_placement');
|
||||||
|
if ($placement !== 'top') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isloggedin() || isguestuser()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = new \moodle_url('/filter/mediacms/my_media.php');
|
||||||
|
$node = \navigation_node::create(
|
||||||
|
get_string('mymedia', 'filter_mediacms'),
|
||||||
|
$url,
|
||||||
|
\navigation_node::TYPE_CUSTOM,
|
||||||
|
null,
|
||||||
|
'mediacms_mymedia',
|
||||||
|
new \pix_icon('i/media', '')
|
||||||
|
);
|
||||||
|
|
||||||
|
$primarynav = $hook->get_primarynav();
|
||||||
|
if ($primarynav === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$primarynav->add_node($node);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
lms-plugins/mediacms-moodle/filter/mediacms/db/hooks.php
Normal file
13
lms-plugins/mediacms-moodle/filter/mediacms/db/hooks.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Hook registrations for filter_mediacms.
|
||||||
|
* Primary navigation is handled via extend_navigation() in lib.php instead.
|
||||||
|
*
|
||||||
|
* @package filter_mediacms
|
||||||
|
* @copyright 2026 MediaCMS
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
$callbacks = [];
|
||||||
@@ -17,3 +17,13 @@ $string['iframeheight_desc'] = 'Default height for embedded videos (pixels).';
|
|||||||
$string['enableautoconvert'] = 'Auto-convert URLs';
|
$string['enableautoconvert'] = 'Auto-convert URLs';
|
||||||
$string['enableautoconvert_desc'] = 'Automatically convert MediaCMS URLs (e.g., /view?m=xyz) in text to embedded players.';
|
$string['enableautoconvert_desc'] = 'Automatically convert MediaCMS URLs (e.g., /view?m=xyz) in text to embedded players.';
|
||||||
$string['privacy:metadata'] = 'The MediaCMS filter does not store any personal data.';
|
$string['privacy:metadata'] = 'The MediaCMS filter does not store any personal data.';
|
||||||
|
|
||||||
|
// My Media page.
|
||||||
|
$string['mymedia'] = 'My Media';
|
||||||
|
$string['mymedia_placement'] = 'My Media link placement';
|
||||||
|
$string['mymedia_placement_desc'] = 'Where to display the My Media link in the Moodle interface.';
|
||||||
|
$string['mymedia_placement_top'] = 'Top navigation';
|
||||||
|
$string['mymedia_placement_user'] = 'User navigation';
|
||||||
|
$string['notconfigured'] = 'MediaCMS is not fully configured. Please set the MediaCMS URL and LTI Tool in Site Administration → Plugins → Filters → MediaCMS.';
|
||||||
|
$string['ltitoolnotfound'] = 'The configured LTI tool could not be found. Please check the MediaCMS filter settings.';
|
||||||
|
$string['cannotcreatedummyactivity'] = 'Could not create the MediaCMS launcher activity. Please check course permissions.';
|
||||||
|
|||||||
@@ -11,65 +11,10 @@ require_once(__DIR__ . '/../../config.php');
|
|||||||
require_once($CFG->dirroot . '/mod/lti/lib.php');
|
require_once($CFG->dirroot . '/mod/lti/lib.php');
|
||||||
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||||
require_once($CFG->dirroot . '/course/modlib.php');
|
require_once($CFG->dirroot . '/course/modlib.php');
|
||||||
|
require_once(__DIR__ . '/locallib.php');
|
||||||
|
|
||||||
global $SITE, $DB, $PAGE, $OUTPUT, $CFG;
|
global $SITE, $DB, $PAGE, $OUTPUT, $CFG;
|
||||||
|
|
||||||
/**
|
|
||||||
* Find first LTI activity for the MediaCMS tool, or create dummy if none exists
|
|
||||||
*/
|
|
||||||
function filter_mediacms_get_dummy_activity($courseid, $typeid) {
|
|
||||||
global $DB;
|
|
||||||
|
|
||||||
// Find any existing LTI activity with this tool
|
|
||||||
$sql = "SELECT cm.id
|
|
||||||
FROM {course_modules} cm
|
|
||||||
JOIN {modules} m ON m.id = cm.module
|
|
||||||
JOIN {lti} lti ON lti.id = cm.instance
|
|
||||||
WHERE cm.course = :courseid
|
|
||||||
AND m.name = 'lti'
|
|
||||||
AND lti.typeid = :typeid
|
|
||||||
AND cm.deletioninprogress = 0
|
|
||||||
LIMIT 1";
|
|
||||||
|
|
||||||
$existing = $DB->get_record_sql($sql, ['courseid' => $courseid, 'typeid' => $typeid]);
|
|
||||||
if ($existing) {
|
|
||||||
// Ensure it's accessible (fix if created with visible=0)
|
|
||||||
$cm = get_coursemodule_from_id('lti', $existing->id, 0, false, IGNORE_MISSING);
|
|
||||||
if ($cm && !$cm->visible) {
|
|
||||||
set_coursemodule_visible($existing->id, 1);
|
|
||||||
}
|
|
||||||
return $existing->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No existing activity - create dummy in stealth mode (accessible but completely hidden)
|
|
||||||
$moduleinfo = new stdClass();
|
|
||||||
$moduleinfo->course = $courseid;
|
|
||||||
$moduleinfo->module = $DB->get_field('modules', 'id', ['name' => 'lti']);
|
|
||||||
$moduleinfo->modulename = 'lti';
|
|
||||||
$moduleinfo->section = 0;
|
|
||||||
$moduleinfo->visible = 1; // Accessible to all
|
|
||||||
$moduleinfo->visibleoncoursepage = 0; // Hidden from course page
|
|
||||||
$moduleinfo->availability = null; // No restrictions
|
|
||||||
$moduleinfo->showdescription = 0; // Don't show description
|
|
||||||
$moduleinfo->name = 'MediaCMS Filter Launcher';
|
|
||||||
$moduleinfo->intro = ''; // Empty intro
|
|
||||||
$moduleinfo->introformat = FORMAT_HTML;
|
|
||||||
$moduleinfo->typeid = $typeid;
|
|
||||||
$moduleinfo->instructorchoiceacceptgrades = 0;
|
|
||||||
$moduleinfo->grade = 0;
|
|
||||||
$moduleinfo->instructorchoicesendname = 1;
|
|
||||||
$moduleinfo->instructorchoicesendemailaddr = 1;
|
|
||||||
$moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
|
|
||||||
$moduleinfo->instructorcustomparameters = '';
|
|
||||||
|
|
||||||
$result = add_moduleinfo($moduleinfo, get_course($courseid));
|
|
||||||
|
|
||||||
// Additionally hide from activity navigation by setting it as orphaned
|
|
||||||
set_coursemodule_visible($result->coursemodule, 1, 0); // visible but not on course page
|
|
||||||
|
|
||||||
return $result->coursemodule;
|
|
||||||
}
|
|
||||||
|
|
||||||
require_login();
|
require_login();
|
||||||
|
|
||||||
$mediatoken = required_param('token', PARAM_ALPHANUMEXT);
|
$mediatoken = required_param('token', PARAM_ALPHANUMEXT);
|
||||||
|
|||||||
72
lms-plugins/mediacms-moodle/filter/mediacms/lib.php
Normal file
72
lms-plugins/mediacms-moodle/filter/mediacms/lib.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Navigation callbacks for filter_mediacms
|
||||||
|
*
|
||||||
|
* @package filter_mediacms
|
||||||
|
* @copyright 2026 MediaCMS
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add My Media to the global / flat navigation (nav drawer).
|
||||||
|
* Fires on every page when placement is set to 'top'.
|
||||||
|
* In Moodle 4.x Boost the nav drawer is opened via the hamburger icon
|
||||||
|
* and the node appears as a top-level item alongside Home / My courses.
|
||||||
|
*/
|
||||||
|
function filter_mediacms_extend_navigation(global_navigation $navigation): void {
|
||||||
|
$placement = get_config('filter_mediacms', 'mymedia_placement');
|
||||||
|
if ($placement !== 'top') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isloggedin() || isguestuser()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = new moodle_url('/filter/mediacms/my_media.php');
|
||||||
|
$node = navigation_node::create(
|
||||||
|
get_string('mymedia', 'filter_mediacms'),
|
||||||
|
$url,
|
||||||
|
navigation_node::TYPE_CUSTOM,
|
||||||
|
null,
|
||||||
|
'mediacms_mymedia',
|
||||||
|
new pix_icon('i/media', '')
|
||||||
|
);
|
||||||
|
// showinflatnavigation = true makes it visible in the Boost nav drawer.
|
||||||
|
$node->showinflatnavigation = true;
|
||||||
|
$navigation->add_node($node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add My Media to the user account / settings navigation.
|
||||||
|
* Fires when placement is set to 'user'.
|
||||||
|
* In Moodle 4.x Boost this section is reachable via avatar → Preferences.
|
||||||
|
*/
|
||||||
|
function filter_mediacms_extend_navigation_user_settings(
|
||||||
|
navigation_node $navigation,
|
||||||
|
stdClass $user,
|
||||||
|
context_user $usercontext,
|
||||||
|
stdClass $course,
|
||||||
|
context_course $coursecontext
|
||||||
|
): void {
|
||||||
|
$placement = get_config('filter_mediacms', 'mymedia_placement');
|
||||||
|
if ($placement !== 'user') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isloggedin() || isguestuser()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = new moodle_url('/filter/mediacms/my_media.php');
|
||||||
|
$navigation->add(
|
||||||
|
get_string('mymedia', 'filter_mediacms'),
|
||||||
|
$url,
|
||||||
|
navigation_node::TYPE_SETTING,
|
||||||
|
null,
|
||||||
|
'mediacms_mymedia',
|
||||||
|
new pix_icon('i/media', '')
|
||||||
|
);
|
||||||
|
}
|
||||||
67
lms-plugins/mediacms-moodle/filter/mediacms/locallib.php
Normal file
67
lms-plugins/mediacms-moodle/filter/mediacms/locallib.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Local helper functions for filter_mediacms
|
||||||
|
*
|
||||||
|
* @package filter_mediacms
|
||||||
|
* @copyright 2026 MediaCMS
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first LTI activity for the MediaCMS tool in a course, or create a
|
||||||
|
* hidden dummy one if none exists.
|
||||||
|
*
|
||||||
|
* @param int $courseid
|
||||||
|
* @param int $typeid LTI tool type ID
|
||||||
|
* @return int course-module ID
|
||||||
|
*/
|
||||||
|
function filter_mediacms_get_dummy_activity($courseid, $typeid) {
|
||||||
|
global $DB;
|
||||||
|
|
||||||
|
$sql = "SELECT cm.id
|
||||||
|
FROM {course_modules} cm
|
||||||
|
JOIN {modules} m ON m.id = cm.module
|
||||||
|
JOIN {lti} lti ON lti.id = cm.instance
|
||||||
|
WHERE cm.course = :courseid
|
||||||
|
AND m.name = 'lti'
|
||||||
|
AND lti.typeid = :typeid
|
||||||
|
AND cm.deletioninprogress = 0
|
||||||
|
LIMIT 1";
|
||||||
|
|
||||||
|
$existing = $DB->get_record_sql($sql, ['courseid' => $courseid, 'typeid' => $typeid]);
|
||||||
|
if ($existing) {
|
||||||
|
$cm = get_coursemodule_from_id('lti', $existing->id, 0, false, IGNORE_MISSING);
|
||||||
|
if ($cm && !$cm->visible) {
|
||||||
|
set_coursemodule_visible($existing->id, 1);
|
||||||
|
}
|
||||||
|
return $existing->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a stealth dummy activity (accessible but hidden from the course page).
|
||||||
|
$moduleinfo = new stdClass();
|
||||||
|
$moduleinfo->course = $courseid;
|
||||||
|
$moduleinfo->module = $DB->get_field('modules', 'id', ['name' => 'lti']);
|
||||||
|
$moduleinfo->modulename = 'lti';
|
||||||
|
$moduleinfo->section = 0;
|
||||||
|
$moduleinfo->visible = 1;
|
||||||
|
$moduleinfo->visibleoncoursepage = 0;
|
||||||
|
$moduleinfo->availability = null;
|
||||||
|
$moduleinfo->showdescription = 0;
|
||||||
|
$moduleinfo->name = 'MediaCMS Filter Launcher';
|
||||||
|
$moduleinfo->intro = '';
|
||||||
|
$moduleinfo->introformat = FORMAT_HTML;
|
||||||
|
$moduleinfo->typeid = $typeid;
|
||||||
|
$moduleinfo->instructorchoiceacceptgrades = 0;
|
||||||
|
$moduleinfo->grade = 0;
|
||||||
|
$moduleinfo->instructorchoicesendname = 1;
|
||||||
|
$moduleinfo->instructorchoicesendemailaddr = 1;
|
||||||
|
$moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
|
||||||
|
$moduleinfo->instructorcustomparameters = '';
|
||||||
|
|
||||||
|
$result = add_moduleinfo($moduleinfo, get_course($courseid));
|
||||||
|
set_coursemodule_visible($result->coursemodule, 1, 0);
|
||||||
|
|
||||||
|
return $result->coursemodule;
|
||||||
|
}
|
||||||
78
lms-plugins/mediacms-moodle/filter/mediacms/lti_launch.php
Normal file
78
lms-plugins/mediacms-moodle/filter/mediacms/lti_launch.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* My Media LTI launch page — runs inside the iframe from my_media.php.
|
||||||
|
*
|
||||||
|
* Builds custom_publishdata (enrolled courses + roles) and initiates
|
||||||
|
* the LTI 1.3 OIDC login flow, outputting the auto-submit form directly.
|
||||||
|
*
|
||||||
|
* @package filter_mediacms
|
||||||
|
* @copyright 2026 MediaCMS
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/../../config.php');
|
||||||
|
require_once($CFG->dirroot . '/mod/lti/lib.php');
|
||||||
|
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||||
|
require_once($CFG->dirroot . '/course/modlib.php');
|
||||||
|
require_once(__DIR__ . '/locallib.php');
|
||||||
|
|
||||||
|
global $SITE, $DB, $CFG, $USER;
|
||||||
|
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
|
||||||
|
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
|
||||||
|
|
||||||
|
if (empty($mediacmsurl) || empty($ltitoolid)) {
|
||||||
|
throw new moodle_exception('notconfigured', 'filter_mediacms');
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
|
||||||
|
if (!$type) {
|
||||||
|
throw new moodle_exception('ltitoolnotfound', 'filter_mediacms');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build custom_publishdata: all courses the user is enrolled in + role.
|
||||||
|
$enrolled_courses = enrol_get_users_courses($USER->id, true, ['id', 'shortname', 'fullname']);
|
||||||
|
|
||||||
|
$publish_data = [];
|
||||||
|
foreach ($enrolled_courses as $enrolled_course) {
|
||||||
|
if ((int)$enrolled_course->id === SITEID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$course_context = context_course::instance($enrolled_course->id);
|
||||||
|
$roles = get_user_roles($course_context, $USER->id, false);
|
||||||
|
|
||||||
|
$role_shortname = 'student';
|
||||||
|
if (!empty($roles)) {
|
||||||
|
$role = reset($roles);
|
||||||
|
$role_shortname = $role->shortname;
|
||||||
|
}
|
||||||
|
|
||||||
|
$publish_data[] = [
|
||||||
|
'id' => (int)$enrolled_course->id,
|
||||||
|
'shortname' => $enrolled_course->shortname,
|
||||||
|
'fullname' => $enrolled_course->fullname,
|
||||||
|
'role' => $role_shortname,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$publishdata_b64 = base64_encode(json_encode($publish_data));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dummy_cmid = filter_mediacms_get_dummy_activity(SITEID, $type->id);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
throw new moodle_exception('cannotcreatedummyactivity', 'filter_mediacms');
|
||||||
|
}
|
||||||
|
|
||||||
|
$cm = get_coursemodule_from_id('lti', $dummy_cmid, 0, false, MUST_EXIST);
|
||||||
|
$instance = $DB->get_record('lti', ['id' => $cm->instance], '*', MUST_EXIST);
|
||||||
|
|
||||||
|
$instance->instructorcustomparameters = 'publishdata=' . $publishdata_b64;
|
||||||
|
$instance->name = 'MediaCMS My Media';
|
||||||
|
|
||||||
|
$typeconfig = lti_get_type_type_config($type->id);
|
||||||
|
$content = lti_initiate_login($SITE->id, $dummy_cmid, $instance, $typeconfig, null, $instance->name);
|
||||||
|
|
||||||
|
echo $content;
|
||||||
38
lms-plugins/mediacms-moodle/filter/mediacms/my_media.php
Normal file
38
lms-plugins/mediacms-moodle/filter/mediacms/my_media.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* My Media page — renders the Moodle shell with an LTI iframe.
|
||||||
|
*
|
||||||
|
* @package filter_mediacms
|
||||||
|
* @copyright 2026 MediaCMS
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/../../config.php');
|
||||||
|
|
||||||
|
global $SITE, $PAGE, $OUTPUT, $USER;
|
||||||
|
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
$context = context_system::instance();
|
||||||
|
|
||||||
|
$PAGE->set_context($context);
|
||||||
|
$PAGE->set_url(new moodle_url('/filter/mediacms/my_media.php'));
|
||||||
|
$PAGE->set_course($SITE);
|
||||||
|
$PAGE->set_pagelayout('mydashboard');
|
||||||
|
$PAGE->set_title(get_string('mymedia', 'filter_mediacms'));
|
||||||
|
$PAGE->set_heading(get_string('mymedia', 'filter_mediacms'));
|
||||||
|
|
||||||
|
echo $OUTPUT->header();
|
||||||
|
|
||||||
|
$attr = [
|
||||||
|
'id' => 'contentframe',
|
||||||
|
'src' => (new moodle_url('/filter/mediacms/lti_launch.php'))->out(false),
|
||||||
|
'width' => '100%',
|
||||||
|
'height' => '600px',
|
||||||
|
'allowfullscreen' => 'true',
|
||||||
|
'allow' => 'autoplay *; fullscreen *; encrypted-media *; camera *; microphone *;',
|
||||||
|
'style' => 'border:none;display:block;',
|
||||||
|
];
|
||||||
|
echo html_writer::tag('iframe', '', $attr);
|
||||||
|
|
||||||
|
echo $OUTPUT->footer();
|
||||||
@@ -39,4 +39,16 @@ if ($ADMIN->fulltree) {
|
|||||||
0,
|
0,
|
||||||
$ltioptions
|
$ltioptions
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// My Media link placement.
|
||||||
|
$settings->add(new admin_setting_configselect(
|
||||||
|
'filter_mediacms/mymedia_placement',
|
||||||
|
get_string('mymedia_placement', 'filter_mediacms'),
|
||||||
|
get_string('mymedia_placement_desc', 'filter_mediacms'),
|
||||||
|
'top',
|
||||||
|
[
|
||||||
|
'top' => get_string('mymedia_placement_top', 'filter_mediacms'),
|
||||||
|
'user' => get_string('mymedia_placement_user', 'filter_mediacms'),
|
||||||
|
]
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
130
lti/handlers.py
130
lti/handlers.py
@@ -8,7 +8,9 @@ Provides functions to:
|
|||||||
- Create and manage LTI sessions
|
- Create and manage LTI sessions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
@@ -200,6 +202,134 @@ def provision_lti_context(platform, claims, resource_link_id):
|
|||||||
return category, rbac_group, resource_link
|
return category, rbac_group, resource_link
|
||||||
|
|
||||||
|
|
||||||
|
# Moodle role shortnames → MediaCMS group roles.
|
||||||
|
_MOODLE_ROLE_TO_GROUP_ROLE = {
|
||||||
|
'student': 'member',
|
||||||
|
'guest': 'member',
|
||||||
|
'teacher': 'manager',
|
||||||
|
'editingteacher': 'manager',
|
||||||
|
'manager': 'manager',
|
||||||
|
'coursecreator': 'manager',
|
||||||
|
'ta': 'contributor',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def provision_lti_bulk_contexts(platform, user, publish_data_raw):
|
||||||
|
"""
|
||||||
|
Bulk-provision categories, groups, and memberships for every course the
|
||||||
|
user is enrolled in, as reported by the LMS via the custom_publishdata
|
||||||
|
parameter.
|
||||||
|
|
||||||
|
Called on My Media launches where there is no specific course context.
|
||||||
|
Skips the Moodle site course (ID 1).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
platform: LTIPlatform instance
|
||||||
|
user: User instance
|
||||||
|
publish_data_raw: base64-encoded JSON string — list of dicts with
|
||||||
|
keys: id, shortname, fullname, role
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Restore any stripped base64 padding before decoding.
|
||||||
|
padding = 4 - len(publish_data_raw) % 4
|
||||||
|
if padding != 4:
|
||||||
|
publish_data_raw += '=' * padding
|
||||||
|
courses = json.loads(base64.b64decode(publish_data_raw).decode('utf-8'))
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning('provision_lti_bulk_contexts: failed to decode publishdata: %s', exc)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(courses, list):
|
||||||
|
logger.warning('provision_lti_bulk_contexts: publishdata is not a list')
|
||||||
|
return
|
||||||
|
|
||||||
|
for course in courses:
|
||||||
|
try:
|
||||||
|
course_id = str(course.get('id', '')).strip()
|
||||||
|
|
||||||
|
# Always skip the Moodle site course.
|
||||||
|
if not course_id or course_id == '1':
|
||||||
|
continue
|
||||||
|
|
||||||
|
fullname = course.get('fullname', '')
|
||||||
|
shortname = course.get('shortname', '')
|
||||||
|
group_role = _MOODLE_ROLE_TO_GROUP_ROLE.get(course.get('role', 'student'), 'member')
|
||||||
|
|
||||||
|
# ── Category & group ──────────────────────────────────────────
|
||||||
|
# Reuse the same .filter(...).first() pattern as provision_lti_context
|
||||||
|
# so that a real course-specific launch later finds the same record.
|
||||||
|
resource_link = LTIResourceLink.objects.filter(
|
||||||
|
platform=platform,
|
||||||
|
context_id=course_id,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if resource_link:
|
||||||
|
category = resource_link.category
|
||||||
|
rbac_group = resource_link.rbac_group
|
||||||
|
|
||||||
|
# Keep the category title in sync with the LMS.
|
||||||
|
if fullname and category and category.title != fullname:
|
||||||
|
category.title = fullname
|
||||||
|
category.save(update_fields=['title'])
|
||||||
|
else:
|
||||||
|
category = Category.objects.create(
|
||||||
|
title=fullname or shortname or f'Course {course_id}',
|
||||||
|
description=f'Auto-created from {platform.name}: {fullname}',
|
||||||
|
is_global=False,
|
||||||
|
is_rbac_category=True,
|
||||||
|
is_lms_course=True,
|
||||||
|
lti_platform=platform,
|
||||||
|
lti_context_id=course_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
rbac_group = RBACGroup.objects.create(
|
||||||
|
name=f'{fullname or shortname} ({platform.name})',
|
||||||
|
description=f'LTI course group from {platform.name}',
|
||||||
|
)
|
||||||
|
rbac_group.categories.add(category)
|
||||||
|
|
||||||
|
# Use a synthetic resource_link_id so provision_lti_context
|
||||||
|
# can find and reuse this record when the user later launches
|
||||||
|
# from an actual course page (it searches by platform+context_id
|
||||||
|
# without filtering on resource_link_id).
|
||||||
|
LTIResourceLink.objects.create(
|
||||||
|
platform=platform,
|
||||||
|
context_id=course_id,
|
||||||
|
resource_link_id=f'bulk_{course_id}',
|
||||||
|
context_title=fullname,
|
||||||
|
context_label=shortname,
|
||||||
|
category=category,
|
||||||
|
rbac_group=rbac_group,
|
||||||
|
)
|
||||||
|
|
||||||
|
if rbac_group is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── Membership ────────────────────────────────────────────────
|
||||||
|
existing = RBACMembership.objects.filter(user=user, rbac_group=rbac_group)
|
||||||
|
if existing.exists():
|
||||||
|
# Upgrade role if the LMS reports a higher privilege than stored.
|
||||||
|
current_role = existing.first().role
|
||||||
|
final_role = get_higher_privilege_group(current_role, group_role)
|
||||||
|
if final_role != current_role:
|
||||||
|
m = existing.first()
|
||||||
|
m.role = final_role
|
||||||
|
m.save(update_fields=['role'])
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
RBACMembership.objects.create(user=user, rbac_group=rbac_group, role=group_role)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
'provision_lti_bulk_contexts: error processing course %s: %s',
|
||||||
|
course.get('id'),
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
def apply_lti_roles(user, platform, lti_roles, rbac_group):
|
def apply_lti_roles(user, platform, lti_roles, rbac_group):
|
||||||
"""
|
"""
|
||||||
Apply role mappings from LTI to MediaCMS
|
Apply role mappings from LTI to MediaCMS
|
||||||
|
|||||||
18
lti/views.py
18
lti/views.py
@@ -36,6 +36,7 @@ from .adapters import DjangoRequest, DjangoSessionService, DjangoToolConfig
|
|||||||
from .handlers import (
|
from .handlers import (
|
||||||
apply_lti_roles,
|
apply_lti_roles,
|
||||||
create_lti_session,
|
create_lti_session,
|
||||||
|
provision_lti_bulk_contexts,
|
||||||
provision_lti_context,
|
provision_lti_context,
|
||||||
provision_lti_user,
|
provision_lti_user,
|
||||||
validate_lti_session,
|
validate_lti_session,
|
||||||
@@ -327,13 +328,26 @@ class LaunchView(View):
|
|||||||
# This ensures filter launches (which are deep linking) have authenticated user
|
# This ensures filter launches (which are deep linking) have authenticated user
|
||||||
user = provision_lti_user(platform, launch_data)
|
user = provision_lti_user(platform, launch_data)
|
||||||
|
|
||||||
if 'https://purl.imsglobal.org/spec/lti/claim/context' in launch_data:
|
context_claim = launch_data.get('https://purl.imsglobal.org/spec/lti/claim/context', {})
|
||||||
category, rbac_group, resource_link_obj = provision_lti_context(platform, launch_data, resource_link_id)
|
context_id = context_claim.get('id', '')
|
||||||
|
|
||||||
|
# Skip category/group creation for the Moodle site course (ID 1).
|
||||||
|
# My Media launches use Course 1 as a dummy context; real provisioning
|
||||||
|
# for those launches happens below via provision_lti_bulk_contexts.
|
||||||
|
is_site_context = str(context_id) == '1'
|
||||||
|
|
||||||
|
if context_claim and not is_site_context:
|
||||||
|
category, rbac_group, resource_link_obj = provision_lti_context(platform, launch_data, resource_link_id)
|
||||||
apply_lti_roles(user, platform, roles, rbac_group)
|
apply_lti_roles(user, platform, roles, rbac_group)
|
||||||
else:
|
else:
|
||||||
resource_link_obj = None
|
resource_link_obj = None
|
||||||
|
|
||||||
|
# Bulk-provision all enrolled courses when the LMS sends custom_publishdata
|
||||||
|
# (only present on My Media launches; transparent on normal course launches).
|
||||||
|
publish_data_raw = custom_claims.get('publishdata') or custom_claims.get('custom_publishdata')
|
||||||
|
if publish_data_raw:
|
||||||
|
provision_lti_bulk_contexts(platform, user, publish_data_raw)
|
||||||
|
|
||||||
create_lti_session(request, user, message_launch, platform)
|
create_lti_session(request, user, message_launch, platform)
|
||||||
|
|
||||||
message_type = launch_data.get('https://purl.imsglobal.org/spec/lti/claim/message_type')
|
message_type = launch_data.get('https://purl.imsglobal.org/spec/lti/claim/message_type')
|
||||||
|
|||||||
Reference in New Issue
Block a user