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
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)
|