Files
mediacms/lti/deep_linking.py
Markos Gogoulos a47bf5a3f8 wtv
2025-12-29 19:11:02 +02:00

197 lines
7.2 KiB
Python

"""
LTI Deep Linking 2.0 for MediaCMS
Allows instructors to select media from MediaCMS library and embed in Moodle courses
"""
import time
import traceback
import uuid
import jwt
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from jwcrypto import jwk
from files.models import Media
from files.views.media import MediaList
from .models import LTIPlatform, LTIToolKeys
@method_decorator(login_required, name='dispatch')
class SelectMediaView(View):
"""
UI for instructors to select media for deep linking
Flow: Instructor clicks "Add MediaCMS" in Moodle → Deep link launch →
This view → Instructor selects media → Return to Moodle
"""
def get(self, request):
"""Display media selection interface"""
# Get deep link session data
deep_link_data = request.session.get('lti_deep_link')
if not deep_link_data:
return JsonResponse({'error': 'No deep linking session data found'}, status=400)
# Reuse MediaList logic to get media with proper permissions
media_list_view = MediaList()
# Get base queryset with all permission/RBAC logic applied
media_queryset = media_list_view._get_media_queryset(request)
# Apply filtering based on query params
show_my_media_only = request.GET.get('my_media_only', 'false').lower() == 'true'
if show_my_media_only:
media_queryset = media_queryset.filter(user=request.user)
# Order by recent and limit for performance
media_list = media_queryset.order_by('-add_date')[:100]
context = {
'media_list': media_list,
'show_my_media_only': show_my_media_only,
'deep_link_data': deep_link_data,
}
return render(request, 'lti/select_media.html', context)
@method_decorator(csrf_exempt)
def post(self, request):
"""Return selected media as deep linking content items"""
# Get deep link session data
deep_link_data = request.session.get('lti_deep_link')
if not deep_link_data:
return JsonResponse({'error': 'Invalid session'}, status=400)
# Get selected media IDs
selected_ids = request.POST.getlist('media_ids[]')
if not selected_ids:
return JsonResponse({'error': 'No media selected'}, status=400)
# Build content items
content_items = []
for media_id in selected_ids:
try:
media = Media.objects.get(id=media_id)
# Build embed URL
embed_url = request.build_absolute_uri(reverse('lti:embed_media', args=[media.friendly_token]))
content_item = {
'type': 'ltiResourceLink',
'title': media.title,
'url': embed_url,
'custom': {
'media_friendly_token': media.friendly_token,
},
}
# Add thumbnail if available
if media.thumbnail_url:
content_item['thumbnail'] = {'url': media.thumbnail_url, 'width': 344, 'height': 194}
# Add iframe configuration
content_item['iframe'] = {'width': 960, 'height': 540}
content_items.append(content_item)
except Media.DoesNotExist:
continue
if not content_items:
return JsonResponse({'error': 'No valid media found'}, status=400)
# Create deep linking JWT response
# Note: This is a simplified version
# Full implementation would use PyLTI1p3's DeepLink response builder
jwt_response = self.create_deep_link_jwt(deep_link_data, content_items, request)
# Return auto-submit form that posts JWT back to Moodle
context = {
'return_url': deep_link_data['deep_link_return_url'],
'jwt': jwt_response,
}
return render(request, 'lti/deep_link_return.html', context)
def create_deep_link_jwt(self, deep_link_data, content_items, request):
"""
Create JWT response for deep linking - manual implementation
"""
try:
platform_id = deep_link_data['platform_id']
platform = LTIPlatform.objects.get(id=platform_id)
deployment_id = deep_link_data['deployment_id']
message_launch_data = deep_link_data['message_launch_data']
# Get deep linking settings from original launch data
deep_linking_settings = message_launch_data.get('https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings', {})
# Get tool's private key for signing
key_obj = LTIToolKeys.get_or_create_keys()
jwk_obj = jwk.JWK(**key_obj.private_key_jwk)
pem_bytes = jwk_obj.export_to_pem(private_key=True, password=None)
private_key = serialization.load_pem_private_key(pem_bytes, password=None, backend=default_backend())
# Build JWT payload according to LTI Deep Linking spec
now = int(time.time())
# Convert content_items to LTI content item format
lti_content_items = []
for item in content_items:
lti_item = {
'type': item['type'],
'title': item['title'],
'url': item['url'],
}
if item.get('custom'):
lti_item['custom'] = item['custom']
if item.get('thumbnail'):
lti_item['thumbnail'] = item['thumbnail']
if item.get('iframe'):
lti_item['iframe'] = item['iframe']
lti_content_items.append(lti_item)
# Create JWT payload
payload = {
'iss': request.build_absolute_uri('/')[:-1], # Tool's issuer (MediaCMS URL)
'aud': [platform.client_id],
'exp': now + 3600,
'iat': now,
'nonce': str(uuid.uuid4()),
'https://purl.imsglobal.org/spec/lti/claim/message_type': 'LtiDeepLinkingResponse',
'https://purl.imsglobal.org/spec/lti/claim/version': '1.3.0',
'https://purl.imsglobal.org/spec/lti/claim/deployment_id': deployment_id,
'https://purl.imsglobal.org/spec/lti-dl/claim/content_items': lti_content_items,
'https://purl.imsglobal.org/spec/lti-dl/claim/data': deep_linking_settings.get('data', ''),
}
# Sign JWT with tool's private key
kid = key_obj.private_key_jwk['kid']
response_jwt = jwt.encode(payload, private_key, algorithm='RS256', headers={'kid': kid})
return response_jwt
except Exception as e:
# Log error for debugging
traceback.print_exc()
raise ValueError(f"Failed to create Deep Linking JWT: {str(e)}")