diff --git a/cms/version.py b/cms/version.py
index 520b0d5b..efd9ce7e 100644
--- a/cms/version.py
+++ b/cms/version.py
@@ -1 +1 @@
-VERSION = "7.5"
+VERSION = "7.8"
diff --git a/files/context_processors.py b/files/context_processors.py
index 0fc539f8..d5787700 100644
--- a/files/context_processors.py
+++ b/files/context_processors.py
@@ -4,7 +4,6 @@ from cms.version import VERSION
from .frontend_translations import get_translation, get_translation_strings
from .methods import is_mediacms_editor, is_mediacms_manager
-from .models import Category
def stuff(request):
@@ -70,13 +69,5 @@ def stuff(request):
if lti_session and request.user.is_authenticated:
ret['lti_session'] = lti_session
- platform_id = lti_session.get('platform_id')
- context_id = lti_session.get('context_id')
- if platform_id and context_id:
- category = Category.objects.filter(lti_platform_id=platform_id, lti_context_id=context_id).first()
- if category:
- has_access = request.user.has_contributor_access_to_category(category)
- if has_access:
- ret['lti_category_uid'] = category.uid
return ret
diff --git a/files/helpers.py b/files/helpers.py
index 8d3a892a..a6b589bc 100644
--- a/files/helpers.py
+++ b/files/helpers.py
@@ -965,3 +965,13 @@ def get_alphanumeric_only(string):
"""
string = "".join([char for char in string if char.isalnum()])
return string.lower()
+
+
+def get_alphanumeric_and_spaces(string):
+ """Returns a query that contains only alphanumeric characters and spaces
+ This include characters other than the English alphabet too
+ """
+ string = "".join([char for char in string if char.isalnum() or char.isspace()])
+ # Replace multiple spaces with single space and strip
+ string = " ".join(string.split())
+ return string.lower()
diff --git a/files/models/category.py b/files/models/category.py
index 5f397c3c..f677c6bf 100644
--- a/files/models/category.py
+++ b/files/models/category.py
@@ -144,7 +144,7 @@ class Tag(models.Model):
return True
def save(self, *args, **kwargs):
- self.title = helpers.get_alphanumeric_only(self.title)
+ self.title = helpers.get_alphanumeric_and_spaces(self.title)
self.title = self.title[:100]
super(Tag, self).save(*args, **kwargs)
diff --git a/files/urls.py b/files/urls.py
index 81c60930..f88d5dbd 100644
--- a/files/urls.py
+++ b/files/urls.py
@@ -80,6 +80,7 @@ urlpatterns = [
views.trim_video,
),
re_path(r"^api/v1/categories$", views.CategoryList.as_view()),
+ re_path(r"^api/v1/categories/contributor$", views.CategoryListContributor.as_view()),
re_path(r"^api/v1/tags$", views.TagList.as_view()),
re_path(r"^api/v1/comments$", views.CommentList.as_view()),
re_path(
diff --git a/files/views/__init__.py b/files/views/__init__.py
index da7e8240..7454c0ad 100644
--- a/files/views/__init__.py
+++ b/files/views/__init__.py
@@ -1,7 +1,7 @@
# Import all views for backward compatibility
from .auth import custom_login_view, saml_metadata # noqa: F401
-from .categories import CategoryList, TagList # noqa: F401
+from .categories import CategoryList, CategoryListContributor, TagList # noqa: F401
from .comments import CommentDetail, CommentList # noqa: F401
from .encoding import EncodeProfileList, EncodingDetail # noqa: F401
from .media import MediaActions # noqa: F401
diff --git a/files/views/categories.py b/files/views/categories.py
index 3e101a51..d625e988 100644
--- a/files/views/categories.py
+++ b/files/views/categories.py
@@ -43,6 +43,40 @@ class CategoryList(APIView):
return Response(ret)
+class CategoryListContributor(APIView):
+ """List categories where user has contributor access"""
+
+ @swagger_auto_schema(
+ manual_parameters=[],
+ tags=['Categories'],
+ operation_summary='Lists Categories for Contributors',
+ operation_description='Lists all categories where the user has contributor access',
+ responses={
+ 200: openapi.Response('response description', CategorySerializer),
+ },
+ )
+ def get(self, request, format=None):
+ if not request.user.is_authenticated:
+ return Response([])
+
+ categories = Category.objects.none()
+
+ # Get global/public categories (non-RBAC)
+ public_categories = Category.objects.filter(is_rbac_category=False).prefetch_related("user")
+
+ # Get RBAC categories where user has contributor access
+ if getattr(settings, 'USE_RBAC', False):
+ rbac_categories = request.user.get_rbac_categories_as_contributor()
+ categories = public_categories.union(rbac_categories)
+ else:
+ categories = public_categories
+
+ categories = categories.order_by("title")
+
+ serializer = CategorySerializer(categories, many=True, context={"request": request})
+ return Response(serializer.data)
+
+
class TagList(APIView):
"""List tags"""
diff --git a/templates/cms/add-media.html b/templates/cms/add-media.html
index 8d112601..acfabc84 100644
--- a/templates/cms/add-media.html
+++ b/templates/cms/add-media.html
@@ -19,6 +19,7 @@
{% block topimports %}
+
{%endblock topimports %}
{% block innercontent %}
@@ -84,7 +85,7 @@
Edit filename create
- {{ "View media" | custom_translate:LANGUAGE_CODE}}open_in_new
+ {{ "View media" | custom_translate:LANGUAGE_CODE}}open_in_new
@@ -119,6 +120,31 @@
+
+
+
+
{{ "Publish in categories (optional)" | custom_translate:LANGUAGE_CODE}}
+
+
+ {{ "Select the categories that the media will be published in" | custom_translate:LANGUAGE_CODE}}
+
+
{% else %}
@@ -156,11 +182,94 @@
}
return cookieVal;
}
+
+ // Initialize category widget
+ var categoryWidget = document.getElementById('category-widget-container');
+ var selectedCategorySet = new Set();
+ var allCategories = [];
+ var searchInput = categoryWidget.querySelector('.category-search');
+ var leftPanel = categoryWidget.querySelector('[data-panel="left"]');
+ var rightPanel = categoryWidget.querySelector('[data-panel="right"]');
+ var hiddenInputs = categoryWidget.querySelector('.hidden-inputs');
+
+ function updateCategoryUI() {
+ // Update left panel (available categories)
+ var filteredCategories = allCategories.filter(function(c) {
+ return !selectedCategorySet.has(c.uid) &&
+ (!searchInput.value || c.title.toLowerCase().includes(searchInput.value.toLowerCase()));
+ });
+
+ leftPanel.innerHTML = filteredCategories.map(function(c) {
+ return '' +
+ '' + c.title + '' +
+ '' +
+ '
';
+ }).join('') || 'No categories available
';
+
+ // Update right panel (selected categories)
+ var selectedCategories = Array.from(selectedCategorySet).map(function(id) {
+ return allCategories.find(function(c) { return c.uid === id; });
+ }).filter(Boolean);
+
+ rightPanel.innerHTML = selectedCategories.map(function(c) {
+ return '' +
+ '' + c.title + '' +
+ '' +
+ '
';
+ }).join('') || 'No categories selected
';
+
+ // Update hidden inputs
+ hiddenInputs.innerHTML = Array.from(selectedCategorySet).map(function(id) {
+ return '';
+ }).join('');
+ }
+
+ // Fetch categories from API
+ fetch('/api/v1/categories/contributor')
+ .then(function(response) { return response.json(); })
+ .then(function(categories) {
+ allCategories = categories;
+ updateCategoryUI();
+ })
+ .catch(function(error) {
+ console.error('Error fetching categories:', error);
+ leftPanel.innerHTML = 'Error loading categories
';
+ });
+
+ // Event handlers
+ searchInput.addEventListener('input', updateCategoryUI);
+
+ leftPanel.addEventListener('click', function(e) {
+ var item = e.target.closest('.category-item');
+ if (item) {
+ selectedCategorySet.add(item.dataset.id);
+ updateCategoryUI();
+ }
+ });
+
+ rightPanel.addEventListener('click', function(e) {
+ var item = e.target.closest('.category-item');
+ if (item) {
+ selectedCategorySet.delete(item.dataset.id);
+ updateCategoryUI();
+ }
+ });
+
+ // Function to get selected categories for FineUploader
+ function getSelectedCategories() {
+ return Array.from(selectedCategorySet).join(',');
+ }
+
var default_concurrent_chunked_uploader = new qq.FineUploader({
- debug: false,
+ debug: true,
element: document.querySelector('.media-uploader'),
request: {
- endpoint: '{% url 'uploader:upload' %}{% if lti_category_uid %}?publish_to_category={{ lti_category_uid }}{% endif %}',
+ endpoint: '{% url 'uploader:upload' %}',
+ params: {
+ 'publish_to_category': function() {
+ return getSelectedCategories();
+ }
+ },
customHeaders: {
'X-CSRFToken': getCSRFToken('csrftoken'),
},
@@ -179,7 +288,7 @@
enabled: true,
},
success: {
- endpoint: '{% url 'uploader:upload' %}?done{% if lti_category_uid %}&publish_to_category={{ lti_category_uid }}{% endif %}',
+ endpoint: '{% url 'uploader:upload' %}?done',
},
},
callbacks: {
diff --git a/templates/config/core/url.html b/templates/config/core/url.html
index 98a8ec05..4737563e 100644
--- a/templates/config/core/url.html
+++ b/templates/config/core/url.html
@@ -14,7 +14,7 @@ MediaCMS.url = {
likedMedia: "/liked",
history: "/history",
/* Add pages */
- addMedia: "/upload{% if lti_category_uid %}?publish_to_category={{ lti_category_uid }}{% endif %}",
+ addMedia: "/upload",
recordScreen: "/record_screen",
/* Profile/account edit pages */
editProfile: "{{user.edit_url}}",
diff --git a/uploader/views.py b/uploader/views.py
index ea57a000..e702eae2 100644
--- a/uploader/views.py
+++ b/uploader/views.py
@@ -10,7 +10,7 @@ from django.views import generic
from files.helpers import rm_file
from files.methods import user_allowed_to_upload
-from files.models import Category, Media
+from files.models import Category, Media, Tag
from .fineuploader import ChunkedFineUploader
from .forms import FineUploaderUploadForm, FineUploaderUploadSuccessForm
@@ -67,14 +67,21 @@ class FineUploaderView(generic.FormView):
myfile = File(f)
new = Media.objects.create(media_file=myfile, user=self.request.user, title=self.upload.original_filename)
- publish_to_category = self.request.GET.get('publish_to_category', '').strip()
-
+ publish_to_category = self.request.POST.get('publish_to_category', '') or self.request.GET.get('publish_to_category', '')
+ publish_to_category = publish_to_category.strip()
if publish_to_category:
- category = Category.objects.filter(uid=publish_to_category).first()
- if category:
- has_access = self.request.user.has_contributor_access_to_category(category)
- if has_access:
- new.category.add(category)
+ category_uids = [uid.strip() for uid in publish_to_category.split(',') if uid.strip()]
+
+ for category_uid in category_uids:
+ category = Category.objects.filter(uid=category_uid).first()
+ if category:
+ has_access = self.request.user.has_contributor_access_to_category(category) or category.is_rbac_category is False
+ if has_access:
+ new.category.add(category)
+
+ if category.is_lms_course:
+ tag, created = Tag.objects.get_or_create(title=category.title)
+ new.tags.add(tag)
rm_file(media_file)
shutil.rmtree(os.path.join(settings.MEDIA_ROOT, self.upload.file_path))