From c33882ec4a1b72ad4045e625ed221de868dc584b Mon Sep 17 00:00:00 2001 From: adviti Date: Mon, 4 Dec 2023 22:17:03 -0500 Subject: [PATCH 01/37] Attempted to fix race condition through using locks. Added 2 test cases to stimulate race condition in worker.py --- .../core/tasks/test/taskrunner/test_worker.py | 55 +++++++++++++++++++ kolibri/core/tasks/worker.py | 46 +++++++++++----- 2 files changed, 88 insertions(+), 13 deletions(-) diff --git a/kolibri/core/tasks/test/taskrunner/test_worker.py b/kolibri/core/tasks/test/taskrunner/test_worker.py index a6e8e7170d..201757a51f 100644 --- a/kolibri/core/tasks/test/taskrunner/test_worker.py +++ b/kolibri/core/tasks/test/taskrunner/test_worker.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import threading import time import pytest @@ -49,6 +50,60 @@ def worker(): b.storage.clear(force=True) b.shutdown() +def test_keyerror_prevention(worker): + # Create a job with the same ID as the one in worker.enqueue_job_runs_job + job = Job(id, args=(9,)) + worker.storage.enqueue_job(job, QUEUE) + + # Simulate a race condition by having another thread try to delete the future + # while the job is running + def delete_future(): + time.sleep(0.5) # Wait for the job to start + del worker.future_job_mapping[job.job_id] + + # Start the delete_future thread + delete_thread = threading.Thread(target=delete_future) + delete_thread.start() + + while job.state != "COMPLETED": + job = worker.storage.get_job(job.job_id) + time.sleep(0.1) + + assert job.state == "COMPLETED" + +def test_keyerror_prevention_multiple_jobs(worker): + # Create multiple jobs with the same ID to trigger the race condition + job1 = Job(id, args=(9,)) + job2 = Job(id, args=(9,)) + + # Enqueue the first job + worker.storage.enqueue_job(job1, QUEUE) + + # Simulate a race condition by having another thread try to delete the future + # while the first job is running + def delete_future(): + time.sleep(0.5) # Wait for the first job to start + del worker.future_job_mapping[job1.job_id] + + # Start the delete_future thread + delete_thread = threading.Thread(target=delete_future) + delete_thread.start() + + # Enqueue the second job + worker.storage.enqueue_job(job2, QUEUE) + + while job1.state != "COMPLETED": + job1 = worker.storage.get_job(job1.job_id) + time.sleep(0.1) + + assert job1.state == "COMPLETED" + + # Wait for the second job to complete + while job2.state != "COMPLETED": + job2 = worker.storage.get_job(job2.job_id) + time.sleep(0.1) + + assert job2.state == "COMPLETED" @pytest.mark.django_db class TestWorker: diff --git a/kolibri/core/tasks/worker.py b/kolibri/core/tasks/worker.py index 3d52d5fef8..be88318f50 100644 --- a/kolibri/core/tasks/worker.py +++ b/kolibri/core/tasks/worker.py @@ -1,3 +1,4 @@ +import threading import logging from concurrent.futures import CancelledError @@ -45,6 +46,10 @@ def __init__(self, connection, regular_workers=2, high_workers=1): # Key: job_id, Value: future object self.future_job_mapping = {} + # Locks to synchronize access to dictionaries + self.job_future_mapping_lock = threading.Lock() + self.future_job_mapping_lock = threading.Lock() + self.storage = Storage(connection) self.requeue_stalled_jobs() @@ -81,16 +86,23 @@ def start_workers(self): def handle_finished_future(self, future): # get back the job assigned to the future - job = self.job_future_mapping[future] - - # Clean up tracking of this job and its future - del self.job_future_mapping[future] - del self.future_job_mapping[job.job_id] - - try: - future.result() - except CancelledError: - self.storage.mark_job_as_canceled(job.job_id) + job = None + + # Acquire locks before accessing dictionaries + with self.job_future_mapping_lock: + with self.future_job_mapping_lock: + if future in self.job_future_mapping: + # get back the job assigned to the future + job = self.job_future_mapping[future] + # Clean up tracking of this job and its future + del self.job_future_mapping[future] + del self.future_job_mapping[job.job_id] + + if job: + try: + future.result() + except CancelledError: + self.storage.mark_job_as_canceled(job.job_id) def shutdown(self, wait=True): logger.info("Asking job schedulers to shut down.") @@ -170,9 +182,17 @@ def start_next_job(self, job): job_id=job.job_id, ) - # assign the futures to a dict, mapping them to a job - self.job_future_mapping[future] = job - self.future_job_mapping[job.job_id] = future + # Acquire locks before modifying dictionaries + with self.job_future_mapping_lock: + with self.future_job_mapping_lock: + # Check if the job ID already exists in the future_job_mapping dictionary + if job.job_id in self.future_job_mapping: + logger.error(f"Job ID {job.job_id} is already in future_job_mapping.") + raise ValueError(f"Duplicate job ID: {job.job_id}") + + # assign the futures to a dict, mapping them to a job + self.job_future_mapping[future] = job + self.future_job_mapping[job.job_id] = future # callback for when the future is now! future.add_done_callback(self.handle_finished_future) From 1c8514b3307a14a2e86990462069038ceb7eba32 Mon Sep 17 00:00:00 2001 From: Adviti Mishra Date: Mon, 4 Dec 2023 22:22:29 -0500 Subject: [PATCH 02/37] Create test_pr_1150_keyerror.yml Workflow for task_runner tests in multiprocessing mode in the macOS environment to stimulate conditions for the KeyError issue --- .github/workflows/test_pr_1150_keyerror.yml | 52 +++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/test_pr_1150_keyerror.yml diff --git a/.github/workflows/test_pr_1150_keyerror.yml b/.github/workflows/test_pr_1150_keyerror.yml new file mode 100644 index 0000000000..49791e951f --- /dev/null +++ b/.github/workflows/test_pr_1150_keyerror.yml @@ -0,0 +1,52 @@ +name: KeyError Issue + +on: + push: + branches: + - develop + - 'release-v**' + pull_request: + branches: + - develop + - 'release-v**' + +jobs: + replicate_issue: + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Install Node.js and Yarn + run: | + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm install 16.20.0 + npm install -g yarn + + - name: Create and activate virtual environment + run: | + python -m venv venv + source venv/bin/activate + + - name: Install Python dependencies + run: | + pip install -r requirements.txt --upgrade + pip install -r requirements/dev.txt --upgrade + pip install -e . + pip install -r requirements/docs.txt + pip install -r requirements/build.txt + pip install pytest + pip install mock + + - name: Run tests in multiprocessing mode + run: | + export KOLIBRI_USE_WORKER_MULTIPROCESSING=True + pytest kolibri/core/tasks/test/taskrunner/ From a8c41f82a9aab952af3fa095073e3ed751b11ffa Mon Sep 17 00:00:00 2001 From: Adviti Mishra Date: Mon, 4 Dec 2023 22:30:55 -0500 Subject: [PATCH 03/37] Update test_pr_1150_keyerror.yml running specific test cases made to test the fix --- .github/workflows/test_pr_1150_keyerror.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_1150_keyerror.yml b/.github/workflows/test_pr_1150_keyerror.yml index 49791e951f..f2e9a89c8c 100644 --- a/.github/workflows/test_pr_1150_keyerror.yml +++ b/.github/workflows/test_pr_1150_keyerror.yml @@ -49,4 +49,4 @@ jobs: - name: Run tests in multiprocessing mode run: | export KOLIBRI_USE_WORKER_MULTIPROCESSING=True - pytest kolibri/core/tasks/test/taskrunner/ + pytest kolibri/core/tasks/test/taskrunner/test_worker.py From 799e3f65d0a5f2fb521f5b9e5442c46c7f0df1dc Mon Sep 17 00:00:00 2001 From: adviti Date: Tue, 5 Dec 2023 17:16:07 -0500 Subject: [PATCH 04/37] Deleted our GitHub workflow that tests our fix because existing workflows already run all tests --- .github/workflows/test_pr_1150_keyerror.yml | 52 --------------------- 1 file changed, 52 deletions(-) delete mode 100644 .github/workflows/test_pr_1150_keyerror.yml diff --git a/.github/workflows/test_pr_1150_keyerror.yml b/.github/workflows/test_pr_1150_keyerror.yml deleted file mode 100644 index f2e9a89c8c..0000000000 --- a/.github/workflows/test_pr_1150_keyerror.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: KeyError Issue - -on: - push: - branches: - - develop - - 'release-v**' - pull_request: - branches: - - develop - - 'release-v**' - -jobs: - replicate_issue: - runs-on: macos-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.9' - - - name: Install Node.js and Yarn - run: | - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm install 16.20.0 - npm install -g yarn - - - name: Create and activate virtual environment - run: | - python -m venv venv - source venv/bin/activate - - - name: Install Python dependencies - run: | - pip install -r requirements.txt --upgrade - pip install -r requirements/dev.txt --upgrade - pip install -e . - pip install -r requirements/docs.txt - pip install -r requirements/build.txt - pip install pytest - pip install mock - - - name: Run tests in multiprocessing mode - run: | - export KOLIBRI_USE_WORKER_MULTIPROCESSING=True - pytest kolibri/core/tasks/test/taskrunner/test_worker.py From 6d34bd5b152bc31ae20dbec1117e81de94e4562a Mon Sep 17 00:00:00 2001 From: adviti Date: Tue, 5 Dec 2023 21:40:06 -0500 Subject: [PATCH 05/37] Made formatted string compatible with Python 2.7 --- kolibri/core/tasks/worker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kolibri/core/tasks/worker.py b/kolibri/core/tasks/worker.py index be88318f50..b2aa32ab00 100644 --- a/kolibri/core/tasks/worker.py +++ b/kolibri/core/tasks/worker.py @@ -187,8 +187,8 @@ def start_next_job(self, job): with self.future_job_mapping_lock: # Check if the job ID already exists in the future_job_mapping dictionary if job.job_id in self.future_job_mapping: - logger.error(f"Job ID {job.job_id} is already in future_job_mapping.") - raise ValueError(f"Duplicate job ID: {job.job_id}") + logger.error("Job ID {} is already in future_job_mapping.".format(job.job_id)) + raise ValueError("Duplicate job ID: {}".format(job.job_id)) # assign the futures to a dict, mapping them to a job self.job_future_mapping[future] = job From 44b3e0e2e1c3dbac775730f26d31e442af67a549 Mon Sep 17 00:00:00 2001 From: ozer550 Date: Mon, 8 Jan 2024 17:55:24 +0530 Subject: [PATCH 06/37] set localStorage key after channels are not present --- .../assets/src/views/ManageContentPage/index.vue | 8 +++----- .../learn/assets/src/views/LibraryPage/index.vue | 13 +++++-------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/kolibri/plugins/device/assets/src/views/ManageContentPage/index.vue b/kolibri/plugins/device/assets/src/views/ManageContentPage/index.vue index bac04b3e3b..b91f3fe37e 100644 --- a/kolibri/plugins/device/assets/src/views/ManageContentPage/index.vue +++ b/kolibri/plugins/device/assets/src/views/ManageContentPage/index.vue @@ -3,7 +3,7 @@ @@ -182,12 +182,10 @@ welcomeModalVisible() { return ( this.welcomeModalVisibleState && - window.localStorage.getItem(welcomeDismissalKey) !== 'true' + window.localStorage.getItem(welcomeDismissalKey) !== 'true' && + !this.installedChannelsWithResources.length > 0 ); }, - areChannelsImported() { - return this.installedChannelsWithResources.length > 0; - }, }, watch: { installedChannelsWithResources: { diff --git a/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue b/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue index 10583f7365..41d3644b23 100644 --- a/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue +++ b/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue @@ -4,9 +4,7 @@ @@ -416,7 +414,7 @@ }; }, computed: { - ...mapGetters(['isLearnerOnlyImport']), + ...mapGetters(['isLearner']), ...mapState({ welcomeModalVisibleState: 'welcomeModalVisible', }), @@ -429,7 +427,9 @@ welcomeModalVisible() { return ( this.welcomeModalVisibleState && - window.localStorage.getItem(welcomeDismissalKey) !== 'true' + window.localStorage.getItem(welcomeDismissalKey) !== 'true' && + !(this.rootNodes.length > 0) && + !this.isLearner ); }, showOtherLibraries() { @@ -479,9 +479,6 @@ studioId() { return KolibriStudioId; }, - areChannelsImported() { - return this.rootNodes.length > 0; - }, }, watch: { rootNodes(newNodes) { From eb5b178a820f9fbb7a449f742bbe74b97cc36133 Mon Sep 17 00:00:00 2001 From: ozer550 Date: Thu, 11 Jan 2024 16:41:47 +0530 Subject: [PATCH 07/37] fix welcome modal appearance for first imported user in LOD --- kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue b/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue index 41d3644b23..0ba50dae0b 100644 --- a/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue +++ b/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue @@ -414,7 +414,7 @@ }; }, computed: { - ...mapGetters(['isLearner']), + ...mapGetters(['isLearnerOnlyImport', 'canManageContent']), ...mapState({ welcomeModalVisibleState: 'welcomeModalVisible', }), @@ -429,7 +429,8 @@ this.welcomeModalVisibleState && window.localStorage.getItem(welcomeDismissalKey) !== 'true' && !(this.rootNodes.length > 0) && - !this.isLearner + this.canManageContent && + !this.isLearnerOnlyImport ); }, showOtherLibraries() { From 4a8f4d634f76397318423a43d6e88c9efda19c05 Mon Sep 17 00:00:00 2001 From: nick2432 Date: Thu, 11 Jan 2024 22:52:56 +0530 Subject: [PATCH 08/37] fix:username-disambiguation --- kolibri/core/auth/api.py | 6 +++++- kolibri/core/auth/backends.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/kolibri/core/auth/api.py b/kolibri/core/auth/api.py index 3783172bd7..b107c39f4d 100644 --- a/kolibri/core/auth/api.py +++ b/kolibri/core/auth/api.py @@ -880,7 +880,11 @@ def create(self, request): ], status=status.HTTP_400_BAD_REQUEST, ) - + except FacilityUser.MultipleObjectsReturned: + # Handle case of multiple matching usernames + unauthenticated_user = FacilityUser.objects.get( + username__exact=username, facility=facility_id + ) user = authenticate(username=username, password=password, facility=facility_id) if user is not None and user.is_active: # Correct password, and the user is marked "active" diff --git a/kolibri/core/auth/backends.py b/kolibri/core/auth/backends.py index 927dbeeb0e..6c59b861aa 100644 --- a/kolibri/core/auth/backends.py +++ b/kolibri/core/auth/backends.py @@ -25,15 +25,23 @@ def authenticate(self, request, username=None, password=None, **kwargs): :keyword facility: a Facility object or facility ID :return: A FacilityUser instance if successful, or None if authentication failed. """ - users = FacilityUser.objects.filter(username__iexact=username) facility = kwargs.get(FACILITY_CREDENTIAL_KEY, None) + # First, attempt case-sensitive login + user = self.authenticate_case_sensitive(username, password, facility) + if user: + return user + + # If case-sensitive login fails, attempt case-insensitive login + user = self.authenticate_case_insensitive(username, password, facility) + return user + def authenticate_case_sensitive(self, username, password, facility): + users = FacilityUser.objects.filter(username=username) if facility: users = users.filter(facility=facility) + for user in users: if user.check_password(password): return user - # Allow login without password for learners for facilities that allow this. - # Must specify the facility, to prevent accidental logins elif ( facility and user.dataset.learner_can_login_with_no_password @@ -43,6 +51,22 @@ def authenticate(self, request, username=None, password=None, **kwargs): return user return None + def authenticate_case_insensitive(self, username, password, facility): + users = FacilityUser.objects.filter(username__iexact=username) + if facility: + users = users.filter(facility=facility) + + for user in users: + if user.check_password(password): + return user + elif ( + facility + and user.dataset.learner_can_login_with_no_password + and not user.roles.count() + and not user.is_superuser + ): + return user + return None def get_user(self, user_id): """ Gets a user. Auth backends are required to implement this. From de2566c2ab97336ac15c4412b1523f2e0e412275 Mon Sep 17 00:00:00 2001 From: nick2432 Date: Fri, 12 Jan 2024 08:24:51 +0530 Subject: [PATCH 09/37] fix:username-disambiguation --- kolibri/core/auth/backends.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/kolibri/core/auth/backends.py b/kolibri/core/auth/backends.py index 6c59b861aa..c13213ac49 100644 --- a/kolibri/core/auth/backends.py +++ b/kolibri/core/auth/backends.py @@ -34,14 +34,14 @@ def authenticate(self, request, username=None, password=None, **kwargs): # If case-sensitive login fails, attempt case-insensitive login user = self.authenticate_case_insensitive(username, password, facility) return user - def authenticate_case_sensitive(self, username, password, facility): - users = FacilityUser.objects.filter(username=username) + def _authenticate_users(self, users, password, facility): if facility: users = users.filter(facility=facility) - for user in users: if user.check_password(password): return user + # Allow login without password for learners for facilities that allow this. + # Must specify the facility, to prevent accidental logins elif ( facility and user.dataset.learner_can_login_with_no_password @@ -50,23 +50,13 @@ def authenticate_case_sensitive(self, username, password, facility): ): return user return None + def authenticate_case_sensitive(self, username, password, facility): + users = FacilityUser.objects.filter(username=username) + return self._authenticate_users(users, password, facility) def authenticate_case_insensitive(self, username, password, facility): users = FacilityUser.objects.filter(username__iexact=username) - if facility: - users = users.filter(facility=facility) - - for user in users: - if user.check_password(password): - return user - elif ( - facility - and user.dataset.learner_can_login_with_no_password - and not user.roles.count() - and not user.is_superuser - ): - return user - return None + return self._authenticate_users(users, password, facility) def get_user(self, user_id): """ Gets a user. Auth backends are required to implement this. From 5bd813d1d1dd8dbd9b5d5579a885f9954876e2c7 Mon Sep 17 00:00:00 2001 From: nick2432 Date: Fri, 12 Jan 2024 10:40:05 +0530 Subject: [PATCH 10/37] fix:username-disambiguation --- kolibri/core/auth/backends.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kolibri/core/auth/backends.py b/kolibri/core/auth/backends.py index c13213ac49..939640bedc 100644 --- a/kolibri/core/auth/backends.py +++ b/kolibri/core/auth/backends.py @@ -34,6 +34,7 @@ def authenticate(self, request, username=None, password=None, **kwargs): # If case-sensitive login fails, attempt case-insensitive login user = self.authenticate_case_insensitive(username, password, facility) return user + def _authenticate_users(self, users, password, facility): if facility: users = users.filter(facility=facility) @@ -50,6 +51,7 @@ def _authenticate_users(self, users, password, facility): ): return user return None + def authenticate_case_sensitive(self, username, password, facility): users = FacilityUser.objects.filter(username=username) return self._authenticate_users(users, password, facility) @@ -57,6 +59,7 @@ def authenticate_case_sensitive(self, username, password, facility): def authenticate_case_insensitive(self, username, password, facility): users = FacilityUser.objects.filter(username__iexact=username) return self._authenticate_users(users, password, facility) + def get_user(self, user_id): """ Gets a user. Auth backends are required to implement this. From f4147965e0be50d343420e17c47983384e562141 Mon Sep 17 00:00:00 2001 From: nick2432 Date: Sat, 13 Jan 2024 09:34:06 +0530 Subject: [PATCH 11/37] fix:username-disambiguation --- kolibri/core/auth/test/test_api.py | 39 ++++++++++++++++++++++++++ kolibri/core/auth/test/test_backend.py | 29 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/kolibri/core/auth/test/test_api.py b/kolibri/core/auth/test/test_api.py index 3932ef058e..aa88ed4e1b 100644 --- a/kolibri/core/auth/test/test_api.py +++ b/kolibri/core/auth/test/test_api.py @@ -1063,6 +1063,12 @@ def setUpTestData(cls): cls.cr = ClassroomFactory.create(parent=cls.facility) cls.cr.add_coach(cls.admin) cls.session_store = import_module(settings.SESSION_ENGINE).SessionStore() + cls.user1 = FacilityUserFactory.create( + username="Shared_Username", facility=cls.facility + ) + cls.user2 = FacilityUserFactory.create( + username="shared_username", facility=cls.facility + ) def test_login_and_logout_superuser(self): self.client.post( @@ -1147,6 +1153,39 @@ def test_session_update_last_active(self): new_expire_date = self.client.session.get_expiry_date() self.assertLess(expire_date, new_expire_date) + def test_case_insensitive_matching_usernames(self): + # Attempt to log in with a different case username + response_user1 = self.client.post( + reverse("kolibri:core:session-list"), + data={ + "username": "shared_username", + "password": DUMMY_PASSWORD, + "facility": self.facility.id, + }, + format="json", + ) + + # Assert the expected behavior based on the application's design + # This may involve checking the response status code or the content. + self.assertEqual(response_user1.status_code, 200) + response_user2 = self.client.post( + reverse("kolibri:core:session-list"), + data={ + "username": "shared_username", + "password": DUMMY_PASSWORD, + "facility": self.facility.id, + }, + format="json", + ) + + # Assert the expected behavior for the second user + self.assertEqual(response_user2.status_code, 200) + # Add more assertions as needed for the second user + + # Cleanup: Delete the created users + self.user1.delete() + self.user2.delete() + class SignUpBase(object): @classmethod diff --git a/kolibri/core/auth/test/test_backend.py b/kolibri/core/auth/test/test_backend.py index 60e0918fdf..9a06c32d49 100644 --- a/kolibri/core/auth/test/test_backend.py +++ b/kolibri/core/auth/test/test_backend.py @@ -17,6 +17,11 @@ def setUpTestData(cls): cls.user = FacilityUser(username="Mike", facility=cls.facility) cls.user.set_password("foo") cls.user.save() + cls.user_other_mike = FacilityUser.objects.create( + username="mike", facility=cls.facility + ) + cls.user_other_mike.set_password("foo") + cls.user_other_mike.save() cls.request = mock.Mock() def test_facility_user_authenticated(self): @@ -27,6 +32,14 @@ def test_facility_user_authenticated(self): ), ) + def test_facility_user_other_mike_authenticated(self): + self.assertEqual( + self.user_other_mike, + FacilityUserBackend().authenticate( + self.request, username="mike", password="foo", facility=self.facility + ), + ) + def test_facility_user_authenticated__facility_id(self): self.assertEqual( self.user, @@ -35,6 +48,14 @@ def test_facility_user_authenticated__facility_id(self): ), ) + def test_facility_user_other_mike_authenticated__facility_id(self): + self.assertEqual( + self.user_other_mike, + FacilityUserBackend().authenticate( + self.request, username="mike", password="foo", facility=self.facility.pk + ), + ) + def test_facility_user_authentication_does_not_require_facility(self): self.assertEqual( self.user, @@ -43,6 +64,14 @@ def test_facility_user_authentication_does_not_require_facility(self): ), ) + def test_facility_user_other_mike_authentication_does_not_require_facility(self): + self.assertEqual( + self.user_other_mike, + FacilityUserBackend().authenticate( + self.request, username="mike", password="foo" + ), + ) + def test_device_owner_not_authenticated(self): self.assertIsNone( FacilityUserBackend().authenticate( From 1684f877db84920fa668a0af76839ec1604f78dc Mon Sep 17 00:00:00 2001 From: nick2432 Date: Sat, 13 Jan 2024 09:36:26 +0530 Subject: [PATCH 12/37] fix:username-disambiguation --- kolibri/core/auth/test/test_api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/kolibri/core/auth/test/test_api.py b/kolibri/core/auth/test/test_api.py index aa88ed4e1b..d2c1359cc7 100644 --- a/kolibri/core/auth/test/test_api.py +++ b/kolibri/core/auth/test/test_api.py @@ -1154,7 +1154,6 @@ def test_session_update_last_active(self): self.assertLess(expire_date, new_expire_date) def test_case_insensitive_matching_usernames(self): - # Attempt to log in with a different case username response_user1 = self.client.post( reverse("kolibri:core:session-list"), data={ @@ -1166,8 +1165,8 @@ def test_case_insensitive_matching_usernames(self): ) # Assert the expected behavior based on the application's design - # This may involve checking the response status code or the content. self.assertEqual(response_user1.status_code, 200) + response_user2 = self.client.post( reverse("kolibri:core:session-list"), data={ @@ -1180,7 +1179,6 @@ def test_case_insensitive_matching_usernames(self): # Assert the expected behavior for the second user self.assertEqual(response_user2.status_code, 200) - # Add more assertions as needed for the second user # Cleanup: Delete the created users self.user1.delete() From fc4842956d4be757fb069a81a3676b2af51d27b8 Mon Sep 17 00:00:00 2001 From: Marcella Maki Date: Wed, 8 Nov 2023 16:52:45 -0500 Subject: [PATCH 13/37] Swap on-my-own const for using the existing Personal const, to prevent state management snafus --- kolibri/plugins/setup_wizard/assets/src/constants.js | 1 - .../setup_wizard/assets/src/machines/wizardMachine.js | 10 ++++++++-- .../views/onboarding-forms/HowAreYouUsingKolibri.vue | 9 ++++++--- .../src/views/onboarding-forms/SettingUpKolibri.vue | 4 ++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/kolibri/plugins/setup_wizard/assets/src/constants.js b/kolibri/plugins/setup_wizard/assets/src/constants.js index ef2b6b1ecf..b010398ed9 100644 --- a/kolibri/plugins/setup_wizard/assets/src/constants.js +++ b/kolibri/plugins/setup_wizard/assets/src/constants.js @@ -11,7 +11,6 @@ const Presets = Object.freeze({ * enum identifying whether the user has gone to the on my own flow or not */ const UsePresets = Object.freeze({ - ON_MY_OWN: 'on my own', GROUP: 'group', }); diff --git a/kolibri/plugins/setup_wizard/assets/src/machines/wizardMachine.js b/kolibri/plugins/setup_wizard/assets/src/machines/wizardMachine.js index e6b52db6da..6e76e19e38 100644 --- a/kolibri/plugins/setup_wizard/assets/src/machines/wizardMachine.js +++ b/kolibri/plugins/setup_wizard/assets/src/machines/wizardMachine.js @@ -1,6 +1,12 @@ import uniq from 'lodash/uniq'; import { checkCapability } from 'kolibri.utils.appCapabilities'; -import { DeviceTypePresets, FacilityTypePresets, LodTypePresets, UsePresets } from '../constants'; +import { + DeviceTypePresets, + FacilityTypePresets, + LodTypePresets, + UsePresets, + Presets, +} from '../constants'; /** * __ Setting up the XState Visualizer __ @@ -536,7 +542,7 @@ export const wizardMachine = createMachine( // Functions used to return a true/false value. When the functions are called, they are passed // the current value of the machine's context as the only parameter isOnMyOwnOrGroup: context => { - return context.onMyOwnOrGroup === UsePresets.ON_MY_OWN; + return context.onMyOwnOrGroup === Presets.PERSONAL; }, isGroupSetup: context => { return context.onMyOwnOrGroup === UsePresets.GROUP; diff --git a/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/HowAreYouUsingKolibri.vue b/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/HowAreYouUsingKolibri.vue index 608a9492be..3d754008c6 100644 --- a/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/HowAreYouUsingKolibri.vue +++ b/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/HowAreYouUsingKolibri.vue @@ -9,7 +9,7 @@ @@ -36,18 +36,21 @@ mixins: [commonSyncElements], inject: ['wizardService'], data() { - const selected = this.wizardService.state.context['onMyOwnOrGroup'] || UsePresets.ON_MY_OWN; + const selected = this.wizardService.state.context['onMyOwnOrGroup'] || Presets.PERSONAL; return { selected, }; }, computed: { isOnMyOwnSetup() { - return this.selected === UsePresets.ON_MY_OWN; + return this.selected === Presets.PERSONAL; }, UsePresets() { return UsePresets; }, + Presets() { + return Presets; + }, }, methods: { handleContinue() { diff --git a/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/SettingUpKolibri.vue b/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/SettingUpKolibri.vue index 344b96f407..b0eb359023 100644 --- a/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/SettingUpKolibri.vue +++ b/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/SettingUpKolibri.vue @@ -44,7 +44,7 @@ import urls from 'kolibri.urls'; import client from 'kolibri.client'; import Lockr from 'lockr'; - import { DeviceTypePresets, UsePresets } from '../../constants'; + import { DeviceTypePresets, Presets } from '../../constants'; export default { name: 'SettingUpKolibri', @@ -160,7 +160,7 @@ /** Introspecting the machine via it's `state.context` properties */ isOnMyOwnSetup() { - return this.wizardContext('onMyOwnOrGroup') == UsePresets.ON_MY_OWN; + return this.wizardContext('onMyOwnOrGroup') == Presets.PERSONAL; }, }, created() { From a14933c998894d9294e57ff33e0f27ac18dcff0e Mon Sep 17 00:00:00 2001 From: Marcella Maki Date: Mon, 15 Jan 2024 17:32:35 -0500 Subject: [PATCH 14/37] Add computed prop to guard against invalid presets --- .../src/views/onboarding-forms/SettingUpKolibri.vue | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/SettingUpKolibri.vue b/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/SettingUpKolibri.vue index b0eb359023..8dd0072885 100644 --- a/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/SettingUpKolibri.vue +++ b/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/SettingUpKolibri.vue @@ -143,7 +143,7 @@ ...this.facilityData, superuser, settings: omitBy(settings, v => v === null), - preset: this.wizardContext('formalOrNonformal') || 'nonformal', + preset: this.presetValue, language_id: currentLanguage, device_name: this.wizardContext('deviceName') || @@ -162,6 +162,16 @@ isOnMyOwnSetup() { return this.wizardContext('onMyOwnOrGroup') == Presets.PERSONAL; }, + presetValue() { + // this computed prop was to guard against a strange edge case where a mismatched + // preset was inadvertently being sent to the backend, where the values + // were not valid, including an incorrect fallback, and 'on my own' being sent as a value + if (this.isOnMyOwnSetup) { + return Presets.PERSONAL; + } else { + return this.wizardContext('formalOrNonformal'); + } + }, }, created() { this.provisionDevice(); From d6fd8eb0ea07fb80a9265dfa0afaf928eb1aa320 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 16 Jan 2024 16:29:51 -0800 Subject: [PATCH 15/37] Catch badly formed UUIDs passed to facility parameter --- kolibri/core/public/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kolibri/core/public/api.py b/kolibri/core/public/api.py index 09113f60ec..b595a2d5f1 100644 --- a/kolibri/core/public/api.py +++ b/kolibri/core/public/api.py @@ -353,7 +353,7 @@ def list(self, request, *args, **kwargs): return Response(content, status=status.HTTP_412_PRECONDITION_FAILED) try: facility = Facility.objects.get(id=facility_id) - except (AttributeError, Facility.DoesNotExist): + except (AttributeError, Facility.DoesNotExist, ValueError): content = "The facility does not exist in this device" return Response(content, status=status.HTTP_404_NOT_FOUND) From ec9492894ee5207f434ae5e21ae9aae8ee417071 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 16 Jan 2024 16:37:01 -0800 Subject: [PATCH 16/37] Unpin docs requirements. Do minimal fixes to docs to allow build to proceed. --- docs/backend_architecture/content/index.rst | 1 - docs/backend_architecture/content/models.rst | 5 ----- docs/conf.py | 2 +- docs/howtos/another_kolibri_instance.md | 2 ++ docs/howtos/another_kolibri_instance.rst | 6 ------ docs/howtos/installing_pyenv.md | 2 ++ docs/howtos/installing_pyenv.rst | 6 ------ docs/howtos/nodeenv.md | 2 ++ docs/howtos/nodeenv.rst | 6 ------ docs/howtos/pyenv_virtualenv.md | 2 ++ docs/howtos/pyenv_virtualenv.rst | 6 ------ requirements/docs.txt | 16 ++++++---------- 12 files changed, 15 insertions(+), 41 deletions(-) delete mode 100644 docs/backend_architecture/content/models.rst delete mode 100644 docs/howtos/another_kolibri_instance.rst delete mode 100644 docs/howtos/installing_pyenv.rst delete mode 100644 docs/howtos/nodeenv.rst delete mode 100644 docs/howtos/pyenv_virtualenv.rst diff --git a/docs/backend_architecture/content/index.rst b/docs/backend_architecture/content/index.rst index 8eb702455a..dd21aae554 100644 --- a/docs/backend_architecture/content/index.rst +++ b/docs/backend_architecture/content/index.rst @@ -6,7 +6,6 @@ This is a core module found in ``kolibri/core/content``. .. toctree:: :maxdepth: 1 - models concepts_and_definitions implementation api_methods diff --git a/docs/backend_architecture/content/models.rst b/docs/backend_architecture/content/models.rst deleted file mode 100644 index f3e260b918..0000000000 --- a/docs/backend_architecture/content/models.rst +++ /dev/null @@ -1,5 +0,0 @@ -Models -====== - -.. automodule:: kolibri.core.content.models - :members: diff --git a/docs/conf.py b/docs/conf.py index 488a87e8d4..59259452e9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -189,7 +189,7 @@ def setup(app): # Register the docstring processor with sphinx app.connect("autodoc-process-docstring", process_docstring) # Add our custom CSS overrides - app.add_stylesheet("theme_overrides.css") + app.add_css_file("theme_overrides.css") # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, diff --git a/docs/howtos/another_kolibri_instance.md b/docs/howtos/another_kolibri_instance.md index ec703a0fda..33d55019b2 100644 --- a/docs/howtos/another_kolibri_instance.md +++ b/docs/howtos/another_kolibri_instance.md @@ -1,3 +1,5 @@ +# Running another Kolibri instance alongside the development server + This guide will walk you through the process of setting up and running another instance of Kolibri alongside your development server using the `pex` executable. ## Introduction diff --git a/docs/howtos/another_kolibri_instance.rst b/docs/howtos/another_kolibri_instance.rst deleted file mode 100644 index b011f4d6b8..0000000000 --- a/docs/howtos/another_kolibri_instance.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _another_kolibri_instance: - -Running another Kolibri instance alongside the development server -================================================================= - -.. mdinclude:: ./another_kolibri_instance.md diff --git a/docs/howtos/installing_pyenv.md b/docs/howtos/installing_pyenv.md index 7319c02113..c10292031e 100644 --- a/docs/howtos/installing_pyenv.md +++ b/docs/howtos/installing_pyenv.md @@ -1,3 +1,5 @@ +## Installing pyenv + ### Prerequisites [Git](https://git-scm.com/) installed. diff --git a/docs/howtos/installing_pyenv.rst b/docs/howtos/installing_pyenv.rst deleted file mode 100644 index 98cfb59c10..0000000000 --- a/docs/howtos/installing_pyenv.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _installing_pyenv: - -Installing pyenv -================ - -.. mdinclude:: ./installing_pyenv.md diff --git a/docs/howtos/nodeenv.md b/docs/howtos/nodeenv.md index 6b2f7c5f98..d33a48b173 100644 --- a/docs/howtos/nodeenv.md +++ b/docs/howtos/nodeenv.md @@ -1,3 +1,5 @@ +# Using nodeenv + ## Instructions Once you've created a python virtual environment, you can use `nodeenv` to install particular versions of node.js within the environment. This allows you to use a different node.js version in the virtual environment than what's available on your host, keep multiple virtual enviroments with different versions of node.js, and to install node.js "global" modules that are only available within the virtual environment. diff --git a/docs/howtos/nodeenv.rst b/docs/howtos/nodeenv.rst deleted file mode 100644 index 67cc908dc2..0000000000 --- a/docs/howtos/nodeenv.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _nodeenv: - -Using nodeenv -============= - -.. mdinclude:: ./nodeenv.md diff --git a/docs/howtos/pyenv_virtualenv.md b/docs/howtos/pyenv_virtualenv.md index acbbb94f26..918087b535 100644 --- a/docs/howtos/pyenv_virtualenv.md +++ b/docs/howtos/pyenv_virtualenv.md @@ -1,3 +1,5 @@ +## Using pyenv-virtualenv + ### Virtual Environments Virtual environments allow a developer to have an encapsulated Python environment, using a specific version of Python, and with dependencies installed in a way that only affect the virtual environment. This is important as different projects or even different versions of the same project may have different dependencies, and virtual environments allow you to switch between them seamlessly and explicitly. diff --git a/docs/howtos/pyenv_virtualenv.rst b/docs/howtos/pyenv_virtualenv.rst deleted file mode 100644 index c98277a1af..0000000000 --- a/docs/howtos/pyenv_virtualenv.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _pyenv_virtualenv: - -Using pyenv-virtualenv -====================== - -.. mdinclude:: ./pyenv_virtualenv.md diff --git a/requirements/docs.txt b/requirements/docs.txt index 8fcb73ec5d..afd8175e81 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,13 +1,9 @@ -r base.txt # These are for building the docs -sphinx==2.4.4 -sphinx-intl==2.0.1 -sphinx-rtd-theme==0.5.2 -sphinx-autobuild==0.7.1 -alabaster==0.7.13 # Pin as later versions require newer versions of Sphinx -m2r==0.2.1 -mistune<2.0.0 -sphinx-notfound-page==0.6 -# Do this to prevent undefined imports in newer versions of Jinja2 -Jinja2<3.1 +sphinx +sphinx-intl +sphinx-rtd-theme +sphinx-autobuild +m2r +sphinx-notfound-page From 13db9c601140c569176ecb5fc94c093d7a328e25 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 16 Jan 2024 17:33:09 -0800 Subject: [PATCH 17/37] Simplify code to minimize diff. --- kolibri/core/tasks/worker.py | 48 ++++++++++++++---------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/kolibri/core/tasks/worker.py b/kolibri/core/tasks/worker.py index b2aa32ab00..336b814fbf 100644 --- a/kolibri/core/tasks/worker.py +++ b/kolibri/core/tasks/worker.py @@ -1,4 +1,3 @@ -import threading import logging from concurrent.futures import CancelledError @@ -46,10 +45,6 @@ def __init__(self, connection, regular_workers=2, high_workers=1): # Key: job_id, Value: future object self.future_job_mapping = {} - # Locks to synchronize access to dictionaries - self.job_future_mapping_lock = threading.Lock() - self.future_job_mapping_lock = threading.Lock() - self.storage = Storage(connection) self.requeue_stalled_jobs() @@ -85,24 +80,19 @@ def start_workers(self): return pool def handle_finished_future(self, future): - # get back the job assigned to the future - job = None - - # Acquire locks before accessing dictionaries - with self.job_future_mapping_lock: - with self.future_job_mapping_lock: - if future in self.job_future_mapping: - # get back the job assigned to the future - job = self.job_future_mapping[future] - # Clean up tracking of this job and its future - del self.job_future_mapping[future] - del self.future_job_mapping[job.job_id] - - if job: + try: + # get back the job assigned to the future + job = self.job_future_mapping[future] + # Clean up tracking of this job and its future + del self.job_future_mapping[future] + del self.future_job_mapping[job.job_id] + try: future.result() except CancelledError: self.storage.mark_job_as_canceled(job.job_id) + except KeyError: + pass def shutdown(self, wait=True): logger.info("Asking job schedulers to shut down.") @@ -182,17 +172,15 @@ def start_next_job(self, job): job_id=job.job_id, ) - # Acquire locks before modifying dictionaries - with self.job_future_mapping_lock: - with self.future_job_mapping_lock: - # Check if the job ID already exists in the future_job_mapping dictionary - if job.job_id in self.future_job_mapping: - logger.error("Job ID {} is already in future_job_mapping.".format(job.job_id)) - raise ValueError("Duplicate job ID: {}".format(job.job_id)) - - # assign the futures to a dict, mapping them to a job - self.job_future_mapping[future] = job - self.future_job_mapping[job.job_id] = future + # Check if the job ID already exists in the future_job_mapping dictionary + if job.job_id in self.future_job_mapping: + logger.warn( + "Job id {} is already in future_job_mapping.".format(job.job_id) + ) + + # assign the futures to a dict, mapping them to a job + self.job_future_mapping[future] = job + self.future_job_mapping[job.job_id] = future # callback for when the future is now! future.add_done_callback(self.handle_finished_future) From 6053e075f172519e0811dd4881d78ccac2512fd4 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 16 Jan 2024 17:40:55 -0800 Subject: [PATCH 18/37] Fix linting. --- kolibri/core/tasks/test/taskrunner/test_worker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kolibri/core/tasks/test/taskrunner/test_worker.py b/kolibri/core/tasks/test/taskrunner/test_worker.py index 201757a51f..9a361d3908 100644 --- a/kolibri/core/tasks/test/taskrunner/test_worker.py +++ b/kolibri/core/tasks/test/taskrunner/test_worker.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -import threading +import threading import time import pytest @@ -50,6 +50,7 @@ def worker(): b.storage.clear(force=True) b.shutdown() + def test_keyerror_prevention(worker): # Create a job with the same ID as the one in worker.enqueue_job_runs_job job = Job(id, args=(9,)) @@ -71,6 +72,7 @@ def delete_future(): assert job.state == "COMPLETED" + def test_keyerror_prevention_multiple_jobs(worker): # Create multiple jobs with the same ID to trigger the race condition job1 = Job(id, args=(9,)) @@ -105,6 +107,7 @@ def delete_future(): assert job2.state == "COMPLETED" + @pytest.mark.django_db class TestWorker: def test_enqueue_job_runs_job(self, worker): From adc090e71001fbe23fac70251760bf8f444b4a04 Mon Sep 17 00:00:00 2001 From: ozer550 Date: Wed, 17 Jan 2024 18:26:07 +0530 Subject: [PATCH 19/37] add isLearnerOnlyImport check in device page --- .../device/assets/src/views/ManageContentPage/index.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kolibri/plugins/device/assets/src/views/ManageContentPage/index.vue b/kolibri/plugins/device/assets/src/views/ManageContentPage/index.vue index b91f3fe37e..c0b4cb4e21 100644 --- a/kolibri/plugins/device/assets/src/views/ManageContentPage/index.vue +++ b/kolibri/plugins/device/assets/src/views/ManageContentPage/index.vue @@ -140,6 +140,7 @@ 'channelIsBeingDeleted', 'managedTasks', ]), + ...mapGetters(['isLearnerOnlyImport']), ...mapState('manageContent/wizard', ['pageName']), ...mapState('manageContent', ['channelListLoading']), ...mapState({ @@ -183,7 +184,7 @@ return ( this.welcomeModalVisibleState && window.localStorage.getItem(welcomeDismissalKey) !== 'true' && - !this.installedChannelsWithResources.length > 0 + (!this.installedChannelsWithResources.length > 0) & !this.isLearnerOnlyImport ); }, }, From 11421e0f9d708db330a0a7f4b808a67d344cdf8c Mon Sep 17 00:00:00 2001 From: nick2432 Date: Wed, 17 Jan 2024 19:11:06 +0530 Subject: [PATCH 20/37] username-validation --- kolibri/core/device/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kolibri/core/device/serializers.py b/kolibri/core/device/serializers.py index 81f5f49a06..a5db7576f5 100644 --- a/kolibri/core/device/serializers.py +++ b/kolibri/core/device/serializers.py @@ -168,7 +168,7 @@ def create(self, validated_data): # noqa C901 # We've imported a facility if the username exists try: superuser = FacilityUser.objects.get( - username=superuser_data["username"] + username__iexact=superuser_data["username"] ) except FacilityUser.DoesNotExist: try: From 826ef10548abe6e80c91dd9b20c70369cf5fb9ce Mon Sep 17 00:00:00 2001 From: Marcella Maki Date: Wed, 17 Jan 2024 15:52:09 -0500 Subject: [PATCH 21/37] simplify advancement to next step, and remove call to non-existent SET_FACILITY_PRESET mutation --- .../views/onboarding-forms/HowAreYouUsingKolibri.vue | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/HowAreYouUsingKolibri.vue b/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/HowAreYouUsingKolibri.vue index 3d754008c6..30a432d786 100644 --- a/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/HowAreYouUsingKolibri.vue +++ b/kolibri/plugins/setup_wizard/assets/src/views/onboarding-forms/HowAreYouUsingKolibri.vue @@ -42,9 +42,6 @@ }; }, computed: { - isOnMyOwnSetup() { - return this.selected === Presets.PERSONAL; - }, UsePresets() { return UsePresets; }, @@ -54,14 +51,6 @@ }, methods: { handleContinue() { - if (this.isOnMyOwnSetup) { - // If the user is on their own, set the preset to personal here - // If not then the user will set it using a form later on - this.$store.commit('SET_FACILITY_PRESET', Presets.PERSONAL); - } - this.goToNextStep(); - }, - goToNextStep() { this.wizardService.send({ type: 'CONTINUE', value: this.selected }); }, }, From 8984adde25ec7706853a8d4734e62f06f73a5ebe Mon Sep 17 00:00:00 2001 From: Allan Otodi Opeto <103313919+AllanOXDi@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:31:12 +0300 Subject: [PATCH 22/37] replaced isEmbeddedWebView with isAppContext from a userUser composable --- kolibri/core/assets/src/composables/useUser.js | 2 ++ kolibri/core/assets/src/utils/browserInfo.js | 2 +- .../src/views/ContentRenderer/DownloadButton.vue | 11 +++++++++-- .../assets/src/views/reports/ReportsControls.vue | 11 +++++++++-- .../device/assets/src/modules/deviceInfo/handlers.js | 4 ++-- .../facility/assets/src/views/DataPage/index.vue | 8 ++++++-- 6 files changed, 29 insertions(+), 9 deletions(-) diff --git a/kolibri/core/assets/src/composables/useUser.js b/kolibri/core/assets/src/composables/useUser.js index 479f2ae81d..a61c4841b4 100644 --- a/kolibri/core/assets/src/composables/useUser.js +++ b/kolibri/core/assets/src/composables/useUser.js @@ -9,6 +9,7 @@ export default function useUser() { const isAdmin = computed(() => store.getters.isAdmin); const isSuperuser = computed(() => store.getters.isSuperuser); const canManageContent = computed(() => store.getters.canManageContent); + const isAppContext = computed(() => store.getters.isAppContext); return { isLearnerOnlyImport, @@ -18,5 +19,6 @@ export default function useUser() { isAdmin, isSuperuser, canManageContent, + isAppContext, }; } diff --git a/kolibri/core/assets/src/utils/browserInfo.js b/kolibri/core/assets/src/utils/browserInfo.js index 412b0d9249..af402ce36d 100644 --- a/kolibri/core/assets/src/utils/browserInfo.js +++ b/kolibri/core/assets/src/utils/browserInfo.js @@ -86,7 +86,7 @@ export const isMacWebView = /** * All web views */ -export const isEmbeddedWebView = isAndroidWebView || isMacWebView; +export const isAppContext = isAndroidWebView || isMacWebView; // Check for presence of the touch event in DOM or multi-touch capabilities export const isTouchDevice = diff --git a/kolibri/core/assets/src/views/ContentRenderer/DownloadButton.vue b/kolibri/core/assets/src/views/ContentRenderer/DownloadButton.vue index 7429efb3e5..0bc460e0e6 100644 --- a/kolibri/core/assets/src/views/ContentRenderer/DownloadButton.vue +++ b/kolibri/core/assets/src/views/ContentRenderer/DownloadButton.vue @@ -20,12 +20,19 @@