from django.contrib import admin from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from django.db.models import Count from django import forms from unfold.admin import ModelAdmin, TabularInline from unfold.contrib.filters.admin import RangeNumericFilter, RangeDateTimeFilter from django.shortcuts import redirect from django.urls import reverse from unfold.decorators import action, display from django.contrib import messages from apps.chat.models import RoomMessage, ChatMessage, MessageReadStatus from utils.admin import project_admin_site, admin_url_generator from django.contrib.auth import get_user_model User = get_user_model() # --- HELPER FUNCTION: GET ALLOWED USERS FOR A ROOM --- def get_allowed_users_for_room(room): """ Returns a queryset of active users allowed in a specific room. Private: Initiator + Recipient Group: Initiator + Recipient + Course Professor + Active Course Participants """ allowed_ids = set() if room.initiator_id: allowed_ids.add(room.initiator_id) if room.recipient_id: allowed_ids.add(room.recipient_id) if room.room_type == 'group' and room.course_id: # Add Professor if room.course.professor_id: allowed_ids.add(room.course.professor_id) # Add Active Participants from apps.course.models import Participant participant_ids = Participant.objects.filter( course_id=room.course_id, is_active=True ).values_list('student_id', flat=True) allowed_ids.update(participant_ids) return User.objects.filter(is_active=True, id__in=allowed_ids) # --- WIDTH ENFORCEMENT FOR SELECT2 DROPDOWNS IN INLINES --- class MinWidthInlineForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) target_dropdown_fields = ['sender', 'user'] for field_name, field in self.fields.items(): if field_name in target_dropdown_fields and hasattr(field.widget, 'attrs'): existing_class = field.widget.attrs.get('class', '') field.widget.attrs['class'] = f"{existing_class} min-w-[250px] w-full" existing_style = field.widget.attrs.get('style', '') field.widget.attrs['style'] = f"{existing_style} min-width: 250px; width: 250px;" class ChatMessageInline(TabularInline): model = ChatMessage form = MinWidthInlineForm extra = 1 # 🔔 Allows you to add 1 new message from the room tab tab = True # 🔔 Using real database fields so you can actually input new messages fields = ('sender', 'content', 'content_type', 'file_attachment', 'sent_at', 'is_deleted') readonly_fields = ('sent_at',) can_delete = False show_change_link = True verbose_name = _("Recent Message") verbose_name_plural = _("Recent Messages (Latest 50)") def get_queryset(self, request): qs = super().get_queryset(request) object_id = request.resolver_match.kwargs.get('object_id') if object_id: latest_ids = list(qs.filter(room_id=object_id).order_by('-sent_at').values_list('id', flat=True)[:50]) return qs.filter(id__in=latest_ids).order_by('-sent_at') return qs.none() # 🔔 FILTER THE SENDER DROPDOWN IN THE TAB def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "sender": room_id = request.resolver_match.kwargs.get('object_id') if room_id: try: room = RoomMessage.objects.get(pk=room_id) kwargs["queryset"] = get_allowed_users_for_room(room) except RoomMessage.DoesNotExist: kwargs["queryset"] = User.objects.filter(is_active=True) else: kwargs["queryset"] = User.objects.filter(is_active=True) return super().formfield_for_foreignkey(db_field, request, **kwargs) class MessageReadStatusAdmin(ModelAdmin): list_display = ( 'user', 'message', 'is_read_status', 'read_at', ) list_filter = ( ('read_at', RangeDateTimeFilter), 'is_read', ) search_fields = ('user__username', 'user__email', 'message__content') readonly_fields = ('read_at',) @display(description=_("Read Status")) def is_read_status(self, obj): if obj.is_read: return format_html('{}', _("Read")) return format_html('{}', _("Unread")) class RoomMessageAdmin(ModelAdmin): list_display = ( 'name', 'room_type_badge', 'course', 'initiator', 'messages_count', 'is_locked' ) list_filter = ( 'room_type', ('created_at', RangeDateTimeFilter), ('updated_at', RangeDateTimeFilter), 'course','is_locked' ) search_fields = ('name', 'description', 'course__title', 'initiator__username', 'recipient__username') ordering = ('-created_at',) readonly_fields = ('created_at', 'updated_at', 'messages_count') inlines = [ChatMessageInline] actions_detail = ['manage_all_messages'] fieldsets = ( (_("Room Information"), { 'fields': ('name', 'description', 'room_type', 'messages_count','is_locked'), 'classes': ('grid-col-2',), }), (_("Relations"), { 'fields': ('course', 'initiator', 'recipient'), 'classes': ('grid-col-2',), }), (_("Timestamps"), { 'fields': ('created_at', 'updated_at'), 'classes': ('grid-col-2',), }), ) def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name in ["initiator", "recipient"]: kwargs["queryset"] = User.objects.filter(is_active=True, email__isnull=False) return super().formfield_for_foreignkey(db_field, request, **kwargs) @display(description=_("Messages Count")) def messages_count(self, obj): count = obj.messages.count() return format_html( '' '{}', count ) @display(description=_("Room Type")) def room_type_badge(self, obj): if obj.room_type == 'group': return format_html('{}', _("Group")) return format_html('{}', _("Private")) def get_queryset(self, request): queryset = super().get_queryset(request) queryset = queryset.annotate( total_messages=Count('messages') ) return queryset @action( description=_("Manage All Messages"), icon="chat", ) def manage_all_messages(self, request, object_id): """Redirect to the pre-filtered Chat Message changelist for this room.""" room = self.get_object(request, object_id) if not room: messages.error(request, _("Room not found")) return redirect(admin_url_generator(request, "chat_roommessage_changelist")) base_url = admin_url_generator(request, "chat_chatmessage_changelist") url = f"{base_url}?room__id__exact={object_id}" return redirect(url) class MessageReadStatusInline(TabularInline): model = MessageReadStatus form = MinWidthInlineForm extra = 0 fields = ('user', 'is_read', 'read_at') readonly_fields = ('read_at',) can_delete = False show_change_link = True classes = ['collapse'] verbose_name = _("Read Status") verbose_name_plural = _("Read Statuses") # 🔔 FILTER THE USER DROPDOWN IN THE READ STATUS TAB def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "user": message_id = request.resolver_match.kwargs.get('object_id') if message_id: try: msg = ChatMessage.objects.get(pk=message_id) kwargs["queryset"] = get_allowed_users_for_room(msg.room) except ChatMessage.DoesNotExist: kwargs["queryset"] = User.objects.filter(is_active=True) else: kwargs["queryset"] = User.objects.filter(is_active=True) return super().formfield_for_foreignkey(db_field, request, **kwargs) class ChatMessageAdmin(ModelAdmin): list_display = ( 'id', 'room', 'sender', 'content_type_badge', 'content_preview', 'content_size_display', 'has_attachment', 'sent_at', 'is_deleted_status' ) list_filter = ( 'room', 'content_type', 'is_deleted', ('sent_at', RangeDateTimeFilter), ('updated_at', RangeDateTimeFilter), ('content_size', RangeNumericFilter) ) search_fields = ('room__name', 'sender__username', 'content') ordering = ('-sent_at',) readonly_fields = ('sent_at', 'updated_at', 'content_size', 'attachment_preview') inlines = [MessageReadStatusInline] fieldsets = ( (_("Message Information"), { 'fields': ('room', 'sender', 'content', 'content_type'), 'classes': ('grid-col-2',), }), (_("Attachments"), { 'fields': ('file_attachment', 'image_attachment', 'attachment_preview'), 'classes': ('grid-col-2',), }), (_("Additional Info"), { 'fields': ('content_size',), 'classes': ('grid-col-1',), }), (_("Status"), { 'fields': ('is_deleted', 'deleted_at'), 'classes': ('grid-col-2',), }), (_("Timestamps"), { 'fields': ('sent_at', 'updated_at'), 'classes': ('grid-col-2',), }), ) actions_list = ["back_to_chat_rooms"] # 🔔 FILTER THE SENDER DROPDOWN IN THE CHAT MESSAGE FORM def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "sender": message_id = request.resolver_match.kwargs.get('object_id') if message_id: try: msg = ChatMessage.objects.get(pk=message_id) kwargs["queryset"] = get_allowed_users_for_room(msg.room) except ChatMessage.DoesNotExist: kwargs["queryset"] = User.objects.filter(is_active=True) else: kwargs["queryset"] = User.objects.filter(is_active=True) return super().formfield_for_foreignkey(db_field, request, **kwargs) @action( description=_("Back to Chat Rooms"), icon="arrow_back", ) def back_to_chat_rooms(self, request): url = reverse('admin:chat_roommessage_changelist') return redirect(url) @display(description=_("Content Preview")) def content_preview(self, obj): if obj.content_type == 'text': preview = obj.content[:50] + '...' if len(obj.content) > 50 else obj.content return preview return _("%(type)s content") % {'type': obj.get_content_type_display()} @display(description=_("Type")) def content_type_badge(self, obj): badges = { 'text': ('bg-green-500', _('Text')), 'file': ('bg-green-500', _('File')), 'audio': ('bg-green-500', _('Audio')), 'image': ('bg-green-500', _('Image')), } bg_color, label = badges.get(obj.content_type, ('bg-gray-500', obj.content_type)) return format_html( '{}', bg_color, label ) @display(description=_("Status")) def is_deleted_status(self, obj): if obj.is_deleted: return format_html('{}', _("Deleted")) return format_html('{}', _("Active")) @display(description=_("Size")) def content_size_display(self, obj): if obj.content_size: if obj.content_size > 1024: size_kb = obj.content_size / 1024 return f"{size_kb:.1f} KB" return _("{} bytes").format(obj.content_size) return "-" @display(description=_("Attachment")) def has_attachment(self, obj): if obj.image_attachment: return format_html('{}', _("📷 Image")) elif obj.file_attachment: return format_html('{}', _("📎 File")) elif obj.content and obj.content_type != 'text': return format_html('{}', _("🔗 Legacy")) return "-" @display(description=_("Attachment Preview")) def attachment_preview(self, obj): if obj.image_attachment: return format_html( '
{}:
' '' '
{}
', _("Image"), obj.image_attachment.url, obj.image_attachment.url, _("Open in new tab") ) elif obj.file_attachment: return format_html( '
{}:
' '{}
', _("File"), obj.file_attachment.url, _("📥 Download File") ) elif obj.content and obj.content_type != 'text': return format_html( '
{}:
{}
', _("Legacy URL"), obj.content ) return "-" project_admin_site.register(RoomMessage, RoomMessageAdmin) project_admin_site.register(ChatMessage, ChatMessageAdmin) project_admin_site.register(MessageReadStatus, MessageReadStatusAdmin)