Browse Source
feat(course): add live session models, admin and API metadata
feat(course): add live session models, admin and API metadata
- create CourseLiveSession, LiveSessionUser, LiveSessionRecording with indexes and unique constraints - register new models in admin and add UNFOLD navigation entries - update token validation to derive is_online and include livesession_started_at, livesession_ended_at, and can_start_online_class - extend online class entry flow documentation with new fields - add migration for new live session tablesmaster
8 changed files with 529 additions and 13 deletions
-
3apps/course/admin/__init__.py
-
87apps/course/admin/live_session.py
-
104apps/course/migrations/0008_auto_20251013_1724.py
-
3apps/course/models/__init__.py
-
171apps/course/models/live_session.py
-
56apps/course/views/course.py
-
43config/settings/base.py
-
75docs/online_class_entry_flow.md
@ -1,3 +1,4 @@ |
|||
from .course import * |
|||
from .lesson import * |
|||
from .participant import * |
|||
from .participant import * |
|||
from .live_session import * |
|||
@ -0,0 +1,87 @@ |
|||
from django.contrib import admin |
|||
from django.utils.translation import gettext_lazy as _ |
|||
|
|||
from unfold.admin import ModelAdmin |
|||
from unfold.contrib.filters.admin import ( |
|||
ChoicesDropdownFilter, |
|||
MultipleRelatedDropdownFilter, |
|||
RangeDateFilter, |
|||
) |
|||
|
|||
from utils.admin import project_admin_site |
|||
from apps.course.models import ( |
|||
CourseLiveSession, |
|||
LiveSessionRecording, |
|||
LiveSessionUser, |
|||
USER_ROLE_CHOICES, |
|||
RECORDING_TYPE_CHOICES, |
|||
) |
|||
|
|||
|
|||
class CourseLiveSessionAdmin(ModelAdmin): |
|||
list_display = ("subject", "course", "started_at", "ended_at", "created_at") |
|||
list_filter = ( |
|||
("course", MultipleRelatedDropdownFilter), |
|||
("started_at", RangeDateFilter), |
|||
) |
|||
search_fields = ("subject", "course__title") |
|||
ordering = ("-started_at",) |
|||
autocomplete_fields = ("course",) |
|||
readonly_fields = ("created_at", "updated_at") |
|||
fieldsets = ( |
|||
(None, {"fields": ("course", "subject", "started_at", "ended_at")}), |
|||
(_("Timestamps"), {"fields": ("created_at", "updated_at")}), |
|||
) |
|||
|
|||
|
|||
class LiveSessionUserAdmin(ModelAdmin): |
|||
list_display = ("user", "session", "role", "is_online", "entered_at", "exited_at") |
|||
list_filter = ( |
|||
("session", MultipleRelatedDropdownFilter), |
|||
("user", MultipleRelatedDropdownFilter), |
|||
("role", ChoicesDropdownFilter), |
|||
("entered_at", RangeDateFilter), |
|||
("is_online", admin.BooleanFieldListFilter), |
|||
) |
|||
search_fields = ( |
|||
"user__email", |
|||
"user__fullname", |
|||
"session__subject", |
|||
) |
|||
autocomplete_fields = ("user", "session") |
|||
readonly_fields = ("created_at", "updated_at") |
|||
fieldsets = ( |
|||
(None, {"fields": ("session", "user", "role")}), |
|||
(_("Session Timing"), {"fields": ("entered_at", "exited_at", "is_online")}), |
|||
(_("Timestamps"), {"fields": ("created_at", "updated_at")}), |
|||
) |
|||
|
|||
def get_role_choices(self, request): |
|||
return USER_ROLE_CHOICES |
|||
|
|||
|
|||
class LiveSessionRecordingAdmin(ModelAdmin): |
|||
list_display = ("title", "session", "recording_type", "is_active", "created_at") |
|||
list_filter = ( |
|||
("session", MultipleRelatedDropdownFilter), |
|||
("recording_type", ChoicesDropdownFilter), |
|||
("created_at", RangeDateFilter), |
|||
("is_active", admin.BooleanFieldListFilter), |
|||
) |
|||
search_fields = ("title", "session__subject", "session__course__title") |
|||
autocomplete_fields = ("session",) |
|||
readonly_fields = ("created_at", "updated_at") |
|||
fieldsets = ( |
|||
(None, {"fields": ("session", "title", "recording_type")}), |
|||
(_("Files"), {"fields": ("file", "file_time", "thumbnail")}), |
|||
(_("Status"), {"fields": ("is_active",)}), |
|||
(_("Timestamps"), {"fields": ("created_at", "updated_at")}), |
|||
) |
|||
|
|||
def get_recording_type_choices(self, request): |
|||
return RECORDING_TYPE_CHOICES |
|||
|
|||
|
|||
project_admin_site.register(CourseLiveSession, CourseLiveSessionAdmin) |
|||
project_admin_site.register(LiveSessionUser, LiveSessionUserAdmin) |
|||
project_admin_site.register(LiveSessionRecording, LiveSessionRecordingAdmin) |
|||
@ -0,0 +1,104 @@ |
|||
# Generated by Django 3.2.4 on 2025-10-13 17:24 |
|||
|
|||
from django.conf import settings |
|||
from django.db import migrations, models |
|||
import django.db.models.deletion |
|||
|
|||
|
|||
class Migration(migrations.Migration): |
|||
|
|||
dependencies = [ |
|||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
|||
('course', '0007_alter_course_thumbnail'), |
|||
] |
|||
|
|||
operations = [ |
|||
migrations.CreateModel( |
|||
name='CourseLiveSession', |
|||
fields=[ |
|||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|||
('subject', models.CharField(help_text='Topic of the live session.', max_length=255, verbose_name='Subject')), |
|||
('started_at', models.DateTimeField(help_text='Start time of the live session.', verbose_name='Started At')), |
|||
('ended_at', models.DateTimeField(blank=True, help_text='End time of the live session.', null=True, verbose_name='Ended At')), |
|||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), |
|||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), |
|||
('course', models.ForeignKey(help_text='Course that this live session belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='live_sessions', to='course.course', verbose_name='Course')), |
|||
], |
|||
options={ |
|||
'verbose_name': 'Course Live Session', |
|||
'verbose_name_plural': 'Course Live Sessions', |
|||
'ordering': ('-started_at', '-id'), |
|||
}, |
|||
), |
|||
migrations.CreateModel( |
|||
name='LiveSessionUser', |
|||
fields=[ |
|||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|||
('role', models.CharField(choices=[('participant', 'Participant'), ('moderator', 'Moderator'), ('observer', 'Observer')], help_text='Role of the user in the session', max_length=50, verbose_name='Role')), |
|||
('entered_at', models.DateTimeField(help_text='Time the user entered the session', verbose_name='Entered At')), |
|||
('exited_at', models.DateTimeField(blank=True, default=None, help_text='Time the user exited the session', null=True, verbose_name='Exited At')), |
|||
('is_online', models.BooleanField(default=True, help_text='Is the user currently online?', verbose_name='Is online')), |
|||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), |
|||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), |
|||
('session', models.ForeignKey(help_text='Live session that the user joined.', on_delete=django.db.models.deletion.CASCADE, related_name='user_sessions', to='course.courselivesession', verbose_name='Live Session')), |
|||
('user', models.ForeignKey(help_text='User participating in the live session.', on_delete=django.db.models.deletion.CASCADE, related_name='live_session_entries', to=settings.AUTH_USER_MODEL, verbose_name='User')), |
|||
], |
|||
options={ |
|||
'verbose_name': 'User Session', |
|||
'verbose_name_plural': 'User Sessions', |
|||
'ordering': ('-entered_at', '-id'), |
|||
}, |
|||
), |
|||
migrations.CreateModel( |
|||
name='LiveSessionRecording', |
|||
fields=[ |
|||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|||
('title', models.CharField(help_text='Title of the recording', max_length=255, verbose_name='Title')), |
|||
('file', models.FileField(help_text='File of the recorded session', upload_to='recorded_sessions/', verbose_name='Recording File')), |
|||
('file_time', models.DurationField(blank=True, help_text='Duration of the recording file', null=True, verbose_name='File Duration')), |
|||
('recording_type', models.CharField(choices=[('voice', 'Voice'), ('video', 'Video')], help_text='Type of the recording (voice or video)', max_length=10, verbose_name='Recording Type')), |
|||
('thumbnail', models.ImageField(blank=True, help_text='Thumbnail image for video recordings', null=True, upload_to='recording_thumbnails/', verbose_name='Thumbnail')), |
|||
('created_at', models.DateTimeField(auto_now_add=True, help_text='Time the recording was created', verbose_name='Created At')), |
|||
('updated_at', models.DateTimeField(auto_now=True, help_text='The datetime when the recording was last updated', verbose_name='Updated At')), |
|||
('is_active', models.BooleanField(default=True, help_text='Whether this recording is active or not', verbose_name='Is Active')), |
|||
('session', models.ForeignKey(help_text='Live session that this recording belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='recordings', to='course.courselivesession', verbose_name='Live Session')), |
|||
], |
|||
options={ |
|||
'verbose_name': 'Live Session Recording', |
|||
'verbose_name_plural': 'Live Session Recordings', |
|||
'ordering': ('-created_at', '-id'), |
|||
}, |
|||
), |
|||
migrations.AddIndex( |
|||
model_name='livesessionuser', |
|||
index=models.Index(fields=['session', 'user'], name='course_live_session_b1eaa5_idx'), |
|||
), |
|||
migrations.AddIndex( |
|||
model_name='livesessionuser', |
|||
index=models.Index(fields=['session', 'is_online'], name='course_live_session_5ef9bc_idx'), |
|||
), |
|||
migrations.AddIndex( |
|||
model_name='livesessionuser', |
|||
index=models.Index(fields=['user', 'is_online'], name='course_live_user_id_384830_idx'), |
|||
), |
|||
migrations.AlterUniqueTogether( |
|||
name='livesessionuser', |
|||
unique_together={('session', 'user', 'entered_at')}, |
|||
), |
|||
migrations.AddIndex( |
|||
model_name='livesessionrecording', |
|||
index=models.Index(fields=['session', 'is_active'], name='course_live_session_f35db0_idx'), |
|||
), |
|||
migrations.AddIndex( |
|||
model_name='livesessionrecording', |
|||
index=models.Index(fields=['session', 'recording_type'], name='course_live_session_84b2bf_idx'), |
|||
), |
|||
migrations.AddIndex( |
|||
model_name='courselivesession', |
|||
index=models.Index(fields=['course', 'started_at'], name='course_cour_course__b8968b_idx'), |
|||
), |
|||
migrations.AddIndex( |
|||
model_name='courselivesession', |
|||
index=models.Index(fields=['course', 'created_at'], name='course_cour_course__142085_idx'), |
|||
), |
|||
] |
|||
@ -1,3 +1,4 @@ |
|||
from .course import * |
|||
from .lesson import * |
|||
from .participant import * |
|||
from .participant import * |
|||
from .live_session import * |
|||
@ -0,0 +1,171 @@ |
|||
from django.db import models |
|||
from django.utils.translation import gettext_lazy as _ |
|||
|
|||
from .course import Course |
|||
from apps.account.models import User |
|||
|
|||
|
|||
class CourseLiveSession(models.Model): |
|||
course = models.ForeignKey( |
|||
Course, |
|||
on_delete=models.CASCADE, |
|||
related_name="live_sessions", |
|||
verbose_name=_("Course"), |
|||
help_text=_("Course that this live session belongs to."), |
|||
) |
|||
subject = models.CharField( |
|||
max_length=255, |
|||
verbose_name=_("Subject"), |
|||
help_text=_("Topic of the live session."), |
|||
) |
|||
started_at = models.DateTimeField( |
|||
verbose_name=_("Started At"), |
|||
help_text=_("Start time of the live session."), |
|||
) |
|||
ended_at = models.DateTimeField( |
|||
verbose_name=_("Ended At"), |
|||
help_text=_("End time of the live session."), |
|||
null=True, |
|||
blank=True, |
|||
) |
|||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) |
|||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) |
|||
|
|||
def __str__(self): |
|||
return f"{self.course} - {self.subject}" |
|||
|
|||
class Meta: |
|||
ordering = ("-started_at", "-id") |
|||
verbose_name = _("Course Live Session") |
|||
verbose_name_plural = _("Course Live Sessions") |
|||
indexes = [ |
|||
models.Index(fields=["course", "started_at"]), |
|||
models.Index(fields=["course", "created_at"]), |
|||
] |
|||
|
|||
|
|||
USER_ROLE_CHOICES = ( |
|||
("participant", "Participant"), |
|||
("moderator", "Moderator"), |
|||
("observer", "Observer"), |
|||
) |
|||
|
|||
|
|||
class LiveSessionUser(models.Model): |
|||
session = models.ForeignKey( |
|||
CourseLiveSession, |
|||
on_delete=models.CASCADE, |
|||
related_name="user_sessions", |
|||
verbose_name=_("Live Session"), |
|||
help_text=_("Live session that the user joined."), |
|||
) |
|||
user = models.ForeignKey( |
|||
User, |
|||
on_delete=models.CASCADE, |
|||
related_name="live_session_entries", |
|||
verbose_name=_("User"), |
|||
help_text=_("User participating in the live session."), |
|||
) |
|||
role = models.CharField( |
|||
max_length=50, |
|||
choices=USER_ROLE_CHOICES, |
|||
verbose_name=_("Role"), |
|||
help_text=_("Role of the user in the session"), |
|||
) |
|||
entered_at = models.DateTimeField( |
|||
verbose_name=_("Entered At"), |
|||
help_text=_("Time the user entered the session"), |
|||
) |
|||
exited_at = models.DateTimeField( |
|||
verbose_name=_("Exited At"), |
|||
help_text=_("Time the user exited the session"), |
|||
null=True, |
|||
blank=True, |
|||
default=None, |
|||
) |
|||
is_online = models.BooleanField( |
|||
default=True, |
|||
verbose_name=_("Is online"), |
|||
help_text=_("Is the user currently online?"), |
|||
) |
|||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) |
|||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) |
|||
|
|||
def __str__(self): |
|||
return f"{self.user} @ {self.session}" |
|||
|
|||
class Meta: |
|||
verbose_name = _("User Session") |
|||
verbose_name_plural = _("User Sessions") |
|||
ordering = ("-entered_at", "-id") |
|||
indexes = [ |
|||
models.Index(fields=["session", "user"]), |
|||
models.Index(fields=["session", "is_online"]), |
|||
models.Index(fields=["user", "is_online"]), |
|||
] |
|||
unique_together = ("session", "user", "entered_at") |
|||
|
|||
|
|||
RECORDING_TYPE_CHOICES = ( |
|||
("voice", "Voice"), |
|||
("video", "Video"), |
|||
) |
|||
|
|||
|
|||
class LiveSessionRecording(models.Model): |
|||
session = models.ForeignKey( |
|||
CourseLiveSession, |
|||
on_delete=models.CASCADE, |
|||
related_name="recordings", |
|||
verbose_name=_("Live Session"), |
|||
help_text=_("Live session that this recording belongs to."), |
|||
) |
|||
title = models.CharField( |
|||
max_length=255, |
|||
verbose_name=_("Title"), |
|||
help_text=_("Title of the recording"), |
|||
) |
|||
file = models.FileField( |
|||
upload_to="recorded_sessions/", |
|||
verbose_name=_("Recording File"), |
|||
help_text=_("File of the recorded session"), |
|||
) |
|||
file_time = models.DurationField( |
|||
verbose_name=_("File Duration"), |
|||
help_text=_("Duration of the recording file"), |
|||
null=True, |
|||
blank=True, |
|||
) |
|||
recording_type = models.CharField( |
|||
max_length=10, |
|||
choices=RECORDING_TYPE_CHOICES, |
|||
verbose_name=_("Recording Type"), |
|||
help_text=_("Type of the recording (voice or video)"), |
|||
) |
|||
thumbnail = models.ImageField( |
|||
upload_to="recording_thumbnails/", |
|||
verbose_name=_("Thumbnail"), |
|||
help_text=_("Thumbnail image for video recordings"), |
|||
null=True, |
|||
blank=True, |
|||
) |
|||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"), help_text=_("Time the recording was created")) |
|||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"), help_text=_("The datetime when the recording was last updated")) |
|||
is_active = models.BooleanField( |
|||
default=True, |
|||
verbose_name=_("Is Active"), |
|||
help_text=_("Whether this recording is active or not"), |
|||
) |
|||
|
|||
def __str__(self): |
|||
meet_id = getattr(self.session, "meet_id", self.session_id) |
|||
return f"meet:<{meet_id}><{self.id}>{self.title} - {self.recording_type}" |
|||
|
|||
class Meta: |
|||
verbose_name = _("Live Session Recording") |
|||
verbose_name_plural = _("Live Session Recordings") |
|||
ordering = ("-created_at", "-id") |
|||
indexes = [ |
|||
models.Index(fields=["session", "is_active"]), |
|||
models.Index(fields=["session", "recording_type"]), |
|||
] |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue