You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

271 lines
11 KiB

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 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
from apps.chat.models import RoomMessage, ChatMessage, MessageReadStatus
from utils.admin import project_admin_site
class ChatMessageInline(TabularInline):
model = ChatMessage
extra = 0
fields = ('sender', 'content', 'content_type', 'sent_at', 'is_deleted')
readonly_fields = ('sent_at',)
can_delete = False
show_change_link = True
classes = ['collapse']
verbose_name = _("Message")
verbose_name_plural = _("Messages")
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',)
def is_read_status(self, obj):
if obj.is_read:
return format_html('<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Read</span>')
return format_html('<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">Unread</span>')
is_read_status.short_description = _("Read Status")
from django.contrib.auth import get_user_model
User = get_user_model()
class RoomMessageAdmin(ModelAdmin):
list_display = (
'name', 'room_type_badge', 'course', 'initiator',
'messages_count', 'view_messages_button','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]
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 == "initiator":
kwargs["queryset"] = User.objects.filter(is_active=True, email__isnull=False)
if db_field.name == "recipient":
kwargs["queryset"] = User.objects.filter(is_active=True, email__isnull=False)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
def messages_count(self, obj):
count = obj.messages.count()
return format_html('<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">{}</span>', count)
messages_count.short_description = _("Messages Count")
def room_type_badge(self, obj):
if obj.room_type == 'group':
return format_html('<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">Group</span>')
return format_html('<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">Private</span>')
room_type_badge.short_description = _("Room Type")
def get_queryset(self, request):
queryset = super().get_queryset(request)
queryset = queryset.annotate(
total_messages=Count('messages')
)
return queryset
def view_messages_button(self, obj):
from django.urls import reverse
url = f"{reverse('admin:chat_chatmessage_changelist')}?room__id__exact={obj.id}"
return format_html(
'<a href="{}" class="inline-flex items-center px-3 py-1.5 rounded text-xs font-medium bg-blue-500 text-white hover:bg-blue-600">'
'<span class="material-icons-outlined mr-1" style="font-size: 14px;">chat</span> {}</a>',
url, _("View Messages")
)
view_messages_button.short_description = _("Messages")
class MessageReadStatusInline(TabularInline):
model = MessageReadStatus
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")
class ChatMessageAdmin(ModelAdmin):
# change_list_template = 'admin/chat/chatmessage/change_list.html'
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"]
@action(
description=_("Back to Chat Rooms"),
icon="arrow_back", # Unfold natively supports Google Material Icons!
)
def back_to_chat_rooms(self, request):
"""Redirects the admin back to the RoomMessage list"""
url = reverse('admin:chat_roommessage_changelist')
return redirect(url)
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 f"{obj.content_type} content"
content_preview.short_description = _("Content Preview")
def content_type_badge(self, obj):
badges = {
'text': ('blue', 'Text'),
'file': ('yellow', 'File'),
'audio': ('green', 'Audio'),
'image': ('pink', 'Image'),
}
color, label = badges.get(obj.content_type, ('gray', obj.content_type))
return format_html(
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-{}-100 text-{}-800">{}</span>',
color, color, label
)
content_type_badge.short_description = _("Type")
def is_deleted_status(self, obj):
if obj.is_deleted:
return format_html('<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">Deleted</span>')
return format_html('<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Active</span>')
is_deleted_status.short_description = _("Status")
def content_size_display(self, obj):
if obj.content_size:
# Format size in KB if larger than 1024 bytes
if obj.content_size > 1024:
size_kb = obj.content_size / 1024
return f"{size_kb:.1f} KB"
return f"{obj.content_size} bytes"
return "-"
content_size_display.short_description = _("Size")
def has_attachment(self, obj):
"""Show if message has file/image attachment"""
if obj.image_attachment:
return format_html(
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">📷 Image</span>'
)
elif obj.file_attachment:
return format_html(
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">📎 File</span>'
)
elif obj.content and obj.content_type != 'text':
return format_html(
'<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">🔗 Legacy</span>'
)
return "-"
has_attachment.short_description = _("Attachment")
def attachment_preview(self, obj):
"""Display attachment preview in detail view"""
if obj.image_attachment:
return format_html(
'<div><strong>Image:</strong><br/>'
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 10px;" />'
'<br/><a href="{}" target="_blank" style="margin-top: 10px; display: inline-block;">Open in new tab</a></div>',
obj.image_attachment.url,
obj.image_attachment.url
)
elif obj.file_attachment:
return format_html(
'<div><strong>File:</strong><br/>'
'<a href="{}" target="_blank" style="margin-top: 10px; display: inline-block; padding: 8px 16px; background: #3b82f6; color: white; border-radius: 4px; text-decoration: none;">📥 Download File</a></div>',
obj.file_attachment.url
)
elif obj.content and obj.content_type != 'text':
return format_html(
'<div><strong>Legacy URL:</strong><br/><code style="background: #f3f4f6; padding: 4px 8px; border-radius: 4px;">{}</code></div>',
obj.content
)
return "-"
attachment_preview.short_description = _("Attachment Preview")
# Register models with the custom admin site
project_admin_site.register(RoomMessage, RoomMessageAdmin)
project_admin_site.register(ChatMessage, ChatMessageAdmin)
project_admin_site.register(MessageReadStatus, MessageReadStatusAdmin)