diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ea60aa9..53c5654 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,11 @@ Change Log Unreleased ~~~~~~~~~~ +[4.5.0] - 2024-3-19 +~~~~~~~~~~~~~~~~~~~~ +* Added ``clear_learning_context_completion`` to enable clearing a learner's + completion for a course + [4.4.1] - 2023-10-27 ~~~~~~~~~~~~~~~~~~~~ * Fix RemovedInDjango41Warning by removing `django_app_config` diff --git a/completion/__init__.py b/completion/__init__.py index 8050a70..142bd91 100644 --- a/completion/__init__.py +++ b/completion/__init__.py @@ -3,4 +3,4 @@ """ -__version__ = '4.4.1' +__version__ = '4.5.0' diff --git a/completion/models.py b/completion/models.py index 946db43..5e7a0d3 100644 --- a/completion/models.py +++ b/completion/models.py @@ -172,6 +172,22 @@ def submit_batch_completion(self, user, blocks): block_completions[block_completion] = is_new return block_completions + @transaction.atomic() + def clear_learning_context_completion(self, user, context_key): + """ + Performs a batch delete of all completion objects for a specified user and context. + + Parameters: + * user (django.contrib.auth.models.User): The user for whom the + completions are being deleted. + * context_key: (ContextKey) The course / context identifier for which + completions are being deleted. + + Return Value: (int) The number of models deleted + """ + total, _ = BlockCompletion.user_learning_context_completion_queryset(user, context_key).delete() + return total + # pylint: disable=model-has-unicode class BlockCompletion(TimeStampedModel, models.Model): diff --git a/completion/tests/test_models.py b/completion/tests/test_models.py index 70533a2..57326c2 100644 --- a/completion/tests/test_models.py +++ b/completion/tests/test_models.py @@ -3,6 +3,8 @@ """ import datetime +from random import randint +from uuid import uuid4 from pytz import UTC from django.core.exceptions import ValidationError @@ -215,3 +217,92 @@ def test_latest_blocks_completed_all_courses(self): self.course_key_one: (datetime.datetime(2050, 1, 3, tzinfo=UTC), self.block_keys_one[2]) } ) + + +class CompletionClearingTestCase(CompletionSetUpMixin, TestCase): + """ + Tests for clear_learning_context_completion + """ + COMPLETION_SWITCH_ENABLED = True + BLOCKS_PER_CONTEXT = 3 + + def setUp(self): + super().setUp() + # Create two learning contexts with some blocks + self.context_key, self.blocks = self._set_up_course('SomeCourse') + self.other_context_key, self.other_blocks = self._set_up_course('SomeOtherCourse') + + # Create two users + self.user = UserFactory() + self.other_user = UserFactory() + + # Create completions for all blocks in both contexts for each learner + self._create_test_completions(self.user) + self._create_test_completions(self.other_user) + + def _create_test_completions(self, user): + # Create random completions for `user` for all blocks in both test contexts + models.BlockCompletion.objects.submit_batch_completion( + user, + [ + (block, float(f"0.{randint(1,9)}")) + for block in self.blocks + self.other_blocks + ] + ) + + def _set_up_course(self, course): + """ Create a context with some blocks """ + blocks = [ + UsageKey.from_string(f'block-v1:edx+{course}+run+type@problem+block@{uuid4()}') + for _ in range(self.BLOCKS_PER_CONTEXT) + ] + return blocks[0].context_key, blocks + + def _assert_completions(self, user, context, expect_completions): + """ Helper to assert the existance of completions for a given learner and context """ + completions = models.BlockCompletion.get_learning_context_completions(user, context) + if expect_completions: + assert len(completions) == self.BLOCKS_PER_CONTEXT + else: + assert not completions + + def test_clear_learning_context_completion(self): + """ + When we clear learning context completion, it should clear all completion records for + the given user and the given context without affecting any other user or context + """ + self._assert_completions(self.user, self.context_key, True) + self._assert_completions(self.user, self.other_context_key, True) + self._assert_completions(self.other_user, self.context_key, True) + self._assert_completions(self.other_user, self.other_context_key, True) + + deleted = models.BlockCompletion.objects.clear_learning_context_completion( + self.user, self.context_key + ) + assert deleted == self.BLOCKS_PER_CONTEXT + + self._assert_completions(self.user, self.context_key, False) + self._assert_completions(self.user, self.other_context_key, True) + self._assert_completions(self.other_user, self.context_key, True) + self._assert_completions(self.other_user, self.other_context_key, True) + + deleted = models.BlockCompletion.objects.clear_learning_context_completion( + self.other_user, self.other_context_key + ) + assert deleted == self.BLOCKS_PER_CONTEXT + + self._assert_completions(self.user, self.context_key, False) + self._assert_completions(self.user, self.other_context_key, True) + self._assert_completions(self.other_user, self.context_key, True) + self._assert_completions(self.other_user, self.other_context_key, False) + + def test_user_no_completions(self): + """ + Calling the method for a user with no completions does nothing and raises no error + """ + stranger = UserFactory() + assert not models.BlockCompletion.objects.filter(user=stranger).exists() + deleted = models.BlockCompletion.objects.clear_learning_context_completion( + stranger, self.context_key + ) + assert deleted == 0