diff --git a/openedx/core/djangoapps/content/course_structures/admin.py b/openedx/core/djangoapps/content/course_structures/admin.py deleted file mode 100644 index d2b7c41e31..0000000000 --- a/openedx/core/djangoapps/content/course_structures/admin.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Django Admin model registry for Course Structures sub-application -""" -from django.contrib import admin - -from .models import CourseStructure - - -class CourseStructureAdmin(admin.ModelAdmin): - """ - Django Admin class for managing Course Structures model data - """ - search_fields = ('course_id',) - list_display = ('course_id', 'modified') - ordering = ('course_id', '-modified') - - -admin.site.register(CourseStructure, CourseStructureAdmin) diff --git a/openedx/core/djangoapps/content/course_structures/api/__init__.py b/openedx/core/djangoapps/content/course_structures/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openedx/core/djangoapps/content/course_structures/api/v0/__init__.py b/openedx/core/djangoapps/content/course_structures/api/v0/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openedx/core/djangoapps/content/course_structures/api/v0/api.py b/openedx/core/djangoapps/content/course_structures/api/v0/api.py deleted file mode 100644 index 1fa2e0b45f..0000000000 --- a/openedx/core/djangoapps/content/course_structures/api/v0/api.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -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 openedx.core.lib.exceptions import CourseNotFoundError -from .serializers import GradingPolicySerializer, CourseStructureSerializer -from .errors import 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, many=True).data diff --git a/openedx/core/djangoapps/content/course_structures/api/v0/errors.py b/openedx/core/djangoapps/content/course_structures/api/v0/errors.py deleted file mode 100644 index 378a8ca975..0000000000 --- a/openedx/core/djangoapps/content/course_structures/api/v0/errors.py +++ /dev/null @@ -1,6 +0,0 @@ -""" Errors used by the Course Structure API. """ - - -class CourseStructureNotAvailableError(Exception): - """ The course structure still needs to be generated. """ - pass diff --git a/openedx/core/djangoapps/content/course_structures/api/v0/serializers.py b/openedx/core/djangoapps/content/course_structures/api/v0/serializers.py deleted file mode 100644 index 881be80437..0000000000 --- a/openedx/core/djangoapps/content/course_structures/api/v0/serializers.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -API Serializers -""" -from collections import defaultdict - -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() - - def to_representation(self, obj): - """ - Return a representation of the grading policy. - """ - # Backwards compatibility with the behavior of DRF v2. - # When the grader dictionary was missing keys, DRF v2 would default to None; - # DRF v3 unhelpfully raises an exception. - return dict( - super(GradingPolicySerializer, self).to_representation( - defaultdict(lambda: None, obj) - ) - ) - - -# 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(required=False) - display_name = serializers.CharField() - graded = serializers.BooleanField(default=False) - format = serializers.CharField() - children = serializers.CharField() - - def to_representation(self, obj): - """ - Return a representation of the block. - - NOTE: this method maintains backwards compatibility with the behavior - of Django Rest Framework v2. - """ - data = super(BlockSerializer, self).to_representation(obj) - - # Backwards compatibility with the behavior of DRF v2 - # Include a NULL value for "parent" in the representation - # (instead of excluding the key entirely) - if obj.get("parent") is None: - data["parent"] = None - - # Backwards compatibility with the behavior of DRF v2 - # Leave the children list as a list instead of serializing - # it to a string. - data["children"] = obj["children"] - - return data - - -class CourseStructureSerializer(serializers.Serializer): - """ Serializer for course structure. """ - root = serializers.CharField() - blocks = serializers.SerializerMethodField() - - def get_blocks(self, structure): - """ Serialize the individual blocks. """ - serialized = {} - - for key, block in structure['blocks'].iteritems(): - serialized[key] = BlockSerializer(block).data - - return serialized diff --git a/openedx/core/djangoapps/content/course_structures/api/v0/tests_api.py b/openedx/core/djangoapps/content/course_structures/api/v0/tests_api.py deleted file mode 100644 index d61d7b87e1..0000000000 --- a/openedx/core/djangoapps/content/course_structures/api/v0/tests_api.py +++ /dev/null @@ -1,153 +0,0 @@ -""" -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" - - ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] - - ENABLED_SIGNALS = ['course_published'] - - 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.caches['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.caches['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.caches['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.caches['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) diff --git a/openedx/core/djangoapps/content/course_structures/apps.py b/openedx/core/djangoapps/content/course_structures/apps.py index 51ca56a572..a41d0b9234 100644 --- a/openedx/core/djangoapps/content/course_structures/apps.py +++ b/openedx/core/djangoapps/content/course_structures/apps.py @@ -9,11 +9,3 @@ class CourseStructuresConfig(AppConfig): Custom AppConfig for openedx.core.djangoapps.content.course_structures """ name = u'openedx.core.djangoapps.content.course_structures' - - def ready(self): - """ - Define tasks to perform at app loading time: - - * Connect signal handlers - """ - from . import signals # pylint: disable=unused-variable diff --git a/openedx/core/djangoapps/content/course_structures/management/__init__.py b/openedx/core/djangoapps/content/course_structures/management/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openedx/core/djangoapps/content/course_structures/management/commands/__init__.py b/openedx/core/djangoapps/content/course_structures/management/commands/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openedx/core/djangoapps/content/course_structures/management/commands/generate_course_structure.py b/openedx/core/djangoapps/content/course_structures/management/commands/generate_course_structure.py deleted file mode 100644 index 2e09ecf154..0000000000 --- a/openedx/core/djangoapps/content/course_structures/management/commands/generate_course_structure.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Django Management Command: Generate Course Structure -Generates and stores course structure information for one or more courses. -""" -import logging - -from django.core.management.base import BaseCommand -from opaque_keys.edx.keys import CourseKey -from six import text_type - -from openedx.core.djangoapps.content.course_structures.tasks import update_course_structure -from xmodule.modulestore.django import modulestore - -log = logging.getLogger(__name__) - - -class Command(BaseCommand): - """ - Generates and stores course structure information for one or more courses. - """ - help = 'Generates and stores course structure for one or more courses.' - - def add_arguments(self, parser): - parser.add_argument('course_id', nargs='*') - parser.add_argument('--all', - action='store_true', - help='Generate structures for all courses.') - - def handle(self, *args, **options): - """ - Perform the course structure generation workflow - """ - if options['all']: - course_keys = [course.id for course in modulestore().get_courses()] - else: - course_keys = [CourseKey.from_string(arg) for arg in options['course_id']] - - if not course_keys: - log.fatal('No courses specified.') - return - - log.info('Generating course structures for %d courses.', len(course_keys)) - log.debug('Generating course structure(s) for the following courses: %s', course_keys) - - for course_key in course_keys: - try: - # Run the update task synchronously so that we know when all course structures have been updated. - # TODO Future improvement: Use .delay(), add return value to ResultSet, and wait for execution of - # all tasks using ResultSet.join(). I (clintonb) am opting not to make this improvement right now - # as I do not have time to test it fully. - update_course_structure.apply(args=[text_type(course_key)]) - except Exception as ex: - log.exception('An error occurred while generating course structure for %s: %s', - text_type(course_key), text_type(ex)) - - log.info('Finished generating course structures.') diff --git a/openedx/core/djangoapps/content/course_structures/signals.py b/openedx/core/djangoapps/content/course_structures/signals.py deleted file mode 100644 index 0770d573b4..0000000000 --- a/openedx/core/djangoapps/content/course_structures/signals.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Django Signals classes and functions for the Course Structure application -""" -from django.dispatch.dispatcher import receiver - -from xmodule.modulestore.django import SignalHandler - -from .models import CourseStructure - - -@receiver(SignalHandler.course_published) -def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument - """ - Course Structure application receiver for the course_published signal - """ - # Import tasks here to avoid a circular import. - from .tasks import update_course_structure - - # Delete the existing discussion id map cache to avoid inconsistencies - try: - structure = CourseStructure.objects.get(course_id=course_key) - structure.discussion_id_map_json = None - structure.save() - except CourseStructure.DoesNotExist: - pass - - # Note: The countdown=0 kwarg is set to to ensure the method below does not attempt to access the course - # before the signal emitter has finished all operations. This is also necessary to ensure all tests pass. - update_course_structure.apply_async([unicode(course_key)], countdown=0) diff --git a/openedx/core/djangoapps/content/course_structures/tasks.py b/openedx/core/djangoapps/content/course_structures/tasks.py deleted file mode 100644 index bccf529c55..0000000000 --- a/openedx/core/djangoapps/content/course_structures/tasks.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Asynchronous tasks related to the Course Structure sub-application -""" -import json -import logging - -from celery.task import task -from opaque_keys.edx.keys import CourseKey -from six import text_type - -from xmodule.modulestore.django import modulestore - - -log = logging.getLogger('edx.celery.task') - - -def _generate_course_structure(course_key): - """ - Generates a course structure dictionary for the specified course. - """ - with modulestore().bulk_operations(course_key): - course = modulestore().get_course(course_key, depth=None) - blocks_stack = [course] - blocks_dict = {} - discussions = {} - while blocks_stack: - curr_block = blocks_stack.pop() - children = curr_block.get_children() if curr_block.has_children else [] - key = unicode(curr_block.scope_ids.usage_id) - block = { - "usage_key": key, - "block_type": curr_block.category, - "display_name": curr_block.display_name, - "children": [unicode(child.scope_ids.usage_id) for child in children] - } - - if (curr_block.category == 'discussion' and - hasattr(curr_block, 'discussion_id') and - curr_block.discussion_id): - discussions[curr_block.discussion_id] = unicode(curr_block.scope_ids.usage_id) - - # Retrieve these attributes separately so that we can fail gracefully - # if the block doesn't have the attribute. - attrs = (('graded', False), ('format', None)) - for attr, default in attrs: - if hasattr(curr_block, attr): - block[attr] = getattr(curr_block, attr, default) - else: - log.warning('Failed to retrieve %s attribute of block %s. Defaulting to %s.', attr, key, default) - block[attr] = default - - blocks_dict[key] = block - - # Add this blocks children to the stack so that we can traverse them as well. - blocks_stack.extend(children) - return { - 'structure': { - "root": unicode(course.scope_ids.usage_id), - "blocks": blocks_dict - }, - 'discussion_id_map': discussions - } - - -@task(name=u'openedx.core.djangoapps.content.course_structures.tasks.update_course_structure') -def update_course_structure(course_key): - """ - Regenerates and updates the course structure (in the database) for the specified course. - """ - # Import here to avoid circular import. - from .models import CourseStructure - - # Ideally we'd like to accept a CourseLocator; however, CourseLocator is not JSON-serializable (by default) so - # Celery's delayed tasks fail to start. For this reason, callers should pass the course key as a Unicode string. - if not isinstance(course_key, basestring): - raise ValueError('course_key must be a string. {} is not acceptable.'.format(type(course_key))) - - course_key = CourseKey.from_string(course_key) - - try: - structure = _generate_course_structure(course_key) - except Exception as ex: - log.exception('An error occurred while generating course structure: %s', text_type(ex)) - raise - - structure_json = json.dumps(structure['structure']) - discussion_id_map_json = json.dumps(structure['discussion_id_map']) - - structure_model, created = CourseStructure.objects.get_or_create( - course_id=course_key, - defaults={ - 'structure_json': structure_json, - 'discussion_id_map_json': discussion_id_map_json - } - ) - - if not created: - structure_model.structure_json = structure_json - structure_model.discussion_id_map_json = discussion_id_map_json - structure_model.save() diff --git a/openedx/core/djangoapps/content/course_structures/tests.py b/openedx/core/djangoapps/content/course_structures/tests.py deleted file mode 100644 index 3c1bef4c19..0000000000 --- a/openedx/core/djangoapps/content/course_structures/tests.py +++ /dev/null @@ -1,243 +0,0 @@ -""" -Course Structure Content sub-application test cases -""" -import json -from nose.plugins.attrib import attr -from opaque_keys.edx.django.models import UsageKey - -from xmodule.modulestore.django import SignalHandler -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from openedx.core.djangoapps.content.course_structures.models import CourseStructure -from openedx.core.djangoapps.content.course_structures.signals import listen_for_course_publish -from openedx.core.djangoapps.content.course_structures.tasks import _generate_course_structure, update_course_structure - - -class SignalDisconnectTestMixin(object): - """ - Mixin for tests to disable calls to signals.listen_for_course_publish when the course_published signal is fired. - """ - - def setUp(self): - super(SignalDisconnectTestMixin, self).setUp() - SignalHandler.course_published.disconnect(listen_for_course_publish) - - def tearDown(self): - SignalHandler.course_published.connect(listen_for_course_publish) - super(SignalDisconnectTestMixin, self).tearDown() - - -@attr(shard=2) -class CourseStructureTaskTests(ModuleStoreTestCase): - """ - Test cases covering Course Structure task-related workflows - """ - def setUp(self, **kwargs): - super(CourseStructureTaskTests, self).setUp() - self.course = CourseFactory.create(org='TestX', course='TS101', run='T1') - self.section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') - self.discussion_xblock_1 = ItemFactory.create( - parent=self.course, - category='discussion', - discussion_id='test_discussion_id_1' - ) - self.discussion_xblock_2 = ItemFactory.create( - parent=self.course, - category='discussion', - discussion_id='test_discussion_id_2' - ) - CourseStructure.objects.all().delete() - - def test_generate_course_structure(self): - blocks = {} - - def add_block(block): - """ - Inserts new child XBlocks into the existing course tree - """ - children = block.get_children() if block.has_children else [] - - blocks[unicode(block.location)] = { - "usage_key": unicode(block.location), - "block_type": block.category, - "display_name": block.display_name, - "graded": block.graded, - "format": block.format, - "children": [unicode(child.location) for child in children] - } - - for child in children: - add_block(child) - - add_block(self.course) - - expected = { - 'root': unicode(self.course.location), - 'blocks': blocks - } - - self.maxDiff = None - actual = _generate_course_structure(self.course.id) - self.assertDictEqual(actual['structure'], expected) - - def test_structure_json(self): - """ - Although stored as compressed data, CourseStructure.structure_json should always return the uncompressed string. - """ - course_id = 'a/b/c' - structure = { - 'root': course_id, - 'blocks': { - course_id: { - 'id': course_id - } - } - } - structure_json = json.dumps(structure) - structure = CourseStructure.objects.create(course_id=self.course.id, structure_json=structure_json) - self.assertEqual(structure.structure_json, structure_json) - - # Reload the data to ensure the init signal is fired to decompress the data. - cs = CourseStructure.objects.get(course_id=self.course.id) - self.assertEqual(cs.structure_json, structure_json) - - def test_structure(self): - """ - CourseStructure.structure should return the uncompressed, JSON-parsed course structure. - """ - structure = { - 'root': 'a/b/c', - 'blocks': { - 'a/b/c': { - 'id': 'a/b/c' - } - } - } - structure_json = json.dumps(structure) - cs = CourseStructure.objects.create(course_id=self.course.id, structure_json=structure_json) - self.assertDictEqual(cs.structure, structure) - - def test_ordered_blocks(self): - structure = { - 'root': 'a/b/c', - 'blocks': { - 'a/b/c': { - 'id': 'a/b/c', - 'children': [ - 'g/h/i' - ] - }, - 'd/e/f': { - 'id': 'd/e/f', - 'children': [] - }, - 'g/h/i': { - 'id': 'h/j/k', - 'children': [ - 'j/k/l', - 'd/e/f' - ] - }, - 'j/k/l': { - 'id': 'j/k/l', - 'children': [] - } - } - } - in_order_blocks = ['a/b/c', 'g/h/i', 'j/k/l', 'd/e/f'] - structure_json = json.dumps(structure) - retrieved_course_structure = CourseStructure.objects.create( - course_id=self.course.id, structure_json=structure_json - ) - - self.assertEqual(retrieved_course_structure.ordered_blocks.keys(), in_order_blocks) - - def test_block_with_missing_fields(self): - """ - The generator should continue to operate on blocks/XModule that do not have graded or format fields. - """ - # TODO In the future, test logging using testfixtures.LogCapture - # (https://pythonhosted.org/testfixtures/logging.html). Talk to TestEng before adding that library. - category = 'peergrading' - display_name = 'Testing Module' - module = ItemFactory.create(parent=self.section, category=category, display_name=display_name) - structure = _generate_course_structure(self.course.id) - - usage_key = unicode(module.location) - actual = structure['structure']['blocks'][usage_key] - expected = { - "usage_key": usage_key, - "block_type": category, - "display_name": display_name, - "graded": False, - "format": None, - "children": [] - } - self.assertEqual(actual, expected) - - def test_generate_discussion_id_map(self): - id_map = {} - - def add_block(block): - """Adds the given block and all of its children to the expected discussion id map""" - children = block.get_children() if block.has_children else [] - - if block.category == 'discussion': - id_map[block.discussion_id] = unicode(block.location) - - for child in children: - add_block(child) - - add_block(self.course) - - actual = _generate_course_structure(self.course.id) - self.assertEqual(actual['discussion_id_map'], id_map) - - def test_discussion_id_map_json(self): - id_map = { - 'discussion_id_1': 'module_location_1', - 'discussion_id_2': 'module_location_2' - } - id_map_json = json.dumps(id_map) - structure = CourseStructure.objects.create(course_id=self.course.id, discussion_id_map_json=id_map_json) - self.assertEqual(structure.discussion_id_map_json, id_map_json) - - structure = CourseStructure.objects.get(course_id=self.course.id) - self.assertEqual(structure.discussion_id_map_json, id_map_json) - - def test_discussion_id_map(self): - id_map = { - 'discussion_id_1': 'block-v1:TestX+TS101+T1+type@discussion+block@b141953dff414921a715da37eb14ecdc', - 'discussion_id_2': 'i4x://TestX/TS101/discussion/466f474fa4d045a8b7bde1b911e095ca' - } - id_map_json = json.dumps(id_map) - structure = CourseStructure.objects.create(course_id=self.course.id, discussion_id_map_json=id_map_json) - expected_id_map = { - key: UsageKey.from_string(value).map_into_course(self.course.id) - for key, value in id_map.iteritems() - } - self.assertEqual(structure.discussion_id_map, expected_id_map) - - def test_discussion_id_map_missing(self): - structure = CourseStructure.objects.create(course_id=self.course.id) - self.assertIsNone(structure.discussion_id_map) - - def test_update_course_structure(self): - """ - Test the actual task that orchestrates data generation and updating the database. - """ - # Method requires string input - course_id = self.course.id - self.assertRaises(ValueError, update_course_structure, course_id) - - # Ensure a CourseStructure object is created - expected_structure = _generate_course_structure(course_id) - update_course_structure(unicode(course_id)) - structure = CourseStructure.objects.get(course_id=course_id) - self.assertEqual(structure.course_id, course_id) - self.assertEqual(structure.structure, expected_structure['structure']) - self.assertEqual(structure.discussion_id_map.keys(), expected_structure['discussion_id_map'].keys()) - self.assertEqual( - [unicode(value) for value in structure.discussion_id_map.values()], - expected_structure['discussion_id_map'].values() - )