Change completion API to work with any learning context

This commit is contained in:
Braden MacDonald
2019-10-01 16:40:21 -04:00
parent 7dbee1c3dd
commit 5fc73468b3
24 changed files with 214 additions and 89 deletions

View File

@@ -49,7 +49,7 @@ class ScorableCompletionHandlerTestCase(CompletionSetUpMixin, TestCase):
def setUp(self):
super(ScorableCompletionHandlerTestCase, self).setUp()
self.block_key = self.course_key.make_usage_key(block_type='problem', block_id='red')
self.block_key = self.context_key.make_usage_key(block_type='problem', block_id='red')
def call_scorable_block_completion_handler(self, block_key, score_deleted=None):
"""
@@ -64,7 +64,7 @@ class ScorableCompletionHandlerTestCase(CompletionSetUpMixin, TestCase):
handlers.scorable_block_completion(
sender=self,
user_id=self.user.id,
course_id=six.text_type(self.course_key),
course_id=six.text_type(self.context_key),
usage_id=six.text_type(block_key),
weighted_earned=0.0,
weighted_possible=3.0,
@@ -83,39 +83,39 @@ class ScorableCompletionHandlerTestCase(CompletionSetUpMixin, TestCase):
self.call_scorable_block_completion_handler(self.block_key, score_deleted)
completion = BlockCompletion.objects.get(
user=self.user,
course_key=self.course_key,
context_key=self.context_key,
block_key=self.block_key,
)
self.assertEqual(completion.completion, expected_completion)
@XBlock.register_temp_plugin(CustomScorableBlock, 'custom_scorable')
def test_handler_skips_custom_block(self):
custom_block_key = self.course_key.make_usage_key(block_type='custom_scorable', block_id='green')
custom_block_key = self.context_key.make_usage_key(block_type='custom_scorable', block_id='green')
self.call_scorable_block_completion_handler(custom_block_key)
completion = BlockCompletion.objects.filter(
user=self.user,
course_key=self.course_key,
context_key=self.context_key,
block_key=custom_block_key,
)
self.assertFalse(completion.exists())
@XBlock.register_temp_plugin(ExcludedScorableBlock, 'excluded_scorable')
def test_handler_skips_excluded_block(self):
excluded_block_key = self.course_key.make_usage_key(block_type='excluded_scorable', block_id='blue')
excluded_block_key = self.context_key.make_usage_key(block_type='excluded_scorable', block_id='blue')
self.call_scorable_block_completion_handler(excluded_block_key)
completion = BlockCompletion.objects.filter(
user=self.user,
course_key=self.course_key,
context_key=self.context_key,
block_key=excluded_block_key,
)
self.assertFalse(completion.exists())
def test_handler_skips_discussion_block(self):
discussion_block_key = self.course_key.make_usage_key(block_type='discussion', block_id='blue')
discussion_block_key = self.context_key.make_usage_key(block_type='discussion', block_id='blue')
self.call_scorable_block_completion_handler(discussion_block_key)
completion = BlockCompletion.objects.filter(
user=self.user,
course_key=self.course_key,
context_key=self.context_key,
block_key=discussion_block_key,
)
self.assertFalse(completion.exists())
@@ -125,7 +125,7 @@ class ScorableCompletionHandlerTestCase(CompletionSetUpMixin, TestCase):
grades_signals.PROBLEM_WEIGHTED_SCORE_CHANGED.send_robust(
sender=self,
user_id=self.user.id,
course_id=six.text_type(self.course_key),
course_id=six.text_type(self.context_key),
usage_id=six.text_type(self.block_key),
weighted_earned=0.0,
weighted_possible=3.0,
@@ -145,13 +145,13 @@ class DisabledCompletionHandlerTestCase(CompletionSetUpMixin, TestCase):
def setUp(self):
super(DisabledCompletionHandlerTestCase, self).setUp()
self.block_key = self.course_key.make_usage_key(block_type='problem', block_id='red')
self.block_key = self.context_key.make_usage_key(block_type='problem', block_id='red')
def test_disabled_handler_does_not_submit_completion(self):
handlers.scorable_block_completion(
sender=self,
user_id=self.user.id,
course_id=six.text_type(self.course_key),
course_id=six.text_type(self.context_key),
usage_id=six.text_type(self.block_key),
weighted_earned=0.0,
weighted_possible=3.0,
@@ -161,6 +161,6 @@ class DisabledCompletionHandlerTestCase(CompletionSetUpMixin, TestCase):
with self.assertRaises(BlockCompletion.DoesNotExist):
BlockCompletion.objects.get(
user=self.user,
course_key=self.course_key,
context_key=self.context_key,
block_key=self.block_key
)

View File

@@ -44,7 +44,7 @@ class CompletionSetUpMixin(CompletionWaffleTestMixin):
self.block_key = UsageKey.from_string(u'block-v1:edx+test+run+type@video+block@doggos')
self.completion = models.BlockCompletion.objects.create(
user=self.user,
course_key=self.block_key.course_key,
context_key=self.block_key.context_key,
block_type=self.block_key.block_type,
block_key=self.block_key,
completion=0.5,
@@ -67,7 +67,6 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase):
# OTHER = user exists, completion exists
completion, isnew = models.BlockCompletion.objects.submit_completion(
user=self.user,
course_key=self.block_key.course_key,
block_key=self.block_key,
completion=0.9,
)
@@ -80,7 +79,6 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase):
with self.assertNumQueries(SELECT + 2 * SAVEPOINT):
completion, isnew = models.BlockCompletion.objects.submit_completion(
user=self.user,
course_key=self.block_key.course_key,
block_key=self.block_key,
completion=0.5,
)
@@ -94,7 +92,6 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase):
with self.assertNumQueries(SELECT + UPDATE + 4 * SAVEPOINT):
_, isnew = models.BlockCompletion.objects.submit_completion(
user=newuser,
course_key=self.block_key.course_key,
block_key=self.block_key,
completion=0.0,
)
@@ -106,7 +103,6 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase):
with self.assertNumQueries(SELECT + UPDATE + 4 * SAVEPOINT):
_, isnew = models.BlockCompletion.objects.submit_completion(
user=self.user,
course_key=newblock.course_key,
block_key=newblock,
completion=1.0,
)
@@ -117,7 +113,6 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase):
with self.assertRaises(ValidationError):
models.BlockCompletion.objects.submit_completion(
user=self.user,
course_key=self.block_key.course_key,
block_key=self.block_key,
completion=1.2
)
@@ -143,7 +138,6 @@ class CompletionDisabledTestCase(CompletionSetUpMixin, TestCase):
with self.assertRaises(RuntimeError):
models.BlockCompletion.objects.submit_completion(
user=self.user,
course_key=self.block_key.course_key,
block_key=self.block_key,
completion=0.9,
)
@@ -167,7 +161,7 @@ class SubmitBatchCompletionTestCase(CompletionWaffleTestMixin, TestCase):
def test_submit_batch_completion(self):
blocks = [(self.block_key, 1.0)]
models.BlockCompletion.objects.submit_batch_completion(self.user, self.course_key_obj, blocks)
models.BlockCompletion.objects.submit_batch_completion(self.user, blocks)
self.assertEqual(models.BlockCompletion.objects.count(), 1)
self.assertEqual(models.BlockCompletion.objects.last().completion, 1.0)
@@ -175,19 +169,19 @@ class SubmitBatchCompletionTestCase(CompletionWaffleTestMixin, TestCase):
with waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, False):
with self.assertRaises(RuntimeError):
blocks = [(self.block_key, 1.0)]
models.BlockCompletion.objects.submit_batch_completion(self.user, self.course_key_obj, blocks)
models.BlockCompletion.objects.submit_batch_completion(self.user, blocks)
def test_submit_batch_completion_with_same_block_new_completion_value(self):
blocks = [(self.block_key, 0.0)]
self.assertEqual(models.BlockCompletion.objects.count(), 0)
models.BlockCompletion.objects.submit_batch_completion(self.user, self.course_key_obj, blocks)
models.BlockCompletion.objects.submit_batch_completion(self.user, blocks)
self.assertEqual(models.BlockCompletion.objects.count(), 1)
model = models.BlockCompletion.objects.first()
self.assertEqual(model.completion, 0.0)
blocks = [
(UsageKey.from_string('block-v1:edx+test+run+type@video+block@doggos'), 1.0),
]
models.BlockCompletion.objects.submit_batch_completion(self.user, self.course_key_obj, blocks)
models.BlockCompletion.objects.submit_batch_completion(self.user, blocks)
self.assertEqual(models.BlockCompletion.objects.count(), 1)
model = models.BlockCompletion.objects.first()
self.assertEqual(model.completion, 1.0)
@@ -207,20 +201,25 @@ class BatchCompletionMethodTests(CompletionWaffleTestMixin, TestCase):
self.course_key = CourseKey.from_string("edX/MOOC101/2049_T2")
self.other_course_key = CourseKey.from_string("course-v1:ReedX+Hum110+1904")
self.block_keys = [UsageKey.from_string("i4x://edX/MOOC101/video/{}".format(number)) for number in range(5)]
self.block_keys_with_runs = [key.replace(course_key=self.course_key) for key in self.block_keys]
self.other_course_block_keys = [self.other_course_key.make_usage_key('html', '1')]
submit_completions_for_testing(self.user, self.course_key, self.block_keys[:3])
submit_completions_for_testing(self.other_user, self.course_key, self.block_keys[2:])
submit_completions_for_testing(self.user, self.other_course_key, [self.block_keys[4]])
# Submit completions for the main course:
submit_completions_for_testing(self.user, self.block_keys_with_runs[:3])
# Different user:
submit_completions_for_testing(self.other_user, self.block_keys_with_runs[2:])
# Different course:
submit_completions_for_testing(self.user, self.other_course_block_keys)
def test_get_course_completions_missing_runs(self):
actual_completions = models.BlockCompletion.get_course_completions(self.user, self.course_key)
expected_block_keys = [key.replace(course_key=self.course_key) for key in self.block_keys[:3]]
def test_get_learning_context_completions_missing_runs(self):
actual_completions = models.BlockCompletion.get_learning_context_completions(self.user, self.course_key)
expected_block_keys = self.block_keys_with_runs[:3]
expected_completions = dict(list(zip(expected_block_keys, [1.0, 0.8, 0.6])))
self.assertEqual(expected_completions, actual_completions)
def test_get_course_completions_empty_result_set(self):
def test_get_learning_context_completions_empty_result_set(self):
self.assertEqual(
models.BlockCompletion.get_course_completions(self.other_user, self.other_course_key),
models.BlockCompletion.get_learning_context_completions(self.other_user, self.other_course_key),
{}
)

View File

@@ -79,7 +79,6 @@ class CompletionServiceTestCase(CompletionWaffleTestMixin, SharedModuleStoreTest
# Proper completions for the given runtime
BlockCompletion.objects.submit_completion(
user=self.user,
course_key=self.course_key,
block_key=self.html.location,
completion=1.0,
)
@@ -87,7 +86,6 @@ class CompletionServiceTestCase(CompletionWaffleTestMixin, SharedModuleStoreTest
for idx, block_key in enumerate(self.block_keys[0:3]):
BlockCompletion.objects.submit_completion(
user=self.user,
course_key=self.course_key,
block_key=block_key,
completion=1.0 - (0.2 * idx),
)
@@ -96,7 +94,6 @@ class CompletionServiceTestCase(CompletionWaffleTestMixin, SharedModuleStoreTest
for idx, block_key in enumerate(self.block_keys[2:]):
BlockCompletion.objects.submit_completion(
user=self.other_user,
course_key=self.course_key,
block_key=block_key,
completion=0.9 - (0.2 * idx),
)
@@ -104,8 +101,7 @@ class CompletionServiceTestCase(CompletionWaffleTestMixin, SharedModuleStoreTest
# Wrong course
BlockCompletion.objects.submit_completion(
user=self.user,
course_key=self.other_course_key,
block_key=self.block_keys[4],
block_key=self.other_course_key.make_usage_key('problem', 'other'),
completion=0.75,
)
@@ -137,7 +133,6 @@ class CompletionServiceTestCase(CompletionWaffleTestMixin, SharedModuleStoreTest
for block_key in self.block_keys:
BlockCompletion.objects.submit_completion(
user=self.user,
course_key=self.course_key,
block_key=block_key,
completion=1.0
)
@@ -153,7 +148,6 @@ class CompletionServiceTestCase(CompletionWaffleTestMixin, SharedModuleStoreTest
# Mark all the child blocks completed except the last one
BlockCompletion.objects.submit_completion(
user=self.user,
course_key=self.course_key,
block_key=self.block_keys[i],
completion=1.0
)

View File

@@ -9,9 +9,11 @@ from completion import waffle
from completion.test_utils import CompletionWaffleTestMixin
from django.urls import reverse
from rest_framework.test import APIClient
import six
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@@ -25,8 +27,11 @@ class CompletionBatchTestCase(CompletionWaffleTestMixin, ModuleStoreTestCase):
"""
ENROLLED_USERNAME = 'test_user'
UNENROLLED_USERNAME = 'unenrolled_user'
COURSE_KEY = 'TestX/101/Test'
BLOCK_KEY = 'i4x://TestX/101/problem/Test_Problem'
COURSE_KEY = 'course-v1:TestX+101+Test'
BLOCK_KEY = 'block-v1:TestX+101+Test+type@problem+block@Test_Problem'
# And for old mongo:
COURSE_KEY_DEPRECATED = 'TestX/201/Test'
BLOCK_KEY_DEPRECATED = 'i4x://TestX/201/problem/Test_Problem'
def setUp(self):
"""
@@ -39,12 +44,25 @@ class CompletionBatchTestCase(CompletionWaffleTestMixin, ModuleStoreTestCase):
self.override_waffle_switch(True)
# Create course
self.course = CourseFactory.create(org='TestX', number='101', display_name='Test')
self.problem = ItemFactory.create(
parent=self.course,
category="problem",
display_name="Test Problem",
self.course = CourseFactory.create(
org='TestX', number='101', display_name='Test',
default_store=ModuleStoreEnum.Type.split,
)
self.assertEqual(six.text_type(self.course.id), self.COURSE_KEY)
self.problem = ItemFactory.create(
parent=self.course, category="problem", display_name="Test Problem", publish_item=False,
)
self.assertEqual(six.text_type(self.problem.location), self.BLOCK_KEY)
# And an old mongo course:
self.course_deprecated = CourseFactory.create(
org='TestX', number='201', display_name='Test',
default_store=ModuleStoreEnum.Type.mongo,
)
self.assertEqual(six.text_type(self.course_deprecated.id), self.COURSE_KEY_DEPRECATED)
self.problem_deprecated = ItemFactory.create(
parent=self.course_deprecated, category="problem", display_name="Test Problem",
)
self.assertEqual(six.text_type(self.problem_deprecated.location), self.BLOCK_KEY_DEPRECATED)
# Create users
self.staff_user = UserFactory(is_staff=True)
@@ -53,6 +71,7 @@ class CompletionBatchTestCase(CompletionWaffleTestMixin, ModuleStoreTestCase):
# Enrol one user in the course
CourseEnrollmentFactory.create(user=self.enrolled_user, course_id=self.course.id)
CourseEnrollmentFactory.create(user=self.enrolled_user, course_id=self.course_deprecated.id)
# Login the enrolled user by for all tests
self.client = APIClient()
@@ -81,6 +100,16 @@ class CompletionBatchTestCase(CompletionWaffleTestMixin, ModuleStoreTestCase):
}
}, 200, {'detail': 'ok'}
),
# Valid submission (old mongo)
(
{
'username': ENROLLED_USERNAME,
'course_key': COURSE_KEY_DEPRECATED,
'blocks': {
BLOCK_KEY_DEPRECATED: 1.0,
}
}, 200, {'detail': 'ok'}
),
# Blocks list can be empty, though it's a no-op
(
{
@@ -97,7 +126,7 @@ class CompletionBatchTestCase(CompletionWaffleTestMixin, ModuleStoreTestCase):
'blocks': {
BLOCK_KEY: 1.0,
}
}, 400, {"detail": "Invalid course key: not:a:course:key"}
}, 400, {"detail": "Invalid learning context key: not:a:course:key"}
),
# Block must be a valid key
(
@@ -115,13 +144,14 @@ class CompletionBatchTestCase(CompletionWaffleTestMixin, ModuleStoreTestCase):
'username': ENROLLED_USERNAME,
'course_key': COURSE_KEY,
'blocks': {
'i4x://some/other_course/problem/Test_Problem': 1.0,
'block-v1:TestX+101+OtherCourse+type@problem+block@other': 1.0,
}
},
400,
{
"detail": u"Block with key: 'i4x://some/other_course/problem/Test_Problem' is not in course {}".format(
COURSE_KEY,
"detail": (
u"Block with key: 'block-v1:TestX+101+OtherCourse+type@problem+block@other' "
u"is not in context {}".format(COURSE_KEY)
)
}
),
@@ -164,7 +194,7 @@ class CompletionBatchTestCase(CompletionWaffleTestMixin, ModuleStoreTestCase):
(
{
'username': ENROLLED_USERNAME,
'course_key': 'TestX/101/Test2',
'course_key': 'course-v1:TestX+101+Test2',
'blocks': {
BLOCK_KEY: 1.0,
}
@@ -191,6 +221,16 @@ class CompletionBatchTestCase(CompletionWaffleTestMixin, ModuleStoreTestCase):
}
}, 200, {'detail': 'ok'}
),
# Staff can submit completion on behalf of other users (old mongo)
(
{
'username': ENROLLED_USERNAME,
'course_key': COURSE_KEY_DEPRECATED,
'blocks': {
BLOCK_KEY_DEPRECATED: 1.0,
}
}, 200, {'detail': 'ok'}
),
# User must be enrolled in the course
(
{