Change completion API to work with any learning context
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
{}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user