Commit bce7d9e4 by Diana Huang Committed by Calen Pennington

Add tests and clean up A/B testing

Also fixes STUD-1351
parent 2d5c37b2
......@@ -179,3 +179,24 @@ class ContentStoreImportTest(ModuleStoreTestCase):
u'i4x://testX/peergrading_copy/combinedopenended/SampleQuestion',
peergrading_module.link_to_location
)
def test_rewrite_reference_value_dict(self):
module_store = modulestore('direct')
target_location = Location(['i4x', 'testX', 'split_test_copy', 'course', 'copy_run'])
import_from_xml(
module_store,
'common/test/data/',
['split_test_module'],
target_location_namespace=target_location
)
split_test_module = module_store.get_item(
Location(['i4x', 'testX', 'split_test_copy', 'split_test', 'split1'])
)
self.assertIsNotNone(split_test_module)
self.assertEqual(
{
"0": "i4x://testX/split_test_copy/vertical/sample_0",
"2": "i4x://testX/split_test_copy/vertical/sample_2",
},
split_test_module.group_id_to_child,
)
"""
Middleware for user api.
Adds user's tags to tracking event context.
"""
from track.contexts import COURSE_REGEX
from eventtracking import tracker
from user_api.models import UserCourseTag
class UserTagsEventContextMiddleware(object):
"""Middleware that adds a user's tags to tracking event context."""
CONTEXT_NAME = 'user_tags_context'
def process_request(self, request):
......@@ -41,4 +47,4 @@ class UserTagsEventContextMiddleware(object):
except: # pylint: disable=bare-except
pass
return response
\ No newline at end of file
return response
......@@ -8,7 +8,7 @@ class UserPreference(models.Model):
key = models.CharField(max_length=255, db_index=True)
value = models.TextField()
class Meta:
class Meta: # pylint: disable=missing-docstring
unique_together = ("user", "key")
@classmethod
......@@ -45,5 +45,5 @@ class UserCourseTag(models.Model):
course_id = models.CharField(max_length=255, db_index=True)
value = models.TextField()
class Meta:
class Meta: # pylint: disable=missing-docstring
unique_together = ("user", "course_id", "key")
"""Provides factories for User API models."""
from factory.django import DjangoModelFactory
from factory import SubFactory
from student.tests.factories import UserFactory
from user_api.models import UserPreference, UserCourseTag
# Factories don't have __init__ methods, and are self documenting
# pylint: disable=W0232, C0111
class UserPreferenceFactory(DjangoModelFactory):
FACTORY_FOR = UserPreference
......
"""Tests for user API middleware"""
from mock import Mock, patch
from unittest import TestCase
from django.http import HttpRequest, HttpResponse
from django.http import HttpResponse
from django.test.client import RequestFactory
from student.tests.factories import UserFactory, AnonymousUserFactory
......@@ -41,7 +42,8 @@ class TagsMiddlewareTest(TestCase):
self.assertEquals(self.middleware.process_request(self.request), None)
def assertContextSetTo(self, context):
self.tracker.get_tracker.return_value.enter_context.assert_called_with(
"""Asserts UserTagsEventContextMiddleware.CONTEXT_NAME matches ``context``"""
self.tracker.get_tracker.return_value.enter_context.assert_called_with( # pylint: disable=maybe-no-member
UserTagsEventContextMiddleware.CONTEXT_NAME,
context
)
......@@ -98,7 +100,7 @@ class TagsMiddlewareTest(TestCase):
self.assertContextSetTo({'course_id': self.course_id, 'course_user_tags': {}})
def test_remove_context(self):
get_tracker = self.tracker.get_tracker
get_tracker = self.tracker.get_tracker # pylint: disable=maybe-no-member
exit_context = get_tracker.return_value.exit_context
# The middleware should clean up the context when the request is done
......
......@@ -9,6 +9,10 @@ UserCourseTag model.
from user_api.models import UserCourseTag
# Scopes
# (currently only allows per-course tags. Can be expanded to support
# global tags (e.g. using the existing UserPreferences table))
COURSE_SCOPE = 'course'
def get_course_tag(user, course_id, key):
"""
......
......@@ -158,6 +158,7 @@ class TextbookList(List):
class UserPartitionList(List):
"""Special List class for listing UserPartitions"""
def from_json(self, values):
return [UserPartition.from_json(v) for v in values]
......@@ -175,8 +176,9 @@ class CourseFields(object):
# advanced_settings.
user_partitions = UserPartitionList(
help="List of user partitions of this course into groups, used e.g. for experiments",
default=[], scope=Scope.content)
default=[],
scope=Scope.content
)
wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content)
enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings)
......
<div class="split-test-view" id="split-test">
<select class="split-test-select">
<option value="0">Group 0</option>
<option value="1">Group 1</option>
<option value="2">Group 2</option>
</select>
<div class="split-test-child" data-group-id="0">
&lt;div class='condition-text'&gt;condition 0&lt;/div&gt;
</div>
<div class="split-test-child" data-group-id="1">
&lt;div class='condition-text'&gt;condition 1&lt;/div&gt;
</div>
<div class="split-test-child" data-group-id="2">
&lt;div class='condition-text'&gt;condition 2&lt;/div&gt;
</div>
<div class='split-test-child-container'></div>
</div>
......@@ -57,6 +57,7 @@ lib_paths:
- common_static/js/vendor/analytics.js
- common_static/js/test/add_ajax_prefix.js
- common_static/js/src/utility.js
- public/js/split_test_staff.js
# Paths to spec (test) JavaScript files
spec_paths:
......
../public/
\ No newline at end of file
describe('Tests for split_test staff view switching', function() {
var ab_module;
var elem;
beforeEach(function() {
loadFixtures('split_test_staff.html');
elem = $('#split-test');
window.XBlock = jasmine.createSpyObj('XBlock', ['initializeBlocks']);
ab_module = ABTestSelector(null, elem);
});
afterEach(function() {
delete window.XBlock;
});
it("test that we have only one visible condition", function() {
var containers = elem.find('.split-test-child-container').length;
var conditions_shown = elem.find('.split-test-child-container .condition-text').length;
expect(containers).toEqual(1);
expect(conditions_shown).toEqual(1);
expect(XBlock.initializeBlocks).toHaveBeenCalled();
});
it("test that the right child is visible when selected", function() {
var groups = ['0', '1', '2'];
for(var i = 0; i < groups.length; i++) {
var to_select = groups[i];
elem.find('.split-test-select').val(to_select).change();
var child_text = elem.find('.split-test-child-container .condition-text').text();
expect(child_text).toContain(to_select);
expect(XBlock.initializeBlocks).toHaveBeenCalled();
}
});
});
......@@ -23,8 +23,8 @@ class @Sequence
updatePageTitle: ->
# update the page title to include the current section
position_link = @link_for(@position)
if position_link and position_link.attr('title')
document.title = position_link.attr('title') + @base_page_title
if position_link and position_link.data('page-title')
document.title = position_link.data('page-title') + @base_page_title
hookUpProgressEvent: ->
$('.problems-wrapper').bind 'progressChanged', @updateProgress
......@@ -98,10 +98,10 @@ class @Sequence
# Added for aborting video bufferization, see ../video/10_main.js
@el.trigger "sequence:change"
@mark_active new_position
current_tab = @contents.eq(new_position - 1)
@content_container.html(current_tab.text()).attr("aria-labelledby", current_tab.attr("aria-labelledby"))
XBlock.initializeBlocks(@content_container)
window.update_schematics() # For embedded circuit simulator exercises in 6.002x
......@@ -115,8 +115,8 @@ class @Sequence
sequence_links.click @goto
# Focus on the first available xblock.
@content_container.find('.vert .xblock :first').focus()
@$("a.active").blur()
@$("a.active").blur()
goto: (event) =>
event.preventDefault()
if $(event.target).hasClass 'seqnav' # Links from courseware <a class='seqnav' href='n'>...</a>
......
......@@ -6,7 +6,7 @@ import json
from .xml import XMLModuleStore, ImportSystem, ParentTracker
from xmodule.modulestore import Location
from xblock.fields import Scope, Reference, ReferenceList
from xblock.fields import Scope, Reference, ReferenceList, ReferenceValueDict
from xmodule.contentstore.content import StaticContent
from .inheritance import own_metadata
from xmodule.errortracker import make_error_tracker
......@@ -547,15 +547,25 @@ def remap_namespace(module, target_location_namespace):
).url()
return new_ref
for field in all_fields:
if isinstance(module.fields.get(field), Reference):
new_ref = convert_ref(getattr(module, field))
setattr(module, field, new_ref)
for field_name in all_fields:
field_object = module.fields.get(field_name)
if isinstance(field_object, Reference):
new_ref = convert_ref(getattr(module, field_name))
setattr(module, field_name, new_ref)
module.save()
elif isinstance(module.fields.get(field), ReferenceList):
references = getattr(module, field)
elif isinstance(field_object, ReferenceList):
references = getattr(module, field_name)
new_references = [convert_ref(reference) for reference in references]
setattr(module, field, new_references)
setattr(module, field_name, new_references)
module.save()
elif isinstance(field_object, ReferenceValueDict):
reference_dict = getattr(module, field_name)
new_reference_dict = {
key: convert_ref(reference)
for key, reference
in reference_dict.items()
}
setattr(module, field_name, new_reference_dict)
module.save()
return module
......
......@@ -88,7 +88,7 @@ class PartitionService(object):
and persist the info.
"""
key = self._key_for_partition(user_partition)
scope = self._user_tags_service.COURSE
scope = self._user_tags_service.COURSE_SCOPE
group_id = self._user_tags_service.get_tag(scope, key)
if group_id is not None:
......@@ -133,6 +133,6 @@ class PartitionService(object):
'partition_name': user_partition.name
}
# TODO: Use the XBlock publish api instead
self._track_function('edx.split_test.assigned_user_to_partition', event_info)
self._track_function('xmodule.partitions.assigned_user_to_partition', event_info)
return group.id
......@@ -3,6 +3,7 @@ Test the partitions and partitions service
"""
from collections import defaultdict
from unittest import TestCase
from mock import Mock, MagicMock
......@@ -49,6 +50,131 @@ class TestGroup(TestCase):
self.assertEqual(group.id, test_id)
self.assertEqual(group.name, name)
def test_from_json_broken(self):
test_id = 5
name = "Grendel"
# Bad version
jsonified = {
"id": test_id,
"name": name,
"version": 9001
}
with self.assertRaisesRegexp(TypeError, "has unexpected version"):
group = Group.from_json(jsonified)
# Missing key "id"
jsonified = {
"name": name,
"version": Group.VERSION
}
with self.assertRaisesRegexp(TypeError, "missing value key 'id'"):
group = Group.from_json(jsonified)
# Has extra key - should not be a problem
jsonified = {
"id": test_id,
"name": name,
"version": Group.VERSION,
"programmer": "Cale"
}
group = Group.from_json(jsonified)
self.assertNotIn("programmer", group.to_json())
class TestUserPartition(TestCase):
"""Test constructing UserPartitions"""
def test_construct(self):
groups = [Group(0, 'Group 1'), Group(1, 'Group 2')]
user_partition = UserPartition(0, 'Test Partition', 'for testing purposes', groups)
self.assertEqual(user_partition.id, 0)
self.assertEqual(user_partition.name, "Test Partition")
self.assertEqual(user_partition.description, "for testing purposes")
self.assertEqual(user_partition.groups, groups)
def test_string_id(self):
groups = [Group(0, 'Group 1'), Group(1, 'Group 2')]
user_partition = UserPartition("70", 'Test Partition', 'for testing purposes', groups)
self.assertEqual(user_partition.id, 70)
def test_to_json(self):
groups = [Group(0, 'Group 1'), Group(1, 'Group 2')]
upid = 0
upname = "Test Partition"
updesc = "for testing purposes"
user_partition = UserPartition(upid, upname, updesc, groups)
jsonified = user_partition.to_json()
act_jsonified = {
"id": upid,
"name": upname,
"description": updesc,
"groups": [group.to_json() for group in groups],
"version": user_partition.VERSION
}
self.assertEqual(jsonified, act_jsonified)
def test_from_json(self):
groups = [Group(0, 'Group 1'), Group(1, 'Group 2')]
upid = 1
upname = "Test Partition"
updesc = "For Testing Purposes"
jsonified = {
"id": upid,
"name": upname,
"description": updesc,
"groups": [group.to_json() for group in groups],
"version": UserPartition.VERSION
}
user_partition = UserPartition.from_json(jsonified)
self.assertEqual(user_partition.id, upid)
self.assertEqual(user_partition.name, upname)
self.assertEqual(user_partition.description, updesc)
for act_group in user_partition.groups:
self.assertIn(act_group.id, [0, 1])
exp_group = groups[act_group.id]
self.assertEqual(exp_group.id, act_group.id)
self.assertEqual(exp_group.name, act_group.name)
def test_from_json_broken(self):
groups = [Group(0, 'Group 1'), Group(1, 'Group 2')]
upid = 1
upname = "Test Partition"
updesc = "For Testing Purposes"
# Missing field
jsonified = {
"name": upname,
"description": updesc,
"groups": [group.to_json() for group in groups],
"version": UserPartition.VERSION
}
with self.assertRaisesRegexp(TypeError, "missing value key 'id'"):
user_partition = UserPartition.from_json(jsonified)
# Wrong version (it's over 9000!)
jsonified = {
'id': upid,
"name": upname,
"description": updesc,
"groups": [group.to_json() for group in groups],
"version": 9001
}
with self.assertRaisesRegexp(TypeError, "has unexpected version"):
user_partition = UserPartition.from_json(jsonified)
# Has extra key - should not be a problem
jsonified = {
'id': upid,
"name": upname,
"description": updesc,
"groups": [group.to_json() for group in groups],
"version": UserPartition.VERSION,
"programmer": "Cale"
}
user_partition = UserPartition.from_json(jsonified)
self.assertNotIn("programmer", user_partition.to_json())
class StaticPartitionService(PartitionService):
"""
......@@ -63,32 +189,37 @@ class StaticPartitionService(PartitionService):
return self._partitions
class MemoryUserTagsService(object):
"""
An implementation of a user_tags XBlock service that
uses an in-memory dictionary for storage
"""
COURSE_SCOPE = 'course'
def __init__(self):
self._tags = defaultdict(dict)
def get_tag(self, scope, key):
"""Sets the value of ``key`` to ``value``"""
print 'GETTING', scope, key, self._tags
return self._tags[scope].get(key)
def set_tag(self, scope, key, value):
"""Gets the value of ``key``"""
self._tags[scope][key] = value
print 'SET', scope, key, value, self._tags
class TestPartitionsService(TestCase):
"""
Test getting a user's group out of a partition
"""
def setUp(self):
groups = [Group(0, 'Group 1'), Group(1, 'Group 2')]
self.partition_id = 0
# construct the user_service
self.user_tags = dict()
self.user_tags_service = MagicMock()
def mock_set_tag(_scope, key, value):
"""Sets the value of ``key`` to ``value``"""
self.user_tags[key] = value
def mock_get_tag(_scope, key):
"""Gets the value of ``key``"""
if key in self.user_tags:
return self.user_tags[key]
return None
self.user_tags_service.set_tag = mock_set_tag
self.user_tags_service.get_tag = mock_get_tag
self.user_tags_service = MemoryUserTagsService()
user_partition = UserPartition(self.partition_id, 'Test Partition', 'for testing purposes', groups)
self.partitions_service = StaticPartitionService(
......
......@@ -4,30 +4,26 @@
* @constructor
*/
function ABTestSelector(elem) {
me = this;
me.elem = $(elem);
function ABTestSelector(runtime, elem) {
var _this = this;
_this.elem = $(elem);
_this.children = _this.elem.find('.split-test-child');
_this.content_container = _this.elem.find('.split-test-child-container');
select_child = function(group_id) {
function select_child(group_id) {
// iterate over all the children and hide all the ones that haven't been selected
// and show the one that was selected
me.elem.find('.split-test-child').each(function() {
_this.children.each(function() {
// force this id to remain a string, even if it looks like something else
child_group_id = $(this).data('group-id').toString();
var child_group_id = $(this).data('group-id').toString();
if(child_group_id === group_id) {
$(this).show();
_this.content_container.html($(this).text());
XBlock.initializeBlocks(_this.content_container);
}
else {
$(this).hide();
}
});
}
// hide all the children
me.elem.find('.split-test-child').hide();
select = me.elem.find('.split-test-select');
select = _this.elem.find('.split-test-select');
cur_group_id = select.val();
select_child(cur_group_id);
......
/* Javascript for the Acid XBlock. */
/* Javascript for the Split Test XBlock. */
function SplitTestStudentView(runtime, element) {
$.post(runtime.handlerUrl(element, 'log_child_render'));
return {};
......
......@@ -88,13 +88,12 @@ class SequenceModule(SequenceFields, XModule):
rendered_child = child.render('student_view', context)
fragment.add_frag_resources(rendered_child)
titles = child.get_content_titles()
print titles
childinfo = {
'content': rendered_child.content,
'title': "\n".join(
grand_child.display_name
for grand_child in child.get_children()
if grand_child.display_name is not None
),
'title': "\n".join(titles),
'page_title': titles[0] if titles else '',
'progress_status': Progress.to_js_status_str(progress),
'progress_detail': Progress.to_js_detail_str(progress),
'type': child.get_icon_class(),
......
......@@ -12,15 +12,18 @@ from xmodule.x_module import XModule, module_attr
from lxml import etree
from xblock.core import XBlock
from xblock.fields import Scope, Integer, Dict
from xblock.fields import Scope, Integer, ReferenceValueDict
from xblock.fragment import Fragment
log = logging.getLogger('edx.' + __name__)
class SplitTestFields(object):
user_partition_id = Integer(help="Which user partition is used for this test",
scope=Scope.content)
"""Fields needed for split test module"""
user_partition_id = Integer(
help="Which user partition is used for this test",
scope=Scope.content
)
# group_id is an int
# child is a serialized UsageId (aka Location). This child
......@@ -31,9 +34,10 @@ class SplitTestFields(object):
# TODO: is there a way to add some validation around this, to
# be run on course load or in studio or ....
group_id_to_child = Dict(help="Which child module students in a particular "
"group_id should see",
scope=Scope.content)
group_id_to_child = ReferenceValueDict(
help="Which child module students in a particular group_id should see",
scope=Scope.content
)
@XBlock.needs('user_tags')
......@@ -79,6 +83,25 @@ class SplitTestModule(SplitTestFields, XModule):
return None
def get_content_titles(self):
"""
Returns list of content titles for split_test's child.
This overwrites the get_content_titles method included in x_module by default.
WHY THIS OVERWRITE IS NECESSARY: If we fetch *all* of split_test's children,
we'll end up getting all of the possible conditions users could ever see.
Ex: If split_test shows a video to group A and HTML to group B, the
regular get_content_titles in x_module will get the title of BOTH the video
AND the HTML.
We only want the content titles that should actually be displayed to the user.
split_test's .child property contains *only* the child that should actually
be shown to the user, so we call get_content_titles() on only that child.
"""
return self.child.get_content_titles()
def get_child_descriptors(self):
"""
For grading--return just the chosen child.
......@@ -127,13 +150,9 @@ class SplitTestModule(SplitTestFields, XModule):
fragment.add_content(self.system.render_template('split_test_staff_view.html', {
'items': contents,
}))
frag_js = """
$(document).ready(function() {{
ABTestSelector($('.split-test-view'));
}});
"""
fragment.add_javascript(frag_js)
fragment.add_css('.split-test-child { display: none; }')
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/split_test_staff.js'))
fragment.initialize_js('ABTestSelector')
return fragment
def student_view(self, context):
......@@ -159,9 +178,12 @@ class SplitTestModule(SplitTestFields, XModule):
return fragment
@XBlock.handler
def log_child_render(self, request, suffix=''):
def log_child_render(self, request, suffix=''): # pylint: disable=unused-argument
"""
Record in the tracking logs which child was rendered
"""
# TODO: use publish instead, when publish is wired to the tracking logs
self.system.track_function('split-test-child-render', {'child-id': self.child.scope_ids.usage_id})
self.system.track_function('xblock.split_test.child_render', {'child-id': self.child.scope_ids.usage_id})
return Response()
def get_icon_class(self):
......@@ -184,6 +206,7 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor):
child_descriptor = module_attr('child_descriptor')
log_child_render = module_attr('log_child_render')
get_content_titles = module_attr('get_content_titles')
def definition_to_xml(self, resource_fs):
......@@ -200,3 +223,4 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor):
makes it use module.get_child_descriptors().
"""
return True
......@@ -8,9 +8,7 @@ from xmodule.tests.xml import factories as xml
from xmodule.tests.xml import XModuleXmlImportTest
from xmodule.tests import get_test_system
from xmodule.partitions.partitions import Group, UserPartition
from xmodule.partitions.partitions_service import PartitionService
from xmodule.partitions.test_partitions import StaticPartitionService
from xmodule.partitions.test_partitions import StaticPartitionService, MemoryUserTagsService
class SplitTestModuleFactory(xml.XmlImportFactory):
......@@ -38,15 +36,24 @@ class SplitTestModuleTest(XModuleXmlImportTest):
'group_id_to_child': '{"0": "i4x://edX/xml_test_course/html/split_test_cond0", "1": "i4x://edX/xml_test_course/html/split_test_cond1"}'
}
)
xml.HtmlFactory(parent=split_test, url_name='split_test_cond0')
xml.HtmlFactory(parent=split_test, url_name='split_test_cond1')
xml.HtmlFactory(parent=split_test, url_name='split_test_cond0', text='HTML FOR GROUP 0')
xml.HtmlFactory(parent=split_test, url_name='split_test_cond1', text='HTML FOR GROUP 1')
self.course = self.process_xml(course)
course_seq = self.course.get_children()[0]
self.module_system = get_test_system()
self.tags_service = Mock(name='user_tags')
self.module_system._services['user_tags'] = self.tags_service
def get_module(descriptor):
module_system = get_test_system()
module_system.get_module = get_module
descriptor.bind_for_student(module_system, descriptor._field_data)
return descriptor
self.module_system.get_module = get_module
self.module_system.descriptor_system = self.course.runtime
self.tags_service = MemoryUserTagsService()
self.module_system._services['user_tags'] = self.tags_service # pylint: disable=protected-access
self.partitions_service = StaticPartitionService(
[
......@@ -57,18 +64,59 @@ class SplitTestModuleTest(XModuleXmlImportTest):
course_id=self.course.id,
track_function=Mock(name='track_function'),
)
self.module_system._services['partitions'] = self.partitions_service
self.module_system._services['partitions'] = self.partitions_service # pylint: disable=protected-access
self.split_test_module = course_seq.get_children()[0]
self.split_test_module.bind_for_student(self.module_system, self.split_test_module._field_data)
self.split_test_descriptor = course_seq.get_children()[0]
self.split_test_descriptor.bind_for_student(
self.module_system,
self.split_test_descriptor._field_data
)
@ddt.data(('0', 'split_test_cond0'), ('1', 'split_test_cond1'))
@ddt.unpack
def test_child(self, user_tag, child_url_name):
self.tags_service.get_tag.return_value = user_tag
self.tags_service.set_tag(
self.tags_service.COURSE_SCOPE,
'xblock.partition_service.partition_0',
user_tag
)
self.assertEquals(self.split_test_module.child_descriptor.url_name, child_url_name)
@ddt.data(('0',), ('1',))
@ddt.unpack
def test_child_old_tag_value(self, user_tag):
# If user_tag has a stale value, we should still get back a valid child url
self.tags_service.set_tag(
self.tags_service.COURSE_SCOPE,
'xblock.partition_service.partition_0',
'2'
)
self.assertEquals(self.split_test_descriptor.child_descriptor.url_name, child_url_name)
self.assertIn(self.split_test_module.child_descriptor.url_name, ['split_test_cond0', 'split_test_cond1'])
@ddt.data(('0', 'HTML FOR GROUP 0'), ('1', 'HTML FOR GROUP 1'))
@ddt.unpack
def test_get_html(self, user_tag, child_content):
self.tags_service.set_tag(
self.tags_service.COURSE_SCOPE,
'xblock.partition_service.partition_0',
user_tag
)
self.assertIn(
child_content,
self.module_system.render(self.split_test_module, 'student_view').content
)
@ddt.data(('0',), ('1',))
@ddt.unpack
def test_child_missing_tag_value(self, user_tag):
# If user_tag has a missing value, we should still get back a valid child url
self.assertIn(self.split_test_module.child_descriptor.url_name, ['split_test_cond0', 'split_test_cond1'])
@ddt.data(('100',), ('200',), ('300',), ('400',), ('500',), ('600',), ('700',), ('800',), ('900',), ('1000',))
@ddt.unpack
def test_child_persist_new_tag_value_when_tag_missing(self, user_tag):
# If a user_tag has a missing value, a group should be saved/persisted for that user.
# So, we check that we get the same url_name when we call on the url_name twice.
# We run the test ten times so that, if our storage is failing, we'll be most likely to notice it.
self.assertEquals(self.split_test_module.child_descriptor.url_name, self.split_test_module.child_descriptor.url_name)
......@@ -146,6 +146,10 @@ class SequenceFactory(XmlImportFactory):
"""Factory for <sequential> nodes"""
tag = 'sequential'
class VerticalFactory(XmlImportFactory):
"""Factory for <vertical> nodes"""
tag = 'vertical'
class ProblemFactory(XmlImportFactory):
"""Factory for <problem> nodes"""
......@@ -154,5 +158,5 @@ class ProblemFactory(XmlImportFactory):
class HtmlFactory(XmlImportFactory):
"""Factory for <problem> nodes"""
"""Factory for <html> nodes"""
tag = 'html'
......@@ -217,6 +217,31 @@ class XModuleMixin(XBlockMixin):
self.save()
return self._field_data._kvs # pylint: disable=protected-access
def get_content_titles(self):
"""
Returns list of content titles for all of self's children.
SEQUENCE
|
VERTICAL
/ \
SPLIT_TEST DISCUSSION
/ \
VIDEO A VIDEO B
Essentially, this function returns a list of display_names (e.g. content titles)
for all of the leaf nodes. In the diagram above, calling get_content_titles on
SEQUENCE would return the display_names of `VIDEO A`, `VIDEO B`, and `DISCUSSION`.
This is most obviously useful for sequence_modules, which need this list to display
tooltips to users, though in theory this should work for any tree that needs
the display_names of all its leaf nodes.
"""
if self.has_children:
return sum((child.get_content_titles() for child in self.get_children()), [])
else:
return [self.display_name_with_default]
def get_children(self):
"""Returns a list of XBlock instances for the children of
this module"""
......
<course url_name='split_test_course' org='split_test' course='split_test'>
<chapter>
<sequential>
<vertical>
<split_test url_name="split1" user_partition_id="0" group_id_to_child='{"0": "i4x://split_test/split_test/vertical/sample_0", "2": "i4x://split_test/split_test/vertical/sample_2"}'>
<vertical url_name="sample_0">
<html>Here is a prompt for group 0, please respond in the discussion.</html>
<discussion for="split test discussion 0" id="split_test_d0" discussion_category="Lectures"/>
</vertical>
<vertical url_name="sample_2">
<html>Here is a prompt for group 2, please respond in the discussion.</html>
<discussion for="split test discussion 2" id="split_test_d2" discussion_category="Lectures"/>
</vertical>
</split_test>
</vertical>
</sequential>
</chapter>
</course>
\ No newline at end of file
"""
Test for split test XModule
"""
import ddt
from mock import MagicMock, patch, Mock
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.partitions.partitions import Group, UserPartition
from xmodule.partitions.test_partitions import StaticPartitionService
from user_api.tests.factories import UserCourseTagFactory
from xmodule.partitions.partitions import Group, UserPartition
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class SplitTestBase(ModuleStoreTestCase):
__test__ = False
def setUp(self):
self.partition = UserPartition(
0,
'first_partition',
'First Partition',
[
Group(0, 'alpha'),
Group(1, 'beta')
]
)
self.course = CourseFactory.create(
number=self.COURSE_NUMBER,
user_partitions=[self.partition]
)
self.chapter = ItemFactory.create(
parent_location=self.course.location,
category="chapter",
display_name="test chapter",
)
self.sequential = ItemFactory.create(
parent_location=self.chapter.location,
category="sequential",
display_name="Split Test Tests",
)
self.student = UserFactory.create()
CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id)
self.client.login(username=self.student.username, password='test')
def _video(self, parent, group):
return ItemFactory.create(
parent_location=parent.location,
category="video",
display_name="Group {} Sees This Video".format(group),
)
def _problem(self, parent, group):
return ItemFactory.create(
parent_location=parent.location,
category="problem",
display_name="Group {} Sees This Problem".format(group),
data="<h1>No Problem Defined Yet!</h1>",
)
def _html(self, parent, group):
return ItemFactory.create(
parent_location=parent.location,
category="html",
display_name="Group {} Sees This HTML".format(group),
data="Some HTML for group {}".format(group),
)
def test_split_test_0(self):
self._check_split_test(0)
def test_split_test_1(self):
self._check_split_test(1)
def _check_split_test(self, user_tag):
tag_factory = UserCourseTagFactory(
user=self.student,
course_id=self.course.id,
key='xblock.partition_service.partition_{0}'.format(self.partition.id),
value=str(user_tag)
)
resp = self.client.get(reverse('courseware_section',
kwargs={'course_id': self.course.id,
'chapter': self.chapter.url_name,
'section': self.sequential.url_name}
))
content = resp.content
print content
# Assert we see the proper icon in the top display
self.assertIn('<a class="{} inactive progress-0"'.format(self.ICON_CLASSES[user_tag]), content)
# And proper tooltips
for tooltip in self.TOOLTIPS[user_tag]:
self.assertIn(tooltip, content)
for hidden in self.HIDDEN_CONTENT[user_tag]:
self.assertNotIn(hidden, content)
# Assert that we can see the data from the appropriate test condition
for visible in self.VISIBLE_CONTENT[user_tag]:
self.assertIn(visible, content)
class TestVertSplitTestVert(SplitTestBase):
"""
Tests related to xmodule/split_test_module
"""
__test__ = True
COURSE_NUMBER='vert-split-vert'
ICON_CLASSES = [
'seq_problem',
'seq_video',
]
TOOLTIPS = [
['Group 0 Sees This Video', "Group 0 Sees This Problem"],
['Group 1 Sees This Video', 'Group 1 Sees This HTML'],
]
HIDDEN_CONTENT = [
['Condition 0 vertical'],
['Condition 1 vertical'],
]
# Data is html encoded, because it's inactive inside the
# sequence until javascript is executed
VISIBLE_CONTENT = [
['class=&#34;problems-wrapper'],
['Some HTML for group 1']
]
def setUp(self):
super(TestVertSplitTestVert, self).setUp()
# vert <- split_test
# split_test cond 0 = vert <- {video, problem}
# split_test cond 1 = vert <- {video, html}
vert1 = ItemFactory.create(
parent_location=self.sequential.location,
category="vertical",
display_name="Split test vertical",
)
c0_url = self.course.location._replace(category="vertical", name="split_test_cond0")
c1_url = self.course.location._replace(category="vertical", name="split_test_cond1")
split_test = ItemFactory.create(
parent_location=vert1.location,
category="split_test",
display_name="Split test",
user_partition_id='0',
group_id_to_child={"0": c0_url.url(), "1": c1_url.url()},
)
cond0vert = ItemFactory.create(
parent_location=split_test.location,
category="vertical",
display_name="Condition 0 vertical",
location=c0_url,
)
video0 = self._video(cond0vert, 0)
problem0 = self._problem(cond0vert, 0)
cond1vert = ItemFactory.create(
parent_location=split_test.location,
category="vertical",
display_name="Condition 1 vertical",
location=c1_url,
)
video1 = self._video(cond1vert, 1)
html1 = self._html(cond1vert, 1)
class TestSplitTestVert(SplitTestBase):
"""
Tests related to xmodule/split_test_module
"""
__test__ = True
COURSE_NUMBER = 'split-vert'
ICON_CLASSES = [
'seq_problem',
'seq_video',
]
TOOLTIPS = [
['Group 0 Sees This Video', "Group 0 Sees This Problem"],
['Group 1 Sees This Video', 'Group 1 Sees This HTML'],
]
HIDDEN_CONTENT = [
['Condition 0 vertical'],
['Condition 1 vertical'],
]
# Data is html encoded, because it's inactive inside the
# sequence until javascript is executed
VISIBLE_CONTENT = [
['class=&#34;problems-wrapper'],
['Some HTML for group 1']
]
def setUp(self):
super(TestSplitTestVert, self).setUp()
# split_test cond 0 = vert <- {video, problem}
# split_test cond 1 = vert <- {video, html}
c0_url = self.course.location._replace(category="vertical", name="split_test_cond0")
c1_url = self.course.location._replace(category="vertical", name="split_test_cond1")
split_test = ItemFactory.create(
parent_location=self.sequential.location,
category="split_test",
display_name="Split test",
user_partition_id='0',
group_id_to_child={"0": c0_url.url(), "1": c1_url.url()},
)
cond0vert = ItemFactory.create(
parent_location=split_test.location,
category="vertical",
display_name="Condition 0 vertical",
location=c0_url,
)
video0 = self._video(cond0vert, 0)
problem0 = self._problem(cond0vert, 0)
cond1vert = ItemFactory.create(
parent_location=split_test.location,
category="vertical",
display_name="Condition 1 vertical",
location=c1_url,
)
video1 = self._video(cond1vert, 1)
html1 = self._html(cond1vert, 1)
......@@ -133,15 +133,14 @@ class UserTagsService(object):
A runtime class that provides an interface to the user service. It handles filling in
the current course id and current user.
"""
# Scopes
# (currently only allows per-course tags. Can be expanded to support
# global tags (e.g. using the existing UserPreferences table))
COURSE = 'course'
COURSE_SCOPE = user_service.COURSE_SCOPE
def __init__(self, runtime):
self.runtime = runtime
def _get_current_user(self):
"""Returns the real, not anonymized, current user."""
real_user = self.runtime.get_real_user(self.runtime.anonymous_student_id)
return real_user
......@@ -152,7 +151,7 @@ class UserTagsService(object):
scope: the current scope of the runtime
key: the key for the value we want
"""
if scope != self.COURSE:
if scope != user_service.COURSE_SCOPE:
raise ValueError("unexpected scope {0}".format(scope))
return user_service.get_course_tag(self._get_current_user(),
......@@ -166,7 +165,7 @@ class UserTagsService(object):
key: the key that to the value to be set
value: the value to set
"""
if scope != self.COURSE:
if scope != user_service.COURSE_SCOPE:
raise ValueError("unexpected scope {0}".format(scope))
return user_service.set_course_tag(self._get_current_user(),
......
......@@ -88,7 +88,8 @@ class TestHandlerUrl(TestCase):
self.assertIn('handler_a', self._parsed_path('handler_a'))
class TestUserServiceInterface(TestCase):
class TestUserServiceAPI(TestCase):
"""Test the user service interface"""
def setUp(self):
self.course_id = "org/course/run"
......@@ -97,6 +98,7 @@ class TestUserServiceInterface(TestCase):
self.user.save()
def mock_get_real_user(_anon_id):
"""Just returns the test user"""
return self.user
self.runtime = LmsModuleSystem(
......@@ -126,3 +128,11 @@ class TestUserServiceInterface(TestCase):
tag = self.runtime.service(self.mock_block, 'user_tags').get_tag(self.scope, self.key)
self.assertEqual(tag, set_value)
# Try to set tag in wrong scope
with self.assertRaises(ValueError):
self.runtime.service(self.mock_block, 'user_tags').set_tag('fake_scope', self.key, set_value)
# Try to get tag in wrong scope
with self.assertRaises(ValueError):
self.runtime.service(self.mock_block, 'user_tags').get_tag('fake_scope', self.key)
......@@ -19,6 +19,7 @@
data-element="${idx+1}"
href="javascript:void(0);"
title="${item['title']|h}"
data-page-title="${item['page_title']|h}"
aria-controls="seq_contents_${idx}"
id="tab_${idx}"
tabindex="0"
......@@ -36,7 +37,7 @@
</nav>
% for idx, item in enumerate(items):
<div id="seq_contents_${idx}"
<div id="seq_contents_${idx}"
aria-labelledby="tab_${idx}"
aria-hidden="true"
class="seq_contents tex2jax_ignore asciimath2jax_ignore">
......
<%! from django.utils.translation import ugettext as _ %>
<div class="split-test-view">
<select class="split-test-select">
% for idx, item in enumerate(items):
<option value="${item['group_id']}">Group ${item['group_id']}</option>
## Translators: The 'Group' here refers to the group of users that has been sorted into group_id
<option value="${item['group_id']}">${_("Group {group_id}").format(group_id=item['group_id'])}</option>
%endfor
</select>
% for idx, item in enumerate(items):
<div class="split-test-child" data-group-id="${item['group_id']}"data-id="${item['id']}">
${item['content']}
<div class="split-test-child" data-group-id="${item['group_id']}" data-id="${item['id']}">
${item['content'] | h}
</div>
% endfor
<div class='split-test-child-container'></div>
</div>
......@@ -17,7 +17,7 @@
-e git+https://github.com/appliedsec/pygeoip.git@95e69341cebf5a6a9fbf7c4f5439d458898bdc3b#egg=pygeoip
# Our libraries:
-e git+https://github.com/edx/XBlock.git@893cd83dfb24405ce81b07f49c1c2e3053cdc865#egg=XBlock
-e git+https://github.com/edx/XBlock.git@6dd8a9223cae34184ba5e2e1a186f36c4df1e080#egg=XBlock
-e git+https://github.com/edx/codejail.git@e3d98f9455#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.2.9#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool
......
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