This commit is contained in:
Markos Gogoulos
2026-05-07 14:08:18 +03:00
parent 56db5f3907
commit 630eb77173
7 changed files with 65 additions and 33 deletions
+1 -1
View File
@@ -1 +1 @@
VERSION = "8.0.1e" VERSION = "8.0.1f"
+4
View File
@@ -67,6 +67,10 @@ urlpatterns = [
name="api_get_encoding", name="api_get_encoding",
), ),
re_path(r"^api/v1/search$", views.MediaSearch.as_view()), re_path(r"^api/v1/search$", views.MediaSearch.as_view()),
re_path(
rf"^api/v1/media/{friendly_token}/share$",
views.MediaShare.as_view(),
),
re_path( re_path(
rf"^api/v1/media/{friendly_token}/actions$", rf"^api/v1/media/{friendly_token}/actions$",
views.MediaActions.as_view(), views.MediaActions.as_view(),
+1
View File
@@ -9,6 +9,7 @@ from .media import MediaBulkUserActions # noqa: F401
from .media import MediaDetail # noqa: F401 from .media import MediaDetail # noqa: F401
from .media import MediaList # noqa: F401 from .media import MediaList # noqa: F401
from .media import MediaSearch # noqa: F401 from .media import MediaSearch # noqa: F401
from .media import MediaShare # noqa: F401
from .pages import about # noqa: F401 from .pages import about # noqa: F401
from .pages import add_subtitle # noqa: F401 from .pages import add_subtitle # noqa: F401
from .pages import approval_required # noqa: F401 from .pages import approval_required # noqa: F401
+28
View File
@@ -4,6 +4,8 @@ from django.conf import settings
from django.contrib.postgres.search import SearchQuery from django.contrib.postgres.search import SearchQuery
from django.db.models import Count, F, Prefetch, Q, prefetch_related_objects from django.db.models import Count, F, Prefetch, Q, prefetch_related_objects
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework import permissions, status from rest_framework import permissions, status
@@ -1233,3 +1235,29 @@ class MediaSearch(APIView):
page = paginator.paginate_queryset(media, request) page = paginator.paginate_queryset(media, request)
serializer = MediaSearchSerializer(page, many=True, context={"request": request}) serializer = MediaSearchSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data) return paginator.get_paginated_response(serializer.data)
@method_decorator(csrf_exempt, name='dispatch')
class MediaShare(APIView):
"""Mark a media item as shared when the owner embeds it via the LTI plugin."""
permission_classes = [permissions.IsAuthenticated]
def post(self, request, friendly_token):
media = get_object_or_404(Media, friendly_token=friendly_token)
if media.user != request.user:
return Response(status=status.HTTP_403_FORBIDDEN)
MediaPermission.objects.get_or_create(
media=media,
user=request.user,
defaults={'owner_user': request.user, 'permission': 'owner'},
)
courseid = request.data.get('courseid')
if courseid:
category = Category.objects.filter(lti_context_id=str(courseid), is_rbac_category=True).first()
if category:
EmbedMediaCourse.objects.get_or_create(media=media, category=category)
return Response(status=status.HTTP_200_OK)
@@ -269,21 +269,7 @@ class text_filter extends \core_filters\text_filter {
$view_url = new moodle_url('/filter/mediacms/my_media.php', $view_params); $view_url = new moodle_url('/filter/mediacms/my_media.php', $view_params);
$launch_url = new moodle_url('/filter/mediacms/launch.php', $view_params); return html_writer::tag('a', $text_matches[1], [
// Hidden iframe fires the LTI launch silently on every page load.
// When the media owner (teacher) loads the page, EmbedMediaLTIView's
// auto-share logic runs, marking the media as shared — same as for
// regular embedded iframes. Visits by non-owners are harmless.
$hidden_iframe = html_writer::tag('iframe', '', [
'src' => $launch_url->out(false),
'style' => 'display:none;width:0;height:0;border:0;',
'title' => '',
'tabindex' => '-1',
'aria-hidden' => 'true',
]);
return $hidden_iframe . html_writer::tag('a', $text_matches[1], [
'href' => $view_url->out(false), 'href' => $view_url->out(false),
'target' => '_blank', 'target' => '_blank',
'rel' => 'noopener noreferrer', 'rel' => 'noopener noreferrer',
@@ -7,7 +7,7 @@ import Selectors from './selectors';
import { getLti, getData } from './options'; import { getLti, getData } from './options';
const PREFS_KEY = 'tiny_mediacms_embed_prefs'; const PREFS_KEY = 'tiny_mediacms_embed_prefs';
const PREFS_FIELDS = ['showTitle', 'linkTitle', 'showUserAvatar', 'width', 'height']; const PREFS_FIELDS = ['showTitle', 'linkTitle', 'showUserAvatar', 'width', 'height', 'textLinkOnly'];
export default class IframeEmbed { export default class IframeEmbed {
editor = null; editor = null;
@@ -210,6 +210,32 @@ export default class IframeEmbed {
return url.toString(); return url.toString();
} }
signalShare(values) {
const parsed = this.parseInput(values.url);
if (!parsed || parsed.isGeneric || !parsed.videoId) {
return;
}
const editorData = getData(this.editor);
const baseUrl = parsed.isLtiLaunch
? (editorData?.mediacmsBaseUrl || '')
: parsed.baseUrl;
if (!baseUrl) {
return;
}
const ltiConfig = getLti(this.editor);
const courseId = ltiConfig?.courseId || 0;
fetch(`${baseUrl}/api/v1/media/${parsed.videoId}/share`, {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({courseid: courseId}),
}).catch(() => {});
}
savePrefs(values) { savePrefs(values) {
try { try {
const prefs = {}; const prefs = {};
@@ -251,7 +277,7 @@ export default class IframeEmbed {
showTitle: getDefault('showTitle'), showTitle: getDefault('showTitle'),
linkTitle: getDefault('linkTitle'), linkTitle: getDefault('linkTitle'),
showUserAvatar: getDefault('showUserAvatar'), showUserAvatar: getDefault('showUserAvatar'),
textLinkOnly: data.textLinkOnly || false, textLinkOnly: getDefault('textLinkOnly', false),
startAtEnabled: data.startAtEnabled || false, startAtEnabled: data.startAtEnabled || false,
startAt: data.startAt || '0:00', startAt: data.startAt || '0:00',
width, width,
@@ -531,6 +557,7 @@ export default class IframeEmbed {
} }
this.savePrefs(values); this.savePrefs(values);
this.signalShare(values);
const html = await this.generateIframeHtml(values); const html = await this.generateIframeHtml(values);
if (html) { if (html) {
if (this.isUpdating && this.selectedIframe) { if (this.isUpdating && this.selectedIframe) {
+1 -15
View File
@@ -30,7 +30,7 @@ from pylti1p3.exception import LtiException
from pylti1p3.message_launch import MessageLaunch from pylti1p3.message_launch import MessageLaunch
from pylti1p3.oidc_login import OIDCLogin from pylti1p3.oidc_login import OIDCLogin
from files.models import Category, EmbedMediaCourse, Media, MediaPermission from files.models import Media, MediaPermission
from rbac.models import RBACMembership from rbac.models import RBACMembership
from .adapters import DjangoRequest, DjangoSessionService, DjangoToolConfig from .adapters import DjangoRequest, DjangoSessionService, DjangoToolConfig
@@ -728,20 +728,6 @@ class EmbedMediaLTIView(View):
context_id = lti_session.get('context_id') context_id = lti_session.get('context_id')
platform_id = lti_session.get('platform_id') platform_id = lti_session.get('platform_id')
# Auto-share: when the media owner loads their own embed via LTI,
# mark it as shared and link it to the course. This fires on the
# teacher's first page view after saving (Moodle redirects there automatically).
if media.user == request.user:
MediaPermission.objects.get_or_create(
media=media,
user=request.user,
defaults={'owner_user': request.user, 'permission': 'owner'},
)
if context_id:
category = Category.objects.filter(lti_context_id=context_id, is_rbac_category=True).first()
if category:
EmbedMediaCourse.objects.get_or_create(media=media, category=category)
if media.is_shared and context_id and platform_id: if media.is_shared and context_id and platform_id:
try: try:
resource_link = ( resource_link = (