mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-06-06 17:13:02 -04:00
feat: LTI support and Moodle plugin
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user