Skip to content

Commit

Permalink
More granular sync status information
Browse files Browse the repository at this point in the history
Differentiate between successful and failed sync

Resolves #9091
  • Loading branch information
MisRob committed Jun 30, 2022
1 parent fb86918 commit 3762195
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,135 @@
import { shallowMount } from '@vue/test-utils';
import FacilityNameAndSyncStatus from '../index';

describe('FacilityNameAndSyncStatus', () => {
it('smoke test', () => {
describe(`FacilityNameAndSyncStatus`, () => {
it(`smoke test`, () => {
const wrapper = shallowMount(FacilityNameAndSyncStatus);
expect(wrapper.exists()).toBeTruthy();
});

describe(`sync status`, () => {
describe(`when a facility has never been synced`, () => {
it(`shows the "Never synced" message`, () => {
const wrapper = shallowMount(FacilityNameAndSyncStatus, {
propsData: {
facility: {
name: 'Test facility',
last_successful_sync: null,
last_failed_sync: null,
},
},
});
expect(wrapper.html()).toContain('Never synced');
});

describe(`when the sync task has failed`, () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(FacilityNameAndSyncStatus, {
propsData: {
facility: {
name: 'Test facility',
last_successful_sync: null,
last_failed_sync: null,
},
syncTaskHasFailed: true,
},
});
});

it(`doesn't show the "Never synced" message`, () => {
expect(wrapper.html()).not.toContain('Never synced');
});

it(`shows the "Most recent sync failed" message`, () => {
expect(wrapper.html()).toContain('Most recent sync failed');
});
});
});

describe(`when a facility has been synced at least once in the past`, () => {
describe(`when the last failed sync is more recent than the last successful sync`, () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(FacilityNameAndSyncStatus, {
propsData: {
facility: {
name: 'Test facility',
last_successful_sync: '2022-04-21T16:00:00Z',
last_failed_sync: '2022-06-25T13:00:00Z',
},
},
});
});

it(`shows relative time of the last successful sync`, () => {
expect(wrapper.html()).toContain(
`Last successful sync: ${wrapper.vm.$formatRelative('2022-04-21T16:00:00Z', {
now: wrapper.vm.now,
})}`
);
});

it(`shows the "Most recent sync failed" message`, () => {
expect(wrapper.html()).toContain('Most recent sync failed');
});
});

describe(`when the last failed sync is older than the last successful sync`, () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(FacilityNameAndSyncStatus, {
propsData: {
facility: {
name: 'Test facility',
last_successful_sync: '2022-06-25T13:00:00Z',
last_failed_sync: '2022-04-21T16:00:00Z',
},
},
});
});

it(`shows relative time of the last successful sync`, () => {
expect(wrapper.html()).toContain(
`Last successful sync: ${wrapper.vm.$formatRelative('2022-06-25T13:00:00Z', {
now: wrapper.vm.now,
})}`
);
});

it(`doesn't show the "Most recent sync failed" message`, () => {
expect(wrapper.html()).not.toContain('Most recent sync failed');
});
});

describe(`when the sync task has failed`, () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(FacilityNameAndSyncStatus, {
propsData: {
facility: {
name: 'Test facility',
last_successful_sync: '2022-06-25T13:00:00Z',
last_failed_sync: '2022-04-21T16:00:00Z',
},
syncTaskHasFailed: true,
},
});
});

it(`shows relative time of the last successful sync`, () => {
expect(wrapper.html()).toContain(
`Last successful sync: ${wrapper.vm.$formatRelative('2022-06-25T13:00:00Z', {
now: wrapper.vm.now,
})}`
);
});

// failed sync task information takes precendence over information on the facility object
it(`shows the "Most recent sync failed" message`, () => {
expect(wrapper.html()).toContain('Most recent sync failed');
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,17 @@
{{ getTaskString('removingFacilityStatus') }}
</template>
<template v-else>
<span v-if="syncHasFailed" class="sync-message">
<!-- Always show the last successful sync time when available on the first row -->
<span v-if="facility.last_successful_sync" class="sync-message">
{{ $tr('lastSync', { relativeTime: formattedTime(facility.last_successful_sync) }) }}
</span>

<span v-if="syncFailed" class="sync-message">
{{ $tr('syncFailed') }}
</span>
<span v-if="facility.last_synced === null" class="sync-message">
<span v-else-if="neverSynced" class="sync-message">
{{ $tr('neverSynced') }}
</span>
<span v-else class="sync-message">
{{ $tr('lastSync', { relativeTime: formattedTime(facility.last_synced) }) }}
</span>
</template>
</span>
</div>
Expand All @@ -67,7 +69,7 @@
type: Boolean,
default: false,
},
syncHasFailed: {
syncTaskHasFailed: {
type: Boolean,
default: false,
},
Expand All @@ -81,6 +83,22 @@
now: now(),
};
},
computed: {
syncFailed() {
const lastSyncFailed =
this.facility &&
this.facility.last_successful_sync &&
this.facility.last_failed_sync &&
new Date(this.facility.last_successful_sync).getTime() <
new Date(this.facility.last_failed_sync).getTime();
return this.syncTaskHasFailed || lastSyncFailed;
},
neverSynced() {
return (
this.facility && !this.facility.last_successful_sync && !this.facility.last_failed_sync
);
},
},
methods: {
formattedTime(datetime) {
if (this.now - new Date(datetime) < 10000) {
Expand Down
47 changes: 38 additions & 9 deletions kolibri/core/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
from django_filters.rest_framework import FilterSet
from django_filters.rest_framework import ModelChoiceFilter
from morango.api.permissions import BasicMultiArgumentAuthentication
from morango.constants import transfer_stages
from morango.constants import transfer_statuses
from morango.models import TransferSession
from rest_framework import decorators
from rest_framework import filters
Expand Down Expand Up @@ -486,13 +488,28 @@ class FacilityViewSet(ValuesViewset):
queryset = Facility.objects.all()
serializer_class = FacilitySerializer

facility_values = ["id", "name", "num_classrooms", "num_users", "last_synced"]
facility_values = [
"id",
"name",
"num_classrooms",
"num_users",
"last_successful_sync",
"last_failed_sync",
]

values = tuple(facility_values + dataset_keys)

field_map = {"dataset": _map_dataset}

def annotate_queryset(self, queryset):
transfer_session_dataset_filter = Func(
Cast(OuterRef("dataset"), TextField()),
Value("-"),
Value(""),
function="replace",
output_field=TextField(),
)

return (
queryset.annotate(
num_users=SQCount(
Expand All @@ -505,15 +522,27 @@ def annotate_queryset(self, queryset):
)
)
.annotate(
last_synced=Subquery(
last_successful_sync=Subquery(
# the sync command does a pull, then a push, so if the push succeeded,
# the pull likely did too, which means this should represent when the
# facility was last fully and successfully synced
TransferSession.objects.filter(
push=True,
active=False,
transfer_stage=transfer_stages.CLEANUP,
transfer_stage_status=transfer_statuses.COMPLETED,
filter=transfer_session_dataset_filter,
)
.order_by("-last_activity_timestamp")
.values("last_activity_timestamp")[:1]
)
)
.annotate(
last_failed_sync=Subquery(
# Here we simply look for if any transfer session has errored
TransferSession.objects.filter(
filter=Func(
Cast(OuterRef("dataset"), TextField()),
Value("-"),
Value(""),
function="replace",
output_field=TextField(),
)
transfer_stage_status=transfer_statuses.ERRORED,
filter=transfer_session_dataset_filter,
)
.order_by("-last_activity_timestamp")
.values("last_activity_timestamp")[:1]
Expand Down
63 changes: 63 additions & 0 deletions kolibri/core/auth/test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
import base64
import collections
import sys
import uuid
from datetime import datetime
from importlib import import_module

import factory
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils import timezone
from morango.constants import transfer_stages
from morango.constants import transfer_statuses
from morango.models import SyncSession
from morango.models import TransferSession
from rest_framework import status
from rest_framework.test import APITestCase as BaseTestCase

Expand Down Expand Up @@ -430,6 +437,33 @@ def setUpTestData(cls):
cls.facility2 = FacilityFactory.create()
cls.user1 = FacilityUserFactory.create(facility=cls.facility1)
cls.user2 = FacilityUserFactory.create(facility=cls.facility2)
cls.date_completed_push_transfer_session = datetime(
2022, 6, 30, tzinfo=timezone.utc
)
cls.date_failed_transfer_session = datetime(2022, 6, 14, tzinfo=timezone.utc)
cls.sync_session = SyncSession.objects.create(
id=uuid.uuid4().hex,
profile="facilitydata",
last_activity_timestamp=cls.date_completed_push_transfer_session,
)
cls.completed_push_transfer_session = TransferSession.objects.create(
id=uuid.uuid4().hex,
sync_session_id=cls.sync_session.id,
filter=cls.facility1.dataset_id,
push=True,
active=False,
transfer_stage=transfer_stages.CLEANUP,
transfer_stage_status=transfer_statuses.COMPLETED,
last_activity_timestamp=cls.date_completed_push_transfer_session,
)
cls.failed_transfer_session = TransferSession.objects.create(
id=uuid.uuid4().hex,
sync_session_id=cls.sync_session.id,
filter=cls.facility1.dataset_id,
push=True,
transfer_stage_status=transfer_statuses.ERRORED,
last_activity_timestamp=cls.date_failed_transfer_session,
)

def test_sanity(self):
self.assertTrue(
Expand All @@ -455,6 +489,35 @@ def test_facility_user_can_get_detail(self):
{"name": self.facility1.name}, dict(response.data)
)

def test_facility_user_can_get_last_successful_sync(self):
self.client.login(
username=self.user1.username,
password=DUMMY_PASSWORD,
facility=self.facility1,
)
response = self.client.get(
reverse("kolibri:core:facility-detail", kwargs={"pk": self.facility1.pk}),
format="json",
)
self.assertEqual(
response.data["last_successful_sync"],
self.date_completed_push_transfer_session,
)

def test_facility_user_can_get_last_failed_sync(self):
self.client.login(
username=self.user1.username,
password=DUMMY_PASSWORD,
facility=self.facility1,
)
response = self.client.get(
reverse("kolibri:core:facility-detail", kwargs={"pk": self.facility1.pk}),
format="json",
)
self.assertEqual(
response.data["last_failed_sync"], self.date_failed_transfer_session
)

def test_device_admin_can_create_facility(self):
new_facility_name = "New Facility"
self.client.login(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@
};
},
computed: {
// Assume that if first facility has non-null 'last_synced'
// Assume that if first facility has non-null 'last_successful_sync'
// field, then it was imported in Setup Wizard.
// This used to determine Select Source workflow to enter into
importedFacility() {
const [facility] = this.$store.state.core.facilities;
if (facility && facility.last_synced !== null) {
if (facility && facility.last_successful_sync !== null) {
return facility;
}
return null;
Expand Down

0 comments on commit 3762195

Please sign in to comment.