""" Tasks for bookmarks. """ import logging from celery import shared_task from django.db import transaction from edx_django_utils.monitoring import set_code_owner_attribute from opaque_keys.edx.keys import CourseKey from xmodule.modulestore.django import modulestore from . import PathItem log = logging.getLogger('edx.celery.task') def _calculate_course_xblocks_data(course_key): """ Fetch data for all the blocks in the course. This data consists of the display_name and path of the block. """ with modulestore().bulk_operations(course_key): course = modulestore().get_course(course_key, depth=None) blocks_info_dict = {} # Collect display_name and children usage keys. blocks_stack = [course] while blocks_stack: current_block = blocks_stack.pop() children = current_block.get_children() if current_block.has_children else [] usage_id = str(current_block.scope_ids.usage_id) block_info = { 'usage_key': current_block.scope_ids.usage_id, 'display_name': current_block.display_name_with_default, 'children_ids': [str(child.scope_ids.usage_id) for child in children] } blocks_info_dict[usage_id] = block_info # Add this blocks children to the stack so that we can traverse them as well. blocks_stack.extend(children) # Set children for block in blocks_info_dict.values(): block.setdefault('children', []) for child_id in block['children_ids']: block['children'].append(blocks_info_dict[child_id]) block.pop('children_ids', None) # Calculate paths def add_path_info(block_info, current_path): """Do a DFS and add paths info to each block_info.""" block_info.setdefault('paths', []) block_info['paths'].append(current_path) for child_block_info in block_info['children']: add_path_info(child_block_info, current_path + [block_info]) add_path_info(blocks_info_dict[str(course.scope_ids.usage_id)], []) return blocks_info_dict def _paths_from_data(paths_data): """ Construct a list of paths from path data. """ paths = [] for path_data in paths_data: paths.append([ PathItem(item['usage_key'], item['display_name']) for item in path_data if item['usage_key'].block_type != 'course' ]) return [path for path in paths if path] def paths_equal(paths_1, paths_2): """ Check if two paths are equivalent. """ if len(paths_1) != len(paths_2): return False for path_1, path_2 in zip(paths_1, paths_2): if len(path_1) != len(path_2): return False for path_item_1, path_item_2 in zip(path_1, path_2): if path_item_1.display_name != path_item_2.display_name: return False usage_key_1 = path_item_1.usage_key.replace( course_key=modulestore().fill_in_run(path_item_1.usage_key.course_key) ) usage_key_2 = path_item_1.usage_key.replace( course_key=modulestore().fill_in_run(path_item_2.usage_key.course_key) ) if usage_key_1 != usage_key_2: return False return True def _update_xblocks_cache(course_key): """ Calculate the XBlock cache data for a course and update the XBlockCache table. """ from .models import XBlockCache blocks_data = _calculate_course_xblocks_data(course_key) def update_block_cache_if_needed(block_cache, block_data): """ Compare block_cache object with data and update if there are differences. """ paths = _paths_from_data(block_data['paths']) if block_cache.display_name != block_data['display_name'] or not paths_equal(block_cache.paths, paths): log.info('Updating XBlockCache with usage_key: %s', str(block_cache.usage_key)) block_cache.display_name = block_data['display_name'] block_cache.paths = paths block_cache.save() with transaction.atomic(): block_caches = XBlockCache.objects.filter(course_key=course_key) for block_cache in block_caches: block_data = blocks_data.pop(str(block_cache.usage_key), None) if block_data: update_block_cache_if_needed(block_cache, block_data) for block_data in blocks_data.values(): with transaction.atomic(): paths = _paths_from_data(block_data['paths']) log.info('Creating XBlockCache with usage_key: %s', str(block_data['usage_key'])) block_cache, created = XBlockCache.objects.get_or_create(usage_key=block_data['usage_key'], defaults={ 'course_key': course_key, 'display_name': block_data['display_name'], 'paths': paths, }) if not created: update_block_cache_if_needed(block_cache, block_data) @shared_task(name='openedx.core.djangoapps.bookmarks.tasks.update_xblocks_cache') @set_code_owner_attribute def update_xblocks_cache(course_id): """ Update the XBlocks cache for a course. Arguments: course_id (String): The course_id of a course. """ # 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_id, str): raise ValueError('course_id must be a string. {} is not acceptable.'.format(type(course_id))) course_key = CourseKey.from_string(course_id) log.info('Starting XBlockCaches update for course_key: %s', course_id) _update_xblocks_cache(course_key) log.info('Ending XBlockCaches update for course_key: %s', course_id)