Repository: learningequality/kolibri

Issue:
Setup Wizard - Creating a learn-only device by creating or importing a user is not working
## Observed behavior
Creating a learn-only device by creating or importing a user is not working in all scenarios. Observed while testing the latest 0.16alfa19 build and is not observed in alfa18 so the regression is recent. Tried on both Windows and Ubuntu. 

## Expected behavior
Fully functional learn-only device setup flow.

## Steps to reproduce the issue
1. Start the Setup Wizard using the build asset from the following [PR](https://github.com/learningequality/kolibri/pull/10974) or latest develop
2. Go through the Group learning > Learn-only device > Import one or more existing accounts flows
3. Try all 3 options - creating a new user account, importing an individual user account or importing users by entering admin credentials

## Video

https://github.com/learningequality/kolibri/assets/79847249/a16b474c-4c11-40ac-afcf-7a4c40d51c34

https://github.com/learningequality/kolibri/assets/79847249/46ec0482-f468-4605-b3e6-d9e881776406

## Logs

[logs.zip](https://github.com/learningequality/kolibri/files/12047723/logs.zip)

[browser-console.log](https://github.com/learningequality/kolibri/files/12047710/browser-console.log)

## Usage Details
Kolibri 0.16alfa19
Windows 10, Ubuntu - Chrome


Discussion:
@radinamatic 

Resolving patch:
diff --git a/kolibri/core/content/utils/assignment.py b/kolibri/core/content/utils/assignment.py
--- a/kolibri/core/content/utils/assignment.py
+++ b/kolibri/core/content/utils/assignment.py
@@ -87,9 +87,10 @@ def find_all_downloadable_assignments(
         :param transfer_session_id:
         :rtype: list of ContentAssignment
         """
-
-        if dataset_id is None and transfer_session_id is None:
-            raise ValueError("Either dataset_id or transfer_session_id is required")
+        if (dataset_id is None) == (transfer_session_id is None):
+            raise ValueError(
+                "One parameter needs specified: dataset_id and transfer_session_id"
+            )
 
         for manager in CONTENT_ASSIGNMENT_MANAGER_REGISTRY.values():
             for assignment in manager.find_downloadable_assignments(
@@ -104,9 +105,10 @@ def find_all_removable_assignments(cls, dataset_id=None, transfer_session_id=Non
         :param transfer_session_id:optional argument to filter assignments by transfer_session_id
         :rtype: list of ContentAssignment or DeletedAssignment
         """
-
-        if dataset_id is None and transfer_session_id is None:
-            raise ValueError("Either dataset_id or transfer_session_id is required")
+        if (dataset_id is None) == (transfer_session_id is None):
+            raise ValueError(
+                "One parameter needs specified: dataset_id and transfer_session_id"
+            )
 
         for manager in CONTENT_ASSIGNMENT_MANAGER_REGISTRY.values():
             for assignment in manager.find_removable_assignments(
diff --git a/kolibri/core/content/utils/content_request.py b/kolibri/core/content/utils/content_request.py
--- a/kolibri/core/content/utils/content_request.py
+++ b/kolibri/core/content/utils/content_request.py
@@ -62,18 +62,19 @@ def synchronize_content_requests(dataset_id, transfer_session=None):
     if transfer_session is None and dataset_id is None:
         raise ValueError("Either dataset_id or transfer_session_id is required")
 
-    assignments = ContentAssignmentManager.find_all_downloadable_assignments(
-        dataset_id=dataset_id,
-        transfer_session_id=transfer_session.id if transfer_session else None,
-    )
-    removable_assignments = ContentAssignmentManager.find_all_removable_assignments(
-        dataset_id=dataset_id,
-        transfer_session_id=transfer_session.id if transfer_session else None,
-    )
+    # transfer_session_id takes precedence over dataset_id, since it's more specific
+    # (a transfer session should only affect one dataset)
+    find_kwargs = {}
+    if transfer_session:
+        find_kwargs.update(transfer_session_id=transfer_session.id)
+    else:
+        find_kwargs.update(dataset_id=dataset_id)
 
     # process the new assignments
     logger.info("Processing new content assignment requests")
-    for assignment in assignments:
+    for assignment in ContentAssignmentManager.find_all_downloadable_assignments(
+        **find_kwargs
+    ):
         related_removals = ContentRemovalRequest.objects.filter(
             reason=ContentRequestReason.SyncInitiated,
             source_model=assignment.source_model,
@@ -95,7 +96,9 @@ def synchronize_content_requests(dataset_id, transfer_session=None):
 
     # process new removals
     logger.info("Processing new content removal requests")
-    for assignment in removable_assignments:
+    for assignment in ContentAssignmentManager.find_all_removable_assignments(
+        **find_kwargs
+    ):
         related_downloads = ContentDownloadRequest.objects.filter(
             reason=ContentRequestReason.SyncInitiated,
             source_model=assignment.source_model,


Test patch:
diff --git a/kolibri/core/content/test/utils/test_content_request.py b/kolibri/core/content/test/utils/test_content_request.py
new file mode 100644
--- /dev/null
+++ b/kolibri/core/content/test/utils/test_content_request.py
@@ -0,0 +1,62 @@
+import uuid
+
+import mock
+from django.test import TestCase
+
+from kolibri.core.content.utils.content_request import synchronize_content_requests
+
+
+_module = "kolibri.core.content.utils.content_request."
+
+
+def _facility(dataset_id=None):
+    return mock.MagicMock(id=uuid.uuid4().hex, dataset_id=dataset_id)
+
+
+@mock.patch(_module + "Facility.objects.get", new=_facility)
+class ContentRequestsTestCase(TestCase):
+    def setUp(self):
+        super(ContentRequestsTestCase, self).setUp()
+
+        self.dataset_id = uuid.uuid4().hex
+        self.transfer_session = mock.MagicMock()
+        self.transfer_session.id = uuid.uuid4().hex
+
+    @mock.patch(_module + "ContentAssignmentManager.find_all_removable_assignments")
+    @mock.patch(_module + "ContentAssignmentManager.find_all_downloadable_assignments")
+    def test_synchronize_content_requests__dataset_id_passthrough(
+        self,
+        find_all_downloadable_assignments_mock,
+        find_all_removable_assignments_mock,
+    ):
+        find_all_downloadable_assignments_mock.return_value = []
+        find_all_removable_assignments_mock.return_value = []
+
+        synchronize_content_requests(self.dataset_id, None)
+        find_all_downloadable_assignments_mock.assert_called_once_with(
+            dataset_id=self.dataset_id
+        )
+        find_all_removable_assignments_mock.assert_called_once_with(
+            dataset_id=self.dataset_id
+        )
+
+    @mock.patch(_module + "ContentAssignmentManager.find_all_removable_assignments")
+    @mock.patch(_module + "ContentAssignmentManager.find_all_downloadable_assignments")
+    def test_synchronize_content_requests__transfer_session_id_passthrough(
+        self,
+        find_all_downloadable_assignments_mock,
+        find_all_removable_assignments_mock,
+    ):
+        find_all_downloadable_assignments_mock.return_value = []
+        find_all_removable_assignments_mock.return_value = []
+
+        synchronize_content_requests(
+            self.dataset_id,
+            transfer_session=self.transfer_session,
+        )
+        find_all_downloadable_assignments_mock.assert_called_once_with(
+            transfer_session_id=self.transfer_session.id
+        )
+        find_all_removable_assignments_mock.assert_called_once_with(
+            transfer_session_id=self.transfer_session.id
+        )