mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-01-20 07:12:58 -05:00
push
This commit is contained in:
@@ -1 +1 @@
|
|||||||
VERSION = "7.5"
|
VERSION = "7.8"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from cms.version import VERSION
|
|||||||
|
|
||||||
from .frontend_translations import get_translation, get_translation_strings
|
from .frontend_translations import get_translation, get_translation_strings
|
||||||
from .methods import is_mediacms_editor, is_mediacms_manager
|
from .methods import is_mediacms_editor, is_mediacms_manager
|
||||||
from .models import Category
|
|
||||||
|
|
||||||
|
|
||||||
def stuff(request):
|
def stuff(request):
|
||||||
@@ -70,13 +69,5 @@ def stuff(request):
|
|||||||
|
|
||||||
if lti_session and request.user.is_authenticated:
|
if lti_session and request.user.is_authenticated:
|
||||||
ret['lti_session'] = lti_session
|
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
|
return ret
|
||||||
|
|||||||
@@ -965,3 +965,13 @@ def get_alphanumeric_only(string):
|
|||||||
"""
|
"""
|
||||||
string = "".join([char for char in string if char.isalnum()])
|
string = "".join([char for char in string if char.isalnum()])
|
||||||
return string.lower()
|
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()
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class Tag(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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]
|
self.title = self.title[:100]
|
||||||
super(Tag, self).save(*args, **kwargs)
|
super(Tag, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ urlpatterns = [
|
|||||||
views.trim_video,
|
views.trim_video,
|
||||||
),
|
),
|
||||||
re_path(r"^api/v1/categories$", views.CategoryList.as_view()),
|
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/tags$", views.TagList.as_view()),
|
||||||
re_path(r"^api/v1/comments$", views.CommentList.as_view()),
|
re_path(r"^api/v1/comments$", views.CommentList.as_view()),
|
||||||
re_path(
|
re_path(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Import all views for backward compatibility
|
# Import all views for backward compatibility
|
||||||
|
|
||||||
from .auth import custom_login_view, saml_metadata # noqa: F401
|
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 .comments import CommentDetail, CommentList # noqa: F401
|
||||||
from .encoding import EncodeProfileList, EncodingDetail # noqa: F401
|
from .encoding import EncodeProfileList, EncodingDetail # noqa: F401
|
||||||
from .media import MediaActions # noqa: F401
|
from .media import MediaActions # noqa: F401
|
||||||
|
|||||||
@@ -43,6 +43,40 @@ class CategoryList(APIView):
|
|||||||
return Response(ret)
|
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):
|
class TagList(APIView):
|
||||||
"""List tags"""
|
"""List tags"""
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
{% block topimports %}
|
{% block topimports %}
|
||||||
<link href="{% static "css/add-media.css" %}" rel="preload" as="style">
|
<link href="{% static "css/add-media.css" %}" rel="preload" as="style">
|
||||||
<link href="{% static "css/add-media.css" %}" rel="stylesheet">
|
<link href="{% static "css/add-media.css" %}" rel="stylesheet">
|
||||||
|
<link href="{% static "css/category_modal.css" %}" rel="stylesheet">
|
||||||
{%endblock topimports %}
|
{%endblock topimports %}
|
||||||
|
|
||||||
{% block innercontent %}
|
{% block innercontent %}
|
||||||
@@ -84,7 +85,7 @@
|
|||||||
<span class="filename-edit qq-edit-filename-icon-selector" aria-label="Edit filename">Edit filename <i class="material-icons">create</i></span>
|
<span class="filename-edit qq-edit-filename-icon-selector" aria-label="Edit filename">Edit filename <i class="material-icons">create</i></span>
|
||||||
<button type="button" class="delete-media-upload-item qq-upload-delete-selector" aria-label="Delete">Delete <i class="material-icons">delete</i></button>
|
<button type="button" class="delete-media-upload-item qq-upload-delete-selector" aria-label="Delete">Delete <i class="material-icons">delete</i></button>
|
||||||
<button type="button" class="cancel-media-upload-item qq-upload-cancel-selector" aria-label="Cancel">Cancel <i class="material-icons">cancel</i></button>
|
<button type="button" class="cancel-media-upload-item qq-upload-cancel-selector" aria-label="Cancel">Cancel <i class="material-icons">cancel</i></button>
|
||||||
<a href="#" class="view-uploaded-media-link qq-hide" target="_blank">{{ "View media" | custom_translate:LANGUAGE_CODE}}<i class="material-icons">open_in_new</i></a>
|
<a href="#" class="view-uploaded-media-link qq-hide">{{ "View media" | custom_translate:LANGUAGE_CODE}}<i class="material-icons">open_in_new</i></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-upload-item-bottom-actions">
|
<div class="media-upload-item-bottom-actions">
|
||||||
<button type="button" class="continue-media-upload-item qq-upload-continue-selector" aria-label="Continue"><i class="material-icons">play_circle_outline</i> Continue</button>
|
<button type="button" class="continue-media-upload-item qq-upload-continue-selector" aria-label="Continue"><i class="material-icons">play_circle_outline</i> Continue</button>
|
||||||
@@ -119,6 +120,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
<div class="media-uploader"></div>
|
<div class="media-uploader"></div>
|
||||||
|
|
||||||
|
<!-- Category Selector -->
|
||||||
|
<div class="category-selector-section" style="margin-top: 30px; padding: 20px; background: #f9f9f9; border-radius: 8px; max-width: 600px;">
|
||||||
|
<h3 style="margin: 0 0 15px; font-size: 18px; font-weight: 500;">{{ "Publish in categories (optional)" | custom_translate:LANGUAGE_CODE}}</h3>
|
||||||
|
<div id="category-widget-container" class="category-widget" data-name="publish_to_category" style="max-width: 100%;">
|
||||||
|
<div class="category-content">
|
||||||
|
<div class="category-panel">
|
||||||
|
<input type="text" class="category-search" placeholder="{{ 'Search categories...' | custom_translate:LANGUAGE_CODE}}">
|
||||||
|
<div class="category-list scrollable" data-panel="left">
|
||||||
|
<div class="empty-message">{{ "Loading categories..." | custom_translate:LANGUAGE_CODE}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="category-panel">
|
||||||
|
<h3>{{ "Selected Categories" | custom_translate:LANGUAGE_CODE}}</h3>
|
||||||
|
<div class="category-list scrollable" data-panel="right">
|
||||||
|
<div class="empty-message">{{ "No categories selected" | custom_translate:LANGUAGE_CODE}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden-inputs"></div>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 15px; font-size: 14px; color: #666;">
|
||||||
|
{{ "Select the categories that the media will be published in" | custom_translate:LANGUAGE_CODE}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -156,11 +182,94 @@
|
|||||||
}
|
}
|
||||||
return cookieVal;
|
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 '<div class="category-item" data-id="' + c.uid + '">' +
|
||||||
|
'<span>' + c.title + '</span>' +
|
||||||
|
'<button class="add-btn" type="button">+</button>' +
|
||||||
|
'</div>';
|
||||||
|
}).join('') || '<div class="empty-message">No categories available</div>';
|
||||||
|
|
||||||
|
// 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 '<div class="category-item" data-id="' + c.uid + '">' +
|
||||||
|
'<span>' + c.title + '</span>' +
|
||||||
|
'<button class="remove-btn" type="button">×</button>' +
|
||||||
|
'</div>';
|
||||||
|
}).join('') || '<div class="empty-message">No categories selected</div>';
|
||||||
|
|
||||||
|
// Update hidden inputs
|
||||||
|
hiddenInputs.innerHTML = Array.from(selectedCategorySet).map(function(id) {
|
||||||
|
return '<input type="hidden" name="publish_to_category" value="' + id + '">';
|
||||||
|
}).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 = '<div class="empty-message">Error loading categories</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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({
|
var default_concurrent_chunked_uploader = new qq.FineUploader({
|
||||||
debug: false,
|
debug: true,
|
||||||
element: document.querySelector('.media-uploader'),
|
element: document.querySelector('.media-uploader'),
|
||||||
request: {
|
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: {
|
customHeaders: {
|
||||||
'X-CSRFToken': getCSRFToken('csrftoken'),
|
'X-CSRFToken': getCSRFToken('csrftoken'),
|
||||||
},
|
},
|
||||||
@@ -179,7 +288,7 @@
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
endpoint: '{% url 'uploader:upload' %}?done{% if lti_category_uid %}&publish_to_category={{ lti_category_uid }}{% endif %}',
|
endpoint: '{% url 'uploader:upload' %}?done',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ MediaCMS.url = {
|
|||||||
likedMedia: "/liked",
|
likedMedia: "/liked",
|
||||||
history: "/history",
|
history: "/history",
|
||||||
/* Add pages */
|
/* Add pages */
|
||||||
addMedia: "/upload{% if lti_category_uid %}?publish_to_category={{ lti_category_uid }}{% endif %}",
|
addMedia: "/upload",
|
||||||
recordScreen: "/record_screen",
|
recordScreen: "/record_screen",
|
||||||
/* Profile/account edit pages */
|
/* Profile/account edit pages */
|
||||||
editProfile: "{{user.edit_url}}",
|
editProfile: "{{user.edit_url}}",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from django.views import generic
|
|||||||
|
|
||||||
from files.helpers import rm_file
|
from files.helpers import rm_file
|
||||||
from files.methods import user_allowed_to_upload
|
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 .fineuploader import ChunkedFineUploader
|
||||||
from .forms import FineUploaderUploadForm, FineUploaderUploadSuccessForm
|
from .forms import FineUploaderUploadForm, FineUploaderUploadSuccessForm
|
||||||
@@ -67,14 +67,21 @@ class FineUploaderView(generic.FormView):
|
|||||||
myfile = File(f)
|
myfile = File(f)
|
||||||
new = Media.objects.create(media_file=myfile, user=self.request.user, title=self.upload.original_filename)
|
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:
|
if publish_to_category:
|
||||||
category = Category.objects.filter(uid=publish_to_category).first()
|
category_uids = [uid.strip() for uid in publish_to_category.split(',') if uid.strip()]
|
||||||
if category:
|
|
||||||
has_access = self.request.user.has_contributor_access_to_category(category)
|
for category_uid in category_uids:
|
||||||
if has_access:
|
category = Category.objects.filter(uid=category_uid).first()
|
||||||
new.category.add(category)
|
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)
|
rm_file(media_file)
|
||||||
shutil.rmtree(os.path.join(settings.MEDIA_ROOT, self.upload.file_path))
|
shutil.rmtree(os.path.join(settings.MEDIA_ROOT, self.upload.file_path))
|
||||||
|
|||||||
Reference in New Issue
Block a user