ECOM-1592: Adding incourse reverification as a credit requirement in course
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user