Skip to content

Instantly share code, notes, and snippets.

@mitchellrj
Created July 18, 2011 15:13
Show Gist options
  • Save mitchellrj/1089811 to your computer and use it in GitHub Desktop.
Save mitchellrj/1089811 to your computer and use it in GitHub Desktop.
A quick script to make your Plone 3 templates suitable for use in Plone 4 by inserting definitions of globals where required.See http://plone.org/documentation/manual/upgrade-guide/version/upgrading-plone-3-x-to-4.0/updating-add-on-products-for-plone-4.0/no-more-global-definitions-in-templates.
#! /usr/bin/env python
#
# Copyright 2011 Richard Mitchell
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
fix_plone_4_template_vars
-------------------------
Given two arguments, file_in and file_out, reads a ZPT from file_in, searches
for undefined usages of variables previously in global defines and inserts
the relevant definitions at the highest possible level.
This script may not be foolproof! Always inspect any diffs by hand before
committing its output.
This is kind of scrappy and I'm sure there are efficiency gains to be made,
but for a one-off task, it's fine, IMO.
Pro-bash usage:
for f in `find . -iname '*.pt'`
do
fix_plone_4_template_vars $f $f
if test $? -gt 0
then
echo "Failed to fix template: $f"
fi
done
"""
from copy import copy
from lxml import etree
import os
import sys
from StringIO import StringIO
tal_namespace = ''
# we assume no-one will be attempting to divide by any of these values in Python.
invalid_identifier_start_chars = '[^a-zA-Z\./]'
invalid_identifier_end_chars = '([^a-zA-Z0-9_]|\\b)'
definition_re = '(^|;)\s*((local|global)\s+)?%s\s+'
talNS = 'http://xml.zope.org/namespaces/tal'
namespaces = {'tal':talNS,
're': 'http://exslt.org/regular-expressions',
'metal': 'http://xml.zope.org/namespaces/metal',
'i18n': 'http://xml.zope.org/namespaces/i18n'}
variables = {
'template_id': 'template/getId',
'normalizeString': 'nocall:context/@@plone/normalizeString',
'toLocalizedTime': 'nocall:context/@@plone/toLocalizedTime',
'portal_properties': 'context/portal_properties',
'site_properties': 'context/portal_properties/site_properties',
'here_url': 'context/@@plone_context_state/object_url',
'portal': 'context/@@plone_portal_state/portal',
'isAnon': 'context/@@plone_portal_state/anonymous',
'member': 'context/@@plone_portal_state/member',
'actions': 'python:context.portal_actions.' + \
'listFilteredActionsFor(context)',
'mtool': 'context/portal_membership',
'wtool': 'context/portal_workflow',
'wf_state': 'context/@@plone_context_state/workflow_state',
'default_language': 'context/@@plone_portal_state/default_language',
'is_editable': 'context/@@plone_context_state/is_editable',
'isContextDefaultPage': 'context/@@plone_context_state/is_default_page',
'object_title': 'context/@@plone_context_state/object_title',
'putils': 'context/plone_utils',
'ztu': 'modules/ZTUtils',
'acl_users': 'context/acl_users',
'ifacetool': 'context/portal_interface',
'syntool': 'context/portal_syndication',
}
def get_undefined_usage_nodes(tree, variable):
""" Returns a set of nodes that use the given variable in TAL,
but do not descend from a node which defines it, using one
epic XPath.
"""
defined_space_query = ('//tal:*[re:match(@define, "%(definition_re)s")]/ancestor-or-self::*| ' +\
'//*[re:match(@tal:define, "%(definition_re)s")]/ancestor-or-self::*') % \
{'definition_re': definition_re % (variable,)}
usage_nodes = []
for tal in ('define', 'replace', 'attributes', 'condition',
'omit-tag', 'on-error', 'repeat'):
usage_nodes += map(lambda s: s % {'tal': tal,
'usage_re': '%s%s%s' % \
(invalid_identifier_start_chars, variable,
invalid_identifier_end_chars)},
['//tal:*[re:match(@%(tal)s, "%(usage_re)s")]', \
'//*[re:match(@tal:%(tal)s, "%(usage_re)s")]'])
xpath = '(%(usage)s)[not(%(definition_query)s)]' % \
{'usage': '|'.join(usage_nodes),
'definition_query': defined_space_query}
return tree.xpath(xpath, namespaces=namespaces)
def add_var_to_most_common_ancestors(tree, nodes, variable):
""" Given a tree, a variable and a set of nodes that make use of the
variable, inserts the correct definition of the variable at
the highest common ancestor of all the given nodes.
If the given nodes do not share any common ancestors between them,
the nodes are grouped in the largest numbers possible to achieve
the minimum number of definitions.
"""
if not nodes:
return
current_set = copy(nodes)
remaining_nodes = copy(nodes)
common_nodes = set(list(nodes[0].iterancestors())+[nodes[0],])
for n in nodes:
new_common_nodes = common_nodes.intersection(set(list(n.iterancestors())+[n,]))
if new_common_nodes:
common_nodes = new_common_nodes
remaining_nodes.remove(n)
if common_nodes:
# There should always be at least some common nodes, even if it's just one,
# but best to check, eh?
common_nodes = list(common_nodes)
# sort by depth, descending
common_nodes.sort(lambda x,y: cmp(len(tree.getpath(x).split('/')),
len(tree.getpath(y).split('/'))),
reverse=True)
node = common_nodes[0]
insert_definition(node, variable)
if remaining_nodes:
# recurse if we couldn't find a single common ancestor
add_var_to_most_common_ancestors(tree, remaining_nodes, variable)
def insert_definition(node, variable):
""" Inserts the relevant definition of the given variable at the
given node.
"""
attrib = '{%s}define' % (talNS,)
if node.tag.startswith('{%s}'%(talNS,)) and node.attrib.get('define'):
attrib = 'define'
if attrib not in node.attrib:
node.attrib[attrib]=''
node.attrib[attrib] = '%s %s;%s' % \
(variable, variables[variable], node.attrib[attrib])
def fix_up_template(file_in, file_out):
""" Given an input file object (or path) and an output file object
(or path), inserts definitions as appropriate for all variables
removed as part of the removal of global defines in Plone 4.
"""
tree = etree.parse(file_in, parser=etree.XMLParser(strip_cdata=False))
for var in variables:
nodes = get_undefined_usage_nodes(tree, var)
add_var_to_most_common_ancestors(tree, nodes, var)
tree.write(file_out)
if __name__ == '__main__':
if not os.path.isfile(sys.argv[1]):
print "%s is not a file" % sys.argv[1]
sys.exit(1)
if sys.argv[1]!=sys.argv[2]:
file_in = open(sys.argv[1], 'r')
file_out = open(sys.argv[2], 'w')
else:
file_in = open(sys.argv[1], 'r')
file_out = StringIO('')
try:
fix_up_template(file_in, file_out)
except Exception, e:
sys.stderr.write('%s\n' % (e,))
sys.exit(1)
finally:
file_in.close()
if sys.argv[1]==sys.argv[2]:
fo = open(sys.argv[2], 'w')
file_out.seek(0)
fo.write(file_out.read())
fo.close()
file_out.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment