feat: enable editing of media slug, show categories on manage media

This commit is contained in:
Markos Gogoulos 2025-06-24 11:13:33 +03:00 committed by GitHub
parent a5acce4ab1
commit 83f3eec940
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 85 additions and 9 deletions

View File

@ -503,6 +503,8 @@ JAZZMIN_UI_TWEAKS = {"theme": "flatly"}
USE_ROUNDED_CORNERS = True USE_ROUNDED_CORNERS = True
ALLOW_VIDEO_TRIMMER = True ALLOW_VIDEO_TRIMMER = True
ALLOW_CUSTOM_MEDIA_URLS = False
try: try:
# keep a local_settings.py file for local overrides # keep a local_settings.py file for local overrides
from .local_settings import * # noqa from .local_settings import * # noqa

View File

@ -1 +1 @@
VERSION = "6.0.1" VERSION = "6.1.0"

View File

@ -25,7 +25,7 @@
- [22. Role-Based Access Control](#22-role-based-access-control) - [22. Role-Based Access Control](#22-role-based-access-control)
- [23. SAML setup](#23-saml-setup) - [23. SAML setup](#23-saml-setup)
- [24. Identity Providers setup](#24-identity-providers-setup) - [24. Identity Providers setup](#24-identity-providers-setup)
- [25. Custom urls](#25-custom-urls)
## 1. Welcome ## 1. Welcome
@ -965,3 +965,6 @@ USE_IDENTITY_PROVIDERS = True
Visiting the admin, you will see the Identity Providers tab and you can add one. Visiting the admin, you will see the Identity Providers tab and you can add one.
## 25. Custom urls
To enable custom urls, set `ALLOW_CUSTOM_MEDIA_URLS = True` on settings.py or local_settings.py
This will enable editing the URL of the media, while editing a media. If the URL is already taken you get a message you cannot update this.

View File

@ -22,6 +22,7 @@ class MediaMetadataForm(forms.ModelForm):
class Meta: class Meta:
model = Media model = Media
fields = ( fields = (
"friendly_token",
"title", "title",
"new_tags", "new_tags",
"add_date", "add_date",
@ -38,11 +39,13 @@ class MediaMetadataForm(forms.ModelForm):
"thumbnail_time": forms.NumberInput(attrs={'min': 0, 'step': 0.1}), "thumbnail_time": forms.NumberInput(attrs={'min': 0, 'step': 0.1}),
} }
labels = { labels = {
"friendly_token": "Slug",
"uploaded_poster": "Poster Image", "uploaded_poster": "Poster Image",
"thumbnail_time": "Thumbnail Time (seconds)", "thumbnail_time": "Thumbnail Time (seconds)",
} }
help_texts = { help_texts = {
"title": "", "title": "",
"friendly_token": "Media URL slug",
"thumbnail_time": "Select the time in seconds for the video thumbnail", "thumbnail_time": "Select the time in seconds for the video thumbnail",
"uploaded_poster": "Maximum file size: 5MB", "uploaded_poster": "Maximum file size: 5MB",
} }
@ -50,6 +53,8 @@ class MediaMetadataForm(forms.ModelForm):
def __init__(self, user, *args, **kwargs): def __init__(self, user, *args, **kwargs):
self.user = user self.user = user
super(MediaMetadataForm, self).__init__(*args, **kwargs) super(MediaMetadataForm, self).__init__(*args, **kwargs)
if not getattr(settings, 'ALLOW_CUSTOM_MEDIA_URLS', False):
self.fields.pop("friendly_token")
if self.instance.media_type != "video": if self.instance.media_type != "video":
self.fields.pop("thumbnail_time") self.fields.pop("thumbnail_time")
if self.instance.media_type == "image": if self.instance.media_type == "image":
@ -74,9 +79,22 @@ class MediaMetadataForm(forms.ModelForm):
if self.instance.media_type == "video": if self.instance.media_type == "video":
self.helper.layout.append(CustomField('thumbnail_time')) self.helper.layout.append(CustomField('thumbnail_time'))
if getattr(settings, 'ALLOW_CUSTOM_MEDIA_URLS', False):
self.helper.layout.insert(0, CustomField('friendly_token'))
self.helper.layout.append(FormActions(Submit('submit', 'Update Media', css_class='primaryAction'))) self.helper.layout.append(FormActions(Submit('submit', 'Update Media', css_class='primaryAction')))
def clean_friendly_token(self):
token = self.cleaned_data.get("friendly_token", "").strip()
if token:
if not all(c.isalnum() or c in "-_" for c in token):
raise forms.ValidationError("Slug can only contain alphanumeric characters, underscores, or hyphens.")
if Media.objects.filter(friendly_token=token).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError("This slug is already in use. Please choose a different one.")
return token
def clean_uploaded_poster(self): def clean_uploaded_poster(self):
image = self.cleaned_data.get("uploaded_poster", False) image = self.cleaned_data.get("uploaded_poster", False)
if image: if image:

View File

@ -46,6 +46,7 @@ class MediaList(APIView):
featured = params.get("featured", "").strip() featured = params.get("featured", "").strip()
is_reviewed = params.get("is_reviewed", "").strip() is_reviewed = params.get("is_reviewed", "").strip()
category = params.get("category", "").strip()
sort_by_options = [ sort_by_options = [
"title", "title",
@ -98,6 +99,9 @@ class MediaList(APIView):
if is_reviewed != "all": if is_reviewed != "all":
qs = qs.filter(is_reviewed=is_reviewed) qs = qs.filter(is_reviewed=is_reviewed)
if category:
qs = qs.filter(category__title__contains=category)
media = qs.order_by(f"{ordering}{sort_by}") media = qs.order_by(f"{ordering}{sort_by}")
paginator = pagination_class() paginator = pagination_class()

View File

@ -0,0 +1,17 @@
# Generated by Django 5.1.6 on 2025-06-20 08:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('files', '0008_alter_media_state_videotrimrequest'),
]
operations = [
migrations.AlterField(
model_name='media',
name='friendly_token',
field=models.CharField(blank=True, db_index=True, help_text='Identifier for the Media', max_length=150, unique=True),
),
]

View File

@ -155,7 +155,7 @@ class Media(models.Model):
help_text="Whether media is globally featured by a MediaCMS editor", help_text="Whether media is globally featured by a MediaCMS editor",
) )
friendly_token = models.CharField(blank=True, max_length=12, db_index=True, help_text="Identifier for the Media") friendly_token = models.CharField(blank=True, max_length=150, db_index=True, unique=True, help_text="Identifier for the Media")
hls_file = models.CharField(max_length=1000, blank=True, help_text="Path to HLS file for videos") hls_file = models.CharField(max_length=1000, blank=True, help_text="Path to HLS file for videos")

View File

@ -51,7 +51,7 @@ urlpatterns = [
re_path(r"^api/v1/media$", views.MediaList.as_view()), re_path(r"^api/v1/media$", views.MediaList.as_view()),
re_path(r"^api/v1/media/$", views.MediaList.as_view()), re_path(r"^api/v1/media/$", views.MediaList.as_view()),
re_path( re_path(
r"^api/v1/media/(?P<friendly_token>[\w]*)$", r"^api/v1/media/(?P<friendly_token>[\w\-_]*)$",
views.MediaDetail.as_view(), views.MediaDetail.as_view(),
name="api_get_media", name="api_get_media",
), ),

View File

@ -506,6 +506,9 @@ def liked_media(request):
def manage_users(request): def manage_users(request):
"""List users management view""" """List users management view"""
if not is_mediacms_editor(request.user):
return HttpResponseRedirect("/")
context = {} context = {}
return render(request, "cms/manage_users.html", context) return render(request, "cms/manage_users.html", context)
@ -513,14 +516,19 @@ def manage_users(request):
@login_required @login_required
def manage_media(request): def manage_media(request):
"""List media management view""" """List media management view"""
if not is_mediacms_editor(request.user):
return HttpResponseRedirect("/")
context = {} categories = Category.objects.all().order_by('title').values_list('title', flat=True)
context = {'categories': list(categories)}
return render(request, "cms/manage_media.html", context) return render(request, "cms/manage_media.html", context)
@login_required @login_required
def manage_comments(request): def manage_comments(request):
"""List comments management view""" """List comments management view"""
if not is_mediacms_editor(request.user):
return HttpResponseRedirect("/")
context = {} context = {}
return render(request, "cms/manage_comments.html", context) return render(request, "cms/manage_comments.html", context)

View File

@ -66,7 +66,7 @@
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
width: 20%; width: 10%;
&:nth-child(3n + 1), &:nth-child(3n + 1),
&:nth-child(3n + 2), &:nth-child(3n + 2),

View File

@ -5,6 +5,11 @@ import { FilterOptions } from '../_shared';
import './ManageItemList-filters.scss'; import './ManageItemList-filters.scss';
// Get categories from window if available
const categories = window.CATEGORIES ?
[{ id: 'all', title: 'All' }].concat(window.CATEGORIES.map(cat => ({ id: cat, title: cat }))) :
[{ id: 'all', title: 'All' }];
const filters = { const filters = {
state: [ state: [
{ id: 'all', title: 'All' }, { id: 'all', title: 'All' },
@ -46,6 +51,7 @@ export function ManageMediaFilters(props) {
const [encodingStatus, setEncodingStatus] = useState('all'); const [encodingStatus, setEncodingStatus] = useState('all');
const [isFeatured, setIsFeatured] = useState('all'); const [isFeatured, setIsFeatured] = useState('all');
const [isReviewed, setIsReviewed] = useState('all'); const [isReviewed, setIsReviewed] = useState('all');
const [category, setCategory] = useState('all');
const containerRef = useRef(null); const containerRef = useRef(null);
const innerContainerRef = useRef(null); const innerContainerRef = useRef(null);
@ -63,6 +69,7 @@ export function ManageMediaFilters(props) {
encoding_status: encodingStatus, encoding_status: encodingStatus,
featured: isFeatured, featured: isFeatured,
is_reviewed: isReviewed, is_reviewed: isReviewed,
category: category,
}; };
switch (ev.currentTarget.getAttribute('filter')) { switch (ev.currentTarget.getAttribute('filter')) {
@ -91,6 +98,11 @@ export function ManageMediaFilters(props) {
props.onFiltersUpdate(args); props.onFiltersUpdate(args);
setIsReviewed(args.is_reviewed); setIsReviewed(args.is_reviewed);
break; break;
case 'category':
args.category = ev.currentTarget.getAttribute('value');
props.onFiltersUpdate(args);
setCategory(args.category);
break;
} }
} }
@ -151,6 +163,13 @@ export function ManageMediaFilters(props) {
<FilterOptions id={'featured'} options={filters.featured} selected={isFeatured} onSelect={onFilterSelect} /> <FilterOptions id={'featured'} options={filters.featured} selected={isFeatured} onSelect={onFilterSelect} />
</div> </div>
</div> </div>
<div className="mi-filter">
<div className="mi-filter-title">CATEGORY</div>
<div className="mi-filter-options">
<FilterOptions id={'category'} options={categories} selected={category} onSelect={onFilterSelect} />
</div>
</div>
</div> </div>
</div> </div>
); );

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,12 @@
{% endblock headermeta %} {% endblock headermeta %}
{% block content %}<div id="page-manage-media"></div>{% endblock %} {% block content %}
<script>
window.CATEGORIES = {{ categories|safe }};
</script>
<div id="page-manage-media"></div>
{% endblock %}
{% block bottomimports %} {% block bottomimports %}
<script src="{% static "js/manage-media.js" %}"></script> <script src="{% static "js/manage-media.js" %}"></script>