from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from django.utils.html import format_html
from django.db import models
from unfold.admin import ModelAdmin, StackedInline, TabularInline
from django.contrib.admin import SimpleListFilter
from unfold.widgets import UnfoldAdminSelectWidget
from unfold.decorators import display, action
from django import forms
from utils.admin import project_admin_site
from unfold.sections import TableSection
from apps.podcast.models import *
class PodcastPlaylistInCollectionInlineForCollection(TabularInline):
model = PodcastPlaylistInCollection
extra = 1
autocomplete_fields = ('playlist',)
fields = ('playlist', 'order')
ordering = ('order',)
verbose_name = _('Playlist')
verbose_name_plural = _('Playlists')
tab = True
class PodcastCollectionAdminBase(ModelAdmin):
list_display = ('get_title', 'get_display_position', 'status', 'order', 'count_playlists')
list_filter = ('status', 'order')
search_fields = ('title',)
ordering = ('order',)
list_filter_submit = True
warn_unsaved_form = True
change_form_show_cancel_button = True
inlines = [PodcastPlaylistInCollectionInlineForCollection]
fieldsets = (
(None, {
'fields': ('title', 'summary', 'thumbnail', 'status', 'pin_top', 'order')
}),
)
exclude = ('display_position',)
@display(description=_('Title'))
def get_title(self, obj):
return str(obj.title)
@display(description=_('Display Position'))
def get_display_position(self, obj):
if obj.display_position == PodcastCollection.DisplayPosition.PINNED:
return format_html('📌 Pinned (Top)')
else:
return format_html('📋 Regular (Middle)')
@display(description=_('Number of Playlists'))
def count_playlists(self, obj):
count = obj.related_playlists.count()
if count > 0:
url = reverse('admin:podcast_podcastplaylist_changelist') + f'?collections__id__exact={obj.id}'
return format_html('{}', url, count)
return count
class PinnedPodcastCollectionForm(forms.ModelForm):
class Meta:
model = PinnedPodcastCollection
exclude = ('slug',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['thumbnail'].required = True
class PinnedPodcastCollectionAdmin(PodcastCollectionAdminBase):
form = PinnedPodcastCollectionForm
# Add help text to clarify this is for top section
class Media:
css = {
'all': ()
}
def get_queryset(self, request):
return super().get_queryset(request).filter(display_position=PodcastCollection.DisplayPosition.PINNED)
def save_model(self, request, obj, form, change):
obj.display_position = PodcastCollection.DisplayPosition.PINNED
super().save_model(request, obj, form, change)
@display(description=_('Title'))
def get_title(self, obj):
from django.templatetags.static import static
thumbnail_path = obj.thumbnail.url if obj.thumbnail else None
return obj.title
class MiddlePodcastCollectionAdmin(PodcastCollectionAdminBase):
fieldsets = (
(None, {
'fields': ('title', 'status', 'pin_top', 'order')
}),
)
# Add help text to clarify this is for middle section
class Media:
css = {
'all': ()
}
def get_queryset(self, request):
return super().get_queryset(request).filter(display_position=PodcastCollection.DisplayPosition.MIDDLE)
def save_model(self, request, obj, form, change):
obj.display_position = PodcastCollection.DisplayPosition.MIDDLE
super().save_model(request, obj, form, change)
class PodcastCategoryAdmin(ModelAdmin):
list_display = ('title', 'slug', 'status', 'order', 'count_playlists', 'created_at')
list_filter = ('status', 'created_at', 'updated_at')
search_fields = ('title', 'slug')
search_help_text = _("Search by title or slug")
search_fields_placeholder = _("Search categories")
@admin.display(description=_('Number of Playlists'))
def count_playlists(self, obj):
count = obj.playlists.filter(status=True).count()
if count > 0:
url = reverse('admin:podcast_podcastplaylist_changelist') + f'?categories__id__exact={obj.id}'
return format_html('{}', url, count)
return count
def get_form(self, request, obj=None, change=False, **kwargs):
form = super().get_form(request, obj, change, **kwargs)
if form.base_fields.get('slug'):
form.base_fields['slug'].required = False
return form
class PodcastAdmin(ModelAdmin):
list_display = ('title', 'slug', 'status', 'view_count', 'created_at')
list_filter = ('status', 'created_at', 'updated_at')
search_fields = ('title', 'slug', 'description')
autocomplete_fields = ('categories',)
save_as = True
search_help_text = _("Search by title, slug, or description")
search_fields_placeholder = _("Search podcasts")
fieldsets = (
(None, {
'fields': ('title', 'slug', 'description', 'thumbnail', 'categories')
}),
(_('Audio Information'), {
'fields': ('audio_file', 'audio_time')
}),
(_('Status'), {
'fields': ('status',)
}),
(_('Statistics'), {
'fields': ('view_count', 'download_count')
}),
)
def get_form(self, request, obj=None, change=False, **kwargs):
form = super().get_form(request, obj, change, **kwargs)
if form.base_fields.get('slug'):
form.base_fields['slug'].required = False
if form.base_fields.get('thumbnail'):
form.base_fields['thumbnail'].required = True
return form
class PodcastPlaylistItemForm(forms.ModelForm):
class Meta:
model = PlaylistItem
fields = ('podcast', 'priority')
def clean_podcast(self):
podcast = self.cleaned_data.get('podcast')
if not podcast:
return podcast
# If we're editing, exclude the current instance from the check
instance = getattr(self, 'instance', None)
if instance and instance.pk and instance.podcast == podcast:
return podcast
# Check if this podcast exists in another playlist
existing_item = PlaylistItem.objects.filter(podcast=podcast).first()
if existing_item:
playlist_name = existing_item.playlist.title
raise forms.ValidationError(
_('This podcast is already used in playlist "{}". Each podcast can only be in one playlist.').format(playlist_name)
)
return podcast
class PodcastPlaylistItemInline(StackedInline):
model = PlaylistItem
form = PodcastPlaylistItemForm
extra = 1
autocomplete_fields = ('podcast',)
fields = ('podcast', 'priority')
ordering = ('priority',)
verbose_name = _('Playlist Item')
verbose_name_plural = _('Playlist Items')
class PodcastPlaylistInCollectionInline(TabularInline):
model = PodcastPlaylistInCollection
extra = 1
raw_id_fields = ('collection',)
fields = ('collection', 'order')
ordering = ('order',)
verbose_name = _('Collection')
verbose_name_plural = _('Collections')
tab = True
class PodcastPlaylistAdmin(ModelAdmin):
list_display = ('title', 'slug', 'status', 'order', 'view_count', 'count_podcasts', 'created_at')
list_filter = ('status', 'created_at', 'categories')
search_fields = ('title', 'slug', 'slogan', 'description')
autocomplete_fields = ('categories',)
list_filter_submit = True
warn_unsaved_form = True
change_form_show_cancel_button = True
inlines = [PodcastPlaylistItemInline, PodcastPlaylistInCollectionInline]
fieldsets = (
(None, {
'fields': ('title', 'slug', 'slogan', 'description', 'thumbnail')
}),
(_('Categories'), {
'fields': ('categories',)
}),
(_('Display Settings'), {
'fields': ('order', 'status')
}),
(_('Statistics'), {
'fields': ('view_count', 'total_time')
}),
)
def get_form(self, request, obj=None, change=False, **kwargs):
form = super().get_form(request, obj, change, **kwargs)
if form.base_fields.get('slug'):
form.base_fields['slug'].required = False
if form.base_fields.get('thumbnail'):
form.base_fields['thumbnail'].required = False
if form.base_fields.get('total_time'):
form.base_fields['total_time'].required = False
form.base_fields['total_time'].help_text = _('Will be auto-calculated from podcasts')
return form
@display(description=_('Number of Podcasts'))
def count_podcasts(self, obj):
count = obj.playlist_items.count()
if count > 0:
return format_html('{}', count)
return count
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
# Auto-calculate total_time
obj.total_time = obj.calculate_total_time()
obj.save(update_fields=['total_time'])
def save_formset(self, request, form, formset, change):
"""
Additional validation to ensure each podcast is used in only one playlist
"""
instances = formset.save(commit=False)
# Collect all podcasts that are being saved
podcasts_to_save = []
for instance in instances:
if instance.podcast:
podcasts_to_save.append(instance.podcast)
# Check for duplicate podcasts in this formset
podcast_counts = {}
for podcast in podcasts_to_save:
podcast_counts[podcast.id] = podcast_counts.get(podcast.id, 0) + 1
duplicate_podcasts = [podcast_id for podcast_id, count in podcast_counts.items() if count > 1]
if duplicate_podcasts:
# If there are duplicate podcasts in this form, show an error
formset._non_form_errors = formset.error_class(
[_('A podcast cannot be used multiple times in the same playlist.')]
)
return
# Check if podcasts are used in other playlists
for instance in instances:
if instance.podcast: # For both new and edited items
playlist_id = form.instance.pk
query = PlaylistItem.objects.filter(
podcast=instance.podcast
).exclude(
playlist_id=playlist_id
)
# If we're editing an existing item, exclude it from the check
if instance.pk:
query = query.exclude(pk=instance.pk)
existing_item = query.first()
if existing_item:
playlist_name = existing_item.playlist.title
formset._non_form_errors = formset.error_class(
[_('Podcast "{}" is already used in playlist "{}". Each podcast can only be in one playlist.').format(
instance.podcast.title, playlist_name
)]
)
return
# If all validations pass, save the formset
super().save_formset(request, form, formset, change)
class UserPlaylistAdmin(ModelAdmin):
list_display = ('user', 'podcast', 'status', 'created_at', 'updated_at')
list_filter = ('status', 'created_at', 'updated_at')
search_fields = ('user__username', 'podcast__title')
autocomplete_fields = ('user', 'podcast')
fieldsets = (
(None, {
'fields': ('user', 'podcast', 'status')
}),
)
project_admin_site.register(PodcastCategory, PodcastCategoryAdmin)
project_admin_site.register(Podcast, PodcastAdmin)
project_admin_site.register(PinnedPodcastCollection, PinnedPodcastCollectionAdmin)
project_admin_site.register(MiddlePodcastCollection, MiddlePodcastCollectionAdmin)
project_admin_site.register(PodcastPlaylist, PodcastPlaylistAdmin)
project_admin_site.register(UserPlaylist, UserPlaylistAdmin)