Commit d8d4111a by Chris Dodge

Merge branch 'master' of github.com:edx/mitx into feature/cdodge/autoprovision-forums-master

parents 3c747f0e d522b53f
......@@ -75,11 +75,7 @@ def set_module_info(store, location, post_data):
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module._model_data:
del module._model_data[metadata_key]
......
......@@ -678,11 +678,7 @@ def save_item(request):
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in existing_item.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
......
......@@ -14,13 +14,14 @@ class CourseMetadata(object):
The objects have no predefined attrs but instead are obj encodings of the
editable metadata.
'''
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start',
'end',
'enrollment_start',
'enrollment_end',
'tabs',
'graceperiod',
'checklists']
FILTERED_LIST = ['xml_attributes',
'start',
'end',
'enrollment_start',
'enrollment_end',
'tabs',
'graceperiod',
'checklists']
@classmethod
def fetch(cls, course_location):
......
......@@ -72,3 +72,14 @@ describe "CMS.Views.ModuleEdit", ->
it "loads the .xmodule-display inside the module editor", ->
expect(XModule.loadModule).toHaveBeenCalled()
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display'))
describe "changedMetadata", ->
it "returns empty if no metadata loaded", ->
expect(@moduleEdit.changedMetadata()).toEqual({})
it "returns only changed values", ->
@moduleEdit.originalMetadata = {'foo', 'bar'}
spyOn(@moduleEdit, 'metadata').andReturn({'a': '', 'b': 'before', 'c': ''})
@moduleEdit.loadEdit()
@moduleEdit.metadata.andReturn({'a': '', 'b': 'after', 'd': 'only_after'})
expect(@moduleEdit.changedMetadata()).toEqual({'b' : 'after', 'd' : 'only_after'})
......@@ -20,6 +20,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
loadEdit: ->
if not @module
@module = XModule.loadModule(@$el.find('.xmodule_edit'))
@originalMetadata = @metadata()
metadata: ->
# cdodge: package up metadata which is separated into a number of input fields
......@@ -35,6 +36,14 @@ class CMS.Views.ModuleEdit extends Backbone.View
return _metadata
changedMetadata: ->
currentMetadata = @metadata()
changedMetadata = {}
for key of currentMetadata
if currentMetadata[key] != @originalMetadata[key]
changedMetadata[key] = currentMetadata[key]
return changedMetadata
cloneTemplate: (parent, template) ->
$.post("/clone_item", {
parent_location: parent
......@@ -60,7 +69,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
course: course_location_analytics
id: _this.model.id
data.metadata = _.extend(data.metadata || {}, @metadata())
data.metadata = _.extend(data.metadata || {}, @changedMetadata())
@hideModal()
@model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3)
......
<%
import hashlib
from xmodule.fields import StringyInteger, StringyFloat
hlskey = hashlib.md5(module.location.url()).hexdigest()
%>
<section class="metadata_edit">
......@@ -7,17 +8,40 @@
% for field_name, field_value in editable_metadata_fields.items():
<li>
% if field_name == 'source_code':
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
% if field_value['is_default'] is False:
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
% endif
% else:
<label>${field_name}:</label>
<input type='text' data-metadata-name='${field_name}' value='${field_value}' size='60' />
<label>${field_value['field'].display_name}:</label>
<input type='text' data-metadata-name='${field_value["field"].display_name}'
## This is a hack to keep current behavior for weight and attempts (empty will parse OK as unset).
## This hack will go away with our custom editors.
% if field_value["value"] == None and (isinstance(field_value["field"], StringyFloat) or isinstance(field_value["field"], StringyInteger)):
value = ''
% else:
value='${field_value["field"].to_json(field_value["value"])}'
% endif
size='60' />
## Change to True to see all the information being passed through.
% 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>
% if field_value['field'].values:
<label>Possible values:</label>
% for value in field_value['field'].values:
<label>${value}</label>
% endfor
% endif
% endif
% endif
</li>
% endfor
</ul>
% if 'source_code' in editable_metadata_fields:
<%include file="source-edit.html" />
% if 'source_code' in editable_metadata_fields and not editable_metadata_fields['source_code']['is_default']:
<%include file="source-edit.html" />
% endif
</section>
......@@ -12,7 +12,7 @@
<form id="hls-form" enctype="multipart/form-data">
<section class="source-edit">
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${editable_metadata_fields['source_code']|h}</textarea>
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${editable_metadata_fields['source_code']['value']|h}</textarea>
</section>
<div class="submit">
<button type="reset" class="hls-compile">Save &amp; Compile to edX XML</button>
......
......@@ -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.
`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:
safe_key = fasthash(safe_key)
# If the total length is too long for memcache, hash it
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.contrib.auth.models import AnonymousUser
......
......@@ -65,7 +65,8 @@ class CapaFields(object):
max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
due = Date(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed",
values=["answered", "always", "attempted", "closed", "never"])
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content)
......@@ -882,16 +883,6 @@ class CapaDescriptor(CapaFields, RawDescriptor):
'enable_markdown': self.markdown is not None})
return _context
@property
def editable_metadata_fields(self):
"""Remove metadata from the editable fields since it has its own editor"""
subset = super(CapaDescriptor, self).editable_metadata_fields
if 'markdown' in subset:
del subset['markdown']
if 'empty' in subset:
del subset['empty']
return subset
# VS[compat]
# TODO (cpennington): Delete this method once all fall 2012 course are being
# edited in the cms
......@@ -901,3 +892,10 @@ class CapaDescriptor(CapaFields, RawDescriptor):
'problems/' + path[8:],
path[8:],
]
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(CapaDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([CapaDescriptor.due, CapaDescriptor.graceperiod,
CapaDescriptor.force_save_button, CapaDescriptor.markdown])
return non_editable_fields
......@@ -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):
......
......@@ -37,3 +37,10 @@ class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawD
metadata_translations = dict(RawDescriptor.metadata_translations)
metadata_translations['id'] = 'discussion_id'
metadata_translations['for'] = 'discussion_target'
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(DiscussionDescriptor, self).non_editable_metadata_fields
# We may choose to enable sort_keys in the future, but while Kevin is investigating....
non_editable_fields.extend([DiscussionDescriptor.discussion_id, DiscussionDescriptor.sort_key])
return non_editable_fields
......@@ -19,6 +19,7 @@ log = logging.getLogger("mitx.courseware")
class HtmlFields(object):
data = String(help="Html contents to display for this module", scope=Scope.content)
source_code = String(help="Source code for LaTeX documents. This feature is not well-supported.", scope=Scope.settings)
class HtmlModule(HtmlFields, XModule):
......@@ -166,16 +167,6 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
elt.set("filename", relname)
return elt
@property
def editable_metadata_fields(self):
"""Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user."""
subset = super(HtmlDescriptor, self).editable_metadata_fields
if 'empty' in subset:
del subset['empty']
return subset
class AboutDescriptor(HtmlDescriptor):
"""
......
# Please do not ignore *.js files. Some xmodules are written in JS.
# Ignore .js files in this folder as they are compiled from coffeescript
# For each of the xmodules subdirectories, add a .gitignore file that
# will version any *.js file that is specifically written, not compiled.
*.js
......@@ -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'))
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'))
@problem_button = $('.problem-button')
@problem_button.click @show_results
@message_container = $('.message-container')
@message_container.toggle(not @message_container.is(':empty'))
@problem_list = $('.problem-list')
@construct_progress_bar()
@problem_button = $('.problem-button')
@problem_button.click @show_results
if @use_single_location
@activate_problem()
@problem_list = $('.problem-list')
@construct_progress_bar()
construct_progress_bar: () =>
problems = @problem_list.find('tr').next()
......
from .x_module import XModuleDescriptor, DescriptorSystem
from .modulestore.inheritance import own_metadata
class MakoDescriptorSystem(DescriptorSystem):
......@@ -34,20 +33,10 @@ class MakoModuleDescriptor(XModuleDescriptor):
"""
return {
'module': self,
'editable_metadata_fields': self.editable_metadata_fields,
'editable_metadata_fields': self.editable_metadata_fields
}
def get_html(self):
return self.system.render_template(
self.mako_template, self.get_context())
# cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata)
@property
def editable_metadata_fields(self):
fields = {}
for field, value in own_metadata(self).items():
if field in self.system_metadata_fields:
continue
fields[field] = value
return fields
......@@ -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)
......
from xmodule.x_module import XModuleFields
from xblock.core import Scope, String, Object
from xmodule.fields import Date, StringyInteger
from xmodule.xml_module import XmlDescriptor
import unittest
from . import test_system
from mock import Mock
class TestFields(object):
# Will be returned by editable_metadata_fields.
max_attempts = StringyInteger(scope=Scope.settings)
# 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)
class EditableMetadataFieldsTest(unittest.TestCase):
def test_display_name_field(self):
editable_fields = self.get_xml_editable_fields({})
# 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)
def test_override_default(self):
# Tests that is_default is correct when a value overrides the default.
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'])
def test_additional_field(self):
editable_fields = self.get_module_editable_fields({'max_attempts' : '7'})
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)
editable_fields = self.get_module_editable_fields({})
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts, True, False, None)
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')
# Start of helper methods
def get_xml_editable_fields(self, model_data):
system = test_system()
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):
class TestModuleDescriptor(TestFields, XmlDescriptor):
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(TestModuleDescriptor, self).non_editable_metadata_fields
non_editable_fields.append(TestModuleDescriptor.due)
return non_editable_fields
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
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):
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(value, test_field['value'])
......@@ -82,7 +82,7 @@ class XModuleFields(object):
display_name = String(
help="Display name for this module",
scope=Scope.settings,
default=None,
default=None
)
......@@ -334,12 +334,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# (like a practice problem).
has_score = False
# cdodge: this is a list of metadata names which are 'system' metadata
# and should not be edited by an end-user
system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft',
'discussion_id', 'xml_attributes']
# A list of descriptor attributes that must be equal for the descriptors to
# be equal
equality_attributes = ('_model_data', 'location')
......@@ -612,6 +606,48 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
model_data=self._model_data,
))
@property
def non_editable_metadata_fields(self):
"""
Return the list of fields that should not be editable in Studio.
When overriding, be sure to append to the superclasses' list.
"""
# We are not allowing editing of xblock tag and name fields at this time (for any component).
return [XBlock.tags, XBlock.name]
@property
def editable_metadata_fields(self):
"""
Returns the metadata fields to be edited in Studio. These are fields with scope `Scope.settings`.
Can be limited by extending `non_editable_metadata_fields`.
"""
inherited_metadata = getattr(self, '_inherited_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
value = getattr(self, field.name)
if field.name in self._model_data:
default = False
if field.name in inherited_metadata:
if self._model_data.get(field.name) == inherited_metadata.get(field.name):
inherited = True
else:
default = True
metadata[field.name] = {'field': field,
'value': value,
'is_inherited': inherited,
'is_default': default}
return metadata
class DescriptorSystem(object):
def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
......
......@@ -84,7 +84,8 @@ class XmlDescriptor(XModuleDescriptor):
Mixin class for standardized parsing of from xml
"""
xml_attributes = Object(help="Map of unhandled xml attributes, used only for storage between import and export", default={}, scope=Scope.settings)
xml_attributes = Object(help="Map of unhandled xml attributes, used only for storage between import and export",
default={}, scope=Scope.settings)
# Extension to append to filename paths
filename_extension = 'xml'
......@@ -418,3 +419,9 @@ class XmlDescriptor(XModuleDescriptor):
"""
raise NotImplementedError(
"%s does not implement definition_to_xml" % self.__class__.__name__)
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(XmlDescriptor, self).non_editable_metadata_fields
non_editable_fields.append(XmlDescriptor.xml_attributes)
return non_editable_fields
'''
Test for lms courseware app
'''
import logging
import json
import time
import random
from urlparse import urlsplit, urlunsplit
from uuid import uuid4
from django.contrib.auth.models import User, Group
from django.test import TestCase
......@@ -62,7 +62,7 @@ def mongo_store_config(data_dir):
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
......@@ -81,7 +81,7 @@ def draft_mongo_store_config(data_dir):
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
......@@ -92,7 +92,7 @@ def draft_mongo_store_config(data_dir):
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
......
......@@ -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
pending_grading = False
#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}
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
......
......@@ -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
......
......@@ -149,7 +149,7 @@
}
label {
color: #999;
color: #646464;
&.field-error {
display: block;
......
......@@ -156,7 +156,7 @@
<div id="calculator_wrapper">
<form id="calculator">
<div class="input-wrapper">
<input type="text" id="calculator_input" />
<input type="text" id="calculator_input" title="Calculator Input Field" />
<div class="help-wrapper">
<a href="#">Hints</a>
......@@ -176,8 +176,8 @@
</dl>
</div>
</div>
<input id="calculator_button" type="submit" value="="/>
<input type="text" id="calculator_output" readonly />
<input id="calculator_button" type="submit" title="Calculate" value="="/>
<input type="text" id="calculator_output" title="Calculator Output Field" readonly />
</form>
</div>
......
......@@ -12,19 +12,19 @@
</div>
<form id="pwd_reset_form" action="${reverse('password_reset')}" method="post" data-remote="true">
<label for="id_email">E-mail address:</label>
<input id="id_email" type="email" name="email" maxlength="75" placeholder="Your E-mail"/>
<label for="pwd_reset_email">E-mail address:</label>
<input id="pwd_reset_email" type="email" name="email" maxlength="75" placeholder="Your E-mail"/>
<div class="submit">
<input type="submit" id="pwd_reset_button" value="Reset my password" />
</div>
</form>
</div>
<div class="close-modal">
<a href="#" class="close-modal" title="Close Modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</a>
</div>
</section>
......@@ -40,5 +40,10 @@
$('#pwd_error').stop().css("display", "block");
}
});
// removing close link's default behavior
$('#login-modal .close-modal').click(function(e) {
e.preventDefault();
});
})(this)
</script>
......@@ -9,14 +9,17 @@
</header>
<form id="login_form" class="login_form" method="post" data-remote="true" action="/login">
<label>E-mail</label>
<input name="email" type="email">
<label>Password</label>
<input name="password" type="password">
<label class="remember-me">
<input name="remember" type="checkbox" value="true">
<label for="login_email">E-mail</label>
<input id="login_email" type="email" name="email" placeholder="e.g. yourname@domain.com" />
<label for="login_password">Password</label>
<input id="login_password" type="password" name="password" placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;" />
<label for="login_remember_me" class="remember-me">
<input id="login_remember_me" type="checkbox" name="remember" value="true" />
Remember me
</label>
<div class="submit">
<input name="submit" type="submit" value="Access My Courses">
</div>
......@@ -34,11 +37,11 @@
% endif
</section>
<div class="close-modal">
<a href="#" class="close-modal" title="Close Modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</a>
</div>
</section>
......@@ -59,5 +62,10 @@
$('#login_error').html(json.value).stop().css("display", "block");
}
});
// removing close link's default behavior
$('#login-modal .close-modal').click(function(e) {
e.preventDefault();
});
})(this)
</script>
......@@ -10,7 +10,8 @@
<li>
<a class="seq_${item['type']} inactive progress-${item['progress_status']}"
data-id="${item['id']}"
data-element="${idx+1}">
data-element="${idx+1}"
href="javascript:void(0);">
<p>${item['title']}</p>
</a>
</li>
......
......@@ -20,27 +20,31 @@
<div class="input-group">
% if has_extauth_info is UNDEFINED:
<label data-field="email">E-mail*</label>
<input name="email" type="email" placeholder="eg. yourname@domain.com">
<label data-field="password">Password*</label>
<input name="password" type="password" placeholder="****">
<label data-field="username">Public Username*</label>
<input name="username" type="text" placeholder="Shown on forums">
<label data-field="name">Full Name*</label>
<input name="name" type="text" placeholder="For your certificate">
<label data-field="email" for="signup_email">E-mail *</label>
<input id="signup_email" type="email" name="email" placeholder="e.g. yourname@domain.com" required />
<label data-field="password" for="signup_password">Password *</label>
<input id="signup_password" type="password" name="password" placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;" required />
<label data-field="username" for="signup_username">Public Username *</label>
<input id="signup_username" type="text" name="username" placeholder="e.g. yourname (shown on forums)" required />
<label data-field="name" for="signup_fullname">Full Name *</label>
<input id="signup_fullname" type="text" name="name" placeholder="e.g. Your Name (for certificates)" required />
% else:
<p><i>Welcome</i> ${extauth_email}</p><br/>
<p><i>Enter a public username:</i></p>
<label data-field="username">Public Username*</label>
<input name="username" type="text" value="${extauth_username}" placeholder="Shown on forums">
<label data-field="username" for="signup_username">Public Username *</label>
<input id="signup_username" type="text" name="username" value="${extauth_username}" placeholder="e.g. yourname (shown on forums)" required />
% endif
</div>
<div class="input-group">
<section class="citizenship">
<label data-field="level_of_education">Ed. completed</label>
<label data-field="level_of_education" for="signup_ed_level">Ed. Completed</label>
<div class="input-wrapper">
<select name="level_of_education">
<select id="signup_ed_level" name="level_of_education">
<option value="">--</option>
%for code, ed_level in UserProfile.LEVEL_OF_EDUCATION_CHOICES:
<option value="${code}">${ed_level}</option>
......@@ -50,9 +54,9 @@
</section>
<section class="gender">
<label data-field="gender">Gender</label>
<label data-field="gender" for="signup_gender">Gender</label>
<div class="input-wrapper">
<select name="gender">
<select id="signup_gender" name="gender">
<option value="">--</option>
%for code, gender in UserProfile.GENDER_CHOICES:
<option value="${code}">${gender}</option>
......@@ -62,9 +66,9 @@
</section>
<section class="date-of-birth">
<label data-field="date-of-birth">Year of birth</label>
<label data-field="date-of-birth" for="signup_birth_year">Year of birth</label>
<div class="input-wrapper">
<select name="year_of_birth">
<select id="signup_birth_year" name="year_of_birth">
<option value="">--</option>
%for year in UserProfile.VALID_YEARS:
<option value="${year}">${year}</option>
......@@ -74,22 +78,23 @@
</div>
</section>
<label data-field="mailing_address">Mailing address</label>
<textarea name="mailing_address"></textarea>
<label data-field="goals">Goals in signing up for edX</label>
<textarea name="goals"></textarea>
<label data-field="mailing_address" for="signup_mailing_address">Mailing address</label>
<textarea id="signup_mailing_address" name="mailing_address"></textarea>
<label data-field="goals" for="signup_goals">Goals in signing up for edX</label>
<textarea name="goals" id="signup_goals"></textarea>
</div>
<div class="input-group">
<label data-field="terms_of_service" class="terms-of-service">
<input name="terms_of_service" type="checkbox" value="true">
<label data-field="terms_of_service" class="terms-of-service" for="signup_tos">
<input id="signup_tos" name="terms_of_service" type="checkbox" value="true">
I agree to the
<a href="${reverse('tos')}" target="_blank">Terms of Service</a>*
</label>
<label data-field="honor_code" class="honor-code">
<input name="honor_code" type="checkbox" value="true">
<label data-field="honor_code" class="honor-code" for="signup_honor">
<input id="signup_honor" name="honor_code" type="checkbox" value="true">
I agree to the
<a href="${reverse('honor')}" target="_blank">Honor Code</a>*
</label>
......@@ -110,11 +115,11 @@
</div>
<div class="close-modal">
<a href="#" class="close-modal" title="Close Modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</a>
</div>
</section>
......@@ -129,5 +134,10 @@
$("[data-field='"+json.field+"']").addClass('field-error')
}
});
// removing close link's default behavior
$('#login-modal .close-modal').click(function(e) {
e.preventDefault();
});
})(this)
</script>
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