Display warning message on course outline in Studio when course contains deprecated features/components

TNL-2303
This commit is contained in:
muhammad-ammar
2015-06-10 16:37:02 +05:00
parent 1a9a69829a
commit b0eda4cac9
18 changed files with 693 additions and 63 deletions

View File

@@ -0,0 +1,4 @@
"""
Setup the signals on startup.
"""
import openedx.core.djangoapps.content.course_structures.signals

View File

@@ -0,0 +1,127 @@
"""
API implementation of the Course Structure API for Python code.
Note: The course list and course detail functionality isn't currently supported here because
of the tricky interactions between DRF and the code.
Most of that information is available by accessing the course objects directly.
"""
from collections import OrderedDict
from .serializers import GradingPolicySerializer, CourseStructureSerializer
from .errors import CourseNotFoundError, CourseStructureNotAvailableError
from openedx.core.djangoapps.content.course_structures import models, tasks
from util.cache import cache
from xmodule.modulestore.django import modulestore
def _retrieve_course(course_key):
"""Retrieves the course for the given course key.
Args:
course_key: The CourseKey for the course we'd like to retrieve.
Returns:
the course that matches the CourseKey
Raises:
CourseNotFoundError
"""
course = modulestore().get_course(course_key, depth=0)
if course is None:
raise CourseNotFoundError
return course
def course_structure(course_key, block_types=None):
"""
Retrieves the entire course structure, including information about all the blocks used in the
course if `block_types` is None else information about `block_types` will be returned only.
Final serialized information will be cached.
Args:
course_key: the CourseKey of the course we'd like to retrieve.
block_types: list of required block types. Possible values include sequential,
vertical, html, problem, video, and discussion. The type can also be
the name of a custom type of block used for the course.
Returns:
The serialized output of the course structure:
* root: The ID of the root node of the course structure.
* blocks: A dictionary that maps block IDs to a collection of
information about each block. Each block contains the following
fields.
* id: The ID of the block.
* type: The type of block. Possible values include sequential,
vertical, html, problem, video, and discussion. The type can also be
the name of a custom type of block used for the course.
* display_name: The display name configured for the block.
* graded: Whether or not the sequential or problem is graded. The
value is true or false.
* format: The assignment type.
* children: If the block has child blocks, a list of IDs of the child
blocks.
Raises:
CourseStructureNotAvailableError, CourseNotFoundError
"""
course = _retrieve_course(course_key)
modified_timestamp = models.CourseStructure.objects.filter(course_id=course_key).values('modified')
if modified_timestamp.exists():
cache_key = 'openedx.content.course_structures.api.v0.api.course_structure.{}.{}.{}'.format(
course_key, modified_timestamp[0]['modified'], '_'.join(block_types or [])
)
data = cache.get(cache_key) # pylint: disable=maybe-no-member
if data is not None:
return data
try:
requested_course_structure = models.CourseStructure.objects.get(course_id=course.id)
except models.CourseStructure.DoesNotExist:
pass
else:
structure = requested_course_structure.structure
if block_types is not None:
blocks = requested_course_structure.ordered_blocks
required_blocks = OrderedDict()
for usage_id, block_data in blocks.iteritems():
if block_data['block_type'] in block_types:
required_blocks[usage_id] = block_data
structure['blocks'] = required_blocks
data = CourseStructureSerializer(structure).data
cache.set(cache_key, data, None) # pylint: disable=maybe-no-member
return data
# If we don't have data stored, generate it and return an error.
tasks.update_course_structure.delay(unicode(course_key))
raise CourseStructureNotAvailableError
def course_grading_policy(course_key):
"""
Retrieves the course grading policy.
Args:
course_key: CourseKey the corresponds to the course we'd like to know grading policy information about.
Returns:
The serialized version of the course grading policy containing the following information:
* assignment_type: The type of the assignment, as configured by course
staff. For example, course staff might make the assignment types Homework,
Quiz, and Exam.
* count: The number of assignments of the type.
* dropped: Number of assignments of the type that are dropped.
* weight: The weight, or effect, of the assignment type on the learner's
final grade.
"""
course = _retrieve_course(course_key)
return GradingPolicySerializer(course.raw_grader).data

View File

@@ -0,0 +1,11 @@
""" Errors used by the Course Structure API. """
class CourseNotFoundError(Exception):
""" The course was not found. """
pass
class CourseStructureNotAvailableError(Exception):
""" The course structure still needs to be generated. """
pass

View File

@@ -0,0 +1,39 @@
"""
API Serializers
"""
from rest_framework import serializers
class GradingPolicySerializer(serializers.Serializer):
""" Serializer for course grading policy. """
assignment_type = serializers.CharField(source='type')
count = serializers.IntegerField(source='min_count')
dropped = serializers.IntegerField(source='drop_count')
weight = serializers.FloatField()
# pylint: disable=invalid-name
class BlockSerializer(serializers.Serializer):
""" Serializer for course structure block. """
id = serializers.CharField(source='usage_key')
type = serializers.CharField(source='block_type')
parent = serializers.CharField(source='parent')
display_name = serializers.CharField()
graded = serializers.BooleanField(default=False)
format = serializers.CharField()
children = serializers.CharField()
class CourseStructureSerializer(serializers.Serializer):
""" Serializer for course structure. """
root = serializers.CharField(source='root')
blocks = serializers.SerializerMethodField('get_blocks')
def get_blocks(self, structure):
""" Serialize the individual blocks. """
serialized = {}
for key, block in structure['blocks'].iteritems():
serialized[key] = BlockSerializer(block).data
return serialized

View File

@@ -0,0 +1,149 @@
"""
Course Structure api.py tests
"""
from .api import course_structure
from openedx.core.djangoapps.content.course_structures.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, ItemFactory
import mock
from django.core import cache
class CourseStructureApiTests(ModuleStoreTestCase):
"""
CourseStructure API Tests
"""
MOCK_CACHE = "openedx.core.djangoapps.content.course_structures.api.v0.api.cache"
def setUp(self):
"""
Test setup
"""
# For some reason, `listen_for_course_publish` is not called when we run
# all (paver test_system -s cms) tests, If we run only run this file then tests run fine.
SignalHandler.course_published.connect(listen_for_course_publish)
super(CourseStructureApiTests, self).setUp()
self.course = CourseFactory.create()
self.chapter = ItemFactory.create(
parent_location=self.course.location, category='chapter', display_name="Week 1"
)
self.sequential = ItemFactory.create(
parent_location=self.chapter.location, category='sequential', display_name="Lesson 1"
)
self.vertical = ItemFactory.create(
parent_location=self.sequential.location, category='vertical', display_name='Subsection 1'
)
self.video = ItemFactory.create(
parent_location=self.vertical.location, category="video", display_name="My Video"
)
self.video = ItemFactory.create(
parent_location=self.vertical.location, category="html", display_name="My HTML"
)
self.addCleanup(self._disconnect_course_published_event)
def _disconnect_course_published_event(self):
"""
Disconnect course_published event.
"""
# If we don't disconnect then tests are getting failed in test_crud.py
SignalHandler.course_published.disconnect(listen_for_course_publish)
def _expected_blocks(self, block_types=None, get_parent=False):
"""
Construct expected blocks.
Arguments:
block_types (list): List of required block types. Possible values include sequential,
vertical, html, problem, video, and discussion. The type can also be
the name of a custom type of block used for the course.
get_parent (bool): If True then add child's parent location else parent is set to None
Returns:
dict: Information about required block types.
"""
blocks = {}
def add_block(xblock):
"""
Returns expected blocks dict.
"""
children = xblock.get_children()
if block_types is None or xblock.category in block_types:
parent = None
if get_parent:
item = xblock.get_parent()
parent = unicode(item.location) if item is not None else None
blocks[unicode(xblock.location)] = {
u'id': unicode(xblock.location),
u'type': xblock.category,
u'display_name': xblock.display_name,
u'format': xblock.format,
u'graded': xblock.graded,
u'parent': parent,
u'children': [unicode(child.location) for child in children]
}
for child in children:
add_block(child)
course = self.store.get_course(self.course.id, depth=None)
add_block(course)
return blocks
def test_course_structure_with_no_block_types(self):
"""
Verify that course_structure returns info for entire course.
"""
with mock.patch(self.MOCK_CACHE, cache.get_cache(backend='default')):
with self.assertNumQueries(3):
structure = course_structure(self.course.id)
expected = {
u'root': unicode(self.course.location),
u'blocks': self._expected_blocks()
}
self.assertDictEqual(structure, expected)
with mock.patch(self.MOCK_CACHE, cache.get_cache(backend='default')):
with self.assertNumQueries(2):
course_structure(self.course.id)
def test_course_structure_with_block_types(self):
"""
Verify that course_structure returns info for required block_types only when specific block_types are requested.
"""
block_types = ['html', 'video']
with mock.patch(self.MOCK_CACHE, cache.get_cache(backend='default')):
with self.assertNumQueries(3):
structure = course_structure(self.course.id, block_types=block_types)
expected = {
u'root': unicode(self.course.location),
u'blocks': self._expected_blocks(block_types=block_types, get_parent=True)
}
self.assertDictEqual(structure, expected)
with mock.patch(self.MOCK_CACHE, cache.get_cache(backend='default')):
with self.assertNumQueries(2):
course_structure(self.course.id, block_types=block_types)
def test_course_structure_with_non_existed_block_types(self):
"""
Verify that course_structure returns empty info for non-existed block_types.
"""
block_types = ['phantom']
structure = course_structure(self.course.id, block_types=block_types)
expected = {
u'root': unicode(self.course.location),
u'blocks': {}
}
self.assertDictEqual(structure, expected)

View File

@@ -52,7 +52,3 @@ class CourseStructure(TimeStampedModel):
for child_node in cur_block['children']:
self._traverse_tree(child_node, unordered_structure, ordered_blocks, parent=block)
# Signals must be imported in a file that is automatically loaded at app startup (e.g. models.py). We import them
# at the end of this file to avoid circular dependencies.
import signals # pylint: disable=unused-import