Skip to content

Commit 414d0f8

Browse files
Fixes inability to delete view-only channels
* Fixed Deletion of View Only Channel * Added Unit Tests for Deletion of View Only Channels * Added Suggested changes * Modified Dialog box and context menu strs
1 parent aa7e3cc commit 414d0f8

File tree

5 files changed

+141
-14
lines changed

5 files changed

+141
-14
lines changed

contentcuration/contentcuration/frontend/channelList/views/Channel/ChannelItem.vue

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@
190190
icon="trash"
191191
/>
192192
</VListTileAvatar>
193-
<VListTileTitle>{{ $tr('deleteChannel') }}</VListTileTitle>
193+
<VListTileTitle>
194+
{{ canEdit ? $tr('deleteChannel') : $tr('removeChannel') }}
195+
</VListTileTitle>
194196
</VListTile>
195197
</VList>
196198
</Menu>
@@ -202,14 +204,14 @@
202204
<!-- Delete dialog -->
203205
<KModal
204206
v-if="deleteDialog"
205-
:title="$tr('deleteTitle')"
206-
:submitText="$tr('deleteChannel')"
207+
:title="canEdit ? $tr('deleteTitle') : $tr('removeTitle')"
208+
:submitText="canEdit ? $tr('deleteChannel') : $tr('removeBtn')"
207209
:cancelText="$tr('cancel')"
208210
data-test="delete-modal"
209211
@submit="handleDelete"
210212
@cancel="deleteDialog = false"
211213
>
212-
{{ $tr('deletePrompt') }}
214+
{{ canEdit ? $tr('deletePrompt') : $tr('removePrompt') }}
213215
</KModal>
214216
<!-- Copy dialog -->
215217
<ChannelTokenModal
@@ -343,13 +345,21 @@
343345
}
344346
},
345347
methods: {
346-
...mapActions('channel', ['deleteChannel']),
348+
...mapActions('channel', ['deleteChannel', 'removeViewer']),
347349
...mapMutations('channel', { updateChannel: 'UPDATE_CHANNEL' }),
348350
handleDelete() {
349-
this.deleteChannel(this.channelId).then(() => {
350-
this.deleteDialog = false;
351-
this.$store.dispatch('showSnackbarSimple', this.$tr('channelDeletedSnackbar'));
352-
});
351+
if (!this.canEdit) {
352+
const currentUserId = this.$store.state.session.currentUser.id;
353+
this.removeViewer({ channelId: this.channelId, userId: currentUserId }).then(() => {
354+
this.deleteDialog = false;
355+
this.$store.dispatch('showSnackbarSimple', this.$tr('channelRemovedSnackbar'));
356+
});
357+
} else {
358+
this.deleteChannel(this.channelId).then(() => {
359+
this.deleteDialog = false;
360+
this.$store.dispatch('showSnackbarSimple', this.$tr('channelDeletedSnackbar'));
361+
});
362+
}
353363
},
354364
goToChannelRoute() {
355365
this.linkToChannelTree
@@ -374,8 +384,14 @@
374384
copyToken: 'Copy channel token',
375385
deleteChannel: 'Delete channel',
376386
deleteTitle: 'Delete this channel',
387+
removeChannel: 'Remove from channel list',
388+
removeBtn: 'Remove',
389+
removeTitle: 'Remove from channel list',
377390
deletePrompt: 'This channel will be permanently deleted. This cannot be undone.',
391+
removePrompt:
392+
'You have view-only access to this channel. Confirm that you want to remove it from your list of channels.',
378393
channelDeletedSnackbar: 'Channel deleted',
394+
channelRemovedSnackbar: 'Channel removed',
379395
channelLanguageNotSetIndicator: 'No language set',
380396
cancel: 'Cancel',
381397
},

contentcuration/contentcuration/frontend/channelList/views/Channel/__tests__/channelItem.spec.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,37 @@ describe('channelItem', () => {
7676
wrapper.find('[data-test="token-listitem"]').trigger('click');
7777
expect(wrapper.vm.tokenDialog).toBe(true);
7878
});
79-
it('clicking delete button in dialog should delete the channel', () => {
79+
it('when user can edit, clicking delete button in dialog should call deleteChannel', async () => {
80+
const deleteChannelSpy = jest.fn().mockResolvedValue();
81+
const removeViewerSpy = jest.fn().mockResolvedValue();
82+
wrapper = makeWrapper(true, deleteStub);
83+
wrapper.setMethods({
84+
deleteChannel: deleteChannelSpy,
85+
removeViewer: removeViewerSpy,
86+
});
87+
88+
wrapper.setData({ deleteDialog: true });
89+
wrapper.find('[data-test="delete-modal"]').trigger('submit');
90+
await wrapper.vm.$nextTick(() => {
91+
expect(deleteChannelSpy).toHaveBeenCalledWith(channelId);
92+
expect(removeViewerSpy).not.toHaveBeenCalled();
93+
});
94+
});
95+
96+
it('when user cannot edit, clicking delete button in dialog should call removeViewer', async () => {
97+
const deleteChannelSpy = jest.fn().mockResolvedValue();
98+
const removeViewerSpy = jest.fn().mockResolvedValue();
99+
wrapper = makeWrapper(false, deleteStub);
100+
wrapper.setMethods({
101+
deleteChannel: deleteChannelSpy,
102+
removeViewer: removeViewerSpy,
103+
});
104+
80105
wrapper.setData({ deleteDialog: true });
81106
wrapper.find('[data-test="delete-modal"]').trigger('submit');
82-
wrapper.vm.$nextTick(() => {
83-
expect(deleteStub).toHaveBeenCalled();
107+
await wrapper.vm.$nextTick(() => {
108+
expect(removeViewerSpy).toHaveBeenCalledWith({ channelId, userId: 0 });
109+
expect(deleteChannelSpy).not.toHaveBeenCalled();
84110
});
85111
});
86112

contentcuration/contentcuration/frontend/shared/data/__tests__/resources.spec.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { UpdatedDescendantsChange } from '../changes';
2+
import { ViewerM2M, ChannelUser, Channel, ContentNode } from '../resources';
23
import db from 'shared/data/db';
34
import { CHANGE_TYPES, TABLE_NAMES } from 'shared/data/constants';
45
import { ContentKindsNames } from 'shared/leUtils/ContentKinds';
5-
import { ContentNode } from 'shared/data/resources';
66
import { mockChannelScope, resetMockChannelScope } from 'shared/utils/testing';
7+
import client from 'shared/client';
8+
import urls from 'shared/urls';
79

810
const CLIENTID = 'test-client-id';
911

@@ -170,5 +172,53 @@ describe('Resources', () => {
170172
expect(change.mods).toEqual(changes);
171173
});
172174
});
175+
describe('ChannelUser resource', () => {
176+
const testChannelId = 'test-channel-id';
177+
const testUserId = 'test-user-id';
178+
179+
beforeEach(async () => {
180+
await db[TABLE_NAMES.VIEWER_M2M].clear();
181+
await db[TABLE_NAMES.CHANNEL].clear();
182+
jest.spyOn(client, 'delete').mockResolvedValue({});
183+
jest.spyOn(Channel.table, 'delete').mockResolvedValue(true);
184+
jest.spyOn(urls, 'channeluser_remove_self').mockReturnValue(`fake_url_for_${testUserId}`);
185+
});
186+
187+
afterEach(() => {
188+
client.delete.mockRestore();
189+
Channel.table.delete.mockRestore();
190+
urls.channeluser_remove_self.mockRestore();
191+
});
192+
193+
it('should remove the user from the ViewerM2M table when removeViewer is called', async () => {
194+
await ViewerM2M.add({ user: testUserId, channel: testChannelId });
195+
let viewer = await ViewerM2M.get([testUserId, testChannelId]);
196+
expect(viewer).toBeTruthy();
197+
198+
await ChannelUser.removeViewer(testChannelId, testUserId);
199+
200+
viewer = await ViewerM2M.get([testUserId, testChannelId]);
201+
expect(viewer).toBeUndefined();
202+
expect(client.delete).toHaveBeenCalledWith(urls.channeluser_remove_self(testUserId), {
203+
params: { channel_id: testChannelId },
204+
});
205+
});
206+
207+
it('should call Channel.table.delete(channel) when removeViewer is called', async () => {
208+
await ViewerM2M.add({ user: testUserId, channel: testChannelId });
209+
const viewer = await ViewerM2M.get([testUserId, testChannelId]);
210+
expect(viewer).toBeTruthy();
211+
await ChannelUser.removeViewer(testChannelId, testUserId);
212+
expect(Channel.table.delete).toHaveBeenCalledWith(testChannelId);
213+
});
214+
215+
it('should handle error from client.delete when removeViewer is called', async () => {
216+
jest.spyOn(client, 'delete').mockRejectedValue(new Error('error deleting'));
217+
await ViewerM2M.add({ user: testUserId, channel: testChannelId });
218+
await expect(ChannelUser.removeViewer(testChannelId, testUserId)).rejects.toThrow(
219+
'error deleting'
220+
);
221+
});
222+
});
173223
});
174224
});

contentcuration/contentcuration/frontend/shared/data/resources.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2058,7 +2058,14 @@ export const ChannelUser = new APIResource({
20582058
});
20592059
},
20602060
removeViewer(channel, user) {
2061-
return ViewerM2M.delete([user, channel]);
2061+
const modelUrl = urls.channeluser_remove_self(user);
2062+
const params = { channel_id: channel };
2063+
return ViewerM2M.delete([user, channel])
2064+
.then(() => client.delete(modelUrl, { params }))
2065+
.then(() => Channel.table.delete(channel))
2066+
.catch(err => {
2067+
throw err;
2068+
});
20622069
},
20632070
fetchCollection(params) {
20642071
return client.get(this.collectionUrl(), { params }).then(response => {

contentcuration/contentcuration/viewsets/user.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
from django.db.models import Value
1212
from django.db.models.functions import Cast
1313
from django.db.models.functions import Concat
14+
from django.http import HttpResponseBadRequest
15+
from django.http.response import HttpResponseForbidden
16+
from django.http.response import HttpResponseNotFound
1417
from django_filters.rest_framework import BooleanFilter
1518
from django_filters.rest_framework import CharFilter
1619
from django_filters.rest_framework import FilterSet
@@ -19,6 +22,7 @@
1922
from rest_framework.permissions import BasePermission
2023
from rest_framework.permissions import IsAuthenticated
2124
from rest_framework.response import Response
25+
from rest_framework.status import HTTP_204_NO_CONTENT
2226

2327
from contentcuration.constants import feature_flags
2428
from contentcuration.models import boolean_val
@@ -267,6 +271,30 @@ def create_from_changes(self, changes):
267271
def delete_from_changes(self, changes):
268272
return self._handle_relationship_changes(changes)
269273

274+
@action(detail=True, methods=['delete'])
275+
def remove_self(self, request, pk=None):
276+
"""
277+
Allows a user to remove themselves from a channel as a viewer.
278+
"""
279+
user = self.get_object()
280+
channel_id = request.query_params.get('channel_id', None)
281+
282+
if not channel_id:
283+
return HttpResponseBadRequest('Channel ID is required.')
284+
285+
channel = Channel.objects.get(id=channel_id)
286+
if not channel:
287+
return HttpResponseNotFound("Channel not found {}".format(channel_id))
288+
289+
if request.user != user and not request.user.can_edit(channel_id):
290+
return HttpResponseForbidden("You do not have permission to remove this user {}".format(user.id))
291+
292+
if channel.viewers.filter(id=user.id).exists():
293+
channel.viewers.remove(user)
294+
return Response(status=HTTP_204_NO_CONTENT)
295+
else:
296+
return HttpResponseBadRequest('User is not a viewer of this channel.')
297+
270298

271299
class AdminUserFilter(FilterSet):
272300
keywords = CharFilter(method="filter_keywords")

0 commit comments

Comments
 (0)