This commit is contained in:
Markos Gogoulos
2026-02-19 18:15:35 +02:00
parent 6071921908
commit 699f4bd09d
4 changed files with 177 additions and 318 deletions

View File

@@ -31,49 +31,18 @@ Installation
- Go to: Site Administration → Notifications - Go to: Site Administration → Notifications
- Click "Upgrade Moodle database now" - Click "Upgrade Moodle database now"
- Both plugins will be installed automatically - Both plugins will be installed automatically
- Set the MediaCMS tool under the LTI Tool
4. Make sure Filter is enabled 4. Make sure Filter is enabled
- As Administrator, visit Plugins, 'Manage Filters', find MediaCMS filter and enable it. - As Administrator, visit Plugins, 'Manage Filters', find MediaCMS filter and enable it.
Then place it at the top of the filter. This is important, otherwise embeds won't load. Then place it at the top of the filter. This is important, otherwise embeds won't load.
CONFIGURATION
-------------
1. CORE SETTINGS (Required) 5. Enter 'My Media' on top navigation.
Site Administration → Plugins → Filters → MediaCMS (Settings) - Log in as Administrator
- Go to: Site Administration → Appearance → Advanced Theme settings -> Custom menu items:
add: My Media|/filter/mediacms/my_media.php
- MediaCMS URL: https://lti.mediacms.io (your instance) What to expect
- LTI Tool: Select your MediaCMS tool (see LTI SETUP below)
✓ These settings are shared by both plugins!
2. ENABLE FILTER
Site Administration → Plugins → Filters → Manage filters
- Set "MediaCMS" to "On"
3. LTI SETUP (Required for Video Library)
Site Administration → Plugins → Activity modules → External tool →
Manage tools → "Configure a tool manually"
- Tool name: MediaCMS
- Tool URL: https://lti.mediacms.io/lti/login/
- LTI version: LTI 1.3
- Public key type: Keyset URL
- Public keyset: https://lti.mediacms.io/lti/jwks/
- Initiate login URL: https://lti.mediacms.io/lti/login/
- Redirection URI(s): https://lti.mediacms.io/lti/launch/
Services:
✓ IMS LTI Deep Linking (required for video library)
Save and copy the configuration URLs to provide to MediaCMS admin.
4. AUTO-CONVERT DEFAULTS (Optional)
Site Administration → Plugins → Text editors → TinyMCE → MediaCMS
Configure default display options for pasted URLs.
TESTING
------- -------
1. Create a test course 1. Create a test course
@@ -81,19 +50,6 @@ TESTING
3. Click MediaCMS button in TinyMCE editor 3. Click MediaCMS button in TinyMCE editor
4. Try inserting from video library or pasting a URL 4. Try inserting from video library or pasting a URL
TROUBLESHOOTING
---------------
Video library won't load:
- Check LTI tool is selected in filter settings
- Verify you're in a course context
- Check LTI tool configuration
URLs not auto-converting:
- Enable MediaCMS filter in Manage filters
- Verify MediaCMS URL setting matches your instance
- Clear caches: Site Administration → Development → Purge caches
SUPPORT SUPPORT
------- -------
Issues: https://github.com/mediacms-io/mediacms/issues Issues: https://github.com/mediacms-io/mediacms/issues

View File

@@ -82,6 +82,12 @@ try {
$cm = get_coursemodule_from_id('lti', $dummy_cmid, 0, false, MUST_EXIST); $cm = get_coursemodule_from_id('lti', $dummy_cmid, 0, false, MUST_EXIST);
$instance = $DB->get_record('lti', ['id' => $cm->instance], '*', MUST_EXIST); $instance = $DB->get_record('lti', ['id' => $cm->instance], '*', MUST_EXIST);
// DEBUG: log enrolled courses retrieved.
error_log('MediaCMS My Media publishdata courses (' . count($publish_data) . '): ' . json_encode($publish_data));
// Write publishdata to DB — Moodle's auth.php re-reads the instance from DB
// when building the LTI launch JWT, so in-memory changes are ignored.
$DB->set_field('lti', 'instructorcustomparameters', 'publishdata=' . $publishdata_b64, ['id' => $cm->instance]);
$instance->instructorcustomparameters = 'publishdata=' . $publishdata_b64; $instance->instructorcustomparameters = 'publishdata=' . $publishdata_b64;
$instance->name = 'MediaCMS My Media'; $instance->name = 'MediaCMS My Media';

View File

@@ -27,27 +27,117 @@ from .models import LTIResourceLink, LTIRoleMapping, LTIUserMapping
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DEFAULT_LTI_ROLE_MAPPINGS = { DEFAULT_LTI_ROLE_MAPPINGS = {
# LTI role names (used in standard launches)
'Instructor': {'global_role': '', 'group_role': 'manager'}, 'Instructor': {'global_role': '', 'group_role': 'manager'},
'TeachingAssistant': {'global_role': '', 'group_role': 'contributor'}, 'TeachingAssistant': {'global_role': '', 'group_role': 'contributor'},
'Learner': {'global_role': '', 'group_role': 'member'}, 'Learner': {'global_role': '', 'group_role': 'member'},
'Student': {'global_role': '', 'group_role': 'member'}, 'Student': {'global_role': '', 'group_role': 'member'},
'Administrator': {'global_role': '', 'group_role': 'manager'}, 'Administrator': {'global_role': '', 'group_role': 'manager'},
'Faculty': {'global_role': '', 'group_role': 'manager'}, 'Faculty': {'global_role': '', 'group_role': 'manager'},
# Moodle role shortnames (used in custom_publishdata from My Media launches)
'student': {'global_role': '', 'group_role': 'member'},
'guest': {'global_role': '', 'group_role': 'member'},
'teacher': {'global_role': '', 'group_role': 'manager'},
'editingteacher': {'global_role': '', 'group_role': 'manager'},
'manager': {'global_role': '', 'group_role': 'manager'},
'coursecreator': {'global_role': '', 'group_role': 'manager'},
'ta': {'global_role': '', 'group_role': 'contributor'},
} }
def _ensure_course_context(platform, context_id, title, label, resource_link_id):
"""
Find or create the LTIResourceLink, Category, and RBACGroup for a course.
When a record already exists (e.g. created by a bulk My Media launch), it is
reused and its metadata is kept in sync. The resource_link_id is only
promoted when a real launch ID arrives to replace a 'bulk_*' placeholder.
Returns:
Tuple of (category, rbac_group, resource_link)
"""
resource_link = LTIResourceLink.objects.filter(
platform=platform,
context_id=context_id,
).first()
if resource_link:
category = resource_link.category
rbac_group = resource_link.rbac_group
rl_updates = []
if title and resource_link.context_title != title:
resource_link.context_title = title
rl_updates.append('context_title')
if label and resource_link.context_label != label:
resource_link.context_label = label
rl_updates.append('context_label')
# Promote from bulk placeholder to real resource link ID.
if resource_link_id and not resource_link_id.startswith('bulk_') and resource_link.resource_link_id != resource_link_id:
resource_link.resource_link_id = resource_link_id
rl_updates.append('resource_link_id')
if rl_updates:
resource_link.save(update_fields=rl_updates)
if title and category and category.title != title:
category.title = title
category.save(update_fields=['title'])
else:
category = Category.objects.create(
title=title or label or f'Course {context_id}',
description=f'Auto-created from {platform.name}: {title}',
is_global=False,
is_rbac_category=True,
is_lms_course=True,
lti_platform=platform,
lti_context_id=context_id,
)
rbac_group = RBACGroup.objects.create(
name=f'{title or label} ({platform.name})',
description=f'LTI course group from {platform.name}',
)
rbac_group.categories.add(category)
resource_link = LTIResourceLink.objects.create(
platform=platform,
context_id=context_id,
resource_link_id=resource_link_id,
context_title=title,
context_label=label,
category=category,
rbac_group=rbac_group,
)
return category, rbac_group, resource_link
def _ensure_membership(user, rbac_group, group_role):
"""
Ensure the user is a member of rbac_group with at least group_role.
Upgrades the role if the user already has a lower one; never downgrades.
"""
existing = RBACMembership.objects.filter(user=user, rbac_group=rbac_group).first()
if existing:
final_role = get_higher_privilege_group(existing.role, group_role)
if final_role != existing.role:
existing.role = final_role
existing.save(update_fields=['role'])
else:
try:
RBACMembership.objects.create(user=user, rbac_group=rbac_group, role=group_role)
except Exception:
pass
# ── Public API ─────────────────────────────────────────────────────────────────
def provision_lti_user(platform, claims): def provision_lti_user(platform, claims):
""" """
Provision MediaCMS user from LTI launch claims Provision MediaCMS user from LTI launch claims.
Args:
platform: LTIPlatform instance
claims: Dict of LTI launch claims
Returns: Returns:
User instance User instance
Pattern: Similar to saml_auth.adapter.perform_user_actions()
""" """
lti_user_id = claims.get('sub') lti_user_id = claims.get('sub')
if not lti_user_id: if not lti_user_id:
@@ -67,15 +157,12 @@ def provision_lti_user(platform, claims):
if email and user.email != email: if email and user.email != email:
user.email = email user.email = email
update_fields.append('email') update_fields.append('email')
if given_name and user.first_name != given_name: if given_name and user.first_name != given_name:
user.first_name = given_name user.first_name = given_name
update_fields.append('first_name') update_fields.append('first_name')
if family_name and user.last_name != family_name: if family_name and user.last_name != family_name:
user.last_name = family_name user.last_name = family_name
update_fields.append('last_name') update_fields.append('last_name')
if name and user.name != name: if name and user.name != name:
user.name = name user.name = name
update_fields.append('name') update_fields.append('name')
@@ -85,11 +172,17 @@ def provision_lti_user(platform, claims):
else: else:
username = generate_username_from_lti(lti_user_id, email, given_name, family_name) username = generate_username_from_lti(lti_user_id, email, given_name, family_name)
if User.objects.filter(username=username).exists(): if User.objects.filter(username=username).exists():
username = f"{username}_{hashlib.md5(lti_user_id.encode()).hexdigest()[:6]}" username = f"{username}_{hashlib.md5(lti_user_id.encode()).hexdigest()[:6]}"
user = User.objects.create_user(username=username, email=email or '', first_name=given_name, last_name=family_name, name=name or username, is_active=True) user = User.objects.create_user(
username=username,
email=email or '',
first_name=given_name,
last_name=family_name,
name=name or username,
is_active=True,
)
if email: if email:
try: try:
@@ -103,13 +196,12 @@ def provision_lti_user(platform, claims):
def generate_username_from_lti(lti_user_id, email, given_name, family_name): def generate_username_from_lti(lti_user_id, email, given_name, family_name):
"""Generate a username from LTI user info""" """Generate a username from LTI user info."""
if email and '@' in email: if email and '@' in email:
username = email.split('@')[0] username = email.split('@')[0]
username = ''.join(c if c.isalnum() or c in '_-' else '_' for c in username) username = ''.join(c if c.isalnum() or c in '_-' else '_' for c in username)
if len(username) >= 4: if len(username) >= 4:
return username[:30] # Max 30 chars return username[:30]
if given_name and family_name: if given_name and family_name:
username = f"{given_name}.{family_name}".lower() username = f"{given_name}.{family_name}".lower()
@@ -117,120 +209,38 @@ def generate_username_from_lti(lti_user_id, email, given_name, family_name):
if len(username) >= 4: if len(username) >= 4:
return username[:30] return username[:30]
user_hash = hashlib.md5(lti_user_id.encode()).hexdigest()[:10] return f"lti_user_{hashlib.md5(lti_user_id.encode()).hexdigest()[:10]}"
return f"lti_user_{user_hash}"
def provision_lti_context(platform, claims, resource_link_id): def provision_lti_context(platform, claims, resource_link_id):
""" """
Provision MediaCMS category and RBAC group for LTI context (course) Provision MediaCMS category and RBAC group for an LTI context (course).
Args:
platform: LTIPlatform instance
claims: Dict of LTI launch claims
resource_link_id: Resource link ID
Returns: Returns:
Tuple of (category, rbac_group, resource_link) Tuple of (category, rbac_group, resource_link)
Pattern: Integrates with existing Category and RBACGroup models
""" """
context = claims.get('https://purl.imsglobal.org/spec/lti/claim/context', {}) context = claims.get('https://purl.imsglobal.org/spec/lti/claim/context', {})
context_id = context.get('id') context_id = context.get('id')
if not context_id: if not context_id:
raise ValueError("Missing context ID in LTI launch") raise ValueError("Missing context ID in LTI launch")
context_title = context.get('title', '') return _ensure_course_context(
context_label = context.get('label', '')
resource_link = LTIResourceLink.objects.filter(
platform=platform, platform=platform,
context_id=context_id, context_id=str(context_id),
).first() title=context.get('title', ''),
label=context.get('label', ''),
if resource_link: resource_link_id=resource_link_id,
category = resource_link.category )
rbac_group = resource_link.rbac_group
update_fields = []
if context_title and resource_link.context_title != context_title:
resource_link.context_title = context_title
update_fields.append('context_title')
if context_label and resource_link.context_label != context_label:
resource_link.context_label = context_label
update_fields.append('context_label')
# TODO / TOCHECK: consider whether we need to update this or not
if resource_link.resource_link_id != resource_link_id:
resource_link.resource_link_id = resource_link_id
update_fields.append('resource_link_id')
if update_fields:
resource_link.save(update_fields=update_fields)
if context_title and category and category.title != context_title:
category.title = context_title
category.save(update_fields=['title'])
else:
category = Category.objects.create(
title=context_title or context_label or f"Course {context_id}",
description=f"Auto-created from {platform.name}: {context_title}",
is_global=False,
is_rbac_category=True,
is_lms_course=True,
lti_platform=platform,
lti_context_id=context_id,
)
rbac_group = RBACGroup.objects.create(
name=f"{context_title or context_label} ({platform.name})",
description=f"LTI course group from {platform.name}",
)
rbac_group.categories.add(category)
resource_link = LTIResourceLink.objects.create(
platform=platform,
context_id=context_id,
resource_link_id=resource_link_id,
context_title=context_title,
context_label=context_label,
category=category,
rbac_group=rbac_group,
)
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): def provision_lti_bulk_contexts(platform, user, publish_data_raw):
""" """
Bulk-provision categories, groups, and memberships for every course the Bulk-provision categories, groups, and memberships for every course the
user is enrolled in, as reported by the LMS via the custom_publishdata user is enrolled in, as reported by the LMS via custom_publishdata.
parameter.
Called on My Media launches where there is no specific course context. Called on My Media launches. Skips the Moodle site course (ID 1).
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: try:
# Restore any stripped base64 padding before decoding.
padding = 4 - len(publish_data_raw) % 4 padding = 4 - len(publish_data_raw) % 4
if padding != 4: if padding != 4:
publish_data_raw += '=' * padding publish_data_raw += '=' * padding
@@ -246,80 +256,23 @@ def provision_lti_bulk_contexts(platform, user, publish_data_raw):
for course in courses: for course in courses:
try: try:
course_id = str(course.get('id', '')).strip() course_id = str(course.get('id', '')).strip()
if not course_id:
# Always skip the Moodle site course.
if not course_id or course_id == '1':
continue continue
fullname = course.get('fullname', '') fullname = course.get('fullname', '')
shortname = course.get('shortname', '') shortname = course.get('shortname', '')
group_role = _MOODLE_ROLE_TO_GROUP_ROLE.get(course.get('role', 'student'), 'member') group_role = DEFAULT_LTI_ROLE_MAPPINGS.get(course.get('role', 'student'), {}).get('group_role', 'member')
# ── Category & group ────────────────────────────────────────── _, rbac_group, _ = _ensure_course_context(
# 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, platform=platform,
context_id=course_id, context_id=course_id,
).first() title=fullname,
label=shortname,
resource_link_id=f'bulk_{course_id}',
)
if resource_link: if rbac_group:
category = resource_link.category _ensure_membership(user, rbac_group, group_role)
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: except Exception as exc:
logger.warning( logger.warning(
@@ -327,26 +280,17 @@ def provision_lti_bulk_contexts(platform, user, publish_data_raw):
course.get('id'), course.get('id'),
exc, 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 role URIs to MediaCMS global and group roles.
Args: Returns:
user: User instance Tuple of (global_role, group_role)
platform: LTIPlatform instance
lti_roles: List of LTI role URIs
rbac_group: RBACGroup instance for course
Pattern: Similar to saml_auth.adapter.handle_role_mapping()
""" """
if not lti_roles:
lti_roles = []
short_roles = [] short_roles = []
for role in lti_roles: for role in lti_roles or []:
if '#' in role: if '#' in role:
short_roles.append(role.split('#')[-1]) short_roles.append(role.split('#')[-1])
elif '/' in role: elif '/' in role:
@@ -354,89 +298,50 @@ def apply_lti_roles(user, platform, lti_roles, rbac_group):
else: else:
short_roles.append(role) short_roles.append(role)
custom_mappings = {} custom_mappings = {m.lti_role: {'global_role': m.global_role, 'group_role': m.group_role} for m in LTIRoleMapping.objects.filter(platform=platform)}
for mapping in LTIRoleMapping.objects.filter(platform=platform):
custom_mappings[mapping.lti_role] = {
'global_role': mapping.global_role,
'group_role': mapping.group_role,
}
all_mappings = {**DEFAULT_LTI_ROLE_MAPPINGS, **custom_mappings} all_mappings = {**DEFAULT_LTI_ROLE_MAPPINGS, **custom_mappings}
global_role = 'user' global_role = 'user'
for role in short_roles:
if role in all_mappings:
role_global = all_mappings[role].get('global_role')
if role_global:
global_role = get_higher_privilege_global(global_role, role_global)
user.set_role_from_mapping(global_role)
group_role = 'member' group_role = 'member'
for role in short_roles: for role in short_roles:
if role in all_mappings: if role in all_mappings:
role_group = all_mappings[role].get('group_role') if all_mappings[role].get('global_role'):
if role_group: global_role = get_higher_privilege_global(global_role, all_mappings[role]['global_role'])
group_role = get_higher_privilege_group(group_role, role_group) if all_mappings[role].get('group_role'):
group_role = get_higher_privilege_group(group_role, all_mappings[role]['group_role'])
memberships = RBACMembership.objects.filter(user=user, rbac_group=rbac_group) user.set_role_from_mapping(global_role)
_ensure_membership(user, rbac_group, group_role)
if memberships.exists():
if not memberships.filter(role=group_role).exists():
first_membership = memberships.first()
first_membership.role = group_role
try:
first_membership.save()
except Exception:
pass
else:
try:
RBACMembership.objects.create(user=user, rbac_group=rbac_group, role=group_role)
except Exception:
pass
return global_role, group_role return global_role, group_role
def get_higher_privilege_global(role1, role2): def get_higher_privilege_global(role1, role2):
"""Return the higher privilege global role""" """Return the higher privilege global role."""
privilege_order = ['user', 'advancedUser', 'editor', 'manager', 'admin'] privilege_order = ['user', 'advancedUser', 'editor', 'manager', 'admin']
try: try:
index1 = privilege_order.index(role1) return privilege_order[max(privilege_order.index(role1), privilege_order.index(role2))]
index2 = privilege_order.index(role2)
return privilege_order[max(index1, index2)]
except ValueError: except ValueError:
return role2 # Default to role2 if role1 is unknown return role2
def get_higher_privilege_group(role1, role2): def get_higher_privilege_group(role1, role2):
"""Return the higher privilege group role""" """Return the higher privilege group role."""
privilege_order = ['member', 'contributor', 'manager'] privilege_order = ['member', 'contributor', 'manager']
try: try:
index1 = privilege_order.index(role1) return privilege_order[max(privilege_order.index(role1), privilege_order.index(role2))]
index2 = privilege_order.index(role2)
return privilege_order[max(index1, index2)]
except ValueError: except ValueError:
return role2 # Default to role2 if role1 is unknown return role2
def create_lti_session(request, user, launch_data, platform): def create_lti_session(request, user, launch_data, platform):
""" """Create a MediaCMS session from an LTI launch."""
Create MediaCMS session from LTI launch
Args:
request: Django request
user: User instance
launch_data: Dict of validated LTI launch data
platform: LTIPlatform instance
Pattern: Uses Django's session framework
"""
login(request, user, backend='django.contrib.auth.backends.ModelBackend') login(request, user, backend='django.contrib.auth.backends.ModelBackend')
context = launch_data.get_launch_data().get('https://purl.imsglobal.org/spec/lti/claim/context', {}) ld = launch_data.get_launch_data()
resource_link = launch_data.get_launch_data().get('https://purl.imsglobal.org/spec/lti/claim/resource_link', {}) context = ld.get('https://purl.imsglobal.org/spec/lti/claim/context', {})
roles = launch_data.get_launch_data().get('https://purl.imsglobal.org/spec/lti/claim/roles', []) resource_link = ld.get('https://purl.imsglobal.org/spec/lti/claim/resource_link', {})
roles = ld.get('https://purl.imsglobal.org/spec/lti/claim/roles', [])
request.session['lti_session'] = { request.session['lti_session'] = {
'platform_id': platform.id, 'platform_id': platform.id,
@@ -448,10 +353,7 @@ def create_lti_session(request, user, launch_data, platform):
'launch_time': timezone.now().isoformat(), 'launch_time': timezone.now().isoformat(),
} }
timeout = getattr(settings, 'LTI_SESSION_TIMEOUT', 3600) request.session.set_expiry(getattr(settings, 'LTI_SESSION_TIMEOUT', 3600))
request.session.set_expiry(timeout)
# CRITICAL: Explicitly save session before redirect (for cross-site contexts)
request.session.modified = True request.session.modified = True
request.session.save() request.session.save()
@@ -460,18 +362,11 @@ def create_lti_session(request, user, launch_data, platform):
def validate_lti_session(request): def validate_lti_session(request):
""" """
Validate that an LTI session exists and is valid Validate that an LTI session exists and is valid.
Returns: Returns:
Dict of LTI session data or None Dict of LTI session data or None
""" """
lti_session = request.session.get('lti_session')
if not lti_session:
return None
if not request.user.is_authenticated: if not request.user.is_authenticated:
return None return None
return request.session.get('lti_session')
return lti_session

View File

@@ -331,23 +331,25 @@ class LaunchView(View):
context_claim = launch_data.get('https://purl.imsglobal.org/spec/lti/claim/context', {}) context_claim = launch_data.get('https://purl.imsglobal.org/spec/lti/claim/context', {})
context_id = context_claim.get('id', '') context_id = context_claim.get('id', '')
# Skip category/group creation for the Moodle site course (ID 1). # Detect My Media launches: publishdata is only sent on My Media launches.
# My Media launches use Course 1 as a dummy context; real provisioning publish_data_raw = custom_claims.get('publishdata') or custom_claims.get('custom_publishdata')
# 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: print(f"[MediaCMS LTI] context_id={context_id!r} publish_data_raw present={bool(publish_data_raw)}")
if publish_data_raw:
print(f"[MediaCMS LTI] publishdata (raw, first 200 chars): {publish_data_raw[:200]}")
if publish_data_raw:
# My Media launch: provision all enrolled courses from publishdata.
# Skip individual context provisioning to avoid double-provisioning.
resource_link_obj = None
provision_lti_bulk_contexts(platform, user, publish_data_raw)
elif context_claim:
# Normal course launch: provision only this context.
category, rbac_group, resource_link_obj = provision_lti_context(platform, launch_data, resource_link_id) 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')