Merge pull request #9109 from jazkarta/ccx_course_published_signal
Changes to a CCX course should emit a course_published event
This commit is contained in:
45
lms/djangoapps/ccx/tasks.py
Normal file
45
lms/djangoapps/ccx/tasks.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Asynchronous tasks for the CCX app.
|
||||
"""
|
||||
|
||||
from django.dispatch import receiver
|
||||
import logging
|
||||
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from ccx_keys.locator import CCXLocator
|
||||
from xmodule.modulestore.django import SignalHandler
|
||||
from lms import CELERY_APP
|
||||
|
||||
from .models import CustomCourseForEdX
|
||||
|
||||
log = logging.getLogger("edx.ccx")
|
||||
|
||||
|
||||
@receiver(SignalHandler.course_published)
|
||||
def course_published_handler(sender, course_key, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Consume signals that indicate course published. If course already a CCX, do nothing.
|
||||
"""
|
||||
if not isinstance(course_key, CCXLocator):
|
||||
send_ccx_course_published.delay(unicode(course_key))
|
||||
|
||||
|
||||
@CELERY_APP.task
|
||||
def send_ccx_course_published(course_key):
|
||||
"""
|
||||
Find all CCX derived from this course, and send course published event for them.
|
||||
"""
|
||||
course_key = CourseLocator.from_string(course_key)
|
||||
for ccx in CustomCourseForEdX.objects.filter(course_id=course_key):
|
||||
try:
|
||||
ccx_key = CCXLocator.from_course_locator(course_key, ccx.id)
|
||||
except InvalidKeyError:
|
||||
log.info('Attempt to publish course with deprecated id. Course: %s. CCX: %s', course_key, ccx.id)
|
||||
continue
|
||||
responses = SignalHandler.course_published.send(
|
||||
sender=ccx,
|
||||
course_key=ccx_key
|
||||
)
|
||||
for rec, response in responses:
|
||||
log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response)
|
||||
110
lms/djangoapps/ccx/tests/test_tasks.py
Normal file
110
lms/djangoapps/ccx/tests/test_tasks.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Tests for celery tasks defined in tasks module
|
||||
"""
|
||||
|
||||
from mock_django import mock_signal_receiver
|
||||
|
||||
from ccx.tests.factories import ( # pylint: disable=import-error
|
||||
CcxFactory,
|
||||
)
|
||||
from student.roles import CourseCcxCoachRole # pylint: disable=import-error
|
||||
from student.tests.factories import ( # pylint: disable=import-error
|
||||
AdminFactory,
|
||||
)
|
||||
from xmodule.modulestore.django import SignalHandler
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase,
|
||||
TEST_DATA_SPLIT_MODULESTORE)
|
||||
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
|
||||
from ccx_keys.locator import CCXLocator
|
||||
|
||||
from ..tasks import send_ccx_course_published
|
||||
|
||||
|
||||
class TestSendCCXCoursePublished(ModuleStoreTestCase):
|
||||
"""unit tests for the send ccx course published task
|
||||
"""
|
||||
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up tests
|
||||
"""
|
||||
super(TestSendCCXCoursePublished, self).setUp()
|
||||
course = self.course = CourseFactory.create(org="edX", course="999", display_name="Run 666")
|
||||
course2 = self.course2 = CourseFactory.create(org="edX", course="999a", display_name="Run 667")
|
||||
coach = AdminFactory.create()
|
||||
role = CourseCcxCoachRole(course.id)
|
||||
role.add_users(coach)
|
||||
self.ccx = CcxFactory(course_id=course.id, coach=coach)
|
||||
self.ccx2 = CcxFactory(course_id=course.id, coach=coach)
|
||||
self.ccx3 = CcxFactory(course_id=course.id, coach=coach)
|
||||
self.ccx4 = CcxFactory(course_id=course2.id, coach=coach)
|
||||
|
||||
def call_fut(self, course_key):
|
||||
"""Call the function under test
|
||||
"""
|
||||
send_ccx_course_published(unicode(course_key))
|
||||
|
||||
def test_signal_not_sent_for_ccx(self):
|
||||
"""Check that course published signal is not sent when course key is for a ccx
|
||||
"""
|
||||
course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)
|
||||
with mock_signal_receiver(SignalHandler.course_published) as receiver:
|
||||
self.call_fut(course_key)
|
||||
self.assertEqual(receiver.call_count, 0)
|
||||
|
||||
def test_signal_sent_for_ccx(self):
|
||||
"""Check that course published signal is sent when course key is not for a ccx.
|
||||
We have 4 ccx's, but only 3 are derived from the course id used here, so call
|
||||
count must be 3 to confirm that all derived courses and no more got the signal.
|
||||
"""
|
||||
with mock_signal_receiver(SignalHandler.course_published) as receiver:
|
||||
self.call_fut(self.course.id)
|
||||
self.assertEqual(receiver.call_count, 3)
|
||||
|
||||
def test_course_structure_generated(self):
|
||||
"""Check that course structure is generated after course published signal is sent
|
||||
"""
|
||||
ccx_structure = {
|
||||
u"blocks": {
|
||||
u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course": {
|
||||
u"block_type": u"course",
|
||||
u"graded": False,
|
||||
u"format": None,
|
||||
u"usage_key": u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course",
|
||||
u"children": [
|
||||
],
|
||||
u"display_name": u"Run 666"
|
||||
}
|
||||
},
|
||||
u"root": u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course"
|
||||
}
|
||||
course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)
|
||||
structure = CourseStructure.objects.filter(course_id=course_key)
|
||||
# no structure exists before signal is called
|
||||
self.assertEqual(len(structure), 0)
|
||||
with mock_signal_receiver(SignalHandler.course_published) as receiver:
|
||||
self.call_fut(self.course.id)
|
||||
self.assertEqual(receiver.call_count, 3)
|
||||
structure = CourseStructure.objects.get(course_id=course_key)
|
||||
self.assertEqual(structure.structure, ccx_structure)
|
||||
|
||||
def test_course_overview_deleted(self):
|
||||
"""Check that course overview is deleted after course published signal is sent
|
||||
"""
|
||||
course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)
|
||||
overview = CourseOverview(id=course_key)
|
||||
overview.version = 1
|
||||
overview.save()
|
||||
overview = CourseOverview.objects.filter(id=course_key)
|
||||
self.assertEqual(len(overview), 1)
|
||||
with mock_signal_receiver(SignalHandler.course_published) as receiver:
|
||||
self.call_fut(self.course.id)
|
||||
self.assertEqual(receiver.call_count, 3)
|
||||
overview = CourseOverview.objects.filter(id=course_key)
|
||||
self.assertEqual(len(overview), 0)
|
||||
Reference in New Issue
Block a user