Add tests and clean up A/B testing
Also fixes STUD-1351
This commit is contained in:
committed by
Calen Pennington
parent
2d5c37b2a0
commit
bce7d9e43d
@@ -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,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
"""
|
||||
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
|
||||
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")
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""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
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""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)
|
||||
|
||||
19
common/lib/xmodule/xmodule/js/fixtures/split_test_staff.html
Normal file
19
common/lib/xmodule/xmodule/js/fixtures/split_test_staff.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<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">
|
||||
<div class='condition-text'>condition 0</div>
|
||||
</div>
|
||||
<div class="split-test-child" data-group-id="1">
|
||||
<div class='condition-text'>condition 1</div>
|
||||
</div>
|
||||
<div class="split-test-child" data-group-id="2">
|
||||
<div class='condition-text'>condition 2</div>
|
||||
</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:
|
||||
|
||||
1
common/lib/xmodule/xmodule/js/public
Symbolic link
1
common/lib/xmodule/xmodule/js/public
Symbolic link
@@ -0,0 +1 @@
|
||||
../public/
|
||||
@@ -0,0 +1,37 @@
|
||||
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);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* 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_descriptor.child_descriptor.url_name, child_url_name)
|
||||
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.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"""
|
||||
|
||||
19
common/test/data/split_test_module/course.xml
Normal file
19
common/test/data/split_test_module/course.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<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>
|
||||
244
lms/djangoapps/courseware/tests/test_split_module.py
Normal file
244
lms/djangoapps/courseware/tests/test_split_module.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
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="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="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">
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<%! 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
|
||||
|
||||
Reference in New Issue
Block a user