Merge branch 'feature/christina/metadata-ui' of github.com:edx/edx-platform into feature/christina/metadata-ui
This commit is contained in:
@@ -26,7 +26,7 @@ class LMSLinksTestCase(TestCase):
|
||||
link = utils.get_lms_link_for_item(location, True)
|
||||
self.assertEquals(
|
||||
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):
|
||||
|
||||
if settings.LMS_BASE is not None:
|
||||
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:
|
||||
lms_base = settings.LMS_BASE
|
||||
|
||||
|
||||
@@ -178,8 +178,7 @@ def edit_unit(request, location):
|
||||
break
|
||||
index = index + 1
|
||||
|
||||
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
|
||||
'preview.' + settings.LMS_BASE)
|
||||
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE')
|
||||
|
||||
preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
|
||||
preview_lms_base=preview_lms_base,
|
||||
|
||||
@@ -81,6 +81,7 @@ with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
|
||||
ENV_TOKENS = json.load(env_file)
|
||||
|
||||
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']
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ MITX_FEATURES = {
|
||||
'STUDIO_NPS_SURVEY': True,
|
||||
'SEGMENT_IO': True,
|
||||
|
||||
# Enable URL that shows information about the status of variuous services
|
||||
'ENABLE_SERVICE_STATUS': False,
|
||||
# Enable URL that shows information about the status of various services
|
||||
'ENABLE_SERVICE_STATUS': False
|
||||
}
|
||||
ENABLE_JASMINE = False
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ DATABASES = {
|
||||
}
|
||||
|
||||
LMS_BASE = "localhost:8000"
|
||||
MITX_FEATURES['PREVIEW_LMS_BASE'] = "localhost:8000"
|
||||
|
||||
REPOS = {
|
||||
'edx4edx': {
|
||||
|
||||
@@ -82,6 +82,7 @@ DATABASES = {
|
||||
}
|
||||
|
||||
LMS_BASE = "localhost:8000"
|
||||
MITX_FEATURES['PREVIEW_LMS_BASE'] = "preview"
|
||||
|
||||
CACHES = {
|
||||
# This is the cache used for most things. Askbot will not work without a
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
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
|
||||
================================================================================
|
||||
|
||||
@@ -527,12 +527,12 @@ def _do_create_account(post_vars):
|
||||
js = {'success': False}
|
||||
# Figure out the cause of the integrity error
|
||||
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'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
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'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
|
||||
@@ -469,6 +469,7 @@ class LoncapaProblem(object):
|
||||
random_seed=self.seed,
|
||||
python_path=python_path,
|
||||
cache=self.system.cache,
|
||||
slug=self.problem_id,
|
||||
)
|
||||
except Exception as err:
|
||||
log.exception("Error while execing script code: " + all_code)
|
||||
|
||||
@@ -140,6 +140,8 @@ class LoncapaResponse(object):
|
||||
self.context = context
|
||||
self.system = system
|
||||
|
||||
self.id = xml.get('id')
|
||||
|
||||
for abox in inputfields:
|
||||
if abox.tag not in self.allowed_inputfields:
|
||||
msg = "%s: cannot have input field %s" % (
|
||||
@@ -286,7 +288,7 @@ class LoncapaResponse(object):
|
||||
}
|
||||
|
||||
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:
|
||||
msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
|
||||
msg += "\nSee XML source line %s" % getattr(
|
||||
@@ -935,7 +937,6 @@ class CustomResponse(LoncapaResponse):
|
||||
# if <customresponse> has an "expect" (or "answer") attribute then save
|
||||
# that
|
||||
self.expect = xml.get('expect') or xml.get('answer')
|
||||
self.myid = xml.get('id')
|
||||
|
||||
log.debug('answer_ids=%s' % self.answer_ids)
|
||||
|
||||
@@ -972,7 +973,7 @@ class CustomResponse(LoncapaResponse):
|
||||
'ans': ans,
|
||||
}
|
||||
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 check_function
|
||||
|
||||
@@ -981,7 +982,7 @@ class CustomResponse(LoncapaResponse):
|
||||
if not self.code:
|
||||
if answer is None:
|
||||
log.error("[courseware.capa.responsetypes.customresponse] missing"
|
||||
" code checking script! id=%s" % self.myid)
|
||||
" code checking script! id=%s" % self.id)
|
||||
self.code = ''
|
||||
else:
|
||||
answer_src = answer.get('src')
|
||||
@@ -1034,7 +1035,7 @@ class CustomResponse(LoncapaResponse):
|
||||
# note that this doesn't help the "cfn" version - only the exec version
|
||||
self.context.update({
|
||||
# my ID
|
||||
'response_id': self.myid,
|
||||
'response_id': self.id,
|
||||
|
||||
# expected answer (if given as attribute)
|
||||
'expect': self.expect,
|
||||
@@ -1089,7 +1090,7 @@ class CustomResponse(LoncapaResponse):
|
||||
# exec the check function
|
||||
if isinstance(self.code, basestring):
|
||||
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:
|
||||
self._handle_exec_exception(err)
|
||||
|
||||
@@ -1813,7 +1814,7 @@ class SchematicResponse(LoncapaResponse):
|
||||
]
|
||||
self.context.update({'submission': submission})
|
||||
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:
|
||||
msg = 'Error %s in evaluating SchematicResponse' % err
|
||||
raise ResponseError(msg)
|
||||
|
||||
@@ -71,7 +71,7 @@ def update_hash(hasher, obj):
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
@@ -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,
|
||||
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.
|
||||
if cache:
|
||||
@@ -112,7 +115,7 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None
|
||||
try:
|
||||
codejail_safe_exec(
|
||||
code_prolog + LAZY_IMPORTS + code, globals_dict,
|
||||
python_path=python_path,
|
||||
python_path=python_path, slug=slug,
|
||||
)
|
||||
except SafeExecException as e:
|
||||
emsg = e.message
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
"""Tests for the logic in input type mako templates."""
|
||||
"""
|
||||
Tests for the logic in input type mako templates.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import capa
|
||||
import os.path
|
||||
import json
|
||||
from lxml import etree
|
||||
from mako.template import Template as MakoTemplate
|
||||
from mako import exceptions
|
||||
|
||||
|
||||
class TemplateError(Exception):
|
||||
"""Error occurred while rendering a Mako template"""
|
||||
"""
|
||||
Error occurred while rendering a Mako template.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TemplateTestCase(unittest.TestCase):
|
||||
"""Utilitites for testing templates"""
|
||||
"""
|
||||
Utilitites for testing templates.
|
||||
"""
|
||||
|
||||
# Subclasses override this to specify the file name of the template
|
||||
# to be loaded from capa/templates.
|
||||
@@ -23,7 +30,9 @@ class TemplateTestCase(unittest.TestCase):
|
||||
TEMPLATE_NAME = None
|
||||
|
||||
def setUp(self):
|
||||
"""Load the template"""
|
||||
"""
|
||||
Load the template under test.
|
||||
"""
|
||||
capa_path = capa.__path__[0]
|
||||
self.template_path = os.path.join(capa_path,
|
||||
'templates',
|
||||
@@ -33,18 +42,31 @@ class TemplateTestCase(unittest.TestCase):
|
||||
template_file.close()
|
||||
|
||||
def render_to_xml(self, context_dict):
|
||||
"""Render the template using the `context_dict` dict.
|
||||
|
||||
Returns an `etree` XML element."""
|
||||
"""
|
||||
Render the template using the `context_dict` dict.
|
||||
Returns an `etree` XML element.
|
||||
"""
|
||||
try:
|
||||
xml_str = self.template.render_unicode(**context_dict)
|
||||
except:
|
||||
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):
|
||||
"""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
|
||||
`xpath` is an XPath string, such as `'/foo/bar'`
|
||||
@@ -57,7 +79,8 @@ class TemplateTestCase(unittest.TestCase):
|
||||
self.assertEqual(len(xml_root.xpath(xpath)), exact_num, msg=message)
|
||||
|
||||
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`.
|
||||
|
||||
`xml_root` is an etree XML element
|
||||
@@ -67,7 +90,8 @@ class TemplateTestCase(unittest.TestCase):
|
||||
self.assert_has_xpath(xml_root, xpath, context_dict, exact_num=0)
|
||||
|
||||
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`.
|
||||
|
||||
`xml_root` is an etree XML element
|
||||
@@ -88,7 +112,9 @@ class TemplateTestCase(unittest.TestCase):
|
||||
|
||||
|
||||
class ChoiceGroupTemplateTest(TemplateTestCase):
|
||||
"""Test mako template for `<choicegroup>` input"""
|
||||
"""
|
||||
Test mako template for `<choicegroup>` input.
|
||||
"""
|
||||
|
||||
TEMPLATE_NAME = 'choicegroup.html'
|
||||
|
||||
@@ -103,8 +129,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
|
||||
super(ChoiceGroupTemplateTest, self).setUp()
|
||||
|
||||
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['input_type'] = 'checkbox'
|
||||
@@ -123,8 +151,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
|
||||
self.context)
|
||||
|
||||
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 = [
|
||||
{'status': 'incorrect', 'input_type': 'radio', 'value': ''},
|
||||
{'status': 'incorrect', 'input_type': 'checkbox', 'value': []},
|
||||
@@ -151,8 +181,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
|
||||
self.context)
|
||||
|
||||
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 = [
|
||||
{'status': 'unsubmitted', 'input_type': 'radio', 'value': ''},
|
||||
{'status': 'unsubmitted', 'input_type': 'radio', 'value': []},
|
||||
@@ -181,8 +213,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
|
||||
self.context)
|
||||
|
||||
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 = [
|
||||
{'input_type': 'radio', 'value': '2'},
|
||||
{'input_type': 'radio', 'value': ['2']}]
|
||||
@@ -200,8 +234,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
|
||||
self.assert_no_xpath(xml, xpath, self.context)
|
||||
|
||||
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 = [
|
||||
{'input_type': 'radio', 'value': '2'},
|
||||
{'input_type': 'radio', 'value': ['2']}]
|
||||
@@ -219,7 +255,8 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
|
||||
self.assert_no_xpath(xml, xpath, self.context)
|
||||
|
||||
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.
|
||||
|
||||
This is used, for example, by the Justice course to ask
|
||||
@@ -268,8 +305,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
|
||||
self.context['submitted_message'])
|
||||
|
||||
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 = [
|
||||
{'input_type': 'radio', 'status': 'unsubmitted', 'value': ''},
|
||||
@@ -298,7 +337,9 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
|
||||
|
||||
|
||||
class TextlineTemplateTest(TemplateTestCase):
|
||||
"""Test mako template for `<textline>` input"""
|
||||
"""
|
||||
Test mako template for `<textline>` input.
|
||||
"""
|
||||
|
||||
TEMPLATE_NAME = 'textline.html'
|
||||
|
||||
@@ -405,3 +446,271 @@ class TextlineTemplateTest(TemplateTestCase):
|
||||
|
||||
xpath = "//span[@class='message']"
|
||||
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')
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
Modules that get shown to the users when an error has occured while
|
||||
loading or rendering other modules
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import json
|
||||
@@ -22,12 +27,19 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ErrorFields(object):
|
||||
"""
|
||||
XBlock fields used by the ErrorModules
|
||||
"""
|
||||
contents = String(scope=Scope.content)
|
||||
error_msg = String(scope=Scope.content)
|
||||
display_name = String(scope=Scope.settings)
|
||||
|
||||
|
||||
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):
|
||||
'''Show an error to staff.
|
||||
@@ -42,6 +54,10 @@ class ErrorModule(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):
|
||||
'''Show an error to a student.
|
||||
TODO (vshnayder): proper style, divs, etc.
|
||||
@@ -61,7 +77,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
|
||||
module_class = ErrorModule
|
||||
|
||||
@classmethod
|
||||
def _construct(self, system, contents, error_msg, location):
|
||||
def _construct(cls, system, contents, error_msg, location):
|
||||
|
||||
if location.name is None:
|
||||
location = location._replace(
|
||||
@@ -80,7 +96,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
|
||||
'contents': contents,
|
||||
'display_name': 'Error: ' + location.name
|
||||
}
|
||||
return ErrorDescriptor(
|
||||
return cls(
|
||||
system,
|
||||
location,
|
||||
model_data,
|
||||
|
||||
@@ -268,7 +268,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
query = {'_id.org': location.org,
|
||||
'_id.course': location.course,
|
||||
'_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical',
|
||||
'wrapper', 'problemset', 'conditional']}
|
||||
'wrapper', 'problemset', 'conditional', 'randomize']}
|
||||
}
|
||||
# we just want the Location, children, and inheritable metadata
|
||||
record_filter = {'_id': 1, 'definition.children': 1}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: default
|
||||
data_dir: a_made_up_name
|
||||
display_name: Video Alpha 1
|
||||
version: 1
|
||||
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: []
|
||||
|
||||
51
common/lib/xmodule/xmodule/tests/test_error_module.py
Normal file
51
common/lib/xmodule/xmodule/tests/test_error_module.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
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 @@
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from xmodule.poll_module import PollDescriptor
|
||||
from xmodule.conditional_module import ConditionalDescriptor
|
||||
from xmodule.word_cloud_module import WordCloudDescriptor
|
||||
|
||||
from xmodule.videoalpha_module import VideoAlphaDescriptor
|
||||
|
||||
class PostData:
|
||||
"""Class which emulate postdata."""
|
||||
@@ -117,3 +119,33 @@ class WordCloudModuleTest(LogicTest):
|
||||
)
|
||||
|
||||
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):
|
||||
return result
|
||||
|
||||
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. """
|
||||
|
||||
def parse_time(s):
|
||||
@@ -103,11 +103,13 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
|
||||
return ''
|
||||
else:
|
||||
x = time.strptime(s, '%H:%M:%S')
|
||||
return datetime.timedelta(hours=x.tm_hour,
|
||||
minutes=x.tm_min,
|
||||
seconds=x.tm_sec).total_seconds()
|
||||
return datetime.timedelta(
|
||||
hours=x.tm_hour,
|
||||
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):
|
||||
"""Handle ajax calls to this video.
|
||||
|
||||
@@ -13,8 +13,8 @@ Feature: Register for a course
|
||||
Scenario: I can unregister for a course
|
||||
Given I am registered for the course "6.002x"
|
||||
And I visit the dashboard
|
||||
When I click the link with the text "Unregister"
|
||||
And I press the "Unregister" button in the Unenroll dialog
|
||||
Then All dialogs should be closed
|
||||
And I should be on the dashboard page
|
||||
Then I should see the course numbered "6.002x" in my dashboard
|
||||
When I unregister for the course numbered "6.002x"
|
||||
Then 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 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):
|
||||
assert world.is_css_present(course_link_css)
|
||||
|
||||
|
||||
@step(u'I press the "([^"]*)" button in the Unenroll dialog')
|
||||
def i_press_the_button_in_the_unenroll_dialog(step, value):
|
||||
button_css = 'section#unenroll-modal input[value="%s"]' % value
|
||||
@step(u'I should NOT see the course numbered "([^"]*)" in my dashboard$')
|
||||
def i_should_not_see_that_course_in_my_dashboard(step, course):
|
||||
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)
|
||||
assert world.is_css_present('section.container.dashboard')
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 518 B After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 48 KiB |
@@ -33,8 +33,8 @@
|
||||
|
||||
// colophon
|
||||
.colophon {
|
||||
margin-right: flex-gutter(2);
|
||||
width: flex-grid(6,12);
|
||||
margin-right: flex-gutter();
|
||||
width: flex-grid(8,12);
|
||||
float: left;
|
||||
|
||||
.nav-colophon {
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
p {
|
||||
float: left;
|
||||
width: 460px;
|
||||
width: flex-grid(6,8);
|
||||
margin-left: $baseline;
|
||||
padding-left: $baseline;
|
||||
font-size: em(13);
|
||||
@@ -91,7 +91,6 @@
|
||||
text-align: right;
|
||||
|
||||
li {
|
||||
margin-right: ($baseline/10);
|
||||
display: inline-block;
|
||||
|
||||
&:last-child {
|
||||
@@ -154,9 +153,5 @@
|
||||
.colophon-about img {
|
||||
margin-top: ($baseline*1.5);
|
||||
}
|
||||
|
||||
.colophon-about p {
|
||||
width: 360px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ header.global {
|
||||
padding: 18px 10px 0px;
|
||||
max-width: grid-width(12);
|
||||
min-width: 760px;
|
||||
width: flex-grid(12);
|
||||
}
|
||||
|
||||
h1.logo {
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
<section class="outside-app">
|
||||
<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>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="content">
|
||||
<div class="log-in-form">
|
||||
<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">
|
||||
<label>Email</label>
|
||||
<input name="email" type="email" class="email-field" tabindex="1">
|
||||
|
||||
11
lms/urls.py
11
lms/urls.py
@@ -22,10 +22,6 @@ urlpatterns = ('', # nopep8
|
||||
|
||||
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'^email_confirm/(?P<key>[^/]*)$', 'student.views.confirm_email_change'),
|
||||
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'):
|
||||
## Jasmine and admin
|
||||
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'):
|
||||
urlpatterns += (
|
||||
url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'),
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
|
||||
# Our libraries:
|
||||
-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
|
||||
|
||||
Reference in New Issue
Block a user