Repository: learningequality/kolibri

Issue:
Creating a group, then renaming it before refresh returns 403 error

### Observed behavior

After creating a new Learner Group, then renaming it, the request will fail with a 403 error.

In terminal (but not the browser, you also see these logs)

```
ERROR    [u"FacilityDataset ('dataset') not provided, and could not be inferred."]
WARNING  "POST /api/learnergroup/?1533150173390=1533150173390 HTTP/1.1" 403 63
```

But a user can refresh the page and then rename it. The requests are different:

**Request before refresh**

```
POST /api/learnergroup/

{id: "f60ea91861d5d72480b6fb62e68a6e93", name: "A1"}
```

**Request after refresh**
```
PATCH http://localhost:8000/api/learnergroup/f60ea91861d5d72480b6fb62e68a6e93/

{name: "A1"}
```
### Expected behavior

A user can rename a group after creating it.

### User-facing consequences
<!--
Implications and real-world consequences for learners, coaches, admins, and other users of the application
-->

…

### Errors and logs
<!--
Relevant logs from:
 * the command line
 * ~/.kolibri/kolibri.log
 * the browser console

Please wrap errors in triple backticks for clean formatting like this:
```
01:10 info: something happened
01:12 error: something bad happened
```
-->

…

### Steps to reproduce
<!--
Precise steps that someone else can follow in order to see this behavior
-->

…

### Context

Develop branch



Discussion:

Resolving patch:
diff --git a/kolibri/core/logger/serializers.py b/kolibri/core/logger/serializers.py
--- a/kolibri/core/logger/serializers.py
+++ b/kolibri/core/logger/serializers.py
@@ -1,5 +1,3 @@
-import json
-
 from django.db.models import Sum
 from django.utils.timezone import now
 from le_utils.constants import exercises
@@ -62,7 +60,7 @@ class Meta:
                   'end_timestamp', 'completion_timestamp', 'mastery_criterion', 'mastery_level', 'complete')
 
     def get_pastattempts(self, obj):
-        mastery_criterion = json.loads(obj.mastery_criterion)
+        mastery_criterion = obj.mastery_criterion
         exercise_type = mastery_criterion.get('type')
         attemptlogs = AttemptLog.objects.filter(masterylog__summarylog=obj.summarylog) \
                                         .values('correct', 'hinted', 'error') \


Test patch:
diff --git a/kolibri/plugins/coach/assets/test/makeStore.js b/kolibri/plugins/coach/assets/test/makeStore.js
--- a/kolibri/plugins/coach/assets/test/makeStore.js
+++ b/kolibri/plugins/coach/assets/test/makeStore.js
@@ -1,5 +1,5 @@
 import { coreStoreFactory } from 'kolibri.coreVue.vuex.store';
-import pluginModule from '../src/state/pluginModule';
+import pluginModule from '../src/modules/pluginModule';
 
 export default function makeStore() {
   return coreStoreFactory(pluginModule);
diff --git a/kolibri/plugins/coach/assets/test/showPageActions.spec.js b/kolibri/plugins/coach/assets/test/showPageActions.spec.js
--- a/kolibri/plugins/coach/assets/test/showPageActions.spec.js
+++ b/kolibri/plugins/coach/assets/test/showPageActions.spec.js
@@ -1,6 +1,6 @@
 import { jestMockResource } from 'testUtils'; // eslint-disable-line
 import { ClassroomResource, ContentNodeResource, ExamResource } from 'kolibri.resources';
-import { showExamsPage } from '../src/state/actions/exam';
+import { showExamsPage } from '../src/modules/examsRoot/handlers';
 import makeStore from './makeStore';
 
 jestMockResource(ClassroomResource);
@@ -156,7 +156,7 @@ describe('showPage actions for coach exams section', () => {
       await showExamsPage(store, classId)._promise;
       expect(ClassroomResource.getCollection).toHaveBeenCalled();
       expect(ExamResource.getCollection).toHaveBeenCalledWith({ collection: classId });
-      expect(store.state.pageState).toMatchObject({
+      expect(store.state.examsRoot).toMatchObject({
         exams: fakeExamState,
         examsModalSet: false,
         busy: false,
diff --git a/kolibri/plugins/coach/assets/test/views/channel-list-page.spec.js b/kolibri/plugins/coach/assets/test/views/channel-list-page.spec.js
--- a/kolibri/plugins/coach/assets/test/views/channel-list-page.spec.js
+++ b/kolibri/plugins/coach/assets/test/views/channel-list-page.spec.js
@@ -31,7 +31,7 @@ describe('channel list page component', () => {
       { id: 'not_recent_channel', title: 'Not Recent Channel' },
       { id: 'null_channel', title: 'Null Channel' },
     ];
-    store.state.pageState = {
+    store.commit('reports/SET_STATE', {
       showRecentOnly: false,
       tableData: [
         {
@@ -53,7 +53,7 @@ describe('channel list page component', () => {
       viewBy: ViewBy.CHANNEL,
       sortColumn: '',
       sortOrder: '',
-    };
+    });
     // sets clock to 4/19/2017
     now.mockReturnValue(new Date(2017, 3, 19));
   });
@@ -75,7 +75,7 @@ describe('channel list page component', () => {
 
   describe('in "show recent only" mode', () => {
     beforeEach(() => {
-      store.state.pageState.showRecentOnly = true;
+      store.state.reports.showRecentOnly = true;
     });
 
     it('shows the "recent activity" header', () => {
diff --git a/kolibri/plugins/coach/assets/test/views/exam-report-page.spec.js b/kolibri/plugins/coach/assets/test/views/exam-report-page.spec.js
--- a/kolibri/plugins/coach/assets/test/views/exam-report-page.spec.js
+++ b/kolibri/plugins/coach/assets/test/views/exam-report-page.spec.js
@@ -5,18 +5,16 @@ import makeStore from '../makeStore';
 function makeWrapper(options = {}) {
   const store = makeStore();
   store.state.classId = 'class_1';
-  store.state.pageState = {
+  store.commit('examReport/SET_STATE', {
     channelId: 'channel_1',
-    examTakers: [],
+    examTakers: options.examTakers || [],
     exam: {
       title: 'exam',
       question_count: 6,
     },
     learnerGroups: [],
-  };
-  if (options.examTakers) {
-    store.state.pageState.examTakers = options.examTakers;
-  }
+  });
+
   return mount(ExamReportPage, { ...options, store, stubs: ['KRouterLink', 'AssignmentSummary'] });
 }
 
diff --git a/kolibri/plugins/device_management/assets/test/actions/contentTreeViewerActions.spec.js b/kolibri/plugins/device_management/assets/test/actions/contentTreeViewerActions.spec.js
--- a/kolibri/plugins/device_management/assets/test/actions/contentTreeViewerActions.spec.js
+++ b/kolibri/plugins/device_management/assets/test/actions/contentTreeViewerActions.spec.js
@@ -1,3 +1,4 @@
+//
 import omit from 'lodash/fp/omit';
 import { jestMockResource } from 'testUtils'; // eslint-disable-line
 import {
@@ -6,12 +7,8 @@ import {
   ContentNodeGranularResource,
   TaskResource,
 } from 'kolibri.resources';
-import {
-  addNodeForTransfer,
-  removeNodeForTransfer,
-} from '../../src/state/actions/contentTreeViewerActions';
 import { makeNode, contentNodeGranularPayload } from '../utils/data';
-import { updateTreeViewTopic } from '../../src/state/actions/selectContentActions';
+import { updateTreeViewTopic } from '../../src/modules/wizard/handlers';
 import { makeSelectContentPageStore } from '../utils/makeStore';
 
 const simplePath = (...ids) => ids.map(id => ({ id, title: `node_${id}` }));
@@ -21,16 +18,23 @@ jestMockResource(ContentNodeFileSizeResource);
 jestMockResource(ContentNodeGranularResource);
 jestMockResource(TaskResource);
 
+const ADD_NODE_ACTION = 'manageContent/wizard/addNodeForTransfer';
+const REMOVE_NODE_ACTION = 'manageContent/wizard/removeNodeForTransfer';
+
 describe('contentTreeViewer actions', () => {
   let store;
 
   function assertIncludeEquals(expected) {
     // HACK add the hard-coded file sizes to the expected array
-    expect(store.getters.nodesForTransfer.included).toEqual(expected.map(addFileSizes));
+    expect(store.state.manageContent.wizard.nodesForTransfer.included).toEqual(
+      expected.map(addFileSizes)
+    );
   }
 
   function assertOmitEquals(expected) {
-    expect(store.getters.nodesForTransfer.omitted).toEqual(expected.map(addFileSizes));
+    expect(store.state.manageContent.wizard.nodesForTransfer.omitted).toEqual(
+      expected.map(addFileSizes)
+    );
   }
 
   function assertFilesResourcesEqual(
@@ -38,17 +42,19 @@ describe('contentTreeViewer actions', () => {
     expectedResources,
     transferType = 'remoteimport'
   ) {
-    const { fileSize, resources } = store.getters.nodeTransferCounts(transferType);
+    const { fileSize, resources } = store.getters['manageContent/wizard/nodeTransferCounts'](
+      transferType
+    );
     expect(fileSize).toEqual(expectedFiles);
     expect(resources).toEqual(expectedResources);
   }
 
   function setIncludedNodes(nodes) {
-    store.getters.nodesForTransfer.included = nodes.map(addFileSizes);
+    store.state.manageContent.wizard.nodesForTransfer.included = nodes.map(addFileSizes);
   }
 
   function setOmittedNodes(nodes) {
-    store.getters.nodesForTransfer.omitted = nodes.map(addFileSizes);
+    store.state.manageContent.wizard.nodesForTransfer.omitted = nodes.map(addFileSizes);
   }
 
   beforeEach(() => {
@@ -87,7 +93,7 @@ describe('contentTreeViewer actions', () => {
         total_resources: 200,
         on_device_resources: 50,
       });
-      return addNodeForTransfer(store, node_1).then(() => {
+      return store.dispatch(ADD_NODE_ACTION, node_1).then(() => {
         assertIncludeEquals([node_1]);
         assertOmitEquals([]);
         assertFilesResourcesEqual(1, 150, 'remoteimport');
@@ -108,9 +114,10 @@ describe('contentTreeViewer actions', () => {
         total_resources: 200,
         on_device_resources: 25,
       });
-      return addNodeForTransfer(store, node_1)
+      return store
+        .dispatch(ADD_NODE_ACTION, node_1)
         .then(() => {
-          return addNodeForTransfer(store, node_2);
+          return store.dispatch(ADD_NODE_ACTION, node_2);
         })
         .then(() => {
           assertIncludeEquals([node_1, node_2]);
@@ -130,9 +137,10 @@ describe('contentTreeViewer actions', () => {
       });
       const descendantNode_1 = makeNode('1_1_1', { path: simplePath('1', '1_1') });
       const descendantNode_2 = makeNode('1_1_1_1', { path: simplePath('1', '1_1', '1_1_1') });
-      return addNodeForTransfer(store, descendantNode_1)
-        .then(() => addNodeForTransfer(store, descendantNode_2))
-        .then(() => addNodeForTransfer(store, ancestorNode))
+      return store
+        .dispatch(ADD_NODE_ACTION, descendantNode_1)
+        .then(() => store.dispatch(ADD_NODE_ACTION, descendantNode_2))
+        .then(() => store.dispatch(ADD_NODE_ACTION, ancestorNode))
         .then(() => {
           assertIncludeEquals([ancestorNode]);
           assertOmitEquals([]);
@@ -154,7 +162,7 @@ describe('contentTreeViewer actions', () => {
       const siblingNode = makeNode('1_2', { path: simplePath('1') });
       setIncludedNodes([]);
       setOmittedNodes([node, childNode, siblingNode]);
-      return addNodeForTransfer(store, node).then(() => {
+      return store.dispatch(ADD_NODE_ACTION, node).then(() => {
         assertIncludeEquals([node]);
         assertOmitEquals([]);
         assertFilesResourcesEqual(1, 235, 'remoteimport');
@@ -172,11 +180,12 @@ describe('contentTreeViewer actions', () => {
     it('removing a single Node that was originally in `include`', () => {
       // ...straightforwardly removes it from `include`
       const node_1 = makeNode('1_1_1', { path: simplePath('1', '1_1') });
-      return addNodeForTransfer(store, node_1)
+      return store
+        .dispatch(ADD_NODE_ACTION, node_1)
         .then(() => {
           assertIncludeEquals([node_1]);
           assertOmitEquals([]);
-          return removeNodeForTransfer(store, node_1);
+          return store.dispatch(REMOVE_NODE_ACTION, node_1);
         })
         .then(() => {
           assertIncludeEquals([]);
@@ -199,9 +208,10 @@ describe('contentTreeViewer actions', () => {
         total_resources: 20,
         on_device_resources: 10,
       });
-      return addNodeForTransfer(store, parentNode)
+      return store
+        .dispatch(ADD_NODE_ACTION, parentNode)
         .then(() => {
-          return removeNodeForTransfer(store, childNode);
+          return store.dispatch(REMOVE_NODE_ACTION, childNode);
         })
         .then(() => {
           assertIncludeEquals([parentNode]);
@@ -219,12 +229,13 @@ describe('contentTreeViewer actions', () => {
         total_resources: 1222,
       });
       const node_2 = makeNode('1_2', { path: simplePath('1') });
-      return addNodeForTransfer(store, node_1)
+      return store
+        .dispatch(ADD_NODE_ACTION, node_1)
         .then(() => {
-          return addNodeForTransfer(store, node_2);
+          return store.dispatch(ADD_NODE_ACTION, node_2);
         })
         .then(() => {
-          removeNodeForTransfer(store, node_2);
+          store.dispatch(REMOVE_NODE_ACTION, node_2);
         })
         .then(() => {
           assertIncludeEquals([node_1]);
@@ -255,7 +266,7 @@ describe('contentTreeViewer actions', () => {
       // May need to remove or rewrite this test.
       setIncludedNodes([node, childNode, grandchildNode]);
       setOmittedNodes([]);
-      return removeNodeForTransfer(store, node).then(() => {
+      return store.dispatch(REMOVE_NODE_ACTION, node).then(() => {
         assertIncludeEquals([]);
         assertOmitEquals([]);
         assertFilesResourcesEqual(0, 0, 'remoteimport');
@@ -286,7 +297,7 @@ describe('contentTreeViewer actions', () => {
       // Not sure this is possible in practice. On UI, childNode will
       // be indeterminate, so user will need to click it once, making it selected
       // which will then remove grandchildNode. After then, they can de-select it.
-      return removeNodeForTransfer(store, childNode).then(() => {
+      return store.dispatch(REMOVE_NODE_ACTION, childNode).then(() => {
         assertIncludeEquals([topNode]);
         assertOmitEquals([childNode]);
         assertFilesResourcesEqual(0, 5, 'remoteimport');
@@ -317,7 +328,7 @@ describe('contentTreeViewer actions', () => {
       });
       setIncludedNodes([topNode]);
       setOmittedNodes([childNode]);
-      return removeNodeForTransfer(store, siblingNode).then(() => {
+      return store.dispatch(REMOVE_NODE_ACTION, siblingNode).then(() => {
         assertIncludeEquals([]);
         assertOmitEquals([]);
         assertFilesResourcesEqual(0, 0, 'remoteimport');
@@ -327,7 +338,7 @@ describe('contentTreeViewer actions', () => {
     });
 
     it('(EXPORT) when removing a Node leads to an included parent Node being un-selected', () => {
-      store.state.pageState.wizardState.transferType = 'localexport';
+      store.state.manageContent.wizard.transferType = 'localexport';
       const topNode = makeNode('1', {
         path: simplePath(),
         // Make it so that not all resources are installed
@@ -346,7 +357,7 @@ describe('contentTreeViewer actions', () => {
       });
       setIncludedNodes([topNode]);
       setOmittedNodes([childNode]);
-      return removeNodeForTransfer(store, siblingNode).then(() => {
+      return store.dispatch(REMOVE_NODE_ACTION, siblingNode).then(() => {
         assertIncludeEquals([]);
         assertOmitEquals([]);
         assertFilesResourcesEqual(0, 0, 'remoteimport');
@@ -378,7 +389,7 @@ describe('updateTreeViewTopic action', () => {
   });
 
   function assertPathEquals(expected) {
-    expect(store.getters.wizardState.path).toEqual(expected.map(omit('path')));
+    expect(store.state.manageContent.wizard.path).toEqual(expected.map(omit('path')));
   }
 
   it('moving forward by one topic', () => {
diff --git a/kolibri/plugins/device_management/assets/test/actions/showSelectContentPage.spec.js b/kolibri/plugins/device_management/assets/test/actions/showSelectContentPage.spec.js
--- a/kolibri/plugins/device_management/assets/test/actions/showSelectContentPage.spec.js
+++ b/kolibri/plugins/device_management/assets/test/actions/showSelectContentPage.spec.js
@@ -1,6 +1,6 @@
+//
 import { ChannelResource, ContentNodeGranularResource, TaskResource } from 'kolibri.resources';
-import { loadChannelMetaData } from '../../src/state/actions/selectContentActions';
-import { wizardState } from '../../src/state/getters';
+import { loadChannelMetaData } from '../../src/modules/wizard/actions/selectContentActions';
 import { jestMockResource } from 'testUtils'; // eslint-disable-line
 import { defaultChannel } from '../utils/data';
 import { makeSelectContentPageStore } from '../utils/makeStore';
@@ -13,7 +13,7 @@ jestMockResource(TaskResource);
 // resolves successfully
 function hackStoreWatcher(store) {
   setTimeout(() => {
-    store.commit('SET_CONTENT_PAGE_TASKS', [{ id: 'task_1', status: 'COMPLETED' }]);
+    store.commit('manageContent/SET_TASK_LIST', [{ id: 'task_1', status: 'COMPLETED' }]);
   }, 1);
 }
 
@@ -28,8 +28,8 @@ describe('loadChannelMetaData action', () => {
 
   beforeEach(() => {
     store = makeSelectContentPageStore();
-    store.commit('SET_TRANSFERRED_CHANNEL', defaultChannel);
-    store.commit('SET_CHANNEL_LIST', [
+    store.commit('manageContent/wizard/SET_TRANSFERRED_CHANNEL', defaultChannel);
+    store.commit('manageContent/SET_CHANNEL_LIST', [
       { id: 'channel_1', name: 'Installed Channel', root: 'channel_1_root', available: true },
     ]);
     hackStoreWatcher(store);
@@ -57,21 +57,20 @@ describe('loadChannelMetaData action', () => {
   });
 
   function setUpStateForTransferType(transferType) {
-    store.commit('SET_TRANSFER_TYPE', transferType);
-    store.commit('SET_DRIVE_LIST', [
-      {
-        id: `${transferType}_specs_drive`,
-        name: 'test drive',
-      },
-    ]);
-    store.commit('SET_SELECTED_DRIVE', `${transferType}_specs_drive`);
-    store.commit('SET_TRANSFERRED_CHANNEL', {
+    const drive = {
+      id: `${transferType}_specs_drive`,
+      name: 'test drive',
+    };
+    store.commit('manageContent/wizard/SET_TRANSFER_TYPE', transferType);
+    store.commit('manageContent/wizard/SET_DRIVE_LIST', [drive]);
+    store.state.manageContent.wizard.selectedDrive = drive;
+    store.commit('manageContent/wizard/SET_TRANSFERRED_CHANNEL', {
       id: `${transferType}_brand_new_channel`,
     });
   }
 
   function useInstalledChannel() {
-    store.commit('SET_TRANSFERRED_CHANNEL', { id: 'channel_1' });
+    store.commit('manageContent/wizard/SET_TRANSFERRED_CHANNEL', { id: 'channel_1' });
   }
 
   // Tests for common behavior
@@ -105,7 +104,7 @@ describe('loadChannelMetaData action', () => {
     it('errors from startDiskChannelImport are handled', () => {
       TaskResource.startDiskChannelImport.mockRejectedValue();
       return loadChannelMetaData(store).then(() => {
-        expect(wizardState(store.state).status).toEqual('CONTENT_DB_LOADING_ERROR');
+        expect(store.state.manageContent.wizard.status).toEqual('CONTENT_DB_LOADING_ERROR');
       });
     });
   });
@@ -132,7 +131,7 @@ describe('loadChannelMetaData action', () => {
     it('errors from startRemoteChannelImport are handled', () => {
       TaskResource.startRemoteChannelImport.mockRejectedValue();
       return loadChannelMetaData(store).then(() => {
-        expect(wizardState(store.state).status).toEqual('CONTENT_DB_LOADING_ERROR');
+        expect(store.state.manageContent.wizard.status).toEqual('CONTENT_DB_LOADING_ERROR');
       });
     });
   });
diff --git a/kolibri/plugins/device_management/assets/test/utils/makeStore.js b/kolibri/plugins/device_management/assets/test/utils/makeStore.js
--- a/kolibri/plugins/device_management/assets/test/utils/makeStore.js
+++ b/kolibri/plugins/device_management/assets/test/utils/makeStore.js
@@ -1,6 +1,6 @@
 import cloneDeep from 'lodash/cloneDeep';
 import { coreStoreFactory } from 'kolibri.coreVue.vuex.store';
-import pluginModule from '../../src/state/pluginModule';
+import pluginModule from '../../src/modules/pluginModule';
 import { contentNodeGranularPayload } from './data';
 
 const allChannels = [
@@ -67,8 +67,8 @@ const channelsOnDevice = [
 // channel-list-item
 export function makeAvailableChannelsPageStore() {
   const store = coreStoreFactory(cloneDeep(pluginModule));
-  store.state.pageState.channelList = [...channelsOnDevice];
-  Object.assign(store.state.pageState.wizardState, {
+  store.state.manageContent.channelList = [...channelsOnDevice];
+  Object.assign(store.state.manageContent.wizard, {
     driveList: [
       {
         id: 'f9e29616935fbff37913ed46bf20e2c1',
@@ -89,11 +89,11 @@ export function makeAvailableChannelsPageStore() {
 // contentTreeViewer
 export function makeSelectContentPageStore() {
   const store = coreStoreFactory(cloneDeep(pluginModule));
-  Object.assign(store.state.pageState, {
+  Object.assign(store.state.manageContent, {
     channelList: channelsOnDevice,
     taskList: [],
   });
-  Object.assign(store.state.pageState.wizardState, {
+  Object.assign(store.state.manageContent.wizard, {
     availableChannels: [...allChannels],
     transferType: 'localimport',
     transferredChannel: { ...allChannels[0] },
diff --git a/kolibri/plugins/device_management/assets/test/views/available-channels-page.spec.js b/kolibri/plugins/device_management/assets/test/views/available-channels-page.spec.js
--- a/kolibri/plugins/device_management/assets/test/views/available-channels-page.spec.js
+++ b/kolibri/plugins/device_management/assets/test/views/available-channels-page.spec.js
@@ -48,7 +48,7 @@ describe('availableChannelsPage', () => {
   });
 
   function setTransferType(transferType) {
-    store.commit('SET_TRANSFER_TYPE', transferType);
+    store.commit('manageContent/wizard/SET_TRANSFER_TYPE', transferType);
   }
 
   it('in REMOTEIMPORT mode, the unlisted channel button is available', () => {
@@ -71,7 +71,10 @@ describe('availableChannelsPage', () => {
 
   it('in LOCALEXPORT mode, the back link text and title are correct', () => {
     setTransferType('localexport');
-    store.commit('SET_SELECTED_DRIVE', 'f9e29616935fbff37913ed46bf20e2c1');
+    const selectedDrive = store.state.manageContent.wizard.driveList.find(
+      ({ id }) => id === 'f9e29616935fbff37913ed46bf20e2c1'
+    );
+    store.state.manageContent.wizard.selectedDrive = selectedDrive;
     const wrapper = makeWrapper({ store });
     const { titleText } = getElements(wrapper);
     expect(titleText()).toEqual('Your channels');
@@ -79,7 +82,10 @@ describe('availableChannelsPage', () => {
 
   it('in LOCALIMPORT mode, the back link text and title are correct', () => {
     setTransferType('localimport');
-    store.commit('SET_SELECTED_DRIVE', 'f9e29616935fbff37913ed46bf20e2c0');
+    const selectedDrive = store.state.manageContent.wizard.driveList.find(
+      ({ id }) => id === 'f9e29616935fbff37913ed46bf20e2c0'
+    );
+    store.state.manageContent.wizard.selectedDrive = selectedDrive;
     const wrapper = makeWrapper({ store });
     const { titleText } = getElements(wrapper);
     expect(titleText()).toEqual('SANDISK (G:)');
@@ -109,7 +115,7 @@ describe('availableChannelsPage', () => {
   });
 
   it('if there are no channels, then filters do not appear', () => {
-    store.commit('SET_AVAILABLE_CHANNELS', []);
+    store.commit('manageContent/wizard/SET_AVAILABLE_CHANNELS', []);
     const wrapper = makeWrapper({ store });
     const { filters } = getElements(wrapper);
     expect(filters().exists()).toEqual(false);
diff --git a/kolibri/plugins/device_management/assets/test/views/channel-list-item.spec.js b/kolibri/plugins/device_management/assets/test/views/channel-list-item.spec.js
--- a/kolibri/plugins/device_management/assets/test/views/channel-list-item.spec.js
+++ b/kolibri/plugins/device_management/assets/test/views/channel-list-item.spec.js
@@ -39,7 +39,7 @@ function getElements(wrapper) {
     version: () => wrapper.find('.version').text().trim(),
     description: () => wrapper.find('.spec-ref-description').text().trim(),
     thumbnail: () => wrapper.find('.spec-ref-thumbnail'),
-    addTaskMutation: (task) => wrapper.vm.$store.commit('SET_CONTENT_PAGE_TASKS', [task]),
+    addTaskMutation: (task) => wrapper.vm.$store.commit('manageContent/SET_TASK_LIST', [task]),
     dropdownMenu: () => wrapper.find({ name: 'KDropdownMenu' }),
   };
 }
diff --git a/kolibri/plugins/device_management/assets/test/views/channels-grid.spec.js b/kolibri/plugins/device_management/assets/test/views/channels-grid.spec.js
--- a/kolibri/plugins/device_management/assets/test/views/channels-grid.spec.js
+++ b/kolibri/plugins/device_management/assets/test/views/channels-grid.spec.js
@@ -32,7 +32,7 @@ describe('channelsGrid component', () => {
 
   beforeEach(() => {
     store = makeAvailableChannelsPageStore();
-    store.commit('SET_CHANNEL_LIST', [
+    store.commit('manageContent/SET_CHANNEL_LIST', [
       {
         name: 'visible channel',
         id: 'visible_channel',
@@ -45,7 +45,7 @@ describe('channelsGrid component', () => {
 
   it('shows an empty state if there are no visible channels', () => {
     // "visible" meaning it has on-device resources
-    store.commit('SET_CHANNEL_LIST', [
+    store.commit('manageContent/SET_CHANNEL_LIST', [
       {
         name: 'hidden channel',
         id: 'hidden_channel',
@@ -62,12 +62,12 @@ describe('channelsGrid component', () => {
   it('shows a progress bar if channels are loading', () => {
     const wrapper = makeWrapper({ store });
     const { ProgressBar } = getElements(wrapper);
-    store.commit('SET_CHANNEL_LIST_LOADING', true);
+    store.commit('manageContent/SET_CHANNEL_LIST_LOADING', true);
     expect(ProgressBar().isVueInstance()).toEqual(true);
   });
 
   it('channels appear sorted by name', () => {
-    store.commit('SET_CHANNEL_LIST', [
+    store.commit('manageContent/SET_CHANNEL_LIST', [
       {
         name: 'beautiful channel',
         id: 'beautiful_channel',
diff --git a/kolibri/plugins/device_management/assets/test/views/content-tree-viewer.spec.js b/kolibri/plugins/device_management/assets/test/views/content-tree-viewer.spec.js
--- a/kolibri/plugins/device_management/assets/test/views/content-tree-viewer.spec.js
+++ b/kolibri/plugins/device_management/assets/test/views/content-tree-viewer.spec.js
@@ -45,15 +45,15 @@ describe('contentTreeViewer component', () => {
   let store;
 
   function setChildren(children) {
-    store.state.pageState.wizardState.currentTopicNode.children = children;
+    store.state.manageContent.wizard.currentTopicNode.children = children;
   }
 
   function setIncludedNodes(nodes) {
-    store.commit('REPLACE_INCLUDE_LIST', nodes);
+    store.commit('manageContent/wizard/REPLACE_INCLUDE_LIST', nodes);
   }
 
   function setOmittedNodes(nodes) {
-    store.commit('REPLACE_OMIT_LIST', nodes);
+    store.commit('manageContent/wizard/REPLACE_OMIT_LIST', nodes);
   }
 
   beforeEach(() => {
@@ -62,8 +62,8 @@ describe('contentTreeViewer component', () => {
 
   it('in REMOTEIMPORT, all nodes are shown', () => {
     // API does annotate them as being importable, though...
-    store.commit('SET_TRANSFER_TYPE', 'remoteimport');
-    store.commit('SET_CURRENT_TOPIC_NODE', {
+    store.commit('manageContent/wizard/SET_TRANSFER_TYPE', 'remoteimport');
+    store.commit('manageContent/wizard/SET_CURRENT_TOPIC_NODE', {
       id: 'topic',
       children: [
         {
@@ -84,8 +84,8 @@ describe('contentTreeViewer component', () => {
   });
 
   it('if in LOCALIMPORT, then non-importable nodes are filtered from the list', () => {
-    store.commit('SET_TRANSFER_TYPE', 'localimport');
-    store.commit('SET_CURRENT_TOPIC_NODE', {
+    store.commit('manageContent/wizard/SET_TRANSFER_TYPE', 'localimport');
+    store.commit('manageContent/wizard/SET_CURRENT_TOPIC_NODE', {
       id: 'topic',
       children: [
         {
@@ -104,8 +104,8 @@ describe('contentTreeViewer component', () => {
   });
 
   it('in LOCALEXPORT, if a node has available: false, then it is not shown', () => {
-    store.commit('SET_TRANSFER_TYPE', 'localexport');
-    store.commit('SET_CURRENT_TOPIC_NODE', {
+    store.commit('manageContent/wizard/SET_TRANSFER_TYPE', 'localexport');
+    store.commit('manageContent/wizard/SET_CURRENT_TOPIC_NODE', {
       id: 'topic',
       children: [
         {
@@ -143,7 +143,7 @@ describe('contentTreeViewer component', () => {
   });
 
   it('child nodes are annotated with their full path', () => {
-    store.state.pageState.wizardState.path = [
+    store.state.manageContent.wizard.path = [
       { id: 'channel_1', title: 'Channel 1' },
       { id: 'topic_1', title: 'Topic 1' },
     ];
@@ -170,7 +170,7 @@ describe('contentTreeViewer component', () => {
       expect(checkboxIsChecked(wrapper)).toEqual(false);
     });
     it('if any ancestor of the topic is selected, then "Select All" is checked', () => {
-      store.state.pageState.wizardState.path = [{ id: 'channel_1' }];
+      store.state.manageContent.wizard.path = [{ id: 'channel_1' }];
       setIncludedNodes([makeNode('channel_1')]);
       const wrapper = makeWrapper({ store });
       expect(checkboxIsChecked(wrapper)).toEqual(true);
@@ -277,7 +277,7 @@ describe('contentTreeViewer component', () => {
         total_resources: 1,
       });
 
-      store.state.pageState.wizardState.path = simplePath(['channel_1']);
+      store.state.manageContent.wizard.path = simplePath(['channel_1']);
       setChildren([subTopic, subTopic2]);
       setIncludedNodes([subSubTopic]);
       const wrapper = makeWrapper({ store });
diff --git a/kolibri/plugins/device_management/assets/test/views/select-content-page.spec.js b/kolibri/plugins/device_management/assets/test/views/select-content-page.spec.js
--- a/kolibri/plugins/device_management/assets/test/views/select-content-page.spec.js
+++ b/kolibri/plugins/device_management/assets/test/views/select-content-page.spec.js
@@ -1,7 +1,6 @@
 import VueRouter from 'vue-router';
 import { mount } from '@vue/test-utils';
 import SelectContentPage from '../../src/views/SelectContentPage';
-import { wizardState } from '../../src/state/getters';
 import SelectedResourcesSize from '../../src/views/SelectContentPage/SelectedResourcesSize';
 import { makeSelectContentPageStore } from '../utils/makeStore';
 
@@ -43,8 +42,8 @@ function getElements(wrapper) {
 }
 
 function updateMetaChannel(store, updates) {
-  const { transferredChannel } = store.state.pageState.wizardState;
-  store.commit('SET_TRANSFERRED_CHANNEL', {
+  const { transferredChannel } = store.state.manageContent.wizard;
+  store.commit('manageContent/wizard/SET_TRANSFERRED_CHANNEL', {
     ...transferredChannel,
     ...updates,
   });
@@ -106,8 +105,8 @@ describe('selectContentPage', () => {
 
   it('in LOCALIMPORT, clicking the "update" button triggers a downloadChannelMetadata action', () => {
     updateMetaChannel(store, { version: 1000 });
-    store.state.pageState.wizardState.transferType = 'localimport';
-    store.state.pageState.wizardState.selectedDrive = {
+    store.state.manageContent.wizard.transferType = 'localimport';
+    store.state.manageContent.wizard.selectedDrive = {
       driveId: 'drive_1',
     };
     const wrapper = makeWrapper({ store });
@@ -119,7 +118,7 @@ describe('selectContentPage', () => {
 
   it('in REMOTEIMPORT, clicking the "update" button triggers a downloadChannelMetadata action', () => {
     updateMetaChannel(store, { version: 1000 });
-    wizardState(store.state).transferType = 'remoteimport';
+    store.state.manageContent.wizard.transferType = 'remoteimport';
     const wrapper = makeWrapper({ store });
     const { updateButton } = getElements(wrapper);
     const stub = jest.spyOn(wrapper.vm, 'downloadChannelMetadata').mockResolvedValue();
diff --git a/kolibri/plugins/device_management/assets/test/views/select-drive-modal.spec.js b/kolibri/plugins/device_management/assets/test/views/select-drive-modal.spec.js
--- a/kolibri/plugins/device_management/assets/test/views/select-drive-modal.spec.js
+++ b/kolibri/plugins/device_management/assets/test/views/select-drive-modal.spec.js
@@ -2,7 +2,6 @@ import { mount } from '@vue/test-utils';
 import KModal from 'kolibri.coreVue.components.KModal';
 import UiAlert from 'keen-ui/src/UiAlert';
 import SelectDriveModal from '../../src/views/ManageContentPage/SelectTransferSourceModal/SelectDriveModal';
-import { wizardState } from '../../src/state/getters';
 import { makeAvailableChannelsPageStore } from '../utils/makeStore';
 
 SelectDriveModal.methods.refreshDriveList = () => Promise.resolve();
@@ -20,7 +19,7 @@ function makeWrapper(options = {}) {
 
 function makeStore() {
   const store = makeAvailableChannelsPageStore();
-  store.commit('SET_DRIVE_LIST', [
+  store.commit('manageContent/wizard/SET_DRIVE_LIST', [
     {
       id: 'unwritable_drive',
       metadata: { channels: [{ id: 'installed_channel' }] },
@@ -76,7 +75,7 @@ describe('selectDriveModal component', () => {
   });
 
   function setTransferType(transferType) {
-    store.state.pageState.wizardState.transferType = transferType;
+    store.commit('manageContent/wizard/SET_TRANSFER_TYPE', transferType);
   }
 
   it('when drive list is loading, show a message', async () => {
@@ -109,8 +108,8 @@ describe('selectDriveModal component', () => {
       id: 'channel_1',
       version: 1,
     };
-    store.commit('SET_TRANSFERRED_CHANNEL', channel);
-    store.state.pageState.channelList = [{ ...channel }];
+    store.commit('manageContent/wizard/SET_TRANSFERRED_CHANNEL', channel);
+    store.state.manageContent.channelList = [{ ...channel }];
     const wrapper = makeWrapper({ store });
     const { writableImportableRadio } = getElements(wrapper);
     expect(writableImportableRadio().is('input')).toEqual(true);
@@ -122,8 +121,8 @@ describe('selectDriveModal component', () => {
       id: 'channel_2',
       version: 6,
     };
-    store.commit('SET_TRANSFERRED_CHANNEL', channel);
-    store.state.pageState.channelList = [{ ...channel }];
+    store.commit('manageContent/wizard/SET_TRANSFERRED_CHANNEL', channel);
+    store.state.manageContent.channelList = [{ ...channel }];
     const wrapper = makeWrapper({ store });
     const { incompatibleRadio } = getElements(wrapper);
     expect(incompatibleRadio().exists()).toEqual(false);
@@ -140,7 +139,7 @@ describe('selectDriveModal component', () => {
 
   it('in import mode, if there are no drives with content, there is an empty state', () => {
     setTransferType('localimport');
-    wizardState(store.state).driveList.forEach(d => {
+    store.state.manageContent.wizard.driveList.forEach(d => {
       d.metadata.channels = [];
     });
     const wrapper = makeWrapper({ store });
@@ -151,7 +150,7 @@ describe('selectDriveModal component', () => {
 
   it('in export mode, if there are no writable drives, there is an empty state', () => {
     setTransferType('localexport');
-    wizardState(store.state).driveList.forEach(d => {
+    store.state.manageContent.wizard.driveList.forEach(d => {
       d.writable = false;
     });
     const wrapper = makeWrapper({ store });
@@ -188,13 +187,12 @@ describe('selectDriveModal component', () => {
     });
   });
 
-  it('clicking "Cancel" triggers a "cancel" event', () => {
+  it('clicking "Cancel" triggers a "cancel" event', async () => {
     const wrapper = makeWrapper({ store });
     const { cancelButton } = getElements(wrapper);
     cancelButton().trigger('click');
-    wrapper.vm.$nextTick().then(() => {
-      expect(wrapper.emitted().cancel).toHaveLength(1);
-    });
+    await wrapper.vm.$nextTick();
+    expect(wrapper.find({ name: 'KModal' }).emitted().cancel).toHaveLength(1);
   });
 
   // not tested
diff --git a/kolibri/plugins/device_management/assets/test/views/select-import-source-modal.spec.js b/kolibri/plugins/device_management/assets/test/views/select-import-source-modal.spec.js
--- a/kolibri/plugins/device_management/assets/test/views/select-import-source-modal.spec.js
+++ b/kolibri/plugins/device_management/assets/test/views/select-import-source-modal.spec.js
@@ -26,24 +26,13 @@ describe('selectImportSourceModal component', () => {
   });
 
   it('when at select source stage, shows correct modal', () => {
-    store.commit('SET_WIZARD_PAGENAME', 'SELECT_IMPORT_SOURCE');
-    const { els } = makeWrapper({ store });
+    const { els } = makeWrapper({ store, propsData: { pageName: 'SELECT_IMPORT_SOURCE' } });
     expect(els.selectImportSource().exists()).toEqual(true);
     expect(els.selectDrive().exists()).toEqual(false);
   });
 
-  it('when importing, shows the correct modal', () => {
-    store.commit('SET_WIZARD_PAGENAME', 'SELECT_DRIVE');
-    store.commit('SET_TRANSFER_TYPE', 'localimport');
-    const { els } = makeWrapper({ store });
-    expect(els.selectDrive().exists()).toEqual(true);
-    expect(els.selectImportSource().exists()).toEqual(false);
-  });
-
-  it('when exporting, shows the correct modal', () => {
-    store.commit('SET_WIZARD_PAGENAME', 'SELECT_DRIVE');
-    store.commit('SET_TRANSFER_TYPE', 'localexport');
-    const { els } = makeWrapper({ store });
+  it('when exporting or local importing, shows the correct modal', () => {
+    const { els } = makeWrapper({ store, propsData: { pageName: 'SELECT_DRIVE' } });
     expect(els.selectDrive().exists()).toEqual(true);
     expect(els.selectImportSource().exists()).toEqual(false);
   });
diff --git a/kolibri/plugins/facility_management/assets/test/makeStore.js b/kolibri/plugins/facility_management/assets/test/makeStore.js
--- a/kolibri/plugins/facility_management/assets/test/makeStore.js
+++ b/kolibri/plugins/facility_management/assets/test/makeStore.js
@@ -1,5 +1,5 @@
 import { coreStoreFactory } from 'kolibri.coreVue.vuex.store';
-import pluginModule from '../src/state/pluginModule';
+import pluginModule from '../src/modules/pluginModule';
 
 export default function makeStore() {
   return coreStoreFactory(pluginModule);
diff --git a/kolibri/plugins/facility_management/assets/test/state/facilityConfig.spec.js b/kolibri/plugins/facility_management/assets/test/state/facilityConfig.spec.js
--- a/kolibri/plugins/facility_management/assets/test/state/facilityConfig.spec.js
+++ b/kolibri/plugins/facility_management/assets/test/state/facilityConfig.spec.js
@@ -1,9 +1,5 @@
 import { FacilityResource, FacilityDatasetResource } from 'kolibri.resources';
-import {
-  showFacilityConfigPage,
-  saveFacilityConfig,
-  resetFacilityConfig,
-} from '../../src/state/actions/facilityConfig';
+import { showFacilityConfigPage } from '../../src/modules/facilityConfig/handlers';
 import { jestMockResource } from 'testUtils'; // eslint-disable-line
 import makeStore from '../makeStore';
 
@@ -42,14 +38,13 @@ describe('facility config page actions', () => {
     });
     FacilityResource.__resetMocks();
     FacilityDatasetResource.__resetMocks();
-    store.state.pageState = {};
   });
 
   describe('showFacilityConfigPage action', () => {
     it('when resources load successfully', () => {
       FacilityStub.__getModelFetchReturns(fakeFacility);
       DatasetStub.__getCollectionFetchReturns(fakeDatasets);
-      const expectedPageState = {
+      const expectedState = {
         facilityDatasetId: 'dataset_2',
         facilityName: 'Nalanda Maths',
         settings: expect.objectContaining({
@@ -71,14 +66,14 @@ describe('facility config page actions', () => {
       return showFacilityConfigPage(store).then(() => {
         expect(DatasetStub.getCollection).toHaveBeenCalledWith({ facility_id: 1 });
         expect(commitStub).toHaveBeenCalledWith(
-          'SET_PAGE_STATE',
-          expect.objectContaining(expectedPageState)
+          'facilityConfig/SET_STATE',
+          expect.objectContaining(expectedState)
         );
       });
     });
 
     describe('error handling', () => {
-      const expectedPageState = {
+      const expectedState = {
         facilityName: '',
         settings: null,
         notification: 'PAGELOAD_FAILURE',
@@ -88,8 +83,8 @@ describe('facility config page actions', () => {
         DatasetStub.__getCollectionFetchReturns(fakeDatasets);
         return showFacilityConfigPage(store).then(() => {
           expect(commitStub).toHaveBeenCalledWith(
-            'SET_PAGE_STATE',
-            expect.objectContaining(expectedPageState)
+            'facilityConfig/SET_STATE',
+            expect.objectContaining(expectedState)
           );
         });
       });
@@ -99,8 +94,8 @@ describe('facility config page actions', () => {
         DatasetStub.__getCollectionFetchReturns('incomprehensible error', true);
         return showFacilityConfigPage(store).then(() => {
           expect(commitStub).toHaveBeenCalledWith(
-            'SET_PAGE_STATE',
-            expect.objectContaining(expectedPageState)
+            'facilityConfig/SET_STATE',
+            expect.objectContaining(expectedState)
           );
         });
       });
@@ -109,7 +104,7 @@ describe('facility config page actions', () => {
 
   describe('saveFacilityConfig action', () => {
     beforeEach(() => {
-      store.state.pageState = {
+      store.commit('facilityConfig/SET_STATE', {
         facilityDatasetId: 1000,
         settings: {
           learnerCanEditName: true,
@@ -118,7 +113,7 @@ describe('facility config page actions', () => {
           learnerCanDeleteAccount: true,
           learnerCanSignUp: false,
         },
-      };
+      });
     });
 
     it('when save is successful', () => {
@@ -132,30 +127,42 @@ describe('facility config page actions', () => {
       // IRL returns the updated Model
       const saveStub = DatasetStub.__getModelSaveReturns('ok');
 
-      return saveFacilityConfig(store).then(() => {
+      return store.dispatch('facilityConfig/saveFacilityConfig').then(() => {
         expect(DatasetStub.getModel).toHaveBeenCalledWith(1000, {});
         expect(saveStub).toHaveBeenCalledWith(expect.objectContaining(expectedRequest), false);
-        expect(commitStub).toHaveBeenCalledWith('CONFIG_PAGE_NOTIFY', 'SAVE_SUCCESS');
+        expect(store.state.facilityConfig.notification).toEqual('SAVE_SUCCESS');
       });
     });
 
     it('when save fails', () => {
       const saveStub = DatasetStub.__getModelSaveReturns('heck no', true);
-      return saveFacilityConfig(store).then(() => {
+      return store.dispatch('facilityConfig/saveFacilityConfig').then(() => {
         expect(saveStub).toHaveBeenCalled();
-        expect(commitStub).toHaveBeenCalledWith('CONFIG_PAGE_NOTIFY', 'SAVE_FAILURE');
-        expect(commitStub).toHaveBeenCalledWith('CONFIG_PAGE_UNDO_SETTINGS_CHANGE');
+        expect(store.state.facilityConfig.notification).toEqual('SAVE_FAILURE');
+        expect(store.state.facilityConfig.settings).toEqual({
+          learnerCanEditName: true,
+          learnerCanEditUsername: false,
+          learnerCanEditPassword: true,
+          learnerCanDeleteAccount: true,
+          learnerCanSignUp: false,
+        });
       });
     });
 
-    it('resetFacilityConfig action dispatches a modify all settings action before saving', () => {
+    it('resetFacilityConfig action dispatches resets settings and makes a save request', () => {
       const saveStub = DatasetStub.__getModelSaveReturns('ok default');
-      return resetFacilityConfig(store).then(() => {
-        expect(commitStub).toHaveBeenCalledWith(
-          'CONFIG_PAGE_MODIFY_ALL_SETTINGS',
-          expect.any(Object)
-        );
+      return store.dispatch('facilityConfig/resetFacilityConfig').then(() => {
         expect(saveStub).toHaveBeenCalled();
+        expect(store.state.facilityConfig.settings).toEqual({
+          allowGuestAccess: true,
+          learnerCanEditUsername: true,
+          learnerCanEditName: true,
+          learnerCanEditPassword: true,
+          learnerCanSignUp: true,
+          learnerCanDeleteAccount: true,
+          learnerCanLoginWithNoPassword: false,
+          showDownloadButtonInLearn: false,
+        });
       });
     });
   });
diff --git a/kolibri/plugins/facility_management/assets/test/views/facility-config-page.spec.js b/kolibri/plugins/facility_management/assets/test/views/facility-config-page.spec.js
--- a/kolibri/plugins/facility_management/assets/test/views/facility-config-page.spec.js
+++ b/kolibri/plugins/facility_management/assets/test/views/facility-config-page.spec.js
@@ -1,24 +1,12 @@
-import Vuex from 'vuex';
 import { mount } from '@vue/test-utils';
 import ConfigPage from '../../src/views/FacilityConfigPage';
-import confirmResetModal from '../../src/views/FacilityConfigPage/ConfirmResetModal';
+import makeStore from '../makeStore';
 
 function makeWrapper(propsData = {}) {
-  const store = new Vuex.Store({
-    state: {
-      pageState: {
-        settings: {
-          learner_can_edit_username: false,
-        },
-      },
-      // A fake part of the state to confirm efficacy of mutation
-      TEST_DROPBOX: null,
-    },
-    // TODO bring in real mutations instead of faking them
-    mutations: {
-      CONFIG_PAGE_MODIFY_SETTING(state, payload) {
-        state.TEST_DROPBOX = payload;
-      },
+  const store = makeStore();
+  store.commit('facilityConfig/SET_STATE', {
+    settings: {
+      learnerCanEditUsername: false,
     },
   });
   return mount(ConfigPage, { propsData, store });
@@ -51,13 +39,10 @@ describe('facility config page view', () => {
     const wrapper = makeWrapper();
     const { checkbox } = getElements(wrapper);
     checkbox().trigger('click');
-    expect(wrapper.vm.$store.state.TEST_DROPBOX).toEqual({
-      name: 'learnerCanEditUsername',
-      value: true,
-    });
+    expect(wrapper.vm.$store.state.facilityConfig.settings.learnerCanEditUsername).toEqual(true);
   });
 
-  it('clicking save button dispatches a save action', () => {
+  it('clicking save button dispatches a save action', async () => {
     const wrapper = makeWrapper();
     const { mock } = (wrapper.vm.saveFacilityConfig = jest.fn().mockResolvedValue());
     const { saveButton } = getElements(wrapper);
@@ -86,7 +71,7 @@ describe('facility config page view', () => {
   it('confirming reset calls the reset action and closes modal', () => {
     const wrapper = makeWrapper();
     const { resetButton, confirmResetModal } = getElements(wrapper);
-    const { mock } = (wrapper.vm.resetFacilityConfig = jest.fn());
+    const { mock } = (wrapper.vm.resetFacilityConfig = jest.fn().mockResolvedValue());
     resetButton().trigger('click');
     assertModalIsUp(wrapper);
     confirmResetModal().vm.$emit('submit');
diff --git a/kolibri/plugins/learn/assets/test/makeStore.js b/kolibri/plugins/learn/assets/test/makeStore.js
--- a/kolibri/plugins/learn/assets/test/makeStore.js
+++ b/kolibri/plugins/learn/assets/test/makeStore.js
@@ -1,5 +1,5 @@
 import { coreStoreFactory } from 'kolibri.coreVue.vuex.store';
-import pluginModule from '../src/state/pluginModule';
+import pluginModule from '../src/modules/pluginModule';
 
 export default function makeStore(options = {}) {
   const store = coreStoreFactory(pluginModule);
diff --git a/kolibri/plugins/learn/assets/test/state/prepareLearnApp.spec.js b/kolibri/plugins/learn/assets/test/state/prepareLearnApp.spec.js
--- a/kolibri/plugins/learn/assets/test/state/prepareLearnApp.spec.js
+++ b/kolibri/plugins/learn/assets/test/state/prepareLearnApp.spec.js
@@ -1,6 +1,6 @@
 import { jestMockResource } from 'testUtils'; // eslint-disable-line import/no-unresolved
 import { MembershipResource } from 'kolibri.resources';
-import prepareLearnApp from '../../src/state/prepareLearnApp';
+import { prepareLearnApp } from '../../src/modules/coreLearn/actions';
 import makeStore from '../makeStore';
 
 jestMockResource(MembershipResource);
@@ -8,7 +8,7 @@ jestMockResource(MembershipResource);
 describe('prepareLearnApp action', () => {
   let store;
 
-  const getMemberships = ({ state }) => state.learnAppState.memberships;
+  const getMemberships = ({ state }) => state.memberships;
   const setSessionUserId = userId => {
     store.state.core.session.user_id = userId;
   };
diff --git a/kolibri/plugins/learn/assets/test/util/makeStore.js b/kolibri/plugins/learn/assets/test/util/makeStore.js
--- a/kolibri/plugins/learn/assets/test/util/makeStore.js
+++ b/kolibri/plugins/learn/assets/test/util/makeStore.js
@@ -1,37 +1,6 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import cloneDeep from 'lodash/cloneDeep';
-import mutations from '../../src/state/mutations';
-import initialState from '../../src/state/initialState';
+import { coreStoreFactory } from 'kolibri.coreVue.vuex.store';
+import pluginModule from '../../src/state/pluginModule';
 
-Vue.use(Vuex);
-
-export default function makeStore(options = {}) {
-  const { pageName } = options;
-  const coreState = {
-    core: {
-      session: {
-        kind: [],
-      },
-      logging: {
-        mastery: {},
-        summary: {},
-      },
-    },
-  };
-  const state = cloneDeep(initialState);
-  Object.assign(state, coreState);
-  state.pageState.content = {};
-  state.pageState.topic = {};
-  if (pageName) {
-    state.pageName = pageName;
-  }
-
-  return new Vuex.Store({
-    mutations: {
-      ...mutations,
-      CORE_SET_ERROR() {},
-    },
-    state,
-  });
+export default function makeStore() {
+  return coreStoreFactory(pluginModule);
 }
diff --git a/kolibri/plugins/learn/assets/test/views/assessment-wrapper.spec.js b/kolibri/plugins/learn/assets/test/views/assessment-wrapper.spec.js
--- a/kolibri/plugins/learn/assets/test/views/assessment-wrapper.spec.js
+++ b/kolibri/plugins/learn/assets/test/views/assessment-wrapper.spec.js
@@ -1,5 +1,5 @@
 import Vue from 'vue';
-import Vuex from 'vuex';
+import makeStore from '../makeStore';
 import assessmentWrapper from '../../src/views/AssessmentWrapper';
 
 const createComponent = (totalattempts, pastattempts, masteryModel) => {
@@ -7,27 +7,24 @@ const createComponent = (totalattempts, pastattempts, masteryModel) => {
     id: 'test',
     kind: 'test',
   };
-  const store = new Vuex.Store({
-    state: {
-      core: {
-        logging: {
-          mastery: {
-            totalattempts,
-            pastattempts,
-          },
-        },
-        session: {
-          user_id: 'test',
-        },
-      },
-      pageState: {
-        content: {
-          assessmentIds: [],
-          masteryModel: masteryModel || {},
-          randomize: false,
-        },
+  const store = makeStore();
+  store.state.core = {
+    logging: {
+      mastery: {
+        totalattempts,
+        pastattempts,
       },
     },
+    session: {
+      user_id: 'test',
+    },
+  };
+  store.commit('topicsTree/SET_STATE', {
+    content: {
+      assessmentIds: [],
+      masteryModel: masteryModel || {},
+      randomize: false,
+    },
   });
   const Component = Vue.extend(assessmentWrapper);
   return new Component({ propsData, store });
diff --git a/kolibri/plugins/learn/assets/test/views/breadcrumbs.spec.js b/kolibri/plugins/learn/assets/test/views/breadcrumbs.spec.js
--- a/kolibri/plugins/learn/assets/test/views/breadcrumbs.spec.js
+++ b/kolibri/plugins/learn/assets/test/views/breadcrumbs.spec.js
@@ -5,6 +5,7 @@ import Breadcrumbs from '../../src/views/Breadcrumbs';
 import makeStore from '../makeStore';
 import { PageNames } from '../../src/constants';
 
+//
 const router = new VueRouter({
   routes: [
     { path: '/recommended', name: PageNames.RECOMMENDED },
@@ -46,7 +47,7 @@ describe('learn page breadcrumbs', () => {
 
     it('shows correct breadcrumbs at a Channel', () => {
       const store = makeStore({ pageName: PageNames.TOPICS_CHANNEL });
-      store.state.pageState.channel = {
+      store.state.topicsTree.channel = {
         title: 'Recommended Channel',
       };
       const wrapper = makeWrapper({ store });
@@ -60,11 +61,11 @@ describe('learn page breadcrumbs', () => {
 
     it('shows correct breadcrumbs at a non-Channel Topic', () => {
       const store = makeStore({ pageName: PageNames.TOPICS_TOPIC });
-      store.state.pageState.channel = {
+      store.state.topicsTree.channel = {
         title: 'Another Recommended Channel',
         root_id: 'another_channel',
       };
-      store.state.pageState.topic = {
+      store.state.topicsTree.topic = {
         title: 'Recommended Topic',
         breadcrumbs: [{ id: 'previous_topic', title: 'Previous Topic' }],
       };
@@ -90,11 +91,11 @@ describe('learn page breadcrumbs', () => {
 
     it('shows correct breadcrumbs at a Content Item', () => {
       const store = makeStore({ pageName: PageNames.TOPICS_CONTENT });
-      store.state.pageState.channel = {
+      store.state.topicsTree.channel = {
         title: 'Another Recommended Channel',
         root_id: 'another_channel',
       };
-      store.state.pageState.content = {
+      store.state.topicsTree.content = {
         title: 'Recommended Item',
         breadcrumbs: [{ id: 'previous_topic', title: 'Previous Topic' }],
       };
diff --git a/kolibri/plugins/learn/assets/test/views/learn-index.spec.js b/kolibri/plugins/learn/assets/test/views/learn-index.spec.js
--- a/kolibri/plugins/learn/assets/test/views/learn-index.spec.js
+++ b/kolibri/plugins/learn/assets/test/views/learn-index.spec.js
@@ -21,7 +21,7 @@ function makeWrapper(options) {
       contentUnavailablePage: true,
       CoreBase: '<div><slot></slot></div>',
       topicsPage: true,
-      totalPoints: true,
+      TotalPoints: true,
     },
     router,
   });
@@ -45,7 +45,7 @@ describe('learn plugin index page', () => {
     store.state.core.session.user_id = 'test';
   };
   const setMemberships = memberships => {
-    store.state.learnAppState.memberships = memberships;
+    store.state.memberships = memberships;
   };
   const setPageName = pageName => {
     store.state.pageName = pageName;
@@ -53,7 +53,6 @@ describe('learn plugin index page', () => {
 
   beforeEach(() => {
     store = makeStore();
-    store.state.pageState.topic = {};
   });
 
   it('there are no tabs if showing content unavailable page', () => {
diff --git a/kolibri/plugins/user/assets/test/makeStore.js b/kolibri/plugins/user/assets/test/makeStore.js
--- a/kolibri/plugins/user/assets/test/makeStore.js
+++ b/kolibri/plugins/user/assets/test/makeStore.js
@@ -1,5 +1,5 @@
 import { coreStoreFactory } from 'kolibri.coreVue.vuex.store';
-import pluginModule from '../src/state/pluginModule';
+import pluginModule from '../src/modules/pluginModule';
 
 export default function makeStore() {
   return coreStoreFactory(pluginModule);
diff --git a/kolibri/plugins/user/assets/test/util/makeStore.js b/kolibri/plugins/user/assets/test/util/makeStore.js
deleted file mode 100644
--- a/kolibri/plugins/user/assets/test/util/makeStore.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { coreStoreFactory } from 'kolibri.coreVue.vuex.store';
-import pluginModule from '../../src/state/pluginModule';
-
-export default function makeStore() {
-  return coreStoreFactory(pluginModule);
-}
diff --git a/kolibri/plugins/user/assets/test/views/profile-page.spec.js b/kolibri/plugins/user/assets/test/views/profile-page.spec.js
--- a/kolibri/plugins/user/assets/test/views/profile-page.spec.js
+++ b/kolibri/plugins/user/assets/test/views/profile-page.spec.js
@@ -7,8 +7,11 @@ ProfilePage.mixins = [];
 
 function makeWrapper() {
   const store = makeStore();
-  store.state.pageState.passwordState = {};
-  store.commit('SET_PROFILE_PASSWORD_MODAL', false);
+  store.commit('profile/SET_STATE', {
+    passwordState: {
+      modal: false,
+    },
+  });
   return mount(ProfilePage, {
     store,
   });
diff --git a/kolibri/plugins/user/assets/test/views/user-page.spec.js b/kolibri/plugins/user/assets/test/views/user-page.spec.js
--- a/kolibri/plugins/user/assets/test/views/user-page.spec.js
+++ b/kolibri/plugins/user/assets/test/views/user-page.spec.js
@@ -4,8 +4,6 @@ import makeStore from '../makeStore';
 
 jest.mock('kolibri.urls');
 
-jest.mock('kolibri.urls');
-
 function makeWrapper() {
   return mount(UserPage, {
     store: makeStore(),