This commit is contained in:
Markos Gogoulos
2026-02-24 10:54:03 +02:00
parent b39789c2c4
commit dc328cd33c
17 changed files with 52 additions and 33 deletions

View File

@@ -1 +1 @@
VERSION = "7.9e" VERSION = "7.9f"

View File

@@ -176,6 +176,9 @@ class MediaPublishForm(forms.ModelForm):
if is_embed_mode: if is_embed_mode:
current_queryset = self.fields['category'].queryset current_queryset = self.fields['category'].queryset
self.fields['category'].queryset = current_queryset.filter(is_lms_course=True) self.fields['category'].queryset = current_queryset.filter(is_lms_course=True)
self.fields['category'].label = 'Course'
self.fields['category'].help_text = 'Media can be part of one or more courses'
self.fields['category'].widget.is_lms_mode = True
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_tag = True self.helper.form_tag = True

View File

@@ -14,6 +14,8 @@ class CategoryModalWidget(forms.SelectMultiple):
js = ('js/category_modal.js',) js = ('js/category_modal.js',)
def render(self, name, value, attrs=None, renderer=None): def render(self, name, value, attrs=None, renderer=None):
is_lms_mode = getattr(self, 'is_lms_mode', False)
# Get all categories as JSON # Get all categories as JSON
categories = [] categories = []
for opt_value, opt_label in self.choices: for opt_value, opt_label in self.choices:
@@ -30,20 +32,24 @@ class CategoryModalWidget(forms.SelectMultiple):
all_categories_json = json.dumps(categories) all_categories_json = json.dumps(categories)
selected_ids_json = json.dumps([str(v) for v in (value or [])]) selected_ids_json = json.dumps([str(v) for v in (value or [])])
lms_mode_json = json.dumps(is_lms_mode)
search_placeholder = "Search courses..." if is_lms_mode else "Search categories..."
selected_header = "Selected Courses" if is_lms_mode else "Selected Categories"
html = f'''<div class="category-widget" data-name="{name}"> html = f'''<div class="category-widget" data-name="{name}">
<div class="category-content"> <div class="category-content">
<div class="category-panel"> <div class="category-panel">
<input type="text" class="category-search" placeholder="Search categories..."> <input type="text" class="category-search" placeholder="{search_placeholder}">
<div class="category-list scrollable" data-panel="left"></div> <div class="category-list scrollable" data-panel="left"></div>
</div> </div>
<div class="category-panel"> <div class="category-panel">
<h3>Selected Categories</h3> <h3>{selected_header}</h3>
<div class="category-list scrollable" data-panel="right"></div> <div class="category-list scrollable" data-panel="right"></div>
</div> </div>
</div> </div>
<div class="hidden-inputs"></div> <div class="hidden-inputs"></div>
<script type="application/json" class="category-data">{{"all":{all_categories_json},"selected":{selected_ids_json}}}</script> <script type="application/json" class="category-data">{{"all":{all_categories_json},"selected":{selected_ids_json},"lms_mode":{lms_mode_json}}}</script>
</div>''' </div>'''
return mark_safe(html) return mark_safe(html)

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import './BulkActionCategoryModal.scss'; import './BulkActionCategoryModal.scss';
import { translateString } from '../utils/helpers/'; import { translateString } from '../utils/helpers/';
import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
interface Category { interface Category {
title: string; title: string;
@@ -24,6 +25,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
onError, onError,
csrfToken, csrfToken,
}) => { }) => {
const isLmsMode = inEmbeddedApp();
const [existingCategories, setExistingCategories] = useState<Category[]>([]); const [existingCategories, setExistingCategories] = useState<Category[]>([]);
const [allCategories, setAllCategories] = useState<Category[]>([]); const [allCategories, setAllCategories] = useState<Category[]>([]);
const [categoriesToAdd, setCategoriesToAdd] = useState<Category[]>([]); const [categoriesToAdd, setCategoriesToAdd] = useState<Category[]>([]);
@@ -66,20 +68,27 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
const existingData = await existingResponse.json(); const existingData = await existingResponse.json();
const existing = existingData.results || []; const existing = existingData.results || [];
// Fetch all categories // Fetch all categories (or LMS courses only in embed mode)
const allResponse = await fetch('/api/v1/categories'); const categoriesUrl = isLmsMode
? '/api/v1/categories/contributor?lms_courses_only=true'
: '/api/v1/categories';
const allResponse = await fetch(categoriesUrl);
if (!allResponse.ok) { if (!allResponse.ok) {
throw new Error(translateString('Failed to fetch all categories')); throw new Error(isLmsMode ? translateString('Failed to fetch courses') : translateString('Failed to fetch all categories'));
} }
const allData = await allResponse.json(); const allData = await allResponse.json();
const all = allData.results || allData; const all = allData.results || allData;
setExistingCategories(existing); // In LMS mode, filter existing to only show LMS course categories
const allUids = new Set(all.map((c: Category) => c.uid));
const filteredExisting = isLmsMode ? existing.filter((c: Category) => allUids.has(c.uid)) : existing;
setExistingCategories(filteredExisting);
setAllCategories(all); setAllCategories(all);
} catch (error) { } catch (error) {
console.error('Error fetching categories:', error); console.error('Error fetching categories:', error);
onError(translateString('Failed to load categories')); onError(isLmsMode ? translateString('Failed to load courses') : translateString('Failed to load categories'));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -126,7 +135,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
}); });
if (!addResponse.ok) { if (!addResponse.ok) {
throw new Error(translateString('Failed to add categories')); throw new Error(isLmsMode ? translateString('Failed to add courses') : translateString('Failed to add categories'));
} }
} }
@@ -147,15 +156,15 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
}); });
if (!removeResponse.ok) { if (!removeResponse.ok) {
throw new Error(translateString('Failed to remove categories')); throw new Error(isLmsMode ? translateString('Failed to remove courses') : translateString('Failed to remove categories'));
} }
} }
onSuccess(translateString('Successfully updated categories')); onSuccess(isLmsMode ? translateString('Successfully updated courses') : translateString('Successfully updated categories'));
onCancel(); onCancel();
} catch (error) { } catch (error) {
console.error('Error processing categories:', error); console.error('Error processing categories:', error);
onError(translateString('Failed to update categories. Please try again.')); onError(isLmsMode ? translateString('Failed to update courses. Please try again.') : translateString('Failed to update categories. Please try again.'));
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
} }
@@ -184,7 +193,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
<div className="category-modal-overlay"> <div className="category-modal-overlay">
<div className="category-modal"> <div className="category-modal">
<div className="category-modal-header"> <div className="category-modal-header">
<h2>{translateString('Add / Remove from Categories')}</h2> <h2>{isLmsMode ? translateString('Share with Course') : translateString('Add / Remove from Categories')}</h2>
<button className="category-modal-close" onClick={onCancel}> <button className="category-modal-close" onClick={onCancel}>
× ×
</button> </button>
@@ -192,14 +201,14 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
<div className="category-modal-content"> <div className="category-modal-content">
<div className="category-panel"> <div className="category-panel">
<h3>{translateString('Categories')}</h3> <h3>{isLmsMode ? translateString('Courses') : translateString('Categories')}</h3>
{isLoading ? ( {isLoading ? (
<div className="loading-message">{translateString('Loading categories...')}</div> <div className="loading-message">{isLmsMode ? translateString('Loading courses...') : translateString('Loading categories...')}</div>
) : ( ) : (
<div className="category-list scrollable"> <div className="category-list scrollable">
{leftPanelCategories.length === 0 ? ( {leftPanelCategories.length === 0 ? (
<div className="empty-message">{translateString('All categories already added')}</div> <div className="empty-message">{isLmsMode ? translateString('All courses already added') : translateString('All categories already added')}</div>
) : ( ) : (
leftPanelCategories.map((category) => ( leftPanelCategories.map((category) => (
<div <div
@@ -227,11 +236,11 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
</h3> </h3>
{isLoading ? ( {isLoading ? (
<div className="loading-message">{translateString('Loading categories...')}</div> <div className="loading-message">{isLmsMode ? translateString('Loading courses...') : translateString('Loading categories...')}</div>
) : ( ) : (
<div className="category-list scrollable"> <div className="category-list scrollable">
{rightPanelCategories.length === 0 ? ( {rightPanelCategories.length === 0 ? (
<div className="empty-message">{translateString('No categories')}</div> <div className="empty-message">{isLmsMode ? translateString('No courses') : translateString('No categories')}</div>
) : ( ) : (
rightPanelCategories.map((category) => { rightPanelCategories.map((category) => {
const isExisting = existingCategories.some((c) => c.uid === category.uid); const isExisting = existingCategories.some((c) => c.uid === category.uid);
@@ -251,7 +260,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
removeCategoryFromAddList(category); removeCategoryFromAddList(category);
} }
}} }}
title={isMarkedForRemoval ? translateString('Undo removal') : isExisting ? translateString('Remove category') : translateString('Remove from list')} title={isMarkedForRemoval ? translateString('Undo removal') : isExisting ? (isLmsMode ? translateString('Remove course') : translateString('Remove category')) : translateString('Remove from list')}
> >
{isMarkedForRemoval ? '↺' : '×'} {isMarkedForRemoval ? '↺' : '×'}
</button> </button>

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import './BulkActionsDropdown.scss'; import './BulkActionsDropdown.scss';
import { translateString } from '../utils/helpers/'; import { translateString } from '../utils/helpers/';
import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
interface BulkActionsDropdownProps { interface BulkActionsDropdownProps {
selectedCount: number; selectedCount: number;
@@ -12,7 +13,7 @@ const BULK_ACTIONS = [
{ value: 'add-remove-coeditors', label: translateString('Add / Remove Co-Editors'), enabled: true }, { value: 'add-remove-coeditors', label: translateString('Add / Remove Co-Editors'), enabled: true },
{ value: 'add-remove-coowners', label: translateString('Add / Remove Co-Owners'), enabled: true }, { value: 'add-remove-coowners', label: translateString('Add / Remove Co-Owners'), enabled: true },
{ value: 'add-remove-playlist', label: translateString('Add to / Remove from Playlist'), enabled: true }, { value: 'add-remove-playlist', label: translateString('Add to / Remove from Playlist'), enabled: true },
{ value: 'add-remove-category', label: translateString('Add to / Remove from Category'), enabled: true }, { value: 'add-remove-category', label: inEmbeddedApp() ? translateString('Share with Course') : translateString('Add to / Remove from Category'), enabled: true },
{ value: 'add-remove-tags', label: translateString('Add / Remove Tags'), enabled: true }, { value: 'add-remove-tags', label: translateString('Add / Remove Tags'), enabled: true },
{ value: 'enable-comments', label: translateString('Enable Comments'), enabled: true }, { value: 'enable-comments', label: translateString('Enable Comments'), enabled: true },
{ value: 'disable-comments', label: translateString('Disable Comments'), enabled: true }, { value: 'disable-comments', label: translateString('Disable Comments'), enabled: true },

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('.category-widget').forEach(w=>{const d=JSON.parse(w.querySelector('.category-data').textContent),all=d.all,sel=new Set(d.selected),name=w.dataset.name,srch=w.querySelector('.category-search'),left=w.querySelector('[data-panel="left"]'),right=w.querySelector('[data-panel="right"]'),inputs=w.querySelector('.hidden-inputs');const upd=()=>{left.innerHTML=all.filter(c=>!sel.has(c.id)&&(!srch.value||c.title.toLowerCase().includes(srch.value.toLowerCase()))).map(c=>`<div class="category-item" data-id="${c.id}"><span>${c.title}</span><button class="add-btn" type="button">+</button></div>`).join('')||'<div class="empty-message">No categories available</div>';right.innerHTML=[...sel].map(id=>all.find(c=>c.id==id)).filter(Boolean).map(c=>`<div class="category-item" data-id="${c.id}"><span>${c.title}</span><button class="remove-btn" type="button">&times;</button></div>`).join('')||'<div class="empty-message">No categories selected</div>';inputs.innerHTML=[...sel].map(id=>`<input type="hidden" name="${name}" value="${id}">`).join('')};srch.oninput=upd;left.onclick=e=>{const item=e.target.closest('.category-item');if(item){sel.add(item.dataset.id);upd()}};right.onclick=e=>{const item=e.target.closest('.category-item');if(item){sel.delete(item.dataset.id);upd()}};upd()})}); document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('.category-widget').forEach(w=>{const d=JSON.parse(w.querySelector('.category-data').textContent),all=d.all,lms=d.lms_mode,sel=new Set(d.selected),name=w.dataset.name,srch=w.querySelector('.category-search'),left=w.querySelector('[data-panel="left"]'),right=w.querySelector('[data-panel="right"]'),inputs=w.querySelector('.hidden-inputs');const upd=()=>{left.innerHTML=all.filter(c=>!sel.has(c.id)&&(!srch.value||c.title.toLowerCase().includes(srch.value.toLowerCase()))).map(c=>`<div class="category-item" data-id="${c.id}"><span>${c.title}</span><button class="add-btn" type="button">+</button></div>`).join('')||(lms?'<div class="empty-message">No courses available</div>':'<div class="empty-message">No categories available</div>');right.innerHTML=[...sel].map(id=>all.find(c=>c.id==id)).filter(Boolean).map(c=>`<div class="category-item" data-id="${c.id}"><span>${c.title}</span><button class="remove-btn" type="button">&times;</button></div>`).join('')||(lms?'<div class="empty-message">No courses selected</div>':'<div class="empty-message">No categories selected</div>');inputs.innerHTML=[...sel].map(id=>`<input type="hidden" name="${name}" value="${id}">`).join('')};srch.oninput=upd;left.onclick=e=>{const item=e.target.closest('.category-item');if(item){sel.add(item.dataset.id);upd()}};right.onclick=e=>{const item=e.target.closest('.category-item');if(item){sel.delete(item.dataset.id);upd()}};upd()})});

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