mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-06-26 23:11:23 +02:00
feat: enable editing of media slug, show categories on manage media
This commit is contained in:
parent
a5acce4ab1
commit
83f3eec940
@ -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
|
||||||
|
@ -1 +1 @@
|
|||||||
VERSION = "6.0.1"
|
VERSION = "6.1.0"
|
||||||
|
@ -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.
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
17
files/migrations/0009_alter_media_friendly_token.py
Normal file
17
files/migrations/0009_alter_media_friendly_token.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -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")
|
||||||
|
|
||||||
|
@ -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",
|
||||||
),
|
),
|
||||||
|
@ -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)
|
||||||
|
@ -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),
|
||||||
|
@ -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
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user