diff --git a/.github/workflows/deploytest.yml b/.github/workflows/deploytest.yml index b5c3fef9a3..cb4e4a2346 100644 --- a/.github/workflows/deploytest.yml +++ b/.github/workflows/deploytest.yml @@ -61,7 +61,9 @@ jobs: ${{ runner.os }}-pyprod- - name: Install pip-tools and python dependencies run: | - python -m pip install --upgrade pip + # Pin pip to 25.2 to avoid incompatibility with pip-tools and 25.3 + # see https://github.com/jazzband/pip-tools/issues/2252 + python -m pip install pip==25.2 pip install pip-tools pip-sync requirements.txt - name: Use pnpm diff --git a/.github/workflows/pythontest.yml b/.github/workflows/pythontest.yml index ec99bec269..5467f8a47c 100644 --- a/.github/workflows/pythontest.yml +++ b/.github/workflows/pythontest.yml @@ -82,7 +82,9 @@ jobs: ${{ runner.os }}-pytest- - name: Install pip-tools and python dependencies run: | - python -m pip install --upgrade pip + # Pin pip to 25.2 to avoid incompatibility with pip-tools and 25.3 + # see https://github.com/jazzband/pip-tools/issues/2252 + python -m pip install pip==25.2 pip install pip-tools pip-sync requirements.txt requirements-dev.txt - name: Test pytest diff --git a/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/ImportFromChannelsModal.vue b/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/ImportFromChannelsModal.vue index 004cc44828..98847971e7 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/ImportFromChannelsModal.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/ImportFromChannelsModal.vue @@ -52,21 +52,19 @@ > check_circle {{ $tr('addedText') }} - - {{ $tr('removeButton') }} - + /> - - {{ $tr('addButton') }} - + /> diff --git a/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchOrBrowseWindow.vue b/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchOrBrowseWindow.vue index ceb73ca731..373fe19ee7 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchOrBrowseWindow.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchOrBrowseWindow.vue @@ -13,8 +13,9 @@ v-if="!isBrowsing" class="my-2" > - @@ -44,25 +45,27 @@ -
- + - diff --git a/contentcuration/contentcuration/frontend/channelEdit/views/files/ContentRenderer.vue b/contentcuration/contentcuration/frontend/channelEdit/views/files/ContentRenderer.vue index b363de29b6..96d578c135 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/views/files/ContentRenderer.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/views/files/ContentRenderer.vue @@ -1,12 +1,12 @@ diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/getters.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/getters.js index 2927514b80..3e31ce6ed3 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/getters.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/getters.js @@ -7,7 +7,7 @@ import messages from '../../translator'; import { parseNode } from './utils'; import { getNodeDetailsErrors, getNodeFilesErrors } from 'shared/utils/validation'; import { ContentKindsNames } from 'shared/leUtils/ContentKinds'; -import { NEW_OBJECT } from 'shared/constants'; +import { NEW_OBJECT, ValidationErrors } from 'shared/constants'; import { COPYING_STATUS, COPYING_STATUS_VALUES } from 'shared/data/constants'; function sorted(nodes) { @@ -158,14 +158,22 @@ export function getContentNodeIsValid(state, getters, rootState, rootGetters) { export function getContentNodeDetailsAreValid(state) { return function (contentNodeId) { - const contentNode = state.contentNodesMap[contentNodeId]; - return contentNode && (contentNode[NEW_OBJECT] || !getNodeDetailsErrors(contentNode).length); + return !getNodeDetailsErrorsList(state)(contentNodeId).length; }; } export function getNodeDetailsErrorsList(state) { return function (contentNodeId) { const contentNode = state.contentNodesMap[contentNodeId]; + + if (!contentNode) { + return [ValidationErrors.MISSING_NODE]; + } + + if (contentNode[NEW_OBJECT]) { + return []; + } + return getNodeDetailsErrors(contentNode); }; } diff --git a/contentcuration/contentcuration/frontend/shared/constants.js b/contentcuration/contentcuration/frontend/shared/constants.js index 77ddaa9b90..f17081da03 100644 --- a/contentcuration/contentcuration/frontend/shared/constants.js +++ b/contentcuration/contentcuration/frontend/shared/constants.js @@ -190,6 +190,7 @@ export const ValidationErrors = { ACTIVITY_DURATION_MAX_FOR_LONG_ACTIVITY: 'ACTIVITY_DURATION_MAX_FOR_LONG_ACTIVITY', ACTIVITY_DURATION_MIN_REQUIREMENT: 'ACTIVITY_DURATION_MIN_REQUIREMENT', ACTIVITY_DURATION_TOO_LONG: 'ACTIVITY_DURATION_TOO_LONG', + MISSING_NODE: 'MISSING_NODE', ...fileErrors, }; diff --git a/contentcuration/contentcuration/frontend/shared/feedbackApiUtils.js b/contentcuration/contentcuration/frontend/shared/feedbackApiUtils.js index 49ba10d165..bdc8f15879 100644 --- a/contentcuration/contentcuration/frontend/shared/feedbackApiUtils.js +++ b/contentcuration/contentcuration/frontend/shared/feedbackApiUtils.js @@ -12,9 +12,9 @@ export const FeedbackTypeOptions = { flagged: 'FLAGGED', }; -export const FLAG_FEEDBACK_EVENT_ENDPOINT = 'flagged'; -export const RECOMMENDATION_EVENT_ENDPOINT = 'recommendations'; -export const RECOMMENDATION_INTERACTION_EVENT_ENDPOINT = 'recommendations-interaction'; +export const FLAG_FEEDBACK_EVENT_ENDPOINT = 'flagged-events'; +export const RECOMMENDATION_EVENT_ENDPOINT = 'recommendations-events'; +export const RECOMMENDATION_INTERACTION_EVENT_ENDPOINT = 'recommendations-interaction-events'; /** * @typedef {Object} BaseFeedbackParams diff --git a/contentcuration/contentcuration/frontend/shared/styles/main.scss b/contentcuration/contentcuration/frontend/shared/styles/main.scss index 8e3188950c..8a6910724c 100644 --- a/contentcuration/contentcuration/frontend/shared/styles/main.scss +++ b/contentcuration/contentcuration/frontend/shared/styles/main.scss @@ -48,9 +48,12 @@ body { text-align: center; } - .button:focus-visible, .v-btn:focus-visible { - outline: 2px solid var(--v-secondary-base) !important; + // ensures that until KDS migration is complete, + // Vuetify-based buttons have a visible focus style + // consistent with KDS buttons and links + outline: 3px solid #33acf5 !important; + outline-offset: 4px !important; } > .v-dialog__content, diff --git a/contentcuration/contentcuration/frontend/shared/views/channel/ChannelSharing.vue b/contentcuration/contentcuration/frontend/shared/views/channel/ChannelSharing.vue index 9fbd995ced..f08183ddfa 100644 --- a/contentcuration/contentcuration/frontend/shared/views/channel/ChannelSharing.vue +++ b/contentcuration/contentcuration/frontend/shared/views/channel/ChannelSharing.vue @@ -59,13 +59,13 @@ - {{ $tr('inviteButton') }} - + - - - - - - + {{ + $tr('removeViewerText', { first_name: selected.first_name, last_name: selected.last_name }) + }} + - - - - + {{ $tr('deleteInvitationText', { email: selected.email }) }} + - - - - + {{ + $tr('makeEditorText', { first_name: selected.first_name, last_name: selected.last_name }) + }} +
@@ -173,13 +98,9 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import { SharingPermissions } from 'shared/constants'; - import MessageDialog from 'shared/views/MessageDialog'; export default { name: 'ChannelSharingTable', - components: { - MessageDialog, - }, props: { channelId: { type: String, @@ -246,6 +167,40 @@ 'makeEditor', 'removeViewer', ]), + getMenuOptions(item) { + if (item.pending) { + return [ + { id: 'resend-invitation', label: this.$tr('resendInvitation'), item }, + { id: 'delete-invitation', label: this.$tr('deleteInvitation'), item }, + ]; + } else { + return [ + { id: 'make-editor', label: this.$tr('makeEditor'), item }, + { id: 'remove-viewer', label: this.$tr('removeViewer'), item }, + ]; + } + }, + onMenuSelect(selection) { + switch (selection.id) { + case 'resend-invitation': + this.resendInvitation(selection.item.email); + break; + case 'delete-invitation': + this.selected = selection.item; + this.showDeleteInvitation = true; + break; + case 'make-editor': + this.selected = selection.item; + this.showMakeEditor = true; + break; + case 'remove-viewer': + this.selected = selection.item; + this.showRemoveViewer = true; + break; + default: + break; + } + }, getUserText(user) { const nameParams = { first_name: user.first_name, diff --git a/contentcuration/contentcuration/frontend/shared/views/channel/__tests__/channelSharingTable.spec.js b/contentcuration/contentcuration/frontend/shared/views/channel/__tests__/channelSharingTable.spec.js index 5b50e04c02..e12f95fd87 100644 --- a/contentcuration/contentcuration/frontend/shared/views/channel/__tests__/channelSharingTable.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/channel/__tests__/channelSharingTable.spec.js @@ -1,118 +1,292 @@ -import { mount } from '@vue/test-utils'; -import ChannelSharingTable from './../ChannelSharingTable'; -import storeFactory from 'shared/vuex/baseStore'; +import { render, screen } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import { createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import VueRouter from 'vue-router'; +import ChannelSharingTable from '../ChannelSharingTable'; import { SharingPermissions } from 'shared/constants'; -const store = storeFactory(); -const currentUser = { id: 'testId' }; -store.state.session.currentUser = currentUser; +const localVue = createLocalVue(); +localVue.use(Vuex); const channelId = 'test-channel'; -const channelUsers = [{ id: 'other-user' }, currentUser]; -const channelInvitations = [{ id: 'invitation-test' }]; -function makeWrapper(users, invites) { - return mount(ChannelSharingTable, { - store, - propsData: { - channelId, - mode: SharingPermissions.EDIT, - }, - computed: { - getChannelUsers() { - return () => users || channelUsers; +const currentUser = { + id: 'current-user-id', + email: 'current@example.com', + first_name: 'Current', + last_name: 'User', +}; +const otherUser = { + id: 'other-user-id', + email: 'other@example.com', + first_name: 'Other', + last_name: 'User', +}; +const pendingInvitation = { + id: 'pending-invitation', + email: 'pending@example.com', + first_name: 'Pending', + last_name: 'User', + pending: true, + declined: false, +}; + +const mockActions = { + sendInvitation: jest.fn(() => Promise.resolve()), + deleteInvitation: jest.fn(() => Promise.resolve()), + makeEditor: jest.fn(() => Promise.resolve()), + removeViewer: jest.fn(() => Promise.resolve()), + showSnackbar: jest.fn(() => Promise.resolve()), +}; + +const createMockStore = (users = [currentUser, otherUser], invitations = [pendingInvitation]) => { + return new Vuex.Store({ + state: { + session: { + currentUser, }, - getChannelInvitations() { - return () => invites || channelInvitations; + }, + modules: { + channel: { + namespaced: true, + actions: { + sendInvitation: mockActions.sendInvitation, + deleteInvitation: mockActions.deleteInvitation, + makeEditor: mockActions.makeEditor, + removeViewer: mockActions.removeViewer, + }, + getters: { + getChannelUsers: () => () => users, + getChannelInvitations: () => () => invitations, + }, }, }, + actions: { + showSnackbar: mockActions.showSnackbar, + }, }); -} +}; + +const renderComponent = (props = {}, storeOverrides = {}) => { + const defaultProps = { + channelId, + mode: SharingPermissions.VIEW_ONLY, + ...props, + }; -describe('channelSharingTable', () => { - let wrapper; + const store = createMockStore(storeOverrides.users, storeOverrides.invitations); + return render(ChannelSharingTable, { + localVue, + store, + props: defaultProps, + routes: new VueRouter(), + }); +}; + +describe('ChannelSharingTable', () => { beforeEach(() => { - wrapper = makeWrapper(); + jest.clearAllMocks(); + }); + + it('shows "No users found" message when no users exist', () => { + renderComponent({}, { users: [], invitations: [] }); + expect(screen.getByText('No users found')).toBeInTheDocument(); + }); + + it('displays current user correctly', () => { + renderComponent(); + const cell = screen.getByText('Current User (you)'); + expect(cell).toBeInTheDocument(); + const row = cell.closest('tr'); + expect(row).toHaveTextContent('current@example.com'); + expect(row).not.toHaveTextContent('Invite pending'); }); - it('should add current user to top of list', () => { - expect(wrapper.vm.users[0].id).toBe(currentUser.id); - expect(wrapper.vm.users[1].id).toBe(channelUsers[0].id); + it('displays other user correctly', () => { + renderComponent(); + const cell = screen.getByText('Other User'); + expect(cell).toBeInTheDocument(); + const row = cell.closest('tr'); + expect(row).toHaveTextContent('other@example.com'); + expect(row).not.toHaveTextContent('Invite pending'); }); - it('should set all invitations as pending', () => { - expect(wrapper.vm.invitations[0].pending).toBe(true); + it('displays pending user correctly', () => { + renderComponent(); + const cell = screen.getByText('Pending User'); + expect(cell).toBeInTheDocument(); + const row = cell.closest('tr'); + expect(row).toHaveTextContent('pending@example.com'); + expect(row).toHaveTextContent('Invite pending'); }); - describe('confirmation modals', () => { - it('clicking make editor option should open makeEditor confirmation modal', async () => { - await wrapper.setProps({ mode: SharingPermissions.VIEW_ONLY }); - await wrapper.findComponent('[data-test="makeeditor"]').trigger('click'); - expect(wrapper.vm.showMakeEditor).toBe(true); + describe('in edit mode', () => { + it('displays the correct header', () => { + renderComponent({ mode: SharingPermissions.EDIT }); + expect(screen.getByText(/users who can edit/)).toBeInTheDocument(); }); - it('clicking remove viewer option should open removeViewer confirmation modal', async () => { - await wrapper.setProps({ mode: SharingPermissions.VIEW_ONLY }); - await wrapper.findComponent('[data-test="removeviewer"]').trigger('click'); - expect(wrapper.vm.showRemoveViewer).toBe(true); + it('does not display the options dropdown for the current user', () => { + renderComponent({ mode: SharingPermissions.EDIT }); + const row = screen.getByText('Current User (you)').closest('tr'); + const optionsButton = row.querySelector('button'); + expect(optionsButton).not.toBeInTheDocument(); + }); + + it('does not display the options dropdown for users who accepted invitations', () => { + renderComponent({ mode: SharingPermissions.EDIT }); + const row = screen.getByText('Other User').closest('tr'); + const optionsButton = row.querySelector('button'); + expect(optionsButton).not.toBeInTheDocument(); + }); + + it('displays the options dropdown with correct options for pending invitations', async () => { + renderComponent({ mode: SharingPermissions.EDIT }); + const row = screen.getByText('Pending User').closest('tr'); + const optionsButton = row.querySelector('button'); + expect(optionsButton).toBeInTheDocument(); + + await userEvent.click(optionsButton); + + expect(screen.getByText('Resend invitation')).toBeInTheDocument(); + expect(screen.getByText('Delete invitation')).toBeInTheDocument(); + expect(screen.queryByText('Grant edit permissions')).not.toBeInTheDocument(); + expect(screen.queryByText('Revoke view permissions')).not.toBeInTheDocument(); }); }); - describe('actions', () => { - const invite = { - id: 'test-invitation', - email: 'test@testing.com', - declined: false, - }; - const user = { - id: 'test-user', - }; - - beforeEach(() => { - wrapper = makeWrapper([user], [invite]); + describe('in view-only mode', () => { + it('displays the correct header', () => { + renderComponent({ mode: SharingPermissions.VIEW_ONLY }); + expect(screen.getByText(/users who can view/)).toBeInTheDocument(); }); - it('resendInvitation should call sendInvitation', async () => { - const sendInvitation = jest.spyOn(wrapper.vm, 'sendInvitation'); - sendInvitation.mockImplementation(() => Promise.resolve()); - await wrapper.find('[data-test="resend"]').trigger('click'); - expect(sendInvitation).toHaveBeenCalledWith({ - email: invite.email, - channelId, - shareMode: SharingPermissions.EDIT, - }); + it('displays the options dropdown with correct options for pending invitations', async () => { + renderComponent({ mode: SharingPermissions.VIEW_ONLY }); + const row = screen.getByText('Pending User').closest('tr'); + const optionsButton = row.querySelector('button'); + expect(optionsButton).toBeInTheDocument(); + + await userEvent.click(optionsButton); + + expect(screen.getByText('Resend invitation')).toBeInTheDocument(); + expect(screen.getByText('Delete invitation')).toBeInTheDocument(); + expect(screen.queryByText('Grant edit permissions')).not.toBeInTheDocument(); + expect(screen.queryByText('Revoke view permissions')).not.toBeInTheDocument(); }); - it('handleDelete should call deleteInvitation', async () => { - await wrapper.setData({ selected: invite }); - const deleteInvitation = jest.spyOn(wrapper.vm, 'deleteInvitation'); - deleteInvitation.mockImplementation(() => Promise.resolve()); - await wrapper.findComponent('[data-test="confirm-delete"]').trigger('click'); - expect(deleteInvitation).toHaveBeenCalledWith(invite.id); + it('displays the options dropdown with correct options for users who accepted invitations', async () => { + renderComponent({ mode: SharingPermissions.VIEW_ONLY }); + const row = screen.getByText('Other User').closest('tr'); + const optionsButton = row.querySelector('button'); + expect(optionsButton).toBeInTheDocument(); + + await userEvent.click(optionsButton); + + expect(screen.getByText('Grant edit permissions')).toBeInTheDocument(); + expect(screen.getByText('Revoke view permissions')).toBeInTheDocument(); + expect(screen.queryByText('Resend invitation')).not.toBeInTheDocument(); + expect(screen.queryByText('Delete invitation')).not.toBeInTheDocument(); }); + }); + + describe(`clicking 'Resend invitation' menu option`, () => { + it('should resend the invitation and show a success message', async () => { + const user = userEvent.setup(); - it('clicking delete option should open delete confirmation modal', async () => { - await wrapper.findComponent('[data-test="delete"]').trigger('click'); - expect(wrapper.vm.showDeleteInvitation).toBe(true); + renderComponent(); + const row = screen.getByText('Pending User').closest('tr'); + const optionsButton = row.querySelector('button'); + + await userEvent.click(optionsButton); + const resendOption = screen.getByText('Resend invitation'); + await user.click(resendOption); + + expect(mockActions.sendInvitation).toHaveBeenCalledWith(expect.any(Object), { + email: 'pending@example.com', + shareMode: SharingPermissions.VIEW_ONLY, + channelId: channelId, + }); }); + }); + + describe(`clicking 'Delete invitation' menu option`, () => { + it('should open confirmation modal and delete invitation on confirm', async () => { + const dialogMessage = `Are you sure you would like to delete the invitation for pending@example.com?`; + const user = userEvent.setup(); + + renderComponent(); + const row = screen.getByText('Pending User').closest('tr'); + const optionsButton = row.querySelector('button'); + + expect(screen.queryByText(dialogMessage)).not.toBeInTheDocument(); - it('grantEditAccess should call makeEditor', async () => { - await wrapper.setData({ selected: user }); - await wrapper.setProps({ mode: SharingPermissions.VIEW_ONLY }); - const makeEditor = jest.spyOn(wrapper.vm, 'makeEditor'); - makeEditor.mockImplementation(() => Promise.resolve()); - await wrapper.findComponent('[data-test="confirm-makeeditor"]').trigger('click'); - expect(makeEditor).toHaveBeenCalledWith({ userId: user.id, channelId }); + await userEvent.click(optionsButton); + const deleteOption = screen.getByText('Delete invitation'); + await user.click(deleteOption); + + expect(screen.getByText(dialogMessage)).toBeInTheDocument(); + const confirmButton = screen.getByRole('button', { name: 'Delete invitation' }); + await user.click(confirmButton); + + expect(mockActions.deleteInvitation).toHaveBeenCalledWith( + expect.any(Object), + pendingInvitation.id, + ); }); + }); + + describe(`clicking 'Grant edit permissions' menu option`, () => { + it('should open confirmation modal and upgrade user on confirm', async () => { + const dialogMessage = `Are you sure you would like to grant edit permissions to Other User?`; + const user = userEvent.setup(); + + renderComponent(); + const row = screen.getByText('Other User').closest('tr'); + const optionsButton = row.querySelector('button'); + + expect(screen.queryByText(dialogMessage)).not.toBeInTheDocument(); + + await userEvent.click(optionsButton); + const grantOption = screen.getByText('Grant edit permissions'); + await user.click(grantOption); - it('handleRemoveViewer should call removeViewer', async () => { - await wrapper.setData({ selected: user }); - await wrapper.setProps({ mode: SharingPermissions.VIEW_ONLY }); - const removeViewer = jest.spyOn(wrapper.vm, 'removeViewer'); - removeViewer.mockImplementation(() => Promise.resolve()); - await wrapper.findComponent('[data-test="confirm-remove"]').trigger('click'); - expect(removeViewer).toHaveBeenCalledWith({ userId: user.id, channelId }); + expect(screen.getByText(dialogMessage)).toBeInTheDocument(); + const confirmButton = screen.getByRole('button', { name: 'Yes, grant permissions' }); + await user.click(confirmButton); + + expect(mockActions.makeEditor).toHaveBeenCalledWith(expect.any(Object), { + channelId, + userId: otherUser.id, + }); + }); + }); + + describe(`clicking 'Revoke view permissions' menu option`, () => { + it('should open confirmation modal and remove viewer on confirm', async () => { + const dialogMessage = `Are you sure you would like to revoke view permissions for Other User?`; + const user = userEvent.setup(); + + renderComponent(); + const row = screen.getByText('Other User').closest('tr'); + const optionsButton = row.querySelector('button'); + + expect(screen.queryByText(dialogMessage)).not.toBeInTheDocument(); + + await userEvent.click(optionsButton); + const revokeOption = screen.getByText('Revoke view permissions'); + await user.click(revokeOption); + + expect(screen.getByText(dialogMessage)).toBeInTheDocument(); + const confirmButton = screen.getByRole('button', { name: 'Yes, revoke' }); + await user.click(confirmButton); + + expect(mockActions.removeViewer).toHaveBeenCalledWith(expect.any(Object), { + channelId, + userId: otherUser.id, + }); }); }); }); diff --git a/contentcuration/contentcuration/tests/viewsets/test_flagged.py b/contentcuration/contentcuration/tests/viewsets/test_flagged.py index 1f2acf3ac2..f82b1c4b73 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_flagged.py +++ b/contentcuration/contentcuration/tests/viewsets/test_flagged.py @@ -34,7 +34,7 @@ def test_create_flag_event(self): self.client.force_authenticate(user=self.user) flagged_content = self.flag_feedback_object response = self.client.post( - reverse("flagged-list"), + reverse("flagged-events-list"), flagged_content, format="json", ) @@ -46,7 +46,7 @@ def test_create_flag_event_fails_for_flag_test_dev_feature_disabled(self): self.user.save() self.client.force_authenticate(user=self.user) response = self.client.post( - reverse("flagged-list"), + reverse("flagged-events-list"), flagged_content, format="json", ) @@ -58,7 +58,7 @@ def test_create_flag_event_fails_for_flag_test_dev_feature_None(self): self.user.save() self.client.force_authenticate(user=self.user) response = self.client.post( - reverse("flagged-list"), + reverse("flagged-events-list"), flagged_content, format="json", ) @@ -67,7 +67,7 @@ def test_create_flag_event_fails_for_flag_test_dev_feature_None(self): def test_create_flag_event_fails_for_unauthorized_user(self): flagged_content = self.flag_feedback_object response = self.client.post( - reverse("flagged-list"), + reverse("flagged-events-list"), flagged_content, format="json", ) @@ -77,7 +77,7 @@ def test_list_flagged_content_super_admin(self): self.user.is_admin = True self.user.save() self.client.force_authenticate(self.user) - response = self.client.get(reverse("flagged-list"), format="json") + response = self.client.get(reverse("flagged-events-list"), format="json") self.assertEqual(response.status_code, 200, response.content) def test_retreive_fails_for_normal_user(self): @@ -94,18 +94,18 @@ def test_retreive_fails_for_normal_user(self): user=self.user, ) response = self.client.get( - reverse("flagged-detail", kwargs={"pk": flag_feedback_object.id}), + reverse("flagged-events-detail", kwargs={"pk": flag_feedback_object.id}), format="json", ) self.assertEqual(response.status_code, 403, response.content) def test_list_fails_for_normal_user(self): self.client.force_authenticate(user=self.user) - response = self.client.get(reverse("flagged-list"), format="json") + response = self.client.get(reverse("flagged-events-list"), format="json") self.assertEqual(response.status_code, 403, response.content) def test_list_fails_for_user_dev_feature_enabled(self): - response = self.client.get(reverse("flagged-list"), format="json") + response = self.client.get(reverse("flagged-events-list"), format="json") self.assertEqual(response.status_code, 403, response.content) def test_destroy_flagged_content_super_admin(self): @@ -124,7 +124,7 @@ def test_destroy_flagged_content_super_admin(self): user=self.user, ) response = self.client.delete( - reverse("flagged-detail", kwargs={"pk": flag_feedback_object.id}), + reverse("flagged-events-detail", kwargs={"pk": flag_feedback_object.id}), format="json", ) self.assertEqual(response.status_code, 204, response.content) @@ -145,7 +145,7 @@ def test_destroy_flagged_content_fails_for_user_with_feature_flag_disabled(self) user=self.user, ) response = self.client.delete( - reverse("flagged-detail", kwargs={"pk": flag_feedback_object.id}), + reverse("flagged-events-detail", kwargs={"pk": flag_feedback_object.id}), format="json", ) self.assertEqual(response.status_code, 403, response.content) diff --git a/contentcuration/contentcuration/tests/viewsets/test_recommendations.py b/contentcuration/contentcuration/tests/viewsets/test_recommendations.py index e792cfc75b..dcc865244f 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_recommendations.py +++ b/contentcuration/contentcuration/tests/viewsets/test_recommendations.py @@ -186,14 +186,16 @@ def setUp(self): def test_create_recommendations_event(self): recommendations_event = self.recommendations_event_object response = self.client.post( - reverse("recommendations-list"), + reverse("recommendations-events-list"), recommendations_event, format="json", ) self.assertEqual(response.status_code, 201, response.content) def test_list_fails(self): - response = self.client.get(reverse("recommendations-list"), format="json") + response = self.client.get( + reverse("recommendations-events-list"), format="json" + ) self.assertEqual(response.status_code, 405, response.content) def test_retrieve_fails(self): @@ -214,7 +216,9 @@ def test_retrieve_fails(self): user=self.user, ) response = self.client.get( - reverse("recommendations-detail", kwargs={"pk": recommendations_event.id}), + reverse( + "recommendations-events-detail", kwargs={"pk": recommendations_event.id} + ), format="json", ) self.assertEqual(response.status_code, 405, response.content) @@ -242,7 +246,9 @@ def test_update_recommendations_event(self): "breadcrumbs": "#Title#->Updated", } response = self.client.put( - reverse("recommendations-detail", kwargs={"pk": recommendations_event.id}), + reverse( + "recommendations-events-detail", kwargs={"pk": recommendations_event.id} + ), updated_data, format="json", ) @@ -266,7 +272,9 @@ def test_partial_update_recommendations_event(self): user=self.user, ) response = self.client.patch( - reverse("recommendations-detail", kwargs={"pk": recommendations_event.id}), + reverse( + "recommendations-events-detail", kwargs={"pk": recommendations_event.id} + ), {"context": {"model_version": 2}}, format="json", ) @@ -290,7 +298,9 @@ def test_destroy_recommendations_event(self): user=self.user, ) response = self.client.delete( - reverse("recommendations-detail", kwargs={"pk": recommendations_event.id}), + reverse( + "recommendations-events-detail", kwargs={"pk": recommendations_event.id} + ), format="json", ) self.assertEqual(response.status_code, 405, response.content) @@ -345,7 +355,7 @@ def setUp(self): def test_create_recommendations_interaction(self): recommendations_interaction = self.recommendations_interaction_object response = self.client.post( - reverse("recommendations-interaction-list"), + reverse("recommendations-interaction-events-list"), recommendations_interaction, format="json", ) @@ -371,7 +381,7 @@ def test_bulk_create_recommendations_interaction(self): }, ] response = self.client.post( - reverse("recommendations-interaction-list"), + reverse("recommendations-interaction-events-list"), recommendations_interactions, format="json", ) @@ -399,7 +409,7 @@ def test_bulk_create_recommendations_interaction_failure(self): }, ] response = self.client.post( - reverse("recommendations-interaction-list"), + reverse("recommendations-interaction-events-list"), recommendations_interactions, format="json", ) @@ -408,7 +418,7 @@ def test_bulk_create_recommendations_interaction_failure(self): def test_list_fails(self): response = self.client.get( - reverse("recommendations-interaction-list"), format="json" + reverse("recommendations-interaction-events-list"), format="json" ) self.assertEqual(response.status_code, 405, response.content) @@ -423,7 +433,7 @@ def test_retrieve_fails(self): ) response = self.client.get( reverse( - "recommendations-interaction-detail", + "recommendations-interaction-events-detail", kwargs={"pk": recommendations_interaction.id}, ), format="json", @@ -443,7 +453,7 @@ def test_update_recommendations_interaction(self): updated_data["feedback_type"] = "PREVIEWED" response = self.client.put( reverse( - "recommendations-interaction-detail", + "recommendations-interaction-events-detail", kwargs={"pk": recommendations_interaction.id}, ), updated_data, @@ -462,7 +472,7 @@ def test_partial_update_recommendations_interaction(self): ) response = self.client.patch( reverse( - "recommendations-interaction-detail", + "recommendations-interaction-events-detail", kwargs={"pk": recommendations_interaction.id}, ), {"feedback_type": "IMPORTED"}, @@ -481,7 +491,7 @@ def test_destroy_recommendations_interaction(self): ) response = self.client.delete( reverse( - "recommendations-interaction-detail", + "recommendations-interaction-events-detail", kwargs={"pk": recommendations_interaction.id}, ), format="json", diff --git a/contentcuration/contentcuration/urls.py b/contentcuration/contentcuration/urls.py index 59cf09e891..720bbc3d55 100644 --- a/contentcuration/contentcuration/urls.py +++ b/contentcuration/contentcuration/urls.py @@ -76,14 +76,16 @@ def get_redirect_url(self, *args, **kwargs): router.register(r"assessmentitem", AssessmentItemViewSet) router.register(r"admin-users", AdminUserViewSet, basename="admin-users") router.register(r"clipboard", ClipboardViewSet, basename="clipboard") -router.register(r"flagged", FlagFeedbackEventViewSet, basename="flagged") +router.register(r"events/flagged", FlagFeedbackEventViewSet, basename="flagged-events") router.register( - r"recommendations", RecommendationsEventViewSet, basename="recommendations" + r"events/recommendations", + RecommendationsEventViewSet, + basename="recommendations-events", ) router.register( - r"recommendationsinteraction", + r"events/recommendationsinteraction", RecommendationsInteractionEventViewSet, - basename="recommendations-interaction", + basename="recommendations-interaction-events", ) urlpatterns = [