From ceef6b71cac021ffb97ee5f0768ddb893ed7705e Mon Sep 17 00:00:00 2001 From: "J. Cliff Dyer" Date: Mon, 1 Oct 2018 17:18:34 -0400 Subject: [PATCH] Fix freshly-calculated responses: * Ensure new percent is included in live-updated aggregators. * Fix problems with zero possible. * Ensure all recalculated aggregators are used, not just the ones that previously existed. --- completion_aggregator/__init__.py | 2 +- completion_aggregator/serializers.py | 5 +- .../tasks/aggregation_tasks.py | 7 +- test_settings.py | 8 +- test_utils/test_blocks.py | 7 + tests/test_aggregation_tasks.py | 149 ++++++++++++++++++ tests/test_serializers.py | 29 ++-- tests/test_views.py | 122 +++++++++++--- 8 files changed, 285 insertions(+), 44 deletions(-) diff --git a/completion_aggregator/__init__.py b/completion_aggregator/__init__.py index 1e6199fe..6a5d7d85 100644 --- a/completion_aggregator/__init__.py +++ b/completion_aggregator/__init__.py @@ -4,6 +4,6 @@ from __future__ import absolute_import, unicode_literals -__version__ = '1.5.1' +__version__ = '1.5.2' default_app_config = 'completion_aggregator.apps.CompletionAggregatorAppConfig' # pylint: disable=invalid-name diff --git a/completion_aggregator/serializers.py b/completion_aggregator/serializers.py index 0ffb5eed..198007fc 100644 --- a/completion_aggregator/serializers.py +++ b/completion_aggregator/serializers.py @@ -148,14 +148,11 @@ def update_aggregators(self, iterable, is_stale=False): """ if is_stale: log.info("Stale completions found for %s+%s, recalculating.", self.user, self.course_key) - updated_aggregators = calculate_updated_aggregators( + iterable = calculate_updated_aggregators( self.user, self.course_key, force=True, ) - updated_dict = {aggregator.block_key: aggregator for aggregator in updated_aggregators} - iterable = (updated_dict.get(agg.block_key, agg) for agg in iterable) - for aggregator in iterable: self.add_aggregator(aggregator) diff --git a/completion_aggregator/tasks/aggregation_tasks.py b/completion_aggregator/tasks/aggregation_tasks.py index e6578019..e3dc758b 100644 --- a/completion_aggregator/tasks/aggregation_tasks.py +++ b/completion_aggregator/tasks/aggregation_tasks.py @@ -233,7 +233,10 @@ def update_for_aggregator(self, block, affected_aggregators, force): if modified is not None: last_modified = max(last_modified, modified) if self._aggregator_needs_update(block, last_modified, force): - + if total_possible == 0.0: + percent = 1.0 + else: + percent = total_earned / total_possible Aggregator.objects.validate(self.user, self.course_key, block) if block not in self.aggregators: aggregator = Aggregator( @@ -243,6 +246,7 @@ def update_for_aggregator(self, block, affected_aggregators, force): aggregation_name=block.block_type, earned=total_earned, possible=total_possible, + percent=percent, last_modified=last_modified, ) self.aggregators[block] = aggregator @@ -250,6 +254,7 @@ def update_for_aggregator(self, block, affected_aggregators, force): aggregator = self.aggregators[block] aggregator.earned = total_earned aggregator.possible = total_possible + aggregator.percent = percent aggregator.last_modified = last_modified aggregator.modified = timezone.now() self.updated_aggregators.append(aggregator) diff --git a/test_settings.py b/test_settings.py index 679eac4c..838417c9 100644 --- a/test_settings.py +++ b/test_settings.py @@ -20,7 +20,7 @@ def root(*args): AUTH_USER_MODEL = 'auth.User' CELERY_ALWAYS_EAGER = True -COMPLETION_AGGREGATOR_BLOCK_TYPES = {'course', 'chapter'} +COMPLETION_AGGREGATOR_BLOCK_TYPES = {'course', 'chapter', 'sequential'} COMPLETION_AGGREGATOR_ASYNC_AGGREGATION = True DATABASES = { @@ -63,6 +63,12 @@ def root(*args): ROOT_URLCONF = 'completion_aggregator.urls' SECRET_KEY = 'insecure-secret-key' +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + }, +] USE_TZ = True # pylint: disable=unused-import,wrong-import-position diff --git a/test_utils/test_blocks.py b/test_utils/test_blocks.py index 26aba2c4..8de08c6b 100644 --- a/test_utils/test_blocks.py +++ b/test_utils/test_blocks.py @@ -20,3 +20,10 @@ class StubSequential(XBlock): Stub sequential block """ completion_mode = XBlockCompletionMode.AGGREGATOR + + +class StubHTML(XBlock): + """ + Stub HTML block + """ + completion_mode = XBlockCompletionMode.COMPLETABLE diff --git a/tests/test_aggregation_tasks.py b/tests/test_aggregation_tasks.py index d5d62c54..c0091078 100644 --- a/tests/test_aggregation_tasks.py +++ b/tests/test_aggregation_tasks.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals +from collections import namedtuple from datetime import timedelta import ddt @@ -174,6 +175,154 @@ def test_unexpected_updater_errors(self): update_aggregators(username='saskia', course_key='course-v1:OpenCraft+Onboarding+2018') +class CalculateUpdatedAggregatorsTestCase(TestCase): + """ + Test that AggragationUpdater.calculate_updated_aggregators() finds the latest completions. + """ + expected_result = namedtuple('expected_result', ['block_key', 'earned', 'updated_earned', 'possible']) + + def setUp(self): + self.user = get_user_model().objects.create(username='testuser', email='testuser@example.com') + self.course_key = CourseKey.from_string('OpenCraft/Onboarding/2018') + self.blocks = [ + self.course_key.make_usage_key('course', 'course'), + self.course_key.make_usage_key('chapter', 'course-chapter1'), + self.course_key.make_usage_key('chapter', 'course-chapter2'), + self.course_key.make_usage_key('html', 'course-chapter1-block1'), + self.course_key.make_usage_key('html', 'course-chapter1-block2'), + self.course_key.make_usage_key('html', 'course-chapter2-block1'), + self.course_key.make_usage_key('html', 'course-chapter2-block2'), + self.course_key.make_usage_key('chapter', 'course-zeropossible'), + ] + patch = mock.patch('completion_aggregator.tasks.aggregation_tasks.compat', StubCompat(self.blocks)) + patch.start() + self.addCleanup(patch.stop) + + BlockCompletion.objects.create( + user=self.user, + course_key=self.course_key, + block_key=self.blocks[3], + completion=1.0, + modified=now(), + ) + + def _get_updater(self): + """ + Return a fresh instance of an AggregationUpdater. + """ + return AggregationUpdater(self.user, self.course_key, mock.MagicMock()) + + def assert_expected_results(self, updated, expected): + updated_dict = {agg.block_key: agg for agg in updated} + for outcome in expected: + if outcome.earned is None: + with self.assertRaises(Aggregator.DoesNotExist): + Aggregator.objects.get(block_key=outcome.block_key) + else: + agg = Aggregator.objects.get(block_key=outcome.block_key) + assert agg.earned == outcome.earned + assert agg.possible == outcome.possible + assert agg.percent == outcome.earned / outcome.possible + updated_agg = updated_dict[outcome.block_key] + assert updated_agg.earned == outcome.updated_earned + assert updated_agg.possible == outcome.possible + assert updated_agg.percent == outcome.updated_earned / outcome.possible + + @XBlock.register_temp_plugin(CourseBlock, 'course') + @XBlock.register_temp_plugin(OtherAggBlock, 'chapter') + @XBlock.register_temp_plugin(HTMLBlock, 'html') + def test_unmodified_course(self): + self._get_updater().update() + self.assert_expected_results( + self._get_updater().calculate_updated_aggregators(), + [ + self.expected_result( + block_key=self.blocks[0], + earned=1.0, + updated_earned=1.0, + possible=4.0, + ), + self.expected_result( + block_key=self.blocks[1], + earned=1.0, + updated_earned=1.0, + possible=2.0, + ), + self.expected_result( + block_key=self.blocks[2], + earned=0.0, + updated_earned=0.0, + possible=2.0, + ), + ] + ) + + @XBlock.register_temp_plugin(CourseBlock, 'course') + @XBlock.register_temp_plugin(OtherAggBlock, 'chapter') + @XBlock.register_temp_plugin(HTMLBlock, 'html') + def test_modified_course(self): + self._get_updater().update() + for block in self.blocks[4], self.blocks[6]: + BlockCompletion.objects.create( + user=self.user, + course_key=self.course_key, + block_key=block, + completion=1.0, + modified=now(), + ) + self.assert_expected_results( + self._get_updater().calculate_updated_aggregators(), + [ + self.expected_result( + block_key=self.blocks[0], + earned=1.0, + updated_earned=3.0, + possible=4.0, + ), + self.expected_result( + block_key=self.blocks[1], + earned=1.0, + updated_earned=2.0, + possible=2.0, + ), + self.expected_result( + block_key=self.blocks[2], + earned=0.0, + updated_earned=1.0, + possible=2.0, + ), + ] + ) + + @XBlock.register_temp_plugin(CourseBlock, 'course') + @XBlock.register_temp_plugin(OtherAggBlock, 'chapter') + @XBlock.register_temp_plugin(HTMLBlock, 'html') + def test_never_aggregated(self): + self.assert_expected_results( + self._get_updater().calculate_updated_aggregators(), + [ + self.expected_result( + block_key=self.blocks[0], + earned=None, + updated_earned=1.0, + possible=4.0, + ), + self.expected_result( + block_key=self.blocks[1], + earned=None, + updated_earned=1.0, + possible=2.0, + ), + self.expected_result( + block_key=self.blocks[2], + earned=None, + updated_earned=0.0, + possible=2.0, + ), + ] + ) + + class PartialUpdateTest(TestCase): """ Test that when performing an update for a particular block or subset of diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 93e7a894..e07aa0e7 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -83,7 +83,7 @@ def assert_serialized_completions(self, serializer_cls_args, extra_body, recalc_ for this set of submitted completions. """ serializer_cls = course_completion_serializer_factory(serializer_cls_args) - completions = [ + aggregators = [ models.Aggregator.objects.submit_completion( user=self.test_user, course_key=self.course_key, @@ -112,21 +112,27 @@ def assert_serialized_completions(self, serializer_cls_args, extra_body, recalc_ last_modified=timezone.now(), )[0], ] + is_stale = recalc_stale and models.StaleCompletion.objects.filter( + username=self.test_user.username, + course_key=self.course_key, + resolved=False + ) completion = AggregatorAdapter( user=self.test_user, course_key=self.course_key, - aggregators=completions, + aggregators=aggregators, recalculate_stale=recalc_stale, ) serial = serializer_cls(completion) expected = { 'course_key': str(self.course_key), 'completion': { - 'earned': 16.0, - 'possible': 19.0, - 'percent': 16 / 19, + 'earned': 0.0 if is_stale else 16.0, + 'possible': None if is_stale else 19.0, + 'percent': 0.0 if is_stale else 16 / 19, }, } + expected.update(extra_body) # Need to allow for rounding error when retrieving the percent from the test database self.assertEqual(serial.data['course_key'], expected['course_key']) @@ -177,11 +183,9 @@ def assert_serialized_completions(self, serializer_cls_args, extra_body, recalc_ @ddt.unpack @XBlock.register_temp_plugin(StubCourse, 'course') @XBlock.register_temp_plugin(StubSequential, 'sequential') - @patch.object(AggregationUpdater, 'update') - def test_serialize_student_progress_object(self, serializer_cls_args, extra_body, recalc_stale, mock_update): + def test_serialize_aggregators(self, serializer_cls_args, extra_body, recalc_stale): + assert not models.StaleCompletion.objects.filter(resolved=False).exists() self.assert_serialized_completions(serializer_cls_args, extra_body, recalc_stale) - # no stale completions, so aggregations are not updated - assert mock_update.call_count == 0 @ddt.data( (None, True, False), @@ -207,10 +211,9 @@ def test_serialize_student_progress_object(self, serializer_cls_args, extra_body @patch.object(AggregationUpdater, 'calculate_updated_aggregators') def test_aggregation_recalc_stale_completions(self, stale_block_key, stale_force, recalc_stale, mock_calculate): """ - Ensure that requesting aggregation when recalculating stale completions causes the aggregations to be - recalculated once, and the stale completion resolved. - - If not recalculating the stale completion, then it should remain unresolved. + Ensure that requesting aggregation when recalculating stale completions + causes the aggregations to be recalculated once, but does not resolve + stale completions. """ models.StaleCompletion.objects.create( username=self.test_user.username, diff --git a/tests/test_views.py b/tests/test_views.py index c2f62bb1..d70f2a69 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -20,11 +20,12 @@ from django.test import TestCase from django.utils import timezone +from completion.models import BlockCompletion from completion_aggregator import models from completion_aggregator.api.v1.views import CompletionViewMixin from completion_aggregator.tasks.aggregation_tasks import AggregationUpdater from test_utils.compat import StubCompat -from test_utils.test_blocks import StubCourse, StubSequential +from test_utils.test_blocks import StubCourse, StubHTML, StubSequential empty_compat = StubCompat([]) @@ -55,9 +56,6 @@ def _create_oauth2_token(user): @ddt.ddt -@patch('completion_aggregator.api.common.compat', empty_compat) -@patch('completion_aggregator.serializers.compat', empty_compat) -@patch('completion_aggregator.tasks.aggregation_tasks.compat', empty_compat) class CompletionViewTestCase(TestCase): """ Test that the CompletionView renders completion data properly. @@ -76,6 +74,29 @@ def setUp(self): user=self.test_user, course_id=self.course_key, ) + self.blocks = [ + self.course_key.make_usage_key('course', 'course'), + self.course_key.make_usage_key('sequential', 'course-sequence1'), + self.course_key.make_usage_key('sequential', 'course-sequence2'), + self.course_key.make_usage_key('html', 'course-sequence1-html1'), + self.course_key.make_usage_key('html', 'course-sequence1-html2'), + self.course_key.make_usage_key('html', 'course-sequence1-html3'), + self.course_key.make_usage_key('html', 'course-sequence1-html4'), + self.course_key.make_usage_key('html', 'course-sequence1-html5'), + self.course_key.make_usage_key('html', 'course-sequence2-html6'), + self.course_key.make_usage_key('html', 'course-sequence2-html7'), + self.course_key.make_usage_key('html', 'course-sequence2-html8'), + ] + compat = StubCompat(self.blocks) + for compat_import in ( + 'completion_aggregator.api.common.compat', + 'completion_aggregator.serializers.compat', + 'completion_aggregator.tasks.aggregation_tasks.compat', + ): + patcher = patch(compat_import, compat) + patcher.start() + self.addCleanup(patcher.__exit__, None, None, None) + self.patch_object( CompletionViewMixin, 'authentication_classes', @@ -98,6 +119,7 @@ def patch_object(self, obj, method, **kwargs): """ patcher = patch.object(obj, method, **kwargs) patcher.start() + self.addCleanup(patcher.__exit__, None, None, None) return patcher @@ -105,10 +127,18 @@ def mark_completions(self): """ Create completion data to test against. """ + BlockCompletion.objects.create( + user=self.test_user, + course_key=self.course_key, + block_key=self.blocks[3], + block_type='html', + completion=1.0, + ) + models.StaleCompletion.objects.update(resolved=True) models.Aggregator.objects.submit_completion( user=self.test_user, course_key=self.course_key, - block_key=self.course_key.make_usage_key(block_type='sequential', block_id='vertical_sequential'), + block_key=self.course_key.make_usage_key(block_type='sequential', block_id='course-sequence1'), aggregation_name='sequential', earned=1.0, possible=5.0, @@ -121,7 +151,7 @@ def mark_completions(self): block_key=self.course_key.make_usage_key(block_type='course', block_id='course'), aggregation_name='course', earned=1.0, - possible=12.0, + possible=8.0, last_modified=timezone.now(), ) @@ -134,7 +164,7 @@ def create_enrollment(self, user, course_id): course_id=course_id, ) - def _get_expected_completion(self, version, earned=1.0, possible=12.0, percent=0.0833333333333333): + def _get_expected_completion(self, version, earned=1.0, possible=8.0, percent=0.125): """ Return completion section based on version. """ @@ -167,7 +197,7 @@ def assert_expected_list_view(self, version): """ Ensures that the expected data is returned from the versioned list view. """ - response = self.client.get(self.list_url.format(version)) + response = self.client.get(self.list_url.format(version), params={'username': self.test_user.username}) self.assertEqual(response.status_code, 200) expected = { 'count': 1, @@ -184,6 +214,8 @@ def assert_expected_list_view(self, version): @ddt.data(0, 1) @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') @patch.object(AggregationUpdater, 'update') def test_list_view(self, version, mock_update): self.assert_expected_list_view(version) @@ -192,24 +224,30 @@ def test_list_view(self, version, mock_update): @ddt.data(0, 1) @XBlock.register_temp_plugin(StubCourse, 'course') - @patch.object(AggregationUpdater, 'calculate_updated_aggregators') - def test_list_view_stale_completion(self, version, mock_calculate): + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') + def test_list_view_stale_completion(self, version): """ - Ensure that a stale completion causes the aggregations to be recalculated, but not updated in the db, - and stale completion is not resolved. + Ensure that a stale completion causes the aggregations to be + recalculated, but not updated in the db, and stale completion is not + resolved. """ models.StaleCompletion.objects.create( username=self.test_user.username, course_key=self.course_key, block_key=None, - force=False, + force=True, + resolved=False, ) assert models.StaleCompletion.objects.filter(resolved=False).count() == 1 self.assert_expected_list_view(version) - assert mock_calculate.call_count == 1 + # assert mock_calculate.call_count == 1 assert models.StaleCompletion.objects.filter(resolved=False).count() == 1 @ddt.data(0, 1) + @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_list_view_enrolled_no_progress(self, version): """ Test that the completion API returns a record for each course the user is enrolled in, @@ -230,9 +268,9 @@ def test_list_view_enrolled_no_progress(self, version): 'course_key': 'edX/toy/2012_Fall', 'completion': self._get_expected_completion( version, - earned=0.0, - possible=None, - percent=0.0, + earned=1.0, + possible=8.0, + percent=0.125, ), }, { @@ -251,6 +289,7 @@ def test_list_view_enrolled_no_progress(self, version): @ddt.data(0, 1) @XBlock.register_temp_plugin(StubCourse, 'course') @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_list_view_with_sequentials(self, version): response = self.client.get(self.get_list_url(version, requested_fields='sequential')) self.assertEqual(response.status_code, 200) @@ -265,7 +304,7 @@ def test_list_view_with_sequentials(self, version): 'sequential': [ { 'course_key': u'edX/toy/2012_Fall', - 'block_key': u'i4x://edX/toy/sequential/vertical_sequential', + 'block_key': u'i4x://edX/toy/sequential/course-sequence1', 'completion': self._get_expected_completion( version, earned=1.0, @@ -294,6 +333,8 @@ def assert_expected_detail_view(self, version): @ddt.data(0, 1) @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') @patch.object(AggregationUpdater, 'update') def test_detail_view(self, version, mock_update): self.assert_expected_detail_view(version) @@ -302,8 +343,9 @@ def test_detail_view(self, version, mock_update): @ddt.data(0, 1) @XBlock.register_temp_plugin(StubCourse, 'course') - @patch.object(AggregationUpdater, 'calculate_updated_aggregators') - def test_detail_view_stale_completion(self, version, mock_calculate): + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') + def test_detail_view_stale_completion(self, version): """ Ensure that a stale completion causes the aggregations to be recalculated once. @@ -317,11 +359,12 @@ def test_detail_view_stale_completion(self, version, mock_calculate): ) assert models.StaleCompletion.objects.filter(resolved=False).count() == 1 self.assert_expected_detail_view(version) - assert mock_calculate.call_count == 1 assert models.StaleCompletion.objects.filter(resolved=False).count() == 1 @ddt.data(0, 1) @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_detail_view_oauth2(self, version): """ Test the detail view using OAuth2 Authentication @@ -343,6 +386,9 @@ def test_detail_view_oauth2(self, version): self.assertEqual(response.data['results'][0]['completion']['earned'], 1.0) @ddt.data(0, 1) + @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_detail_view_not_enrolled(self, version): """ Test that requesting course completions for a course the user is not enrolled in @@ -358,6 +404,9 @@ def test_detail_view_not_enrolled(self, version): self.assertEqual(response.status_code, 404) @ddt.data(0, 1) + @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_detail_view_inactive_enrollment(self, version): self.test_enrollment.is_active = False self.test_enrollment.save() @@ -366,6 +415,8 @@ def test_detail_view_inactive_enrollment(self, version): @ddt.data(0, 1) @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_detail_view_no_completion(self, version): """ Test that requesting course completions for a course which has started, but the user has not yet started, @@ -387,6 +438,7 @@ def test_detail_view_no_completion(self, version): @ddt.data(0, 1) @XBlock.register_temp_plugin(StubCourse, 'course') @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_detail_view_with_sequentials(self, version): response = self.client.get(self.get_detail_url(version, self.course_key, requested_fields='sequential')) self.assertEqual(response.status_code, 200) @@ -396,7 +448,7 @@ def test_detail_view_with_sequentials(self, version): 'sequential': [ { 'course_key': u'edX/toy/2012_Fall', - 'block_key': u'i4x://edX/toy/sequential/vertical_sequential', + 'block_key': u'i4x://edX/toy/sequential/course-sequence1', 'completion': self._get_expected_completion(version, earned=1.0, possible=5.0, percent=0.2), }, ] @@ -406,6 +458,8 @@ def test_detail_view_with_sequentials(self, version): @ddt.data(0, 1) @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_detail_view_staff_requested_user(self, version): """ Test that requesting course completions for a specific user filters out the other enrolled users @@ -421,6 +475,8 @@ def test_detail_view_staff_requested_user(self, version): self.assertEqual(response.data, expected) @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') @patch.object(AggregationUpdater, 'update') def test_detail_view_staff_all_users(self, mock_update): """ @@ -436,7 +492,7 @@ def test_detail_view_staff_all_users(self, mock_update): models.Aggregator.objects.submit_completion( user=another_user, course_key=self.course_key, - block_key=self.course_key.make_usage_key(block_type='sequential', block_id='vertical_sequential'), + block_key=self.course_key.make_usage_key(block_type='sequential', block_id='course-sequence1'), aggregation_name='sequential', earned=3.0, possible=5.0, @@ -482,6 +538,9 @@ def test_detail_view_staff_all_users(self, mock_update): assert models.StaleCompletion.objects.filter(resolved=False).count() == 2 @ddt.data(0, 1) + @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_invalid_optional_fields(self, version): response = self.client.get( self.detail_url_fmt.format(version, 'edX/toy/2012_Fall') + '?requested_fields=INVALID' @@ -489,6 +548,9 @@ def test_invalid_optional_fields(self, version): self.assertEqual(response.status_code, 400) @ddt.data(0, 1) + @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_unauthenticated(self, version): self.client.force_authenticate(None) detailresponse = self.client.get(self.get_detail_url(version, self.course_key)) @@ -498,11 +560,16 @@ def test_unauthenticated(self, version): @ddt.data(0, 1) @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_request_self(self, version): response = self.client.get(self.get_list_url(version, username=self.test_user.username)) self.assertEqual(response.status_code, 200) @ddt.data(0, 1) + @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_wrong_user(self, version): user = User.objects.create(username='wrong') self.client.force_authenticate(user) @@ -510,6 +577,9 @@ def test_wrong_user(self, version): self.assertEqual(response.status_code, 404) @ddt.data(0, 1) + @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_no_user(self, version): self.client.logout() response = self.client.get(self.get_list_url(version)) @@ -517,6 +587,8 @@ def test_no_user(self, version): @ddt.data(0, 1) @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_staff_access(self, version): self.client.force_authenticate(self.staff_user) response = self.client.get(self.get_list_url(version, username=self.test_user.username)) @@ -525,12 +597,14 @@ def test_staff_access(self, version): self.assertEqual(response.data['results'][0]['completion'], expected_completion) @ddt.data(0, 1) + @XBlock.register_temp_plugin(StubCourse, 'course') + @XBlock.register_temp_plugin(StubSequential, 'sequential') + @XBlock.register_temp_plugin(StubHTML, 'html') def test_staff_access_non_user(self, version): self.client.force_authenticate(self.staff_user) response = self.client.get(self.get_list_url(version, username='who-dat')) self.assertEqual(response.status_code, 404) - @XBlock.register_temp_plugin(StubCourse, 'course') def get_detail_url(self, version, course_key, **params): """ Given a course_key and a number of key-value pairs as keyword arguments,