Skip to content

Commit

Permalink
Merge pull request #8259 from learningequality/release-v0.15.x
Browse files Browse the repository at this point in the history
Cascade merge release-v0.15.x into develop
  • Loading branch information
rtibbles committed Aug 3, 2021
2 parents 249b93b + b1afacc commit 2670b89
Show file tree
Hide file tree
Showing 13 changed files with 135 additions and 115 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ kolibrihome_test/

# Translations
kolibri/locale/en/
kolibri/locale/CSV_FILES/**/*
kolibri/locale/**/LC_MESSAGES/*.csv

# Mr Developer
.mr.developer.cfg
Expand Down
14 changes: 1 addition & 13 deletions kolibri/core/assets/src/views/AppBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -188,19 +188,7 @@
},
mapSyncStatusOptionToLearner() {
if (this.userSyncStatus) {
if (this.userSyncStatus.active) {
return SyncStatus.SYNCING;
} else if (this.userSyncStatus.queued) {
return SyncStatus.QUEUED;
} else if (this.userSyncStatus.last_synced) {
const currentDateTime = new Date();
const timeDifference = currentDateTime - this.userSyncStatus.last_synced;
if (timeDifference < 5184000000) {
return SyncStatus.RECENTLY_SYNCED;
} else {
return SyncStatus.NOT_RECENTLY_SYNCED;
}
}
return this.userSyncStatus.status;
}
return SyncStatus.NOT_CONNECTED;
},
Expand Down
2 changes: 2 additions & 0 deletions kolibri/core/assets/src/views/SyncStatusDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
[SyncStatus.QUEUED]: this.$tr('queued'),
[SyncStatus.SYNCING]: this.$tr('syncing'),
[SyncStatus.UNABLE_TO_SYNC]: this.$tr('unableToSync'),
[SyncStatus.NOT_RECENTLY_SYNCED]: this.$tr('notRecentlySynced'),
[SyncStatus.UNABLE_OR_NOT_SYNCED]: this.$tr('unableOrNotSynced'),
[SyncStatus.NOT_CONNECTED]: this.$tr('notConnected'),
};
Expand Down Expand Up @@ -72,6 +73,7 @@
syncing: 'Syncing...',
queued: 'Waiting to sync...',
unableToSync: 'Unable to sync',
notRecentlySynced: 'Not recently synced',
unableOrNotSynced: 'Not recently synced or unable to sync',
notConnected: 'Not connected to server',
},
Expand Down
53 changes: 46 additions & 7 deletions kolibri/core/device/api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import timedelta
from sys import version_info

from django.conf import settings
Expand All @@ -6,11 +7,12 @@
from django.db.models import OuterRef
from django.db.models.query import Q
from django.http.response import HttpResponseBadRequest
from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
from django_filters.rest_framework import FilterSet
from django_filters.rest_framework import ModelChoiceFilter
from morango.models import InstanceIDModel
from morango.models import SyncSession
from morango.models import TransferSession
from rest_framework import mixins
from rest_framework import status
from rest_framework import views
Expand All @@ -31,6 +33,8 @@
from kolibri.core.auth.api import KolibriAuthPermissionsFilter
from kolibri.core.auth.models import Collection
from kolibri.core.content.permissions import CanManageContent
from kolibri.core.device.utils import get_device_setting
from kolibri.core.discovery.models import DynamicNetworkLocation
from kolibri.utils.conf import OPTIONS
from kolibri.utils.server import get_urls
from kolibri.utils.server import installation_type
Expand Down Expand Up @@ -193,34 +197,69 @@ class Meta:
fields = ["user", "member_of"]


RECENTLY_SYNCED = "RECENTLY_SYNCED"
SYNCING = "SYNCING"
QUEUED = "QUEUED"
NOT_RECENTLY_SYNCED = "NOT_RECENTLY_SYNCED"


def map_status(status):
"""
Summarize the current state of the sync into a constant for use by
the frontend.
"""
if status["active"]:
return SYNCING
elif status["queued"]:
return QUEUED
elif status["last_synced"]:
# Keep this as a fixed constant for now.
# In future versions this may be configurable.
if timezone.now() - status["last_synced"] < timedelta(minutes=15):
return RECENTLY_SYNCED
else:
return NOT_RECENTLY_SYNCED


class UserSyncStatusViewSet(ReadOnlyValuesViewset):
permission_classes = (KolibriAuthPermissions,)
filter_backends = (KolibriAuthPermissionsFilter, DjangoFilterBackend)
queryset = UserSyncStatus.objects.all()
filter_class = SyncStatusFilter

values = (
"id",
"queued",
"last_synced",
"active",
"user",
"user_id",
)

field_map = {
"status": map_status,
}

def get_queryset(self):
return UserSyncStatus.objects.filter()
# If this is a subset of users device, we should just return no data
# if there are no possible devices we could sync to.
if (
get_device_setting("subset_of_users_device", False)
and not DynamicNetworkLocation.objects.filter(
subset_of_users_device=False
).exists()
):
return UserSyncStatus.objects.none()
return UserSyncStatus.objects.all()

def annotate_queryset(self, queryset):

queryset = queryset.annotate(
last_synced=Max("sync_session__last_activity_timestamp")
)

sync_sessions = SyncSession.objects.filter(
id=OuterRef("sync_session__pk"), active=True
active_transfer_sessions = TransferSession.objects.filter(
sync_session=OuterRef("sync_session"), active=True
)

queryset = queryset.annotate(active=Exists(sync_sessions))
queryset = queryset.annotate(active=Exists(active_transfer_sessions))

return queryset
22 changes: 22 additions & 0 deletions kolibri/core/device/kolibri_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from kolibri.core.auth.hooks import FacilityDataSyncHook
from kolibri.plugins.hooks import register_hook


@register_hook
class UserSyncStatusHook(FacilityDataSyncHook):
def pre_transfer(
self,
dataset_id,
local_is_single_user,
remote_is_single_user,
single_user_id,
context,
):
# if we're about to do a single user sync, update UserSyncStatus accordingly
if context.sync_session and single_user_id is not None:
from kolibri.core.device.models import UserSyncStatus

UserSyncStatus.objects.update_or_create(
user_id=single_user_id,
defaults={"queued": False, "sync_session": context.sync_session},
)
11 changes: 10 additions & 1 deletion kolibri/core/public/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from kolibri.core.content.serializers import PublicChannelSerializer
from kolibri.core.content.utils.file_availability import generate_checksum_integer_mask
from kolibri.core.device.models import SyncQueue
from kolibri.core.device.models import UserSyncStatus
from kolibri.core.device.utils import allow_peer_unlisted_channel_import

MAX_CONCURRENT_SYNCS = 1
Expand Down Expand Up @@ -239,14 +240,18 @@ def create(self, request):
)
data["id"] = element.id

UserSyncStatus.objects.update_or_create(
user_id=user, defaults={"queued": not allow_sync}
)

return Response(data)

def update(self, request, pk=None):
SyncQueue.clean_stale() # first, ensure not expired devices are in the queue
allow_sync, data = self.check_queue(pk)

element = SyncQueue.objects.filter(id=pk).first()
if not allow_sync:
element = SyncQueue.objects.filter(id=pk).first()
if not element:
# this device has been deleted from the queue, likely due to keep alive expiration
content = {
Expand All @@ -259,4 +264,8 @@ def update(self, request, pk=None):
data["id"] = element.id
else:
SyncQueue.objects.filter(id=pk).delete()
if element is not None:
UserSyncStatus.objects.update_or_create(
user=element.user, defaults={"queued": not allow_sync}
)
return Response(data)
63 changes: 18 additions & 45 deletions kolibri/plugins/coach/assets/src/views/ClassLearnersListPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@
/>
</td>
<td>
<span dir="auto">
{{ $tr('lastSyncedTime', { time: mapLastSynctedTimeToLearner(learner.id) }) }}
</span>
<ElapsedTime
:date="mapLastSyncedTimeToLearner(learner.id)"
/>
</td>
</tr>
</tbody>
Expand All @@ -79,6 +79,7 @@
import CoreBase from 'kolibri.coreVue.components.CoreBase';
import CoreTable from 'kolibri.coreVue.components.CoreTable';
import ElapsedTime from 'kolibri.coreVue.components.ElapsedTime';
import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
import { SyncStatus } from 'kolibri.coreVue.vuex.constants';
import { mapState, mapActions } from 'vuex';
Expand All @@ -95,14 +96,15 @@
components: {
CoreBase,
CoreTable,
ElapsedTime,
SyncStatusDisplay,
SyncStatusDescription,
},
mixins: [commonCoreStrings],
data: function() {
return {
displayTroubleshootModal: false,
classSyncStatusList: [],
classSyncStatusList: {},
// poll every 10 seconds
pollingInterval: 10000,
};
Expand Down Expand Up @@ -139,55 +141,27 @@
},
methods: {
...mapActions(['fetchUserSyncStatus']),
mapLastSynctedTimeToLearner(learnerId) {
let learnerSyncData;
if (this.classSyncStatusList) {
learnerSyncData = this.classSyncStatusList.filter(entry => {
entry.user_id == learnerId;
});
}
mapLastSyncedTimeToLearner(learnerId) {
const learnerSyncData = this.classSyncStatusList[learnerId];
if (learnerSyncData) {
learnerSyncData.last_synced = new Date(new Date().valueOf() - 1000);
if (learnerSyncData.last_synced) {
const currentDateTime = new Date();
const timeDifference = currentDateTime - learnerSyncData.last_synced;
if (timeDifference < 5184000000) {
const diffMins = Math.round(timeDifference / 60).toString();
return diffMins.toString();
}
}
return '--';
return learnerSyncData.last_synced;
}
return '--';
return null;
},
mapSyncStatusOptionToLearner(learnerId) {
let learnerSyncData;
if (this.classSyncStatusList) {
learnerSyncData = this.classSyncStatusList.filter(entry => {
return entry.user_id == learnerId;
});
learnerSyncData = learnerSyncData[learnerSyncData.length - 1];
}
const learnerSyncData = this.classSyncStatusList[learnerId];
if (learnerSyncData) {
if (learnerSyncData.active) {
return SyncStatus.SYNCING;
} else if (learnerSyncData.queued) {
return SyncStatus.QUEUED;
} else if (learnerSyncData.last_synced) {
const currentDateTime = new Date();
const timeDifference = currentDateTime - learnerSyncData.last_synced;
if (timeDifference < 5184000000) {
return SyncStatus.RECENTLY_SYNCED;
} else {
return SyncStatus.NOT_RECENTLY_SYNCED;
}
}
return learnerSyncData.status;
}
return SyncStatus.NOT_CONNECTED;
},
pollClassListSyncStatuses() {
this.fetchUserSyncStatus({ member_of: this.$route.params.classId }).then(status => {
this.classSyncStatusList = status;
this.fetchUserSyncStatus({ member_of: this.$route.params.classId }).then(data => {
const statuses = {};
for (let status of data) {
statuses[status.user] = status;
}
this.classSyncStatusList = statuses;
});
if (this.isPolling) {
setTimeout(() => {
Expand All @@ -200,7 +174,6 @@
pageHeader: "Learners in '{className}'",
deviceStatus: 'Device status',
lastSyncedStatus: 'Last synced',
lastSyncedTime: 'Last synced {time} minutes ago',
howToTroubleshootModalHeader: 'Information about sync statuses',
close: 'Close',
},
Expand Down
4 changes: 2 additions & 2 deletions packages/kolibri-tools/lib/i18n/ExtractMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function clearCsvPath(csvPath) {
logging.info(`Removing existing messages files from ${csvPath}`);

try {
const removedFiles = del.sync(csvPath);
const removedFiles = del.sync(path.join(csvPath, '*.csv'));
logging.info(`Successfully cleared path for CSVs by removing: ${removedFiles.join('\n')}`);
} catch (e) {
logging.error('Failed to clear CSV path. Error message to follow...');
Expand Down Expand Up @@ -117,7 +117,7 @@ module.exports = function(pathInfo, ignore, localeDataFolder) {
extractedMessages[namespace] = getAllMessagesFromFilePath(pathData.moduleFilePath, ignore);
});

const csvPath = path.join(localeDataFolder, 'CSV_FILES', 'en');
const csvPath = path.join(localeDataFolder, 'en', 'LC_MESSAGES');
// Let's just get rid of the old files to limit room for issues w/ file system
clearCsvPath(csvPath);

Expand Down

0 comments on commit 2670b89

Please sign in to comment.