This commit is contained in:
Markos Gogoulos
2026-02-20 22:38:52 +02:00
parent 86fa084391
commit b39789c2c4
20 changed files with 103 additions and 21 deletions

View File

@@ -574,12 +574,32 @@ class MediaBulkUserActions(APIView):
elif action == "add_to_category":
category_uids = request.data.get('category_uids', [])
if not category_uids:
return Response({"detail": "category_uids is required for add_to_category action"}, status=status.HTTP_400_BAD_REQUEST)
lti_context_id = request.data.get('lti_context_id')
if not category_uids and not lti_context_id:
return Response({"detail": "category_uids or lti_context_id is required for add_to_category action"}, status=status.HTTP_400_BAD_REQUEST)
categories = Category.objects.none()
# Prioritize category_uids
if category_uids:
# Filter categories by UID and ensure they are NOT RBAC categories
categories = Category.objects.filter(uid__in=category_uids, is_rbac_category=False)
elif lti_context_id:
# Filter categories by lti_context_id and ensure they ARE RBAC categories
potential_categories = Category.objects.filter(lti_context_id=lti_context_id, is_rbac_category=True)
# Check user access (must have contributor access)
valid_category_ids = []
for cat in potential_categories:
if request.user.has_contributor_access_to_category(cat):
valid_category_ids.append(cat.id)
if valid_category_ids:
categories = Category.objects.filter(id__in=valid_category_ids)
categories = Category.objects.filter(uid__in=category_uids)
if not categories:
return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST)
return Response({"detail": "No matching categories found or access denied"}, status=status.HTTP_400_BAD_REQUEST)
added_count = 0
for category in categories:

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { ApiUrlContext, LinksConsumer, MemberContext } from '../utils/contexts';
import { PageStore, ProfilePageStore } from '../utils/stores';
import { ProfilePageActions, PageActions } from '../utils/actions';
import { inEmbeddedApp, inSelectMediaEmbedMode, translateString } from '../utils/helpers/';
import { inEmbeddedApp, inSelectMediaEmbedMode, associateMediaWithLtiCategory, translateString } from '../utils/helpers/';
import { MediaListWrapper } from '../components/MediaListWrapper';
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
@@ -213,6 +213,9 @@ export class ProfileMediaPage extends Page {
newSelectedMedia.add(mediaId);
console.log('Selected media item:', mediaId);
// Associate media with the current LTI course category (fire-and-forget)
associateMediaWithLtiCategory(mediaId);
// Send postMessage to parent window (Moodle TinyMCE plugin)
if (window.parent !== window) {
// Construct the embed URL

View File

@@ -11,7 +11,7 @@ import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFi
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
import { BulkActionsModals } from '../components/BulkActionsModals';
import { inEmbeddedApp, inSelectMediaEmbedMode, translateString } from '../utils/helpers';
import { inEmbeddedApp, inSelectMediaEmbedMode, associateMediaWithLtiCategory, translateString } from '../utils/helpers';
import { withBulkActions } from '../utils/hoc/withBulkActions';
import { Page } from './_Page';
@@ -357,6 +357,9 @@ class ProfileSharedByMePage extends Page {
newSelectedMedia.add(mediaId);
console.log('Selected media item:', mediaId);
// Associate media with the current LTI course category (fire-and-forget)
associateMediaWithLtiCategory(mediaId);
// Send postMessage to parent window (Moodle TinyMCE plugin)
if (window.parent !== window) {
// Construct the embed URL

View File

@@ -10,7 +10,7 @@ import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListA
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
import { inEmbeddedApp, inSelectMediaEmbedMode, translateString } from '../utils/helpers';
import { inEmbeddedApp, inSelectMediaEmbedMode, associateMediaWithLtiCategory, translateString } from '../utils/helpers';
import { Page } from './_Page';
@@ -355,6 +355,9 @@ export class ProfileSharedWithMePage extends Page {
newSelectedMedia.add(mediaId);
console.log('Selected media item:', mediaId);
// Associate media with the current LTI course category (fire-and-forget)
associateMediaWithLtiCategory(mediaId);
// Send postMessage to parent window (Moodle TinyMCE plugin)
if (window.parent !== window) {
// Construct the embed URL

View File

@@ -33,3 +33,49 @@ export function isSelectMediaMode() {
export function inSelectMediaEmbedMode() {
return inEmbeddedApp() && isSelectMediaMode();
}
export function getLtiContextId(): string | null {
try {
const params = new URL(globalThis.location.href).searchParams;
const contextId = params.get('lti_context_id');
if (contextId) {
sessionStorage.setItem('lti_context_id', contextId);
return contextId;
}
return sessionStorage.getItem('lti_context_id');
} catch (e) {
return null;
}
}
export function associateMediaWithLtiCategory(mediaId: string): void {
const ltiContextId = getLtiContextId();
if (!ltiContextId || !mediaId) {
return;
}
const csrfMatch = document.cookie.match(/(?:^|;\s*)csrftoken=([^;]+)/);
const csrfToken = csrfMatch ? csrfMatch[1] : '';
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify({
action: 'add_to_category',
media_ids: [mediaId],
lti_context_id: ltiContextId,
}),
}).then(response => {
if (!response.ok) {
console.warn('[MediaCMS LTI] Failed to associate media with course category:', response.statusText);
}
}).catch(error => {
console.warn('[MediaCMS LTI] Failed to associate media with course category:', error);
});
}

View File

@@ -14,4 +14,4 @@ export * from './quickSort';
export * from './requests';
export { translateString } from './translate';
export { replaceString } from './replacementStrings';
export { inEmbeddedApp, inSelectMediaEmbedMode, isSelectMediaMode } from './embeddedApp';
export { inEmbeddedApp, inSelectMediaEmbedMode, isSelectMediaMode, associateMediaWithLtiCategory } from './embeddedApp';

View File

@@ -7,6 +7,7 @@ Allows instructors to select media from MediaCMS library and embed in Moodle cou
import time
import traceback
import uuid
from urllib.parse import quote
import jwt
from cryptography.hazmat.backends import default_backend
@@ -37,6 +38,12 @@ class SelectMediaView(View):
def get(self, request):
"""Display media selection interface - redirects to user's profile page"""
profile_url = f"/user/{request.user.username}?mode=lms_embed_mode&action=select_media"
lti_session = request.session.get('lti_session', {})
lti_context_id = lti_session.get('context_id', '')
if lti_context_id:
profile_url += f"&lti_context_id={quote(str(lti_context_id))}"
return HttpResponseRedirect(profile_url)
@method_decorator(csrf_exempt)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long