mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-04-23 15:58:08 -04:00
a
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
VERSION = "8.99"
|
||||
VERSION = "8.991"
|
||||
|
||||
+65
-3
@@ -357,6 +357,7 @@ class MediaBulkUserActions(APIView):
|
||||
"remove_from_category",
|
||||
"add_tags",
|
||||
"remove_tags",
|
||||
"course_cleanup",
|
||||
],
|
||||
),
|
||||
'playlist_ids': openapi.Schema(
|
||||
@@ -404,12 +405,15 @@ class MediaBulkUserActions(APIView):
|
||||
media_ids = request.data.get('media_ids', [])
|
||||
action = request.data.get('action')
|
||||
|
||||
if not media_ids:
|
||||
return Response({"detail": "media_ids is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not action:
|
||||
return Response({"detail": "action is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if action == "course_cleanup":
|
||||
return self._handle_course_cleanup(request, media_ids)
|
||||
|
||||
if not media_ids:
|
||||
return Response({"detail": "media_ids is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
media = Media.objects.filter(user=request.user, friendly_token__in=media_ids)
|
||||
|
||||
if not media:
|
||||
@@ -727,6 +731,64 @@ class MediaBulkUserActions(APIView):
|
||||
else:
|
||||
return Response({"detail": f"Unknown action: {action}"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def _handle_course_cleanup(self, request, media_ids):
|
||||
category_uids = request.data.get('category_uids', [])
|
||||
remove_permissions = request.data.get('remove_permissions', False)
|
||||
remove_tags = request.data.get('remove_tags', False)
|
||||
apply_to_all = request.data.get('apply_to_all', False)
|
||||
|
||||
if not category_uids:
|
||||
return Response({"detail": "category_uids is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
categories = Category.objects.filter(uid__in=category_uids)
|
||||
if not categories.exists():
|
||||
return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
valid_categories = [cat for cat in categories if request.user.has_contributor_access_to_category(cat)]
|
||||
if not valid_categories:
|
||||
return Response({"detail": "No contributor access to specified categories"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
has_media = bool(media_ids)
|
||||
selected_media = Media.objects.filter(user=request.user, friendly_token__in=media_ids) if has_media else Media.objects.none()
|
||||
|
||||
for category in valid_categories:
|
||||
# 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()
|
||||
|
||||
course_tag = Tag.objects.filter(title=category.title[:100]).first() if remove_tags else None
|
||||
|
||||
all_course_media = Media.objects.filter(category=category)
|
||||
|
||||
if has_media:
|
||||
if remove_permissions:
|
||||
MediaPermission.objects.filter(media__in=selected_media, user__in=group_users).delete()
|
||||
if remove_tags and course_tag:
|
||||
for m in selected_media:
|
||||
m.tags.remove(course_tag)
|
||||
|
||||
if apply_to_all:
|
||||
other_course_media = all_course_media.exclude(friendly_token__in=media_ids)
|
||||
if remove_permissions:
|
||||
MediaPermission.objects.filter(media__in=other_course_media, user__in=group_users).delete()
|
||||
if remove_tags and course_tag:
|
||||
for m in other_course_media:
|
||||
m.tags.remove(course_tag)
|
||||
for m in other_course_media:
|
||||
m.category.remove(category)
|
||||
|
||||
for m in selected_media:
|
||||
m.category.remove(category)
|
||||
else:
|
||||
if remove_permissions:
|
||||
MediaPermission.objects.filter(media__in=all_course_media, user__in=group_users).delete()
|
||||
if remove_tags and course_tag:
|
||||
for m in all_course_media:
|
||||
m.tags.remove(course_tag)
|
||||
for m in all_course_media:
|
||||
m.category.remove(category)
|
||||
|
||||
return Response({"detail": "Course cleanup completed successfully"})
|
||||
|
||||
|
||||
class MediaDetail(APIView):
|
||||
"""
|
||||
|
||||
@@ -6,12 +6,14 @@ import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
|
||||
interface BulkActionsDropdownProps {
|
||||
selectedCount: number;
|
||||
onActionSelect: (action: string) => void;
|
||||
hasContributorCourses?: boolean;
|
||||
}
|
||||
|
||||
interface BulkAction {
|
||||
value: string;
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
allowsNoSelection?: boolean;
|
||||
}
|
||||
|
||||
interface BulkActionGroup {
|
||||
@@ -19,7 +21,7 @@ interface BulkActionGroup {
|
||||
actions: BulkAction[];
|
||||
}
|
||||
|
||||
export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ selectedCount, onActionSelect }) => {
|
||||
export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ selectedCount, onActionSelect, hasContributorCourses = false }) => {
|
||||
const isLmsMode = inEmbeddedApp();
|
||||
|
||||
const BULK_ACTION_GROUPS: BulkActionGroup[] = [
|
||||
@@ -56,18 +58,24 @@ export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ select
|
||||
{ value: 'change-owner', label: translateString('Change Owner'), enabled: true },
|
||||
{ value: 'copy-media', label: translateString('Copy Media'), enabled: true },
|
||||
{ value: 'delete-media', label: translateString('Delete Media'), enabled: true },
|
||||
...(isLmsMode && hasContributorCourses
|
||||
? [{ value: 'course-cleanup', label: translateString('Course Cleanup'), enabled: true, allowsNoSelection: true }]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
];
|
||||
const noSelection = selectedCount === 0;
|
||||
|
||||
|
||||
const allActions = BULK_ACTION_GROUPS.flatMap((g) => g.actions);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = event.target.value;
|
||||
|
||||
if (!value) return;
|
||||
|
||||
if (noSelection) {
|
||||
const actionDef = allActions.find((a) => a.value === value);
|
||||
if (noSelection && !actionDef?.allowsNoSelection) {
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
@@ -95,7 +103,7 @@ export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ select
|
||||
{BULK_ACTION_GROUPS.map((group) => (
|
||||
<optgroup key={group.label} label={group.label}>
|
||||
{group.actions.map((action) => (
|
||||
<option key={action.value} value={action.value} disabled={noSelection || !action.enabled}>
|
||||
<option key={action.value} value={action.value} disabled={(!action.allowsNoSelection && noSelection) || !action.enabled}>
|
||||
{action.label}
|
||||
</option>
|
||||
))}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BulkActionChangeOwnerModal } from './BulkActionChangeOwnerModal';
|
||||
import { BulkActionPublishStateModal } from './BulkActionPublishStateModal';
|
||||
import { BulkActionCategoryModal } from './BulkActionCategoryModal';
|
||||
import { BulkActionTagModal } from './BulkActionTagModal';
|
||||
import { BulkActionCourseCleanupModal } from './BulkActionCourseCleanupModal';
|
||||
|
||||
/**
|
||||
* Renders all bulk action modals
|
||||
@@ -58,6 +59,12 @@ export function BulkActionsModals({
|
||||
onTagModalSuccess,
|
||||
onTagModalError,
|
||||
|
||||
// Course cleanup modal props
|
||||
showCourseCleanupModal,
|
||||
onCourseCleanupModalCancel,
|
||||
onCourseCleanupModalSuccess,
|
||||
onCourseCleanupModalError,
|
||||
|
||||
// Common props
|
||||
csrfToken,
|
||||
|
||||
@@ -131,6 +138,15 @@ export function BulkActionsModals({
|
||||
csrfToken={csrfToken}
|
||||
/>
|
||||
|
||||
<BulkActionCourseCleanupModal
|
||||
isOpen={showCourseCleanupModal}
|
||||
selectedMediaIds={selectedMediaIds}
|
||||
onCancel={onCourseCleanupModalCancel}
|
||||
onSuccess={onCourseCleanupModalSuccess}
|
||||
onError={onCourseCleanupModalError}
|
||||
csrfToken={csrfToken}
|
||||
/>
|
||||
|
||||
{showNotification && (
|
||||
<div
|
||||
style={{
|
||||
@@ -193,6 +209,11 @@ BulkActionsModals.propTypes = {
|
||||
onTagModalSuccess: PropTypes.func.isRequired,
|
||||
onTagModalError: PropTypes.func.isRequired,
|
||||
|
||||
showCourseCleanupModal: PropTypes.bool.isRequired,
|
||||
onCourseCleanupModalCancel: PropTypes.func.isRequired,
|
||||
onCourseCleanupModalSuccess: PropTypes.func.isRequired,
|
||||
onCourseCleanupModalError: PropTypes.func.isRequired,
|
||||
|
||||
csrfToken: PropTypes.string.isRequired,
|
||||
|
||||
showNotification: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -22,6 +22,7 @@ interface MediaListWrapperProps {
|
||||
onSelectAll?: () => void;
|
||||
onDeselectAll?: () => void;
|
||||
showAddMediaButton?: boolean;
|
||||
hasContributorCourses?: boolean;
|
||||
}
|
||||
|
||||
export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
|
||||
@@ -38,6 +39,7 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
|
||||
onSelectAll = () => {},
|
||||
onDeselectAll = () => {},
|
||||
showAddMediaButton = false,
|
||||
hasContributorCourses = false,
|
||||
}) => {
|
||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup() as [any, any, any];
|
||||
|
||||
@@ -63,7 +65,7 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
|
||||
return (
|
||||
<div className="bulk-actions-container">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<BulkActionsDropdown selectedCount={selectedCount} onActionSelect={onBulkAction} />
|
||||
<BulkActionsDropdown selectedCount={selectedCount} onActionSelect={onBulkAction} hasContributorCourses={hasContributorCourses} />
|
||||
<SelectAllCheckbox
|
||||
totalCount={totalCount}
|
||||
selectedCount={selectedCount}
|
||||
|
||||
@@ -463,6 +463,7 @@ class ProfileMediaPage extends Page {
|
||||
onSelectAll={this.props.bulkActions.handleSelectAll}
|
||||
onDeselectAll={this.props.bulkActions.handleDeselectAll}
|
||||
showAddMediaButton={!isSelectMediaMode && isMediaAuthor}
|
||||
hasContributorCourses={this.props.bulkActions.hasContributorCourses}
|
||||
>
|
||||
<ProfileMediaFilters
|
||||
hidden={this.state.hiddenFilters}
|
||||
@@ -530,6 +531,10 @@ class ProfileMediaPage extends Page {
|
||||
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
|
||||
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
|
||||
onTagModalError={this.props.bulkActions.handleTagModalError}
|
||||
showCourseCleanupModal={this.props.bulkActions.showCourseCleanupModal}
|
||||
onCourseCleanupModalCancel={this.props.bulkActions.handleCourseCleanupModalCancel}
|
||||
onCourseCleanupModalSuccess={this.props.bulkActions.handleCourseCleanupModalSuccess}
|
||||
onCourseCleanupModalError={this.props.bulkActions.handleCourseCleanupModalError}
|
||||
/>
|
||||
) : null,
|
||||
];
|
||||
|
||||
@@ -486,6 +486,7 @@ class ProfileSharedByMePage extends Page {
|
||||
onBulkAction={this.props.bulkActions.handleBulkAction}
|
||||
onSelectAll={this.props.bulkActions.handleSelectAll}
|
||||
onDeselectAll={this.props.bulkActions.handleDeselectAll}
|
||||
hasContributorCourses={this.props.bulkActions.hasContributorCourses}
|
||||
>
|
||||
<ProfileMediaFilters
|
||||
hidden={this.state.hiddenFilters}
|
||||
@@ -554,6 +555,10 @@ class ProfileSharedByMePage extends Page {
|
||||
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
|
||||
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
|
||||
onTagModalError={this.props.bulkActions.handleTagModalError}
|
||||
showCourseCleanupModal={this.props.bulkActions.showCourseCleanupModal}
|
||||
onCourseCleanupModalCancel={this.props.bulkActions.handleCourseCleanupModalCancel}
|
||||
onCourseCleanupModalSuccess={this.props.bulkActions.handleCourseCleanupModalSuccess}
|
||||
onCourseCleanupModalError={this.props.bulkActions.handleCourseCleanupModalError}
|
||||
/>
|
||||
) : null,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { translateString } from '../helpers';
|
||||
import { inEmbeddedApp } from '../helpers/embeddedApp';
|
||||
|
||||
/**
|
||||
* Custom hook for managing bulk actions on media items
|
||||
@@ -22,6 +23,20 @@ export function useBulkActions() {
|
||||
const [showPublishStateModal, setShowPublishStateModal] = useState(false);
|
||||
const [showCategoryModal, setShowCategoryModal] = useState(false);
|
||||
const [showTagModal, setShowTagModal] = useState(false);
|
||||
const [showCourseCleanupModal, setShowCourseCleanupModal] = useState(false);
|
||||
const [hasContributorCourses, setHasContributorCourses] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inEmbeddedApp()) return;
|
||||
fetch('/api/v1/categories/contributor?lms_courses_only=true')
|
||||
.then((r) => r.ok ? r.json() : null)
|
||||
.then((data) => {
|
||||
if (!data) return;
|
||||
const courses = data.results || data;
|
||||
setHasContributorCourses(Array.isArray(courses) && courses.length > 0);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Get CSRF token from cookies
|
||||
const getCsrfToken = () => {
|
||||
@@ -95,6 +110,11 @@ export function useBulkActions() {
|
||||
const handleBulkAction = (action) => {
|
||||
const selectedCount = selectedMedia.size;
|
||||
|
||||
if (action === 'course-cleanup') {
|
||||
setShowCourseCleanupModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedCount === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -500,6 +520,22 @@ export function useBulkActions() {
|
||||
setShowTagModal(false);
|
||||
};
|
||||
|
||||
// Course cleanup modal handlers
|
||||
const handleCourseCleanupModalCancel = () => {
|
||||
setShowCourseCleanupModal(false);
|
||||
};
|
||||
|
||||
const handleCourseCleanupModalSuccess = (message) => {
|
||||
showNotificationMessage(message);
|
||||
clearSelectionAndRefresh();
|
||||
setShowCourseCleanupModal(false);
|
||||
};
|
||||
|
||||
const handleCourseCleanupModalError = (message) => {
|
||||
showNotificationMessage(message, 'error');
|
||||
setShowCourseCleanupModal(false);
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
selectedMedia,
|
||||
@@ -517,6 +553,8 @@ export function useBulkActions() {
|
||||
showPublishStateModal,
|
||||
showCategoryModal,
|
||||
showTagModal,
|
||||
showCourseCleanupModal,
|
||||
hasContributorCourses,
|
||||
|
||||
// Handlers
|
||||
handleMediaSelection,
|
||||
@@ -544,6 +582,9 @@ export function useBulkActions() {
|
||||
handleTagModalCancel,
|
||||
handleTagModalSuccess,
|
||||
handleTagModalError,
|
||||
handleCourseCleanupModalCancel,
|
||||
handleCourseCleanupModalSuccess,
|
||||
handleCourseCleanupModalError,
|
||||
|
||||
// Utility
|
||||
getCsrfToken,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user