Merge branch 'master' of github.com:edx/mitx into feature/cdodge/autoprovision-forums-master
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<%
|
||||
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 & 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.
|
||||
|
||||
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
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
5
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
5
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
@@ -1 +1,4 @@
|
||||
# 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
|
||||
|
||||
2
common/lib/xmodule/xmodule/js/src/capa/.gitignore
vendored
Normal file
2
common/lib/xmodule/xmodule/js/src/capa/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
!imageinput.js
|
||||
!schematic.js
|
||||
1
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!*.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'))
|
||||
|
||||
@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()
|
||||
|
||||
1
common/lib/xmodule/xmodule/js/src/poll/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/poll/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!*.js
|
||||
1
common/lib/xmodule/xmodule/js/src/sequence/display/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/sequence/display/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!*.js
|
||||
1
common/lib/xmodule/xmodule/js/src/videoalpha/display/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/videoalpha/display/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!html5_video.js
|
||||
@@ -1,5 +1,4 @@
|
||||
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)
|
||||
|
||||
79
common/lib/xmodule/xmodule/tests/test_xml_module.py
Normal file
79
common/lib/xmodule/xmodule/tests/test_xml_module.py
Normal file
@@ -0,0 +1,79 @@
|
||||
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
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'''
|
||||
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
|
||||
#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
|
||||
|
||||
@@ -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>✕</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="••••••••" />
|
||||
|
||||
<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>✕</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="••••••••" 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>✕</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>
|
||||
|
||||
Reference in New Issue
Block a user