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 ajaxdatatable.admin import AjaxDatatable 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.video.models import * class VideoPlaylistInCollectionInlineForCollection(TabularInline): model = VideoPlaylistInCollection extra = 1 autocomplete_fields = ('playlist',) fields = ('playlist', 'order') ordering = ('order',) verbose_name = _('Playlist') verbose_name_plural = _('Playlists') tab = True class VideoCollectionAdminBase(ModelAdmin): list_display = ('get_title', '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 = [VideoPlaylistInCollectionInlineForCollection] 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=_('Number of Playlists')) def count_playlists(self, obj): count = obj.related_playlists.count() if count > 0: url = reverse('admin:video_videoplaylist_changelist') + f'?collections__id__exact={obj.id}' return format_html('{}', url, count) return count class PinnedVideoCollectionForm(forms.ModelForm): class Meta: model = PinnedVideoCollection # fields = '__all__' exclude = ('slug',) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['thumbnail'].required = True class PinnedVideoCollectionAdmin(VideoCollectionAdminBase): form = PinnedVideoCollectionForm def get_queryset(self, request): return super().get_queryset(request).filter(display_position=VideoCollection.DisplayPosition.PINNED) def save_model(self, request, obj, form, change): obj.display_position = VideoCollection.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 # return [ # obj.title, # None, # None, # { # "path": thumbnail_path, # "height": 30, # "width": 50, # "borderless": True, # # "squared": True, # }, # ] class MiddleVideoCollectionAdmin(VideoCollectionAdminBase): fieldsets = ( (None, { 'fields': ('title', 'status', 'pin_top', 'order') }), ) def get_queryset(self, request): return super().get_queryset(request).filter(display_position=VideoCollection.DisplayPosition.MIDDLE) def save_model(self, request, obj, form, change): obj.display_position = VideoCollection.DisplayPosition.MIDDLE super().save_model(request, obj, form, change) class VideoCategoryAdmin(ModelAdmin): list_display = ('title', 'slug', 'status', 'order', 'count_videos', 'created_at') list_filter = ('status', 'created_at', 'updated_at') search_fields = ('title', 'slug') @admin.display(description=_('Number of Videos')) def count_videos(self, obj): # Count videos through playlists: Category -> Playlist -> PlaylistItem -> Video count = Video.objects.filter( playlist_appearances__playlist__categories=obj ).distinct().count() if count > 0: # Note: Direct filtering by category in admin might not work due to the relationship # We'll just display the count without a clickable link for now return 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 VideoAdmin(ModelAdmin): list_display = ('title', 'slug', 'video_type', 'status', 'view_count', 'created_at') list_filter = ('status', 'video_type', 'created_at', 'updated_at') search_fields = ('title', 'slug', 'description') conditional_fields = { 'video_file': "video_type == 'video_file'", 'video_url': "video_type == 'youtube_link'", } radio_fields = { "video_type": admin.HORIZONTAL, } save_as = True search_help_text = _("Search by title, slug, or description") search_fields_placeholder = _("Search videos") fieldsets = ( (None, { 'fields': ('title', 'slug', 'description', 'thumbnail') }), (_('Video Information'), { 'fields': ('video_type', 'video_file', 'video_url', 'video_time') }), (_('Status'), { 'fields': ('status',) }), (_('Statistics'), { 'fields': ('view_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 if form.base_fields.get('video_type') and not obj: form.base_fields['video_type'].initial = 'youtube_link' return form class PlaylistItemForm(forms.ModelForm): class Meta: model = PlaylistItem fields = ('video', 'priority') def clean_video(self): video = self.cleaned_data.get('video') if not video: return video # If we're editing, exclude the current instance from the check instance = getattr(self, 'instance', None) if instance and instance.pk and instance.video == video: return video # Check if this video exists in another playlist existing_item = PlaylistItem.objects.filter(video=video).first() if existing_item: playlist_name = existing_item.playlist.title raise forms.ValidationError( _('This video is already used in playlist "{}". Each video can only be in one playlist.').format(playlist_name) ) return video class PlaylistItemInline(StackedInline): model = PlaylistItem form = PlaylistItemForm extra = 1 autocomplete_fields = ('video',) fields = ('video', 'priority') ordering = ('priority',) verbose_name = _('Playlist Item') verbose_name_plural = _('Playlist Items') class VideoPlaylistInCollectionInline(TabularInline): model = VideoPlaylistInCollection extra = 1 raw_id_fields = ('collection',) fields = ('collection', 'order') ordering = ('order',) verbose_name = _('Collection') verbose_name_plural = _('Collections') tab = True class VideoPlaylistAdmin(ModelAdmin): list_display = ('title', 'slug', 'status', 'order', 'view_count', 'count_videos', '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 = [PlaylistItemInline, VideoPlaylistInCollectionInline] 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 videos') return form @display(description=_('Number of Videos')) def count_videos(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 video is used in only one playlist """ instances = formset.save(commit=False) # Collect all videos that are being saved videos_to_save = [] for instance in instances: if instance.video: videos_to_save.append(instance.video) # Check for duplicate videos in this formset video_counts = {} for video in videos_to_save: video_counts[video.id] = video_counts.get(video.id, 0) + 1 duplicate_videos = [video_id for video_id, count in video_counts.items() if count > 1] if duplicate_videos: # If there are duplicate videos in this form, show an error formset._non_form_errors = formset.error_class( [_('A video cannot be used multiple times in the same playlist.')] ) return # Check if videos are used in other playlists for instance in instances: if instance.video: # For both new and edited items playlist_id = form.instance.pk query = PlaylistItem.objects.filter( video=instance.video ).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( [_('Video "{}" is already used in playlist "{}". Each video can only be in one playlist.').format( instance.video.title, playlist_name )] ) return # If all validations pass, save the formset super().save_formset(request, form, formset, change) dovoodi_admin_site.register(VideoCategory, VideoCategoryAdmin) dovoodi_admin_site.register(Video, VideoAdmin) dovoodi_admin_site.register(PinnedVideoCollection, PinnedVideoCollectionAdmin) dovoodi_admin_site.register(MiddleVideoCollection, MiddleVideoCollectionAdmin) dovoodi_admin_site.register(VideoPlaylist, VideoPlaylistAdmin)