Skip to content

Commit

Permalink
Merge pull request #4 from learningequality/release-v0.16.x
Browse files Browse the repository at this point in the history
merge
  • Loading branch information
nick2432 authored Jan 11, 2024
2 parents b0b38f7 + 6ad58b3 commit 8f442b4
Show file tree
Hide file tree
Showing 351 changed files with 3,485 additions and 4,510 deletions.
1 change: 1 addition & 0 deletions kolibri/core/assets/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export const ERROR_CONSTANTS = {
ALREADY_REGISTERED_FOR_COMMUNITY: 'ALREADY_REGISTERED_FOR_COMMUNITY',
// 401 error constants
INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
INVALID_USERNAME: 'INVALID_USERNAME',
// 404 error constants
NOT_FOUND: 'NOT_FOUND',
INVALID_KDP_REGISTRATION_TOKEN: 'INVALID_KDP_REGISTRATION_TOKEN',
Expand Down
8 changes: 7 additions & 1 deletion kolibri/core/auth/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.core.management import call_command
from django.utils import timezone
from rest_framework import serializers
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.exceptions import ValidationError

from kolibri.core.auth.constants.demographics import NOT_SPECIFIED
Expand Down Expand Up @@ -532,7 +533,12 @@ def validate(self, data):
facility_id = data["facility"]
username = data["username"]
password = data["password"]
facility_info = get_remote_users_info(baseurl, facility_id, username, password)
try:
facility_info = get_remote_users_info(
baseurl, facility_id, username, password
)
except AuthenticationFailed as e:
raise ValidationError(detail=str(e.detail), code=e.detail.code)
user_info = facility_info["user"]

# syncing using an admin account (username & password belong to the admin):
Expand Down
18 changes: 16 additions & 2 deletions kolibri/core/auth/utils/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,23 @@ def get_remote_users_info(baseurl, facility_id, username, password):
response.raise_for_status()
except (CommandError, HTTPError, ConnectionError) as e:
if password == NOT_SPECIFIED or not password:
raise AuthenticationFailed(
detail="Password is required", code=error_constants.MISSING_PASSWORD
facility_info_url = reverse_remote(
baseurl,
"kolibri:core:publicfacility-detail",
args=[
facility_id,
],
)
response = requests.get(facility_info_url)
if response.json()["learner_can_login_with_no_password"]:
raise AuthenticationFailed(
detail="The username can not be found",
code=error_constants.INVALID_USERNAME,
)
else:
raise AuthenticationFailed(
detail="Password is required", code=error_constants.MISSING_PASSWORD
)
else:
raise AuthenticationFailed(
detail=str(e), code=error_constants.AUTHENTICATION_FAILED
Expand Down
1 change: 1 addition & 0 deletions kolibri/core/error_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
PASSWORD_NOT_SPECIFIED = "PASSWORD_NOT_SPECIFIED"
# 401 error constants
INVALID_CREDENTIALS = "INVALID_CREDENTIALS"
INVALID_USERNAME = "INVALID_USERNAME"
# 404 error constants
NOT_FOUND = "NOT_FOUND"
FACILITY_DOES_NOT_EXIST = "FACILITY_DOES_NOT_EXIST"
Expand Down
13 changes: 13 additions & 0 deletions kolibri/core/tasks/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,19 @@ def enqueue(self, job=None, retry_interval=None, priority=None, **job_kwargs):
retry_interval=retry_interval,
)

def enqueue_lifo(self, job=None, retry_interval=None, priority=None, **job_kwargs):
"""
Enqueue the function with arguments passed to this method using LIFO order.
:return: enqueued job's id.
"""
return job_storage.enqueue_lifo(
job or self._ready_job(**job_kwargs),
queue=self.queue,
priority=priority or self.priority,
retry_interval=retry_interval,
)

def enqueue_if_not(
self, job=None, retry_interval=None, priority=None, **job_kwargs
):
Expand Down
39 changes: 38 additions & 1 deletion kolibri/core/tasks/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime
from datetime import timedelta

import pytz
from sqlalchemy import Column
from sqlalchemy import DateTime
from sqlalchemy import func as sql_func
Expand Down Expand Up @@ -199,6 +200,42 @@ def enqueue_job(
)
return job.job_id

def enqueue_lifo(
self, job, queue=DEFAULT_QUEUE, priority=Priority.REGULAR, retry_interval=None
):
naive_utc_now = datetime.utcnow()
with self.session_scope() as session:
soonest_job = (
session.query(ORMJob)
.filter(ORMJob.state == State.QUEUED)
.filter(ORMJob.scheduled_time <= naive_utc_now)
.order_by(ORMJob.scheduled_time)
.first()
)
dt = (
pytz.timezone("UTC").localize(soonest_job.scheduled_time)
- timedelta(microseconds=1)
if soonest_job
else self._now()
)
try:
return self.schedule(
dt,
job,
queue,
priority=priority,
interval=0,
repeat=0,
retry_interval=retry_interval,
)
except JobRunning:
logger.debug(
"Attempted to enqueue a running job {job_id}, ignoring.".format(
job_id=job.job_id
)
)
return job.job_id

def enqueue_job_if_not_enqueued(
self, job, queue=DEFAULT_QUEUE, priority=Priority.REGULAR, retry_interval=None
):
Expand Down Expand Up @@ -239,7 +276,7 @@ def _filter_next_query(self, query, priority):
query.filter(ORMJob.state == State.QUEUED)
.filter(ORMJob.scheduled_time <= naive_utc_now)
.filter(ORMJob.priority <= priority)
.order_by(ORMJob.priority, ORMJob.time_created)
.order_by(ORMJob.priority, ORMJob.scheduled_time, ORMJob.time_created)
)

def _postgres_next_queued_job(self, session, priority):
Expand Down
15 changes: 15 additions & 0 deletions kolibri/core/tasks/test/taskrunner/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,21 @@ def test_get_running_jobs(self, defaultbackend):
assert len(defaultbackend.get_running_jobs(queues=[QUEUE])) == 1
assert len(defaultbackend.get_running_jobs(queues=[DEFAULT_QUEUE, QUEUE])) == 3

def test_lifo_behavior_with_scheduled_time(self, defaultbackend, simplejob):
# Enqueue multiple jobs as LIFO
job1 = Job(open)
job2 = Job(open)
job3 = Job(open)
defaultbackend.enqueue_lifo(job1, QUEUE)
defaultbackend.enqueue_lifo(job2, QUEUE)
job3_id = defaultbackend.enqueue_lifo(job3, QUEUE)

# Ensure that the last queued job is returned by get_next_queued_job
last_queued_job_id = defaultbackend.get_next_queued_job().job_id

# Assert that the last queued job matches the expected job
assert last_queued_job_id == job3_id

def test_get_canceling_jobs(self, defaultbackend):
# Schedule jobs
schedule_time = local_now() + datetime.timedelta(hours=1)
Expand Down
18 changes: 18 additions & 0 deletions kolibri/core/tasks/test/test_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,24 @@ def test_enqueue(self, job_storage_mock, _ready_job_mock):
retry_interval=None,
)

@mock.patch("kolibri.core.tasks.registry.RegisteredTask._ready_job")
@mock.patch("kolibri.core.tasks.registry.job_storage")
def test_enqueue_lifo_job(self, job_storage_mock, _ready_job_mock):
args = ("10",)
kwargs = dict(base=10)

_ready_job_mock.return_value = "lifo_job"

self.registered_task.enqueue_lifo(args=args, kwargs=kwargs)

_ready_job_mock.assert_called_once_with(args=args, kwargs=kwargs)
job_storage_mock.enqueue_lifo.assert_called_once_with(
"lifo_job",
queue=self.registered_task.queue,
priority=self.registered_task.priority,
retry_interval=None,
)

@mock.patch("kolibri.core.tasks.registry.RegisteredTask._ready_job")
@mock.patch("kolibri.core.tasks.registry.job_storage")
def test_enqueue__override_priority(self, job_storage_mock, _ready_job_mock):
Expand Down
14 changes: 0 additions & 14 deletions kolibri/deployment/default/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,6 @@
DEVELOPER_MODE = True
os.environ.update({"KOLIBRI_DEVELOPER_MODE": "True"})

try:
process_cache = CACHES["process_cache"] # noqa F405
except KeyError:
process_cache = None

# Create a memcache for each cache
CACHES = {
key: {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}
for key in CACHES # noqa F405
}

if process_cache:
CACHES["process_cache"] = process_cache


REST_FRAMEWORK = {
"UNAUTHENTICATED_USER": "kolibri.core.auth.models.KolibriAnonymousUser",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,11 @@
"FilePresetStrings.video_subtitle": "crwdns335133:0{langCode}crwdnd335133:0{fileSize}crwdne335133:0",
"FilePresetStrings.zim": "crwdns335135:0{fileSize}crwdne335135:0",
"GenderSelect.placeholder": "crwdns335141:0crwdne335141:0",
"GettingStartedFormAlt.configureFacilityAction": "crwdns333279:0crwdne333279:0",
"GettingStartedFormAlt.descriptionParagraph1": "crwdns333281:0crwdne333281:0",
"GettingStartedFormAlt.descriptionParagraph2": "crwdns333283:0crwdne333283:0",
"GettingStartedFormAlt.gettingStartedHeader": "crwdns333285:0crwdne333285:0",
"GettingStartedFormAlt.skipAction": "crwdns333287:0crwdne333287:0",
"InteractionList.currAnswer": "crwdns335143:0value={value}crwdne335143:0",
"InteractionList.noInteractions": "crwdns335145:0crwdne335145:0",
"KolibriLoadingSnippet.kolibriLoading": "crwdns370473:0crwdne370473:0",
Expand Down Expand Up @@ -479,6 +484,10 @@
"MasteryModel.one": "crwdns335215:0crwdne335215:0",
"MasteryModel.streak": "crwdns335217:0count={count}crwdne335217:0",
"MasteryModel.unknown": "crwdns335219:0crwdne335219:0",
"MeteredConnectionNotificationModal.doNotUseMetered": "crwdns370463:0crwdne370463:0",
"MeteredConnectionNotificationModal.modalDescription": "crwdns370465:0crwdne370465:0",
"MeteredConnectionNotificationModal.modalTitle": "crwdns370467:0crwdne370467:0",
"MeteredConnectionNotificationModal.useMetered": "crwdns370469:0crwdne370469:0",
"MissingResourceAlert.learnMore": "crwdns370057:0crwdne370057:0",
"MissingResourceAlert.resourcesUnavailableP1": "crwdns370059:0crwdne370059:0",
"MissingResourceAlert.resourcesUnavailableP2": "crwdns370061:0crwdne370061:0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"CoachClassListPage.noClassesDetailsForFacilityCoach": "crwdns333485:0crwdne333485:0",
"CoachExamsPage.newQuiz": "crwdns333487:0crwdne333487:0",
"CoachExamsPage.noExams": "crwdns333489:0crwdne333489:0",
"CoachExamsPage.noStartedExams": "crwdns369531:0crwdne369531:0",
"CoachExamsPage.selectQuiz": "crwdns333491:0crwdne333491:0",
"CoachExamsPage.totalQuizSize": "crwdns369533:0{size}crwdne369533:0",
"CoachImmersivePage.errorPageTitle": "crwdns369535:0crwdne369535:0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@
"ManageSyncSchedule.addDevice": "crwdns370037:0crwdne370037:0",
"ManageSyncSchedule.connected": "crwdns370039:0crwdne370039:0",
"ManageSyncSchedule.disconnected": "crwdns370461:0crwdne370461:0",
"ManageSyncSchedule.forgetText": "crwdns370043:0crwdne370043:0",
"ManageSyncSchedule.introduction": "crwdns370045:0crwdne370045:0",
"ManageSyncSchedule.syncSchedules": "crwdns370047:0crwdne370047:0",
"ManageTasksPage.appBarTitle": "crwdns332785:0crwdne332785:0",
Expand Down Expand Up @@ -202,6 +201,7 @@
"PinAuthenticationModal.pinPlaceholder": "crwdns370173:0crwdne370173:0",
"PostSetupModalGroup.chooseAnotherSourceLabel": "crwdns332827:0crwdne332827:0",
"PrimaryStorageLocationModal.changePrimaryLocation": "crwdns370175:0crwdne370175:0",
"PrivacyModal.syncToKDP": "crwdns336145:0crwdne336145:0",
"RearrangeChannelsPage.downLabel": "crwdns332829:0{name}crwdne332829:0",
"RearrangeChannelsPage.editChannelOrderTitle": "crwdns370177:0crwdne370177:0",
"RearrangeChannelsPage.failureNotification": "crwdns332831:0crwdne332831:0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@
"ManageSyncSchedule.addDevice": "crwdns370037:0crwdne370037:0",
"ManageSyncSchedule.connected": "crwdns370039:0crwdne370039:0",
"ManageSyncSchedule.disconnected": "crwdns370461:0crwdne370461:0",
"ManageSyncSchedule.forgetText": "crwdns370043:0crwdne370043:0",
"ManageSyncSchedule.introduction": "crwdns370045:0crwdne370045:0",
"ManageSyncSchedule.syncSchedules": "crwdns370047:0crwdne370047:0",
"PaginatedListContainerWithBackend.nextResults": "crwdns369983:0crwdne369983:0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,14 @@
"MissingResourceAlert.resourcesUnavailableP1": "crwdns370059:0crwdne370059:0",
"MissingResourceAlert.resourcesUnavailableP2": "crwdns370061:0crwdne370061:0",
"MissingResourceAlert.resourcesUnavailableTitle": "crwdns370063:0crwdne370063:0",
"PermissionsChangeModal.header": "crwdns332819:0crwdne332819:0",
"PermissionsChangeModal.manageContentMessage1": "crwdns332821:0crwdne332821:0",
"PermissionsChangeModal.superAdminMessage1": "crwdns332823:0crwdne332823:0",
"PermissionsChangeModal.superAdminMessage2": "crwdns332825:0crwdne332825:0",
"PerseusRendererIndex.hint": "crwdns334341:0hintsLeft={hintsLeft}crwdne334341:0",
"PerseusRendererIndex.hintExplanation": "crwdns334343:0crwdne334343:0",
"PerseusRendererIndex.noMoreHint": "crwdns334345:0crwdne334345:0",
"PostSetupModalGroup.chooseAnotherSourceLabel": "crwdns332827:0crwdne332827:0",
"QuizCard.completedPercentLabel": "crwdns334347:0score={score}crwdne334347:0",
"QuizCard.questionsLeft": "crwdns334349:0questionsLeft={questionsLeft}crwdnd334349:0questionsLeft={questionsLeft}crwdne334349:0",
"QuizRenderer.areYouSure": "crwdns334351:0crwdne334351:0",
Expand Down Expand Up @@ -174,6 +179,8 @@
"SearchResultsGrid.viewAsList": "crwdns370413:0crwdne370413:0",
"SidePanelModal.topicHeader": "crwdns370415:0crwdne370415:0",
"SkipNavigationLink.skipToMainContentAction": "crwdns335419:0crwdne335419:0",
"SyncStatusDescription.queuedDescription": "crwdns369493:0crwdne369493:0",
"SyncStatusDescription.syncingDescription": "crwdns369497:0crwdne369497:0",
"TechnicalTextBlock.copiedToClipboardConfirmation": "crwdns370079:0crwdne370079:0",
"TechnicalTextBlock.copyToClipboardButtonPrompt": "crwdns370081:0crwdne370081:0",
"TopicsContentPage.errorPageTitle": "crwdns370417:0crwdne370417:0",
Expand All @@ -182,6 +189,13 @@
"TopicsPage.documentTitleForChannel": "crwdns334397:0{ channelTitle }crwdne334397:0",
"TopicsPage.documentTitleForTopic": "crwdns334399:0{ topicTitle }crwdnd334399:0{ channelTitle }crwdne334399:0",
"UnPinnedDevices.channels": "crwdns370437:0count={count}crwdnd370437:0count={count}crwdne370437:0",
"WelcomeModal.learnOnlyDeviceWelcomeMessage1": "crwdns333055:0crwdne333055:0",
"WelcomeModal.learnOnlyDeviceWelcomeMessage2": "crwdns333057:0crwdne333057:0",
"WelcomeModal.postSyncWelcomeMessage1": "crwdns333059:0crwdne333059:0",
"WelcomeModal.postSyncWelcomeMessage2": "crwdns333061:0{facilityName}crwdne333061:0",
"WelcomeModal.welcomeModalContentDescription": "crwdns333063:0crwdne333063:0",
"WelcomeModal.welcomeModalHeader": "crwdns333065:0crwdne333065:0",
"WelcomeModal.welcomeModalPermissionsDescription": "crwdns333067:0crwdne333067:0",
"YourClasses.noClasses": "crwdns334407:0crwdne334407:0",
"YourClasses.yourClassesHeader": "crwdns334411:0crwdne334411:0"
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,37 @@
{
"ChannelContentsSummary.onDeviceRow": "crwdns332545:0crwdne332545:0",
"CommonLearnStrings.author": "crwdns370303:0crwdne370303:0",
"CommonLearnStrings.backToAllLibraries": "crwdns370305:0crwdne370305:0",
"CommonLearnStrings.cannotConnectToLibrary": "crwdns370307:0{deviceName}crwdnd370307:0{deviceName}crwdne370307:0",
"CommonLearnStrings.channelAndFoldersLabel": "crwdns370309:0crwdne370309:0",
"CommonLearnStrings.classesAndAssignmentsLabel": "crwdns370311:0crwdne370311:0",
"CommonLearnStrings.copyrightHolder": "crwdns370313:0crwdne370313:0",
"CommonLearnStrings.documentTitle": "crwdns370315:0{ contentTitle }crwdnd370315:0{ channelTitle }crwdne370315:0",
"CommonLearnStrings.dontShowThisAgainLabel": "crwdns370317:0crwdne370317:0",
"CommonLearnStrings.estimatedTime": "crwdns370319:0crwdne370319:0",
"CommonLearnStrings.exploreLibraries": "crwdns370321:0crwdne370321:0",
"CommonLearnStrings.exploreResources": "crwdns334197:0crwdne334197:0",
"CommonLearnStrings.filterAndSearchLabel": "crwdns370323:0crwdne370323:0",
"CommonLearnStrings.kolibriLibrary": "crwdns370325:0crwdne370325:0",
"CommonLearnStrings.learnLabel": "crwdns334199:0crwdne334199:0",
"CommonLearnStrings.license": "crwdns370327:0crwdne370327:0",
"CommonLearnStrings.loadingLibraries": "crwdns370329:0crwdne370329:0",
"CommonLearnStrings.locationsInChannel": "crwdns370331:0{channelname}crwdne370331:0",
"CommonLearnStrings.logo": "crwdns334203:0{channelTitle}crwdne334203:0",
"CommonLearnStrings.markResourceAsCompleteLabel": "crwdns334205:0crwdne334205:0",
"CommonLearnStrings.moreLibraries": "crwdns370333:0crwdne370333:0",
"CommonLearnStrings.mostPopularLabel": "crwdns334207:0crwdne334207:0",
"CommonLearnStrings.multipleLearningActivities": "crwdns334209:0crwdne334209:0",
"CommonLearnStrings.nextStepsLabel": "crwdns334213:0crwdne334213:0",
"CommonLearnStrings.popularLabel": "crwdns334215:0crwdne334215:0",
"CommonLearnStrings.resourceCompletedLabel": "crwdns334219:0crwdne334219:0",
"CommonLearnStrings.resumeLabel": "crwdns334223:0crwdne334223:0",
"CommonLearnStrings.shareFile": "crwdns370335:0crwdne370335:0",
"CommonLearnStrings.showLess": "crwdns370337:0crwdne370337:0",
"CommonLearnStrings.suggestedTime": "crwdns334225:0crwdne334225:0",
"CommonLearnStrings.toggleLicenseDescription": "crwdns370339:0crwdne370339:0",
"CommonLearnStrings.viewResource": "crwdns370341:0crwdne370341:0",
"CommonLearnStrings.whatYouWillNeed": "crwdns370343:0crwdne370343:0",
"DownloadRequests.downloadStartedLabel": "crwdns370347:0crwdne370347:0",
"DownloadRequests.goToDownloadsPage": "crwdns370349:0crwdne370349:0",
"DownloadRequests.resourceRemoved": "crwdns370351:0crwdne370351:0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{
"AppError.defaultErrorExitPrompt": "crwdns369995:0crwdne369995:0",
"AppError.defaultErrorHeader": "crwdns369997:0crwdne369997:0",
"AppError.defaultErrorMessage": "crwdns369999:0crwdne369999:0",
"AppError.defaultErrorReportPrompt": "crwdns370001:0crwdne370001:0",
"AppError.defaultErrorResolution": "crwdns370003:0crwdne370003:0",
"AppError.resourceNotFoundHeader": "crwdns370005:0crwdne370005:0",
"AppError.resourceNotFoundMessage": "crwdns370007:0crwdne370007:0",
"CommonProfileStrings.createAccount": "crwdns369649:0crwdne369649:0",
"CommonProfileStrings.mergeAccounts": "crwdns370439:0crwdne370439:0",
"CommonProfileStrings.useAdminAccount": "crwdns369653:0crwdne369653:0",
Expand Down Expand Up @@ -58,6 +65,13 @@
"OnboardingStepBase.newLearningFacilitySteps": "crwdns369885:0{step}crwdnd369885:0{steps}crwdne369885:0",
"PersonalDataConsentForm.description": "crwdns369887:0crwdne369887:0",
"PersonalDataConsentForm.header": "crwdns333339:0crwdne333339:0",
"ReportErrorModal.emailDescription": "crwdns370065:0crwdne370065:0",
"ReportErrorModal.emailPrompt": "crwdns370067:0crwdne370067:0",
"ReportErrorModal.errorDetailsHeader": "crwdns370069:0crwdne370069:0",
"ReportErrorModal.forumPostingTips": "crwdns370071:0crwdne370071:0",
"ReportErrorModal.forumPrompt": "crwdns370073:0crwdne370073:0",
"ReportErrorModal.forumUseTips": "crwdns370075:0crwdne370075:0",
"ReportErrorModal.reportErrorHeader": "crwdns370077:0crwdne370077:0",
"RequirePasswordForLearnersForm.header": "crwdns333343:0crwdne333343:0",
"RequirePasswordForLearnersForm.noOptionLabel": "crwdns369889:0crwdne369889:0",
"RequirePasswordForLearnersForm.yesOptionLabel": "crwdns369891:0crwdne369891:0",
Expand All @@ -76,6 +90,8 @@
"SettingUpKolibri.pageTitle": "crwdns369913:0crwdne369913:0",
"SettingUpKolibri.pleaseWaitMessage": "crwdns369915:0crwdne369915:0",
"SetupWizardIndex.documentTitle": "crwdns333385:0crwdne333385:0",
"TechnicalTextBlock.copiedToClipboardConfirmation": "crwdns370079:0crwdne370079:0",
"TechnicalTextBlock.copyToClipboardButtonPrompt": "crwdns370081:0crwdne370081:0",
"UserCredentialsForm.adminAccountCreationHeader": "crwdns369917:0crwdne369917:0",
"UserCredentialsForm.learnerAccountCreationDescription": "crwdns369919:0{facility}crwdne369919:0",
"UserCredentialsForm.learnerAccountCreationHeader": "crwdns369921:0crwdne369921:0"
Expand Down
Loading

0 comments on commit 8f442b4

Please sign in to comment.