diff --git a/cms/envs/common.py b/cms/envs/common.py index 11c0842b8d0f934e16eb80529dfd4d572e0f95df..62a1a60b2ac5b625d96fe31e1bdef2f58b7fdc66 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -215,6 +215,9 @@ FEATURES = { # Show Language selector 'SHOW_LANGUAGE_SELECTOR': False, + + # Set this to False to facilitate cleaning up invalid xml from your modulestore. + 'ENABLE_XBLOCK_XML_VALIDATION': True, } ENABLE_JASMINE = False diff --git a/common/lib/xmodule/xmodule/capa_base.py b/common/lib/xmodule/xmodule/capa_base.py index ce7b6e8f4f5d326288560331bce446368aa03c70..783687fc8e5918011edf0ca8a1344b8713b62c89 100644 --- a/common/lib/xmodule/xmodule/capa_base.py +++ b/common/lib/xmodule/xmodule/capa_base.py @@ -6,28 +6,27 @@ import hashlib import json import logging import os -import traceback +import re import struct import sys -import re +import traceback +from django.conf import settings # We don't want to force a dependency on datadog, so make the import conditional try: import dogstats_wrapper as dog_stats_api except ImportError: dog_stats_api = None +from pytz import utc from capa.capa_problem import LoncapaProblem, LoncapaSystem -from capa.responsetypes import StudentInputError, \ - ResponseError, LoncapaProblemError +from capa.responsetypes import StudentInputError, ResponseError, LoncapaProblemError from capa.util import convert_files_to_filenames, get_inner_html_from_xpath -from .progress import Progress -from xmodule.exceptions import NotFoundError -from xblock.fields import Scope, String, Boolean, Dict, Integer, Float -from .fields import Timedelta, Date -from django.utils.timezone import UTC +from xblock.fields import Boolean, Dict, Float, Integer, Scope, String, XMLString from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER -from django.conf import settings +from xmodule.exceptions import NotFoundError +from .fields import Date, Timedelta +from .progress import Progress from openedx.core.djangolib.markup import HTML, Text @@ -42,6 +41,8 @@ NUM_RANDOMIZATION_BINS = 20 # Never produce more than this many different seeds, no matter what. MAX_RANDOMIZATION_BINS = 1000 +FEATURES = getattr(settings, 'FEATURES', {}) + def randomization_bin(seed, problem_id): """ @@ -76,7 +77,7 @@ class ComplexEncoder(json.JSONEncoder): """ Extend the JSON encoder to correctly handle complex numbers """ - def default(self, obj): + def default(self, obj): # pylint: disable=method-hidden """ Print a nicely formatted complex number, or default to the JSON encoder """ @@ -157,7 +158,12 @@ class CapaFields(object): {"display_name": _("Per Student"), "value": RANDOMIZATION.PER_STUDENT} ] ) - data = String(help=_("XML data for the problem"), scope=Scope.content, default="<problem></problem>") + data = XMLString( + help=_("XML data for the problem"), + scope=Scope.content, + enforce_type=FEATURES.get('ENABLE_XBLOCK_XML_VALIDATION', True), + default="<problem></problem>" + ) correct_map = Dict(help=_("Dictionary with the correctness of current student answers"), scope=Scope.user_state, default={}) input_state = Dict(help=_("Dictionary for maintaining the state of inputtypes"), scope=Scope.user_state) @@ -257,11 +263,13 @@ class CapaMixin(CapaFields): ) ) # create a dummy problem with error message instead of failing - problem_text = (u'<problem><text><span class="inline-error">' - u'Problem {url} has an error:</span>{msg}</text></problem>'.format( - url=self.location.to_deprecated_string(), - msg=msg) - ) + problem_text = ( + u'<problem><text><span class="inline-error">' + u'Problem {url} has an error:</span>{msg}</text></problem>'.format( + url=self.location.to_deprecated_string(), + msg=msg, + ) + ) self.lcp = self.new_lcp(self.get_state_for_lcp(), text=problem_text) else: # add extra info and raise @@ -349,7 +357,7 @@ class CapaMixin(CapaFields): """ Set the module's last submission time (when the problem was submitted) """ - self.last_submission_time = datetime.datetime.now(UTC()) + self.last_submission_time = datetime.datetime.now(utc) def get_score(self): """ @@ -803,7 +811,7 @@ class CapaMixin(CapaFields): Is it now past this problem's due date, including grace period? """ return (self.close_date is not None and - datetime.datetime.now(UTC()) > self.close_date) + datetime.datetime.now(utc) > self.close_date) def closed(self): """ @@ -1093,7 +1101,7 @@ class CapaMixin(CapaFields): metric_name = u'capa.check_problem.{}'.format # Can override current time - current_time = datetime.datetime.now(UTC()) + current_time = datetime.datetime.now(utc) if override_time is not False: current_time = override_time @@ -1128,8 +1136,9 @@ class CapaMixin(CapaFields): # Wait time between resets: check if is too soon for submission. if self.last_submission_time is not None and self.submission_wait_seconds != 0: - if (current_time - self.last_submission_time).total_seconds() < self.submission_wait_seconds: - remaining_secs = int(self.submission_wait_seconds - (current_time - self.last_submission_time).total_seconds()) + seconds_since_submission = (current_time - self.last_submission_time).total_seconds() + if seconds_since_submission < self.submission_wait_seconds: + remaining_secs = int(self.submission_wait_seconds - seconds_since_submission) msg = _(u'You must wait at least {wait_secs} between submissions. {remaining_secs} remaining.').format( wait_secs=self.pretty_print_seconds(self.submission_wait_seconds), remaining_secs=self.pretty_print_seconds(remaining_secs)) @@ -1343,7 +1352,7 @@ class CapaMixin(CapaFields): log.warning('Input id %s is not mapped to an input type.', input_id) answer_response = None - for response, responder in self.lcp.responders.iteritems(): + for responder in self.lcp.responders.itervalues(): if input_id in responder.answer_ids: answer_response = responder @@ -1406,8 +1415,10 @@ class CapaMixin(CapaFields): if not self.lcp.supports_rescoring(): event_info['failure'] = 'unsupported' self.track_function_unmask('problem_rescore_fail', event_info) + # pylint: disable=line-too-long # Translators: 'rescoring' refers to the act of re-submitting a student's solution so it can get a new score. raise NotImplementedError(_("Problem's definition does not support rescoring.")) + # pylint: enable=line-too-long if not self.done: event_info['failure'] = 'unanswered' @@ -1485,8 +1496,10 @@ class CapaMixin(CapaFields): self.track_function_unmask('save_problem_fail', event_info) return { 'success': False, + # pylint: disable=line-too-long # Translators: 'closed' means the problem's due date has passed. You may no longer attempt to solve the problem. - 'msg': _("Problem is closed.") + 'msg': _("Problem is closed."), + # pylint: enable=line-too-long } # Problem submitted. Student should reset before saving @@ -1538,8 +1551,10 @@ class CapaMixin(CapaFields): self.track_function_unmask('reset_problem_fail', event_info) return { 'success': False, + # pylint: disable=line-too-long # Translators: 'closed' means the problem's due date has passed. You may no longer attempt to solve the problem. 'msg': _("You cannot select Reset for a problem that is closed."), + # pylint: enable=line-too-long } if not self.is_submitted(): diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 182f1cf5494cc27aadf6c5ca8ff047dc020486b7..abeb12b143cf2f573fa6a166568335ac01c87afa 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -81,14 +81,7 @@ class CapaFactory(object): ) @classmethod - def create(cls, - attempts=None, - problem_state=None, - correct=False, - xml=None, - override_get_score=True, - **kwargs - ): + def create(cls, attempts=None, problem_state=None, correct=False, xml=None, override_get_score=True, **kwargs): """ All parameters are optional, and are added to the created problem if specified. @@ -228,7 +221,6 @@ class CapaModuleTest(unittest.TestCase): useful as unit-code coverage for this current implementation. I don't see a layer where LoncapaProblem is tested directly """ - from capa.correctmap import CorrectMap student_answers = {'1_2_1': 'abcd'} correct_map = CorrectMap(answer_id='1_2_1', correctness="correct", npoints=0.9) module = CapaFactory.create(correct=True, override_get_score=False) @@ -623,6 +615,7 @@ class CapaModuleTest(unittest.TestCase): module.submit_problem(get_request_dict) + # pylint: disable=line-too-long # _http_post is called like this: # _http_post( # 'http://example.com/xqueue/xqueue/submit/', @@ -639,9 +632,10 @@ class CapaModuleTest(unittest.TestCase): # <open file u'/home/ned/edx/edx-platform/common/test/data/uploads/textbook.pdf', mode 'r' at 0x49c5a50>, # }, # ) + # pylint: enable=line-too-long self.assertEqual(xqueue_interface._http_post.call_count, 1) - _, kwargs = xqueue_interface._http_post.call_args + _, kwargs = xqueue_interface._http_post.call_args # pylint: disable=unpacking-non-sequence self.assertItemsEqual(fpaths, kwargs['files'].keys()) for fpath, fileobj in kwargs['files'].iteritems(): self.assertEqual(fpath, fileobj.name) @@ -674,7 +668,7 @@ class CapaModuleTest(unittest.TestCase): module.handle('xmodule_handler', request, 'problem_check') self.assertEqual(xqueue_interface._http_post.call_count, 1) - _, kwargs = xqueue_interface._http_post.call_args + _, kwargs = xqueue_interface._http_post.call_args # pylint: disable=unpacking-non-sequence self.assertItemsEqual(fnames, kwargs['files'].keys()) for fpath, fileobj in kwargs['files'].iteritems(): self.assertEqual(fpath, fileobj.name) @@ -2487,18 +2481,15 @@ class CapaDescriptorTest(unittest.TestCase): def test_invalid_xml_handling(self): """ - Tests to confirm that invalid XML does not throw a wake-up-ops level error. - See TNL-5057 for quick fix, TNL-5245 for full resolution. + Tests to confirm that invalid XML throws errors during xblock creation, + so as not to allow bad data into modulestore. """ sample_invalid_xml = textwrap.dedent(""" <problem> </proble-oh no my finger broke and I can't close the problem tag properly... """) - descriptor = self._create_descriptor(sample_invalid_xml, name="Invalid XML") - try: - descriptor.has_support(None, "multi_device") - except etree.XMLSyntaxError: - self.fail("Exception raised during XML parsing, this method should be resilient to such errors") + with self.assertRaises(etree.XMLSyntaxError): + self._create_descriptor(sample_invalid_xml, name="Invalid XML") class ComplexEncoderTest(unittest.TestCase): diff --git a/common/test/acceptance/tests/studio/test_import_export.py b/common/test/acceptance/tests/studio/test_import_export.py index 216d1ae60551d3c344e341600e78b62c8e8900d5..d368a6c345b24c83f92fdc43966d62c8b91f5e3c 100644 --- a/common/test/acceptance/tests/studio/test_import_export.py +++ b/common/test/acceptance/tests/studio/test_import_export.py @@ -7,17 +7,14 @@ from datetime import datetime from flaky import flaky from abc import abstractmethod -from bok_choy.promise import EmptyPromise from common.test.acceptance.tests.studio.base_studio_test import StudioLibraryTest, StudioCourseTest -from common.test.acceptance.fixtures.course import XBlockFixtureDesc from common.test.acceptance.pages.studio.import_export import ( ExportLibraryPage, ExportCoursePage, ImportLibraryPage, ImportCoursePage) from common.test.acceptance.pages.studio.library import LibraryEditPage -from common.test.acceptance.pages.studio.container import ContainerPage from common.test.acceptance.pages.studio.overview import CourseOutlinePage from common.test.acceptance.pages.lms.courseware import CoursewarePage from common.test.acceptance.pages.lms.staff_view import StaffPage @@ -86,84 +83,6 @@ class TestLibraryExport(ExportTestMixin, StudioLibraryTest): self.assertEqual(self.export_page.header_text, 'Library Export') -class BadExportMixin(object): - """ - Test mixin for bad exports. - """ - def test_bad_export(self): - """ - Scenario: I should receive an error when attempting to export a broken course or library. - Given that I have a course or library - No error modal should be showing - When I click the export button - An error modal should be shown - When I click the modal's action button - I should arrive at the edit page for the broken component - """ - # No error should be there to start. - self.assertFalse(self.export_page.is_error_modal_showing()) - self.export_page.click_export() - self.export_page.wait_for_error_modal() - self.export_page.click_modal_button() - self.edit_page.wait_for_page() - - -@attr(shard=7) -class TestLibraryBadExport(BadExportMixin, StudioLibraryTest): - """ - Verify exporting a bad library causes an error. - """ - - def setUp(self): - """ - Set up the pages and start the tests. - """ - super(TestLibraryBadExport, self).setUp() - self.export_page = ExportLibraryPage(self.browser, self.library_key) - self.edit_page = LibraryEditPage(self.browser, self.library_key) - self.export_page.visit() - - def populate_library_fixture(self, library_fixture): - """ - Create a library with a bad component. - """ - library_fixture.add_children( - XBlockFixtureDesc("problem", "Bad Problem", data='<'), - ) - - -@attr(shard=7) -class TestCourseBadExport(BadExportMixin, StudioCourseTest): - """ - Verify exporting a bad course causes an error. - """ - ready_method = 'wait_for_component_menu' - - def setUp(self): # pylint: disable=arguments-differ - super(TestCourseBadExport, self).setUp() - self.export_page = ExportCoursePage( - self.browser, - self.course_info['org'], self.course_info['number'], self.course_info['run'], - ) - self.edit_page = ContainerPage(self.browser, self.unit.locator) - self.export_page.visit() - - def populate_course_fixture(self, course_fixture): - """ - Populate the course with a unit that has a bad problem. - """ - self.unit = XBlockFixtureDesc('vertical', 'Unit') - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Main Section').add_children( - XBlockFixtureDesc('sequential', 'Subsection').add_children( - self.unit.add_children( - XBlockFixtureDesc("problem", "Bad Problem", data='<') - ) - ) - ) - ) - - @attr(shard=7) class ImportTestMixin(object): """ diff --git a/lms/envs/common.py b/lms/envs/common.py index f380bb25774cb4983cfb10cd543807323e96976c..195908667028cfa5c515f5ceaec78ee3b81c38a2 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -361,6 +361,9 @@ FEATURES = { # Note: This has no effect unless ANALYTICS_DASHBOARD_URL is already set, # because without that setting, the tab does not show up for any courses. 'ENABLE_CCX_ANALYTICS_DASHBOARD_URL': False, + + # Set this to False to facilitate cleaning up invalid xml from your modulestore. + 'ENABLE_XBLOCK_XML_VALIDATION': True, } # Ignore static asset files on import which match this pattern diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index e38fbc4b6366a0b2fd655177740f713633509edf..7d98714efbbd4e60991c88356b1d9fa33a6b3719 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -71,7 +71,7 @@ git+https://github.com/edx/rfc6266.git@v0.0.5-edx#egg=rfc6266==0.0.5-edx git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002 # Our libraries: -git+https://github.com/edx/XBlock.git@xblock-0.4.12#egg=XBlock==0.4.12 +git+https://github.com/edx/XBlock.git@xblock-0.4.13#egg=XBlock==0.4.13 -e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail -e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1 -e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2