mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-06-08 18:02:36 -04:00
feat: LTI support and Moodle plugin
This commit is contained in:
+72
-19
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -7,6 +8,7 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.core.mail import EmailMessage
|
||||
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.utils.html import mark_safe, strip_tags
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from cms.version import VERSION
|
||||
@@ -24,7 +26,7 @@ from ..forms import (
|
||||
WhisperSubtitlesForm,
|
||||
)
|
||||
from ..frontend_translations import translate_string
|
||||
from ..helpers import get_alphanumeric_only
|
||||
from ..helpers import get_alphanumeric_and_spaces
|
||||
from ..methods import (
|
||||
can_transcribe_video,
|
||||
create_video_trim_request,
|
||||
@@ -243,6 +245,31 @@ def history(request):
|
||||
return render(request, "cms/history.html", context)
|
||||
|
||||
|
||||
_TIMESTAMP_RE = re.compile(r'^(?:(\d+):)?([0-5]?\d):([0-5]?\d)(?:\.(\d{1,3}))?$')
|
||||
|
||||
|
||||
def _timestamp_to_seconds(value):
|
||||
"""Parse 'HH:MM:SS.mmm', 'MM:SS.mmm', etc., or a numeric value, into float seconds.
|
||||
|
||||
Returns None if the value can't be parsed.
|
||||
"""
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
match = _TIMESTAMP_RE.match(value.strip())
|
||||
if not match:
|
||||
return None
|
||||
hours = int(match.group(1)) if match.group(1) else 0
|
||||
minutes = int(match.group(2))
|
||||
seconds = int(match.group(3))
|
||||
millis_str = match.group(4) or '0'
|
||||
millis = int(millis_str.ljust(3, '0'))
|
||||
return hours * 3600 + minutes * 60 + seconds + millis / 1000.0
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@login_required
|
||||
def video_chapters(request, friendly_token):
|
||||
@@ -262,20 +289,39 @@ def video_chapters(request, friendly_token):
|
||||
data = request_data.get("chapters")
|
||||
if data is None:
|
||||
return JsonResponse({'success': False, 'error': 'Request must contain "chapters" array'}, status=400)
|
||||
if not isinstance(data, list):
|
||||
return JsonResponse({'success': False, 'error': '"chapters" must be an array'}, status=400)
|
||||
if len(data) > 200:
|
||||
return JsonResponse({'success': False, 'error': 'Too many chapters (max 200)'}, status=400)
|
||||
|
||||
chapters = []
|
||||
for _, chapter_data in enumerate(data):
|
||||
start_time = chapter_data.get('startTime')
|
||||
end_time = chapter_data.get('endTime')
|
||||
for chapter_data in data:
|
||||
if not isinstance(chapter_data, dict):
|
||||
continue
|
||||
raw_start = chapter_data.get('startTime')
|
||||
raw_end = chapter_data.get('endTime')
|
||||
chapter_title = chapter_data.get('chapterTitle')
|
||||
if start_time and end_time and chapter_title:
|
||||
chapters.append(
|
||||
{
|
||||
'startTime': start_time,
|
||||
'endTime': end_time,
|
||||
'chapterTitle': chapter_title,
|
||||
}
|
||||
)
|
||||
|
||||
start_seconds = _timestamp_to_seconds(raw_start)
|
||||
end_seconds = _timestamp_to_seconds(raw_end)
|
||||
if start_seconds is None or end_seconds is None:
|
||||
continue
|
||||
if start_seconds < 0 or end_seconds < 0 or start_seconds >= end_seconds:
|
||||
continue
|
||||
|
||||
if not isinstance(chapter_title, str) or not chapter_title.strip():
|
||||
continue
|
||||
chapter_title = strip_tags(chapter_title).strip()[:500]
|
||||
if not chapter_title:
|
||||
continue
|
||||
|
||||
chapters.append(
|
||||
{
|
||||
'startTime': raw_start if isinstance(raw_start, str) else start_seconds,
|
||||
'endTime': raw_end if isinstance(raw_end, str) else end_seconds,
|
||||
'chapterTitle': chapter_title,
|
||||
}
|
||||
)
|
||||
except Exception as e: # noqa
|
||||
return JsonResponse({'success': False, 'error': 'Request data must be a list of video chapters with startTime, endTime, chapterTitle'}, status=400)
|
||||
|
||||
@@ -310,8 +356,8 @@ def edit_media(request):
|
||||
media.tags.remove(tag)
|
||||
if form.cleaned_data.get("new_tags"):
|
||||
for tag in form.cleaned_data.get("new_tags").split(","):
|
||||
tag = get_alphanumeric_only(tag)
|
||||
tag = tag[:99]
|
||||
tag = get_alphanumeric_and_spaces(tag)
|
||||
tag = tag[:100]
|
||||
if tag:
|
||||
try:
|
||||
tag = Tag.objects.get(title=tag)
|
||||
@@ -350,13 +396,13 @@ def publish_media(request):
|
||||
return HttpResponseRedirect(media.get_absolute_url())
|
||||
|
||||
if request.method == "POST":
|
||||
form = MediaPublishForm(request.user, request.POST, request.FILES, instance=media)
|
||||
form = MediaPublishForm(request.user, request.POST, request.FILES, instance=media, request=request)
|
||||
if form.is_valid():
|
||||
media = form.save()
|
||||
messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, "Media was edited"))
|
||||
return HttpResponseRedirect(media.get_absolute_url())
|
||||
else:
|
||||
form = MediaPublishForm(request.user, instance=media)
|
||||
form = MediaPublishForm(request.user, instance=media, request=request)
|
||||
|
||||
return render(
|
||||
request,
|
||||
@@ -449,11 +495,18 @@ def edit_chapters(request):
|
||||
if not (is_mediacms_editor(request.user) or request.user.has_contributor_access_to_media(media)):
|
||||
return HttpResponseRedirect("/")
|
||||
|
||||
chapters = media.chapter_data
|
||||
_html_escapes = str.maketrans({'<': r'\u003C', '>': r'\u003E', '&': r'\u0026'})
|
||||
chapters_json = mark_safe(json.dumps(media.chapter_data).translate(_html_escapes))
|
||||
return render(
|
||||
request,
|
||||
"cms/edit_chapters.html",
|
||||
{"media_object": media, "add_subtitle_url": media.add_subtitle_url, "media_file_path": helpers.url_from_path(media.media_file.path), "media_id": media.friendly_token, "chapters": chapters},
|
||||
{
|
||||
"media_object": media,
|
||||
"add_subtitle_url": media.add_subtitle_url,
|
||||
"media_file_path": helpers.url_from_path(media.media_file.path),
|
||||
"media_id": media.friendly_token,
|
||||
"chapters": chapters_json,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -547,7 +600,7 @@ def embed_media(request):
|
||||
media = Media.objects.values("title").filter(friendly_token=friendly_token).first()
|
||||
|
||||
if not media:
|
||||
return HttpResponseRedirect("/")
|
||||
return HttpResponse('This media no longer exists', status=404, content_type='text/plain; charset=utf-8')
|
||||
|
||||
context = {}
|
||||
context["media"] = friendly_token
|
||||
|
||||
Reference in New Issue
Block a user