Skip to content

Commit

Permalink
Merge pull request #12014 from rtibbles/0.16intodevelop
Browse files Browse the repository at this point in the history
0.16 into develop
  • Loading branch information
rtibbles authored Mar 22, 2024
2 parents efcaa7c + 66d0f6d commit 564e713
Show file tree
Hide file tree
Showing 56 changed files with 1,463 additions and 350 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
strategy:
max-parallel: 5
matrix:
python-version: [3.6]
python-version: ['3.10']

steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,4 @@ If you have contributed to Kolibri, feel free to add your name and Github accoun
| Alex Vélez | AlexVelezLl |
| Mazen Oweiss | moweiss |
| Eshaan Aggarwal | EshaanAgg |
| Nikhil Sharma | ThEditor |
2 changes: 1 addition & 1 deletion kolibri/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

#: This may not be the exact version as it's subject to modification with
#: get_version() - use ``kolibri.__version__`` for the exact version string.
VERSION = (0, 16, 0)
VERSION = (0, 16, 1)

__author__ = "Learning Equality"
__email__ = "[email protected]"
Expand Down
2 changes: 0 additions & 2 deletions kolibri/core/assets/src/core-app/apiSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ import LearnOnlyDeviceNotice from '../views/LearnOnlyDeviceNotice';
import themeConfig from '../styles/themeConfig';
import sortLanguages from '../utils/sortLanguages';
import * as sync from '../views/sync/syncComponentSet';
import PageRoot from '../views/PageRoot';
import NotificationsRoot from '../views/NotificationsRoot';
import useMinimumKolibriVersion from '../composables/useMinimumKolibriVersion';
import useUserSyncStatus from '../composables/useUserSyncStatus';
Expand Down Expand Up @@ -203,7 +202,6 @@ export default {
PrivacyLinkAndModal,
LearnOnlyDeviceNotice,
SuggestedTime,
PageRoot,
MasteryModel,
NotificationsRoot,
KolibriLoadingSnippet,
Expand Down
7 changes: 6 additions & 1 deletion kolibri/core/assets/src/kolibri_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ export default class KolibriApp extends KolibriModule {
* @return {Object} A component definition for the root component of this single page app.
*/
get RootVue() {
return {};
// By default return the component that just renders router-view,
// which will render the component for the current route.
return {
functional: true,
render: createElement => createElement('router-view'),
};
}
/*
* @return {Store} A convenience getter to return the vuex store.
Expand Down
18 changes: 13 additions & 5 deletions kolibri/core/assets/src/logging.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,31 @@

import loglevel from 'loglevel';

const console = global.console;

class Logger {
constructor(loggerName) {
this.loggerName = loggerName;
this.logger = loglevel.getLogger(loggerName);
this.setMessagePrefix();
Object.keys(loglevel.levels).forEach(methodName => {
const name = methodName.toLowerCase();
const logFunction = this.logger[name];
if (logFunction) {
this[name] = logFunction.bind(console, this.messagePrefix(name));
this[name] = param => {
return this.logger[name](param);
};
}
});
}

messagePrefix(type) {
return `[${type.toUpperCase()}: ${this.loggerName}]`;
setMessagePrefix() {
var originalFactory = this.logger.methodFactory;
this.logger.methodFactory = function(methodName, logLevel, loggerName) {
var rawMethod = originalFactory(methodName, logLevel, loggerName);
return function(message) {
rawMethod(`[${methodName.toUpperCase()}: ${loggerName}] ` + message);
};
};
this.logger.setLevel(this.logger.getLevel());
}

setLevel(level, persist) {
Expand Down
16 changes: 0 additions & 16 deletions kolibri/core/assets/src/state/modules/core/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,6 @@ export function handleApiError(store, { error, reloadOnReconnect = false } = {})
throw error;
}

/**
* Used to prevent inadvertent actions if a user double-clicks to navigate
*
* Something of a hack. A better strategy would be to create a new
* `setLoading` action which handles both `state.core.loading` and
* `state.core.blockDoubleClicks` with a single function.
*/
export function blockDoubleClicks(store) {
if (!store.state.blockDoubleClicks) {
store.commit('CORE_BLOCK_CLICKS', true);
setTimeout(() => {
store.commit('CORE_BLOCK_CLICKS', false);
}, 500);
}
}

export function setSession(store, { session, clientNow }) {
const serverTime = session.server_time;
if (clientNow) {
Expand Down
1 change: 0 additions & 1 deletion kolibri/core/assets/src/state/modules/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export default {
state() {
return {
error: '',
blockDoubleClicks: false,
loading: true,
pageSessionId: 0,
totalProgress: null,
Expand Down
3 changes: 0 additions & 3 deletions kolibri/core/assets/src/state/modules/core/mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ export default {
CORE_SET_ERROR(state, error) {
state.error = error;
},
CORE_BLOCK_CLICKS(state, blocked) {
state.blockDoubleClicks = blocked;
},
SET_TOTAL_PROGRESS(state, progress) {
state.totalProgress = progress;
},
Expand Down
18 changes: 0 additions & 18 deletions kolibri/core/assets/src/views/PageRoot.vue

This file was deleted.

4 changes: 3 additions & 1 deletion kolibri/core/assets/src/views/SyncStatusDisplay.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>

<span>
<span :style="{ display: 'flex', alignItems: 'center' }">
<KCircularLoader
v-if="syncInProgress"
:size="20"
Expand Down Expand Up @@ -135,6 +135,7 @@
.inline-loader {
display: inline-block;
margin-right: 8px;
margin-left: 0;
vertical-align: bottom;
}
Expand All @@ -146,6 +147,7 @@
.small {
display: inline-block;
margin-top: 0;
margin-bottom: 0;
font-size: 11px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@
windowBreakpoint,
};
},
props: {
parentBreakpoint: {
type: Number,
default: -1,
},
},
data() {
return {
showLanguageModal: false,
Expand All @@ -75,8 +81,14 @@
return availableLanguages[currentLanguage];
},
numVisibleLanguageBtns() {
// At windowBreakpoint = 0, only the "More languages" button will show
return Math.min(4, this.windowBreakpoint);
// At visibleBtns = 0, only the "More languages" button will show
let visibleBtns = 4;
if (this.parentBreakpoint < 0) {
visibleBtns = this.windowBreakpoint;
} else {
visibleBtns = this.parentBreakpoint;
}
return Math.min(4, visibleBtns);
},
numSelectableLanguages() {
return this.selectableLanguages.length;
Expand Down
37 changes: 19 additions & 18 deletions kolibri/core/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,23 @@ def create(self, request):
status=status.HTTP_401_UNAUTHORIZED,
)

# Find the FacilityUser we're looking for use later on
user = None
if interface.enabled and valid_app_key_on_request(request):
# If we are in app context, then try to get the automatically created OS User
# if it matches the username, without needing a password.
user = self._check_os_user(request, username)
if user is None:
# Otherwise attempt full authentication
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"
login(request, user)
# Success!
return self.get_session_response(request)
# Otherwise, try to give a helpful error message
# Find the FacilityUser we're looking for
try:
unauthenticated_user = FacilityUser.objects.get(
username__iexact=username, facility=facility_id
Expand All @@ -917,24 +933,9 @@ def create(self, request):
)
except FacilityUser.MultipleObjectsReturned:
# Handle case of multiple matching usernames
unauthenticated_user = FacilityUser.objects.get(
unauthenticated_user = FacilityUser.objects.filter(
username__exact=username, facility=facility_id
)
user = None
if interface.enabled and valid_app_key_on_request(request):
# If we are in app context, then try to get the automatically created OS User
# if it matches the username, without needing a password.
user = self._check_os_user(request, username)
if user is None:
# Otherwise attempt full authentication
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"
login(request, user)
# Success!
return self.get_session_response(request)
).first()
if unauthenticated_user.password == NOT_SPECIFIED:
# Here - we have a Learner whose password is "NOT_SPECIFIED" because they were created
# while the "Require learners to log in with password" setting was disabled - but now
Expand Down
35 changes: 31 additions & 4 deletions kolibri/core/auth/management/commands/bulkimportusers.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,20 @@ def add_check(self, header_name, check, message):

def get_username(self, row):
username = row.get(self.header_translation["USERNAME"])
if username in self.users.keys():
return None
uuid = row.get(self.header_translation["UUID"])
lowercase_username = username.lower()

# Check if a user with the provided username exists (case-insensitive)
existing_user = FacilityUser.objects.filter(
username__iexact=lowercase_username
).first()
# Convert existing keys in self.users to lowercase
if existing_user and uuid == "":
return None # Duplicate username
# Convert existing keys in self.users to lowercase
lowercase_users = {key.lower(): value for key, value in self.users.items()}
if lowercase_username in lowercase_users:
return None # Duplicate username

return username

Expand Down Expand Up @@ -533,7 +545,7 @@ def compare_fields(self, user_obj, values):
setattr(user_obj, field, values[field])
return changed

def build_users_objects(self, users):
def build_users_objects(self, users): # noqa C901
new_users = []
update_users = []
keeping_users = []
Expand Down Expand Up @@ -563,7 +575,7 @@ def build_users_objects(self, users):
if user_obj.username != user:
# check for duplicated username in the facility
existing_user = FacilityUser.objects.get(
username=user, facility=self.default_facility
username__iexact=user, facility=self.default_facility
)
if existing_user:
error = {
Expand All @@ -578,6 +590,21 @@ def build_users_objects(self, users):
if self.compare_fields(user_obj, values):
update_users.append(user_obj)
else:
# If UUID is not specified, check for a username clash
if values["uuid"] == "":
existing_user = FacilityUser.objects.filter(
username__iexact=user, facility=self.default_facility
).first()
if existing_user:
error = {
"row": users[user]["position"],
"username": user,
"message": MESSAGES[DUPLICATED_USERNAME],
"field": "USERNAME",
"value": user,
}
per_line_errors.append(error)
continue
if values["uuid"] != "":
error = {
"row": users[user]["position"],
Expand Down
31 changes: 28 additions & 3 deletions kolibri/core/auth/test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1240,9 +1240,34 @@ def test_case_insensitive_matching_usernames(self):
# 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()
def test_case_sensitive_matching_usernames(self):
FacilityUserFactory.create(username="shared_username", facility=self.facility)

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)

# Test no error when authentication fails
response_user3 = self.client.post(
reverse("kolibri:core:session-list"),
data={
"username": "shared_username",
"password": "wrong_password",
"facility": self.facility.id,
},
format="json",
)

self.assertEqual(response_user3.status_code, 401)


class SignUpBase(object):
Expand Down
Loading

0 comments on commit 564e713

Please sign in to comment.