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 dovoodi_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') }), ) dovoodi_admin_site.register(PodcastCategory, PodcastCategoryAdmin) dovoodi_admin_site.register(Podcast, PodcastAdmin) dovoodi_admin_site.register(PinnedPodcastCollection, PinnedPodcastCollectionAdmin) dovoodi_admin_site.register(MiddlePodcastCollection, MiddlePodcastCollectionAdmin) dovoodi_admin_site.register(PodcastPlaylist, PodcastPlaylistAdmin) dovoodi_admin_site.register(UserPlaylist, UserPlaylistAdmin)