Commit 13efeaa5 by Calen Pennington

Add demo of CascadeKeys policy working

parent ea1100d4
......@@ -41,6 +41,10 @@ setup(
],
'xmodule.v2': [
'vertical = xmodule.vertical_module:VerticalModule',
'course = xmodule.course_module:CourseModule',
],
'policy.v1': [
'cascade = xmodule.course_module:CascadeKeys',
]
}
)
from fs.errors import ResourceNotFoundError
import logging
from lxml import etree
from path import path # NOTE (THK): Only used for detecting presence of syllabus
import requests
import time
import hashlib
from xmodule.util.decorators import lazyproperty
from xmodule.graders import load_grading_policy
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time, stringify_time
from .util.decorators import lazyproperty
from .graders import load_grading_policy
from .modulestore import Location
from .seq_module import SequenceDescriptor, SequenceModule
from .timeparse import parse_time, stringify_time
from .structure_module import StructureModule
from .xmodule import Plugin
log = logging.getLogger(__name__)
def load_policies(policy_list):
"""
policy_list is a list of dictionaries, each with the following keys:
class: The name of a registered policy plugin
condition: An optional dictionary contaning the optional keys:
ids: A list of user ids for whom this policy should be applied
roles: A list of user roles for whom this policy should be applied
args: An option dictionary containing named arguments to pass to the policy plugin
"""
return [
Policy.load_class(policy['class'])(condition=policy.get('condition'), **policy.get('params', {}))
for policy in policy_list
]
class CourseModule(StructureModule):
@property
def policies(self):
return load_policies(self.content.get('policy_list', []))
def apply_policies(self, user):
# N.B. this code needs to be expanded to handle policies that are
# time specific and thus return an expire header
policies_to_apply = [
policy
for policy in self.policies
if policy.applies_to(user)
]
cache_key = self.cache_id(policies_to_apply)
cached_tree = self.runtime.cache('policy').get(cache_key)
if cached_tree is not None:
return cached_tree
tree = self.usage_tree
for policy in policies_to_apply:
tree = policy.apply(tree)
self.runtime.cache('policy').set(cache_key, tree)
return tree
def cache_id(self, policies):
hasher = hashlib.md5(str(self.usage_tree.as_json()))
for policy in policies:
hasher.update(policy.id)
return hasher.hexdigest()
class Policy(Plugin):
entry_point = 'policy.v1'
def __init__(self, condition):
self.condition = condition
self.id = str(id(self))
def apply(self, tree):
return tree
def applies_to(self, user):
if self.condition is None:
return True
# N.B. This code may need to expand to allow a more expressive
# conditional language
applies_by_id = user.id in self.condition.get('ids', [])
applies_by_role = bool(set(user.groups) & set(self.condition.get('roles', set())))
return applies_by_id or applies_by_role
class CascadeKeys(Policy):
"""
Policy that cascades the values specified for a set of policy keys
down the tree, prioritizing policies already set on descendents
over those being cascaded
"""
def __init__(self, keys, *args, **kwargs):
super(CascadeKeys, self).__init__(*args, **kwargs)
self.keys = keys
def apply(self, tree):
def cascade(settings):
new_settings = dict(settings)
for key in self.keys:
if key not in new_settings and key in tree.settings:
new_settings[key] = tree.settings[key]
return new_settings
children = [
self.apply(child._replace(settings=cascade(child.settings)))
for child in tree.children
]
return tree._replace(children=children)
class Reschedule(Policy):
"""
This policy adds a specified timedelta to all start_dates
"""
def __init__(self, delta, *args, **kwargs):
super(CascadeKeys, self).__init__(*args, **kwargs)
self.delta = delta
def apply(self, tree):
children = [
self.apply(child) for child in tree.children
]
settings = dict(tree.settings)
if 'start_date' in policy:
settings['start_date'] = settings['start_date'] + delta
return tree._replace(settings=settings, children=children)
# class AppendModule(QueryPolicy):
# """
# This module will append a policy after each module matching the query.
# Any keys in policy_to_copy will be copied from the usage node that
# matches the query.
# """
# def __init__(self, query, source, policy_to_copy=None, *args, **kwargs):
# super(AppendModule, self).__init__(query, *args, **kwargs)
# self.policy_to_copy = policy_to_copy if policy_to_copy is not None else []
# self.source = source
# def update(usage):
# """
# Return a list of usages to replace the returned usage with
# """
# to_insert = Usage.create_usage(self.source)
# policy = dict(to_insert.policy)
# for key in self.policy_to_copy:
# if key in usage:
# policy[key] = usage[key]
# return [usage, to_insert._replace(policy=policy)]
class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule
......
from collections import namedtuple
from .xmodule import XModule
# N.B. it would be nice to make settings a frozen dictionary, and children a frozen list
# to force usages to behave entirely like values
class Usage(namedtuple('Usage', 'id source settings children')):
__slots__ = ()
@classmethod
def create_usage(cls, source):
xmodule = xmodule.get_module(source)
return Usage(
uuid(),
xmodule.id,
xmodule.course_settings,
[],
)
def as_json(self):
json = self._asdict()
json['children'] = [child.as_json() for child in json['children']]
return json
def load_usage(usage_tree):
"""
usage_tree is a nested set of dictionaries with the following keys:
id: the uuid of the usage
source: the id and version of the xmodule that this usage is an instance of
settings: default settings values set by the source xmodule
children: child usages
"""
usage_tree['children'] = [load_usage(child) for child in usage_tree['children']]
return Usage(**usage_tree)
class StructureModule(XModule):
def __init__(self, *args, **kwargs):
super(StructureModule, self).__init__(*args, **kwargs)
self._usage_tree = None
@property
def usage_tree(self):
if self._usage_tree is None:
self._usage_tree = load_usage(self.content['usage_tree'])
return self._usage_tree
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