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.
 
 
 
 

305 lines
12 KiB

import logging
from django.utils import timezone
from apps.course.models import Course
from apps.chat.models import RoomMessage
from apps.course.models.course import Course, extract_text_from_json
from apps.course.models.lesson import CourseLesson
from apps.course.models.live_session import LiveSessionRecording
from django.db.models import Q
logger = logging.getLogger(__name__)
from django.db.models.signals import post_save, post_delete, pre_save, m2m_changed
from django.dispatch import receiver
from django.core.cache import cache
from django.contrib.auth import get_user_model
UserModel = get_user_model()
def sync_course_group_room(instance):
title_str = extract_text_from_json(instance.title)
first_professor = instance.professors.first()
if first_professor:
# Try to get an existing group room; if multiple exist, pick the first.
try:
room, created = RoomMessage.objects.get_or_create(
course=instance,
room_type=RoomMessage.RoomTypeChoices.GROUP,
defaults={
"name": f"{title_str} - Group",
"description": f"Group chat for course: {title_str}",
"initiator": first_professor,
},
)
except RoomMessage.MultipleObjectsReturned:
# Fallback: get the earliest room and update it
room = RoomMessage.objects.filter(
course=instance,
room_type=RoomMessage.RoomTypeChoices.GROUP,
).order_by("id").first()
created = False
if not created:
# Update the existing room with latest info
RoomMessage.objects.filter(id=room.id).update(
name=f"{title_str} - Group",
description=f"Group chat for course: {title_str}",
initiator=first_professor,
)
else:
# No professor yet – just ensure the group room exists without initiator
RoomMessage.objects.filter(
course=instance,
room_type=RoomMessage.RoomTypeChoices.GROUP,
).update(
name=f"{title_str} - Group",
description=f"Group chat for course: {title_str}",
)
@receiver(post_save, sender=Course)
def handle_room_message_for_course(sender, instance, created, **kwargs):
title_str = extract_text_from_json(instance.title)
first_professor = instance.professors.first()
if created and not first_professor:
return
if created: # فقط برای موارد جدید اجرا شود
RoomMessage.objects.create(
name=f"{title_str} - Group",
description=f"Group chat for course: {title_str}",
initiator=first_professor, # استاد اول به‌عنوان سازنده اتاق
course=instance,
room_type=RoomMessage.RoomTypeChoices.GROUP
)
else: # این بخش در زمان آپدیت دوره اجرا می‌شود
# Find the existing group room for this course and update its details
update_kwargs = {
'name': f"{title_str} - Group",
'description': f"Group chat for course: {title_str}",
}
if first_professor:
update_kwargs['initiator'] = first_professor
RoomMessage.objects.filter(
course=instance,
room_type=RoomMessage.RoomTypeChoices.GROUP
).update(**update_kwargs)
@receiver(m2m_changed, sender=Course.professors.through)
def handle_course_professors_changed(sender, instance, action, pk_set, **kwargs):
if action not in {"post_add", "post_remove", "post_clear"}:
return
sync_course_group_room(instance)
for professor in instance.professors.all():
professor.ensure_professor_profile()
detail_cache_key = f"professor_detail_{professor.slug}"
cache.delete(detail_cache_key)
@receiver(post_save, sender=Course)
def ensure_professor_role(sender, instance, **kwargs):
for professor in instance.professors.all():
professor.ensure_professor_profile()
@receiver([post_save, post_delete], sender=Course)
def invalidate_professor_course_cache(sender, instance, **kwargs):
"""
Clears the cached professor detail page AND their course list
whenever a course assigned to them is created, updated, or deleted.
"""
for professor in instance.professors.all():
detail_cache_key = f"professor_detail_{professor.slug}"
cache.delete(detail_cache_key)
# Optional: If you update a Professor's profile in the admin directly
@receiver([post_save, post_delete], sender=UserModel)
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)
@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 Professors and Students of this course
# Get all student IDs enrolled in this course
student_ids = instance.participants.values_list('student_id', flat=True)
professor_ids = list(instance.professors.values_list('id', flat=True))
if student_ids and professor_ids:
RoomMessage.objects.filter(
room_type=RoomMessage.RoomTypeChoices.PRIVATE
).filter(
# Find rooms where initiator is any professor and recipient is student, OR vice versa
Q(initiator_id__in=professor_ids, recipient_id__in=student_ids) |
Q(initiator_id__in=student_ids, recipient_id__in=professor_ids)
).update(is_locked=instance.is_professor_chat_locked)
def _recalculate_course_lessons_count(course_id):
Course.recalculate_lessons_count_for_course(course_id)
def _sync_quiz_lesson_numbers(course_id):
if not course_id:
return
from apps.quiz.models import Quiz
from apps.course.models import CourseLesson
active_lessons_count = CourseLesson.objects.filter(
course_id=course_id,
is_active=True
).filter(
Q(chapter__isnull=True) | Q(chapter__is_active=True)
).count()
# Update quizzes where lesson_number > active_lessons_count to 0
Quiz.objects.filter(
course_id=course_id,
lesson_number__gt=active_lessons_count
).update(lesson_number=0)
@receiver(pre_save, sender=CourseLesson)
def capture_previous_course_lesson_state(sender, instance, **kwargs):
if not instance.pk:
instance._previous_course_id = None
instance._previous_is_active = None
return
previous = sender.objects.filter(pk=instance.pk).values('course_id', 'is_active').first()
if previous:
instance._previous_course_id = previous['course_id']
instance._previous_is_active = previous['is_active']
else:
instance._previous_course_id = None
instance._previous_is_active = None
@receiver(post_save, sender=CourseLesson)
def sync_lessons_count_on_course_lesson_save(sender, instance, **kwargs):
affected_course_ids = {instance.course_id}
previous_course_id = getattr(instance, '_previous_course_id', None)
previous_is_active = getattr(instance, '_previous_is_active', None)
if previous_course_id and previous_course_id != instance.course_id:
affected_course_ids.add(previous_course_id)
if previous_is_active is not None and previous_is_active != instance.is_active and previous_course_id:
affected_course_ids.add(previous_course_id)
for course_id in affected_course_ids:
_recalculate_course_lessons_count(course_id)
_sync_quiz_lesson_numbers(course_id)
@receiver(post_delete, sender=CourseLesson)
def sync_lessons_count_on_course_lesson_delete(sender, instance, **kwargs):
_recalculate_course_lessons_count(instance.course_id)
_sync_quiz_lesson_numbers(instance.course_id)
@receiver(post_save, sender=LiveSessionRecording)
def create_course_lesson_from_recording(sender, instance, **kwargs):
"""
Automatically creates a CourseChapter (if not exists) and CourseLesson
whenever a LiveSessionRecording is saved with a valid file.
"""
if not instance.file:
return
# To avoid duplicate creation if save is called multiple times
from apps.course.models.lesson import Lesson, CourseChapter, CourseLesson
if Lesson.objects.filter(content_file=instance.file.name).exists():
return
session = instance.session
course = session.course
# 1. Determine chapter title as recording date (YYYY-MM-DD)
date_val = instance.created_at or timezone.now()
recording_date_str = date_val.strftime('%Y-%m-%d')
# 2. Get or create CourseChapter for this date
from apps.course.models.course import extract_text_from_json
chapter = None
for ch in CourseChapter.objects.filter(course=course):
if extract_text_from_json(ch.title) == recording_date_str:
chapter = ch
break
if not chapter:
chapter = CourseChapter.objects.create(
course=course,
title=recording_date_str,
is_active=True
)
# 3. Determine lesson title: use custom title or subject (with parts if multiple recordings exist)
base_title = session.recording_title or session.subject
# Get all recordings for this session, ordered by creation
recordings = list(LiveSessionRecording.objects.filter(session=session).order_by('created_at', 'id'))
recordings_count = len(recordings)
if recordings_count <= 1:
lesson_title = base_title
else:
try:
current_index = recordings.index(instance) + 1
except ValueError:
current_index = recordings_count
lesson_title = f"{base_title} part {current_index}"
# Retroactively rename the first lesson if this is a subsequent recording
if current_index > 1:
first_recording = recordings[0]
if first_recording.file:
first_lesson = Lesson.objects.filter(content_file=first_recording.file.name).first()
if first_lesson:
first_lesson_title = f"{base_title} part 1"
first_lesson.title = first_lesson_title
first_lesson.save(update_fields=['title'])
# Also update corresponding CourseLesson
CourseLesson.objects.filter(lesson=first_lesson).update(title=first_lesson_title)
# 4. Calculate duration in minutes
duration_minutes = 0
if instance.file_time:
duration_minutes = int(instance.file_time.total_seconds() / 60)
if duration_minutes == 0 and instance.file_time.total_seconds() > 0:
duration_minutes = 1
# 5. Create the Lesson object
lesson = Lesson.objects.create(
title=lesson_title,
content_type="video_file",
content_file=instance.file,
duration=duration_minutes
)
# 6. Create the CourseLesson object linking it to the chapter
course_lesson = CourseLesson.objects.create(
course=course,
chapter=chapter,
lesson=lesson,
title=lesson_title,
is_active=True
)
logger.info(f"✨ [Signal] Automatically created CourseLesson {course_lesson.id} ({lesson_title}) for course {course.id} from recording {instance.id}")