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.
366 lines
15 KiB
366 lines
15 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 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('<span class="bg-green-500 text-white font-medium rounded-full px-3 py-1 text-xs text-center inline-flex items-center justify-center">{}</span>', _("Read"))
|
|
return format_html('<span class="bg-red-500 text-white font-medium rounded-full px-3 py-1 text-xs text-center inline-flex items-center justify-center">{}</span>', _("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(
|
|
'<span class="bg-primary-500 text-white font-medium rounded-full px-3 py-1 text-xs text-center inline-flex items-center justify-center min-w-[2rem]">'
|
|
'{}</span>', count
|
|
)
|
|
|
|
@display(description=_("Room Type"))
|
|
def room_type_badge(self, obj):
|
|
if obj.room_type == 'group':
|
|
return format_html('<span class="bg-purple-500 text-white font-medium rounded-full px-3 py-1 text-xs text-center inline-flex items-center justify-center">{}</span>', _("Group"))
|
|
return format_html('<span class="bg-indigo-500 text-white font-medium rounded-full px-3 py-1 text-xs text-center inline-flex items-center justify-center">{}</span>', _("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(
|
|
'<span class="{} text-white font-medium rounded-full px-3 py-1 text-xs text-center inline-flex items-center justify-center min-w-[4rem]">{}</span>',
|
|
bg_color, label
|
|
)
|
|
|
|
@display(description=_("Status"))
|
|
def is_deleted_status(self, obj):
|
|
if obj.is_deleted:
|
|
return format_html('<span class="bg-red-500 text-white font-medium rounded-full px-3 py-1 text-xs text-center inline-flex items-center justify-center">{}</span>', _("Deleted"))
|
|
return format_html('<span class="bg-green-500 text-white font-medium rounded-full px-3 py-1 text-xs text-center inline-flex items-center justify-center">{}</span>', _("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('<span class="bg-blue-500 text-white font-medium rounded-full px-3 py-1 text-xs text-center inline-flex items-center justify-center">{}</span>', _("📷 Image"))
|
|
elif obj.file_attachment:
|
|
return format_html('<span class="bg-green-500 text-white font-medium rounded-full px-3 py-1 text-xs text-center inline-flex items-center justify-center">{}</span>', _("📎 File"))
|
|
elif obj.content and obj.content_type != 'text':
|
|
return format_html('<span class="bg-orange-500 text-white font-medium rounded-full px-3 py-1 text-xs text-center inline-flex items-center justify-center">{}</span>', _("🔗 Legacy"))
|
|
return "-"
|
|
|
|
@display(description=_("Attachment Preview"))
|
|
def attachment_preview(self, obj):
|
|
if obj.image_attachment:
|
|
return format_html(
|
|
'<div><strong>{}:</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;">{}</a></div>',
|
|
_("Image"),
|
|
obj.image_attachment.url,
|
|
obj.image_attachment.url,
|
|
_("Open in new tab")
|
|
)
|
|
elif obj.file_attachment:
|
|
return format_html(
|
|
'<div><strong>{}:</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;">{}</a></div>',
|
|
_("File"),
|
|
obj.file_attachment.url,
|
|
_("📥 Download File")
|
|
)
|
|
elif obj.content and obj.content_type != 'text':
|
|
return format_html(
|
|
'<div><strong>{}:</strong><br/><code style="background: #f3f4f6; padding: 4px 8px; border-radius: 4px;">{}</code></div>',
|
|
_("Legacy URL"),
|
|
obj.content
|
|
)
|
|
return "-"
|
|
|
|
project_admin_site.register(RoomMessage, RoomMessageAdmin)
|
|
project_admin_site.register(ChatMessage, ChatMessageAdmin)
|
|
project_admin_site.register(MessageReadStatus, MessageReadStatusAdmin)
|