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): ...@@ -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' # 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(): 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 if posted_metadata[metadata_key] is None:
# 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:
# remove both from passed in collection as well as the collection read in from the modulestore # remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module._model_data: if metadata_key in module._model_data:
del module._model_data[metadata_key] del module._model_data[metadata_key]
......
...@@ -678,11 +678,7 @@ def save_item(request): ...@@ -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' # 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(): 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 if posted_metadata[metadata_key] is None:
# 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:
# remove both from passed in collection as well as the collection read in from the modulestore # remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data: if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key] del existing_item._model_data[metadata_key]
......
...@@ -14,13 +14,14 @@ class CourseMetadata(object): ...@@ -14,13 +14,14 @@ class CourseMetadata(object):
The objects have no predefined attrs but instead are obj encodings of the The objects have no predefined attrs but instead are obj encodings of the
editable metadata. editable metadata.
''' '''
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', FILTERED_LIST = ['xml_attributes',
'end', 'start',
'enrollment_start', 'end',
'enrollment_end', 'enrollment_start',
'tabs', 'enrollment_end',
'graceperiod', 'tabs',
'checklists'] 'graceperiod',
'checklists']
@classmethod @classmethod
def fetch(cls, course_location): def fetch(cls, course_location):
......
...@@ -72,3 +72,14 @@ describe "CMS.Views.ModuleEdit", -> ...@@ -72,3 +72,14 @@ describe "CMS.Views.ModuleEdit", ->
it "loads the .xmodule-display inside the module editor", -> it "loads the .xmodule-display inside the module editor", ->
expect(XModule.loadModule).toHaveBeenCalled() expect(XModule.loadModule).toHaveBeenCalled()
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display')) 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 ...@@ -20,6 +20,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
loadEdit: -> loadEdit: ->
if not @module if not @module
@module = XModule.loadModule(@$el.find('.xmodule_edit')) @module = XModule.loadModule(@$el.find('.xmodule_edit'))
@originalMetadata = @metadata()
metadata: -> metadata: ->
# cdodge: package up metadata which is separated into a number of input fields # cdodge: package up metadata which is separated into a number of input fields
...@@ -35,6 +36,14 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -35,6 +36,14 @@ class CMS.Views.ModuleEdit extends Backbone.View
return _metadata return _metadata
changedMetadata: ->
currentMetadata = @metadata()
changedMetadata = {}
for key of currentMetadata
if currentMetadata[key] != @originalMetadata[key]
changedMetadata[key] = currentMetadata[key]
return changedMetadata
cloneTemplate: (parent, template) -> cloneTemplate: (parent, template) ->
$.post("/clone_item", { $.post("/clone_item", {
parent_location: parent parent_location: parent
...@@ -60,7 +69,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -60,7 +69,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
course: course_location_analytics course: course_location_analytics
id: _this.model.id id: _this.model.id
data.metadata = _.extend(data.metadata || {}, @metadata()) data.metadata = _.extend(data.metadata || {}, @changedMetadata())
@hideModal() @hideModal()
@model.save(data).done( => @model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3) # # showToastMessage("Your changes have been saved.", null, 3)
......
<% <%
import hashlib import hashlib
from xmodule.fields import StringyInteger, StringyFloat
hlskey = hashlib.md5(module.location.url()).hexdigest() hlskey = hashlib.md5(module.location.url()).hexdigest()
%> %>
<section class="metadata_edit"> <section class="metadata_edit">
...@@ -7,17 +8,40 @@ ...@@ -7,17 +8,40 @@
% 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':
<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: % else:
<label>${field_name}:</label> <label>${field_value['field'].display_name}:</label>
<input type='text' data-metadata-name='${field_name}' value='${field_value}' size='60' /> <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 % endif
</li> </li>
% endfor % endfor
</ul> </ul>
% if 'source_code' in editable_metadata_fields: % if 'source_code' in editable_metadata_fields and not editable_metadata_fields['source_code']['is_default']:
<%include file="source-edit.html" /> <%include file="source-edit.html" />
% endif % endif
</section> </section>
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
<form id="hls-form" enctype="multipart/form-data"> <form id="hls-form" enctype="multipart/form-data">
<section class="source-edit"> <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> </section>
<div class="submit"> <div class="submit">
<button type="reset" class="hls-compile">Save &amp; Compile to edX XML</button> <button type="reset" class="hls-compile">Save &amp; Compile to edX XML</button>
......
...@@ -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
......
...@@ -65,7 +65,8 @@ class CapaFields(object): ...@@ -65,7 +65,8 @@ class CapaFields(object):
max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings) 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) 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) 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) 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) rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
...@@ -882,16 +883,6 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -882,16 +883,6 @@ class CapaDescriptor(CapaFields, RawDescriptor):
'enable_markdown': self.markdown is not None}) 'enable_markdown': self.markdown is not None})
return _context 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] # VS[compat]
# TODO (cpennington): Delete this method once all fall 2012 course are being # TODO (cpennington): Delete this method once all fall 2012 course are being
# edited in the cms # edited in the cms
...@@ -901,3 +892,10 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -901,3 +892,10 @@ class CapaDescriptor(CapaFields, RawDescriptor):
'problems/' + path[8:], 'problems/' + path[8:],
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): ...@@ -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):
......
...@@ -37,3 +37,10 @@ class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawD ...@@ -37,3 +37,10 @@ class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawD
metadata_translations = dict(RawDescriptor.metadata_translations) metadata_translations = dict(RawDescriptor.metadata_translations)
metadata_translations['id'] = 'discussion_id' metadata_translations['id'] = 'discussion_id'
metadata_translations['for'] = 'discussion_target' 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") ...@@ -19,6 +19,7 @@ log = logging.getLogger("mitx.courseware")
class HtmlFields(object): class HtmlFields(object):
data = String(help="Html contents to display for this module", scope=Scope.content) 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): class HtmlModule(HtmlFields, XModule):
...@@ -166,16 +167,6 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): ...@@ -166,16 +167,6 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
elt.set("filename", relname) elt.set("filename", relname)
return elt 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): 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 ...@@ -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()
......
from .x_module import XModuleDescriptor, DescriptorSystem from .x_module import XModuleDescriptor, DescriptorSystem
from .modulestore.inheritance import own_metadata
class MakoDescriptorSystem(DescriptorSystem): class MakoDescriptorSystem(DescriptorSystem):
...@@ -34,20 +33,10 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -34,20 +33,10 @@ class MakoModuleDescriptor(XModuleDescriptor):
""" """
return { return {
'module': self, 'module': self,
'editable_metadata_fields': self.editable_metadata_fields, 'editable_metadata_fields': self.editable_metadata_fields
} }
def get_html(self): def get_html(self):
return self.system.render_template( return self.system.render_template(
self.mako_template, self.get_context()) 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 ...@@ -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)
......
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): ...@@ -82,7 +82,7 @@ class XModuleFields(object):
display_name = String( display_name = String(
help="Display name for this module", help="Display name for this module",
scope=Scope.settings, scope=Scope.settings,
default=None, default=None
) )
...@@ -334,12 +334,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -334,12 +334,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# (like a practice problem). # (like a practice problem).
has_score = False 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 # A list of descriptor attributes that must be equal for the descriptors to
# be equal # be equal
equality_attributes = ('_model_data', 'location') equality_attributes = ('_model_data', 'location')
...@@ -612,6 +606,48 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -612,6 +606,48 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
model_data=self._model_data, 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): class DescriptorSystem(object):
def __init__(self, load_item, resources_fs, error_tracker, **kwargs): def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
......
...@@ -84,7 +84,8 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -84,7 +84,8 @@ class XmlDescriptor(XModuleDescriptor):
Mixin class for standardized parsing of from xml 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 # Extension to append to filename paths
filename_extension = 'xml' filename_extension = 'xml'
...@@ -418,3 +419,9 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -418,3 +419,9 @@ class XmlDescriptor(XModuleDescriptor):
""" """
raise NotImplementedError( raise NotImplementedError(
"%s does not implement definition_to_xml" % self.__class__.__name__) "%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 Test for lms courseware app
''' '''
import logging import logging
import json import json
import time import time
import random import random
from urlparse import urlsplit, urlunsplit from urlparse import urlsplit, urlunsplit
from uuid import uuid4
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.test import TestCase from django.test import TestCase
...@@ -62,7 +62,7 @@ def mongo_store_config(data_dir): ...@@ -62,7 +62,7 @@ def mongo_store_config(data_dir):
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost', 'host': 'localhost',
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'modulestore', 'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir, 'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string',
} }
...@@ -81,7 +81,7 @@ def draft_mongo_store_config(data_dir): ...@@ -81,7 +81,7 @@ def draft_mongo_store_config(data_dir):
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost', 'host': 'localhost',
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'modulestore', 'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir, 'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string',
} }
...@@ -92,7 +92,7 @@ def draft_mongo_store_config(data_dir): ...@@ -92,7 +92,7 @@ def draft_mongo_store_config(data_dir):
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost', 'host': 'localhost',
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'modulestore', 'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir, 'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string',
} }
......
...@@ -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
......
...@@ -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
......
...@@ -149,7 +149,7 @@ ...@@ -149,7 +149,7 @@
} }
label { label {
color: #999; color: #646464;
&.field-error { &.field-error {
display: block; display: block;
......
...@@ -156,7 +156,7 @@ ...@@ -156,7 +156,7 @@
<div id="calculator_wrapper"> <div id="calculator_wrapper">
<form id="calculator"> <form id="calculator">
<div class="input-wrapper"> <div class="input-wrapper">
<input type="text" id="calculator_input" /> <input type="text" id="calculator_input" title="Calculator Input Field" />
<div class="help-wrapper"> <div class="help-wrapper">
<a href="#">Hints</a> <a href="#">Hints</a>
...@@ -176,8 +176,8 @@ ...@@ -176,8 +176,8 @@
</dl> </dl>
</div> </div>
</div> </div>
<input id="calculator_button" type="submit" value="="/> <input id="calculator_button" type="submit" title="Calculate" value="="/>
<input type="text" id="calculator_output" readonly /> <input type="text" id="calculator_output" title="Calculator Output Field" readonly />
</form> </form>
</div> </div>
......
...@@ -12,19 +12,19 @@ ...@@ -12,19 +12,19 @@
</div> </div>
<form id="pwd_reset_form" action="${reverse('password_reset')}" method="post" data-remote="true"> <form id="pwd_reset_form" action="${reverse('password_reset')}" method="post" data-remote="true">
<label for="id_email">E-mail address:</label> <label for="pwd_reset_email">E-mail address:</label>
<input id="id_email" type="email" name="email" maxlength="75" placeholder="Your E-mail"/> <input id="pwd_reset_email" type="email" name="email" maxlength="75" placeholder="Your E-mail"/>
<div class="submit"> <div class="submit">
<input type="submit" id="pwd_reset_button" value="Reset my password" /> <input type="submit" id="pwd_reset_button" value="Reset my password" />
</div> </div>
</form> </form>
</div> </div>
<div class="close-modal"> <a href="#" class="close-modal" title="Close Modal">
<div class="inner"> <div class="inner">
<p>&#10005;</p> <p>&#10005;</p>
</div> </div>
</div> </a>
</div> </div>
</section> </section>
...@@ -40,5 +40,10 @@ ...@@ -40,5 +40,10 @@
$('#pwd_error').stop().css("display", "block"); $('#pwd_error').stop().css("display", "block");
} }
}); });
// removing close link's default behavior
$('#login-modal .close-modal').click(function(e) {
e.preventDefault();
});
})(this) })(this)
</script> </script>
...@@ -9,14 +9,17 @@ ...@@ -9,14 +9,17 @@
</header> </header>
<form id="login_form" class="login_form" method="post" data-remote="true" action="/login"> <form id="login_form" class="login_form" method="post" data-remote="true" action="/login">
<label>E-mail</label> <label for="login_email">E-mail</label>
<input name="email" type="email"> <input id="login_email" type="email" name="email" placeholder="e.g. yourname@domain.com" />
<label>Password</label>
<input name="password" type="password"> <label for="login_password">Password</label>
<label class="remember-me"> <input id="login_password" type="password" name="password" placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;" />
<input name="remember" type="checkbox" value="true">
<label for="login_remember_me" class="remember-me">
<input id="login_remember_me" type="checkbox" name="remember" value="true" />
Remember me Remember me
</label> </label>
<div class="submit"> <div class="submit">
<input name="submit" type="submit" value="Access My Courses"> <input name="submit" type="submit" value="Access My Courses">
</div> </div>
...@@ -34,11 +37,11 @@ ...@@ -34,11 +37,11 @@
% endif % endif
</section> </section>
<div class="close-modal"> <a href="#" class="close-modal" title="Close Modal">
<div class="inner"> <div class="inner">
<p>&#10005;</p> <p>&#10005;</p>
</div> </div>
</div> </a>
</div> </div>
</section> </section>
...@@ -59,5 +62,10 @@ ...@@ -59,5 +62,10 @@
$('#login_error').html(json.value).stop().css("display", "block"); $('#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) })(this)
</script> </script>
...@@ -10,7 +10,8 @@ ...@@ -10,7 +10,8 @@
<li> <li>
<a class="seq_${item['type']} inactive progress-${item['progress_status']}" <a class="seq_${item['type']} inactive progress-${item['progress_status']}"
data-id="${item['id']}" data-id="${item['id']}"
data-element="${idx+1}"> data-element="${idx+1}"
href="javascript:void(0);">
<p>${item['title']}</p> <p>${item['title']}</p>
</a> </a>
</li> </li>
......
...@@ -20,27 +20,31 @@ ...@@ -20,27 +20,31 @@
<div class="input-group"> <div class="input-group">
% if has_extauth_info is UNDEFINED: % if has_extauth_info is UNDEFINED:
<label data-field="email">E-mail*</label> <label data-field="email" for="signup_email">E-mail *</label>
<input name="email" type="email" placeholder="eg. yourname@domain.com"> <input id="signup_email" type="email" name="email" placeholder="e.g. yourname@domain.com" required />
<label data-field="password">Password*</label>
<input name="password" type="password" placeholder="****"> <label data-field="password" for="signup_password">Password *</label>
<label data-field="username">Public Username*</label> <input id="signup_password" type="password" name="password" placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;" required />
<input name="username" type="text" placeholder="Shown on forums">
<label data-field="name">Full Name*</label> <label data-field="username" for="signup_username">Public Username *</label>
<input name="name" type="text" placeholder="For your certificate"> <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: % else:
<p><i>Welcome</i> ${extauth_email}</p><br/> <p><i>Welcome</i> ${extauth_email}</p><br/>
<p><i>Enter a public username:</i></p> <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 % endif
</div> </div>
<div class="input-group"> <div class="input-group">
<section class="citizenship"> <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"> <div class="input-wrapper">
<select name="level_of_education"> <select id="signup_ed_level" name="level_of_education">
<option value="">--</option> <option value="">--</option>
%for code, ed_level in UserProfile.LEVEL_OF_EDUCATION_CHOICES: %for code, ed_level in UserProfile.LEVEL_OF_EDUCATION_CHOICES:
<option value="${code}">${ed_level}</option> <option value="${code}">${ed_level}</option>
...@@ -50,9 +54,9 @@ ...@@ -50,9 +54,9 @@
</section> </section>
<section class="gender"> <section class="gender">
<label data-field="gender">Gender</label> <label data-field="gender" for="signup_gender">Gender</label>
<div class="input-wrapper"> <div class="input-wrapper">
<select name="gender"> <select id="signup_gender" name="gender">
<option value="">--</option> <option value="">--</option>
%for code, gender in UserProfile.GENDER_CHOICES: %for code, gender in UserProfile.GENDER_CHOICES:
<option value="${code}">${gender}</option> <option value="${code}">${gender}</option>
...@@ -62,9 +66,9 @@ ...@@ -62,9 +66,9 @@
</section> </section>
<section class="date-of-birth"> <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"> <div class="input-wrapper">
<select name="year_of_birth"> <select id="signup_birth_year" name="year_of_birth">
<option value="">--</option> <option value="">--</option>
%for year in UserProfile.VALID_YEARS: %for year in UserProfile.VALID_YEARS:
<option value="${year}">${year}</option> <option value="${year}">${year}</option>
...@@ -74,22 +78,23 @@ ...@@ -74,22 +78,23 @@
</div> </div>
</section> </section>
<label data-field="mailing_address">Mailing address</label> <label data-field="mailing_address" for="signup_mailing_address">Mailing address</label>
<textarea name="mailing_address"></textarea> <textarea id="signup_mailing_address" name="mailing_address"></textarea>
<label data-field="goals">Goals in signing up for edX</label>
<textarea name="goals"></textarea> <label data-field="goals" for="signup_goals">Goals in signing up for edX</label>
<textarea name="goals" id="signup_goals"></textarea>
</div> </div>
<div class="input-group"> <div class="input-group">
<label data-field="terms_of_service" class="terms-of-service"> <label data-field="terms_of_service" class="terms-of-service" for="signup_tos">
<input name="terms_of_service" type="checkbox" value="true"> <input id="signup_tos" name="terms_of_service" type="checkbox" value="true">
I agree to the I agree to the
<a href="${reverse('tos')}" target="_blank">Terms of Service</a>* <a href="${reverse('tos')}" target="_blank">Terms of Service</a>*
</label> </label>
<label data-field="honor_code" class="honor-code"> <label data-field="honor_code" class="honor-code" for="signup_honor">
<input name="honor_code" type="checkbox" value="true"> <input id="signup_honor" name="honor_code" type="checkbox" value="true">
I agree to the I agree to the
<a href="${reverse('honor')}" target="_blank">Honor Code</a>* <a href="${reverse('honor')}" target="_blank">Honor Code</a>*
</label> </label>
...@@ -110,11 +115,11 @@ ...@@ -110,11 +115,11 @@
</div> </div>
<div class="close-modal"> <a href="#" class="close-modal" title="Close Modal">
<div class="inner"> <div class="inner">
<p>&#10005;</p> <p>&#10005;</p>
</div> </div>
</div> </a>
</div> </div>
</section> </section>
...@@ -129,5 +134,10 @@ ...@@ -129,5 +134,10 @@
$("[data-field='"+json.field+"']").addClass('field-error') $("[data-field='"+json.field+"']").addClass('field-error')
} }
}); });
// removing close link's default behavior
$('#login-modal .close-modal').click(function(e) {
e.preventDefault();
});
})(this) })(this)
</script> </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