From ba6012b28ccd51687ea4a03bf2f7fe3334f41872 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 26 Jun 2025 13:15:00 -0500 Subject: [PATCH] Fixes bulk channel editor not saving. Fixes #222 --- apps/channels/api_views.py | 83 +++++++++++++++---- frontend/src/api.js | 9 +- .../src/components/forms/ChannelBatch.jsx | 50 ++++++++--- 3 files changed, 110 insertions(+), 32 deletions(-) diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index 0106ffd5..b651081e 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -8,7 +8,7 @@ from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from django.shortcuts import get_object_or_404, get_list_or_404 from django.db import transaction -import os, json, requests +import os, json, requests, logging from apps.accounts.permissions import ( Authenticated, IsAdmin, @@ -48,6 +48,9 @@ import mimetypes from rest_framework.pagination import PageNumberPagination +logger = logging.getLogger(__name__) + + class OrInFilter(django_filters.Filter): """ Custom filter that handles the OR condition instead of AND. @@ -275,30 +278,76 @@ class ChannelViewSet(viewsets.ModelViewSet): @action(detail=False, methods=["patch"], url_path="edit/bulk") def edit_bulk(self, request): - data_list = request.data - if not isinstance(data_list, list): + """ + Bulk edit channels. + Expects a list of channels with their updates. + """ + data = request.data + if not isinstance(data, list): return Response( - {"error": "Expected a list of channel objects objects"}, + {"error": "Expected a list of channel updates"}, status=status.HTTP_400_BAD_REQUEST, ) updated_channels = [] - try: - with transaction.atomic(): - for item in data_list: - channel = Channel.objects.id(id=item.pop("id")) - for key, value in item.items(): - setattr(channel, key, value) + errors = [] - channel.save(update_fields=item.keys()) - updated_channels.append(channel) - except Exception as e: - logger.error("Error during bulk channel edit", e) - return Response({"error": e}, status=500) + for channel_data in data: + channel_id = channel_data.get("id") + if not channel_id: + errors.append({"error": "Channel ID is required"}) + continue - response_data = ChannelSerializer(updated_channels, many=True).data + try: + channel = Channel.objects.get(id=channel_id) - return Response(response_data, status=status.HTTP_200_OK) + # Handle channel_group_id properly - convert string to integer if needed + if 'channel_group_id' in channel_data: + group_id = channel_data['channel_group_id'] + if group_id is not None: + try: + channel_data['channel_group_id'] = int(group_id) + except (ValueError, TypeError): + channel_data['channel_group_id'] = None + + # Use the serializer to validate and update + serializer = ChannelSerializer( + channel, data=channel_data, partial=True + ) + + if serializer.is_valid(): + updated_channel = serializer.save() + updated_channels.append(updated_channel) + else: + errors.append({ + "channel_id": channel_id, + "errors": serializer.errors + }) + + except Channel.DoesNotExist: + errors.append({ + "channel_id": channel_id, + "error": "Channel not found" + }) + except Exception as e: + errors.append({ + "channel_id": channel_id, + "error": str(e) + }) + + if errors: + return Response( + {"errors": errors, "updated_count": len(updated_channels)}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Serialize the updated channels for response + serialized_channels = ChannelSerializer(updated_channels, many=True).data + + return Response({ + "message": f"Successfully updated {len(updated_channels)} channels", + "channels": serialized_channels + }) @action(detail=False, methods=["get"], url_path="ids") def get_ids(self, request, *args, **kwargs): diff --git a/frontend/src/api.js b/frontend/src/api.js index 17c38b90..cd9d21ca 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -390,9 +390,9 @@ export default class API { static async updateChannels(ids, values) { const body = []; - for (const id in ids) { + for (const id of ids) { body.push({ - id, + id: id, ...values, }); } @@ -406,7 +406,10 @@ export default class API { } ); - useChannelsStore.getState().updateChannels(response); + // Pass the channels array from the response, not the entire response + if (response.channels) { + useChannelsStore.getState().updateChannels(response.channels); + } return response; } catch (e) { errorNotification('Failed to update channels', e); diff --git a/frontend/src/components/forms/ChannelBatch.jsx b/frontend/src/components/forms/ChannelBatch.jsx index 12ee52b8..e26dba38 100644 --- a/frontend/src/components/forms/ChannelBatch.jsx +++ b/frontend/src/components/forms/ChannelBatch.jsx @@ -36,6 +36,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { const [channelGroupModelOpen, setChannelGroupModalOpen] = useState(false); const [selectedChannelGroup, setSelectedChannelGroup] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); const [groupPopoverOpened, setGroupPopoverOpened] = useState(false); const [groupFilter, setGroupFilter] = useState(''); @@ -51,27 +52,38 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { }); const onSubmit = async () => { + setIsSubmitting(true); + const values = { ...form.getValues(), - channel_group_id: selectedChannelGroup, }; + // Handle channel group ID - convert to integer if it exists + if (selectedChannelGroup) { + values.channel_group_id = parseInt(selectedChannelGroup); + } else { + delete values.channel_group_id; + } + if (!values.stream_profile_id || values.stream_profile_id === '0') { values.stream_profile_id = null; } - if (!values.channel_group_id) { - delete values.channel_group_id; - } - if (values.user_level == '-1') { delete values.user_level; } - await API.batchUpdateChannels({ - ids: channelIds, - values, - }); + // Remove the channel_group field from form values as we use channel_group_id + delete values.channel_group; + + try { + await API.updateChannels(channelIds, values); + onClose(); + } catch (error) { + console.error('Failed to update channels:', error); + } finally { + setIsSubmitting(false); + } }; // useEffect(() => { @@ -151,6 +163,21 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { onClick={() => setGroupPopoverOpened(true)} size="xs" style={{ flex: 1 }} + rightSection={ + form.getValues().channel_group && ( + { + e.stopPropagation(); + setSelectedChannelGroup(''); + form.setValues({ channel_group: '' }); + }} + > + + + ) + } /> { label: '(no change)', }, ].concat( - Object.entries(USER_LEVELS).map(([label, value]) => { + Object.entries(USER_LEVELS).map(([, value]) => { return { label: USER_LEVEL_LABELS[value], value: `${value}`, @@ -274,9 +301,8 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { /> - -