Repository: learningequality/kolibri

Issue:
diff stats are sometimes blank

### Observed behavior

note that diff stats are blank:

![image](https://user-images.githubusercontent.com/2367265/70260587-7321d200-1745-11ea-93e1-78a1dccdc8bd.png)


### Expected behavior

diff stats always show

### User-facing consequences

no information in UI

### Errors and logs

no errors in JS or Python

### Steps to reproduce

unknown. observed intermittently - refreshing made the numbers appear:

![image](https://user-images.githubusercontent.com/2367265/70260687-a9f7e800-1745-11ea-9fc8-34cbdae91b5b.png)




### Context

0.13.0 beta 3



Discussion:
This seems like an edge case, that seems to happen when too many identical `channeldiffstats` requests (i.e. same channel and from same source) are triggered very quickly in a way that the client can't cancel or clean them up quickly enough. You can sort of reproduce this by going back and forth between the Manage Channel Page and this page very quickly and wait until this kind of console error appears:

```
  File "/Users/jon/.local/share/virtualenvs/kolibri-UH8N_Uf0/lib/python3.7/site-packages/sqlalchemy/engine/default.py", line 509, in do_execute
    cursor.execute(statement, parameters)
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) table content_contentnode_related already exists [SQL: '\nCREATE TABLE content_contentnode_related (\n\tid INTEGER NOT NULL, \n\tfrom_contentnode_id CHAR(32) NOT NULL, \n\tto_contentnode_id CHAR(32) NOT NULL, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(to_contentnode_id) REFERENCES content_contentnode (id), \n\tFOREIGN KEY(from_contentnode_id) REFERENCES content_contentnode (id)\n)\n\n'] (Background on this error at: http://sqlalche.me/e/e3q8)
```

From the Rest API, the response will be a FAILED task, so I guess we can show an AppError page in that case, which will prompt the user to refresh the page and hopefully get what they need.
> too many requests

This may be one way to trigger it, but during bug bash people were definitely seeing it on the "first try", and that's what I experienced also.

I wonder if it's because there were many concurrent connections during the bug bush. I.e. me triggering many channeldiffstats task on my computer by going back in forth is the same as multiple users trigger channeldiffstats on the server (esp. if they are checking the same channel).

Resolving patch:
diff --git a/kolibri/core/tasks/api.py b/kolibri/core/tasks/api.py
--- a/kolibri/core/tasks/api.py
+++ b/kolibri/core/tasks/api.py
@@ -704,7 +704,7 @@ def _remoteimport(
         job.save_meta()
 
     # Skip importcontent step if updating and no nodes have changed
-    if is_updating and node_ids and len(node_ids) == 0:
+    if is_updating and (node_ids is not None) and len(node_ids) == 0:
         pass
     else:
         call_command(
@@ -748,7 +748,7 @@ def _diskimport(
         job.save_meta()
 
     # Skip importcontent step if updating and no nodes have changed
-    if is_updating and node_ids and len(node_ids) == 0:
+    if is_updating and (node_ids is not None) and len(node_ids) == 0:
         pass
     else:
         call_command(


Test patch:
diff --git a/kolibri/plugins/device/assets/src/views/ManageTasksPage/__test__/TaskPanel.spec.js b/kolibri/plugins/device/assets/src/views/ManageTasksPage/__test__/TaskPanel.spec.js
new file mode 100644
--- /dev/null
+++ b/kolibri/plugins/device/assets/src/views/ManageTasksPage/__test__/TaskPanel.spec.js
@@ -0,0 +1,29 @@
+import { mount } from '@vue/test-utils';
+import TaskPanel from '../TaskPanel';
+
+function makeWrapper(propsData) {
+  const wrapper = mount(TaskPanel, { propsData });
+  return { wrapper };
+}
+
+describe('TaskPanel', () => {
+  const exportTask = {
+    type: 'DISKCONTENTEXPORT',
+    status: 'CANCELED',
+    channel_name: 'Canceled disk export channel test',
+    started_by_username: 'Tester',
+    file_size: 5000,
+    total_resources: 500,
+  };
+
+  it('renders correctly when it is a canceled DISKCONTENTEXPORT task', () => {
+    const { wrapper } = makeWrapper({ task: exportTask });
+    // File size/resource numbers should not be shown for canceled exports
+    expect(wrapper.html()).toMatchSnapshot();
+  });
+
+  it('renders correctly when it is a canceled DISKEXPORT task (bulk export)', () => {
+    const { wrapper } = makeWrapper({ task: { ...exportTask, type: 'DISKEXPORT' } });
+    expect(wrapper.html()).toMatchSnapshot();
+  });
+});
diff --git a/kolibri/plugins/device/assets/src/views/ManageTasksPage/__test__/__snapshots__/TaskPanel.spec.js.snap b/kolibri/plugins/device/assets/src/views/ManageTasksPage/__test__/__snapshots__/TaskPanel.spec.js.snap
new file mode 100644
--- /dev/null
+++ b/kolibri/plugins/device/assets/src/views/ManageTasksPage/__test__/__snapshots__/TaskPanel.spec.js.snap
@@ -0,0 +1,47 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TaskPanel renders correctly when it is a canceled DISKCONTENTEXPORT task 1`] = `
+<div class="task-panel">
+  <div class="icon">
+    <mat-svg name="error" category="alert" style="fill: #d32f2f;" mode="out-in"></mat-svg>
+  </div>
+  <div class="details">
+    <p class="details-status" style="color: rgb(97, 97, 97);">
+      Canceled
+    </p>
+    <h2 class="details-description">
+      Export resources from 'Canceled disk export channel test'
+    </h2>
+    <!---->
+    <!---->
+    <p class="details-startedby" style="color: rgb(97, 97, 97);">
+      Started by 'Tester'
+    </p>
+  </div>
+  <div class="buttons"><button dir="auto" type="button" tabindex="0" class="button KButton-noKey-0_37nh0f"><span>Clear</span>
+      <!----></button></div>
+</div>
+`;
+
+exports[`TaskPanel renders correctly when it is a canceled DISKEXPORT task (bulk export) 1`] = `
+<div class="task-panel">
+  <div class="icon">
+    <mat-svg name="error" category="alert" style="fill: #d32f2f;" mode="out-in"></mat-svg>
+  </div>
+  <div class="details">
+    <p class="details-status" style="color: rgb(97, 97, 97);">
+      Canceled
+    </p>
+    <h2 class="details-description">
+      Export 'Canceled disk export channel test'
+    </h2>
+    <!---->
+    <!---->
+    <p class="details-startedby" style="color: rgb(97, 97, 97);">
+      Started by 'Tester'
+    </p>
+  </div>
+  <div class="buttons"><button dir="auto" type="button" tabindex="0" class="button KButton-noKey-0_37nh0f"><span>Clear</span>
+      <!----></button></div>
+</div>
+`;
diff --git a/kolibri/plugins/device/assets/test/views/SelectDriveModal.spec.js b/kolibri/plugins/device/assets/test/views/SelectDriveModal.spec.js
--- a/kolibri/plugins/device/assets/test/views/SelectDriveModal.spec.js
+++ b/kolibri/plugins/device/assets/test/views/SelectDriveModal.spec.js
@@ -107,6 +107,7 @@ describe('selectDriveModal component', () => {
     const channel = {
       id: 'channel_1',
       version: 1,
+      available: true,
     };
     store.commit('manageContent/wizard/SET_TRANSFERRED_CHANNEL', channel);
     store.state.manageContent.channelList = [{ ...channel }];
@@ -120,6 +121,7 @@ describe('selectDriveModal component', () => {
     const channel = {
       id: 'channel_2',
       version: 6,
+      available: true,
     };
     store.commit('manageContent/wizard/SET_TRANSFERRED_CHANNEL', channel);
     store.state.manageContent.channelList = [{ ...channel }];