Commit a40d2b65 by Nimisha Asthagiri

Course Blocks App MA-1556

parent f84ac6f9
"""
The Course Blocks app, built upon the Block Cache framework in
openedx.core.lib.block_cache, is a higher layer django app in LMS that
provides additional context of Courses and Users (via usage_info.py) with
implementations for Block Structure Transformers that are related to
block structure course access.
As described in the Block Cache framework's __init__ module, this
framework provides faster access to course blocks for performance
sensitive features, by caching all transformer-required data so no
modulestore access is necessary during block access.
It is expected that only Block Access related transformers reside in
this django app, as they are cross-cutting authorization transformers
required across other features. Other higher-level and feature-specific
transformers should be implemented in their own separate apps.
Note: Currently, some of the implementation is redundant with the
has_access code in courseware/access.py. However, we do have short-term
plans for refactoring the current has_access code to use Course Blocks
instead (https://openedx.atlassian.net/browse/MA-1019). We have
introduced this redundancy in the short-term as an incremental
implementation approach, reducing risk with initial release of this app.
"""
# Importing signals is necessary to activate the course publish/delete signal handlers.
from . import signals # pylint: disable=unused-import
"""
API entry point to the course_blocks app with top-level
get_course_blocks and clear_course_from_cache functions.
"""
from django.core.cache import cache
from openedx.core.lib.block_cache.block_cache import get_blocks, clear_block_cache
from xmodule.modulestore.django import modulestore
from .transformers import (
library_content,
start_date,
user_partitions,
visibility,
)
from .usage_info import CourseUsageInfo
# Default list of transformers for manipulating course block structures
# based on the user's access to the course blocks.
COURSE_BLOCK_ACCESS_TRANSFORMERS = [
library_content.ContentLibraryTransformer(),
start_date.StartDateTransformer(),
user_partitions.UserPartitionTransformer(),
visibility.VisibilityTransformer(),
]
def get_course_blocks(
user,
root_block_usage_key,
transformers=None
):
"""
A higher order function implemented on top of the
block_cache.get_blocks function returning a transformed block
structure for the given user starting at root_block_usage_key.
Note: The current implementation requires the root_block_usage_key
to be the root block of its corresponding course. However, this
is a short-term limitation, which will be addressed in a coming
ticket (https://openedx.atlassian.net/browse/MA-1604). Once that
ticket is implemented, callers will be able to get course blocks
starting at any arbitrary location within a block structure.
Arguments:
user (django.contrib.auth.models.User) - User object for
which the block structure is to be transformed.
root_block_usage_key (UsageKey) - The usage_key for the root
of the block structure that is being accessed.
transformers ([BlockStructureTransformer]) - The list of
transformers whose transform methods are to be called.
If None, COURSE_BLOCK_ACCESS_TRANSFORMERS is used.
Returns:
BlockStructureBlockData - A transformed block structure,
starting at root_block_usage_key, that has undergone the
transform methods for the given user and the course
associated with the block structure. If using the default
transformers, the transformed block structure will be
exactly equivalent to the blocks that the given user has
access.
"""
store = modulestore()
if root_block_usage_key != store.make_course_usage_key(root_block_usage_key.course_key):
# Enforce this check for now until MA-1604 is implemented.
# Otherwise, callers will get incorrect block data after a
# new version of the course is published, since
# clear_course_from_cache only clears the cached block
# structures starting at the root block of the course.
raise NotImplementedError
return get_blocks(
cache,
store,
CourseUsageInfo(root_block_usage_key.course_key, user),
root_block_usage_key,
COURSE_BLOCK_ACCESS_TRANSFORMERS if transformers is None else transformers,
)
def clear_course_from_cache(course_key):
"""
A higher order function implemented on top of the
block_cache.clear_block_cache function that clears the block
structure from the cache for the block structure starting at the
root block of the course for the given course_key.
Note: See Note in get_course_blocks. Even after MA-1604 is
implemented, this implementation should still be valid since the
entire block structure of the course is cached, even though
arbitrary access to an intermediate block will be supported.
"""
course_usage_key = modulestore().make_course_usage_key(course_key)
return clear_block_cache(cache, course_usage_key)
"""
Signal handlers for invalidating cached data.
"""
from django.dispatch.dispatcher import receiver
from xmodule.modulestore.django import SignalHandler
from .api import clear_course_from_cache
@receiver(SignalHandler.course_published)
def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument
"""
Catches the signal that a course has been published in the module
store and invalidates the corresponding cache entry if one exists.
"""
clear_course_from_cache(course_key)
@receiver(SignalHandler.course_deleted)
def _listen_for_course_delete(sender, course_key, **kwargs): # pylint: disable=unused-argument
"""
Catches the signal that a course has been deleted from the
module store and invalidates the corresponding cache entry if one
exists.
"""
clear_course_from_cache(course_key)
"""
Module container for all Course Block Access Transformers.
"""
"""
Common Helper utilities for transformers
"""
def get_field_on_block(block, field_name, default_value=None):
"""
Get the field value that is directly set on the xblock.
Do not get the inherited value since field inheritance
returns value from only a single parent chain
(e.g., doesn't take a union in DAGs).
"""
if block.fields[field_name].is_set_on(block):
return getattr(block, field_name)
else:
return default_value
"""
Declares CourseUsageInfo class to be used by the transform method in
Transformers.
"""
from lms.djangoapps.courseware.access import _has_access_to_course
class CourseUsageInfo(object):
'''
A class object that encapsulates the course and user context to be
used as currency across block structure transformers, by passing
an instance of it in calls to BlockStructureTransformer.transform
methods.
'''
def __init__(self, course_key, user):
# Course identifier (opaque_keys.edx.keys.CourseKey)
self.course_key = course_key
# User object (django.contrib.auth.models.User)
self.user = user
# Cached value of whether the user has staff access (bool/None)
self._has_staff_access = None
@property
def has_staff_access(self):
'''
Returns whether the user has staff access to the course
associated with this CourseUsageInfo instance.
For performance reasons (minimizing multiple SQL calls), the
value is cached within this instance.
'''
if self._has_staff_access is None:
self._has_staff_access = _has_access_to_course(self.user, 'staff', self.course_key)
return self._has_staff_access
......@@ -1954,8 +1954,12 @@ INSTALLED_APPS = (
'lms.djangoapps.lms_xblock',
# Course data caching
'openedx.core.djangoapps.content.course_overviews',
'openedx.core.djangoapps.content.course_structures',
'lms.djangoapps.course_blocks',
# Old course structure API
'course_structure_api',
# Mailchimp Syncing
......
......@@ -97,6 +97,11 @@ CACHES = {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'edx_course_structure_mem_cache',
},
'lms.course_blocks': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'KEY_FUNCTION': 'util.memcache.safe_key',
'LOCATION': 'lms_course_blocks_cache',
},
}
......
......@@ -223,6 +223,10 @@ CACHES = {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'edx_location_block_cache',
},
'lms.course_blocks': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'edx_location_course_blocks',
},
}
# Dummy secret key for dev
......
......@@ -51,7 +51,10 @@ Registry. Transformers are registered using the platform's
PluginManager (e.g., Stevedore). This is currently done by updating
setup.py. Only registered transformers are called during the Collect
Phase. And only registered transformers can be used during the
Transform phase.
Transform phase. Exceptions to this rule are any nested transformers
that are contained within higher-order transformers - as long as the
higher-order transformers are registered and appropriately call the
contained transformers within them.
Note: A partial subset (as an ordered list) of the registered
transformers can be requested during the Transform phase, allowing
......
......@@ -36,6 +36,12 @@ def get_blocks(cache, modulestore, usage_info, root_block_usage_key, transformer
transformers whose transform methods are to be called.
This list should be a subset of the list of registered
transformers in the Transformer Registry.
Returns:
BlockStructureBlockData - A transformed block structure,
starting at root_block_usage_key, that has undergone the
transform methods in the given transformers with the
given usage_info.
"""
# Verify that all requested transformers are registered in the
......
......@@ -118,6 +118,16 @@ class BlockStructure(object):
"""
return usage_key in self._block_relations
def get_block_keys(self):
"""
Returns the block keys in the block structure.
Returns:
iterator(UsageKey) - An iterator of the usage
keys of all the blocks in the block structure.
"""
return self._block_relations.iterkeys()
#--- Block structure traversal methods ---#
def topological_traversal(
......@@ -198,13 +208,6 @@ class BlockStructure(object):
# Replace this structure's relations with the newly pruned one.
self._block_relations = pruned_block_relations
def _get_block_keys(self):
"""
Returns an iterator of all the block keys in the block
structure.
"""
return self._block_relations.iterkeys()
def _add_relation(self, parent_key, child_key):
"""
Adds a parent to child relationship in this block structure.
......
......@@ -48,5 +48,11 @@ setup(
"cohort = openedx.core.djangoapps.course_groups.partition_scheme:CohortPartitionScheme",
"verification = openedx.core.djangoapps.credit.partition_schemes:VerificationPartitionScheme",
],
"openedx.block_structure_transformer": [
"library_content = lms.djangoapps.course_blocks.transformers.library_content:ContentLibraryTransformer",
"start_date = lms.djangoapps.course_blocks.transformers.start_date:StartDateTransformer",
"user_partitions = lms.djangoapps.course_blocks.transformers.user_partitions:UserPartitionTransformer",
"visibility = lms.djangoapps.course_blocks.transformers.visibility:VisibilityTransformer",
],
}
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment