mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-04-30 18:36:13 -04:00
course remove
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2026-04-27 17:43
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('files', '0017_mediapermission_source'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EmbedMediaCourse',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='embedded_media', to='files.category')),
|
||||||
|
('media', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='embed_courses', to='files.media')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('media', 'category')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -3,7 +3,7 @@ from .category import Category, Tag # noqa: F401
|
|||||||
from .comment import Comment # noqa: F401
|
from .comment import Comment # noqa: F401
|
||||||
from .encoding import EncodeProfile, Encoding # noqa: F401
|
from .encoding import EncodeProfile, Encoding # noqa: F401
|
||||||
from .license import License # 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 .page import Page, TinyMCEMedia # noqa: F401
|
||||||
from .playlist import Playlist, PlaylistMedia # noqa: F401
|
from .playlist import Playlist, PlaylistMedia # noqa: F401
|
||||||
from .rating import Rating, RatingCategory # noqa: F401
|
from .rating import Rating, RatingCategory # noqa: F401
|
||||||
|
|||||||
@@ -1002,6 +1002,29 @@ class MediaPermission(models.Model):
|
|||||||
return f"{self.user.username} - {self.media.title} ({self.permission})"
|
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)
|
@receiver(post_save, sender=Media)
|
||||||
def media_save(sender, instance, created, **kwargs):
|
def media_save(sender, instance, created, **kwargs):
|
||||||
# media_file path is not set correctly until mode is saved
|
# media_file path is not set correctly until mode is saved
|
||||||
|
|||||||
+29
-11
@@ -36,6 +36,7 @@ from ..methods import (
|
|||||||
from ..models import (
|
from ..models import (
|
||||||
Category,
|
Category,
|
||||||
Comment,
|
Comment,
|
||||||
|
EmbedMediaCourse,
|
||||||
EncodeProfile,
|
EncodeProfile,
|
||||||
Media,
|
Media,
|
||||||
MediaPermission,
|
MediaPermission,
|
||||||
@@ -743,7 +744,7 @@ class MediaBulkUserActions(APIView):
|
|||||||
def _handle_course_cleanup(self, request, media_ids):
|
def _handle_course_cleanup(self, request, media_ids):
|
||||||
category_uids = request.data.get('category_uids', [])
|
category_uids = request.data.get('category_uids', [])
|
||||||
remove_permissions = request.data.get('remove_permissions', False)
|
remove_permissions = request.data.get('remove_permissions', False)
|
||||||
remove_tags = request.data.get('remove_tags', False)
|
remove_comments = request.data.get('remove_comments', False)
|
||||||
apply_to_all = request.data.get('apply_to_all', False)
|
apply_to_all = request.data.get('apply_to_all', False)
|
||||||
|
|
||||||
if not category_uids:
|
if not category_uids:
|
||||||
@@ -764,24 +765,33 @@ class MediaBulkUserActions(APIView):
|
|||||||
# All users who are members of any group linked to this category
|
# All users who are members of any group linked to this category
|
||||||
group_users = User.objects.filter(rbac_groups__in=category.rbac_groups.all()).distinct()
|
group_users = User.objects.filter(rbac_groups__in=category.rbac_groups.all()).distinct()
|
||||||
|
|
||||||
course_tag = Tag.objects.filter(title=category.title[:100]).first() if remove_tags else None
|
# Get media explicitly embedded into this course via LTI
|
||||||
|
embed_qs = EmbedMediaCourse.objects.filter(category=category)
|
||||||
|
embedded_media_ids = list(embed_qs.values_list('media_id', flat=True))
|
||||||
|
|
||||||
all_course_media = Media.objects.filter(category=category)
|
all_course_media = Media.objects.filter(category=category)
|
||||||
|
|
||||||
if has_media:
|
if has_media:
|
||||||
if remove_permissions:
|
if remove_permissions:
|
||||||
MediaPermission.objects.filter(media__in=selected_media, user__in=group_users).delete()
|
MediaPermission.objects.filter(media__in=selected_media, user__in=group_users).delete()
|
||||||
if remove_tags and course_tag:
|
# Delete EmbedMediaCourse records and owner MediaPermissions for embedded media
|
||||||
for m in selected_media:
|
selected_embedded = embed_qs.filter(media__in=selected_media)
|
||||||
m.tags.remove(course_tag)
|
selected_embedded_media_ids = list(selected_embedded.values_list('media_id', flat=True))
|
||||||
|
selected_embedded.delete()
|
||||||
|
MediaPermission.objects.filter(media_id__in=selected_embedded_media_ids).delete()
|
||||||
|
if remove_comments:
|
||||||
|
Comment.objects.filter(media__in=selected_media).delete()
|
||||||
|
|
||||||
if apply_to_all:
|
if apply_to_all:
|
||||||
other_course_media = all_course_media.exclude(friendly_token__in=media_ids)
|
other_course_media = all_course_media.exclude(friendly_token__in=media_ids)
|
||||||
if remove_permissions:
|
if remove_permissions:
|
||||||
MediaPermission.objects.filter(media__in=other_course_media, user__in=group_users).delete()
|
MediaPermission.objects.filter(media__in=other_course_media, user__in=group_users).delete()
|
||||||
if remove_tags and course_tag:
|
other_embedded = embed_qs.filter(media__in=other_course_media)
|
||||||
for m in other_course_media:
|
other_embedded_media_ids = list(other_embedded.values_list('media_id', flat=True))
|
||||||
m.tags.remove(course_tag)
|
other_embedded.delete()
|
||||||
|
MediaPermission.objects.filter(media_id__in=other_embedded_media_ids).delete()
|
||||||
|
if remove_comments:
|
||||||
|
Comment.objects.filter(media__in=other_course_media).delete()
|
||||||
for m in other_course_media:
|
for m in other_course_media:
|
||||||
m.category.remove(category)
|
m.category.remove(category)
|
||||||
|
|
||||||
@@ -790,9 +800,10 @@ class MediaBulkUserActions(APIView):
|
|||||||
else:
|
else:
|
||||||
if remove_permissions:
|
if remove_permissions:
|
||||||
MediaPermission.objects.filter(media__in=all_course_media, user__in=group_users).delete()
|
MediaPermission.objects.filter(media__in=all_course_media, user__in=group_users).delete()
|
||||||
if remove_tags and course_tag:
|
MediaPermission.objects.filter(media_id__in=embedded_media_ids).delete()
|
||||||
for m in all_course_media:
|
embed_qs.delete()
|
||||||
m.tags.remove(course_tag)
|
if remove_comments:
|
||||||
|
Comment.objects.filter(media__in=all_course_media).delete()
|
||||||
for m in all_course_media:
|
for m in all_course_media:
|
||||||
m.category.remove(category)
|
m.category.remove(category)
|
||||||
|
|
||||||
@@ -1241,4 +1252,11 @@ class MediaShare(APIView):
|
|||||||
defaults={'owner_user': request.user, 'permission': 'owner'},
|
defaults={'owner_user': request.user, 'permission': 'owner'},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
lti_session = request.session.get('lti_session', {})
|
||||||
|
context_id = lti_session.get('context_id')
|
||||||
|
if context_id:
|
||||||
|
category = Category.objects.filter(lti_context_id=context_id, is_rbac_category=True).first()
|
||||||
|
if category:
|
||||||
|
EmbedMediaCourse.objects.get_or_create(media=media, category=category)
|
||||||
|
|
||||||
return Response({'status': 'ok'})
|
return Response({'status': 'ok'})
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const BulkActionCourseCleanupModal: React.FC<BulkActionCourseCleanupModal
|
|||||||
const [availableCourses, setAvailableCourses] = useState<Course[]>([]);
|
const [availableCourses, setAvailableCourses] = useState<Course[]>([]);
|
||||||
const [coursesToCleanup, setCoursesToCleanup] = useState<Course[]>([]);
|
const [coursesToCleanup, setCoursesToCleanup] = useState<Course[]>([]);
|
||||||
const [removePermissions, setRemovePermissions] = useState(false);
|
const [removePermissions, setRemovePermissions] = useState(false);
|
||||||
const [removeTags, setRemoveTags] = useState(false);
|
const [removeComments, setRemoveComments] = useState(false);
|
||||||
const [applyToAll, setApplyToAll] = useState(false);
|
const [applyToAll, setApplyToAll] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
@@ -41,7 +41,7 @@ export const BulkActionCourseCleanupModal: React.FC<BulkActionCourseCleanupModal
|
|||||||
setAvailableCourses([]);
|
setAvailableCourses([]);
|
||||||
setCoursesToCleanup([]);
|
setCoursesToCleanup([]);
|
||||||
setRemovePermissions(false);
|
setRemovePermissions(false);
|
||||||
setRemoveTags(false);
|
setRemoveComments(false);
|
||||||
setApplyToAll(false);
|
setApplyToAll(false);
|
||||||
}
|
}
|
||||||
}, [isOpen, selectedMediaIds.join(',')]);
|
}, [isOpen, selectedMediaIds.join(',')]);
|
||||||
@@ -98,7 +98,7 @@ export const BulkActionCourseCleanupModal: React.FC<BulkActionCourseCleanupModal
|
|||||||
media_ids: selectedMediaIds,
|
media_ids: selectedMediaIds,
|
||||||
category_uids: coursesToCleanup.map((c) => c.uid),
|
category_uids: coursesToCleanup.map((c) => c.uid),
|
||||||
remove_permissions: removePermissions,
|
remove_permissions: removePermissions,
|
||||||
remove_tags: removeTags,
|
remove_comments: removeComments,
|
||||||
apply_to_all: applyToAll,
|
apply_to_all: applyToAll,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -198,10 +198,10 @@ export const BulkActionCourseCleanupModal: React.FC<BulkActionCourseCleanupModal
|
|||||||
<label className="course-cleanup-checkbox">
|
<label className="course-cleanup-checkbox">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={removeTags}
|
checked={removeComments}
|
||||||
onChange={(e) => setRemoveTags(e.target.checked)}
|
onChange={(e) => setRemoveComments(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span>{translateString('Remove course tags')}</span>
|
<span>{translateString('Remove Comments')}</span>
|
||||||
</label>
|
</label>
|
||||||
{hasMediaSelected && (
|
{hasMediaSelected && (
|
||||||
<label className="course-cleanup-checkbox">
|
<label className="course-cleanup-checkbox">
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user