diff --git a/apps/chat/admin.py b/apps/chat/admin.py index cb23080..d68c132 100644 --- a/apps/chat/admin.py +++ b/apps/chat/admin.py @@ -4,6 +4,9 @@ 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 @@ -44,13 +47,13 @@ User = get_user_model() class RoomMessageAdmin(ModelAdmin): list_display = ( 'name', 'room_type_badge', 'course', 'initiator', - 'messages_count', 'view_messages_button' + 'messages_count', 'view_messages_button','is_locked' ) list_filter = ( 'room_type', ('created_at', RangeDateTimeFilter), ('updated_at', RangeDateTimeFilter), - 'course' + 'course','is_locked' ) search_fields = ('name', 'description', 'course__title', 'initiator__username', 'recipient__username') ordering = ('-created_at',) @@ -59,7 +62,7 @@ class RoomMessageAdmin(ModelAdmin): fieldsets = ( (_("Room Information"), { - 'fields': ('name', 'description', 'room_type', 'messages_count'), + 'fields': ('name', 'description', 'room_type', 'messages_count','is_locked'), 'classes': ('grid-col-2',), }), (_("Relations"), { @@ -127,7 +130,7 @@ class MessageReadStatusInline(TabularInline): class ChatMessageAdmin(ModelAdmin): - change_list_template = 'admin/chat/chatmessage/change_list.html' + # 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' @@ -167,7 +170,17 @@ class ChatMessageAdmin(ModelAdmin): '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 diff --git a/apps/chat/migrations/0002_roommessage_is_locked.py b/apps/chat/migrations/0002_roommessage_is_locked.py new file mode 100644 index 0000000..ccec89c --- /dev/null +++ b/apps/chat/migrations/0002_roommessage_is_locked.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.12 on 2026-04-26 14:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='roommessage', + name='is_locked', + field=models.BooleanField(default=False, help_text='If True, only the professor and admins can send new messages.', verbose_name='Is Locked'), + ), + ] diff --git a/apps/chat/models.py b/apps/chat/models.py index 253b947..ea5dbc6 100644 --- a/apps/chat/models.py +++ b/apps/chat/models.py @@ -50,6 +50,12 @@ class RoomMessage(models.Model): auto_now=True, verbose_name="Updated At" ) + + is_locked = models.BooleanField( + default=False, + verbose_name="Is Locked", + help_text="If True, only the professor and admins can send new messages." + ) room_type = models.CharField( max_length=10, choices=RoomTypeChoices.choices, diff --git a/apps/course/admin/course.py b/apps/course/admin/course.py index c5b6dbc..c9f1fa6 100644 --- a/apps/course/admin/course.py +++ b/apps/course/admin/course.py @@ -293,6 +293,9 @@ class CourseAdmin(DirectCourseAdmin): (_('Status'), { 'fields': ('status', 'is_online', 'online_link'), }), + (_('Chat Settings'), { + 'fields': ('is_group_chat_locked', 'is_professor_chat_locked'), + }), (_('Course Details'), { 'fields': ('description', 'short_description', 'level', 'duration', 'lessons_count',), # 'classes': ['tab'], diff --git a/apps/course/migrations/0002_course_is_chat_group_lock_course_is_prof_chat_lock.py b/apps/course/migrations/0002_course_is_chat_group_lock_course_is_prof_chat_lock.py new file mode 100644 index 0000000..4420cf4 --- /dev/null +++ b/apps/course/migrations/0002_course_is_chat_group_lock_course_is_prof_chat_lock.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.12 on 2026-04-26 15:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='course', + name='is_chat_group_lock', + field=models.BooleanField(default=False, verbose_name='Lock Group Chat'), + ), + migrations.AddField( + model_name='course', + name='is_prof_chat_lock', + field=models.BooleanField(default=False, verbose_name='Lock Private Chats with Professor'), + ), + ] diff --git a/apps/course/migrations/0003_rename_is_chat_group_lock_course_is_group_chat_locked_and_more.py b/apps/course/migrations/0003_rename_is_chat_group_lock_course_is_group_chat_locked_and_more.py new file mode 100644 index 0000000..3d25c2e --- /dev/null +++ b/apps/course/migrations/0003_rename_is_chat_group_lock_course_is_group_chat_locked_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.12 on 2026-04-26 15:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0002_course_is_chat_group_lock_course_is_prof_chat_lock'), + ] + + operations = [ + migrations.RenameField( + model_name='course', + old_name='is_chat_group_lock', + new_name='is_group_chat_locked', + ), + migrations.RenameField( + model_name='course', + old_name='is_prof_chat_lock', + new_name='is_professor_chat_locked', + ), + ] diff --git a/apps/course/models/course.py b/apps/course/models/course.py index 1a47a8e..99bf5af 100644 --- a/apps/course/models/course.py +++ b/apps/course/models/course.py @@ -98,6 +98,15 @@ class Course(models.Model): verbose_name=_('Course Final Price'), decimal_places=2, max_digits=10, default=0.00, blank=True, help_text=_('This field is automatically calculated based on the discount percentage.') ) + + is_group_chat_locked = models.BooleanField( + default=False, + verbose_name=_('Lock Group Chat') + ) + is_professor_chat_locked = models.BooleanField( + default=False, + verbose_name=_('Lock Private Chats with Professor') + ) timing = models.JSONField(blank=True, null=True, default=default_timing, verbose_name=_("Timing")) features = models.JSONField(verbose_name=_('Course features'), default=dict, blank=True, null=True) diff --git a/apps/course/serializers/course.py b/apps/course/serializers/course.py index 3723ae3..3a4d689 100644 --- a/apps/course/serializers/course.py +++ b/apps/course/serializers/course.py @@ -1,5 +1,4 @@ from rest_framework import serializers - # from dj_filer.admin import get_thumbs from utils import get_thumbs from apps.course.models import Course, CourseCategory, Attachment, Glossary, LessonCompletion, Participant, Lesson, CourseAttachment, CourseGlossary, CourseLesson @@ -134,7 +133,9 @@ class CourseDetailSerializer(serializers.ModelSerializer): 'features', 'last_lesson_id', 'room_id', - 'user_transaction_status' + 'user_transaction_status', + 'is_group_chat_locked', + 'is_professor_chat_locked' ] def get_room_id(self, obj): diff --git a/apps/course/signals.py b/apps/course/signals.py index 4074622..b83ac22 100644 --- a/apps/course/signals.py +++ b/apps/course/signals.py @@ -1,5 +1,6 @@ from apps.course.models import Course from apps.chat.models import RoomMessage +from django.db.models import Q from django.db.models.signals import post_save, post_delete from django.dispatch import receiver @@ -53,4 +54,29 @@ def invalidate_professor_course_cache(sender, instance, **kwargs): def invalidate_professor_profile_cache(sender, instance, **kwargs): if instance.user_type == UserModel.UserType.PROFESSOR: cache_key = f"professor_detail_{instance.slug}" - cache.delete(cache_key) \ No newline at end of file + cache.delete(cache_key) + +@receiver(post_save, sender=Course) +def sync_course_chat_locks(sender, instance, **kwargs): + """ + Automatically locks/unlocks the related chat rooms when the admin + toggles the chat locks on the Course page. + """ + # 1. Update the Group Chat + RoomMessage.objects.filter( + course=instance, + room_type=RoomMessage.RoomTypeChoices.GROUP + ).update(is_locked=instance.is_group_chat_locked) + + # 2. Update the Private Chats between the Professor and Students of this course + # Get all student IDs enrolled in this course + student_ids = instance.participants.values_list('student_id', flat=True) + + if student_ids: + RoomMessage.objects.filter( + room_type=RoomMessage.RoomTypeChoices.PRIVATE + ).filter( + # Find rooms where initiator is prof and recipient is student, OR vice versa + Q(initiator_id=instance.professor_id, recipient_id__in=student_ids) | + Q(initiator_id__in=student_ids, recipient_id=instance.professor_id) + ).update(is_locked=instance.is_professor_chat_locked) \ No newline at end of file diff --git a/templates/admin/chat/chatmessage/change_list.html b/templates/admin/chat/chatmessage/change_list.html deleted file mode 100644 index 8c217de..0000000 --- a/templates/admin/chat/chatmessage/change_list.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "admin/change_list.html" %} -{% load i18n admin_urls %} - -{% block object-tools-items %} - {{ block.super }} -
  • - - arrow_back - {% translate "Back to Chat Rooms" %} - -
  • -{% endblock %} \ No newline at end of file