Merge branch 'master' into feature/abarrett/lms-notes-app

This commit is contained in:
Arthur Barrett
2013-05-08 18:04:24 -04:00
21 changed files with 463 additions and 108 deletions

View File

@@ -8,15 +8,42 @@ import urllib
def fasthash(string):
m = hashlib.new("md4")
m.update(string)
return m.hexdigest()
"""
Hashes `string` into a string representation of a 128-bit digest.
"""
md4 = hashlib.new("md4")
md4.update(string)
return md4.hexdigest()
def cleaned_string(val):
"""
Converts `val` to unicode and URL-encodes special characters
(including quotes and spaces)
"""
return urllib.quote_plus(smart_str(val))
def safe_key(key, key_prefix, version):
safe_key = urllib.quote_plus(smart_str(key))
"""
Given a `key`, `key_prefix`, and `version`,
return a key that is safe to use with memcache.
if len(safe_key) > 250:
safe_key = fasthash(safe_key)
`key`, `key_prefix`, and `version` can be numbers, strings, or unicode.
"""
return ":".join([key_prefix, str(version), safe_key])
# Clean for whitespace and control characters, which
# cause memcache to raise an exception
key = cleaned_string(key)
key_prefix = cleaned_string(key_prefix)
version = cleaned_string(version)
# Attempt to combine the prefix, version, and key
combined = ":".join([key_prefix, version, key])
# If the total length is too long for memcache, hash it
if len(combined) > 250:
combined = fasthash(combined)
# Return the result
return combined

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,124 @@
"""
Tests for memcache in util app
"""
from django.test import TestCase
from django.core.cache import get_cache
from django.conf import settings
from util.memcache import safe_key
class MemcacheTest(TestCase):
"""
Test memcache key cleanup
"""
# Test whitespace, control characters, and some non-ASCII UTF-16
UNICODE_CHAR_CODES = ([c for c in range(0, 30)] + [127] +
[129, 500, 2 ** 8 - 1, 2 ** 8 + 1, 2 ** 16 - 1])
def setUp(self):
self.cache = get_cache('default')
def test_safe_key(self):
key = safe_key('test', 'prefix', 'version')
self.assertEqual(key, 'prefix:version:test')
def test_numeric_inputs(self):
# Numeric key
self.assertEqual(safe_key(1, 'prefix', 'version'), 'prefix:version:1')
# Numeric prefix
self.assertEqual(safe_key('test', 5, 'version'), '5:version:test')
# Numeric version
self.assertEqual(safe_key('test', 'prefix', 5), 'prefix:5:test')
def test_safe_key_long(self):
# Choose lengths close to memcached's cutoff (250)
for length in [248, 249, 250, 251, 252]:
# Generate a key of that length
key = 'a' * length
# Make the key safe
key = safe_key(key, '', '')
# The key should now be valid
self.assertTrue(self._is_valid_key(key),
msg="Failed for key length {0}".format(length))
def test_long_key_prefix_version(self):
# Long key
key = safe_key('a' * 300, 'prefix', 'version')
self.assertTrue(self._is_valid_key(key))
# Long prefix
key = safe_key('key', 'a' * 300, 'version')
self.assertTrue(self._is_valid_key(key))
# Long version
key = safe_key('key', 'prefix', 'a' * 300)
self.assertTrue(self._is_valid_key(key))
def test_safe_key_unicode(self):
for unicode_char in self.UNICODE_CHAR_CODES:
# Generate a key with that character
key = unichr(unicode_char)
# Make the key safe
key = safe_key(key, '', '')
# The key should now be valid
self.assertTrue(self._is_valid_key(key),
msg="Failed for unicode character {0}".format(unicode_char))
def test_safe_key_prefix_unicode(self):
for unicode_char in self.UNICODE_CHAR_CODES:
# Generate a prefix with that character
prefix = unichr(unicode_char)
# Make the key safe
key = safe_key('test', prefix, '')
# The key should now be valid
self.assertTrue(self._is_valid_key(key),
msg="Failed for unicode character {0}".format(unicode_char))
def test_safe_key_version_unicode(self):
for unicode_char in self.UNICODE_CHAR_CODES:
# Generate a version with that character
version = unichr(unicode_char)
# Make the key safe
key = safe_key('test', '', version)
# The key should now be valid
self.assertTrue(self._is_valid_key(key),
msg="Failed for unicode character {0}".format(unicode_char))
def _is_valid_key(self, key):
"""
Test that a key is memcache-compatible.
Based on Django's validator in core.cache.backends.base
"""
# Check the length
if len(key) > 250:
return False
# Check that there are no spaces or control characters
for char in key:
if ord(char) < 33 or ord(char) == 127:
return False
return True

View File

@@ -1,4 +1,4 @@
"""Tests for the util package"""
"""Tests for the Zendesk"""
from django.conf import settings
from django.contrib.auth.models import AnonymousUser

View File

@@ -49,7 +49,10 @@ class _ZendeskApi(object):
settings.ZENDESK_USER,
settings.ZENDESK_API_KEY,
use_api_token=True,
api_version=2
api_version=2,
# As of 2012-05-08, Zendesk is using a CA that is not
# installed on our servers
client_args={"disable_ssl_certificate_validation": True}
)
def create_ticket(self, ticket):

View File

@@ -203,9 +203,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
def save_instance_data(self):
for attribute in self.student_attributes:
child_attr = getattr(self.child_module, attribute)
if child_attr != getattr(self, attribute):
setattr(self, attribute, getattr(self.child_module, attribute))
setattr(self, attribute, getattr(self.child_module, attribute))
class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):

View File

@@ -8,20 +8,23 @@ class @PeerGrading
@use_single_location = @peer_grading_container.data('use-single-location')
@peer_grading_outer_container = $('.peer-grading-container')
@ajax_url = @peer_grading_container.data('ajax-url')
@error_container = $('.error-container')
@error_container.toggle(not @error_container.is(':empty'))
@message_container = $('.message-container')
@message_container.toggle(not @message_container.is(':empty'))
@problem_button = $('.problem-button')
@problem_button.click @show_results
@problem_list = $('.problem-list')
@construct_progress_bar()
if @use_single_location
if @use_single_location.toLowerCase() == "true"
#If the peer grading element is linked to a single location, then activate the backend for that location
@activate_problem()
else
#Otherwise, activate the panel view.
@error_container = $('.error-container')
@error_container.toggle(not @error_container.is(':empty'))
@message_container = $('.message-container')
@message_container.toggle(not @message_container.is(':empty'))
@problem_button = $('.problem-button')
@problem_button.click @show_results
@problem_list = $('.problem-list')
@construct_progress_bar()
construct_progress_bar: () =>
problems = @problem_list.find('tr').next()

View File

@@ -31,15 +31,22 @@ def inherit_metadata(descriptor, model_data):
Only metadata specified in self.inheritable_metadata will
be inherited
"""
# The inherited values that are actually being used.
if not hasattr(descriptor, '_inherited_metadata'):
setattr(descriptor, '_inherited_metadata', {})
# All inheritable metadata values (for which a value exists in model_data).
if not hasattr(descriptor, '_inheritable_metadata'):
setattr(descriptor, '_inheritable_metadata', {})
# Set all inheritable metadata from kwargs that are
# in self.inheritable_metadata and aren't already set in metadata
for attr in INHERITABLE_METADATA:
if attr not in descriptor._model_data and attr in model_data:
descriptor._inherited_metadata[attr] = model_data[attr]
descriptor._model_data[attr] = model_data[attr]
if attr in model_data:
descriptor._inheritable_metadata[attr] = model_data[attr]
if attr not in descriptor._model_data:
descriptor._inherited_metadata[attr] = model_data[attr]
descriptor._model_data[attr] = model_data[attr]
def own_metadata(module):

View File

@@ -11,7 +11,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo
from xblock.core import Object, Integer, Boolean, String, Scope
from xmodule.fields import Date, StringyFloat
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
from open_ended_grading_classes import combined_open_ended_rubric
@@ -28,14 +28,14 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
class PeerGradingFields(object):
use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.",
use_for_single_location = StringyBoolean(help="Whether to use this for a single location or as a panel.",
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
scope=Scope.settings)
is_graded = Boolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
is_graded = StringyBoolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
max_grade = StringyInteger(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
scope=Scope.settings)
student_data_for_location = Object(help="Student data for a given peer grading problem.",
scope=Scope.user_state)
@@ -93,9 +93,9 @@ class PeerGradingModule(PeerGradingFields, XModule):
if not self.ajax_url.endswith("/"):
self.ajax_url = self.ajax_url + "/"
if not isinstance(self.max_grade, (int, long)):
#This could result in an exception, but not wrapping in a try catch block so it moves up the stack
self.max_grade = int(self.max_grade)
#StringyInteger could return None, so keep this check.
if not isinstance(self.max_grade, int):
raise TypeError("max_grade needs to be an integer.")
def closed(self):
return self._closed(self.timeinfo)

View File

@@ -151,6 +151,10 @@ class ImportTestCase(BaseCourseTestCase):
# Check that the child inherits due correctly
child = descriptor.get_children()[0]
self.assertEqual(child.lms.due, Date().from_json(v))
self.assertEqual(child._inheritable_metadata, child._inherited_metadata)
self.assertEqual(2, len(child._inherited_metadata))
self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start'])
self.assertEqual(v, child._inherited_metadata['due'])
# Now export and check things
resource_fs = MemoryFS()
@@ -184,6 +188,60 @@ class ImportTestCase(BaseCourseTestCase):
self.assertEqual(chapter_xml.tag, 'chapter')
self.assertFalse('due' in chapter_xml.attrib)
def test_metadata_no_inheritance(self):
"""
Checks that default value of None (for due) does not get marked as inherited.
"""
system = self.get_system()
url_name = 'test1'
start_xml = '''
<course org="{org}" course="{course}"
url_name="{url_name}" unicorn="purple">
<chapter url="hi" url_name="ch" display_name="CH">
<html url_name="h" display_name="H">Two houses, ...</html>
</chapter>
</course>'''.format(org=ORG, course=COURSE, url_name=url_name)
descriptor = system.process_xml(start_xml)
compute_inherited_metadata(descriptor)
self.assertEqual(descriptor.lms.due, None)
# Check that the child does not inherit a value for due
child = descriptor.get_children()[0]
self.assertEqual(child.lms.due, None)
self.assertEqual(child._inheritable_metadata, child._inherited_metadata)
self.assertEqual(1, len(child._inherited_metadata))
self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start'])
def test_metadata_override_default(self):
"""
Checks that due date can be overriden at child level.
"""
system = self.get_system()
course_due = 'March 20 17:00'
child_due = 'April 10 00:00'
url_name = 'test1'
start_xml = '''
<course org="{org}" course="{course}"
due="{due}" url_name="{url_name}" unicorn="purple">
<chapter url="hi" url_name="ch" display_name="CH">
<html url_name="h" display_name="H">Two houses, ...</html>
</chapter>
</course>'''.format(due=course_due, org=ORG, course=COURSE, url_name=url_name)
descriptor = system.process_xml(start_xml)
child = descriptor.get_children()[0]
child._model_data['due'] = child_due
compute_inherited_metadata(descriptor)
self.assertEqual(descriptor.lms.due, Date().from_json(course_due))
self.assertEqual(child.lms.due, Date().from_json(child_due))
# Test inherited metadata. Due does not appear here (because explicitly set on child).
self.assertEqual(1, len(child._inherited_metadata))
self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start'])
# Test inheritable metadata. This has the course inheritable value for due.
self.assertEqual(2, len(child._inheritable_metadata))
self.assertEqual(course_due, child._inheritable_metadata['due'])
def test_is_pointer_tag(self):
"""
Check that is_pointer_tag works properly.

View File

@@ -9,13 +9,13 @@ from mock import Mock
class TestFields(object):
# Will be returned by editable_metadata_fields.
max_attempts = StringyInteger(scope=Scope.settings)
max_attempts = StringyInteger(scope=Scope.settings, default=1000)
# Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
due = Date(scope=Scope.settings)
# Will not be returned by editable_metadata_fields because is not Scope.settings.
student_answers = Object(scope=Scope.user_state)
# Will be returned, and can override the inherited value from XModule.
display_name = String(scope=Scope.settings)
display_name = String(scope=Scope.settings, default='local default')
class EditableMetadataFieldsTest(unittest.TestCase):
@@ -25,27 +25,45 @@ class EditableMetadataFieldsTest(unittest.TestCase):
# Tests that the xblock fields (currently tags and name) get filtered out.
# Also tests that xml_attributes is filtered out of XmlDescriptor.
self.assertEqual(1, len(editable_fields), "Expected only 1 editable field for xml descriptor.")
self.assert_display_name_default(editable_fields)
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=False, inheritable=False, value=None, default_value=None)
def test_override_default(self):
# Tests that is_default is correct when a value overrides the default.
# Tests that explicitly_set is correct when a value overrides the default (not inheritable).
editable_fields = self.get_xml_editable_fields({'display_name': 'foo'})
display_name = editable_fields['display_name']
self.assertFalse(display_name['is_default'])
self.assertEqual('foo', display_name['value'])
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=True, inheritable=False, value='foo', default_value=None)
def test_additional_field(self):
editable_fields = self.get_module_editable_fields({'max_attempts' : '7'})
descriptor = self.get_descriptor({'max_attempts' : '7'})
editable_fields = descriptor.editable_metadata_fields
self.assertEqual(2, len(editable_fields))
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts, False, False, 7)
self.assert_display_name_default(editable_fields)
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
explicitly_set=True, inheritable=False, value=7, default_value=1000)
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=False, inheritable=False, value='local default', default_value='local default')
editable_fields = self.get_module_editable_fields({})
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts, True, False, None)
editable_fields = self.get_descriptor({}).editable_metadata_fields
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
explicitly_set=False, inheritable=False, value=1000, default_value=1000)
def test_inherited_field(self):
editable_fields = self.get_module_editable_fields({'display_name' : 'inherited'})
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name, False, True, 'inherited')
model_val = {'display_name' : 'inherited'}
descriptor = self.get_descriptor(model_val)
# Mimic an inherited value for display_name (inherited and inheritable are the same in this case).
descriptor._inherited_metadata = model_val
descriptor._inheritable_metadata = model_val
editable_fields = descriptor.editable_metadata_fields
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=False, inheritable=True, value='inherited', default_value='inherited')
descriptor = self.get_descriptor({'display_name' : 'explicit'})
# Mimic the case where display_name WOULD have been inherited, except we explicitly set it.
descriptor._inheritable_metadata = {'display_name' : 'inheritable value'}
descriptor._inherited_metadata = {}
editable_fields = descriptor.editable_metadata_fields
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value')
# Start of helper methods
def get_xml_editable_fields(self, model_data):
@@ -53,7 +71,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
return XmlDescriptor(system=system, location=None, model_data=model_data).editable_metadata_fields
def get_module_editable_fields(self, model_data):
def get_descriptor(self, model_data):
class TestModuleDescriptor(TestFields, XmlDescriptor):
@property
@@ -64,16 +82,12 @@ class EditableMetadataFieldsTest(unittest.TestCase):
system = test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
descriptor = TestModuleDescriptor(system=system, location=None, model_data=model_data)
descriptor._inherited_metadata = {'display_name' : 'inherited'}
return descriptor.editable_metadata_fields
return TestModuleDescriptor(system=system, location=None, model_data=model_data)
def assert_display_name_default(self, editable_fields):
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name, True, False, None)
def assert_field_values(self, editable_fields, name, field, is_default, is_inherited, value):
def assert_field_values(self, editable_fields, name, field, explicitly_set, inheritable, value, default_value):
test_field = editable_fields[name]
self.assertEqual(field, test_field['field'])
self.assertEqual(is_default, test_field['is_default'])
self.assertEqual(is_inherited, test_field['is_inherited'])
self.assertEqual(explicitly_set, test_field['explicitly_set'])
self.assertEqual(inheritable, test_field['inheritable'])
self.assertEqual(value, test_field['value'])
self.assertEqual(default_value, test_field['default_value'])

View File

@@ -624,27 +624,28 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
Can be limited by extending `non_editable_metadata_fields`.
"""
inherited_metadata = getattr(self, '_inherited_metadata', {})
inheritable_metadata = getattr(self, '_inheritable_metadata', {})
metadata = {}
for field in self.fields:
if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
continue
inherited = False
default = False
inheritable = False
value = getattr(self, field.name)
if field.name in self._model_data:
default = False
default_value = field.default
explicitly_set = field.name in self._model_data
if field.name in inheritable_metadata:
inheritable = True
default_value = field.from_json(inheritable_metadata.get(field.name))
if field.name in inherited_metadata:
if self._model_data.get(field.name) == inherited_metadata.get(field.name):
inherited = True
else:
default = True
explicitly_set = False
metadata[field.name] = {'field': field,
'value': value,
'is_inherited': inherited,
'is_default': default}
'default_value': default_value,
'inheritable': inheritable,
'explicitly_set': explicitly_set }
return metadata