From 4bd56da2d805dd968ffb467487bd469dbbe25ac9 Mon Sep 17 00:00:00 2001 From: Markos Gogoulos Date: Fri, 9 Jan 2026 13:29:18 +0200 Subject: [PATCH] push --- cms/version.py | 2 +- files/context_processors.py | 9 --- files/helpers.py | 10 +++ files/models/category.py | 2 +- files/urls.py | 1 + files/views/__init__.py | 2 +- files/views/categories.py | 34 ++++++++++ templates/cms/add-media.html | 117 +++++++++++++++++++++++++++++++-- templates/config/core/url.html | 2 +- uploader/views.py | 23 ++++--- 10 files changed, 177 insertions(+), 25 deletions(-) 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}}

+
+
+
+ +
+
{{ "Loading categories..." | custom_translate:LANGUAGE_CODE}}
+
+
+
+

{{ "Selected Categories" | custom_translate:LANGUAGE_CODE}}

+
+
{{ "No categories selected" | 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))