merge out from master
This commit is contained in:
@@ -8,6 +8,7 @@ from functools import partial
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
from django.http import HttpResponse
|
||||
@@ -208,9 +209,6 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
|
||||
}
|
||||
|
||||
def get_or_default(key, default):
|
||||
getattr(settings, key, default)
|
||||
|
||||
#This is a hacky way to pass settings to the combined open ended xmodule
|
||||
#It needs an S3 interface to upload images to S3
|
||||
#It needs the open ended grading interface in order to get peer grading to be done
|
||||
@@ -226,12 +224,11 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING
|
||||
if is_descriptor_combined_open_ended:
|
||||
s3_interface = {
|
||||
'access_key' : get_or_default('AWS_ACCESS_KEY_ID',''),
|
||||
'secret_access_key' : get_or_default('AWS_SECRET_ACCESS_KEY',''),
|
||||
'storage_bucket_name' : get_or_default('AWS_STORAGE_BUCKET_NAME','')
|
||||
'access_key' : getattr(settings,'AWS_ACCESS_KEY_ID',''),
|
||||
'secret_access_key' : getattr(settings,'AWS_SECRET_ACCESS_KEY',''),
|
||||
'storage_bucket_name' : getattr(settings,'AWS_STORAGE_BUCKET_NAME','openended')
|
||||
}
|
||||
|
||||
|
||||
def inner_get_module(descriptor):
|
||||
"""
|
||||
Delegate to get_module. It does an access check, so may return None
|
||||
@@ -412,6 +409,9 @@ def modx_dispatch(request, dispatch, location, course_id):
|
||||
if not Location.is_valid(location):
|
||||
raise Http404("Invalid location")
|
||||
|
||||
if not request.user.is_authenticated():
|
||||
raise PermissionDenied
|
||||
|
||||
# Check for submitted files and basic file size checks
|
||||
p = request.POST.copy()
|
||||
if request.FILES:
|
||||
|
||||
107
lms/djangoapps/courseware/tests/test_login.py
Normal file
107
lms/djangoapps/courseware/tests/test_login.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import Registration, UserProfile
|
||||
import json
|
||||
|
||||
class LoginTest(TestCase):
|
||||
'''
|
||||
Test student.views.login_user() view
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# Create one user and save it to the database
|
||||
self.user = User.objects.create_user('test', 'test@edx.org', 'test_password')
|
||||
self.user.is_active = True
|
||||
self.user.save()
|
||||
|
||||
# Create a registration for the user
|
||||
Registration().register(self.user)
|
||||
|
||||
# Create a profile for the user
|
||||
UserProfile(user=self.user).save()
|
||||
|
||||
# Create the test client
|
||||
self.client = Client()
|
||||
|
||||
# Store the login url
|
||||
self.url = reverse('login')
|
||||
|
||||
def test_login_success(self):
|
||||
response = self._login_response('test@edx.org', 'test_password')
|
||||
self._assert_response(response, success=True)
|
||||
|
||||
def test_login_success_unicode_email(self):
|
||||
unicode_email = u'test@edx.org' + unichr(40960)
|
||||
|
||||
self.user.email = unicode_email
|
||||
self.user.save()
|
||||
|
||||
response = self._login_response(unicode_email, 'test_password')
|
||||
self._assert_response(response, success=True)
|
||||
|
||||
|
||||
def test_login_fail_no_user_exists(self):
|
||||
response = self._login_response('not_a_user@edx.org', 'test_password')
|
||||
self._assert_response(response, success=False,
|
||||
value='Email or password is incorrect')
|
||||
|
||||
def test_login_fail_wrong_password(self):
|
||||
response = self._login_response('test@edx.org', 'wrong_password')
|
||||
self._assert_response(response, success=False,
|
||||
value='Email or password is incorrect')
|
||||
|
||||
def test_login_not_activated(self):
|
||||
|
||||
# De-activate the user
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
|
||||
# Should now be unable to login
|
||||
response = self._login_response('test@edx.org', 'test_password')
|
||||
self._assert_response(response, success=False,
|
||||
value="This account has not been activated")
|
||||
|
||||
|
||||
def test_login_unicode_email(self):
|
||||
unicode_email = u'test@edx.org' + unichr(40960)
|
||||
response = self._login_response(unicode_email, 'test_password')
|
||||
self._assert_response(response, success=False)
|
||||
|
||||
def test_login_unicode_password(self):
|
||||
unicode_password = u'test_password' + unichr(1972)
|
||||
response = self._login_response('test@edx.org', unicode_password)
|
||||
self._assert_response(response, success=False)
|
||||
|
||||
def _login_response(self, email, password):
|
||||
post_params = {'email': email, 'password': password}
|
||||
return self.client.post(self.url, post_params)
|
||||
|
||||
def _assert_response(self, response, success=None, value=None):
|
||||
'''
|
||||
Assert that the response had status 200 and returned a valid
|
||||
JSON-parseable dict.
|
||||
|
||||
If success is provided, assert that the response had that
|
||||
value for 'success' in the JSON dict.
|
||||
|
||||
If value is provided, assert that the response contained that
|
||||
value for 'value' in the JSON dict.
|
||||
'''
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
try:
|
||||
response_dict = json.loads(response.content)
|
||||
except ValueError:
|
||||
self.fail("Could not parse response content as JSON: %s"
|
||||
% str(response.content))
|
||||
|
||||
if success is not None:
|
||||
self.assertEqual(response_dict['success'], success)
|
||||
|
||||
if value is not None:
|
||||
msg = ("'%s' did not contain '%s'" %
|
||||
(str(response_dict['value']), str(value)))
|
||||
self.assertTrue(value in response_dict['value'], msg)
|
||||
@@ -8,8 +8,8 @@ from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import courseware.module_render as render
|
||||
from xmodule.modulestore.django import modulestore, _MODULESTORES
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase
|
||||
from courseware.model_data import ModelDataCache
|
||||
|
||||
@@ -40,7 +40,6 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
||||
class ModuleRenderTestCase(LoginEnrollmentTestCase):
|
||||
def setUp(self):
|
||||
self.location = ['i4x', 'edX', 'toy', 'chapter', 'Overview']
|
||||
self._MODULESTORES = {}
|
||||
self.course_id = 'edX/toy/2012_Fall'
|
||||
self.toy_course = modulestore().get_course(self.course_id)
|
||||
|
||||
@@ -91,12 +90,23 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase):
|
||||
self.assertEquals(render.get_score_bucket(11, 10), 'incorrect')
|
||||
self.assertEquals(render.get_score_bucket(-1, 10), 'incorrect')
|
||||
|
||||
def test_anonymous_modx_dispatch(self):
|
||||
dispatch_url = reverse(
|
||||
'modx_dispatch',
|
||||
args=[
|
||||
'edX/toy/2012_Fall',
|
||||
'i4x://edX/toy/videosequence/Toy_Videos',
|
||||
'goto_position'
|
||||
]
|
||||
)
|
||||
response = self.client.post(dispatch_url, {'position': 2})
|
||||
self.assertEquals(403, response.status_code)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestTOC(TestCase):
|
||||
"""Check the Table of Contents for a course"""
|
||||
def setUp(self):
|
||||
self._MODULESTORES = {}
|
||||
|
||||
# Toy courses should be loaded
|
||||
self.course_name = 'edX/toy/2012_Fall'
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
from comment_client import CommentClientError
|
||||
from django_comment_client.utils import JsonError
|
||||
import json
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AjaxExceptionMiddleware(object):
|
||||
"""
|
||||
Middleware that captures CommentClientErrors during ajax requests
|
||||
and tranforms them into json responses
|
||||
"""
|
||||
def process_exception(self, request, exception):
|
||||
"""
|
||||
Processes CommentClientErrors in ajax requests. If the request is an ajax request,
|
||||
returns a http response that encodes the error as json
|
||||
"""
|
||||
if isinstance(exception, CommentClientError) and request.is_ajax():
|
||||
return JsonError(json.loads(exception.message))
|
||||
try:
|
||||
return JsonError(json.loads(exception.message))
|
||||
except ValueError:
|
||||
return JsonError(exception.message)
|
||||
return None
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import string
|
||||
import random
|
||||
import collections
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
import comment_client
|
||||
@@ -13,17 +9,19 @@ class AjaxExceptionTestCase(TestCase):
|
||||
|
||||
# TODO: check whether the correct error message is produced.
|
||||
# The error message should be the same as the argument to CommentClientError
|
||||
def setUp(self):
|
||||
self.a = middleware.AjaxExceptionMiddleware()
|
||||
self.request1 = django.http.HttpRequest()
|
||||
self.request0 = django.http.HttpRequest()
|
||||
self.exception1 = comment_client.CommentClientError('{}')
|
||||
self.exception0 = ValueError()
|
||||
self.request1.META['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest"
|
||||
self.request0.META['HTTP_X_REQUESTED_WITH'] = "SHADOWFAX"
|
||||
def setUp(self):
|
||||
self.a = middleware.AjaxExceptionMiddleware()
|
||||
self.request1 = django.http.HttpRequest()
|
||||
self.request0 = django.http.HttpRequest()
|
||||
self.exception1 = comment_client.CommentClientError('{}')
|
||||
self.exception2 = comment_client.CommentClientError('Foo!')
|
||||
self.exception0 = ValueError()
|
||||
self.request1.META['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest"
|
||||
self.request0.META['HTTP_X_REQUESTED_WITH'] = "SHADOWFAX"
|
||||
|
||||
def test_process_exception(self):
|
||||
self.assertIsInstance(self.a.process_exception(self.request1, self.exception1), middleware.JsonError)
|
||||
self.assertIsNone(self.a.process_exception(self.request1, self.exception0))
|
||||
self.assertIsNone(self.a.process_exception(self.request0, self.exception1))
|
||||
self.assertIsNone(self.a.process_exception(self.request0, self.exception0))
|
||||
def test_process_exception(self):
|
||||
self.assertIsInstance(self.a.process_exception(self.request1, self.exception1), middleware.JsonError)
|
||||
self.assertIsInstance(self.a.process_exception(self.request1, self.exception2), middleware.JsonError)
|
||||
self.assertIsNone(self.a.process_exception(self.request1, self.exception0))
|
||||
self.assertIsNone(self.a.process_exception(self.request0, self.exception1))
|
||||
self.assertIsNone(self.a.process_exception(self.request0, self.exception0))
|
||||
|
||||
@@ -111,7 +111,7 @@ def peer_grading(request, course_id):
|
||||
#Get the peer grading modules currently in the course
|
||||
items = modulestore().get_items(['i4x', None, course_id_parts[1], 'peergrading', None])
|
||||
#See if any of the modules are centralized modules (ie display info from multiple problems)
|
||||
items = [i for i in items if i.metadata.get("use_for_single_location", True) in false_dict]
|
||||
items = [i for i in items if getattr(i,"use_for_single_location", True) in false_dict]
|
||||
#Get the first one
|
||||
item_location = items[0].location
|
||||
#Generate a url for the first module and redirect the user to it
|
||||
|
||||
@@ -74,6 +74,15 @@ def to_latex(x):
|
||||
# LatexPrinter._print_dot = _print_dot
|
||||
xs = latex(x)
|
||||
xs = xs.replace(r'\XI', 'XI') # workaround for strange greek
|
||||
|
||||
# substitute back into latex form for scripts
|
||||
# literally something of the form
|
||||
# 'scriptN' becomes '\\mathcal{N}'
|
||||
# note: can't use something akin to the _print_hat method above because we sometimes get 'script(N)__B' or more complicated terms
|
||||
xs = re.sub(r'script([a-zA-Z0-9]+)',
|
||||
'\\mathcal{\\1}',
|
||||
xs)
|
||||
|
||||
#return '<math>%s{}{}</math>' % (xs[1:-1])
|
||||
if xs[0] == '$':
|
||||
return '[mathjax]%s[/mathjax]<br>' % (xs[1:-1]) # for sympy v6
|
||||
@@ -106,6 +115,7 @@ def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False
|
||||
'i': sympy.I, # lowercase i is also sqrt(-1)
|
||||
'Q': sympy.Symbol('Q'), # otherwise it is a sympy "ask key"
|
||||
'I': sympy.Symbol('I'), # otherwise it is sqrt(-1)
|
||||
'N': sympy.Symbol('N'), # or it is some kind of sympy function
|
||||
#'X':sympy.sympify('Matrix([[0,1],[1,0]])'),
|
||||
#'Y':sympy.sympify('Matrix([[0,-I],[I,0]])'),
|
||||
#'Z':sympy.sympify('Matrix([[1,0],[0,-1]])'),
|
||||
@@ -247,6 +257,127 @@ class formula(object):
|
||||
fix_hat(k)
|
||||
fix_hat(xml)
|
||||
|
||||
def flatten_pmathml(xml):
|
||||
''' Give the text version of certain PMathML elements
|
||||
|
||||
Sometimes MathML will be given with each letter separated (it
|
||||
doesn't know if its implicit multiplication or what). From an xml
|
||||
node, find the (text only) variable name it represents. So it takes
|
||||
<mrow>
|
||||
<mi>m</mi>
|
||||
<mi>a</mi>
|
||||
<mi>x</mi>
|
||||
</mrow>
|
||||
and returns 'max', for easier use later on.
|
||||
'''
|
||||
tag = gettag(xml)
|
||||
if tag == 'mn': return xml.text
|
||||
elif tag == 'mi': return xml.text
|
||||
elif tag == 'mrow': return ''.join([flatten_pmathml(y) for y in xml])
|
||||
raise Exception, '[flatten_pmathml] unknown tag %s' % tag
|
||||
|
||||
def fix_mathvariant(parent):
|
||||
'''Fix certain kinds of math variants
|
||||
|
||||
Literally replace <mstyle mathvariant="script"><mi>N</mi></mstyle>
|
||||
with 'scriptN'. There have been problems using script_N or script(N)
|
||||
'''
|
||||
for child in parent:
|
||||
if (gettag(child) == 'mstyle' and child.get('mathvariant') == 'script'):
|
||||
newchild = etree.Element('mi')
|
||||
newchild.text = 'script%s' % flatten_pmathml(child[0])
|
||||
parent.replace(child, newchild)
|
||||
fix_mathvariant(child)
|
||||
fix_mathvariant(xml)
|
||||
|
||||
|
||||
# find "tagged" superscripts
|
||||
# they have the character \u200b in the superscript
|
||||
# replace them with a__b so snuggle doesn't get confused
|
||||
def fix_superscripts(xml):
|
||||
''' Look for and replace sup elements with 'X__Y' or 'X_Y__Z'
|
||||
|
||||
In the javascript, variables with '__X' in them had an invisible
|
||||
character inserted into the sup (to distinguish from powers)
|
||||
E.g. normal:
|
||||
<msubsup>
|
||||
<mi>a</mi>
|
||||
<mi>b</mi>
|
||||
<mi>c</mi>
|
||||
</msubsup>
|
||||
to be interpreted '(a_b)^c' (nothing done by this method)
|
||||
|
||||
And modified:
|
||||
<msubsup>
|
||||
<mi>b</mi>
|
||||
<mi>x</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>d</mi>
|
||||
</mrow>
|
||||
</msubsup>
|
||||
to be interpreted 'a_b__c'
|
||||
|
||||
also:
|
||||
<msup>
|
||||
<mi>x</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>B</mi>
|
||||
</mrow>
|
||||
</msup>
|
||||
to be 'x__B'
|
||||
'''
|
||||
for k in xml:
|
||||
tag = gettag(k)
|
||||
|
||||
# match things like the last example--
|
||||
# the second item in msub is an mrow with the first
|
||||
# character equal to \u200b
|
||||
if (tag == 'msup' and
|
||||
len(k) == 2 and gettag(k[1]) == 'mrow' and
|
||||
gettag(k[1][0]) == 'mo' and k[1][0].text == u'\u200b'): # whew
|
||||
|
||||
# replace the msup with 'X__Y'
|
||||
k[1].remove(k[1][0])
|
||||
newk = etree.Element('mi')
|
||||
newk.text = '%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]))
|
||||
xml.replace(k, newk)
|
||||
|
||||
# match things like the middle example-
|
||||
# the third item in msubsup is an mrow with the first
|
||||
# character equal to \u200b
|
||||
if (tag == 'msubsup' and
|
||||
len(k) == 3 and gettag(k[2]) == 'mrow' and
|
||||
gettag(k[2][0]) == 'mo' and k[2][0].text == u'\u200b'): # whew
|
||||
|
||||
# replace the msubsup with 'X_Y__Z'
|
||||
k[2].remove(k[2][0])
|
||||
newk = etree.Element('mi')
|
||||
newk.text = '%s_%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]), flatten_pmathml(k[2]))
|
||||
xml.replace(k, newk)
|
||||
|
||||
fix_superscripts(k)
|
||||
fix_superscripts(xml)
|
||||
|
||||
# Snuggle returns an error when it sees an <msubsup>
|
||||
# replace such elements with an <msup>, except the first element is of
|
||||
# the form a_b. I.e. map a_b^c => (a_b)^c
|
||||
def fix_msubsup(parent):
|
||||
for child in parent:
|
||||
# fix msubsup
|
||||
if (gettag(child) == 'msubsup' and len(child) == 3):
|
||||
newchild = etree.Element('msup')
|
||||
newbase = etree.Element('mi')
|
||||
newbase.text = '%s_%s' % (flatten_pmathml(child[0]), flatten_pmathml(child[1]))
|
||||
newexp = child[2]
|
||||
newchild.append(newbase)
|
||||
newchild.append(newexp)
|
||||
parent.replace(child, newchild)
|
||||
|
||||
fix_msubsup(child)
|
||||
fix_msubsup(xml)
|
||||
|
||||
self.xml = xml
|
||||
return self.xml
|
||||
|
||||
@@ -257,6 +388,7 @@ class formula(object):
|
||||
try:
|
||||
xml = self.preprocess_pmathml(self.expr)
|
||||
except Exception, err:
|
||||
log.warning('Err %s while preprocessing; expr=%s' % (err, self.expr))
|
||||
return "<html>Error! Cannot process pmathml</html>"
|
||||
pmathml = etree.tostring(xml, pretty_print=True)
|
||||
self.the_pmathml = pmathml
|
||||
|
||||
115
lms/lib/symmath/test_formula.py
Normal file
115
lms/lib/symmath/test_formula.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
Tests of symbolic math
|
||||
"""
|
||||
|
||||
|
||||
import unittest
|
||||
import formula
|
||||
import re
|
||||
from lxml import etree
|
||||
|
||||
def stripXML(xml):
|
||||
xml = xml.replace('\n', '')
|
||||
xml = re.sub(r'\> +\<', '><', xml)
|
||||
return xml
|
||||
|
||||
class FormulaTest(unittest.TestCase):
|
||||
# for readability later
|
||||
mathml_start = '<math xmlns="http://www.w3.org/1998/Math/MathML"><mstyle displaystyle="true">'
|
||||
mathml_end = '</mstyle></math>'
|
||||
|
||||
def setUp(self):
|
||||
self.formulaInstance = formula.formula('')
|
||||
|
||||
def test_replace_mathvariants(self):
|
||||
expr = '''
|
||||
<mstyle mathvariant="script">
|
||||
<mi>N</mi>
|
||||
</mstyle>'''
|
||||
|
||||
expected = '<mi>scriptN</mi>'
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
self.assertEqual(test, expected)
|
||||
|
||||
|
||||
def test_fix_simple_superscripts(self):
|
||||
expr = '''
|
||||
<msup>
|
||||
<mi>a</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>b</mi>
|
||||
</mrow>
|
||||
</msup>'''
|
||||
|
||||
expected = '<mi>a__b</mi>'
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
self.assertEqual(test, expected)
|
||||
|
||||
def test_fix_complex_superscripts(self):
|
||||
expr = '''
|
||||
<msubsup>
|
||||
<mi>a</mi>
|
||||
<mi>b</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>c</mi>
|
||||
</mrow>
|
||||
</msubsup>'''
|
||||
|
||||
expected = '<mi>a_b__c</mi>'
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
self.assertEqual(test, expected)
|
||||
|
||||
|
||||
def test_fix_msubsup(self):
|
||||
expr = '''
|
||||
<msubsup>
|
||||
<mi>a</mi>
|
||||
<mi>b</mi>
|
||||
<mi>c</mi>
|
||||
</msubsup>'''
|
||||
|
||||
expected = '<msup><mi>a_b</mi><mi>c</mi></msup>' # which is (a_b)^c
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
self.assertEqual(test, expected)
|
||||
Reference in New Issue
Block a user