diff --git a/lms/djangoapps/grades/tasks.py b/lms/djangoapps/grades/tasks.py index bf0b8993f1..3b504e61eb 100644 --- a/lms/djangoapps/grades/tasks.py +++ b/lms/djangoapps/grades/tasks.py @@ -24,6 +24,8 @@ from common.djangoapps.util.date_utils import from_timestamp from lms.djangoapps.course_blocks.api import get_course_blocks from lms.djangoapps.courseware.model_data import get_score from lms.djangoapps.grades.config.models import ComputeGradesSetting +from openedx.core.djangoapps.content.block_structure.api import clear_course_from_cache +from openedx.core.djangoapps.content.block_structure.exceptions import UsageKeyNotInBlockStructure from openedx.core.djangoapps.content.course_overviews.models import \ CourseOverview # lint-amnesty, pylint: disable=unused-import from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order @@ -44,6 +46,7 @@ KNOWN_RETRY_ERRORS = ( # Errors we expect occasionally, should be resolved on r DatabaseError, ValidationError, DatabaseNotReadyError, + UsageKeyNotInBlockStructure, ) RECALCULATE_GRADE_DELAY_SECONDS = 2 # to prevent excessive _has_db_updated failures. See TNL-6424. RETRY_DELAY_SECONDS = 40 @@ -315,7 +318,8 @@ def _update_subsection_grades( student = User.objects.get(id=user_id) store = modulestore() with store.bulk_operations(course_key): - course_structure = get_course_blocks(student, store.make_course_usage_key(course_key)) + course_usage_key = store.make_course_usage_key(course_key) + course_structure = get_course_blocks(student, course_usage_key) subsections_to_update = course_structure.get_transformer_block_field( scored_block_usage_key, GradesTransformer, @@ -323,6 +327,17 @@ def _update_subsection_grades( set(), ) + # Clear the course cache if access is restricted and course blocks are + # cached without the restricted blocks. + if not subsections_to_update: + clear_course_from_cache(course_usage_key.course_key) + raise UsageKeyNotInBlockStructure( + "Scored block usage_key '{0}' is not found in the block_structure with root '{1}'".format( + str(scored_block_usage_key), + str(course_usage_key) + ) + ) + course = store.get_course(course_key, depth=0) subsection_grade_factory = SubsectionGradeFactory(student, course, course_structure) diff --git a/lms/djangoapps/grades/tests/test_tasks.py b/lms/djangoapps/grades/tests/test_tasks.py index 4c4f60d731..4dfd2dbef9 100644 --- a/lms/djangoapps/grades/tests/test_tasks.py +++ b/lms/djangoapps/grades/tests/test_tasks.py @@ -14,14 +14,13 @@ import pytz from django.db.utils import IntegrityError from django.utils import timezone from edx_toggles.toggles.testutils import override_waffle_flag -from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory, check_mongo_calls +from stevedore.extension import Extension, ExtensionManager from common.djangoapps.student.models import CourseEnrollment, anonymous_id_for_user from common.djangoapps.student.tests.factories import UserFactory from common.djangoapps.track.event_transaction_utils import create_new_event_transaction_id, get_event_transaction_id from common.djangoapps.util.date_utils import to_timestamp +from lms.djangoapps.courseware.tests.test_group_access import MemoryUserPartitionScheme from lms.djangoapps.grades import tasks from lms.djangoapps.grades.config.waffle import ENFORCE_FREEZE_GRADE_AFTER_COURSE_END from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum @@ -36,6 +35,10 @@ from lms.djangoapps.grades.tasks import ( recalculate_subsection_grade_v3 ) from openedx.core.djangoapps.content.block_structure.exceptions import BlockStructureNotFound +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory, check_mongo_calls +from xmodule.partitions.partitions import USER_PARTITION_SCHEME_NAMESPACE, Group, UserPartition from .utils import mock_get_score @@ -209,6 +212,48 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest {self.sequential.location, accessible_seq.location}, ) + @patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send') + def test_problem_block_with_restricted_access(self, mock_subsection_signal): + """ + Test that `SUBSECTION_SCORE_CHANGED` is sent for a restricted problem block. + """ + self.set_up_course() + + UserPartition.scheme_extensions = ExtensionManager.make_test_instance( + [ + Extension( + "memory", + USER_PARTITION_SCHEME_NAMESPACE, + MemoryUserPartitionScheme(), + None + ) + ], + namespace=USER_PARTITION_SCHEME_NAMESPACE + ) + verified_group = Group(60, 'verified') + verified_partition = UserPartition( + 0, + 'Verified Partition', + 'Verified Learners', + [verified_group], + scheme=UserPartition.get_scheme("memory"), + ) + + accessible_seq = BlockFactory.create(parent=self.chapter, category='sequential') + restricted_problem = BlockFactory.create( + parent=accessible_seq, + category='problem', + display_name='Restricted Problem', + group_access={verified_partition.id: [verified_group.id]} + ) + + self.recalculate_subsection_grade_kwargs['usage_id'] = str(restricted_problem.location) + verified_partition.scheme.set_group_for_user(self.user, verified_partition, verified_group) + + self._apply_recalculate_subsection_grade() + assert mock_subsection_signal.call_count == 1 + UserPartition.scheme_extensions = None + @ddt.data( (ModuleStoreEnum.Type.split, 2, 41), )