diff --git a/openedx/core/djangoapps/credit/signals.py b/openedx/core/djangoapps/credit/signals.py index 9cd9778c6b..10bea0bf71 100644 --- a/openedx/core/djangoapps/credit/signals.py +++ b/openedx/core/djangoapps/credit/signals.py @@ -8,7 +8,7 @@ from xmodule.modulestore.django import SignalHandler @receiver(SignalHandler.course_published) def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument """ - Receives signal and kicks off celery task to update the course requirements + Receives signal and kicks off celery task to update the course requirements. """ # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded diff --git a/openedx/core/djangoapps/credit/tasks.py b/openedx/core/djangoapps/credit/tasks.py index d5e5a5c634..48affc1323 100644 --- a/openedx/core/djangoapps/credit/tasks.py +++ b/openedx/core/djangoapps/credit/tasks.py @@ -13,35 +13,27 @@ from openedx.core.djangoapps.credit.models import CreditCourse from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError - LOGGER = get_task_logger(__name__) # pylint: disable=not-callable @task(default_retry_delay=settings.CREDIT_TASK_DEFAULT_RETRY_DELAY, max_retries=settings.CREDIT_TASK_MAX_RETRIES) def update_course_requirements(course_id): - """ Updates course requirements table for a course. + """Updates course requirements table for a course. Args: course_id(str): A string representation of course identifier Returns: None + """ try: course_key = CourseKey.from_string(course_id) is_credit_course = CreditCourse.is_credit_course(course_key) if is_credit_course: course = modulestore().get_course(course_key) - requirements = [ - { - "namespace": "grade", - "name": "grade", - "criteria": { - "min_grade": get_min_grade_for_credit(course) - } - } - ] + requirements = _get_course_credit_requirements(course) set_credit_requirements(course_key, requirements) except (InvalidKeyError, ItemNotFoundError, InvalidCreditRequirements) as exc: LOGGER.error('Error on adding the requirements for course %s - %s', course_id, unicode(exc)) @@ -50,6 +42,107 @@ def update_course_requirements(course_id): LOGGER.info('Requirements added for course %s', course_id) -def get_min_grade_for_credit(course): - """ Returns the min_grade for the credit requirements """ - return getattr(course, "min_grade", 0.8) +def _get_course_credit_requirements(course): + """Returns the list of credit requirements for the given course. + + It returns the minimum_grade_credit and also the ICRV checkpoints + if any were added in the course + + Args: + course(Course): The course object + + Returns: + List of minimum_grade_credit and ICRV requirements + + """ + icrv_requirements = _get_credit_course_requirement_xblocks(course) + min_grade_requirement = _get_min_grade_requirement(course) + credit_requirements = icrv_requirements + min_grade_requirement + return credit_requirements + + +def _get_min_grade_requirement(course): + """Returns the list of minimum_grade_credit requirements for the given course. + + Args: + course(Course): The course object + + Raises: + AttributeError if the course has not minimum_grade_credit attribute + + Returns: + The list of minimum_grade_credit requirements + + """ + requirement = [] + try: + requirement = [ + { + "namespace": "grade", + "name": "grade", + "criteria": { + "min_grade": getattr(course, "minimum_grade_credit") + } + } + ] + except AttributeError: + LOGGER.error("The course %s does not has minimum_grade_credit attribute", unicode(course.id)) + return requirement + + +def _get_credit_course_requirement_xblocks(course): # pylint: disable=invalid-name + """Generates a course structure dictionary for the specified course. + + Args: + course(Course): The course object + + Returns: + The list of credit requirements xblocks dicts + + """ + blocks_stack = [course] + requirements_blocks = [] + while blocks_stack: + curr_block = blocks_stack.pop() + children = curr_block.get_children() if curr_block.has_children else [] + if _is_credit_requirement(curr_block): + block = { + "namespace": curr_block.get_credit_requirement_namespace(), + "name": curr_block.get_credit_requirement_name(), + "criteria": "" + } + requirements_blocks.append(block) + + # Add this blocks children to the stack so that we can traverse them as well. + blocks_stack.extend(children) + return requirements_blocks + + +def _is_credit_requirement(xblock): + """Check if the given xblock is a credit requirement. + + Args: + xblock(XBlock): The given xblock object + + Returns: + True if xblock is a credit requirement else False + + """ + is_credit_requirement = False + + if callable(getattr(xblock, "is_course_credit_requirement", None)): + is_credit_requirement = xblock.is_course_credit_requirement() + if is_credit_requirement: + if not callable(getattr(xblock, "get_credit_requirement_namespace", None)): + is_credit_requirement = False + LOGGER.error( + "XBlock %v is marked as a credit requirement but does not " + "implement get_credit_requirement_namespace()", xblock + ) + if not callable(getattr(xblock, "get_credit_requirement_name", None)): + is_credit_requirement = False + LOGGER.error( + "XBlock %v is marked as a credit requirement but does not " + "implement get_credit_requirement_name()", xblock + ) + return is_credit_requirement diff --git a/openedx/core/djangoapps/credit/tests/test_tasks.py b/openedx/core/djangoapps/credit/tests/test_tasks.py index f1b14d6f3f..1e9408503f 100644 --- a/openedx/core/djangoapps/credit/tests/test_tasks.py +++ b/openedx/core/djangoapps/credit/tests/test_tasks.py @@ -9,7 +9,7 @@ from openedx.core.djangoapps.credit.models import CreditCourse from openedx.core.djangoapps.credit.signals import listen_for_course_publish from xmodule.modulestore.django import SignalHandler from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory class TestTaskExecution(ModuleStoreTestCase): @@ -26,6 +26,18 @@ class TestTaskExecution(ModuleStoreTestCase): """ raise InvalidCreditRequirements + def add_icrv_xblock(self): + """ Create the 'edx-reverification-block' in course tree """ + + section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') + subsection = ItemFactory.create(parent=section, category='sequential', display_name='Test Subsection') + vertical = ItemFactory.create(parent=subsection, category='vertical', display_name='Test Unit') + reverification = ItemFactory.create( + parent=vertical, + category='edx-reverification-block', + display_name='Test Verification Block' + ) + def setUp(self): super(TestTaskExecution, self).setUp() @@ -57,6 +69,20 @@ class TestTaskExecution(ModuleStoreTestCase): requirements = get_credit_requirements(self.course.id) self.assertEqual(len(requirements), 1) + def test_task_adding_icrv_requirements(self): + """ + Make sure that the receiver correctly fires off the task when + invoked by signal + """ + self.add_credit_course(self.course.id) + self.add_icrv_xblock() + requirements = get_credit_requirements(self.course.id) + self.assertEqual(len(requirements), 0) + listen_for_course_publish(self, self.course.id) + + requirements = get_credit_requirements(self.course.id) + self.assertEqual(len(requirements), 2) + @mock.patch( 'openedx.core.djangoapps.credit.tasks.set_credit_requirements', mock.Mock(