Skip to content

Commit

Permalink
Merge pull request #8681 from indirectlylit/merge-0.15
Browse files Browse the repository at this point in the history
merge 0.15 -> develop
  • Loading branch information
sairina authored Nov 12, 2021
2 parents 625e95e + 91c7814 commit 609eaa6
Show file tree
Hide file tree
Showing 514 changed files with 13,968 additions and 12,014 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ repos:
rev: v1.3.3
hooks:
- id: reorder-python-imports
language_version: python2.7
language_version: python3
- repo: local
hooks:
- id: lint-frontend
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Feature: Learner view selection of next content
In a lesson context, the learner needs to see the contents of the lesson
when opening the side panel.
In a regular context, the learner should see content also in the same topic
and be linked to the next topic when opening the side panel

Background:
Given that I am signed in as a Learner
And that I have been assigned a <lesson> with multiple items

Scenario: Opening the side panel and clicking another resource
Given I am viewing <resource-1> in <lesson>
When I click the the *View lesson resources* icon in the top menu
Then I see a side bar appear on the right
And it says *Also in this lesson*
And it lists resources, not including the one being currently viewed
When I click <resource-2>
Then I am redirected to view <resource-2>
When I click again on the *View lesson resources* icon in the top menu
Then I should see <resource-1> as an option, but not <resource-2>
And I should see my progress reflected next to the entry for <resource-1>
When I click on the gray area, hit the ESC key or click the X icon
Then the side bar is closed and I see <resource-2> unobstructed

Scenario: Viewing a resource whose sibling has a duration set
Given I am viewing <resource> in the same lesson as <resource-2>, which has a set duration property
When I click the *View lesson resources* icon in the top menu
Then I see a side bar appear on the right
And I see <resource-2> and it displays its duration under its title

Background:

Scenario: Viewing content as a guest users
Given I can view <channel> which has multiple resources nested in folders
When I view a <topic> which has no sibling topics in the same folder
And I click to view a <resource> within that <topic>
And I click to open the side panel while viewing <resource>
Then I will see all items belonging to <topic> except <resource>
When I view a <topic-2> which has another folder alongside it <topic-3>
And I click to view a <resource> within <topic-2>
And I click to open the side panel while viewing <resource>
Then I will see all items belonging to <topic-2> except <resource>
And I will see a link at the bottom, linking me to <topic-3>

Examples:
| channel | resource | topic-n | lesson |
| KA English | Counting | Topic N | Counting Lesson |
136 changes: 89 additions & 47 deletions kolibri/core/analytics/test/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import csv
import datetime
import hashlib
import io
import os
import random
Expand All @@ -17,6 +18,7 @@
from kolibri.core.analytics.models import PingbackNotification
from kolibri.core.analytics.utils import calculate_list_stats
from kolibri.core.analytics.utils import create_and_update_notifications
from kolibri.core.analytics.utils import encodestring
from kolibri.core.analytics.utils import extract_channel_statistics
from kolibri.core.analytics.utils import extract_facility_statistics
from kolibri.core.auth.constants import demographics
Expand All @@ -28,7 +30,9 @@
from kolibri.core.content.models import ContentNode
from kolibri.core.content.models import File
from kolibri.core.content.models import LocalFile
from kolibri.core.device.models import DeviceSettings
from kolibri.core.device.utils import provision_device
from kolibri.core.device.utils import provision_single_user_device
from kolibri.core.exams.models import Exam
from kolibri.core.lessons.models import Lesson
from kolibri.core.logger.models import AttemptLog
Expand All @@ -49,7 +53,15 @@ def mean(data):


class BaseDeviceSetupMixin(object):
n_facilities = 1
n_superusers = 1
n_users = 20 # 20 users x 1 facility = 20 users
n_classes = 1 # 1 class x 1 facility = 1 class
min_timestamp = datetime.datetime(2018, 10, 11)
max_timestamp = datetime.datetime(2019, 10, 11)

def setUp(self):
super(BaseDeviceSetupMixin, self).setUp()
# create dummy channel
channel_id = uuid.uuid4().hex
root = ContentNode.objects.create(
Expand All @@ -58,9 +70,8 @@ def setUp(self):
channel_id=channel_id,
content_id=uuid.uuid4().hex,
)
min_timestamp = datetime.datetime(2018, 10, 11)
self.channel = ChannelMetadata.objects.create(
id=channel_id, name="channel", last_updated=min_timestamp, root=root
id=channel_id, name="channel", last_updated=self.min_timestamp, root=root
)
lf = LocalFile.objects.create(
id=uuid.uuid4().hex, available=True, file_size=1048576 # 1 MB
Expand All @@ -72,75 +83,84 @@ def setUp(self):
with io.open(data_path, mode="r", encoding="utf-8") as f:
users = [data for data in csv.DictReader(f)]

n_facilities = 1
n_classes = 1 # 1 class x 1 facility = 1 class
n_users = 20 # 20 users x 1 facility = 20 users
max_timestamp = datetime.datetime(2019, 10, 11)
self.facilities = user_data.get_or_create_facilities(
n_facilities=self.n_facilities
)
self.users = []

self.facilities = user_data.get_or_create_facilities(n_facilities=n_facilities)
for facility in self.facilities:
dataset = facility.dataset
# create superuser and login session
superuser = create_superuser(facility=facility)
facility.add_role(superuser, role_kinds.ADMIN)
UserSessionLog.objects.create(
user=superuser,
start_timestamp=min_timestamp,
last_interaction_timestamp=max_timestamp,
)
# create lesson and exam for facility
Lesson.objects.create(
created_by=superuser, title="lesson", collection=facility
)
exam = Exam.objects.create(
creator=superuser, title="exam", question_count=1, collection=facility
)
for i in range(self.n_superusers):
superuser = create_superuser(
facility=facility, username="superuser{}".format(i)
)
facility.add_role(superuser, role_kinds.ADMIN)
UserSessionLog.objects.create(
user=superuser,
start_timestamp=self.min_timestamp,
last_interaction_timestamp=self.max_timestamp,
)
# create lesson and exam for facility
Lesson.objects.create(
created_by=superuser, title="lesson", collection=facility
)
exam = Exam.objects.create(
creator=superuser,
title="exam",
question_count=1,
collection=facility,
)
exam_id = exam.id
else:
exam_id = uuid.uuid4().hex

classrooms = user_data.get_or_create_classrooms(
n_classes=n_classes, facility=facility
n_classes=self.n_classes, facility=facility
)

# Get all the user data at once so that it is distinct across classrooms
facility_user_data = random.sample(users, n_classes * n_users)
facility_user_data = random.sample(users, self.n_classes * self.n_users)

# create random content id for the session logs
self.content_id = uuid.uuid4().hex
for i, classroom in enumerate(classrooms):
classroom_user_data = facility_user_data[
i * n_users : (i + 1) * n_users
i * self.n_users : (i + 1) * self.n_users
]
users = user_data.get_or_create_classroom_users(
n_users=n_users,
n_users=self.n_users,
classroom=classroom,
user_data=classroom_user_data,
facility=facility,
)
self.users.extend(users)
# create 1 of each type of log per user
for user in users:
for _ in range(1):
sessionlog = ContentSessionLog.objects.create(
user=user,
start_timestamp=min_timestamp,
end_timestamp=max_timestamp,
start_timestamp=self.min_timestamp,
end_timestamp=self.max_timestamp,
content_id=self.content_id,
channel_id=self.channel.id,
time_spent=60, # 1 minute
kind=content_kinds.EXERCISE,
)
AttemptLog.objects.create(
item="item",
start_timestamp=min_timestamp,
end_timestamp=max_timestamp,
completion_timestamp=max_timestamp,
start_timestamp=self.min_timestamp,
end_timestamp=self.max_timestamp,
completion_timestamp=self.max_timestamp,
correct=1,
sessionlog=sessionlog,
)
# create 1 anon log per user session log
ContentSessionLog.objects.create(
dataset=dataset,
user=None,
start_timestamp=min_timestamp,
end_timestamp=max_timestamp,
start_timestamp=self.min_timestamp,
end_timestamp=self.max_timestamp,
content_id=self.content_id,
channel_id=self.channel.id,
time_spent=60, # 1 minute,
Expand All @@ -149,35 +169,35 @@ def setUp(self):
for _ in range(1):
UserSessionLog.objects.create(
user=user,
start_timestamp=min_timestamp,
last_interaction_timestamp=max_timestamp,
start_timestamp=self.min_timestamp,
last_interaction_timestamp=self.max_timestamp,
device_info="Android,9/Chrome Mobile,86",
)
for _ in range(1):
ContentSummaryLog.objects.create(
user=user,
start_timestamp=min_timestamp,
end_timestamp=max_timestamp,
completion_timestamp=max_timestamp,
start_timestamp=self.min_timestamp,
end_timestamp=self.max_timestamp,
completion_timestamp=self.max_timestamp,
content_id=uuid.uuid4().hex,
channel_id=self.channel.id,
)
for _ in range(1):
sl = ContentSessionLog.objects.create(
user=user,
start_timestamp=min_timestamp,
end_timestamp=max_timestamp,
content_id=exam.id,
start_timestamp=self.min_timestamp,
end_timestamp=self.max_timestamp,
content_id=exam_id,
channel_id=None,
time_spent=60, # 1 minute
kind=content_kinds.QUIZ,
)
summarylog = ContentSummaryLog.objects.create(
user=user,
start_timestamp=min_timestamp,
end_timestamp=max_timestamp,
completion_timestamp=max_timestamp,
content_id=exam.id,
start_timestamp=self.min_timestamp,
end_timestamp=self.max_timestamp,
completion_timestamp=self.max_timestamp,
content_id=exam_id,
channel_id=None,
kind=content_kinds.QUIZ,
)
Expand All @@ -191,13 +211,17 @@ def setUp(self):
AttemptLog.objects.create(
masterylog=masterylog,
sessionlog=sl,
start_timestamp=min_timestamp,
end_timestamp=max_timestamp,
completion_timestamp=max_timestamp,
start_timestamp=self.min_timestamp,
end_timestamp=self.max_timestamp,
completion_timestamp=self.max_timestamp,
correct=1,
item="test:test",
)

def tearDown(self):
super(BaseDeviceSetupMixin, self).tearDown()
DeviceSettings.objects.all().delete()


class FacilityStatisticsTestCase(BaseDeviceSetupMixin, TransactionTestCase):
def test_extract_facility_statistics(self):
Expand Down Expand Up @@ -273,6 +297,7 @@ def test_extract_facility_statistics(self):
}

assert actual == expected
self.assertNotIn("sh", actual)

def test_regression_4606_no_usersessions(self):
UserSessionLog.objects.all().delete()
Expand Down Expand Up @@ -300,6 +325,23 @@ def test_regression_4606_no_contentsessions_or_usersessions(self):
assert actual["l"] is None


class SoudFacilityStatisticsTestCase(BaseDeviceSetupMixin, TransactionTestCase):
n_facilities = 1
n_superusers = 0
n_users = 2

def test_extract_facility_statistics__soud_hash(self):
provision_single_user_device(self.users[0])
facility = self.facilities[0]
actual = extract_facility_statistics(facility)
users = sorted(self.users, key=lambda u: u.id)
user_ids = ":".join([user.id for user in users])
expected_soud_hash = encodestring(hashlib.md5(user_ids.encode()).digest())[
:10
].decode()
self.assertEqual(expected_soud_hash, actual.pop("sh"))


class ChannelStatisticsTestCase(BaseDeviceSetupMixin, TransactionTestCase):
def test_extract_channel_statistics(self):
actual = extract_channel_statistics(self.channel)
Expand Down
Loading

0 comments on commit 609eaa6

Please sign in to comment.