Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.16.x into develop #11712

Merged
merged 32 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c0c721b
Add autocomplete attribute to username component.
rtibbles Dec 15, 2023
a1abca0
Prevent vue prop warnings and form warnings for user credentials form.
rtibbles Dec 15, 2023
1a95d79
Simplify AppError component.
rtibbles Dec 15, 2023
1b8ae36
Revert previous suggestion to use UIAlert directly.
rtibbles Dec 15, 2023
7941f44
added isLatestCheck
thesujai Dec 28, 2023
45e7a58
move MeteredConnModal from AppBarPage to Learn Library index + cleanu…
thanksameeelian Jan 2, 2024
45d4580
remove extraneous addition to learn module state
thanksameeelian Dec 7, 2023
8e7aa55
Restore conditional display of errors.
rtibbles Jan 4, 2024
66e00ac
Update package versions.
rtibbles Jan 4, 2024
8b7fdc8
Add command line deprecation warning for Python 2.7
rtibbles Jan 4, 2024
f44cb61
Bump the babel group with 3 updates
dependabot[bot] Jan 5, 2024
21a8834
Bump html-webpack-plugin from 5.5.4 to 5.6.0
dependabot[bot] Jan 5, 2024
1519e6a
Merge pull request #11701 from learningequality/dependabot/npm_and_ya…
rtibbles Jan 5, 2024
0ea3739
Merge pull request #11703 from learningequality/dependabot/npm_and_ya…
rtibbles Jan 5, 2024
18bc580
Merge pull request #11700 from rtibbles/py27_deprecation_warning
marcellamaki Jan 5, 2024
1f75221
Constant to notice an invalid username
Jan 5, 2024
9bfe9ec
distinguish validation when remote facility allows authentication wit…
Jan 5, 2024
3f9b0b1
Merge pull request #11698 from rtibbles/new_npm_version
jredrejo Jan 5, 2024
f730c3d
Merge pull request #11683 from rtibbles/metered_connection
marcellamaki Jan 5, 2024
dfe1a9d
Merge pull request #11655 from rtibbles/better_setup_wizard_error
marcellamaki Jan 5, 2024
08fac49
Clean up existing JSON message files before regenerating.
rtibbles Jan 6, 2024
0842cde
Update message files to latest.
rtibbles Jan 6, 2024
96e8c8a
remove CACHES override in dev settings
thesujai Jan 7, 2024
528b3c8
Merge pull request #11676 from thesujai/new-badge-on-delete-channel
rtibbles Jan 8, 2024
edc50e6
Merge pull request #11705 from rtibbles/erroneous_files
rtibbles Jan 8, 2024
7981c29
Merge pull request #11706 from thesujai/cache-override
rtibbles Jan 8, 2024
c896256
Pin alabaster theme to version that is compatible with our version of…
rtibbles Jan 9, 2024
92bad83
Add method to enqueue jobs as LIFO rather than FIFO
nick2432 Jan 9, 2024
714defb
Bump follow-redirects from 1.15.2 to 1.15.4
dependabot[bot] Jan 10, 2024
a20a9f0
Merge pull request #11709 from rtibbles/minimal_docs_fix
MisRob Jan 10, 2024
d840fb0
Merge pull request #11710 from learningequality/dependabot/npm_and_ya…
rtibbles Jan 10, 2024
6ad58b3
Merge pull request #11704 from jredrejo/message_invalid_username
rtibbles Jan 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
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
4 changes: 0 additions & 4 deletions kolibri/core/assets/src/views/CorePage/AppBarPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@
@cancel="languageModalShown = false"
/>

<MeteredConnectionNotificationModal />

</div>

</template>
Expand All @@ -69,7 +67,6 @@
import { LearnerDeviceStatus } from 'kolibri.coreVue.vuex.constants';
import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
import { isTouchDevice } from 'kolibri.utils.browserInfo';
import MeteredConnectionNotificationModal from 'kolibri-common/components/MeteredConnectionNotificationModal';
import AppBar from '../AppBar';
import StorageNotification from '../StorageNotification';
import useUserSyncStatus from '../../composables/useUserSyncStatus';
Expand All @@ -78,7 +75,6 @@
name: 'AppBarPage',
components: {
AppBar,
MeteredConnectionNotificationModal,
LanguageSwitcherModal,
ScrollingHeader,
SideNav,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
:maxlength="30"
:invalid="Boolean(shownInvalidText)"
:invalidText="shownInvalidText"
autocomplete="username"
@blur="blurred = true"
@input="handleInput"
/>
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
Loading
Loading