6 changed files with 309 additions and 0 deletions
-
127apps/course/management/commands/auto_close_professorless_live_sessions.py
-
38apps/course/migrations/0013_courselivesession_auto_close_fields.py
-
13apps/course/models/live_session.py
-
3apps/course/services/plugnmeet.py
-
62apps/course/tests/test_webhook_live_session.py
-
66apps/course/views/webhook.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(), |
|||
) |
|||
@ -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", |
|||
), |
|||
), |
|||
] |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue