mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-03-11 07:27:22 -04:00
a
This commit is contained in:
130
lti/handlers.py
130
lti/handlers.py
@@ -8,7 +8,9 @@ Provides functions to:
|
||||
- Create and manage LTI sessions
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
|
||||
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
|
||||
|
||||
|
||||
# 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):
|
||||
"""
|
||||
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 (
|
||||
apply_lti_roles,
|
||||
create_lti_session,
|
||||
provision_lti_bulk_contexts,
|
||||
provision_lti_context,
|
||||
provision_lti_user,
|
||||
validate_lti_session,
|
||||
@@ -327,13 +328,26 @@ class LaunchView(View):
|
||||
# This ensures filter launches (which are deep linking) have authenticated user
|
||||
user = provision_lti_user(platform, launch_data)
|
||||
|
||||
if 'https://purl.imsglobal.org/spec/lti/claim/context' in launch_data:
|
||||
category, rbac_group, resource_link_obj = provision_lti_context(platform, launch_data, resource_link_id)
|
||||
context_claim = launch_data.get('https://purl.imsglobal.org/spec/lti/claim/context', {})
|
||||
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)
|
||||
else:
|
||||
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)
|
||||
|
||||
message_type = launch_data.get('https://purl.imsglobal.org/spec/lti/claim/message_type')
|
||||
|
||||
Reference in New Issue
Block a user