Skip to content

Commit

Permalink
Merge pull request #11745 from rtibbles/0.16intodevelop
Browse files Browse the repository at this point in the history
0.16 into develop
  • Loading branch information
rtibbles committed Jan 19, 2024
2 parents 2e82017 + 422a6f1 commit 66589c6
Show file tree
Hide file tree
Showing 49 changed files with 307 additions and 164 deletions.
1 change: 0 additions & 1 deletion docs/backend_architecture/content/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ This is a core module found in ``kolibri/core/content``.
.. toctree::
:maxdepth: 1

models
concepts_and_definitions
implementation
api_methods
Expand Down
5 changes: 0 additions & 5 deletions docs/backend_architecture/content/models.rst

This file was deleted.

2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions docs/howtos/another_kolibri_instance.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 0 additions & 6 deletions docs/howtos/another_kolibri_instance.rst

This file was deleted.

2 changes: 2 additions & 0 deletions docs/howtos/installing_pyenv.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
## Installing pyenv

### Prerequisites

[Git](https://git-scm.com/) installed.
Expand Down
6 changes: 0 additions & 6 deletions docs/howtos/installing_pyenv.rst

This file was deleted.

2 changes: 2 additions & 0 deletions docs/howtos/nodeenv.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
6 changes: 0 additions & 6 deletions docs/howtos/nodeenv.rst

This file was deleted.

2 changes: 2 additions & 0 deletions docs/howtos/pyenv_virtualenv.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
6 changes: 0 additions & 6 deletions docs/howtos/pyenv_virtualenv.rst

This file was deleted.

10 changes: 0 additions & 10 deletions kolibri/core/assets/src/views/CorePage/AppBarPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
class="app-bar"
:title="title"
@toggleSideNav="navShown = !navShown"
@showLanguageModal="languageModalShown = true"
>
<template #sub-nav>
<slot name="subNav"></slot>
Expand Down Expand Up @@ -44,12 +43,6 @@
@shouldFocusFirstEl="findFirstEl()"
/>
</transition>
<LanguageSwitcherModal
v-if="languageModalShown"
ref="languageSwitcherModal"
:style="{ color: $themeTokens.text }"
@cancel="languageModalShown = false"
/>

</div>

Expand All @@ -60,7 +53,6 @@
import { mapGetters } from 'vuex';
import { throttle } from 'frame-throttle';
import LanguageSwitcherModal from 'kolibri.coreVue.components.LanguageSwitcherModal';
import ScrollingHeader from 'kolibri.coreVue.components.ScrollingHeader';
import useKResponsiveWindow from 'kolibri.coreVue.composables.useKResponsiveWindow';
import SideNav from 'kolibri.coreVue.components.SideNav';
Expand All @@ -75,7 +67,6 @@
name: 'AppBarPage',
components: {
AppBar,
LanguageSwitcherModal,
ScrollingHeader,
SideNav,
StorageNotification,
Expand Down Expand Up @@ -110,7 +101,6 @@
data() {
return {
appBarHeight: 0,
languageModalShown: false,
navShown: false,
lastScrollTop: 0,
hideAppBars: true,
Expand Down
16 changes: 0 additions & 16 deletions kolibri/core/assets/test/views/app-bar-core-page.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,4 @@ describe('AppBarPage', () => {
expect(wrapper.findComponent({ name: 'SideNav' }).vm.navShown).toBe(false);
});
});

describe('Toggling the language switcher modal', () => {
it('should show the side nav when the AppBar.showLanguageModal event is emitted', async () => {
const wrapper = createWrapper();
expect(wrapper.findComponent({ name: 'LanguageSwitcherModal' }).exists()).toBe(false);
await wrapper.vm.$refs.appBar.$emit('showLanguageModal');
expect(wrapper.findComponent({ name: 'LanguageSwitcherModal' }).exists()).toBe(true);
});
it('should hide the language switcher modal when LanguageSwitcherModal.cancel is emitted', async () => {
const wrapper = createWrapper();
await wrapper.setData({ languageModalShown: true });
expect(wrapper.findComponent({ name: 'LanguageSwitcherModal' }).exists()).toBe(true);
await wrapper.vm.$refs.languageSwitcherModal.$emit('cancel');
expect(wrapper.findComponent({ name: 'LanguageSwitcherModal' }).exists()).toBe(false);
});
});
});
6 changes: 5 additions & 1 deletion kolibri/core/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,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"
Expand Down
19 changes: 18 additions & 1 deletion kolibri/core/auth/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,17 @@ 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_users(self, users, password, facility):
if facility:
users = users.filter(facility=facility)
for user in users:
Expand All @@ -43,6 +52,14 @@ def authenticate(self, request, username=None, password=None, **kwargs):
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)
return self._authenticate_users(users, password, facility)

def get_user(self, user_id):
"""
Gets a user. Auth backends are required to implement this.
Expand Down
37 changes: 37 additions & 0 deletions kolibri/core/auth/test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,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(
Expand Down Expand Up @@ -1206,6 +1212,37 @@ 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):
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
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)

# Cleanup: Delete the created users
self.user1.delete()
self.user2.delete()


class SignUpBase(object):
@classmethod
Expand Down
29 changes: 29 additions & 0 deletions kolibri/core/auth/test/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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):
Expand All @@ -23,6 +28,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,
Expand All @@ -31,6 +44,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,
Expand All @@ -39,6 +60,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(
Expand Down
2 changes: 1 addition & 1 deletion kolibri/core/content/utils/content_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ def _get_import_metadata(client, contentnode_id):
return response.json()
except NetworkLocationResponseFailure as e:
# 400 level errors, like 404, are ignored
if e.response and 400 <= e.response.status_code < 500:
if e.response is not None and 400 <= e.response.status_code < 500:
logger.debug(
"Metadata request failure: GET {} {}".format(
url_path, e.response.status_code
Expand Down
2 changes: 1 addition & 1 deletion kolibri/core/public/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
58 changes: 58 additions & 0 deletions kolibri/core/tasks/test/taskrunner/test_worker.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import threading
import time

import pytest
Expand Down Expand Up @@ -50,6 +51,63 @@ def worker():
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:
def test_enqueue_job_runs_job(self, worker):
Expand Down
Loading

0 comments on commit 66589c6

Please sign in to comment.