96 lines
2.9 KiB
Python
96 lines
2.9 KiB
Python
import json
|
|
import random
|
|
from lxml import etree
|
|
|
|
from x_module import XModule, XModuleDescriptor
|
|
|
|
|
|
class ModuleDescriptor(XModuleDescriptor):
|
|
pass
|
|
|
|
|
|
def group_from_value(groups, v):
|
|
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value
|
|
in [0,1], return the associated group (in the above case, return
|
|
'a' if v<0.3, 'b' if 0.3<=v<0.7, and 'c' if v>0.7
|
|
'''
|
|
sum = 0
|
|
for (g, p) in groups:
|
|
sum = sum + p
|
|
if sum > v:
|
|
return g
|
|
|
|
# Round off errors might cause us to run to the end of the list
|
|
# If the do, return the last element
|
|
return g
|
|
|
|
|
|
class Module(XModule):
|
|
"""
|
|
Implements an A/B test with an aribtrary number of competing groups
|
|
|
|
Format:
|
|
<abtest>
|
|
<group name="a" portion=".1"><contenta/></group>
|
|
<group name="b" portion=".2"><contentb/></group>
|
|
<default><contentdefault/></default>
|
|
</abtest>
|
|
"""
|
|
|
|
def __init__(self, system, xml, item_id, instance_state=None, shared_state=None):
|
|
XModule.__init__(self, system, xml, item_id, instance_state, shared_state)
|
|
self.xmltree = etree.fromstring(xml)
|
|
|
|
target_groups = self.xmltree.findall('group')
|
|
if shared_state is None:
|
|
target_values = [
|
|
(elem.get('name'), float(elem.get('portion')))
|
|
for elem in target_groups
|
|
]
|
|
default_value = 1 - sum(val for (_, val) in target_values)
|
|
|
|
self.group = group_from_value(
|
|
target_values + [(None, default_value)],
|
|
random.uniform(0, 1)
|
|
)
|
|
else:
|
|
shared_state = json.loads(shared_state)
|
|
|
|
# TODO (cpennington): Remove this once we aren't passing in
|
|
# groups from django groups
|
|
if 'groups' in shared_state:
|
|
self.group = None
|
|
target_names = [elem.get('name') for elem in target_groups]
|
|
for group in shared_state['groups']:
|
|
if group in target_names:
|
|
self.group = group
|
|
break
|
|
else:
|
|
self.group = shared_state['group']
|
|
|
|
def get_shared_state(self):
|
|
return json.dumps({'group': self.group})
|
|
|
|
def _xml_children(self):
|
|
group = None
|
|
if self.group is None:
|
|
group = self.xmltree.find('default')
|
|
else:
|
|
for candidate_group in self.xmltree.find('group'):
|
|
if self.group == candidate_group.get('name'):
|
|
group = candidate_group
|
|
break
|
|
|
|
if group is None:
|
|
return []
|
|
return list(group)
|
|
|
|
def get_children(self):
|
|
return [self.module_from_xml(child) for child in self._xml_children()]
|
|
|
|
def rendered_children(self):
|
|
return [self.render_function(child) for child in self._xml_children()]
|
|
|
|
def get_html(self):
|
|
return '\n'.join(child.get_html() for child in self.get_children())
|