Created
July 18, 2011 15:13
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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