Commit c7197cd5 by Arthur Barrett

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

parents bf21211b ca1aeb5b
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
@include box-sizing(border-box); @include box-sizing(border-box);
.copy { .copy {
@include font-size(13); @extend .t-copy-sub2;
} }
} }
...@@ -184,12 +184,12 @@ ...@@ -184,12 +184,12 @@
} }
.action-primary { .action-primary {
@include font-size(13); @extend .t-action3;
font-weight: 600; font-weight: 600;
} }
.action-secondary { .action-secondary {
@include font-size(13); @extend .t-action3;
} }
} }
} }
...@@ -367,12 +367,12 @@ ...@@ -367,12 +367,12 @@
} }
.copy { .copy {
@include font-size(13); @extend .t-copy-sub2;
width: flex-grid(10, 12); width: flex-grid(10, 12);
color: $gray-l2; color: $gray-l2;
.title { .title {
@include font-size(14); @extend .t-title-4;
margin-bottom: 0; margin-bottom: 0;
color: $white; color: $white;
} }
...@@ -409,13 +409,13 @@ ...@@ -409,13 +409,13 @@
.action-primary { .action-primary {
@include blue-button(); @include blue-button();
@include font-size(13); @extend .t-action3;
border-color: $blue-d2; border-color: $blue-d2;
font-weight: 600; font-weight: 600;
} }
.action-secondary { .action-secondary {
@include font-size(13); @extend .t-action3;
} }
} }
...@@ -504,7 +504,7 @@ ...@@ -504,7 +504,7 @@
// adopted alerts // adopted alerts
.alert { .alert {
@include font-size(14); @extend .t-copy-sub2;
@include box-sizing(border-box); @include box-sizing(border-box);
@include clearfix(); @include clearfix();
margin: 0 auto; margin: 0 auto;
...@@ -530,7 +530,7 @@ ...@@ -530,7 +530,7 @@
} }
.copy { .copy {
@include font-size(13); @extend .t-copy-sub2;
width: flex-grid(10, 12); width: flex-grid(10, 12);
color: $gray-l2; color: $gray-l2;
...@@ -568,12 +568,12 @@ ...@@ -568,12 +568,12 @@
} }
.action-primary { .action-primary {
@include font-size(13); @extend .t-action3;
font-weight: 600; font-weight: 600;
} }
.action-secondary { .action-secondary {
@include font-size(13); @extend .t-action3;
} }
} }
} }
...@@ -730,7 +730,7 @@ body.uxdesign.alerts { ...@@ -730,7 +730,7 @@ body.uxdesign.alerts {
border-radius: 3px; border-radius: 3px;
background: #fbf6e1; background: #fbf6e1;
// background: #edbd3c; // background: #edbd3c;
font-size: 14px; @extend .t-copy-sub1;
@include clearfix; @include clearfix;
.alert-message { .alert-message {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// ==================== // ====================
// headings/titles // 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; color: $gray-d3;
} }
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
} }
.t-title-4 { .t-title-4 {
@include font-size(14);
} }
.t-title-5 { .t-title-5 {
...@@ -82,4 +82,4 @@ ...@@ -82,4 +82,4 @@
// misc // misc
.t-icon { .t-icon {
line-height: 0; line-height: 0;
} }
\ No newline at end of file
...@@ -114,6 +114,7 @@ ...@@ -114,6 +114,7 @@
<li><a href="#alert-announcement2" class="show-alert">Show Announcement</a></li> <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-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-activation" class="show-alert">Show Activiation</a></li>
<li><a href="#alert-threeActions" class="show-alert">Alert with three actions</a></li>
</ul> </ul>
</section> </section>
...@@ -129,6 +130,10 @@ ...@@ -129,6 +130,10 @@
<ul> <ul>
<li> <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="show-notification">Show Change Warning</a>
<a href="#notification-change" class="hide-notification">Hide Change Warning</a> <a href="#notification-change" class="hide-notification">Hide Change Warning</a>
</li> </li>
...@@ -151,6 +156,10 @@ ...@@ -151,6 +156,10 @@
<a href="#notification-help" class="show-notification">Show Help</a> <a href="#notification-help" class="show-notification">Show Help</a>
<a href="#notification-help" class="hide-notification">Hide Help</a> <a href="#notification-help" class="hide-notification">Hide Help</a>
</li> </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> </ul>
</section> </section>
...@@ -182,6 +191,33 @@ ...@@ -182,6 +191,33 @@
</%block> </%block>
<%block name="view_alerts"> <%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">&#x26A0;</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 --> <!-- alert: you're editing a draft -->
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-draft"> <div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-draft">
<div class="alert warning has-actions"> <div class="alert warning has-actions">
...@@ -196,10 +232,10 @@ ...@@ -196,10 +232,10 @@
<h3 class="sr">Alert Actions</h3> <h3 class="sr">Alert Actions</h3>
<ul> <ul>
<li class="nav-item"> <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>
<li class="nav-item"> <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> </li>
</ul> </ul>
</nav> </nav>
...@@ -220,10 +256,10 @@ ...@@ -220,10 +256,10 @@
<h3 class="sr">Alert Actions</h3> <h3 class="sr">Alert Actions</h3>
<ul> <ul>
<li class="nav-item"> <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>
<li class="nav-item"> <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> </li>
</ul> </ul>
</nav> </nav>
...@@ -297,7 +333,7 @@ ...@@ -297,7 +333,7 @@
<h3 class="sr">Alert Actions</h3> <h3 class="sr">Alert Actions</h3>
<ul> <ul>
<li class="nav-item"> <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> </li>
</ul> </ul>
</nav> </nav>
...@@ -367,13 +403,13 @@ ...@@ -367,13 +403,13 @@
<%block name="view_notifications"> <%block name="view_notifications">
<!-- notification: change has been made and a save is needed --> <!-- 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"> <div class="notification change has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-change">&#x1F4DD;</i> <i class="ss-icon ss-symbolicons-block icon icon-change">&#x1F4DD;</i>
<div class="copy"> <div class="copy">
<h2 class="title title-3">You've Made Some Changes</h2> <h2 class="title title-3" id="notification-change-title">You've Made Some Changes</h2>
<p class="message">Your changes will not take effect until you <strong>save your progress</strong>.</p> <p class="message" id="notification-change-description">Your changes will not take effect until you <strong>save your progress</strong>.</p>
</div> </div>
<nav class="nav-actions"> <nav class="nav-actions">
...@@ -390,6 +426,57 @@ ...@@ -390,6 +426,57 @@
</div> </div>
</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">&#x1F4DD;</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">&#x26A0;</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 --> <!-- 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="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"> <div class="notification warning has-actions">
...@@ -404,10 +491,10 @@ ...@@ -404,10 +491,10 @@
<h3 class="sr">Notification Actions</h3> <h3 class="sr">Notification Actions</h3>
<ul> <ul>
<li class="nav-item"> <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>
<li class="nav-item"> <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> </li>
</ul> </ul>
</nav> </nav>
...@@ -428,10 +515,10 @@ ...@@ -428,10 +515,10 @@
<h3 class="sr">Notification Actions</h3> <h3 class="sr">Notification Actions</h3>
<ul> <ul>
<li class="nav-item"> <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>
<li class="nav-item"> <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> </li>
</ul> </ul>
</nav> </nav>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
% for field_name, field_value in editable_metadata_fields.items(): % for field_name, field_value in editable_metadata_fields.items():
<li> <li>
% if field_name == 'source_code': % 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> <a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
% endif % endif
% else: % else:
...@@ -26,8 +26,10 @@ ...@@ -26,8 +26,10 @@
% if False: % if False:
<label>Help: ${field_value['field'].help}</label> <label>Help: ${field_value['field'].help}</label>
<label>Type: ${type(field_value['field']).__name__}</label> <label>Type: ${type(field_value['field']).__name__}</label>
<label>Inherited: ${field_value['is_inherited']}</label> <label>Inheritable: ${field_value['inheritable']}</label>
<label>Default: ${field_value['is_default']}</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: % if field_value['field'].values:
<label>Possible values:</label> <label>Possible values:</label>
% for value in field_value['field'].values: % for value in field_value['field'].values:
...@@ -40,7 +42,7 @@ ...@@ -40,7 +42,7 @@
% endfor % endfor
</ul> </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" /> <%include file="source-edit.html" />
% endif % endif
......
...@@ -8,15 +8,42 @@ import urllib ...@@ -8,15 +8,42 @@ import urllib
def fasthash(string): def fasthash(string):
m = hashlib.new("md4") """
m.update(string) Hashes `string` into a string representation of a 128-bit digest.
return m.hexdigest() """
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): 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.
`key`, `key_prefix`, and `version` can be numbers, strings, or unicode.
"""
# 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 len(safe_key) > 250: # If the total length is too long for memcache, hash it
safe_key = fasthash(safe_key) if len(combined) > 250:
combined = fasthash(combined)
return ":".join([key_prefix, str(version), safe_key]) # Return the result
return combined
"""
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
"""Tests for the util package""" """Tests for the Zendesk"""
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
......
...@@ -49,7 +49,10 @@ class _ZendeskApi(object): ...@@ -49,7 +49,10 @@ class _ZendeskApi(object):
settings.ZENDESK_USER, settings.ZENDESK_USER,
settings.ZENDESK_API_KEY, settings.ZENDESK_API_KEY,
use_api_token=True, 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): def create_ticket(self, ticket):
......
...@@ -203,9 +203,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -203,9 +203,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
def save_instance_data(self): def save_instance_data(self):
for attribute in self.student_attributes: for attribute in self.student_attributes:
child_attr = getattr(self.child_module, attribute) setattr(self, attribute, getattr(self.child_module, attribute))
if child_attr != getattr(self, attribute):
setattr(self, attribute, getattr(self.child_module, attribute))
class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
......
...@@ -8,20 +8,23 @@ class @PeerGrading ...@@ -8,20 +8,23 @@ class @PeerGrading
@use_single_location = @peer_grading_container.data('use-single-location') @use_single_location = @peer_grading_container.data('use-single-location')
@peer_grading_outer_container = $('.peer-grading-container') @peer_grading_outer_container = $('.peer-grading-container')
@ajax_url = @peer_grading_container.data('ajax-url') @ajax_url = @peer_grading_container.data('ajax-url')
@error_container = $('.error-container')
@error_container.toggle(not @error_container.is(':empty'))
@message_container = $('.message-container') if @use_single_location.toLowerCase() == "true"
@message_container.toggle(not @message_container.is(':empty')) #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'))
@problem_button = $('.problem-button') @message_container = $('.message-container')
@problem_button.click @show_results @message_container.toggle(not @message_container.is(':empty'))
@problem_list = $('.problem-list') @problem_button = $('.problem-button')
@construct_progress_bar() @problem_button.click @show_results
if @use_single_location @problem_list = $('.problem-list')
@activate_problem() @construct_progress_bar()
construct_progress_bar: () => construct_progress_bar: () =>
problems = @problem_list.find('tr').next() problems = @problem_list.find('tr').next()
......
...@@ -31,15 +31,22 @@ def inherit_metadata(descriptor, model_data): ...@@ -31,15 +31,22 @@ def inherit_metadata(descriptor, model_data):
Only metadata specified in self.inheritable_metadata will Only metadata specified in self.inheritable_metadata will
be inherited be inherited
""" """
# The inherited values that are actually being used.
if not hasattr(descriptor, '_inherited_metadata'): if not hasattr(descriptor, '_inherited_metadata'):
setattr(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 # Set all inheritable metadata from kwargs that are
# in self.inheritable_metadata and aren't already set in metadata # in self.inheritable_metadata and aren't already set in metadata
for attr in INHERITABLE_METADATA: for attr in INHERITABLE_METADATA:
if attr not in descriptor._model_data and attr in model_data: if attr in model_data:
descriptor._inherited_metadata[attr] = model_data[attr] descriptor._inheritable_metadata[attr] = model_data[attr]
descriptor._model_data[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): def own_metadata(module):
......
...@@ -11,7 +11,7 @@ from xmodule.raw_module import RawDescriptor ...@@ -11,7 +11,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo from .timeinfo import TimeInfo
from xblock.core import Object, Integer, Boolean, String, Scope 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 xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
from open_ended_grading_classes import combined_open_ended_rubric 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 ...@@ -28,14 +28,14 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
class PeerGradingFields(object): 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) default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION, link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
scope=Scope.settings) 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) 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) 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) scope=Scope.settings)
student_data_for_location = Object(help="Student data for a given peer grading problem.", student_data_for_location = Object(help="Student data for a given peer grading problem.",
scope=Scope.user_state) scope=Scope.user_state)
...@@ -93,9 +93,9 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -93,9 +93,9 @@ class PeerGradingModule(PeerGradingFields, XModule):
if not self.ajax_url.endswith("/"): if not self.ajax_url.endswith("/"):
self.ajax_url = self.ajax_url + "/" self.ajax_url = self.ajax_url + "/"
if not isinstance(self.max_grade, (int, long)): #StringyInteger could return None, so keep this check.
#This could result in an exception, but not wrapping in a try catch block so it moves up the stack if not isinstance(self.max_grade, int):
self.max_grade = int(self.max_grade) raise TypeError("max_grade needs to be an integer.")
def closed(self): def closed(self):
return self._closed(self.timeinfo) return self._closed(self.timeinfo)
......
...@@ -151,6 +151,10 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -151,6 +151,10 @@ class ImportTestCase(BaseCourseTestCase):
# Check that the child inherits due correctly # Check that the child inherits due correctly
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
self.assertEqual(child.lms.due, Date().from_json(v)) 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 # Now export and check things
resource_fs = MemoryFS() resource_fs = MemoryFS()
...@@ -184,6 +188,60 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -184,6 +188,60 @@ class ImportTestCase(BaseCourseTestCase):
self.assertEqual(chapter_xml.tag, 'chapter') self.assertEqual(chapter_xml.tag, 'chapter')
self.assertFalse('due' in chapter_xml.attrib) 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): def test_is_pointer_tag(self):
""" """
Check that is_pointer_tag works properly. Check that is_pointer_tag works properly.
......
...@@ -9,13 +9,13 @@ from mock import Mock ...@@ -9,13 +9,13 @@ from mock import Mock
class TestFields(object): class TestFields(object):
# Will be returned by editable_metadata_fields. # 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. # Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
due = Date(scope=Scope.settings) due = Date(scope=Scope.settings)
# Will not be returned by editable_metadata_fields because is not Scope.settings. # Will not be returned by editable_metadata_fields because is not Scope.settings.
student_answers = Object(scope=Scope.user_state) student_answers = Object(scope=Scope.user_state)
# Will be returned, and can override the inherited value from XModule. # 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): class EditableMetadataFieldsTest(unittest.TestCase):
...@@ -25,27 +25,45 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -25,27 +25,45 @@ class EditableMetadataFieldsTest(unittest.TestCase):
# Tests that the xblock fields (currently tags and name) get filtered out. # Tests that the xblock fields (currently tags and name) get filtered out.
# Also tests that xml_attributes is filtered out of XmlDescriptor. # 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.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): 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'}) editable_fields = self.get_xml_editable_fields({'display_name': 'foo'})
display_name = editable_fields['display_name'] self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
self.assertFalse(display_name['is_default']) explicitly_set=True, inheritable=False, value='foo', default_value=None)
self.assertEqual('foo', display_name['value'])
def test_additional_field(self): 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.assertEqual(2, len(editable_fields))
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts, False, False, 7) self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
self.assert_display_name_default(editable_fields) 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({}) editable_fields = self.get_descriptor({}).editable_metadata_fields
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts, True, False, None) 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): def test_inherited_field(self):
editable_fields = self.get_module_editable_fields({'display_name' : 'inherited'}) model_val = {'display_name' : 'inherited'}
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name, False, True, '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 # Start of helper methods
def get_xml_editable_fields(self, model_data): def get_xml_editable_fields(self, model_data):
...@@ -53,7 +71,7 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -53,7 +71,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
return XmlDescriptor(system=system, location=None, model_data=model_data).editable_metadata_fields 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): class TestModuleDescriptor(TestFields, XmlDescriptor):
@property @property
...@@ -64,16 +82,12 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -64,16 +82,12 @@ class EditableMetadataFieldsTest(unittest.TestCase):
system = test_system() system = test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
descriptor = TestModuleDescriptor(system=system, location=None, model_data=model_data) return TestModuleDescriptor(system=system, location=None, model_data=model_data)
descriptor._inherited_metadata = {'display_name' : 'inherited'}
return descriptor.editable_metadata_fields
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] test_field = editable_fields[name]
self.assertEqual(field, test_field['field']) self.assertEqual(field, test_field['field'])
self.assertEqual(is_default, test_field['is_default']) self.assertEqual(explicitly_set, test_field['explicitly_set'])
self.assertEqual(is_inherited, test_field['is_inherited']) self.assertEqual(inheritable, test_field['inheritable'])
self.assertEqual(value, test_field['value']) self.assertEqual(value, test_field['value'])
self.assertEqual(default_value, test_field['default_value'])
...@@ -624,27 +624,28 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -624,27 +624,28 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
Can be limited by extending `non_editable_metadata_fields`. Can be limited by extending `non_editable_metadata_fields`.
""" """
inherited_metadata = getattr(self, '_inherited_metadata', {}) inherited_metadata = getattr(self, '_inherited_metadata', {})
inheritable_metadata = getattr(self, '_inheritable_metadata', {})
metadata = {} metadata = {}
for field in self.fields: for field in self.fields:
if field.scope != Scope.settings or field in self.non_editable_metadata_fields: if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
continue continue
inherited = False inheritable = False
default = False
value = getattr(self, field.name) value = getattr(self, field.name)
if field.name in self._model_data: default_value = field.default
default = False 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 field.name in inherited_metadata:
if self._model_data.get(field.name) == inherited_metadata.get(field.name): explicitly_set = False
inherited = True
else:
default = True
metadata[field.name] = {'field': field, metadata[field.name] = {'field': field,
'value': value, 'value': value,
'is_inherited': inherited, 'default_value': default_value,
'is_default': default} 'inheritable': inheritable,
'explicitly_set': explicitly_set }
return metadata return metadata
......
...@@ -8,4 +8,4 @@ ...@@ -8,4 +8,4 @@
-e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk -e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# Our libraries: # 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 ...@@ -11,6 +11,7 @@ from util.cache import cache
import datetime import datetime
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
import datetime
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -104,6 +105,25 @@ def peer_grading_notifications(course, user): ...@@ -104,6 +105,25 @@ def peer_grading_notifications(course, user):
def combined_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( system = ModuleSystem(
ajax_url=None, ajax_url=None,
track_function=None, track_function=None,
...@@ -112,41 +132,44 @@ def combined_notifications(course, user): ...@@ -112,41 +132,44 @@ def combined_notifications(course, user):
replace_urls=None, replace_urls=None,
xblock_model_data= {} xblock_model_data= {}
) )
#Initialize controller query service using our mock system
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system) controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
student_id = unique_id_for_user(user) student_id = unique_id_for_user(user)
user_is_staff = has_access(user, course, 'staff') user_is_staff = has_access(user, course, 'staff')
course_id = course.id course_id = course.id
notification_type = "combined" 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) success, notification_dict = get_value_from_cache(student_id, course_id, notification_type)
if success: if success:
return notification_dict 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, 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') '-modified')
last_module_seen_count = last_module_seen.count() last_module_seen_count = last_module_seen.count()
if last_module_seen_count > 0: 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)) last_time_viewed = last_module_seen[0]['modified'] - datetime.timedelta(seconds=(NOTIFICATION_CACHE_TIME + 60))
else: 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: try:
#Get the notifications from the grading controller
controller_response = controller_qs.check_combined_notifications(course.id, student_id, user_is_staff, controller_response = controller_qs.check_combined_notifications(course.id, student_id, user_is_staff,
last_time_viewed) last_time_viewed)
log.debug(controller_response)
notifications = json.loads(controller_response) notifications = json.loads(controller_response)
if notifications['success']: if notifications['success']:
if notifications['overall_need_to_check']: if notifications['overall_need_to_check']:
pending_grading = True pending_grading = True
except: except:
#Non catastrophic error, so no real action #Non catastrophic error, so no real action
notifications = {}
#This is a dev_facing_error #This is a dev_facing_error
log.exception( log.exception(
"Problem with getting notifications from controller query service for course {0} user {1}.".format( "Problem with getting notifications from controller query service for course {0} user {1}.".format(
...@@ -157,6 +180,7 @@ def combined_notifications(course, user): ...@@ -157,6 +180,7 @@ def combined_notifications(course, user):
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications} 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) set_value_in_cache(student_id, course_id, notification_type, notification_dict)
return notification_dict return notification_dict
......
...@@ -64,6 +64,7 @@ CACHES = ENV_TOKENS['CACHES'] ...@@ -64,6 +64,7 @@ CACHES = ENV_TOKENS['CACHES']
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL) DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL) DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS) ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
#Timezone overrides #Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
......
...@@ -268,6 +268,7 @@ IGNORABLE_404_ENDS = ('favicon.ico') ...@@ -268,6 +268,7 @@ IGNORABLE_404_ENDS = ('favicon.ico')
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'registration@edx.org' DEFAULT_FROM_EMAIL = 'registration@edx.org'
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org' DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
SERVER_EMAIL = 'devops@edx.org'
ADMINS = ( ADMINS = (
('edX Admins', 'admin@edx.org'), ('edX Admins', 'admin@edx.org'),
) )
......
...@@ -37,6 +37,10 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs): ...@@ -37,6 +37,10 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs):
else: else:
response = requests.request(method, url, params=data_or_params, timeout=5) response = requests.request(method, url, params=data_or_params, timeout=5)
except Exception as err: 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( log.exception("Trying to call {method} on {url} with params {params}".format(
method=method, url=url, params=data_or_params)) method=method, url=url, params=data_or_params))
# Reraise with a single exception type # Reraise with a single exception type
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment