Commit 14eb9c9a by cahrens

Merge branch 'master' into feature/christina/metadata-ui

parents 5d41e2a9 5118b06e
...@@ -26,7 +26,7 @@ class LMSLinksTestCase(TestCase): ...@@ -26,7 +26,7 @@ class LMSLinksTestCase(TestCase):
link = utils.get_lms_link_for_item(location, True) link = utils.get_lms_link_for_item(location, True)
self.assertEquals( self.assertEquals(
link, link,
"//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us" "//preview/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us"
) )
......
...@@ -88,7 +88,7 @@ def get_lms_link_for_item(location, preview=False, course_id=None): ...@@ -88,7 +88,7 @@ def get_lms_link_for_item(location, preview=False, course_id=None):
if settings.LMS_BASE is not None: if settings.LMS_BASE is not None:
if preview: if preview:
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', 'preview.' + settings.LMS_BASE) lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE')
else: else:
lms_base = settings.LMS_BASE lms_base = settings.LMS_BASE
......
...@@ -178,8 +178,7 @@ def edit_unit(request, location): ...@@ -178,8 +178,7 @@ def edit_unit(request, location):
break break
index = index + 1 index = index + 1
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE')
'preview.' + settings.LMS_BASE)
preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format( preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
preview_lms_base=preview_lms_base, preview_lms_base=preview_lms_base,
......
...@@ -81,6 +81,7 @@ with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file: ...@@ -81,6 +81,7 @@ with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
ENV_TOKENS = json.load(env_file) ENV_TOKENS = json.load(env_file)
LMS_BASE = ENV_TOKENS.get('LMS_BASE') LMS_BASE = ENV_TOKENS.get('LMS_BASE')
# Note that MITX_FEATURES['PREVIEW_LMS_BASE'] gets read in from the environment file.
SITE_NAME = ENV_TOKENS['SITE_NAME'] SITE_NAME = ENV_TOKENS['SITE_NAME']
......
...@@ -39,8 +39,8 @@ MITX_FEATURES = { ...@@ -39,8 +39,8 @@ MITX_FEATURES = {
'STUDIO_NPS_SURVEY': True, 'STUDIO_NPS_SURVEY': True,
'SEGMENT_IO': True, 'SEGMENT_IO': True,
# Enable URL that shows information about the status of variuous services # Enable URL that shows information about the status of various services
'ENABLE_SERVICE_STATUS': False, 'ENABLE_SERVICE_STATUS': False
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
......
...@@ -55,6 +55,7 @@ DATABASES = { ...@@ -55,6 +55,7 @@ DATABASES = {
} }
LMS_BASE = "localhost:8000" LMS_BASE = "localhost:8000"
MITX_FEATURES['PREVIEW_LMS_BASE'] = "localhost:8000"
REPOS = { REPOS = {
'edx4edx': { 'edx4edx': {
......
...@@ -82,6 +82,7 @@ DATABASES = { ...@@ -82,6 +82,7 @@ DATABASES = {
} }
LMS_BASE = "localhost:8000" LMS_BASE = "localhost:8000"
MITX_FEATURES['PREVIEW_LMS_BASE'] = "preview"
CACHES = { CACHES = {
# This is the cache used for most things. Askbot will not work without a # This is the cache used for most things. Askbot will not work without a
......
The code in this directory is based on:
django-mako Copyright (c) 2008 Mikeal Rogers
and is redistributed here with modifications under the same Apache 2.0 license
as the orginal.
================================================================================ ================================================================================
django-mako django-mako
================================================================================ ================================================================================
......
...@@ -527,12 +527,12 @@ def _do_create_account(post_vars): ...@@ -527,12 +527,12 @@ def _do_create_account(post_vars):
js = {'success': False} js = {'success': False}
# Figure out the cause of the integrity error # Figure out the cause of the integrity error
if len(User.objects.filter(username=post_vars['username'])) > 0: if len(User.objects.filter(username=post_vars['username'])) > 0:
js['value'] = "An account with this username already exists." js['value'] = "An account with the Public Username '" + post_vars['username'] + "' already exists."
js['field'] = 'username' js['field'] = 'username'
return HttpResponse(json.dumps(js)) return HttpResponse(json.dumps(js))
if len(User.objects.filter(email=post_vars['email'])) > 0: if len(User.objects.filter(email=post_vars['email'])) > 0:
js['value'] = "An account with this e-mail already exists." js['value'] = "An account with the Email '" + post_vars['email'] + "' already exists."
js['field'] = 'email' js['field'] = 'email'
return HttpResponse(json.dumps(js)) return HttpResponse(json.dumps(js))
......
...@@ -469,6 +469,7 @@ class LoncapaProblem(object): ...@@ -469,6 +469,7 @@ class LoncapaProblem(object):
random_seed=self.seed, random_seed=self.seed,
python_path=python_path, python_path=python_path,
cache=self.system.cache, cache=self.system.cache,
slug=self.problem_id,
) )
except Exception as err: except Exception as err:
log.exception("Error while execing script code: " + all_code) log.exception("Error while execing script code: " + all_code)
......
...@@ -140,6 +140,8 @@ class LoncapaResponse(object): ...@@ -140,6 +140,8 @@ class LoncapaResponse(object):
self.context = context self.context = context
self.system = system self.system = system
self.id = xml.get('id')
for abox in inputfields: for abox in inputfields:
if abox.tag not in self.allowed_inputfields: if abox.tag not in self.allowed_inputfields:
msg = "%s: cannot have input field %s" % ( msg = "%s: cannot have input field %s" % (
...@@ -286,7 +288,7 @@ class LoncapaResponse(object): ...@@ -286,7 +288,7 @@ class LoncapaResponse(object):
} }
try: try:
safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path']) safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'], slug=self.id)
except Exception as err: except Exception as err:
msg = 'Error %s in evaluating hint function %s' % (err, hintfn) msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
msg += "\nSee XML source line %s" % getattr( msg += "\nSee XML source line %s" % getattr(
...@@ -935,7 +937,6 @@ class CustomResponse(LoncapaResponse): ...@@ -935,7 +937,6 @@ class CustomResponse(LoncapaResponse):
# if <customresponse> has an "expect" (or "answer") attribute then save # if <customresponse> has an "expect" (or "answer") attribute then save
# that # that
self.expect = xml.get('expect') or xml.get('answer') self.expect = xml.get('expect') or xml.get('answer')
self.myid = xml.get('id')
log.debug('answer_ids=%s' % self.answer_ids) log.debug('answer_ids=%s' % self.answer_ids)
...@@ -972,7 +973,7 @@ class CustomResponse(LoncapaResponse): ...@@ -972,7 +973,7 @@ class CustomResponse(LoncapaResponse):
'ans': ans, 'ans': ans,
} }
globals_dict.update(kwargs) globals_dict.update(kwargs)
safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path']) safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'], slug=self.id)
return globals_dict['cfn_return'] return globals_dict['cfn_return']
return check_function return check_function
...@@ -981,7 +982,7 @@ class CustomResponse(LoncapaResponse): ...@@ -981,7 +982,7 @@ class CustomResponse(LoncapaResponse):
if not self.code: if not self.code:
if answer is None: if answer is None:
log.error("[courseware.capa.responsetypes.customresponse] missing" log.error("[courseware.capa.responsetypes.customresponse] missing"
" code checking script! id=%s" % self.myid) " code checking script! id=%s" % self.id)
self.code = '' self.code = ''
else: else:
answer_src = answer.get('src') answer_src = answer.get('src')
...@@ -1034,7 +1035,7 @@ class CustomResponse(LoncapaResponse): ...@@ -1034,7 +1035,7 @@ class CustomResponse(LoncapaResponse):
# note that this doesn't help the "cfn" version - only the exec version # note that this doesn't help the "cfn" version - only the exec version
self.context.update({ self.context.update({
# my ID # my ID
'response_id': self.myid, 'response_id': self.id,
# expected answer (if given as attribute) # expected answer (if given as attribute)
'expect': self.expect, 'expect': self.expect,
...@@ -1089,7 +1090,7 @@ class CustomResponse(LoncapaResponse): ...@@ -1089,7 +1090,7 @@ class CustomResponse(LoncapaResponse):
# exec the check function # exec the check function
if isinstance(self.code, basestring): if isinstance(self.code, basestring):
try: try:
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache) safe_exec.safe_exec(self.code, self.context, cache=self.system.cache, slug=self.id)
except Exception as err: except Exception as err:
self._handle_exec_exception(err) self._handle_exec_exception(err)
...@@ -1813,7 +1814,7 @@ class SchematicResponse(LoncapaResponse): ...@@ -1813,7 +1814,7 @@ class SchematicResponse(LoncapaResponse):
] ]
self.context.update({'submission': submission}) self.context.update({'submission': submission})
try: try:
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache) safe_exec.safe_exec(self.code, self.context, cache=self.system.cache, slug=self.id)
except Exception as err: except Exception as err:
msg = 'Error %s in evaluating SchematicResponse' % err msg = 'Error %s in evaluating SchematicResponse' % err
raise ResponseError(msg) raise ResponseError(msg)
......
...@@ -71,7 +71,7 @@ def update_hash(hasher, obj): ...@@ -71,7 +71,7 @@ def update_hash(hasher, obj):
@statsd.timed('capa.safe_exec.time') @statsd.timed('capa.safe_exec.time')
def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None): def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None, slug=None):
""" """
Execute python code safely. Execute python code safely.
...@@ -87,6 +87,9 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None ...@@ -87,6 +87,9 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None
to cache the execution, taking into account the code, the values of the globals, to cache the execution, taking into account the code, the values of the globals,
and the random seed. and the random seed.
`slug` is an arbitrary string, a description that's meaningful to the
caller, that will be used in log messages.
""" """
# Check the cache for a previous result. # Check the cache for a previous result.
if cache: if cache:
...@@ -112,7 +115,7 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None ...@@ -112,7 +115,7 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None
try: try:
codejail_safe_exec( codejail_safe_exec(
code_prolog + LAZY_IMPORTS + code, globals_dict, code_prolog + LAZY_IMPORTS + code, globals_dict,
python_path=python_path, python_path=python_path, slug=slug,
) )
except SafeExecException as e: except SafeExecException as e:
emsg = e.message emsg = e.message
......
"""Tests for the logic in input type mako templates.""" """
Tests for the logic in input type mako templates.
"""
import unittest import unittest
import capa import capa
import os.path import os.path
import json
from lxml import etree from lxml import etree
from mako.template import Template as MakoTemplate from mako.template import Template as MakoTemplate
from mako import exceptions from mako import exceptions
class TemplateError(Exception): class TemplateError(Exception):
"""Error occurred while rendering a Mako template""" """
Error occurred while rendering a Mako template.
"""
pass pass
class TemplateTestCase(unittest.TestCase): class TemplateTestCase(unittest.TestCase):
"""Utilitites for testing templates""" """
Utilitites for testing templates.
"""
# Subclasses override this to specify the file name of the template # Subclasses override this to specify the file name of the template
# to be loaded from capa/templates. # to be loaded from capa/templates.
...@@ -23,7 +30,9 @@ class TemplateTestCase(unittest.TestCase): ...@@ -23,7 +30,9 @@ class TemplateTestCase(unittest.TestCase):
TEMPLATE_NAME = None TEMPLATE_NAME = None
def setUp(self): def setUp(self):
"""Load the template""" """
Load the template under test.
"""
capa_path = capa.__path__[0] capa_path = capa.__path__[0]
self.template_path = os.path.join(capa_path, self.template_path = os.path.join(capa_path,
'templates', 'templates',
...@@ -33,18 +42,31 @@ class TemplateTestCase(unittest.TestCase): ...@@ -33,18 +42,31 @@ class TemplateTestCase(unittest.TestCase):
template_file.close() template_file.close()
def render_to_xml(self, context_dict): def render_to_xml(self, context_dict):
"""Render the template using the `context_dict` dict. """
Render the template using the `context_dict` dict.
Returns an `etree` XML element.""" Returns an `etree` XML element.
"""
try: try:
xml_str = self.template.render_unicode(**context_dict) xml_str = self.template.render_unicode(**context_dict)
except: except:
raise TemplateError(exceptions.text_error_template().render()) raise TemplateError(exceptions.text_error_template().render())
return etree.fromstring(xml_str) # Attempt to construct an XML tree from the template
# This makes it easy to use XPath to make assertions, rather
# than dealing with a string.
# We modify the string slightly by wrapping it in <test>
# tags, to ensure it has one root element.
try:
xml = etree.fromstring("<test>" + xml_str + "</test>")
except Exception as exc:
raise TemplateError("Could not parse XML from '{0}': {1}".format(
xml_str, str(exc)))
else:
return xml
def assert_has_xpath(self, xml_root, xpath, context_dict, exact_num=1): def assert_has_xpath(self, xml_root, xpath, context_dict, exact_num=1):
"""Asserts that the xml tree has an element satisfying `xpath`. """
Asserts that the xml tree has an element satisfying `xpath`.
`xml_root` is an etree XML element `xml_root` is an etree XML element
`xpath` is an XPath string, such as `'/foo/bar'` `xpath` is an XPath string, such as `'/foo/bar'`
...@@ -57,7 +79,8 @@ class TemplateTestCase(unittest.TestCase): ...@@ -57,7 +79,8 @@ class TemplateTestCase(unittest.TestCase):
self.assertEqual(len(xml_root.xpath(xpath)), exact_num, msg=message) self.assertEqual(len(xml_root.xpath(xpath)), exact_num, msg=message)
def assert_no_xpath(self, xml_root, xpath, context_dict): def assert_no_xpath(self, xml_root, xpath, context_dict):
"""Asserts that the xml tree does NOT have an element """
Asserts that the xml tree does NOT have an element
satisfying `xpath`. satisfying `xpath`.
`xml_root` is an etree XML element `xml_root` is an etree XML element
...@@ -67,7 +90,8 @@ class TemplateTestCase(unittest.TestCase): ...@@ -67,7 +90,8 @@ class TemplateTestCase(unittest.TestCase):
self.assert_has_xpath(xml_root, xpath, context_dict, exact_num=0) self.assert_has_xpath(xml_root, xpath, context_dict, exact_num=0)
def assert_has_text(self, xml_root, xpath, text, exact=True): def assert_has_text(self, xml_root, xpath, text, exact=True):
"""Find the element at `xpath` in `xml_root` and assert """
Find the element at `xpath` in `xml_root` and assert
that its text is `text`. that its text is `text`.
`xml_root` is an etree XML element `xml_root` is an etree XML element
...@@ -88,7 +112,9 @@ class TemplateTestCase(unittest.TestCase): ...@@ -88,7 +112,9 @@ class TemplateTestCase(unittest.TestCase):
class ChoiceGroupTemplateTest(TemplateTestCase): class ChoiceGroupTemplateTest(TemplateTestCase):
"""Test mako template for `<choicegroup>` input""" """
Test mako template for `<choicegroup>` input.
"""
TEMPLATE_NAME = 'choicegroup.html' TEMPLATE_NAME = 'choicegroup.html'
...@@ -103,8 +129,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -103,8 +129,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
super(ChoiceGroupTemplateTest, self).setUp() super(ChoiceGroupTemplateTest, self).setUp()
def test_problem_marked_correct(self): def test_problem_marked_correct(self):
"""Test conditions under which the entire problem """
(not a particular option) is marked correct""" Test conditions under which the entire problem
(not a particular option) is marked correct.
"""
self.context['status'] = 'correct' self.context['status'] = 'correct'
self.context['input_type'] = 'checkbox' self.context['input_type'] = 'checkbox'
...@@ -123,8 +151,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -123,8 +151,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
self.context) self.context)
def test_problem_marked_incorrect(self): def test_problem_marked_incorrect(self):
"""Test all conditions under which the entire problem """
(not a particular option) is marked incorrect""" Test all conditions under which the entire problem
(not a particular option) is marked incorrect.
"""
conditions = [ conditions = [
{'status': 'incorrect', 'input_type': 'radio', 'value': ''}, {'status': 'incorrect', 'input_type': 'radio', 'value': ''},
{'status': 'incorrect', 'input_type': 'checkbox', 'value': []}, {'status': 'incorrect', 'input_type': 'checkbox', 'value': []},
...@@ -151,8 +181,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -151,8 +181,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
self.context) self.context)
def test_problem_marked_unsubmitted(self): def test_problem_marked_unsubmitted(self):
"""Test all conditions under which the entire problem """
(not a particular option) is marked unanswered""" Test all conditions under which the entire problem
(not a particular option) is marked unanswered.
"""
conditions = [ conditions = [
{'status': 'unsubmitted', 'input_type': 'radio', 'value': ''}, {'status': 'unsubmitted', 'input_type': 'radio', 'value': ''},
{'status': 'unsubmitted', 'input_type': 'radio', 'value': []}, {'status': 'unsubmitted', 'input_type': 'radio', 'value': []},
...@@ -181,8 +213,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -181,8 +213,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
self.context) self.context)
def test_option_marked_correct(self): def test_option_marked_correct(self):
"""Test conditions under which a particular option """
(not the entire problem) is marked correct.""" Test conditions under which a particular option
(not the entire problem) is marked correct.
"""
conditions = [ conditions = [
{'input_type': 'radio', 'value': '2'}, {'input_type': 'radio', 'value': '2'},
{'input_type': 'radio', 'value': ['2']}] {'input_type': 'radio', 'value': ['2']}]
...@@ -200,8 +234,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -200,8 +234,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
self.assert_no_xpath(xml, xpath, self.context) self.assert_no_xpath(xml, xpath, self.context)
def test_option_marked_incorrect(self): def test_option_marked_incorrect(self):
"""Test conditions under which a particular option """
(not the entire problem) is marked incorrect.""" Test conditions under which a particular option
(not the entire problem) is marked incorrect.
"""
conditions = [ conditions = [
{'input_type': 'radio', 'value': '2'}, {'input_type': 'radio', 'value': '2'},
{'input_type': 'radio', 'value': ['2']}] {'input_type': 'radio', 'value': ['2']}]
...@@ -219,7 +255,8 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -219,7 +255,8 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
self.assert_no_xpath(xml, xpath, self.context) self.assert_no_xpath(xml, xpath, self.context)
def test_never_show_correctness(self): def test_never_show_correctness(self):
"""Test conditions under which we tell the template to """
Test conditions under which we tell the template to
NOT show correct/incorrect, but instead show a message. NOT show correct/incorrect, but instead show a message.
This is used, for example, by the Justice course to ask This is used, for example, by the Justice course to ask
...@@ -268,8 +305,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -268,8 +305,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
self.context['submitted_message']) self.context['submitted_message'])
def test_no_message_before_submission(self): def test_no_message_before_submission(self):
"""Ensure that we don't show the `submitted_message` """
before submitting""" Ensure that we don't show the `submitted_message`
before submitting.
"""
conditions = [ conditions = [
{'input_type': 'radio', 'status': 'unsubmitted', 'value': ''}, {'input_type': 'radio', 'status': 'unsubmitted', 'value': ''},
...@@ -298,7 +337,9 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -298,7 +337,9 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
class TextlineTemplateTest(TemplateTestCase): class TextlineTemplateTest(TemplateTestCase):
"""Test mako template for `<textline>` input""" """
Test mako template for `<textline>` input.
"""
TEMPLATE_NAME = 'textline.html' TEMPLATE_NAME = 'textline.html'
...@@ -405,3 +446,271 @@ class TextlineTemplateTest(TemplateTestCase): ...@@ -405,3 +446,271 @@ class TextlineTemplateTest(TemplateTestCase):
xpath = "//span[@class='message']" xpath = "//span[@class='message']"
self.assert_has_text(xml, xpath, self.context['msg']) self.assert_has_text(xml, xpath, self.context['msg'])
class AnnotationInputTemplateTest(TemplateTestCase):
"""
Test mako template for `<annotationinput>` input.
"""
TEMPLATE_NAME = 'annotationinput.html'
def setUp(self):
self.context = {'id': 2,
'value': '<p>Test value</p>',
'title': '<h1>This is a title</h1>',
'text': '<p><b>This</b> is a test.</p>',
'comment': '<p>This is a test comment</p>',
'comment_prompt': '<p>This is a test comment prompt</p>',
'comment_value': '<p>This is the value of a test comment</p>',
'tag_prompt': '<p>This is a tag prompt</p>',
'options': [],
'has_options_value': False,
'debug': False,
'status': 'unsubmitted',
'return_to_annotation': False,
'msg': '<p>This is a test message</p>', }
super(AnnotationInputTemplateTest, self).setUp()
def test_return_to_annotation(self):
"""
Test link for `Return to Annotation` appears if and only if
the flag is set.
"""
xpath = "//a[@class='annotation-return']"
# If return_to_annotation set, then show the link
self.context['return_to_annotation'] = True
xml = self.render_to_xml(self.context)
self.assert_has_xpath(xml, xpath, self.context)
# Otherwise, do not show the links
self.context['return_to_annotation'] = False
xml = self.render_to_xml(self.context)
self.assert_no_xpath(xml, xpath, self.context)
def test_option_selection(self):
"""
Test that selected options are selected.
"""
# Create options 0-4 and select option 2
self.context['options_value'] = [2]
self.context['options'] = [
{'id': id_num,
'choice': 'correct',
'description': '<p>Unescaped <b>HTML {0}</b></p>'.format(id_num)}
for id_num in range(0, 5)]
xml = self.render_to_xml(self.context)
# Expect that each option description is visible
# with unescaped HTML.
# Since the HTML is unescaped, we can traverse the XML tree
for id_num in range(0, 5):
xpath = "//span[@data-id='{0}']/p/b".format(id_num)
self.assert_has_text(xml, xpath, 'HTML {0}'.format(id_num), exact=False)
# Expect that the correct option is selected
xpath = "//span[contains(@class,'selected')]/p/b"
self.assert_has_text(xml, xpath, 'HTML 2', exact=False)
def test_submission_status(self):
"""
Test that the submission status displays correctly.
"""
# Test cases of `(input_status, expected_css_class)` tuples
test_cases = [('unsubmitted', 'unanswered'),
('incomplete', 'incorrect'),
('incorrect', 'incorrect')]
for (input_status, expected_css_class) in test_cases:
self.context['status'] = input_status
xml = self.render_to_xml(self.context)
xpath = "//span[@class='{0}']".format(expected_css_class)
self.assert_has_xpath(xml, xpath, self.context)
# If individual options are being marked, then expect
# just the option to be marked incorrect, not the whole problem
self.context['has_options_value'] = True
self.context['status'] = 'incorrect'
xpath = "//span[@class='incorrect']"
xml = self.render_to_xml(self.context)
self.assert_no_xpath(xml, xpath, self.context)
def test_display_html_comment(self):
"""
Test that HTML comment and comment prompt render.
"""
self.context['comment'] = "<p>Unescaped <b>comment HTML</b></p>"
self.context['comment_prompt'] = "<p>Prompt <b>prompt HTML</b></p>"
self.context['text'] = "<p>Unescaped <b>text</b></p>"
xml = self.render_to_xml(self.context)
# Because the HTML is unescaped, we should be able to
# descend to the <b> tag
xpath = "//div[@class='block']/p/b"
self.assert_has_text(xml, xpath, 'prompt HTML')
xpath = "//div[@class='block block-comment']/p/b"
self.assert_has_text(xml, xpath, 'comment HTML')
xpath = "//div[@class='block block-highlight']/p/b"
self.assert_has_text(xml, xpath, 'text')
def test_display_html_tag_prompt(self):
"""
Test that HTML tag prompts render.
"""
self.context['tag_prompt'] = "<p>Unescaped <b>HTML</b></p>"
xml = self.render_to_xml(self.context)
# Because the HTML is unescaped, we should be able to
# descend to the <b> tag
xpath = "//div[@class='block']/p/b"
self.assert_has_text(xml, xpath, 'HTML')
class MathStringTemplateTest(TemplateTestCase):
"""
Test mako template for `<mathstring>` input.
"""
TEMPLATE_NAME = 'mathstring.html'
def setUp(self):
self.context = {'isinline': False, 'mathstr': '', 'tail': ''}
super(MathStringTemplateTest, self).setUp()
def test_math_string_inline(self):
self.context['isinline'] = True
self.context['mathstr'] = 'y = ax^2 + bx + c'
xml = self.render_to_xml(self.context)
xpath = "//section[@class='math-string']/span[1]"
self.assert_has_text(xml, xpath,
'[mathjaxinline]y = ax^2 + bx + c[/mathjaxinline]')
def test_math_string_not_inline(self):
self.context['isinline'] = False
self.context['mathstr'] = 'y = ax^2 + bx + c'
xml = self.render_to_xml(self.context)
xpath = "//section[@class='math-string']/span[1]"
self.assert_has_text(xml, xpath,
'[mathjax]y = ax^2 + bx + c[/mathjax]')
def test_tail_html(self):
self.context['tail'] = "<p>This is some <b>tail</b> <em>HTML</em></p>"
xml = self.render_to_xml(self.context)
# HTML from `tail` should NOT be escaped.
# We should be able to traverse it as part of the XML tree
xpath = "//section[@class='math-string']/span[2]/p/b"
self.assert_has_text(xml, xpath, 'tail')
xpath = "//section[@class='math-string']/span[2]/p/em"
self.assert_has_text(xml, xpath, 'HTML')
class OptionInputTemplateTest(TemplateTestCase):
"""
Test mako template for `<optioninput>` input.
"""
TEMPLATE_NAME = 'optioninput.html'
def setUp(self):
self.context = {'id': 2, 'options': [], 'status': 'unsubmitted', 'value': 0}
super(OptionInputTemplateTest, self).setUp()
def test_select_options(self):
# Create options 0-4, and select option 2
self.context['options'] = [(id_num, '<b>Option {0}</b>'.format(id_num))
for id_num in range(0, 5)]
self.context['value'] = 2
xml = self.render_to_xml(self.context)
# Should have a dummy default
xpath = "//option[@value='option_2_dummy_default']"
self.assert_has_xpath(xml, xpath, self.context)
# Should have each of the options, with the correct description
# The description HTML should NOT be escaped
# (that's why we descend into the <b> tag)
for id_num in range(0, 5):
xpath = "//option[@value='{0}']/b".format(id_num)
self.assert_has_text(xml, xpath, 'Option {0}'.format(id_num))
# Should have the correct option selected
xpath = "//option[@selected='true']/b"
self.assert_has_text(xml, xpath, 'Option 2')
def test_status(self):
# Test cases, where each tuple represents
# `(input_status, expected_css_class)`
test_cases = [('unsubmitted', 'unanswered'),
('correct', 'correct'),
('incorrect', 'incorrect'),
('incomplete', 'incorrect')]
for (input_status, expected_css_class) in test_cases:
self.context['status'] = input_status
xml = self.render_to_xml(self.context)
xpath = "//span[@class='{0}']".format(expected_css_class)
self.assert_has_xpath(xml, xpath, self.context)
class DragAndDropTemplateTest(TemplateTestCase):
"""
Test mako template for `<draganddropinput>` input.
"""
TEMPLATE_NAME = 'drag_and_drop_input.html'
def setUp(self):
self.context = {'id': 2,
'drag_and_drop_json': '',
'value': 0,
'status': 'unsubmitted',
'msg': ''}
super(DragAndDropTemplateTest, self).setUp()
def test_status(self):
# Test cases, where each tuple represents
# `(input_status, expected_css_class, expected_text)`
test_cases = [('unsubmitted', 'unanswered', 'unanswered'),
('correct', 'correct', 'correct'),
('incorrect', 'incorrect', 'incorrect'),
('incomplete', 'incorrect', 'incomplete')]
for (input_status, expected_css_class, expected_text) in test_cases:
self.context['status'] = input_status
xml = self.render_to_xml(self.context)
# Expect a <div> with the status
xpath = "//div[@class='{0}']".format(expected_css_class)
self.assert_has_xpath(xml, xpath, self.context)
# Expect a <p> with the status
xpath = "//p[@class='status']"
self.assert_has_text(xml, xpath, expected_text, exact=False)
def test_drag_and_drop_json_html(self):
json_with_html = json.dumps({'test': '<p>Unescaped <b>HTML</b></p>'})
self.context['drag_and_drop_json'] = json_with_html
xml = self.render_to_xml(self.context)
# Assert that the JSON-encoded string was inserted without
# escaping the HTML. We should be able to traverse the XML tree.
xpath = "//div[@class='drag_and_drop_problem_json']/p/b"
self.assert_has_text(xml, xpath, 'HTML')
"""
Modules that get shown to the users when an error has occured while
loading or rendering other modules
"""
import hashlib import hashlib
import logging import logging
import json import json
...@@ -22,12 +27,19 @@ log = logging.getLogger(__name__) ...@@ -22,12 +27,19 @@ log = logging.getLogger(__name__)
class ErrorFields(object): class ErrorFields(object):
"""
XBlock fields used by the ErrorModules
"""
contents = String(scope=Scope.content) contents = String(scope=Scope.content)
error_msg = String(scope=Scope.content) error_msg = String(scope=Scope.content)
display_name = String(scope=Scope.settings) display_name = String(scope=Scope.settings)
class ErrorModule(ErrorFields, XModule): class ErrorModule(ErrorFields, XModule):
"""
Module that gets shown to staff when there has been an error while
loading or rendering other modules
"""
def get_html(self): def get_html(self):
'''Show an error to staff. '''Show an error to staff.
...@@ -42,6 +54,10 @@ class ErrorModule(ErrorFields, XModule): ...@@ -42,6 +54,10 @@ class ErrorModule(ErrorFields, XModule):
class NonStaffErrorModule(ErrorFields, XModule): class NonStaffErrorModule(ErrorFields, XModule):
"""
Module that gets shown to students when there has been an error while
loading or rendering other modules
"""
def get_html(self): def get_html(self):
'''Show an error to a student. '''Show an error to a student.
TODO (vshnayder): proper style, divs, etc. TODO (vshnayder): proper style, divs, etc.
...@@ -61,7 +77,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor): ...@@ -61,7 +77,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
module_class = ErrorModule module_class = ErrorModule
@classmethod @classmethod
def _construct(self, system, contents, error_msg, location): def _construct(cls, system, contents, error_msg, location):
if location.name is None: if location.name is None:
location = location._replace( location = location._replace(
...@@ -80,7 +96,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor): ...@@ -80,7 +96,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
'contents': contents, 'contents': contents,
'display_name': 'Error: ' + location.name 'display_name': 'Error: ' + location.name
} }
return ErrorDescriptor( return cls(
system, system,
location, location,
model_data, model_data,
......
...@@ -268,7 +268,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -268,7 +268,7 @@ class MongoModuleStore(ModuleStoreBase):
query = {'_id.org': location.org, query = {'_id.org': location.org,
'_id.course': location.course, '_id.course': location.course,
'_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical', '_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical',
'wrapper', 'problemset', 'conditional']} 'wrapper', 'problemset', 'conditional', 'randomize']}
} }
# we just want the Location, children, and inheritable metadata # we just want the Location, children, and inheritable metadata
record_filter = {'_id': 1, 'definition.children': 1} record_filter = {'_id': 1, 'definition.children': 1}
......
--- ---
metadata: metadata:
display_name: default display_name: Video Alpha 1
data_dir: a_made_up_name version: 1
data: | data: |
<videoalpha youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"/> <videoalpha show_captions="true" sub="name_of_file" youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY" >
<source src="https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp4"/>
<source src="https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.webm"/>
<source src="https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.ogv"/>
</videoalpha>
children: [] children: []
"""
Tests for ErrorModule and NonStaffErrorModule
"""
import unittest
from xmodule.tests import test_system
import xmodule.error_module as error_module
class TestErrorModule(unittest.TestCase):
"""
Tests for ErrorModule and ErrorDescriptor
"""
def setUp(self):
self.system = test_system()
self.org = "org"
self.course = "course"
self.fake_xml = "<problem />"
self.broken_xml = "<problem>"
self.error_msg = "Error"
def test_error_module_create(self):
descriptor = error_module.ErrorDescriptor.from_xml(
self.fake_xml, self.system, self.org, self.course)
self.assertTrue(isinstance(descriptor, error_module.ErrorDescriptor))
def test_error_module_rendering(self):
descriptor = error_module.ErrorDescriptor.from_xml(
self.fake_xml, self.system, self.org, self.course, self.error_msg)
module = descriptor.xmodule(self.system)
rendered_html = module.get_html()
self.assertIn(self.error_msg, rendered_html)
self.assertIn(self.fake_xml, rendered_html)
class TestNonStaffErrorModule(TestErrorModule):
"""
Tests for NonStaffErrorModule and NonStaffErrorDescriptor
"""
def test_non_staff_error_module_create(self):
descriptor = error_module.NonStaffErrorDescriptor.from_xml(
self.fake_xml, self.system, self.org, self.course)
self.assertTrue(isinstance(descriptor, error_module.NonStaffErrorDescriptor))
def test_non_staff_error_module_rendering(self):
descriptor = error_module.NonStaffErrorDescriptor.from_xml(
self.fake_xml, self.system, self.org, self.course)
module = descriptor.xmodule(self.system)
rendered_html = module.get_html()
self.assertNotIn(self.error_msg, rendered_html)
self.assertNotIn(self.fake_xml, rendered_html)
...@@ -4,10 +4,12 @@ ...@@ -4,10 +4,12 @@
import json import json
import unittest import unittest
from lxml import etree
from xmodule.poll_module import PollDescriptor from xmodule.poll_module import PollDescriptor
from xmodule.conditional_module import ConditionalDescriptor from xmodule.conditional_module import ConditionalDescriptor
from xmodule.word_cloud_module import WordCloudDescriptor from xmodule.word_cloud_module import WordCloudDescriptor
from xmodule.videoalpha_module import VideoAlphaDescriptor
class PostData: class PostData:
"""Class which emulate postdata.""" """Class which emulate postdata."""
...@@ -117,3 +119,33 @@ class WordCloudModuleTest(LogicTest): ...@@ -117,3 +119,33 @@ class WordCloudModuleTest(LogicTest):
) )
self.assertEqual(100.0, sum(i['percent'] for i in response['top_words']) ) self.assertEqual(100.0, sum(i['percent'] for i in response['top_words']) )
class VideoAlphaModuleTest(LogicTest):
descriptor_class = VideoAlphaDescriptor
raw_model_data = {
'data': '<videoalpha />'
}
def test_get_timeframe_no_parameters(self):
xmltree = etree.fromstring('<videoalpha>test</videoalpha>')
output = self.xmodule._get_timeframe(xmltree)
self.assertEqual(output, ('', ''))
def test_get_timeframe_with_one_parameter(self):
xmltree = etree.fromstring(
'<videoalpha start_time="00:04:07">test</videoalpha>'
)
output = self.xmodule._get_timeframe(xmltree)
self.assertEqual(output, (247, ''))
def test_get_timeframe_with_two_parameters(self):
xmltree = etree.fromstring(
'''<videoalpha
start_time="00:04:07"
end_time="13:04:39"
>test</videoalpha>'''
)
output = self.xmodule._get_timeframe(xmltree)
self.assertEqual(output, (247, 47079))
...@@ -93,7 +93,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule): ...@@ -93,7 +93,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
return result return result
def _get_timeframe(self, xmltree): def _get_timeframe(self, xmltree):
""" Converts 'from' and 'to' parameters in video tag to seconds. """ Converts 'start_time' and 'end_time' parameters in video tag to seconds.
If there are no parameters, returns empty string. """ If there are no parameters, returns empty string. """
def parse_time(s): def parse_time(s):
...@@ -103,11 +103,13 @@ class VideoAlphaModule(VideoAlphaFields, XModule): ...@@ -103,11 +103,13 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
return '' return ''
else: else:
x = time.strptime(s, '%H:%M:%S') x = time.strptime(s, '%H:%M:%S')
return datetime.timedelta(hours=x.tm_hour, return datetime.timedelta(
minutes=x.tm_min, hours=x.tm_hour,
seconds=x.tm_sec).total_seconds() minutes=x.tm_min,
seconds=x.tm_sec
).total_seconds()
return parse_time(xmltree.get('from')), parse_time(xmltree.get('to')) return parse_time(xmltree.get('start_time')), parse_time(xmltree.get('end_time'))
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
"""Handle ajax calls to this video. """Handle ajax calls to this video.
......
...@@ -13,8 +13,8 @@ Feature: Register for a course ...@@ -13,8 +13,8 @@ Feature: Register for a course
Scenario: I can unregister for a course Scenario: I can unregister for a course
Given I am registered for the course "6.002x" Given I am registered for the course "6.002x"
And I visit the dashboard And I visit the dashboard
When I click the link with the text "Unregister" Then I should see the course numbered "6.002x" in my dashboard
And I press the "Unregister" button in the Unenroll dialog When I unregister for the course numbered "6.002x"
Then All dialogs should be closed Then I should be on the dashboard page
And I should be on the dashboard page
And I should see "Looks like you haven't registered for any courses yet." somewhere in the page And I should see "Looks like you haven't registered for any courses yet." somewhere in the page
And I should NOT see the course numbered "6.002x" in my dashboard
...@@ -25,8 +25,15 @@ def i_should_see_that_course_in_my_dashboard(step, course): ...@@ -25,8 +25,15 @@ def i_should_see_that_course_in_my_dashboard(step, course):
assert world.is_css_present(course_link_css) assert world.is_css_present(course_link_css)
@step(u'I press the "([^"]*)" button in the Unenroll dialog') @step(u'I should NOT see the course numbered "([^"]*)" in my dashboard$')
def i_press_the_button_in_the_unenroll_dialog(step, value): def i_should_not_see_that_course_in_my_dashboard(step, course):
button_css = 'section#unenroll-modal input[value="%s"]' % value course_link_css = 'section.my-courses a[href*="%s"]' % course
assert not world.is_css_present(course_link_css)
@step(u'I unregister for the course numbered "([^"]*)"')
def i_unregister_for_that_course(step, course):
unregister_css = 'section.info a[href*="#unenroll-modal"][data-course-number*="%s"]' % course
world.css_click(unregister_css)
button_css = 'section#unenroll-modal input[value="Unregister"]'
world.css_click(button_css) world.css_click(button_css)
assert world.is_css_present('section.container.dashboard')
lms/static/images/pinned.png

518 Bytes | W: | H:

lms/static/images/pinned.png

49.5 KB | W: | H:

lms/static/images/pinned.png
lms/static/images/pinned.png
lms/static/images/pinned.png
lms/static/images/pinned.png
  • 2-up
  • Swipe
  • Onion skin
lms/static/images/unpinned.png

498 Bytes | W: | H:

lms/static/images/unpinned.png

48.1 KB | W: | H:

lms/static/images/unpinned.png
lms/static/images/unpinned.png
lms/static/images/unpinned.png
lms/static/images/unpinned.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -33,8 +33,8 @@ ...@@ -33,8 +33,8 @@
// colophon // colophon
.colophon { .colophon {
margin-right: flex-gutter(2); margin-right: flex-gutter();
width: flex-grid(6,12); width: flex-grid(8,12);
float: left; float: left;
.nav-colophon { .nav-colophon {
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
p { p {
float: left; float: left;
width: 460px; width: flex-grid(6,8);
margin-left: $baseline; margin-left: $baseline;
padding-left: $baseline; padding-left: $baseline;
font-size: em(13); font-size: em(13);
...@@ -91,7 +91,6 @@ ...@@ -91,7 +91,6 @@
text-align: right; text-align: right;
li { li {
margin-right: ($baseline/10);
display: inline-block; display: inline-block;
&:last-child { &:last-child {
...@@ -154,9 +153,5 @@ ...@@ -154,9 +153,5 @@
.colophon-about img { .colophon-about img {
margin-top: ($baseline*1.5); margin-top: ($baseline*1.5);
} }
.colophon-about p {
width: 360px;
}
} }
} }
...@@ -14,7 +14,6 @@ header.global { ...@@ -14,7 +14,6 @@ header.global {
padding: 18px 10px 0px; padding: 18px 10px 0px;
max-width: grid-width(12); max-width: grid-width(12);
min-width: 760px; min-width: 760px;
width: flex-grid(12);
} }
h1.logo { h1.logo {
......
...@@ -2,5 +2,5 @@ ...@@ -2,5 +2,5 @@
<section class="outside-app"> <section class="outside-app">
<h1>There has been a 500 error on the <em>edX</em> servers</h1> <h1>There has been a 500 error on the <em>edX</em> servers</h1>
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@edx.org">technical@edx.org</a> to report any problems or downtime.</p> <p>Please wait a few seconds and then reload the page. If the problem persists, please email us at <a href="mailto:technical@edx.org">technical@edx.org</a>.</p>
</section> </section>
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<div class="content"> <div class="content">
<div class="log-in-form"> <div class="log-in-form">
<h2>Log in to your courses</h2> <h2>Log in to your courses</h2>
<form id="login_form" data-remote="true" method="post" action="/login"> <form id="login_form" data-remote="true" method="post" action="/login_ajax">
<div class="row"> <div class="row">
<label>Email</label> <label>Email</label>
<input name="email" type="email" class="email-field" tabindex="1"> <input name="email" type="email" class="email-field" tabindex="1">
......
...@@ -22,10 +22,6 @@ urlpatterns = ('', # nopep8 ...@@ -22,10 +22,6 @@ urlpatterns = ('', # nopep8
url(r'^admin_dashboard$', 'dashboard.views.dashboard'), url(r'^admin_dashboard$', 'dashboard.views.dashboard'),
# Adding to allow debugging issues when prod is mysteriously different from staging
# (specifically missing get parameters in certain cases)
url(r'^debug_request$', 'util.views.debug_request'),
url(r'^change_email$', 'student.views.change_email_request', name="change_email"), url(r'^change_email$', 'student.views.change_email_request', name="change_email"),
url(r'^email_confirm/(?P<key>[^/]*)$', 'student.views.confirm_email_change'), url(r'^email_confirm/(?P<key>[^/]*)$', 'student.views.confirm_email_change'),
url(r'^change_name$', 'student.views.change_name_request', name="change_name"), url(r'^change_name$', 'student.views.change_name_request', name="change_name"),
...@@ -334,6 +330,13 @@ if settings.DEBUG or settings.MITX_FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'): ...@@ -334,6 +330,13 @@ if settings.DEBUG or settings.MITX_FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
## Jasmine and admin ## Jasmine and admin
urlpatterns += (url(r'^admin/', include(admin.site.urls)),) urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
if settings.DEBUG:
# Originally added to allow debugging issues when prod is
# mysteriously different from staging (specifically missing get
# parameters in certain cases), but removing from prod because
# it's a security risk.
urlpatterns += (url(r'^debug_request$', 'util.views.debug_request'),)
if settings.MITX_FEATURES.get('AUTH_USE_OPENID'): if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
urlpatterns += ( urlpatterns += (
url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'), url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'),
......
...@@ -9,4 +9,4 @@ ...@@ -9,4 +9,4 @@
# Our libraries: # Our libraries:
-e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock -e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock
-e git+https://github.com/edx/codejail.git@72cf791#egg=codejail -e git+https://github.com/edx/codejail.git@5fb5fa0#egg=codejail
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