Browse Source

cron job codes added for checking professors attendence and auto close room

master
Mohsen Taba 1 month ago
parent
commit
d3d4b2d036
  1. 127
      apps/course/management/commands/auto_close_professorless_live_sessions.py
  2. 38
      apps/course/migrations/0013_courselivesession_auto_close_fields.py
  3. 13
      apps/course/models/live_session.py
  4. 3
      apps/course/services/plugnmeet.py
  5. 62
      apps/course/tests/test_webhook_live_session.py
  6. 66
      apps/course/views/webhook.py

127
apps/course/management/commands/auto_close_professorless_live_sessions.py

@ -0,0 +1,127 @@
import logging
from django.core.management.base import BaseCommand
from django.db import transaction
from django.utils import timezone
from apps.course.models import CourseLiveSession, LiveSessionUser
from apps.course.services import PlugNMeetClient, PlugNMeetError
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = (
"Auto-close live sessions when no moderator has been present for the configured timeout "
"while other participants are still online."
)
def handle(self, *args, **options):
now = timezone.now()
sessions = CourseLiveSession.objects.filter(
ended_at__isnull=True,
auto_close_after_moderator_exit_at__isnull=False,
auto_close_after_moderator_exit_at__lte=now,
).select_related('course')
processed = 0
closed = 0
skipped = 0
for session in sessions:
processed += 1
outcome = self._process_session(session, now)
if outcome == 'closed':
closed += 1
else:
skipped += 1
self.stdout.write(
self.style.SUCCESS(
f"Processed {processed} session(s); closed {closed}; skipped {skipped}."
)
)
def _process_session(self, session: CourseLiveSession, now):
has_moderator_online = LiveSessionUser.objects.filter(
session=session,
role='moderator',
is_online=True,
).exists()
if has_moderator_online:
self._clear_pending_auto_close(session)
logger.info(
"[Auto Close Professorless] Moderator returned - clearing pending close for session_id=%s",
session.id,
)
return 'skipped'
has_any_online_users = LiveSessionUser.objects.filter(
session=session,
is_online=True,
).exists()
if not has_any_online_users:
self._clear_pending_auto_close(session)
logger.info(
"[Auto Close Professorless] No online users remain - deferring to empty timeout for session_id=%s",
session.id,
)
return 'skipped'
try:
client = PlugNMeetClient()
client.end_room(session.room_id)
logger.info(
"[Auto Close Professorless] Ended room via PlugNMeet - session_id=%s room_id=%s",
session.id,
session.room_id,
)
except PlugNMeetError as exc:
logger.warning(
"[Auto Close Professorless] PlugNMeet end_room failed - session_id=%s room_id=%s error=%s",
session.id,
session.room_id,
str(exc),
)
except Exception as exc:
logger.warning(
"[Auto Close Professorless] Unexpected end_room error - session_id=%s room_id=%s error=%s",
session.id,
session.room_id,
str(exc),
)
self._close_session_locally(session, now)
return 'closed'
@staticmethod
def _close_session_locally(session: CourseLiveSession, now):
with transaction.atomic():
updated = CourseLiveSession.objects.filter(
pk=session.pk,
ended_at__isnull=True,
).update(
ended_at=now,
last_moderator_left_at=None,
auto_close_after_moderator_exit_at=None,
updated_at=now,
)
if not updated:
return
LiveSessionUser.objects.filter(
session=session,
is_online=True,
).update(
is_online=False,
exited_at=now,
updated_at=now,
)
@staticmethod
def _clear_pending_auto_close(session: CourseLiveSession):
CourseLiveSession.objects.filter(pk=session.pk).update(
last_moderator_left_at=None,
auto_close_after_moderator_exit_at=None,
updated_at=timezone.now(),
)

38
apps/course/migrations/0013_courselivesession_auto_close_fields.py

@ -0,0 +1,38 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("course", "0012_courselivesession_recording_title"),
]
operations = [
migrations.AddField(
model_name="courselivesession",
name="auto_close_after_moderator_exit_at",
field=models.DateTimeField(
blank=True,
help_text="When the session should be auto-closed if no moderator returns.",
null=True,
verbose_name="Auto Close After Moderator Exit At",
),
),
migrations.AddField(
model_name="courselivesession",
name="last_moderator_left_at",
field=models.DateTimeField(
blank=True,
help_text="Timestamp when the last moderator left the live session.",
null=True,
verbose_name="Last Moderator Left At",
),
),
migrations.AddIndex(
model_name="courselivesession",
index=models.Index(
fields=["ended_at", "auto_close_after_moderator_exit_at"],
name="course_cour_ended_a_0f47b4_idx",
),
),
]

13
apps/course/models/live_session.py

@ -36,6 +36,18 @@ class CourseLiveSession(models.Model):
null=True,
blank=True,
)
last_moderator_left_at = models.DateTimeField(
verbose_name=_("Last Moderator Left At"),
help_text=_("Timestamp when the last moderator left the live session."),
null=True,
blank=True,
)
auto_close_after_moderator_exit_at = models.DateTimeField(
verbose_name=_("Auto Close After Moderator Exit At"),
help_text=_("When the session should be auto-closed if no moderator returns."),
null=True,
blank=True,
)
recording_title = models.CharField(
max_length=255,
verbose_name=_("Recording Title"),
@ -64,6 +76,7 @@ class CourseLiveSession(models.Model):
models.Index(fields=["course", "started_at"]),
models.Index(fields=["course", "created_at"]),
models.Index(fields=["room_id"]),
models.Index(fields=["ended_at", "auto_close_after_moderator_exit_at"]),
]

3
apps/course/services/plugnmeet.py

@ -40,6 +40,9 @@ class PlugNMeetClient:
def is_room_active(self, room_id: str) -> Dict[str, Any]:
return self._post("/auth/room/isRoomActive", {"roomId": room_id})
def end_room(self, room_id: str) -> Dict[str, Any]:
return self._post("/auth/room/endRoom", {"roomId": room_id})
def get_recording_info(self, record_id: str) -> Dict[str, Any]:
"""Get detailed information about a recording."""
return self._post("/auth/recording/recordingInfo", {"recordId": record_id})

62
apps/course/tests/test_webhook_live_session.py

@ -2,7 +2,10 @@ import hashlib
import hmac
import json
import tempfile
from datetime import timedelta
from unittest import mock
from django.core.management import call_command
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import override_settings
from django.urls import reverse
@ -137,3 +140,62 @@ class PlugNMeetWebhookLiveSessionTests(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
second_session.refresh_from_db()
self.assertIsNotNone(second_session.ended_at)
def test_last_moderator_leave_schedules_auto_close(self):
LiveSessionUser.objects.create(
session=self.session,
user=self.professor,
role='moderator',
entered_at=timezone.now(),
is_online=True,
)
response = self._post_webhook(
{
'event': 'participant_left',
'room': {
'name': self.session.room_id,
},
'participant': {
'identity': str(self.professor.id),
},
}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.session.refresh_from_db()
self.assertIsNotNone(self.session.last_moderator_left_at)
self.assertIsNotNone(self.session.auto_close_after_moderator_exit_at)
@mock.patch('apps.course.management.commands.auto_close_professorless_live_sessions.PlugNMeetClient')
def test_auto_close_command_closes_professorless_session(self, mock_client_cls):
LiveSessionUser.objects.create(
session=self.session,
user=self.professor,
role='moderator',
entered_at=timezone.now() - timedelta(minutes=20),
exited_at=timezone.now() - timedelta(minutes=15),
is_online=False,
)
self.session.last_moderator_left_at = timezone.now() - timedelta(minutes=15)
self.session.auto_close_after_moderator_exit_at = timezone.now() - timedelta(minutes=5)
self.session.save(
update_fields=[
'last_moderator_left_at',
'auto_close_after_moderator_exit_at',
'updated_at',
]
)
mock_client = mock_client_cls.return_value
mock_client.end_room.return_value = {'status': True, 'msg': 'room ended successfully'}
call_command('auto_close_professorless_live_sessions')
self.session.refresh_from_db()
self.assertIsNotNone(self.session.ended_at)
self.assertIsNone(self.session.last_moderator_left_at)
self.assertIsNone(self.session.auto_close_after_moderator_exit_at)
self.assertFalse(
LiveSessionUser.objects.get(session=self.session, user=self.student).is_online
)

66
apps/course/views/webhook.py

@ -8,6 +8,7 @@ import subprocess
import base64
import jwt
from typing import Dict, Any, Optional
from datetime import timedelta
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
@ -31,6 +32,12 @@ from utils.exceptions import AppAPIException
logger = logging.getLogger(__name__)
AUTO_CLOSE_AFTER_MODERATOR_EXIT_MINUTES = getattr(
settings,
"ONLINE_CLASS_AUTO_CLOSE_AFTER_MODERATOR_EXIT_MINUTES",
10,
)
class RawJSONParser(BaseParser):
"""
Parser that preserves the raw body bytes for HMAC signature verification.
@ -251,6 +258,8 @@ class PlugNMeetWebhookAPIView(APIView):
'entered_at': timezone.now()
}
)
if role == 'moderator':
self._clear_pending_auto_close(session)
logger.info(f"👤 [PlugNMeet Webhook] User {user.id} joined session {session.id} (created: {created})")
return {'session_user_id': session_user.id, 'created': created}
except Exception as e:
@ -278,6 +287,9 @@ class PlugNMeetWebhookAPIView(APIView):
updated = LiveSessionUser.objects.filter(
session=session, user=user, is_online=True
).update(is_online=False, exited_at=timezone.now(), updated_at=timezone.now())
if updated:
self._update_auto_close_state_after_leave(session, user)
logger.info(f"🚪 [PlugNMeet Webhook] User {user.id} left session {session.id} (updated entries count: {updated})")
return {'updated': bool(updated)}
@ -411,3 +423,57 @@ class PlugNMeetWebhookAPIView(APIView):
except Exception as e:
logger.warning(f"⚠️ [PlugNMeet Webhook] Thumbnail failed: {e}")
return False
def _update_auto_close_state_after_leave(self, session: CourseLiveSession, user: User) -> None:
if session.ended_at or not user.can_manage_course(session.course):
return
has_other_moderator_online = LiveSessionUser.objects.filter(
session=session,
role='moderator',
is_online=True,
).exists()
if has_other_moderator_online:
self._clear_pending_auto_close(session)
return
has_non_moderator_online = LiveSessionUser.objects.filter(
session=session,
is_online=True,
).exclude(role='moderator').exists()
if not has_non_moderator_online:
self._clear_pending_auto_close(session)
return
now = timezone.now()
due_at = now + timedelta(minutes=AUTO_CLOSE_AFTER_MODERATOR_EXIT_MINUTES)
session.last_moderator_left_at = now
session.auto_close_after_moderator_exit_at = due_at
session.save(
update_fields=[
'last_moderator_left_at',
'auto_close_after_moderator_exit_at',
'updated_at',
]
)
logger.info(
"⏳ [PlugNMeet Webhook] Auto-close countdown scheduled - session_id=%s room_id=%s due_at=%s",
session.id,
session.room_id,
due_at.isoformat(),
)
@staticmethod
def _clear_pending_auto_close(session: CourseLiveSession) -> None:
if not session.last_moderator_left_at and not session.auto_close_after_moderator_exit_at:
return
session.last_moderator_left_at = None
session.auto_close_after_moderator_exit_at = None
session.save(
update_fields=[
'last_moderator_left_at',
'auto_close_after_moderator_exit_at',
'updated_at',
]
)
Loading…
Cancel
Save