Merge branch 'master' into feature/abarrett/lms-notes-app
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
@include box-sizing(border-box);
|
||||
|
||||
.copy {
|
||||
@include font-size(13);
|
||||
@extend .t-copy-sub2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,12 +184,12 @@
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@include font-size(13);
|
||||
@extend .t-action3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
@include font-size(13);
|
||||
@extend .t-action3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,12 +367,12 @@
|
||||
}
|
||||
|
||||
.copy {
|
||||
@include font-size(13);
|
||||
@extend .t-copy-sub2;
|
||||
width: flex-grid(10, 12);
|
||||
color: $gray-l2;
|
||||
|
||||
.title {
|
||||
@include font-size(14);
|
||||
@extend .t-title-4;
|
||||
margin-bottom: 0;
|
||||
color: $white;
|
||||
}
|
||||
@@ -409,13 +409,13 @@
|
||||
|
||||
.action-primary {
|
||||
@include blue-button();
|
||||
@include font-size(13);
|
||||
@extend .t-action3;
|
||||
border-color: $blue-d2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
@include font-size(13);
|
||||
@extend .t-action3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,7 +504,7 @@
|
||||
|
||||
// adopted alerts
|
||||
.alert {
|
||||
@include font-size(14);
|
||||
@extend .t-copy-sub2;
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix();
|
||||
margin: 0 auto;
|
||||
@@ -530,7 +530,7 @@
|
||||
}
|
||||
|
||||
.copy {
|
||||
@include font-size(13);
|
||||
@extend .t-copy-sub2;
|
||||
width: flex-grid(10, 12);
|
||||
color: $gray-l2;
|
||||
|
||||
@@ -568,12 +568,12 @@
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@include font-size(13);
|
||||
@extend .t-action3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
@include font-size(13);
|
||||
@extend .t-action3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -730,7 +730,7 @@ body.uxdesign.alerts {
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
// background: #edbd3c;
|
||||
font-size: 14px;
|
||||
@extend .t-copy-sub1;
|
||||
@include clearfix;
|
||||
|
||||
.alert-message {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// ====================
|
||||
|
||||
// headings/titles
|
||||
.t-title-1, .t-title-2, .t-title-3, .t-title-4, .t-title-5, .t-title-5 {
|
||||
.t-title-1, .t-title-2, .t-title-3, .t-title-4, .t-title-5 {
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
|
||||
.t-title-4 {
|
||||
|
||||
@include font-size(14);
|
||||
}
|
||||
|
||||
.t-title-5 {
|
||||
@@ -82,4 +82,4 @@
|
||||
// misc
|
||||
.t-icon {
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
<li><a href="#alert-announcement2" class="show-alert">Show Announcement</a></li>
|
||||
<li><a href="#alert-announcement1" class="show-alert">Show Announcement with Actions</a></li>
|
||||
<li><a href="#alert-activation" class="show-alert">Show Activiation</a></li>
|
||||
<li><a href="#alert-threeActions" class="show-alert">Alert with three actions</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -128,6 +129,10 @@
|
||||
<h3 class="title-3">Different Static Examples of Notifications</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#notification-changesMade" class="show-notification">Show Changes Made (used in Advanced Settings)</a>
|
||||
<a href="#notification-changesMade" class="hide-notification">Hide Changes Made (used in Advanced Settings)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#notification-change" class="show-notification">Show Change Warning</a>
|
||||
<a href="#notification-change" class="hide-notification">Hide Change Warning</a>
|
||||
@@ -151,6 +156,10 @@
|
||||
<a href="#notification-help" class="show-notification">Show Help</a>
|
||||
<a href="#notification-help" class="hide-notification">Hide Help</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#notification-threeActions" class="show-notification">Show Notification with three actions</a>
|
||||
<a href="#notification-threeActions" class="hide-notification">Hide Notification with three actions</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -182,6 +191,33 @@
|
||||
</%block>
|
||||
|
||||
<%block name="view_alerts">
|
||||
<!-- alert: 3 actions -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-threeActions">
|
||||
<div class="alert warning has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-warning">⚠</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">You are editing a draft</h2>
|
||||
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action action-save action-primary">Save Draft</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action action-cancel action-secondary">Disgard Draft</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action action-secondary">Do Something Elsee</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: you're editing a draft -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-draft">
|
||||
<div class="alert warning has-actions">
|
||||
@@ -196,10 +232,10 @@
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button save-button action-primary">Save Draft</a>
|
||||
<a href="#" class="action action-save action-primary">Save Draft</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-secondary">Disgard Draft</a>
|
||||
<a href="#" class="action action-cancel action-secondary">Disgard Draft</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -220,10 +256,10 @@
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button save-button action-primary">Go to Newer Version</a>
|
||||
<a href="#" class="action action-save action-primary">Go to Newer Version</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-secondary">Continue Editing</a>
|
||||
<a href="#" class="action action-cancel action-secondary">Continue Editing</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -297,7 +333,7 @@
|
||||
<h3 class="sr">Alert Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-primary">Cancel Your Submission</a>
|
||||
<a href="#" class="action action-cancel action-primary">Cancel Your Submission</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -367,13 +403,13 @@
|
||||
|
||||
<%block name="view_notifications">
|
||||
<!-- notification: change has been made and a save is needed -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-change" id="notification-change" role="status">
|
||||
<div class="wrapper wrapper-notification wrapper-notification-change" aria-hidden="true" role="dialog" aria-labelledby="notification-change-title" aria-describedby="notification-change-description" id="notification-change">
|
||||
<div class="notification change has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-change">📝</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">You've Made Some Changes</h2>
|
||||
<p class="message">Your changes will not take effect until you <strong>save your progress</strong>.</p>
|
||||
<h2 class="title title-3" id="notification-change-title">You've Made Some Changes</h2>
|
||||
<p class="message" id="notification-change-description">Your changes will not take effect until you <strong>save your progress</strong>.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
@@ -390,6 +426,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- notification: three actions example -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-change" aria-hidden="true" role="dialog" aria-labelledby="notification-threeActions-title" aria-describedby="notification-threeActions-description" id="notification-threeActions">
|
||||
<div class="notification change has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-change">📝</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" id="notification-threeActions-title">You've Made Some Changes</h2>
|
||||
<p class="message" id="notification-threeActions-description">Your changes will not take effect until you <strong>save your progress</strong>.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-primary">Save Changes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-secondary">Don't Save</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-secondary">Do something else</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- notification: change has been made and a save is needed -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-warning" aria-hidden="true" role="dialog" aria-labelledby="notification-changesMade-title" aria-describedby="notification-changesMade-description" id="notification-changesMade">
|
||||
<div class="notification warning has-actions">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-warning">⚠</i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" id="notification-changesMade-title">You've Made Some Changes</h2>
|
||||
<p id="notification-changesMade-description">Your changes will not take effect until you <strong>save your progress</strong>. Take care with key and value formatting, as validation is <strong>not implemented</strong>.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="" class="action action-save action-primary">Save Changes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="" class="action action-cancel action-secondary">Cancel</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- notification: newer version exists -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-warning" id="notification-version" aria-hidden="true" role="dialog" aria-labelledby="notification-warning-title" aria-describedby="notification-warning-description">
|
||||
<div class="notification warning has-actions">
|
||||
@@ -404,10 +491,10 @@
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button save-button action-primary">Go to Newer Version</a>
|
||||
<a href="#" class="action action-save action-primary">Go to Newer Version</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button cancel-button action-secondary">Continue Editing</a>
|
||||
<a href="#" class="action action-cancel action-secondary">Continue Editing</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -428,10 +515,10 @@
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-primary">Yes, I want to Edit X</a>
|
||||
<a href="#" class="action action-proceed action-primary">Yes, I want to Edit X</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="action-secondary">No, I do not</a>
|
||||
<a href="#" class="action action-cancel action-secondary">No, I do not</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
% for field_name, field_value in editable_metadata_fields.items():
|
||||
<li>
|
||||
% if field_name == 'source_code':
|
||||
% if field_value['is_default'] is False:
|
||||
% if field_value['explicitly_set'] is True:
|
||||
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
|
||||
% endif
|
||||
% else:
|
||||
@@ -26,8 +26,10 @@
|
||||
% if False:
|
||||
<label>Help: ${field_value['field'].help}</label>
|
||||
<label>Type: ${type(field_value['field']).__name__}</label>
|
||||
<label>Inherited: ${field_value['is_inherited']}</label>
|
||||
<label>Default: ${field_value['is_default']}</label>
|
||||
<label>Inheritable: ${field_value['inheritable']}</label>
|
||||
<label>Showing inherited value: ${field_value['inheritable'] and not field_value['explicitly_set']}</label>
|
||||
<label>Explicitly set: ${field_value['explicitly_set']}</label>
|
||||
<label>Default value: ${field_value['default_value']}</label>
|
||||
% if field_value['field'].values:
|
||||
<label>Possible values:</label>
|
||||
% for value in field_value['field'].values:
|
||||
@@ -40,7 +42,7 @@
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
% if 'source_code' in editable_metadata_fields and not editable_metadata_fields['source_code']['is_default']:
|
||||
% if 'source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set']:
|
||||
<%include file="source-edit.html" />
|
||||
% endif
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
1
common/djangoapps/util/tests/__init__.py
Normal file
1
common/djangoapps/util/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
124
common/djangoapps/util/tests/test_memcache.py
Normal file
124
common/djangoapps/util/tests/test_memcache.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
-e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
|
||||
|
||||
# Our libraries:
|
||||
-e git+https://github.com/edx/XBlock.git@5ce6f70a#egg=XBlock
|
||||
-e git+https://github.com/edx/XBlock.git@483e0cb1#egg=XBlock
|
||||
|
||||
@@ -11,6 +11,7 @@ from util.cache import cache
|
||||
import datetime
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
import datetime
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -104,6 +105,25 @@ def peer_grading_notifications(course, user):
|
||||
|
||||
|
||||
def combined_notifications(course, user):
|
||||
"""
|
||||
Show notifications to a given user for a given course. Get notifications from the cache if possible,
|
||||
or from the grading controller server if not.
|
||||
@param course: The course object for which we are getting notifications
|
||||
@param user: The user object for which we are getting notifications
|
||||
@return: A dictionary with boolean pending_grading (true if there is pending grading), img_path (for notification
|
||||
image), and response (actual response from grading controller server).
|
||||
"""
|
||||
#Set up return values so that we can return them for error cases
|
||||
pending_grading = False
|
||||
img_path = ""
|
||||
notifications={}
|
||||
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications}
|
||||
|
||||
#We don't want to show anonymous users anything.
|
||||
if not user.is_authenticated():
|
||||
return notification_dict
|
||||
|
||||
#Define a mock modulesystem
|
||||
system = ModuleSystem(
|
||||
ajax_url=None,
|
||||
track_function=None,
|
||||
@@ -112,41 +132,44 @@ def combined_notifications(course, user):
|
||||
replace_urls=None,
|
||||
xblock_model_data= {}
|
||||
)
|
||||
#Initialize controller query service using our mock system
|
||||
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
|
||||
student_id = unique_id_for_user(user)
|
||||
user_is_staff = has_access(user, course, 'staff')
|
||||
course_id = course.id
|
||||
notification_type = "combined"
|
||||
|
||||
#See if we have a stored value in the cache
|
||||
success, notification_dict = get_value_from_cache(student_id, course_id, notification_type)
|
||||
if success:
|
||||
return notification_dict
|
||||
|
||||
min_time_to_query = user.last_login
|
||||
#Get the time of the last login of the user
|
||||
last_login = user.last_login
|
||||
|
||||
#Find the modules they have seen since they logged in
|
||||
last_module_seen = StudentModule.objects.filter(student=user, course_id=course_id,
|
||||
modified__gt=min_time_to_query).values('modified').order_by(
|
||||
modified__gt=last_login).values('modified').order_by(
|
||||
'-modified')
|
||||
last_module_seen_count = last_module_seen.count()
|
||||
|
||||
if last_module_seen_count > 0:
|
||||
#The last time they viewed an updated notification (last module seen minus how long notifications are cached)
|
||||
last_time_viewed = last_module_seen[0]['modified'] - datetime.timedelta(seconds=(NOTIFICATION_CACHE_TIME + 60))
|
||||
else:
|
||||
last_time_viewed = user.last_login
|
||||
#If they have not seen any modules since they logged in, then don't refresh
|
||||
return {'pending_grading': False, 'img_path': img_path, 'response': notifications}
|
||||
|
||||
pending_grading = False
|
||||
|
||||
img_path = ""
|
||||
try:
|
||||
#Get the notifications from the grading controller
|
||||
controller_response = controller_qs.check_combined_notifications(course.id, student_id, user_is_staff,
|
||||
last_time_viewed)
|
||||
log.debug(controller_response)
|
||||
notifications = json.loads(controller_response)
|
||||
if notifications['success']:
|
||||
if notifications['overall_need_to_check']:
|
||||
pending_grading = True
|
||||
except:
|
||||
#Non catastrophic error, so no real action
|
||||
notifications = {}
|
||||
#This is a dev_facing_error
|
||||
log.exception(
|
||||
"Problem with getting notifications from controller query service for course {0} user {1}.".format(
|
||||
@@ -157,6 +180,7 @@ def combined_notifications(course, user):
|
||||
|
||||
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications}
|
||||
|
||||
#Store the notifications in the cache
|
||||
set_value_in_cache(student_id, course_id, notification_type, notification_dict)
|
||||
|
||||
return notification_dict
|
||||
|
||||
@@ -64,6 +64,7 @@ CACHES = ENV_TOKENS['CACHES']
|
||||
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
|
||||
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
|
||||
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
|
||||
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
|
||||
|
||||
#Timezone overrides
|
||||
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
|
||||
|
||||
@@ -268,6 +268,7 @@ IGNORABLE_404_ENDS = ('favicon.ico')
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
DEFAULT_FROM_EMAIL = 'registration@edx.org'
|
||||
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
|
||||
SERVER_EMAIL = 'devops@edx.org'
|
||||
ADMINS = (
|
||||
('edX Admins', 'admin@edx.org'),
|
||||
)
|
||||
|
||||
@@ -37,6 +37,10 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs):
|
||||
else:
|
||||
response = requests.request(method, url, params=data_or_params, timeout=5)
|
||||
except Exception as err:
|
||||
# remove API key if it is in the params
|
||||
if 'api_key' in data_or_params:
|
||||
log.info('Deleting API key from params')
|
||||
del data_or_params['api_key']
|
||||
log.exception("Trying to call {method} on {url} with params {params}".format(
|
||||
method=method, url=url, params=data_or_params))
|
||||
# Reraise with a single exception type
|
||||
|
||||
Reference in New Issue
Block a user