Commit cc059e24 authored by Hanno Schlichting's avatar Hanno Schlichting

Integrate `five.pt` code directly into `Products.PageTemplates`.

Based on zopefoundation/five.pt@197e65819013fbfd3a436286737f20ddc4c48813.
parent 7ea5fc71
......@@ -30,6 +30,8 @@ Features Added
Restructuring
+++++++++++++
- Integrate `five.pt` code directly into `Products.PageTemplates`.
- Move `Products.SiteAccess` into ZServer distribution.
- Simplify Page Template and Scripts ZMI screens.
......
......@@ -3,6 +3,7 @@ AccessControl==4.0a3
Acquisition==4.2.2
AuthEncoding==4.0.0
BTrees==4.3.1
Chameleon==2.24
DateTime==4.1.1
DocumentTemplate==2.13.2
ExtensionClass==4.1.2
......@@ -35,9 +36,11 @@ persistent==4.2.1
pytz==2016.6.1
repoze.retry==1.4
six==1.10.0
sourcecodegen==0.6.14
tempstorage==3.0
transaction==1.6.1
waitress==0.9.0
z3c.pt==2.2.3
zExceptions==3.3
zLOG==3.0
zc.lockfile==1.2.1
......
......@@ -65,9 +65,11 @@ setup(
'repoze.retry',
'setuptools',
'six',
'sourcecodegen',
'transaction',
'waitress',
'zExceptions >= 3.2',
'z3c.pt',
'zope.browser',
'zope.browsermenu',
'zope.browserpage >= 4.0',
......
<configure xmlns="http://namespaces.zope.org/zope">
<include package="z3c.pt" />
<utility
provides="Products.PageTemplates.interfaces.IUnicodeEncodingConflictResolver"
component="Products.PageTemplates.unicodeconflictresolver.PreferredCharsetResolver"
/>
<utility component=".engine.Program" />
</configure>
import re
import logging
from zope.interface import implements
from zope.interface import classProvides
from zope.pagetemplate.interfaces import IPageTemplateEngine
from zope.pagetemplate.interfaces import IPageTemplateProgram
from z3c.pt.pagetemplate import PageTemplate as ChameleonPageTemplate
from AccessControl.class_init import InitializeClass
from AccessControl.SecurityInfo import ClassSecurityInfo
from Products.PageTemplates.Expressions import getEngine
from Products.PageTemplates import ZRPythonExpr
from chameleon.tales import StringExpr
from chameleon.tales import NotExpr
from chameleon.tal import RepeatDict
from z3c.pt.expressions import PythonExpr, ProviderExpr
from .expression import PathExpr
from .expression import TrustedPathExpr
from .expression import NocallExpr
from .expression import ExistsExpr
from .expression import UntrustedPythonExpr
# Declare Chameleon's repeat dictionary public
RepeatDict.security = ClassSecurityInfo()
RepeatDict.security.declareObjectPublic()
RepeatDict.__allow_access_to_unprotected_subobjects__ = True
InitializeClass(RepeatDict)
re_match_pi = re.compile(r'<\?python([^\w].*?)\?>', re.DOTALL)
logger = logging.getLogger('Products.PageTemplates')
class Program(object):
implements(IPageTemplateProgram)
classProvides(IPageTemplateEngine)
# Zope 2 Page Template expressions
secure_expression_types = {
'python': UntrustedPythonExpr,
'string': StringExpr,
'not': NotExpr,
'exists': ExistsExpr,
'path': PathExpr,
'provider': ProviderExpr,
'nocall': NocallExpr,
}
# Zope 3 Page Template expressions
expression_types = {
'python': PythonExpr,
'string': StringExpr,
'not': NotExpr,
'exists': ExistsExpr,
'path': TrustedPathExpr,
'provider': ProviderExpr,
'nocall': NocallExpr,
}
extra_builtins = {
'modules': ZRPythonExpr._SecureModuleImporter()
}
def __init__(self, template):
self.template = template
def __call__(self, context, macros, tal=True, **options):
if tal is False:
return self.template.body
# Swap out repeat dictionary for Chameleon implementation
# and store wrapped dictionary in new variable -- this is
# in turn used by the secure Python expression
# implementation whenever a 'repeat' symbol is found
kwargs = context.vars
kwargs['wrapped_repeat'] = kwargs['repeat']
kwargs['repeat'] = RepeatDict(context.repeat_vars)
return self.template.render(**kwargs)
@classmethod
def cook(cls, source_file, text, engine, content_type):
if engine is getEngine():
def sanitize(m):
match = m.group(1)
logger.info(
'skipped "<?python%s?>" code block in '
'Zope 2 page template object "%s".',
match, source_file
)
return ''
text, count = re_match_pi.subn(sanitize, text)
if count:
logger.warning(
"skipped %d code block%s (not allowed in "
"restricted evaluation scope)." % (
count, 's' if count > 1 else ''
)
)
expression_types = cls.secure_expression_types
else:
expression_types = cls.expression_types
# BBB: Support CMFCore's FSPagetemplateFile formatting
if source_file is not None and source_file.startswith('file:'):
source_file = source_file[5:]
template = ChameleonPageTemplate(
text, filename=source_file, keep_body=True,
expression_types=expression_types,
encoding='utf-8', extra_builtins=cls.extra_builtins,
)
return cls(template), template.macros
from ast import NodeTransformer
from types import ClassType
from compiler import parse as ast24_parse
from OFS.interfaces import ITraversable
from zExceptions import NotFound, Unauthorized
from zope.traversing.adapters import traversePathElement
from zope.traversing.interfaces import TraversalError
from RestrictedPython.RestrictionMutator import RestrictionMutator
from RestrictedPython.Utilities import utility_builtins
from RestrictedPython import MutatingWalker
from Products.PageTemplates.Expressions import render
from AccessControl.ZopeGuards import guarded_getattr
from AccessControl.ZopeGuards import guarded_getitem
from AccessControl.ZopeGuards import guarded_apply
from AccessControl.ZopeGuards import guarded_iter
from AccessControl.ZopeGuards import protected_inplacevar
from chameleon.astutil import Symbol
from chameleon.astutil import Static
from chameleon.codegen import template
from sourcecodegen import generate_code
from z3c.pt import expressions
_marker = object()
zope2_exceptions = (
AttributeError,
LookupError,
NameError,
TypeError,
ValueError,
NotFound,
Unauthorized,
TraversalError,
)
def static(obj):
return Static(template("obj", obj=Symbol(obj), mode="eval"))
class BoboAwareZopeTraverse(object):
traverse_method = 'restrictedTraverse'
__slots__ = ()
@classmethod
def traverse(cls, base, request, path_items):
"""See ``zope.app.pagetemplate.engine``."""
length = len(path_items)
if length:
i = 0
method = cls.traverse_method
while i < length:
name = path_items[i]
i += 1
if ITraversable.providedBy(base):
traverser = getattr(base, method)
base = traverser(name)
else:
base = traversePathElement(
base, name, path_items[i:], request=request
)
return base
def __call__(self, base, econtext, call, path_items):
request = econtext.get('request')
if path_items:
base = self.traverse(base, request, path_items)
if call is False:
return base
if getattr(base, '__call__', _marker) is not _marker or callable(base):
base = render(base, econtext)
return base
class TrustedBoboAwareZopeTraverse(BoboAwareZopeTraverse):
traverse_method = 'unrestrictedTraverse'
__slots__ = ()
def __call__(self, base, econtext, call, path_items):
request = econtext.get('request')
base = self.traverse(base, request, path_items)
if call is False:
return base
if (getattr(base, '__call__', _marker) is not _marker or
isinstance(base, ClassType)):
return base()
return base
class PathExpr(expressions.PathExpr):
exceptions = zope2_exceptions
traverser = Static(template(
"cls()", cls=Symbol(BoboAwareZopeTraverse), mode="eval"
))
class TrustedPathExpr(PathExpr):
traverser = Static(template(
"cls()", cls=Symbol(TrustedBoboAwareZopeTraverse), mode="eval"
))
class NocallExpr(expressions.NocallExpr, PathExpr):
pass
class ExistsExpr(expressions.ExistsExpr):
exceptions = zope2_exceptions
class RestrictionTransform(NodeTransformer):
secured = {
'_getattr_': guarded_getattr,
'_getitem_': guarded_getitem,
'_apply_': guarded_apply,
'_getiter_': guarded_iter,
'_inplacevar_': protected_inplacevar,
}
def visit_Name(self, node):
value = self.secured.get(node.id)
if value is not None:
return Symbol(value)
return node
class UntrustedPythonExpr(expressions.PythonExpr):
rm = RestrictionMutator()
rt = RestrictionTransform()
# Make copy of parent expression builtins
builtins = expressions.PythonExpr.builtins.copy()
# Update builtins with Restricted Python utility builtins
builtins.update(dict(
(name, static(builtin)) for (name, builtin) in utility_builtins.items()
))
def rewrite(self, node):
if node.id == 'repeat':
node.id = 'wrapped_repeat'
else:
node = super(UntrustedPythonExpr, self).rewrite(node)
return node
def parse(self, string):
encoded = string.encode('utf-8')
node = ast24_parse(encoded, 'eval').node
MutatingWalker.walk(node, self.rm)
string = generate_code(node)
decoded = string.decode('utf-8')
value = super(UntrustedPythonExpr, self).parse(decoded)
# Run restricted python transform
self.rt.visit(value)
return value
<div tal:condition="exists: options/callable">ok</div>
\ No newline at end of file
<div xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<div tal:replace="string:view:${view/available|nothing}" />
<div tal:replace="python:'here==context:'+str(here==context)" />
<div tal:replace="python:'here==container:'+str(here==container)" />
<div tal:replace="string:root:${root/getPhysicalPath}" />
<div tal:replace="string:nothing:${nothing}" />
<div tal:define="cgi python:modules['cgi']"
tal:replace="python: dir(cgi)"
tal:on-error="nothing" />
<tal:error on-error="nothing">
<div tal:define="test python: test" tal:replace="python: test" />
<div tal:define="same python: same_type" tal:replace="python: same" />
</tal:error>
</div>
<div xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<div tal:replace="python:context" />
<div tal:replace="python:container" />
</div>
<div xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="test">
<span tal:content="options/missing">
MV
</span>
</div>
<div tal:content="structure nocall: options/callable" />
\ No newline at end of file
<div xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<div tal:repeat="key options">
<div tal:replace="key" /> : <div tal:replace="python:options[key]" />
</div>
</div>
<p xmlns="http://www.w3.org/1999/xhtml"><?python 1/0 ?>Hello world</p>
<html>
<body>
<ul tal:define="refs options/refs;
refs python:sorted(refs, lambda x,y: cmp(x.order, y.order))">
<li tal:repeat="ref refs" tal:content="ref" />
</ul>
</body>
</html>
\ No newline at end of file
<div xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<span tal:define="soup view/tagsoup | options/soup"
tal:replace="structure python: modules['cgi'].escape(soup)" />
</div>
<div xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
tal:define="name string:world">
Hello ${name}!
</div>
import os
import unittest
from Testing.ZopeTestCase import ZopeTestCase
from Testing.ZopeTestCase.sandbox import Sandboxed
path = os.path.dirname(__file__)
class TestPatches(Sandboxed, ZopeTestCase):
def afterSetUp(self):
from Zope2.App import zcml
import Products.PageTemplates
zcml.load_config("configure.zcml", Products.PageTemplates)
def test_pagetemplate(self):
from Products.PageTemplates.PageTemplate import PageTemplate
template = PageTemplate()
# test rendering engine
template.write(open(os.path.join(path, "simple.pt")).read())
self.assertTrue('world' in template())
# test arguments
template.write(open(os.path.join(path, "options.pt")).read())
self.assertTrue('Hello world' in template(greeting='Hello world'))
def test_pagetemplatefile(self):
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
# test rendering engine
template = PageTemplateFile(os.path.join(path, "simple.pt"))
template = template.__of__(self.folder)
self.assertTrue('world' in template())
def test_pagetemplatefile_processing_instruction_skipped(self):
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
# test rendering engine
template = PageTemplateFile(os.path.join(path, "pi.pt"))
template = template.__of__(self.folder)
self.assertIn('world', template())
def test_zopepagetemplate(self):
from Products.PageTemplates.ZopePageTemplate import \
manage_addPageTemplate
template = manage_addPageTemplate(self.folder, 'test')
# aq-wrap before we proceed
template = template.__of__(self.folder)
# test rendering engine
template.write(open(os.path.join(path, "simple.pt")).read())
self.assertTrue('world' in template())
# test arguments
template.write(open(os.path.join(path, "options.pt")).read())
self.assertTrue('Hello world' in template(
greeting='Hello world'))
# test commit
import transaction
transaction.commit()
def test_zopepagetemplate_processing_instruction_skipped(self):
from Products.PageTemplates.ZopePageTemplate import \
manage_addPageTemplate
template = manage_addPageTemplate(self.folder, 'test')
# aq-wrap before we proceed
template = template.__of__(self.folder)
# test rendering engine
template.write(open(os.path.join(path, "pi.pt")).read())
self.assertIn('world', template())
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(TestPatches),
))
import os
import unittest
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Testing.ZopeTestCase import ZopeTestCase
path = os.path.dirname(__file__)
class TestPageTemplateFile(ZopeTestCase):
def afterSetUp(self):
from Zope2.App import zcml
import Products.PageTemplates
zcml.load_config("configure.zcml", Products.PageTemplates)
def _makeOne(self, name):
return PageTemplateFile(os.path.join(path, name)).__of__(self.app)
def test_rr(self):
class Prioritzed(object):
__allow_access_to_unprotected_subobjects__ = 1
def __init__(self, order):
self.order = order
def __str__(self):
return 'P%d' % self.order
template = self._makeOne('rr.pt')
result = template(refs=[Prioritzed(1), Prioritzed(2)])
self.assertTrue('P1' in result)
self.assertTrue(result.index('P1') < result.index('P2'))
def test_locals(self):
template = self._makeOne('locals.pt')
result = template()
self.assertTrue('function test' in result)
self.assertTrue('function same_type' in result)
def test_locals_base(self):
template = self._makeOne('locals_base.pt')
result = template()
self.assertTrue('Application' in result)
def test_nocall(self):
template = self._makeOne("nocall.pt")
def dont_call():
raise RuntimeError()
result = template(callable=dont_call)
self.assertTrue(repr(dont_call) in result)
def test_exists(self):
template = self._makeOne("exists.pt")
def dont_call():
raise RuntimeError()
result = template(callable=dont_call)
self.assertTrue('ok' in result)
def test_simple(self):
template = self._makeOne("simple.pt")
result = template()
self.assertTrue('Hello world!' in result)
def test_secure(self):
soup = '<foo></bar>'
template = self._makeOne("secure.pt")
from zExceptions import Unauthorized
try:
result = template(soup=soup)
except Unauthorized:
pass
else:
self.fail("Expected unauthorized.")
from AccessControl.SecurityInfo import allow_module
allow_module("cgi")
result = template(soup=soup)
self.assertTrue('&lt;foo&gt;&lt;/bar&gt;' in result)
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(TestPageTemplateFile),
))
import cgi
import re
import unittest
from Products.PageTemplates.ZopePageTemplate import manage_addPageTemplate
from Testing.ZopeTestCase import ZopeTestCase
macro_outer = """
<metal:defm define-macro="master">
<metal:defs define-slot="main_slot">
Outer Slot
</metal:defs>
</metal:defm>
""".strip()
macro_middle = """
<metal:def define-macro="master">
<metal:use use-macro="here/macro_outer/macros/master">
<metal:fill fill-slot="main_slot">
<metal:defs define-slot="main_slot">
Middle Slot
</metal:defs>
</metal:fill>
</metal:use>
</metal:def>
""".strip()
macro_inner = """
<metal:use use-macro="here/macro_middle/macros/master">
<metal:fills fill-slot="main_slot">
<tal:block i18n:domain="mydomain" i18n:translate="">
Inner Slot
</tal:block>
</metal:fills>
</metal:use>
""".strip()
simple_i18n = """
<tal:block i18n:domain="mydomain" i18n:translate="">
Hello, World
</tal:block>
""".strip()
simple_error = """
<tal:block content__typo="nothing">
it's an error to use an unknown attribute in the tal namespace
</tal:block>
""".strip()
repeat_object = """
<tal:loop repeat="counter python: range(3)"
content="python: repeat['counter'].index" />
""".strip()
options_capture_update_base = """
<metal:use use-macro="context/macro_outer/macros/master">
<metal:fills fill-slot="main_slot">
<tal:block define="dummy python: capture.update(%s)" />
</metal:fills>
</metal:use>
""".strip()
lp_848200_source = """
<tal:block>
<tag tal:condition="False"
tal:attributes="attrib string:false" />
<tag tal:condition="True"
tal:attributes="attrib string:true" />
</tal:block>
""".strip()
tal_onerror_structure_source = """
<tal:block tal:on-error="structure python: '&lt;i&gt;error!&lt;/i&gt;'">
<i tal:content="python: 1/0" />
</tal:block>
""".strip()
python_nbsp_source = """
<p tal:content="structure python: '&amp;nbsp;'" />
""".strip()
python_path_source = """
<form tal:attributes="method python:path('context/method')" />
""".strip()
def generate_capture_source(names):
params = ", ".join("%s=%s" % (name, name)
for name in names)
return options_capture_update_base % (params,)
textarea_content_search = re.compile(
r'<textarea[^>]*>([^<]+)</textarea>',
re.IGNORECASE | re.MULTILINE
).search
def get_editable_content(template):
edit_form = template.pt_editForm()
editable_text = textarea_content_search(edit_form).group(1)
return editable_text
_marker = object()
class TestPersistent(ZopeTestCase):
def afterSetUp(self):
from Zope2.App import zcml
import Products.PageTemplates
zcml.load_config("configure.zcml", Products.PageTemplates)
self.setRoles(['Manager'])
def _makeOne(self, template_id, source):
return manage_addPageTemplate(self.folder, template_id, text=source)
def test_simple(self):
template = self._makeOne('foo', simple_i18n)
result = template().strip()
self.assertEqual(result, u'Hello, World')
editable_text = get_editable_content(template)
self.assertEqual(editable_text, cgi.escape(simple_i18n))
def test_escape_on_edit(self):
# check that escapable chars can round-trip intact.
source = u"&gt; &amp; &lt;"
template = self._makeOne('foo', source)
self.assertEqual(template(), source) # nothing to render
editable_text = get_editable_content(template)
self.assertEqual(editable_text, cgi.escape(source))
def test_macro_with_i18n(self):
self._makeOne('macro_outer', macro_outer)
self._makeOne('macro_middle', macro_middle)
inner = self._makeOne('macro_inner', macro_inner)
result = inner().strip()
self.assertEqual(result, u'Inner Slot')
def test_pt_render_with_macro(self):
# The pt_render method of ZopePageTemplates allows rendering the
# template with an expanded (and overriden) set of context
# variables.
# Lets test with some common and some unique variables:
extra_context = dict(form=object(),
context=self.folder,
here=object(),)
capture = dict((name, None) for name in extra_context)
source = generate_capture_source(capture)
self._makeOne('macro_outer', macro_outer)
template = self._makeOne('test_pt_render', source)
extra_context['capture'] = capture
template.pt_render(extra_context=extra_context)
del extra_context['capture']
self.assertEquals(extra_context, capture)
# pt_render is also used to retrieve the unrendered source for
# TTW editing purposes.
self.assertEqual(template.pt_render(source=True), source)
def test_avoid_recompilation(self):
template = self._makeOne('foo', simple_i18n)
# Template is already cooked
program = template._v_program
template.pt_render({})
# The program does not change
self.assertEqual(program, template._v_program)
def test_repeat_object_security(self):
template = self._makeOne('foo', repeat_object)
# this should not raise an Unauthorized error
self.assertEquals(template().strip(), u'012')
# XXX-leorochael: the rest of this test is not actually
# testing the security access, but I couldn't find a simpler
# way to test if the RepeatItem instance itself allows public
# access, and there are convoluted situations in production
# that need RepeatItem to be declared public.
src = """
<tal:b repeat="x python: range(1)">
<tal:b define="dummy python: options['do'](repeat)" />
</tal:b>
""".strip()
def do(repeat):
subobject_access = '__allow_access_to_unprotected_subobjects__'
self.assertTrue(getattr(repeat['x'], subobject_access, False))
template = self._makeOne('bar', src)
template(do=do)
def test_path_function(self):
# check that the "path" function inside a python expression works
self.folder.method = 'post'
template = self._makeOne('foo', python_path_source)
self.assertEquals(template(), u'<form method="post" />')
def test_filename_attribute(self):
# check that a persistent page template that happens to have
# a filename attribute doesn't break
template = self._makeOne('foo', repeat_object)
template.filename = 'some/random/path'
# this should still work, without trying to open some random
# file on the filesystem
self.assertEqual(template().strip(), u'012')
def test_edit_with_errors(self):
template = self._makeOne('foo', simple_error)
# this should not raise:
editable_text = get_editable_content(template)
# and the errors should be in an xml comment at the start of
# the editable text
error_prefix = cgi.escape(
'<!-- Page Template Diagnostics\n {0}\n-->\n'.format(
'\n '.join(template._v_errors)
)
)
self.assertTrue(editable_text.startswith(error_prefix))
def test_lp_848200(self):
# https://bugs.launchpad.net/chameleon.zpt/+bug/848200
template = self._makeOne('foo', lp_848200_source)
self.assertEqual(template().strip(), u'<tag attrib="true" />')
def test_onerror_structure(self):
template = self._makeOne('foo', tal_onerror_structure_source)
self.assertEqual(template().strip(), u'<i>error!</i>')
def test_python_nbsp(self):
template = self._makeOne('foo', python_nbsp_source)
self.assertEqual(template().strip(), u'<p>&nbsp;</p>')
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(TestPersistent),
))
import unittest
from Products.Five import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Testing.ZopeTestCase import ZopeTestCase
class SimpleView(BrowserView):
index = ViewPageTemplateFile('simple.pt')
class ProcessingInstructionTestView(BrowserView):
index = ViewPageTemplateFile('pi.pt')
class LocalsView(BrowserView):
def available(self):
return 'yes'
index = ViewPageTemplateFile('locals.pt')
class OptionsView(BrowserView):
index = ViewPageTemplateFile('options.pt')
class SecureView(BrowserView):
index = ViewPageTemplateFile('secure.pt')
__allow_access_to_unprotected_subobjects__ = True
def tagsoup(self):
return '<foo></bar>'
class MissingView(BrowserView):
index = ViewPageTemplateFile('missing.pt')
class TestPageTemplateFile(ZopeTestCase):
def afterSetUp(self):
from Zope2.App import zcml
import Products.PageTemplates
zcml.load_config("configure.zcml", Products.PageTemplates)
def test_simple(self):
view = SimpleView(self.folder, self.folder.REQUEST)
result = view.index()
self.assertTrue('Hello world!' in result)
def test_secure(self):
view = SecureView(self.folder, self.folder.REQUEST)
from zExceptions import Unauthorized
try:
result = view.index()
except Unauthorized:
self.fail("Unexpected exception.")
else:
self.assertTrue('&lt;foo&gt;&lt;/bar&gt;' in result)
def test_locals(self):
view = LocalsView(self.folder, self.folder.REQUEST)
result = view.index()
self.assertTrue("view:yes" in result)
self.assertTrue('here==context:True' in result)
self.assertTrue('here==container:True' in result)
self.assertTrue("root:(\'\',)" in result)
self.assertTrue("nothing:" in result)
self.assertTrue("rfc822" in result)
def test_options(self):
view = OptionsView(self.folder, self.folder.REQUEST)
options = dict(
a=1,
b=2,
c='abc',
)
result = view.index(**options)
self.assertTrue("a : 1" in result)
self.assertTrue("c : abc" in result)
def test_processing_instruction(self):
view = ProcessingInstructionTestView(self.folder, self.folder.REQUEST)
self.assertRaises(ZeroDivisionError, view.index)
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(TestPageTemplateFile),
))
......@@ -6,6 +6,7 @@ AccessControl = 4.0a3
AuthEncoding = 4.0.0
Acquisition = 4.2.2
BTrees = 4.3.1
Chameleon = 2.24
DateTime = 4.1.1
DocumentTemplate = 2.13.2
ExtensionClass = 4.1.2
......@@ -32,11 +33,13 @@ Record = 3.1
repoze.retry = 1.4
RestrictedPython = 3.6.0
six = 1.10.0
sourcecodegen = 0.6.14
tempstorage = 3.0
transaction = 1.6.1
waitress = 0.9.0
WebOb = 1.6.1
WebTest = 2.0.23
z3c.pt = 2.2.3
zc.lockfile = 1.2.1
ZConfig = 3.1.0
zdaemon = 4.1.0
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment