Repository: learningequality/kolibri

Issue:
Users should not be able to create a second account created on a LoD

## Observed behavior
Since setting up multiple learner accounts on LoD seems to have been re-enabled, I decided to go that road on the same setup from #11816. After signing out with the first user, I successfully created another on the same server facility. I was able to see it in Facility-Users, and to enroll it in the same class as the first user. However, it's is unclear what is supposed to happen with syncing this user/account, as its status on both the server and the LoD remained 'Not connected to server', even though the device continued to sync under the first user.

LoD | server
-- | --
https://github.com/learningequality/kolibri/assets/1457929/fbd34f3d-dd6f-4a00-8200-52e8a6a6a38a | <img width="840" alt="2024-01-30_04-09-40" src="https://github.com/learningequality/kolibri/assets/1457929/6060b1a2-8b4e-4406-a1f9-51f87a66ccfb">

## Errors and logs
[Huawei-MediapadT5-AndroidStudio-second-user-created.log](https://github.com/learningequality/kolibri/files/14092368/Huawei-MediapadT5-AndroidStudio-second-user-created.log)

## Expected behavior
Some decisions needed here.

## User-facing consequences
Confusion...? Can I create a second user on an LoD and expect it to sync my progress with the facility that I created it in?

## Steps to reproduce
…

## Context

 * Kolibri version: 0.16b13
 * Operating system: Windows 7 (server) and Android 8 LoD




Discussion:
I am not completely clear on what happened here.

When you say you "created" a second user on the LOD, what exact steps did you take to do that? 
Apologies for not being clearer about the steps:

1. Sign out from the first LoD user (created in #11816)
2. 'Create an account' button was available on the sign in page (see the recording), so I decided to test it.
3. The new account was successfully created and I could see it on the VM in the server's Facility - Users, and could even enroll it to a class.
4. However, the status remained as 'Not connected to server' for a while, and eventually changed to 'Not recently synced', and no class assignments appeared on this user's Home, just 'Explore channels' that were already on the device.

@pcenov also replicated this, and we were both unsure what are actually the expectations here, as we do not seem to have the Gherkin scenarios for this workflow in 0.16. If the creation of the second user account on one LoD is allowed for the same server facility, is this account having the same _treatment_ as the first one (enrolled in the class and receiving assignments), is it supposed to sync 'independently' from the first user...?

OK, so the bug here is that create account should not be appearing there. Users on an LOD cannot currently create a new account except during the setup wizard. 

Resolving patch:
diff --git a/kolibri/core/auth/api.py b/kolibri/core/auth/api.py
--- a/kolibri/core/auth/api.py
+++ b/kolibri/core/auth/api.py
@@ -80,6 +80,7 @@
 from kolibri.core.device.utils import allow_guest_access
 from kolibri.core.device.utils import allow_other_browsers_to_connect
 from kolibri.core.device.utils import APP_AUTH_TOKEN_COOKIE_NAME
+from kolibri.core.device.utils import is_full_facility_import
 from kolibri.core.device.utils import valid_app_key_on_request
 from kolibri.core.logger.models import UserSessionLog
 from kolibri.core.mixins import BulkCreateMixin
@@ -182,6 +183,10 @@ class Meta:
         fields = ["facility_id"]
 
 
+def _is_full_facility_import(dataset):
+    return is_full_facility_import(dataset["id"])
+
+
 class FacilityDatasetViewSet(ValuesViewset):
     permission_classes = (KolibriAuthPermissions,)
     filter_backends = (
@@ -207,7 +212,10 @@ class FacilityDatasetViewSet(ValuesViewset):
         "preset",
     )
 
-    field_map = {"allow_guest_access": lambda x: allow_guest_access()}
+    field_map = {
+        "allow_guest_access": lambda x: allow_guest_access(),
+        "is_full_facility_import": _is_full_facility_import,
+    }
 
     def get_queryset(self):
         return FacilityDataset.objects.filter(
@@ -767,7 +775,11 @@ class SignUpViewSet(viewsets.GenericViewSet, CreateModelMixin):
     serializer_class = FacilityUserSerializer
 
     def check_can_signup(self, serializer):
-        if not serializer.validated_data["facility"].dataset.learner_can_sign_up:
+        facility = serializer.validated_data["facility"]
+        if (
+            not facility.dataset.learner_can_sign_up
+            or not facility.dataset.full_facility_import
+        ):
             raise PermissionDenied("Cannot sign up to this facility")
 
     def perform_create(self, serializer):
diff --git a/kolibri/core/auth/models.py b/kolibri/core/auth/models.py
--- a/kolibri/core/auth/models.py
+++ b/kolibri/core/auth/models.py
@@ -249,6 +249,13 @@ def reset_to_default_settings(self, preset=None):
             setattr(self, key, value)
         self.save()
 
+    @cached_property
+    def full_facility_import(self):
+        """
+        Returns True if this user is a member of a facility that has been fully imported.
+        """
+        return is_full_facility_import(self.id)
+
 
 class AbstractFacilityDataModel(FacilityDataSyncableModel):
     """
diff --git a/kolibri/core/device/utils.py b/kolibri/core/device/utils.py
--- a/kolibri/core/device/utils.py
+++ b/kolibri/core/device/utils.py
@@ -19,6 +19,7 @@
 from kolibri.core.content.constants.schema_versions import MIN_CONTENT_SCHEMA_VERSION
 from kolibri.utils.android import ANDROID_PLATFORM_SYSTEM_VALUE
 from kolibri.utils.android import on_android
+from kolibri.utils.lru_cache import lru_cache
 
 logger = logging.getLogger(__name__)
 
@@ -486,6 +487,7 @@ def get_device_info(version=DEVICE_INFO_VERSION):
     return info
 
 
+@lru_cache()
 def is_full_facility_import(dataset_id):
     """
     Returns True if this the dataset_id holds a facility that has been fully imported.


Test patch: