diff --git a/contentcuration/contentcuration/frontend/channelList/vuex/channelList/__tests__/module.spec.js b/contentcuration/contentcuration/frontend/channelList/vuex/channelList/__tests__/module.spec.js index 2998520efd..e6df06501d 100644 --- a/contentcuration/contentcuration/frontend/channelList/vuex/channelList/__tests__/module.spec.js +++ b/contentcuration/contentcuration/frontend/channelList/vuex/channelList/__tests__/module.spec.js @@ -53,52 +53,50 @@ describe('invitation actions', () => { }); }); }); - // TODO: Figure out why client isn't getting mocked then uncomment this - // describe('acceptInvitation action', () => { - // const channel = { id: channel_id, name: 'test', deleted: false, edit: true }; - // beforeEach(() => { - // store.commit('channelList/SET_INVITATION_LIST', [{ id, ...invitation }]); - // return Channel.add(channel); - // }); - // afterEach(() => { - // return Channel.table.toCollection().delete(); - // }); - // it('should call accept', () => { - // const updateSpy = jest.spyOn(Invitation, 'accept'); - // return store.dispatch('channelList/acceptInvitation', id).then(() => { - // expect(updateSpy).toHaveBeenCalled(); - // expect(updateSpy.mock.calls[0][0]).toBe(id); - // updateSpy.mockRestore(); - // }); - // }); - // it('should load and set the invited channel', () => { - // return store.dispatch('channelList/acceptInvitation', id).then(() => { - // expect(store.getters['channel/getChannel'](channel_id).id).toBeTruthy(); - // }); - // }); - // it('should remove the invitation from the list', () => { - // return store.dispatch('channelList/acceptInvitation', id).then(() => { - // expect(store.getters['channelList/getInvitation'](id)).toBeFalsy(); - // }); - // }); - // it('should set the correct permission on the accepted invite', () => { - // return store.dispatch('channelList/acceptInvitation', id).then(() => { - // expect(store.getters['channel/getChannel'](channel_id).view).toBe(true); - // expect(store.getters['channel/getChannel'](channel_id).edit).toBe(false); - // }); - // }); - // }); + describe('acceptInvitation action', () => { + const channel = { id: channel_id, name: 'test', deleted: false, edit: true }; + beforeEach(() => { + store.commit('channelList/SET_INVITATION_LIST', [{ id, ...invitation }]); + return Channel.add(channel); + }); + afterEach(() => { + return Channel.table.toCollection().delete(); + }); + it('should call accept', () => { + const acceptSpy = jest.spyOn(Invitation, 'accept'); + return store.dispatch('channelList/acceptInvitation', id).then(() => { + expect(acceptSpy).toHaveBeenCalled(); + expect(acceptSpy.mock.calls[0][0]).toBe(id); + acceptSpy.mockRestore(); + }); + }); + it('should load and set the invited channel', () => { + return store.dispatch('channelList/acceptInvitation', id).then(() => { + expect(store.getters['channel/getChannel'](channel_id).id).toBeTruthy(); + }); + }); + it('should remove the invitation from the list', () => { + return store.dispatch('channelList/acceptInvitation', id).then(() => { + expect(store.getters['channelList/getInvitation'](id)).toBeFalsy(); + }); + }); + it('should set the correct permission on the accepted invite', () => { + return store.dispatch('channelList/acceptInvitation', id).then(() => { + expect(store.getters['channel/getChannel'](channel_id).view).toBe(true); + expect(store.getters['channel/getChannel'](channel_id).edit).toBe(false); + }); + }); + }); describe('declineInvitation action', () => { beforeEach(() => { store.commit('channelList/SET_INVITATION_LIST', [{ id, ...invitation }]); }); it('should call client.delete', () => { - const updateSpy = jest.spyOn(Invitation, 'update'); + const declineSpy = jest.spyOn(Invitation, 'decline'); return store.dispatch('channelList/declineInvitation', id).then(() => { - expect(updateSpy).toHaveBeenCalled(); - expect(updateSpy.mock.calls[0][0]).toBe(id); - expect(updateSpy.mock.calls[0][1]).toEqual({ declined: true }); - updateSpy.mockRestore(); + expect(declineSpy).toHaveBeenCalled(); + expect(declineSpy.mock.calls[0][0]).toBe(id); + declineSpy.mockRestore(); }); }); it('should not load and set the invited channel', () => { diff --git a/contentcuration/contentcuration/frontend/channelList/vuex/channelList/actions.js b/contentcuration/contentcuration/frontend/channelList/vuex/channelList/actions.js index 98178b39f4..93cf0405c4 100644 --- a/contentcuration/contentcuration/frontend/channelList/vuex/channelList/actions.js +++ b/contentcuration/contentcuration/frontend/channelList/vuex/channelList/actions.js @@ -76,7 +76,7 @@ export function acceptInvitation(context, invitationId) { } export function declineInvitation(context, invitationId) { - return Invitation.update(invitationId, { declined: true }).then(() => { + return Invitation.decline(invitationId).then(() => { return context.commit('REMOVE_INVITATION', invitationId); }); } diff --git a/contentcuration/contentcuration/frontend/shared/data/resources.js b/contentcuration/contentcuration/frontend/shared/data/resources.js index 5925bd3c8e..25cd88fadf 100644 --- a/contentcuration/contentcuration/frontend/shared/data/resources.js +++ b/contentcuration/contentcuration/frontend/shared/data/resources.js @@ -1932,7 +1932,14 @@ export const Invitation = new Resource({ accept(id) { const changes = { accepted: true }; - return client.post(window.Urls.invitationAccept(id), changes).then(() => { + return this._handleInvitation(id, window.Urls.invitationAccept(id), changes); + }, + decline(id) { + const changes = { declined: true }; + return this._handleInvitation(id, window.Urls.invitationDecline(id), changes); + }, + _handleInvitation(id, url, changes) { + return client.post(url).then(() => { return this.transaction({ mode: 'rw' }, () => { return this.table.update(id, changes); }); diff --git a/contentcuration/contentcuration/tests/viewsets/test_invitation.py b/contentcuration/contentcuration/tests/viewsets/test_invitation.py index a14aae8ccf..fad9b52be4 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_invitation.py +++ b/contentcuration/contentcuration/tests/viewsets/test_invitation.py @@ -319,3 +319,23 @@ def test_delete_invitation(self): reverse("invitation-detail", kwargs={"pk": invitation.id}) ) self.assertEqual(response.status_code, 405, response.content) + + def test_update_invitation_decline(self): + invitation = models.Invitation.objects.create(**self.invitation_db_metadata) + + self.client.force_authenticate(user=self.invited_user) + response = self.client.post(reverse("invitation-decline", kwargs={"pk": invitation.id})) + self.assertEqual(response.status_code, 200, response.content) + try: + invitation = models.Invitation.objects.get(id=invitation.id) + except models.Invitation.DoesNotExist: + self.fail("Invitation was deleted") + + self.assertTrue(invitation.declined) + self.assertFalse(self.channel.editors.filter(pk=self.invited_user.id).exists()) + self.assertTrue( + models.Invitation.objects.filter( + email=self.invited_user.email, channel=self.channel + ).exists() + ) + self.assertTrue(models.Change.objects.filter(channel=self.channel).exists()) diff --git a/contentcuration/contentcuration/viewsets/invitation.py b/contentcuration/contentcuration/viewsets/invitation.py index 55507297ef..81b1e5c680 100644 --- a/contentcuration/contentcuration/viewsets/invitation.py +++ b/contentcuration/contentcuration/viewsets/invitation.py @@ -149,3 +149,15 @@ def accept(self, request, pk=None): ), applied=True, created_by_id=request.user.id ) return Response({"status": "success"}) + + @action(detail=True, methods=["post"]) + def decline(self, request, pk=None): + invitation = self.get_object() + invitation.declined = True + invitation.save() + Change.create_change( + generate_update_event( + invitation.id, INVITATION, {"declined": True}, channel_id=invitation.channel_id + ), applied=True, created_by_id=request.user.id + ) + return Response({"status": "success"})