feat: LTI support and Moodle plugin

This commit is contained in:
Markos Gogoulos
2026-05-11 12:47:09 +03:00
committed by GitHub
parent b7427869b6
commit 55ab7ff34f
307 changed files with 19966 additions and 3748 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ from .category import Category, Tag # noqa: F401
from .comment import Comment # noqa: F401
from .encoding import EncodeProfile, Encoding # noqa: F401
from .license import License # noqa: F401
from .media import Media, MediaPermission # noqa: F401
from .media import EmbedMediaCourse, Media, MediaPermission # noqa: F401
from .page import Page, TinyMCEMedia # noqa: F401
from .playlist import Playlist, PlaylistMedia # noqa: F401
from .rating import Rating, RatingCategory # noqa: F401
+8 -1
View File
@@ -47,6 +47,13 @@ class Category(models.Model):
verbose_name='IDP Config Name',
)
# LTI/LMS integration fields
is_lms_course = models.BooleanField(default=False, db_index=True, help_text='Whether this category represents an LMS course')
lti_platform = models.ForeignKey('lti.LTIPlatform', blank=True, null=True, on_delete=models.SET_NULL, related_name='categories', help_text='LTI Platform if this is an LTI course')
lti_context_id = models.CharField(max_length=255, blank=True, db_index=True, help_text='LTI context ID from platform')
def __str__(self):
return self.title
@@ -137,7 +144,7 @@ class Tag(models.Model):
return True
def save(self, *args, **kwargs):
self.title = helpers.get_alphanumeric_only(self.title)
self.title = helpers.get_alphanumeric_and_spaces(self.title)
self.title = self.title[:100]
super(Tag, self).save(*args, **kwargs)
+39 -20
View File
@@ -352,20 +352,11 @@ class Media(models.Model):
# first get anything interesting out of the media
# that needs to be search able
a_tags = b_tags = ""
a_tags = ""
if self.id:
a_tags = " ".join([tag.title for tag in self.tags.all()])
b_tags = " ".join([tag.title.replace("-", " ") for tag in self.tags.all()])
items = [
self.title,
self.user.username,
self.user.email,
self.user.name,
self.description,
a_tags,
b_tags,
]
items = [self.friendly_token, self.title, self.user.username, self.user.email, self.user.name, self.description, a_tags]
for subtitle in self.subtitles.all():
items.append(subtitle.subtitle_text)
@@ -739,15 +730,6 @@ class Media(models.Model):
ep["updated_time"] = encoding.update_date
return ep
@property
def categories_info(self):
"""Property used on serializers"""
ret = []
for cat in self.category.all():
ret.append({"title": cat.title, "url": cat.get_absolute_url()})
return ret
@property
def tags_info(self):
"""Property used on serializers"""
@@ -974,6 +956,12 @@ class Media(models.Model):
return chapter_data.chapter_data
return data
@property
def is_shared(self):
if not self.pk:
return False
return self.permissions.exists() or self.category.filter(is_rbac_category=True).exists()
class MediaPermission(models.Model):
"""Model to store user permissions for media"""
@@ -984,10 +972,18 @@ class MediaPermission(models.Model):
("owner", "Owner"),
)
SOURCE_LTI_EMBED = 'lti_embed'
SOURCE_EXPLICIT = 'explicit'
SOURCE_CHOICES = (
(SOURCE_LTI_EMBED, 'LTI Embed'),
(SOURCE_EXPLICIT, 'Explicit'),
)
owner_user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='granted_permissions')
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='permissions')
permission = models.CharField(max_length=20, choices=PERMISSION_CHOICES)
source = models.CharField(max_length=32, choices=SOURCE_CHOICES, default=SOURCE_EXPLICIT)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
@@ -997,6 +993,29 @@ class MediaPermission(models.Model):
return f"{self.user.username} - {self.media.title} ({self.permission})"
class EmbedMediaCourse(models.Model):
"""
Records that a user shared a media item into a course during an LTI session.
This is a pure audit/tracking table used by the course cleanup bulk action to
identify which MediaPermission records were created via LTI embedding and should
be removed when the course is cleaned up.
It does NOT add the media to the category (Media.category M2M is untouched),
so no m2m_changed signals fire and no category counts are affected.
"""
media = models.ForeignKey('Media', on_delete=models.CASCADE, related_name='embed_courses')
category = models.ForeignKey('Category', on_delete=models.CASCADE, related_name='embedded_media')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('media', 'category')
def __str__(self):
return f"{self.media.title} in {self.category.title}"
@receiver(post_save, sender=Media)
def media_save(sender, instance, created, **kwargs):
# media_file path is not set correctly until mode is saved
+15 -6
View File
@@ -18,13 +18,22 @@ class VideoChapterData(models.Model):
data = []
if self.data and isinstance(self.data, list):
for item in self.data:
if item.get("startTime") and item.get("endTime") and item.get("chapterTitle"):
chapter_item = {
'startTime': item.get("startTime"),
'endTime': item.get("endTime"),
'chapterTitle': item.get("chapterTitle"),
if not isinstance(item, dict):
continue
start_time = item.get("startTime")
end_time = item.get("endTime")
chapter_title = item.get("chapterTitle")
if start_time is None or end_time is None or not chapter_title:
continue
if not isinstance(start_time, (int, float, str)) or not isinstance(end_time, (int, float, str)):
continue
data.append(
{
'startTime': start_time,
'endTime': end_time,
'chapterTitle': chapter_title,
}
data.append(chapter_item)
)
return data