From 2811673965da29290db947b8f3226a1d488c40e4 Mon Sep 17 00:00:00 2001 From: rtibblesbot Date: Wed, 12 Aug 2026 07:41:06 -0700 Subject: [PATCH 1/3] Point setup_wizard TaskResource calls at the _v2 methods cancel/restart/clearAll are wire-identical to their _v2 pairs; every call site discards the resolved value, so the envelope-to-body change is invisible. --- kolibri/plugins/setup_wizard/frontend/app.js | 2 +- .../frontend/views/ImportMultipleUsers.vue | 2 +- .../frontend/views/LoadingTaskPage.vue | 6 +++--- .../views/__tests__/LoadingTaskPage.spec.js | 14 +++++++------- .../views/onboarding-forms/SettingUpKolibri.vue | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/kolibri/plugins/setup_wizard/frontend/app.js b/kolibri/plugins/setup_wizard/frontend/app.js index f1b79b2daca..aa0895f3e6a 100644 --- a/kolibri/plugins/setup_wizard/frontend/app.js +++ b/kolibri/plugins/setup_wizard/frontend/app.js @@ -21,7 +21,7 @@ class SetupWizardModule extends KolibriApp { // Don't call beat because it may cause a save in the session endpoint // while the device provisioning is in progress logging.info('Clearing facility tasks created in previous sessions...'); - TaskResource.clearAll('facility_task'); + TaskResource.clearAll_v2('facility_task'); this.startRootVue(); } } diff --git a/kolibri/plugins/setup_wizard/frontend/views/ImportMultipleUsers.vue b/kolibri/plugins/setup_wizard/frontend/views/ImportMultipleUsers.vue index 0cc33ae412c..07eea38f00c 100644 --- a/kolibri/plugins/setup_wizard/frontend/views/ImportMultipleUsers.vue +++ b/kolibri/plugins/setup_wizard/frontend/views/ImportMultipleUsers.vue @@ -261,7 +261,7 @@ }); if (isFailingTasks) { this.createSnackbar(this.importUserError$()); - TaskResource.clearAll(SoudQueue); + TaskResource.clearAll_v2(SoudQueue); } } }); diff --git a/kolibri/plugins/setup_wizard/frontend/views/LoadingTaskPage.vue b/kolibri/plugins/setup_wizard/frontend/views/LoadingTaskPage.vue index 0afc206d6d2..c3f602c25b3 100644 --- a/kolibri/plugins/setup_wizard/frontend/views/LoadingTaskPage.vue +++ b/kolibri/plugins/setup_wizard/frontend/views/LoadingTaskPage.vue @@ -211,12 +211,12 @@ }); }, retryImport() { - TaskResource.restart(this.loadingTask.id).catch(error => { + TaskResource.restart_v2(this.loadingTask.id).catch(error => { this.handleApiError({ error }); }); }, cancelTask() { - return TaskResource.cancel(this.loadingTask.id); + return TaskResource.cancel_v2(this.loadingTask.id); }, startOver() { this.isPolling = false; @@ -225,7 +225,7 @@ }); }, clearTasks() { - return TaskResource.clearAll(this.queue); + return TaskResource.clearAll_v2(this.queue); }, handleClickContinue() { this.isPolling = false; diff --git a/kolibri/plugins/setup_wizard/frontend/views/__tests__/LoadingTaskPage.spec.js b/kolibri/plugins/setup_wizard/frontend/views/__tests__/LoadingTaskPage.spec.js index 0f47f77bc63..1aca21ee222 100644 --- a/kolibri/plugins/setup_wizard/frontend/views/__tests__/LoadingTaskPage.spec.js +++ b/kolibri/plugins/setup_wizard/frontend/views/__tests__/LoadingTaskPage.spec.js @@ -13,9 +13,9 @@ const { continueAction$, retryAction$, startOverAction$, cancelAction$ } = coreS const { importFacilityAction$ } = syncStrings; jest.mock('kolibri/apiResources/TaskResource', () => ({ - cancel: jest.fn().mockResolvedValue({}), - clearAll: jest.fn().mockResolvedValue({}), - restart: jest.fn().mockResolvedValue({}), + cancel_v2: jest.fn().mockResolvedValue({}), + clearAll_v2: jest.fn().mockResolvedValue({}), + restart_v2: jest.fn().mockResolvedValue({}), list: jest.fn().mockResolvedValue([]), })); @@ -134,7 +134,7 @@ describe('LoadingTaskPage', () => { await userEvent.click(continueButton); expect(sendMock).toHaveBeenCalledWith('CONTINUE'); - expect(TaskResource.clearAll).toHaveBeenCalledTimes(1); + expect(TaskResource.clearAll_v2).toHaveBeenCalledTimes(1); }); it('when task fails, the "retry" button is available', async () => { @@ -148,7 +148,7 @@ describe('LoadingTaskPage', () => { await userEvent.click(retryButton); - expect(TaskResource.restart).toHaveBeenCalledTimes(1); + expect(TaskResource.restart_v2).toHaveBeenCalledTimes(1); }); it('when task fails, the "start over" button is available', async () => { @@ -164,7 +164,7 @@ describe('LoadingTaskPage', () => { await userEvent.click(startOverButton); - expect(TaskResource.clearAll).toHaveBeenCalledTimes(1); + expect(TaskResource.clearAll_v2).toHaveBeenCalledTimes(1); }); it('a cancel request is made when "cancel" is clicked', async () => { @@ -177,7 +177,7 @@ describe('LoadingTaskPage', () => { await fireEvent.click(cancelButton); await waitFor(() => { - expect(TaskResource.cancel).toHaveBeenCalledTimes(1); + expect(TaskResource.cancel_v2).toHaveBeenCalledTimes(1); }); }); }); diff --git a/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/SettingUpKolibri.vue b/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/SettingUpKolibri.vue index 7309cb7ba94..32f3823efe1 100644 --- a/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/SettingUpKolibri.vue +++ b/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/SettingUpKolibri.vue @@ -277,7 +277,7 @@ Lockr.rm('savedState'); // Clear out saved state machine }, clearPollingTasks() { - TaskResource.clearAll(PROVISION_TASK_QUEUE); + TaskResource.clearAll_v2(PROVISION_TASK_QUEUE); }, }, $trs: { From cce74630a24b5a87bd96f2ffe646248cb2d0e6c8 Mon Sep 17 00:00:00 2001 From: rtibblesbot Date: Wed, 12 Aug 2026 07:41:42 -0700 Subject: [PATCH 2/3] Rewrite the setup_wizard custom resource methods onto request Both resources are plugin-local, so the recipe's in-place rewrite applies rather than a _v2 pair. The four methods now resolve with the response body, which LodJoinFacility destructures one level up. --- .../frontend/__tests__/api.spec.js | 72 +++++++++++++++++++ kolibri/plugins/setup_wizard/frontend/api.js | 42 +++++------ .../frontend/views/LodJoinFacility.vue | 4 +- 3 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 kolibri/plugins/setup_wizard/frontend/__tests__/api.spec.js diff --git a/kolibri/plugins/setup_wizard/frontend/__tests__/api.spec.js b/kolibri/plugins/setup_wizard/frontend/__tests__/api.spec.js new file mode 100644 index 00000000000..b89ef85e131 --- /dev/null +++ b/kolibri/plugins/setup_wizard/frontend/__tests__/api.spec.js @@ -0,0 +1,72 @@ +import client from 'kolibri/client'; +import { SetupWizardResource, FacilityImportResource } from '../api'; + +jest.mock('kolibri/client'); +// Not the shared urls mock: it resolves every name to the same string, so a wrong `action` +// would pass. +jest.mock('kolibri/urls', () => ({ + __esModule: true, + default: new Proxy({}, { get: (target, name) => () => name }), +})); + +describe('setup_wizard resources', () => { + beforeEach(() => { + client.__reset(); + }); + + it('posts a remote user creation and resolves the response body', async () => { + client.__setPayload({ status: 201, errors: [] }); + const user = { + facility_id: 'facility_1', + username: 'learner', + password: 'password', + full_name: 'A Learner', + baseurl: 'http://kolibri.remote', + }; + const result = await SetupWizardResource.createuseronremote(user); + expect(client.mock.calls[0][0]).toMatchObject({ + method: 'POST', + url: expect.stringMatching(/setupwizard_createuseronremote$/), + data: user, + }); + expect(result).toEqual({ status: 201, errors: [] }); + }); + + it('gets the facility admins and resolves the list', async () => { + client.__setPayload([{ id: 'admin_1', username: 'admin' }]); + const result = await FacilityImportResource.facilityadmins(); + expect(client.mock.calls[0][0]).toMatchObject({ + method: 'GET', + url: expect.stringMatching(/facilityimport_facilityadmins$/), + }); + expect(result).toEqual([{ id: 'admin_1', username: 'admin' }]); + }); + + it('posts the superuser grant as a body', async () => { + await FacilityImportResource.grantsuperuserpermissions({ + user_id: 'user_1', + password: 'password', + }); + expect(client.mock.calls[0][0]).toMatchObject({ + method: 'POST', + url: expect.stringMatching(/facilityimport_grantsuperuserpermissions$/), + data: { user_id: 'user_1', password: 'password' }, + }); + }); + + it('posts the new superuser as a body', async () => { + const superuser = { + username: 'admin', + full_name: 'An Admin', + password: 'password', + extra_fields: {}, + facility_name: 'Kolibri School', + }; + await FacilityImportResource.createsuperuser(superuser); + expect(client.mock.calls[0][0]).toMatchObject({ + method: 'POST', + url: expect.stringMatching(/facilityimport_createsuperuser$/), + data: superuser, + }); + }); +}); diff --git a/kolibri/plugins/setup_wizard/frontend/api.js b/kolibri/plugins/setup_wizard/frontend/api.js index f4a1bd9348d..24e198aeef1 100644 --- a/kolibri/plugins/setup_wizard/frontend/api.js +++ b/kolibri/plugins/setup_wizard/frontend/api.js @@ -12,36 +12,38 @@ export const SetupWizardResource = new Resource({ name: 'setupwizard', namespace: 'kolibri.plugins.setup_wizard', - createuseronremote({ facility_id, username, password, full_name, baseurl }) { - return this.postListEndpoint('createuseronremote', { - facility_id, - username, - password, - full_name, - baseurl, + async createuseronremote({ facility_id, username, password, full_name, baseurl }) { + const response = await this.request({ + method: 'POST', + action: 'createuseronremote', + data: { facility_id, username, password, full_name, baseurl }, }); + return response.data; }, }); export const FacilityImportResource = new Resource({ name: 'facilityimport', namespace: 'kolibri.plugins.setup_wizard', - grantsuperuserpermissions({ user_id, password }) { - return this.postListEndpoint('grantsuperuserpermissions', { user_id, password }); - }, - createsuperuser({ username, full_name, password, extra_fields, facility_name }) { - return this.postListEndpoint('createsuperuser', { - username, - full_name, - password, - extra_fields, - facility_name, + async grantsuperuserpermissions({ user_id, password }) { + const response = await this.request({ + method: 'POST', + action: 'grantsuperuserpermissions', + data: { user_id, password }, }); + return response.data; }, - facilityadmins() { - return this.getListEndpoint('facilityadmins').then(response => { - return response.data; + async createsuperuser({ username, full_name, password, extra_fields, facility_name }) { + const response = await this.request({ + method: 'POST', + action: 'createsuperuser', + data: { username, full_name, password, extra_fields, facility_name }, }); + return response.data; + }, + async facilityadmins() { + const response = await this.request({ action: 'facilityadmins' }); + return response.data; }, async listfacilitylearners(params) { const { data } = await client({ diff --git a/kolibri/plugins/setup_wizard/frontend/views/LodJoinFacility.vue b/kolibri/plugins/setup_wizard/frontend/views/LodJoinFacility.vue index e71e56a1144..57f467fe1cf 100644 --- a/kolibri/plugins/setup_wizard/frontend/views/LodJoinFacility.vue +++ b/kolibri/plugins/setup_wizard/frontend/views/LodJoinFacility.vue @@ -67,9 +67,7 @@ facility_id: this.facility.id, baseurl: baseurl.slice(0, -1), ...user, - }).then(response => { - const { status, errors } = response.data; - + }).then(({ status, errors }) => { if (status == 201) { const task_name = 'kolibri.core.auth.tasks.peeruserimport'; const params = { From de6de4b560502cbe0b287e868365f5e569533c03 Mon Sep 17 00:00:00 2001 From: rtibblesbot Date: Wed, 12 Aug 2026 07:42:18 -0700 Subject: [PATCH 3/3] Delete the dead listfacilitylearners from setup_wizard No caller anywhere in kolibri/ or packages/, and a line-for-line duplicate of FacilityUserResource.listRemoteFacilityLearners, which ImportIndividualUserForm already calls. Its removal takes the last client/urls imports with it. --- kolibri/plugins/setup_wizard/frontend/api.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/kolibri/plugins/setup_wizard/frontend/api.js b/kolibri/plugins/setup_wizard/frontend/api.js index 24e198aeef1..ef26f258000 100644 --- a/kolibri/plugins/setup_wizard/frontend/api.js +++ b/kolibri/plugins/setup_wizard/frontend/api.js @@ -1,5 +1,3 @@ -import client from 'kolibri/client'; -import urls from 'kolibri/urls'; import { Resource } from 'kolibri/apiResource'; /** @@ -45,19 +43,4 @@ export const FacilityImportResource = new Resource({ const response = await this.request({ action: 'facilityadmins' }); return response.data; }, - async listfacilitylearners(params) { - const { data } = await client({ - url: urls['kolibri:core:remotefacilityauthenticateduserinfo'](), - method: 'POST', - data: params, - }); - - const admin = data.find(user => user.username === params.username); - const students = data.filter(user => !user.roles || !user.roles.length); - - return { - admin, - students, - }; - }, });