diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index 3b755b0ec2..0757992f2f 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -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" ) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index ea3e3ecd6a..35451cf7cc 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -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 diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 277a251065..a06e55fe8f 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -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, diff --git a/cms/envs/aws.py b/cms/envs/aws.py index 9fabb3b9e8..f6064229e6 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -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'] diff --git a/cms/envs/common.py b/cms/envs/common.py index 8c218bc7ea..e60d337731 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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 diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 9acbf84a95..e63968d338 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -55,6 +55,7 @@ DATABASES = { } LMS_BASE = "localhost:8000" +MITX_FEATURES['PREVIEW_LMS_BASE'] = "localhost:8000" REPOS = { 'edx4edx': { diff --git a/cms/envs/test.py b/cms/envs/test.py index 6d78b0d7d6..8a3f9ba158 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -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 diff --git a/common/djangoapps/mitxmako/README b/common/djangoapps/mitxmako/README index ab04df2cf7..9896d78747 100644 --- a/common/djangoapps/mitxmako/README +++ b/common/djangoapps/mitxmako/README @@ -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 ================================================================================ diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 8059026e12..463ad33316 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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)) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 8543e9e3e1..150b3b3c9b 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -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) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index a166438f17..0fa50079de 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -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 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) diff --git a/common/lib/capa/capa/safe_exec/safe_exec.py b/common/lib/capa/capa/safe_exec/safe_exec.py index b9cdf236bd..67e93be46f 100644 --- a/common/lib/capa/capa/safe_exec/safe_exec.py +++ b/common/lib/capa/capa/safe_exec/safe_exec.py @@ -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 diff --git a/common/lib/capa/capa/tests/test_input_templates.py b/common/lib/capa/capa/tests/test_input_templates.py index 92c4d8b3b7..00a9b3f6c2 100644 --- a/common/lib/capa/capa/tests/test_input_templates.py +++ b/common/lib/capa/capa/tests/test_input_templates.py @@ -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 + # tags, to ensure it has one root element. + try: + xml = etree.fromstring("" + xml_str + "") + 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 `` input""" + """ + Test mako template for `` 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 `` input""" + """ + Test mako template for `` 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 `` input. + """ + + TEMPLATE_NAME = 'annotationinput.html' + + def setUp(self): + self.context = {'id': 2, + 'value': '

Test value

', + 'title': '

This is a title

', + 'text': '

This is a test.

', + 'comment': '

This is a test comment

', + 'comment_prompt': '

This is a test comment prompt

', + 'comment_value': '

This is the value of a test comment

', + 'tag_prompt': '

This is a tag prompt

', + 'options': [], + 'has_options_value': False, + 'debug': False, + 'status': 'unsubmitted', + 'return_to_annotation': False, + 'msg': '

This is a test message

', } + 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': '

Unescaped HTML {0}

'.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'] = "

Unescaped comment HTML

" + self.context['comment_prompt'] = "

Prompt prompt HTML

" + self.context['text'] = "

Unescaped text

" + xml = self.render_to_xml(self.context) + + # Because the HTML is unescaped, we should be able to + # descend to the 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'] = "

Unescaped HTML

" + xml = self.render_to_xml(self.context) + + # Because the HTML is unescaped, we should be able to + # descend to the tag + xpath = "//div[@class='block']/p/b" + self.assert_has_text(xml, xpath, 'HTML') + + +class MathStringTemplateTest(TemplateTestCase): + """ + Test mako template for `` 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'] = "

This is some tail HTML

" + 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 `` 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, 'Option {0}'.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 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 `` 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
with the status + xpath = "//div[@class='{0}']".format(expected_css_class) + self.assert_has_xpath(xml, xpath, self.context) + + # Expect a

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': '

Unescaped HTML

'}) + 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') diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py index ebb61b36f2..8014234f69 100644 --- a/common/lib/xmodule/xmodule/error_module.py +++ b/common/lib/xmodule/xmodule/error_module.py @@ -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, diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 24df17b15b..be01328733 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -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} diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml index 69ed22cc1e..dba8bbd0b4 100644 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -1,7 +1,11 @@ --- metadata: - display_name: default - data_dir: a_made_up_name + display_name: Video Alpha 1 + version: 1 data: | - + + + + + children: [] diff --git a/common/lib/xmodule/xmodule/tests/test_error_module.py b/common/lib/xmodule/xmodule/tests/test_error_module.py new file mode 100644 index 0000000000..d6b6f77ae6 --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_error_module.py @@ -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 = "" + self.broken_xml = "" + 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) diff --git a/common/lib/xmodule/xmodule/tests/test_logic.py b/common/lib/xmodule/xmodule/tests/test_logic.py index 6cd46a26ee..e60af63921 100644 --- a/common/lib/xmodule/xmodule/tests/test_logic.py +++ b/common/lib/xmodule/xmodule/tests/test_logic.py @@ -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': '' + } + + def test_get_timeframe_no_parameters(self): + xmltree = etree.fromstring('test') + output = self.xmodule._get_timeframe(xmltree) + self.assertEqual(output, ('', '')) + + def test_get_timeframe_with_one_parameter(self): + xmltree = etree.fromstring( + 'test' + ) + output = self.xmodule._get_timeframe(xmltree) + self.assertEqual(output, (247, '')) + + def test_get_timeframe_with_two_parameters(self): + xmltree = etree.fromstring( + '''test''' + ) + output = self.xmodule._get_timeframe(xmltree) + self.assertEqual(output, (247, 47079)) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 6754f8f664..16230480a7 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -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. diff --git a/lms/djangoapps/courseware/features/registration.feature b/lms/djangoapps/courseware/features/registration.feature index 43b04a5ad0..6c850a0b43 100644 --- a/lms/djangoapps/courseware/features/registration.feature +++ b/lms/djangoapps/courseware/features/registration.feature @@ -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 diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 72bde65f99..dd2fcb0825 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -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') diff --git a/lms/static/images/pinned.png b/lms/static/images/pinned.png index 76bb207fff..e70df7f9db 100644 Binary files a/lms/static/images/pinned.png and b/lms/static/images/pinned.png differ diff --git a/lms/static/images/unpinned.png b/lms/static/images/unpinned.png index 030198f7e8..a5f5f02894 100644 Binary files a/lms/static/images/unpinned.png and b/lms/static/images/unpinned.png differ diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss index e3e99ae301..864cf57d03 100644 --- a/lms/static/sass/shared/_footer.scss +++ b/lms/static/sass/shared/_footer.scss @@ -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; - } } } diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss index 6987b35c84..0608a8faf4 100644 --- a/lms/static/sass/shared/_header.scss +++ b/lms/static/sass/shared/_header.scss @@ -14,7 +14,6 @@ header.global { padding: 18px 10px 0px; max-width: grid-width(12); min-width: 760px; - width: flex-grid(12); } h1.logo { diff --git a/lms/templates/static_templates/server-error.html b/lms/templates/static_templates/server-error.html index 88ca32ff80..5564ea082e 100644 --- a/lms/templates/static_templates/server-error.html +++ b/lms/templates/static_templates/server-error.html @@ -2,5 +2,5 @@

There has been a 500 error on the edX servers

-

Our staff is currently working to get the site back up as soon as possible. Please email us at technical@edx.org to report any problems or downtime.

+

Please wait a few seconds and then reload the page. If the problem persists, please email us at technical@edx.org.

diff --git a/lms/templates/university_profile/edge.html b/lms/templates/university_profile/edge.html index a3e115ddd8..9e6adfe3d8 100644 --- a/lms/templates/university_profile/edge.html +++ b/lms/templates/university_profile/edge.html @@ -9,7 +9,7 @@