Skip to content
Browse files

Initial checkin of collective.xdv - things that are now in Diazo proper

svn path=/plone.app.theming/trunk/; revision=45605
  • Loading branch information...
0 parents commit c2d2b5a208518ba887a9e4a3f86125052176e9cb @optilude optilude committed Nov 7, 2010
597 README.txt
@@ -0,0 +1,597 @@
+============
+Introduction
+============
+
+This package offers a simple way to develop and deploy Plone themes using
+the `Diazo`_ theming engine. If you are not familiar with Diazo,
+check out the `Diazo documentation <http://diazo.org>`_.
+
+.. contents:: Contents
+
+Installation
+============
+
+``plone.app.theming`` depends on:
+
+ * `Plone`_ 4.0 or later.
+ * `plone.transformchain`_ to hook the transformation into the publisher
+ * `plone.registry`_ and `plone.app.registry`_ to manage settings
+ * `plone.autoform`_, `plone.z3cform`_ and `plone.app.z3cform`_ to render the
+ control panel
+ * `five.globalrequest`_ and `zope.globalrequest`_ for internal request
+ access
+ * `Diazo`_, the theming engine
+ * `lxml`_ to transform Plone's output into a themed page
+
+These will all be pulled in automatically if you are using zc.buildout and
+follow the installation instructions.
+
+To install ``plone.app.theming`` into your Plone instance, locate the file
+``buildout.cfg`` in the root of your Plone instance directory on the file
+system, and open it in a text editor. Locate the section that looks like
+this::
+
+ # extends = http://dist.plone.org/release/3.3/versions.cfg
+ extends = versions.cfg
+ versions = versions
+
+It may also have a URL in the "extends" section, similar to the commented-out
+first line, depending on whether you pull the Plone configuration from the
+network or locally.
+
+To add ``plone.app.theming`` to our setup, we need some slightly different
+versions of a couple of the packages, so we extend the base config with a
+version list from the good-py service, so change this part of the
+configuration so it looks like this::
+
+ extends =
+ versions.cfg
+ http://good-py.appspot.com/release/diazo/1.0b1
+ versions = versions
+
+Note that the last part of the URL above is the Diazo version number. There
+may be a newer version by the time you read this, so check out the `overview
+page <http://good-py.appspot.com/release/plone.app.theming>`_ for the known
+good set.
+
+What happens here is that the dependency list for ``plone.app.theming``
+specifies some new versions for you via the good-py URL. This way, you don't
+have to worry about getting the right versions, Buildout will handle it for
+you.
+
+Next step is to add the actual ``plone.app.theming`` add-on to the "eggs"
+section of ``buildout.cfg``. Look for the section that looks like this::
+
+ eggs =
+ Plone
+
+This section might have additional lines if you have other add-ons already
+installed. Just add the ``plone.app.theming`` on a separate line, like this::
+
+ eggs =
+ Plone
+ plone.app.theming
+
+(Note that there is no need to add a ZCML slug as ``plone.app.theming`` uses
+``z3c.autoinclude`` to configure itself automatically.)
+
+Once you have added these lines to your configuration file, it's time to run
+buildout, so the system can add and set up ``plone.app.theming`` for you. Go
+to the command line, and from the root of your Plone instance (same directory
+as buildout.cfg is located in), run buildout like this::
+
+ $ bin/buildout
+
+You will see output similar to this::
+
+ Getting distribution for 'plone.app.theming==1.0b1'.
+ Got plone.app.theming 1.0b1.
+ Getting distribution for 'plone.app.registry'.
+ Got plone.app.registry 1.0b2.
+ ...
+
+If everything went according to plan, you now have ``plone.app.theming``
+installed in your Zope instance.
+
+Next, start up Zope, e.g with::
+
+ $ bin/instance fg
+
+Then go to the "Add-ons" control panel in Plone as an administrator, and
+install the "Diazo theme support" product. You should then notice a new
+"Diazo theme" control panel in Plone's site setup.
+
+Usage
+=====
+
+In the "Diazo Theme" control panel, you can set the following options:
+
+ Enabled yes/no
+ Whether or not the transform is enabled.
+
+ Rules
+ URL referencing the Diazo rules file. This file in turn references your
+ theme. The URL may be a filesystem or remove URL (in which case you want
+ to enable "read network access" - see below). It can also be an absolute
+ path, starting with a ``/``, in which case it will be resolved relative
+ to the Plone site root, or a special ``python://`` URL - see below.
+
+ Absolute prefix
+ If given, any relative URL in an ``<img />``, ``<link />``, ``<style />``
+ or ``<script />`` in the theme HTML file will be prefixed by this URL
+ snippet when the theme is compiled. This makes it easier to develop theme
+ HTML/CSS on the file system using relative paths that still work on any
+ URL on the server.
+
+ Read network
+ By default, Diazo will not attempt to resolve external URLs referenced in
+ the control panel or in the rules file, as this can have a performance
+ impact. If you need to access external URLs, enable the "read network"
+ setting.
+
+Note that when Zope is in debug mode, the theme will be re-compiled on
+each request. In non-debug mode, it is compiled once on startup, and then
+only if the control panel values are changed.
+
+Resources in Python packages
+----------------------------
+
+When specifying rules or referenced resources (such as the theme), you can use
+a special ``python://`` URI scheme to specify a path relative to the
+installation of a Python package distribution, as installed using
+Distribute/setuptools (e.g. a standard Plone package installed via buildout).
+
+For example, if your package is called ``my.theme`` and it contains a
+directory ``static``, you could reference the file ``rules.xml`` in that
+file as::
+
+ ``python://my.theme/static/rules.xml``
+
+This will be resolved to an absolute ``file://`` URL by ``plone.app.theming``.
+
+Static files and CSS
+--------------------
+
+Typically, the theme will reference static resources such as images or
+stylesheets. It is usually a good idea to keep all of these in a single,
+top-level directory to minimise the risk of clashes with Plone content paths.
+
+If you are using Zope/Plone standalone, you will need to make your static
+resources available through Zope, or serve them from a separate (sub-)domain.
+Here, you have a few options:
+
+ * Create the static resources as ``File`` content objects through Plone.
+ * Create the resources inside the ``portal_skins/custom`` folder in the ZMI.
+ * Install the resources through a filesystem product.
+
+The latter is most the appropriate option if you are distributing your theme
+as a Python package. In this case, you can register a resource directory in
+ZCML like so::
+
+ <configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser">
+
+ ...
+
+ <browser:resourceDirectory
+ name="my.theme"
+ directory="static"
+ />
+
+ ...
+
+ </configure>
+
+The ``static`` directory should be in the same directory as the
+``configure.zcml`` file. You can now put your theme, rules and static
+resources here.
+
+If you make sure that your theme uses only relative URLs to reference any
+stylesheets, JavaScript files, or images that it needs (including those
+referenced from stylesheets), you should now be able to view your static
+theme by going to a URL like::
+
+ http://localhost:8080/Plone/++resource++my.theme/theme.html
+
+You can now set the "Absolute prefix" configuration option to be
+'/++resource++my.theme'. ``plone.app.theming`` will then turn relative URLs
+into appropriate absolute URLs with this prefix.
+
+If you have put Apache, nginx or IIS in front of Zope, you may want to serve
+the static resources from the web server directly instead.
+
+Using portal_css to manage your CSS
+-----------------------------------
+
+Plone's "resource registries", including the ``portal_css`` tool, can be used
+to manage CSS stylesheets. This offers several advantages over simply linking
+to your stylesheets in the template, such as:
+
+* Detailed control over the ordering of stylesheets
+* Merging of stylesheets to reduce the number of downloads required to render
+ your page
+* On-the-fly stylesheet compression (e.g. whitespace removal)
+* The ability to include or exclude a stylesheet based on an expression
+
+It is usually desirable (and sometimes completely necessary) to leave the
+theme file untouched, but you can still use ``portal_css`` to manage your
+stylesheets. The trick is to drop the theme's styles and then include all
+styles from Plone. For example, you could add the following rules::
+
+ <drop theme="/html/head/link" />
+ <drop theme="/html/head/style" />
+
+ <!-- Pull in Plone CSS -->
+ <append theme="/html/head" content="/html/head/link | /html/head/style" />
+
+The use of an "or" expression for the content in the ``<append />`` rule means
+that the precise ordering is maintained.
+
+For an example of how to register stylesheets upon product installation using
+GenericSetup, see below. In short - use the ``cssregistry.xml`` import step
+in your GenericSetup profile directory.
+
+There is one important caveat, however. Your stylesheet may include relative
+URL references of the following form:
+
+ background-image: url(../images/bg.jpg);
+
+If your stylesheet lives in a resource directory (e.g. it is registered in
+``portal_css`` with the id ``++resource++my.package/css/styles.css``), this
+will work fine so long as the registry (and Zope) is in debug mode. The
+relative URL will be resolved by the browser to
+``++resource++my.package/images/bg.jpg``.
+
+However, you may find that the relative URL breaks when the registry is put
+into production mode. This is because resource merging also changes the URL
+of the stylesheet to be something like::
+
+ /plone-site/portal_css/Suburst+Theme/merged-cachekey-1234.css
+
+To correct for this, you must set the ``applyPrefix`` flag to ``true`` when
+installing your CSS resource using ``cssregistry.xml``. There is a
+corresponding flag in the ``portal_css`` user interface.
+
+Controlling Plone's default CSS
+-------------------------------
+
+It is sometimes useful to show some of Plone's CSS in the styled site. You
+can achieve this by using an Diazo ``<append />`` rule or similar to copy the
+CSS from Plone's generated ``<head />`` into the theme. You can use the
+portal_css tool to turn off the style sheets you do not want.
+
+However, if you also want the site to be usable in non-themed mode (e.g. on a
+separate URL), you may want to have a larger set of styles enabled when Diazo
+is not used. To make this easier, you can use the following expressions as
+conditions in the portal_css tool (and ``portal_javascripts``,
+``portal_kss``), in portal_actions, in page templates, and other places that
+use TAL expression syntax::
+
+ request/HTTP_X_THEME_ENABLED | nothing
+
+This expression will return True if Diazo is currently enabled, in which case
+an HTTP header "X-Theme-Enabled" will be set.
+
+If you later deploy the theme to a fronting web server such as nginx, you can
+set the same request header there to get the same effect, even if
+``plone.app.theming`` is uninstalled.
+
+Use::
+
+ not: request/HTTP_X_THEME_ENABLED | nothing
+
+to 'hide' a style sheet from the themed site.
+
+A worked example
+=================
+
+There are many ways to set up an Diazo theme. For example, you could upload
+the theme and rules as content in Plone use absolute paths to configure them.
+You could also serve them from a separate static web server, or even load
+them from the filesystem.
+
+To create a deployable theme, however, it is often best to create a simple
+Python package. This also provides a natural home for theme-related
+customisations such as template overrides.
+
+Although a detailed tutorial is beyond the scope of this help file, a brief,
+worked example is shown below.
+
+1. Create a package and install it in your buildout::
+
+ $ cd src
+ $ paster create -t plone my.theme
+
+See `the buildout manual`_ for details
+
+If you have a recent ``ZopeSkel`` installed, this should work. Pick ``easy``
+mode. Answer "yes" when asked if you want to register a profile.
+
+Then edit ``buildout.cfg`` to add your new package (``my.theme`` above) to the
+``develop`` and ``eggs`` lists.
+
+2. Edit ``setup.py`` inside the newly created package
+
+The ``install_requires`` list should be::
+
+ install_requires=[
+ 'setuptools',
+ 'plone.app.theming',
+ ],
+
+Re-run buildout::
+
+ $ bin/buildout
+
+3. Edit ``configure.zcml`` inside the newly created package.
+
+Add a resource directory inside the ``<configure />`` tag. Note that you may
+need to add the ``browser`` namespace, as shown.
+
+ <configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ xmlns:i18n="http://namespaces.zope.org/i18n"
+ xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
+ i18n_domain="my.theme">
+
+ <genericsetup:registerProfile
+ name="default"
+ title="My theme"
+ directory="profiles/default"
+ description="Installs the my.theme package"
+ provides="Products.GenericSetup.interfaces.EXTENSION"
+ />
+
+ <browser:resourceDirectory
+ name="my.theme"
+ directory="static"
+ />
+
+ </configure>
+
+Here, we have used the package name, ``my.theme``, for the resource directory
+name. Adjust as appropriate.
+
+4. Add a ``static`` directory next to ``configure.zcml``.
+
+5. Put your theme and rules files into this directory.
+
+For example, you may have a ``theme.html`` that references images in a
+sub-directory ``images/`` and stylesheets in a sub-directory ``css/``. Place
+this file and the two directories inside the newly created ``static``
+directory.
+
+Make sure the theme uses relative URLs (e.g. ``<img src="images/foo.jpg" />``)
+to reference its resources. This means you can open theme up from the
+filesystem and view it in its splendour.
+
+Also place a ``rules.xml`` file there. See the `Diazo`_ documentation for
+details about its syntax. You can start with some very simple rules if
+you just want to test::
+
+ <?xml version="1.0" encoding="UTF-8"?>
+ <rules
+ xmlns="http://namespaces.plone.org/diazo"
+ xmlns:css="http://namespaces.plone.org/diazo+css">
+
+ <!-- The default theme, used for standard Plone web pages -->
+ <theme href="theme.html" css:if-content="#visual-portal-wrapper" />
+
+ <!-- Rules applying to a standard Plone web page -->
+ <rules css:if-content="#visual-portal-wrapper">
+
+ <!-- Add meta tags -->
+ <drop theme="/html/head/meta" />
+ <append content="/html/head/meta" theme="/html/head" />
+
+ <!-- Copy style, script and link tags in the order they appear in the content -->
+ <append
+ content="/html/head/style | /html/head/script | /html/head/link"
+ theme="/html/head"
+ />
+
+ <drop theme="/html/head/style" />
+ <drop theme="/html/head/script" />
+ <drop theme="/html/head/link" />
+
+ <!-- Copy over the id/class attributes on the body tag.
+ This is important for per-section styling -->
+ <prepend content="/html/body/@class" theme="/html/body" />
+ <prepend content="/html/body/@id" theme="/html/body" />
+ <prepend content="/html/body/@dir" theme="/html/body" />
+
+ <!-- Logo (link target) -->
+ <prepend content='//*[@id="portal-logo"]/@href' css:theme="#logo" />
+
+ <!-- Site actions -->
+ <copy css:content="#portal-siteactions li" css:theme="#actions" />
+
+ <!-- Global navigation -->
+ <copy css:content='#portal-globalnav li' css:theme='#global-navigation' />
+
+ <!-- Breadcrumbs -->
+ <copy css:content='#portal-breadcrumbs > *' css:theme='#breadcrumbs' />
+
+ <!-- Document Content -->
+ <copy css:content="#content > *" css:theme="#document-content" />
+ <before css:content="#edit-bar" css:theme="#document-content" />
+ <before css:content=".portalMessage" css:theme="#document-content" />
+
+ <!-- Columns -->
+ <copy css:content="#portal-column-one > *" css:theme="#column-one" />
+ <copy css:content="#portal-column-two > *" css:theme="#column-two" />
+
+ </rules>
+
+ </rules>
+
+In this example, we have referenced the theme HTML file relative to the
+directory where the ``rules.xml`` resides. We make this theme conditional
+on the ``#visual-portal-wrapper`` element being present in the content (i.e.
+the web page generated by Plone). This ensures we do not apply the theme to
+things like pop-ups or special pages.
+
+We apply the same condition to the rules. The first few rules are probably
+useful in most Plone themes. The remainder of the rules are examples that
+may or may not apply, pulling in the logo, breadcrumbs, site actions,
+document content, and left/right hand side columns.
+
+See below for some more useful rules.
+
+6. Create the installation profile
+
+The generated code above for the ``<genericsetup:registerProfile />`` tag
+contains a reference to a directory ``profiles/default``. You may need to
+create this next to ``configure.zcml`` if it doesn't exist already, i.e.
+create a new directory ``profiles`` and inside it another directory
+``default``.
+
+In this directory, add a file called ``metadata.xml`` containing::
+
+ <metadata>
+ <version>1</version>
+ <dependencies>
+ <dependency>profile-plone.app.theming:default</dependency>
+ </dependencies>
+ </metadata>
+
+This will install plone.app.theming into Plone when my.theme is installed via
+the add-on control panel later.
+
+Also create a file called ``registry.xml``, with the following contents::
+
+ <registry>
+
+ <!-- plone.app.theming settings -->
+
+ <record interface="plone.app.theming.interfaces.IThemeSettings" field="rules">
+ <value>python://my.theme/static/rules.xml</value>
+ </record>
+
+ <record interface="plone.app.theming.interfaces.IThemeSettings" field="absolutePrefix">
+ <value>/++resource++my.theme</value>
+ </record>
+
+ </registry>
+
+Replace ``my.theme`` with your own package name, and ``rules.xml`` and
+``theme.html`` as appropriate.
+
+This file configures the settings behind the Diazo control panel.
+
+Hint: If you have played with the control panel and want to export your
+settings, you can create a snapshot in the ``portal_setup`` tool in the ZMI.
+Examine the ``registry.xml`` file this creates, and pick out the records that
+relate to ``plone.app.theming``. You should strip out the ``<field />`` tags
+in the export, so that you are left with ``<record />`` and ``<value />`` tags
+as shown above.
+
+Also, add a ``cssregistry.xml`` in the ``profiles/default`` directory to
+configure the ``portal_css`` tool::
+
+ <?xml version="1.0"?>
+ <object name="portal_css">
+
+ <!-- Set conditions on stylesheets we don't want to pull in -->
+ <stylesheet
+ expression="not:request/HTTP_X_THEME_ENABLED | nothing"
+ id="public.css"
+ />
+
+ <!-- Add new stylesheets -->
+ <!-- Note: applyPrefix is not available in Plone < 4.0b3 -->
+
+ <stylesheet title="" authenticated="False" cacheable="True"
+ compression="safe" conditionalcomment="" cookable="True" enabled="on"
+ expression="request/HTTP_X_THEME_ENABLED | nothing"
+ id="++resource++my.theme/css/styles.css" media="" rel="stylesheet"
+ rendering="link"
+ applyPrefix="True"
+ />
+
+ </object>
+
+This shows how to set a condition on an existing stylesheet, as well as
+registering a brand new one. We've set ``applyPrefix`` to True here, as
+explained above.
+
+7. Test
+
+Start up Zope and go to your Plone site. Your new package should show as
+installable in the add-on product control panel. When installed, it should
+install ``plone.app.theming`` as a dependency and pre-configure it to use your
+theme and rule set. By default, the theme is not enabled, so you will need to
+go to the control panel to switch it on.
+
+You can now compare your untouched theme, the unstyled Plone site, and the
+themed site by using the following URLs:
+
+* ``http://localhost:8080`` (or whatever you have configured as the styled
+ domain) for a styled Plone. If you used the sample rule above, this will
+ look almost exactly like your theme, but with the ``<title />`` tag
+ (normally shown in the title bar of your web browser) taken from Plone.
+* ``http://127.0.0.1:8080`` (presuming this is the port where Plone is
+ running) for an unstyled Plone.
+* ``http://localhost:8080/++resource++my.theme/theme.html`` for the pristine
+ theme. This is served as a static resource, almost as if it is being
+ opened on the filesystem.
+
+Common rules
+============
+
+To copy the page title::
+
+ <!-- Head: title -->
+ <replace theme="/html/head/title" content="/html/head/title" />
+
+To copy the ``<base />`` tag (necessary for Plone's links to work)::
+
+ <!-- Base tag -->
+ <replace theme="/html/head/base" content="/html/head/base" />
+
+To drop all styles and JavaScript resources from the theme and copy them
+from Plone's ``portal_css`` tool instead::
+
+ <!-- Drop styles in the head - these are added back by including them from Plone -->
+ <drop theme="/html/head/link" />
+ <drop theme="/html/head/style" />
+
+ <!-- Pull in Plone CSS -->
+ <append theme="/html/head" content="/html/head/link | /html/head/style" />
+
+To copy Plone's JavaScript resources::
+
+ <!-- Pull in Plone CSS -->
+ <append theme="/html/head" content="/html/head/script" />
+
+To copy the class of the ``<body />`` tag (necessary for certain Plone
+JavaScript functions and styles to work properly)::
+
+ <!-- Body -->
+ <prepend theme="/html/body" content="/html/body/attribute::class" />
+
+Other tips
+==========
+
+* Firebug is an excellent tool for inspecting the theme and content when
+ building rules. It even has an XPath extractor.
+* Read up on XPath. It's not as complex as it looks and very powerful.
+* Run Zope in debug mode whilst developing so that you don't need to restart
+ to see changes to theme, rules or, resources.
+
+.. _Diazo: http://diazo.org
+.. _Plone: http://plone.org
+.. _plone.transformchain: http://pypi.python.org/pypi/plone.transformchain
+.. _repoze.zope2: http://pypi.python.org/pypi/repoze.zope2
+.. _plone.transformchain: http://pypi.python.org/pypi/plone.transformchain
+.. _plone.registry: http://pypi.python.org/pypi/plone.registry
+.. _plone.app.registry: http://pypi.python.org/pypi/plone.app.registry
+.. _plone.autoform: http://pypi.python.org/pypi/plone.autoform
+.. _plone.z3cform: http://pypi.python.org/pypi/plone.z3cform
+.. _plone.app.z3cform: http://pypi.python.org/pypi/plone.app.z3cform
+.. _lxml: http://pypi.python.org/pypi/lxml
+.. _five.globalrequest: http://pypi.python.org/pypi/five.globalrequest
+.. _zope.globalrequest: http://pypi.python.org/pypi/zope.globalrequest
+.. _the buildout manual: http://plone.org/documentation/manual/developer-manual/managing-projects-with-buildout
127 bootstrap.py
@@ -0,0 +1,127 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id$
+"""
+
+import os, shutil, sys, tempfile, urllib2
+from optparse import OptionParser
+
+tmpeggs = tempfile.mkdtemp()
+
+is_jython = sys.platform.startswith('java')
+
+# parsing arguments
+parser = OptionParser(
+ 'This is a custom version of the zc.buildout %prog script. It is '
+ 'intended to meet a temporary need if you encounter problems with '
+ 'the zc.buildout 1.5 release.')
+parser.add_option("-v", "--version", dest="version", default='1.4.4',
+ help='Use a specific zc.buildout version. *This '
+ 'bootstrap script defaults to '
+ '1.4.4, unlike usual buildpout bootstrap scripts.*')
+parser.add_option("-d", "--distribute",
+ action="store_true", dest="distribute", default=False,
+ help="Use Disribute rather than Setuptools.")
+
+parser.add_option("-c", None, action="store", dest="config_file",
+ help=("Specify the path to the buildout configuration "
+ "file to be used."))
+
+options, args = parser.parse_args()
+
+# if -c was provided, we push it back into args for buildout' main function
+if options.config_file is not None:
+ args += ['-c', options.config_file]
+
+if options.version is not None:
+ VERSION = '==%s' % options.version
+else:
+ VERSION = ''
+
+USE_DISTRIBUTE = options.distribute
+args = args + ['bootstrap']
+
+to_reload = False
+try:
+ import pkg_resources
+ if not hasattr(pkg_resources, '_distribute'):
+ to_reload = True
+ raise ImportError
+except ImportError:
+ ez = {}
+ if USE_DISTRIBUTE:
+ exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py'
+ ).read() in ez
+ ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True)
+ else:
+ exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+ ).read() in ez
+ ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+ if to_reload:
+ reload(pkg_resources)
+ else:
+ import pkg_resources
+
+if sys.platform == 'win32':
+ def quote(c):
+ if ' ' in c:
+ return '"%s"' % c # work around spawn lamosity on windows
+ else:
+ return c
+else:
+ def quote (c):
+ return c
+
+ws = pkg_resources.working_set
+
+if USE_DISTRIBUTE:
+ requirement = 'distribute'
+else:
+ requirement = 'setuptools'
+
+env = dict(os.environ,
+ PYTHONPATH=
+ ws.find(pkg_resources.Requirement.parse(requirement)).location
+ )
+
+cmd = [quote(sys.executable),
+ '-c',
+ quote('from setuptools.command.easy_install import main; main()'),
+ '-mqNxd',
+ quote(tmpeggs)]
+
+if 'bootstrap-testing-find-links' in os.environ:
+ cmd.extend(['-f', os.environ['bootstrap-testing-find-links']])
+
+cmd.append('zc.buildout' + VERSION)
+
+if is_jython:
+ import subprocess
+ exitcode = subprocess.Popen(cmd, env=env).wait()
+else: # Windows prefers this, apparently; otherwise we would prefer subprocess
+ exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
+assert exitcode == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout' + VERSION)
+import zc.buildout.buildout
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)
33 buildout.cfg
@@ -0,0 +1,33 @@
+[buildout]
+extends =
+ http://dist.plone.org/release/4.0.1/versions.cfg
+ http://good-py.appspot.com/release/diazo/1.0b1
+parts = lxml test instance
+develop = .
+
+extensions = mr.developer
+sources = sources
+auto-checkout = diazo
+
+[lxml]
+recipe = z3c.recipe.staticlxml
+egg = lxml
+
+[sources]
+diazo = svn https://svn.plone.org/svn/plone/diazo/trunk
+
+[instance]
+recipe = plone.recipe.zope2instance
+eggs = plone.app.theming
+
+[test]
+recipe = zc.recipe.testrunner
+eggs =
+ diazo
+ plone.app.theming [test]
+defaults = ['--auto-color', '--auto-progress']
+
+[coverage-report]
+recipe = zc.recipe.egg
+eggs = z3c.coverage
+arguments = ('coverage', 'report')
28 docs/HISTORY.txt
@@ -0,0 +1,28 @@
+Changelog
+=========
+
+1.0b1 - Unreleased
+-------------------
+
+* Removed 'theme' and alternative themes support: Themes should be referenced
+ using the ``<theme />`` directive in the Diazo rules file.
+ [optilude]
+
+* Removed 'domains' support: This can be handled with the rules file syntax
+ by using the ``host`` parameter.
+ [optilude]
+
+* Removed 'notheme' support: This can be handled within the rules file syntax
+ by using the ``path`` parameter.
+ [optilude]
+
+* Added ``path`` and ``host`` as parameters to the Diazo rules file. These
+ can now be used as conditional expressions.
+ [optilude]
+
+* Removed dependency on XDV in favour of dependency on Diazo (which is the
+ new name for XDV).
+ [optilude]
+
+* Forked from collective.xdv 1.0rc11.
+ [optilude]
280 docs/LICENSE.GPL
@@ -0,0 +1,280 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
15 docs/LICENSE.txt
@@ -0,0 +1,15 @@
+plone.app.testing
+Copyright (C) 2010 Plone Foundation
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
45 setup.py
@@ -0,0 +1,45 @@
+from setuptools import setup, find_packages
+import os
+
+version = '1.0b1'
+
+setup(name='plone.app.theming',
+ version=version,
+ description="Integrates the Diazo theming engine with Plone",
+ long_description=open("README.txt").read() + "\n\n" +
+ open(os.path.join("docs", "HISTORY.txt")).read(),
+ # Get more strings from http://www.python.org/pypi?%3Aaction=list_classifiers
+ classifiers=[
+ "Framework :: Plone",
+ "Programming Language :: Python",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ],
+ keywords='plone diazo xdv deliverance theme transform xslt',
+ author='Martin Aspeli and Laurence Rowe',
+ author_email='optilude@gmail.com',
+ url='http://pypi.python.org/pypi/plone.app.theming',
+ license='GPL',
+ packages=find_packages('src'),
+ package_dir={'': 'src'},
+ namespace_packages=['plone', 'plone.app'],
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=[
+ 'setuptools',
+ 'diazo',
+ 'lxml>=2.2.4',
+ 'plone.app.registry>=1.0a2',
+ 'plone.subrequest',
+ 'plone.transformchain',
+ 'repoze.xmliter',
+ 'five.globalrequest',
+ 'Plone',
+ ],
+ extras_require={
+ 'test': ['plone.app.testing'],
+ },
+ entry_points="""
+ [z3c.autoinclude.plugin]
+ target = plone
+ """,
+ )
6 src/plone/__init__.py
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+ __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+ from pkgutil import extend_path
+ __path__ = extend_path(__path__, __name__)
6 src/plone/app/__init__.py
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+ __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+ from pkgutil import extend_path
+ __path__ = extend_path(__path__, __name__)
0 src/plone/app/theming/__init__.py
No changes.
17 src/plone/app/theming/browser.py
@@ -0,0 +1,17 @@
+from plone.app.registry.browser import controlpanel
+from plone.app.theming.interfaces import IThemeSettings, _
+
+class ThemeSettingsEditForm(controlpanel.RegistryEditForm):
+
+ schema = IThemeSettings
+ label = _('theme_settings', u"Theme settings")
+ description = _('use_settings_below',
+ u"Use the settings below to configure a theme for this site")
+
+ def updateWidgets(self):
+ super(ThemeSettingsEditForm, self).updateWidgets()
+ self.widgets['rules'].size = 60
+ self.widgets['absolutePrefix'].size = 60
+
+class ThemeSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
+ form = ThemeSettingsEditForm
55 src/plone/app/theming/configure.zcml
@@ -0,0 +1,55 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:gs="http://namespaces.zope.org/genericsetup"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ xmlns:zcml="http://namespaces.zope.org/zcml"
+ xmlns:i18n="http://namespaces.zope.org/i18n"
+ i18n_domain="plone.app.theming">
+
+ <i18n:registerTranslations directory="locales"/>
+
+ <include package="plone.app.registry" />
+ <include package="plone.transformchain" />
+ <include package="five.globalrequest" />
+
+ <gs:registerProfile
+ name="default"
+ title="Diazo theme support"
+ description="Installs a control panel to allow on-the-fly theming with Diazo"
+ directory="profiles/default"
+ for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+ provides="Products.GenericSetup.interfaces.EXTENSION"
+ />
+
+ <!-- Transform order 8850 - apply theme transform -->
+ <adapter
+ name="plone.app.theming.transform"
+ factory=".transform.ThemeTransform"
+ />
+
+ <subscriber
+ for=".interfaces.IThemeSettings
+ plone.registry.interfaces.IRecordModifiedEvent"
+ handler=".transform.invalidateCache"
+ />
+
+ <browser:page
+ name="theming-controlpanel"
+ for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+ class=".browser.ThemeSettingsControlPanel"
+ permission="cmf.ManagePortal"
+ />
+
+ <!-- Set X-Theme-Enabled header in the request if theming is enabled -->
+ <subscriber
+ for="Products.CMFCore.interfaces.ISiteRoot
+ zope.app.publication.interfaces.IBeforeTraverseEvent"
+ handler=".header.setHeader"
+ />
+
+ <browser:resource
+ name="plone.app.theming.gif"
+ image="icon.gif"
+ />
+
+</configure>
28 src/plone/app/theming/header.py
@@ -0,0 +1,28 @@
+from zope.component import queryUtility
+from plone.registry.interfaces import IRegistry
+
+from plone.app.theming.interfaces import IThemeSettings
+
+def setHeader(object, event):
+ """Set a header X-Theme-Enabled in the request if theming is enabled.
+
+ This is useful for checking in things like the portal_css/portal_javascripts
+ registries.
+ """
+
+ request = event.request
+
+ registry = queryUtility(IRegistry)
+ if registry is None:
+ return
+
+ settings = None
+ try:
+ settings = registry.forInterface(IThemeSettings)
+ except KeyError:
+ return
+
+ if not settings.enabled:
+ return
+
+ request.environ['HTTP_X_THEME_ENABLED'] = True
BIN src/plone/app/theming/icon.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 src/plone/app/theming/interfaces.py
@@ -0,0 +1,48 @@
+from zope.interface import Interface
+from zope import schema
+from zope.i18nmessageid import MessageFactory
+
+_ = MessageFactory(u"plone")
+
+class IThemeSettings(Interface):
+ """Transformation settings
+ """
+
+ enabled = schema.Bool(
+ title=_('enabled', u"Enabled"),
+ description=_('enable_theme_globally',
+ u"Use this option to enable or disable the theme "
+ u"globally. Note that the options will also affect "
+ u"whether the theme is used when this option is "
+ u'enabled.'),
+ required=True,
+ default=False,
+ )
+
+ rules = schema.TextLine(
+ title=_('rules_file', u"Rules file"),
+ description=_('rules_file_path',
+ u"File path to the rules file"),
+ required=False,
+ )
+
+ absolutePrefix = schema.TextLine(
+ title=_('absolute_url_prefix', u"Absolute URL prefix"),
+ description=_('convert_relative_url',
+ u"Convert relative URLs in the theme file to absolute paths "
+ u"using this prefix."),
+ required=False,
+ )
+
+ readNetwork = schema.Bool(
+ title=_('readNetwork', u"Read network"),
+ description=_('network_urls_allowed',
+ u"If enabled, network (http, https) urls are "
+ u"allowed in the rules file and this config."),
+ required=True,
+ default=False,
+ )
+
+class IThemingLayer(Interface):
+ """Browser layer used to indicate that plone.app.theming is installed
+ """
6 src/plone/app/theming/profiles/default/browserlayer.xml
@@ -0,0 +1,6 @@
+<layers>
+ <layer
+ name="plone.app.theming"
+ interface="plone.app.theming.interfaces.IThemingLayer"
+ />
+</layers>
21 src/plone/app/theming/profiles/default/controlpanel.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<object
+ name="portal_controlpanel"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ i18n:domain="plone.app.theming"
+ purge="False">
+
+ <configlet
+ title="Diazo theme"
+ action_id="plone.app.theming"
+ appId="plone.app.theming"
+ category="Plone"
+ condition_expr=""
+ url_expr="string:${portal_url}/@@theming-controlpanel"
+ icon_expr="string:${portal_url}/++resource++plone.app.theming.gif"
+ visible="True"
+ i18n:attributes="title">
+ <permission>Manage portal</permission>
+ </configlet>
+
+</object>
6 src/plone/app/theming/profiles/default/metadata.xml
@@ -0,0 +1,6 @@
+<metadata>
+ <version>1.0a1</version>
+ <dependencies>
+ <dependency>profile-plone.app.registry:default</dependency>
+ </dependencies>
+</metadata>
4 src/plone/app/theming/profiles/default/registry.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<registry>
+ <records interface="plone.app.theming.interfaces.IThemeSettings" />
+</registry>
23 src/plone/app/theming/testing.py
@@ -0,0 +1,23 @@
+from plone.app.testing import PloneSandboxLayer
+from plone.app.testing import PLONE_FIXTURE
+from plone.app.testing import applyProfile
+
+from zope.configuration import xmlconfig
+from plone.app.testing.layers import IntegrationTesting
+from plone.app.testing.layers import FunctionalTesting
+
+class Theming(PloneSandboxLayer):
+ defaultBases = (PLONE_FIXTURE,)
+
+ def setUpZope(self, app, configurationContext):
+ # load ZCML
+ import plone.app.theming
+ xmlconfig.file('configure.zcml', plone.app.theming, context=configurationContext)
+
+ def setUpPloneSite(self, portal):
+ # install into the Plone site
+ applyProfile(portal, 'plone.app.theming:default')
+
+THEMING_FIXTURE = Theming()
+THEMING_INTEGRATION_TESTING = IntegrationTesting(bases=(THEMING_FIXTURE,), name="Theming:Integration")
+THEMING_FUNCTIONAL_TESTING = FunctionalTesting(bases=(THEMING_FIXTURE,), name="Theming:Functional")
0 src/plone/app/theming/tests/__init__.py
No changes.
8 src/plone/app/theming/tests/includes.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>Title</title>
+</head>
+<body>
+ <div id="alpha">(placeholder)</div>
+ <div id="beta">(placeholder)</div>
+</body>
9 src/plone/app/theming/tests/includes.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rules xmlns="http://namespaces.plone.org/diazo" xmlns:css="http://namespaces.plone.org/diazo+css">
+
+ <theme href="python://plone.app.theming.tests/includes.html" />
+
+ <copy content='//*[@id="content"]/text()' css:theme='#alpha' href="/alpha" />
+ <copy content='//*[@id="content"]/text()' css:theme='#beta' href="./beta" />
+
+</rules>
10 src/plone/app/theming/tests/localrules.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rules xmlns="http://namespaces.plone.org/diazo" xmlns:css="http://namespaces.plone.org/diazo+css">
+
+ <theme href="/theme.html" />
+
+ <replace content='/html/head/title' theme='/html/head/title' />
+ <replace content='//h1[class=documentFirstHeading]' theme='/html/body/h1' />
+ <append content='/html/head/link' theme='/html/head' />
+
+</rules>
7 src/plone/app/theming/tests/one.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+ <title>Title</title>
+</head>
+<body>
+ <div id="content">Number one</div>
+</body>
10 src/plone/app/theming/tests/otherrules.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rules xmlns="http://namespaces.plone.org/diazo" xmlns:css="http://namespaces.plone.org/diazo+css">
+
+ <theme href="othertheme.html" />
+
+ <replace content='/html/head/title' theme='/html/head/title' />
+ <replace content='//h1[class=documentFirstHeading]' theme='/html/body/h1' />
+ <append content='/html/head/link' theme='/html/head' />
+
+</rules>
9 src/plone/app/theming/tests/othertheme.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>Title</title>
+</head>
+<body>
+ <h1 id="pageTitle">Page title</h1>
+ <p>This is the other theme.</p>
+</body>
+</html>
11 src/plone/app/theming/tests/rules.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rules xmlns="http://namespaces.plone.org/diazo" xmlns:css="http://namespaces.plone.org/diazo+css">
+
+ <theme href="theme.html" />
+ <theme href="othertheme.html" if-path="news"/>
+
+ <replace content='/html/head/title' theme='/html/head/title' />
+ <replace content='//h1[class=documentFirstHeading]' theme='/html/body/h1' />
+ <append content='/html/head/link' theme='/html/head' />
+
+</rules>
521 src/plone/app/theming/tests/test_transform.py
@@ -0,0 +1,521 @@
+import unittest2 as unittest
+
+from plone.app.theming.testing import THEMING_FUNCTIONAL_TESTING
+from plone.testing.z2 import Browser
+
+from plone.app.testing import setRoles, TEST_USER_NAME
+
+import Globals
+import os.path
+
+from lxml import etree
+
+from urllib2 import HTTPError
+
+from Products.CMFCore.Expression import Expression, getExprContext
+
+from plone.registry.interfaces import IRegistry
+from zope.component import getUtility
+
+from plone.app.theming.interfaces import IThemeSettings
+from plone.app.theming.utils import InternalResolver, PythonResolver, resolvePythonURL
+
+from diazo.compiler import compile_theme
+
+from Products.CMFCore.utils import getToolByName
+
+class TestCase(unittest.TestCase):
+
+ layer = THEMING_FUNCTIONAL_TESTING
+
+ def setUp(self):
+ # Enable debug mode always to ensure cache is disabled by default
+ Globals.DevelopmentMode = True
+
+ self.settings = getUtility(IRegistry).forInterface(IThemeSettings)
+
+ self.settings.enabled = False
+ self.settings.rules = u'python://plone.app.theming/tests/rules.xml'
+
+ import transaction; transaction.commit()
+
+ def tearDown(self):
+ Globals.DevelopmentMode = False
+
+ def evaluate(self, context, expression):
+ ec = getExprContext(context, context)
+ expr = Expression(expression)
+ return expr(ec)
+
+ def test_no_effect_if_not_enabled(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ browser = Browser(app)
+ browser.open(portal.absolute_url())
+
+ # Title - pulled in with rules.xml
+ self.failUnless(portal.title in browser.contents)
+
+ # Elsewhere - not pulled in
+ self.failUnless("Accessibility" in browser.contents)
+
+ # The theme
+ self.failIf("This is the theme" in browser.contents)
+
+ def test_theme_enabled(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ self.settings.enabled = True
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ browser.open(portal.absolute_url())
+
+ # Title - pulled in with rules.xml
+ self.failUnless(portal.title in browser.contents)
+
+ # Elsewhere - not pulled in
+ self.failIf("Accessibility" in browser.contents)
+
+ # The theme
+ self.failUnless("This is the theme" in browser.contents)
+
+ def test_internal_resolver(self):
+ compiler_parser = etree.XMLParser()
+ compiler_parser.resolvers.add(InternalResolver())
+ # We can use a sub-package or a directory since tests is a python package
+ theme = resolvePythonURL(u'python://plone.app.theming.tests/theme.html')
+ rules = resolvePythonURL(u'python://plone.app.theming/tests/rules.xml')
+ compile_theme(rules, theme, compiler_parser=compiler_parser)
+
+ def test_python_resolver(self):
+ compiler_parser = etree.XMLParser()
+ compiler_parser.resolvers.add(PythonResolver())
+ theme = resolvePythonURL(u'python://plone.app.theming.tests/theme.html')
+ rules = resolvePythonURL(u'python://plone.app.theming/tests/rules.xml')
+ compile_theme(rules, theme, compiler_parser=compiler_parser)
+
+ def test_theme_stored_in_plone_site(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ # We'll upload the theme files to the Plone site root
+ rules_contents = open(os.path.join(os.path.split(__file__)[0], 'localrules.xml'))
+ theme_contents = open(os.path.join(os.path.split(__file__)[0], 'theme.html'))
+ portal.manage_addDTMLMethod('theme.html', file=theme_contents)
+ portal.manage_addDTMLMethod('rules.xml', file=rules_contents)
+
+ # These paths should be relative to the Plone site root
+ self.settings.rules = u'/rules.xml'
+ self.settings.enabled = True
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ browser.open(portal.absolute_url())
+
+ # Title - pulled in with rules.xml
+ self.failUnless(portal.title in browser.contents)
+
+ # Elsewhere - not pulled in
+ self.failIf("Accessibility" in browser.contents)
+
+ # The theme
+ self.failUnless("This is the theme" in browser.contents)
+
+ def test_theme_stored_in_plone_site_works_with_virtual_host(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ # We'll upload the theme files to the Plone site root
+ rules_contents = open(os.path.join(os.path.dirname(__file__), 'localrules.xml'))
+ theme_contents = open(os.path.join(os.path.dirname(__file__), 'theme.html'))
+ portal.manage_addDTMLMethod('theme.html', file=theme_contents)
+ portal.manage_addDTMLMethod('rules.xml', file=rules_contents)
+
+ # These paths should be relative to the Plone site root
+ self.settings.rules = u'/rules.xml'
+ self.settings.enabled = True
+
+ from Products.SiteAccess import VirtualHostMonster
+ VirtualHostMonster.manage_addVirtualHostMonster(app, 'virtual_hosting')
+
+ import transaction
+ transaction.commit()
+
+ portalURL = portal.absolute_url()
+ prefix = '/'.join(portalURL.split('/')[:-1])
+ suffix = portalURL.split('/')[-1]
+
+ vhostURL = "%s/VirtualHostBase/http/example.org:80/%s/VirtualHostRoot/_vh_fizz/_vh_buzz/_vh_fizzbuzz/" % (prefix,suffix)
+
+ browser = Browser(app)
+ browser.open(vhostURL)
+
+ # Title - pulled in with rules.xml
+ self.failUnless(portal.title in browser.contents)
+
+ # Elsewhere - not pulled in
+ self.failIf("Accessibility" in browser.contents)
+
+ # The theme
+ self.failUnless("This is the theme" in browser.contents)
+
+ def test_absolutePrefix_disabled(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ self.settings.enabled = True
+ self.settings.absolutePrefix = None
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ browser.open(portal.absolute_url())
+
+ self.failUnless('<img src="relative.jpg" />' in browser.contents)
+
+ def test_absolutePrefix_enabled_uri(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ self.settings.enabled = True
+ self.settings.absolutePrefix = u'http://example.com'
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ browser.open(portal.absolute_url())
+
+ self.failIf('<img src="relative.jpg" />' in browser.contents)
+ self.failUnless('<img src="http://example.com/relative.jpg" />' in browser.contents)
+
+ def test_absolutePrefix_enabled_path(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ self.settings.enabled = True
+ self.settings.absolutePrefix = u'/foo'
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ browser.open(portal.absolute_url())
+
+ self.failIf('<img src="relative.jpg" />' in browser.contents)
+ self.failUnless('<img src="/plone/foo/relative.jpg" />' in browser.contents)
+
+ def test_absolutePrefix_enabled_path_vhosting(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ from Products.SiteAccess import VirtualHostMonster
+ VirtualHostMonster.manage_addVirtualHostMonster(app, 'virtual_hosting')
+
+ import transaction; transaction.commit()
+
+ self.settings.enabled = True
+ self.settings.absolutePrefix = u'/foo'
+
+ portalURL = portal.absolute_url()
+ prefix = '/'.join(portalURL.split('/')[:-1])
+ suffix = portalURL.split('/')[-1]
+
+ vhostURL = "%s/VirtualHostBase/http/example.org:80/%s/VirtualHostRoot/_vh_fizz/_vh_buzz/_vh_fizzbuzz/" % (prefix,suffix)
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ browser.open(vhostURL)
+
+ self.failIf('<img src="relative.jpg" />' in browser.contents)
+ self.failUnless('<img src="/fizz/buzz/fizzbuzz/foo/relative.jpg" />' in browser.contents)
+
+ def test_theme_installed_invalid_config(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ self.settings.enabled = True
+ self.settings.rules = u"invalid"
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ browser.open(portal.absolute_url())
+
+ # Title - pulled in with rules.xml
+ self.failUnless(portal.title in browser.contents)
+
+ # Elsewhere - not pulled in
+ self.failUnless("Accessibility" in browser.contents)
+
+ # The theme
+ self.failIf("This is the theme" in browser.contents)
+
+ def test_non_html_content(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ self.settings.enabled = True
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ browser.open(portal.absolute_url() + '/document_icon.gif')
+ # The theme
+ self.failIf("This is the theme" in browser.contents)
+
+ # XXX: This relies on a _v_ attribute; the test is too brittle
+ #
+ # def test_non_debug_mode_cache(self):
+ # app = self.layer['app']
+ # portal = self.layer['portal']
+ #
+ # Globals.DevelopmentMode = False
+ # self.settings.enabled = True
+ #
+ # # Sneakily seed the cache with dodgy data
+ #
+ # otherrules = unicode(os.path.join(os.path.split(__file__)[0], 'otherrules.xml'))
+ #
+ # compiled_theme = compile_theme(otherrules)
+ # transform = etree.XSLT(compiled_theme)
+ #
+ # getCache(self.settings, portal.absolute_url()).updateTransform(transform)
+ #
+ # import transaction; transaction.commit()
+ #
+ # browser = Browser(app)
+ # browser.open(portal.absolute_url())
+ #
+ # # Title - pulled in with rules.xml
+ # self.failUnless(portal.title in browser.contents)
+ #
+ # # Elsewhere - not pulled in
+ # self.failIf("Accessibility" in browser.contents)
+ #
+ # # The theme
+ # self.failUnless("This is the other theme" in browser.contents)
+ #
+ # # Now invalide the cache by touching the settings utility
+ #
+ # self.settings.enabled = False
+ # self.settings.enabled = True
+ #
+ # import transaction; transaction.commit()
+ #
+ # browser.open(portal.absolute_url())
+ #
+ # # Title - pulled in with rules.xml
+ # self.failUnless(portal.title in browser.contents)
+ #
+ # # Elsewhere - not pulled in
+ # self.failIf("Accessibility" in browser.contents)
+ #
+ # # The theme
+ # self.failUnless("This is the theme" in browser.contents)
+
+ def test_resource_condition(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ portal_css = getToolByName(portal, 'portal_css')
+ portal_css.setDebugMode(True)
+
+ # shown in both
+ thirdLastResource = portal_css.resources[-3]
+ thirdLastResource.setExpression('')
+ thirdLastResource.setRendering('link')
+ thirdLastResource.setEnabled(True)
+
+ # only show in theme
+ secondToLastResource = portal_css.resources[-2]
+ secondToLastResource.setExpression('request/HTTP_X_THEME_ENABLED | nothing')
+ secondToLastResource.setRendering('link')
+ secondToLastResource.setEnabled(True)
+
+ # only show when theme is disabled
+ lastResource = portal_css.resources[-1]
+ lastResource.setExpression('not:request/HTTP_X_THEME_ENABLED | nothing')
+ lastResource.setRendering('link')
+ lastResource.setEnabled(True)
+
+ portal_css.cookResources()
+
+ # First try without the theme
+ self.settings.enabled = False
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ browser.open(portal.absolute_url())
+
+ self.failUnless(thirdLastResource.getId() in browser.contents)
+ self.failIf(secondToLastResource.getId() in browser.contents)
+ self.failUnless(lastResource.getId() in browser.contents)
+
+ self.failUnless(portal.title in browser.contents)
+ self.failUnless("Accessibility" in browser.contents)
+ self.failIf("This is the theme" in browser.contents)
+
+ # Now enable the theme and try again
+ self.settings.enabled = True
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ browser.open(portal.absolute_url())
+
+ self.failUnless(thirdLastResource.getId() in browser.contents)
+ self.failUnless(secondToLastResource.getId() in browser.contents)
+ self.failIf(lastResource.getId() in browser.contents)
+
+ self.failUnless(portal.title in browser.contents)
+ self.failIf("Accessibility" in browser.contents)
+ self.failUnless("This is the theme" in browser.contents)
+
+ def test_theme_different_path(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ setRoles(portal, TEST_USER_NAME, ('Manager',))
+ portal.invokeFactory('Folder', 'news', title=u"News")
+ setRoles(portal, TEST_USER_NAME, ('Member',))
+
+ self.settings.enabled = True
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ browser.open(portal.absolute_url())
+
+ # Title - pulled in with rules.xml
+ self.failUnless(portal.title in browser.contents)
+
+ # Elsewhere - not pulled in
+ self.failIf("Accessibility" in browser.contents)
+
+ # The theme
+ self.failUnless("This is the theme" in browser.contents)
+
+ browser.open(portal['news'].absolute_url())
+
+ # Title - pulled in with rules.xml
+ self.failUnless("News" in browser.contents)
+
+ # Elsewhere - not pulled in
+ self.failIf("Accessibility" in browser.contents)
+
+ # The theme
+ self.failUnless("This is the other theme" in browser.contents)
+
+ def test_theme_for_404(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ self.settings.enabled = True
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+ error = None
+ try:
+ browser.open('%s/404_page' % portal.absolute_url())
+ except HTTPError, e:
+ error = e
+ self.assertEquals(error.code, 404)
+
+ # The theme
+ self.failUnless("This is the theme" in browser.contents)
+
+ def test_resource_condition_404(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ portal_css = getToolByName(portal, 'portal_css')
+ portal_css.setDebugMode(True)
+
+ # shown in both
+ thirdLastResource = portal_css.resources[-3]
+ thirdLastResource.setExpression('')
+ thirdLastResource.setRendering('link')
+ thirdLastResource.setEnabled(True)
+
+ # only show in theme
+ secondToLastResource = portal_css.resources[-2]
+ secondToLastResource.setExpression('request/HTTP_X_THEME_ENABLED | nothing')
+ secondToLastResource.setRendering('link')
+ secondToLastResource.setEnabled(True)
+
+ # only show when theme is disabled
+ lastResource = portal_css.resources[-1]
+ lastResource.setExpression('not:request/HTTP_X_THEME_ENABLED | nothing')
+ lastResource.setRendering('link')
+ lastResource.setEnabled(True)
+
+ portal_css.cookResources()
+
+ self.settings.enabled = True
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+
+ try:
+ browser.open('%s/404_page' % portal.absolute_url())
+ except HTTPError, e:
+ error = e
+ self.assertEquals(error.code, 404)
+
+ self.failUnless(thirdLastResource.getId() in browser.contents)
+ self.failUnless(secondToLastResource.getId() in browser.contents)
+ self.failIf(lastResource.getId() in browser.contents)
+
+ self.failUnless("This is the theme" in browser.contents)
+
+ def test_includes(self):
+ app = self.layer['app']
+ portal = self.layer['portal']
+
+ setRoles(portal, TEST_USER_NAME, ('Manager',))
+
+ one = open(os.path.join(os.path.split(__file__)[0], 'one.html'))
+ two = open(os.path.join(os.path.split(__file__)[0], 'two.html'))
+
+ # Create some test content in the portal root
+ portal.manage_addDTMLMethod('alpha', file=one)
+ portal.manage_addDTMLMethod('beta', file=two)
+
+ one.seek(0)
+ two.seek(0)
+
+ # Create some different content in a subfolder
+ portal.invokeFactory('Folder', 'subfolder')
+
+ portal['subfolder'].manage_addDTMLMethod('alpha', file=two)
+ portal['subfolder'].manage_addDTMLMethod('beta', file=one)
+
+ # Set up transformation
+ self.settings.rules = u'python://plone.app.theming/tests/includes.xml'
+ self.settings.enabled = True
+
+ import transaction; transaction.commit()
+
+ browser = Browser(app)
+
+ # At the root if the site, we should pick up 'one' for alpha (absolute
+ # path, relative to site root) and 'two' for beta (relative path,
+ # relative to current directory)
+
+ browser.open(portal.absolute_url())
+ self.failUnless('<div id="alpha">Number one</div>' in browser.contents)
+ self.failUnless('<div id="beta">Number two</div>' in browser.contents)
+
+ # In the subfolder, we've reversed alpha and beta. We should now get
+ # 'one' twice, since we still get alpha from the site root.
+
+ browser.open(portal['subfolder'].absolute_url())
+ self.failUnless('<div id="alpha">Number one</div>' in browser.contents)
+ self.failUnless('<div id="beta">Number one</div>' in browser.contents)
9 src/plone/app/theming/tests/theme.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>Title</title>
+</head>
+<body>
+ <h1 id="pageTitle">Page title</h1>
+ <p>This is the theme.</p>
+ <img src="relative.jpg" />
+</body>
7 src/plone/app/theming/tests/two.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+ <title>Title</title>
+</head>
+<body>
+ <div id="content">Number two</div>
+</body>
215 src/plone/app/theming/transform.py
@@ -0,0 +1,215 @@
+import logging
+import Globals
+
+from lxml import etree
+from diazo.compiler import compile_theme
+
+from repoze.xmliter.utils import getHTMLSerializer
+
+from zope.interface import implements, Interface
+from zope.component import adapts
+from zope.component import queryUtility
+from zope.site.hooks import getSite
+
+from plone.registry.interfaces import IRegistry
+from plone.transformchain.interfaces import ITransform
+
+from plone.app.theming.interfaces import IThemeSettings, IThemingLayer
+from plone.app.theming.utils import expandAbsolutePrefix, PythonResolver, InternalResolver, NetworkResolver
+
+LOGGER = logging.getLogger('plone.app.theming')
+
+class _Cache(object):
+ """Simple cache for the transform
+ """
+
+ def __init__(self):
+ self.transform = None
+
+ def updateTransform(self, transform):
+ self.transform = transform
+
+def getCache(settings, key):
+ # We need a persistent object to hang a _v_ attribute off for caching.
+
+ registry = settings.__registry__
+ caches = getattr(registry, '_v_plone_app_theming_caches', None)
+ if caches is None:
+ caches = registry._v_plone_app_theming_caches = {}
+ cache = caches.get(key)
+ if cache is None:
+ cache = caches[key] = _Cache()
+ return cache
+
+def invalidateCache(settings, event):
+ """When our settings are changed, invalidate the cache on all zeo clients
+ """
+ registry = settings.__registry__
+ registry._p_changed = True
+ if hasattr(registry, '_v_plone_app_theming_caches'):
+ del registry._v_plone_app_theming_caches
+
+class ThemeTransform(object):
+ """Late stage in the 8000's transform chain. When plone.app.blocks is
+ used, we can benefit from lxml parsing having taken place already.
+ """
+
+ implements(ITransform)
+ adapts(Interface, IThemingLayer)
+
+ order = 8850
+
+ def __init__(self, published, request):
+ self.published = published
+ self.request = request
+
+ def setupTransform(self):
+ request = self.request
+ DevelopmentMode = Globals.DevelopmentMode
+
+ # Find the host name
+ base1 = request.get('BASE1')
+ _, base1 = base1.split('://', 1)
+ host = base1.lower()
+
+ # Make sure it's always possible to see an unstyled page
+ if host == '127.0.0.1':
+ return None
+
+ # Obtain settings. Do nothing if not found
+
+ registry = queryUtility(IRegistry)
+ if registry is None:
+ return None
+
+ try:
+ settings = registry.forInterface(IThemeSettings)
+ except KeyError:
+ return None
+
+ if settings is None:
+ return None
+
+ if not settings.enabled:
+ return None
+
+ rules = settings.rules
+ if not rules:
+ return None
+
+ try:
+ key = getSite().absolute_url()
+ except AttributeError:
+ return None
+ cache = getCache(settings, key)
+
+ # Apply theme
+ transform = None
+
+ if not DevelopmentMode:
+ transform = cache.transform
+
+ if transform is None:
+ absolutePrefix = settings.absolutePrefix or None
+ readNetwork = settings.readNetwork
+ accessControl = etree.XSLTAccessControl(read_file=True, write_file=False, create_dir=False, read_network=readNetwork, write_network=False)
+
+ if absolutePrefix:
+ absolutePrefix = expandAbsolutePrefix(absolutePrefix)
+
+ internalResolver = InternalResolver()
+ pythonResolver = PythonResolver()
+ if readNetwork:
+ networkResolver = NetworkResolver()
+
+ rulesParser = etree.XMLParser(recover=False)
+ rulesParser.resolvers.add(internalResolver)
+ rulesParser.resolvers.add(pythonResolver)
+ if readNetwork:
+ rulesParser.resolvers.add(networkResolver)
+
+ themeParser = etree.HTMLParser()
+ themeParser.resolvers.add(internalResolver)
+ themeParser.resolvers.add(pythonResolver)
+ if readNetwork:
+ themeParser.resolvers.add(networkResolver)
+
+ compilerParser = etree.XMLParser()
+ compilerParser.resolvers.add(internalResolver)
+ compilerParser.resolvers.add(pythonResolver)
+ if readNetwork:
+ compilerParser.resolvers.add(networkResolver)
+
+ compiledTheme = compile_theme(rules,
+ absolute_prefix=absolutePrefix,
+ parser=themeParser,
+ rules_parser=rulesParser,
+ compiler_parser=compilerParser,
+ read_network=readNetwork,
+ access_control=accessControl,
+ update=False,
+ )
+
+ if not compiledTheme:
+ return None
+
+ transform = etree.XSLT(compiledTheme,
+ access_control=accessControl,
+ )
+
+ if not DevelopmentMode:
+ cache.updateTransform(transform)
+
+ return transform
+
+
+ def parseTree(self, result):
+ contentType = self.request.response.getHeader('Content-Type')
+ if contentType is None or not contentType.startswith('text/html'):
+ return None
+
+ contentEncoding = self.request.response.getHeader('Content-Encoding')
+ if contentEncoding and contentEncoding in ('zip', 'deflate', 'compress',):
+ return None
+
+ try:
+ return getHTMLSerializer(result, pretty_print=False)
+ except (TypeError, etree.ParseError):
+ return None
+
+ def transformString(self, result, encoding):
+ return self.transformIterable([result], encoding)
+
+ def transformUnicode(self, result, encoding):
+ return self.transformIterable([result], encoding)
+
+ def transformIterable(self, result, encoding):
+ """Apply the transform if required
+ """
+
+ result = self.parseTree(result)
+ if result is None:
+ return None
+
+ transform = self.setupTransform()
+ if transform is None:
+ return None
+
+ # Find real or virtual path - PATH_INFO has VHM elements in it
+ actualURL = self.request.get('ACTUAL_URL')
+
+ siteURL = getSite().absolute_url()
+ path = actualURL[len(siteURL):]
+
+ # Find the host name
+ base1 = self.request.get('BASE1')
+ _, base1 = base1.split('://', 1)
+ host = base1.lower()
+
+ transformed = transform(result.tree, host='"%s"' % host, path='"%s"' % path)
+ if transformed is None:
+ return None
+
+ result.tree = transformed
+
+ return result
94 src/plone/app/theming/utils.py
@@ -0,0 +1,94 @@
+import pkg_resources
+
+from lxml import etree
+
+from zope.site.hooks import getSite
+from zope.globalrequest import getRequest
+
+from plone.subrequest import subrequest
+
+from Products.CMFCore.utils import getToolByName
+
+class NetworkResolver(etree.Resolver):
+ """Resolver for network urls
+ """
+ def resolve(self, system_url, public_id, context):
+ if '://' in system_url and system_url != 'file:///__diazo__':
+ return self.resolve_filename(system_url, context)
+
+class PythonResolver(etree.Resolver):
+ """Resolver for python:// paths
+ """
+
+ def resolve(self, system_url, public_id, context):
+ if not system_url.lower().startswith('python://'):
+ return None
+ filename = resolvePythonURL(system_url)
+ return self.resolve_filename(filename, context)
+
+
+def resolvePythonURL(url):
+ """Resolve the python resource url to it's path
+
+ This can resolve python://dotted.package.name/file/path URLs to paths.
+ """
+ assert url.lower().startswith('python://')
+ spec = url[9:]
+ package, resource_name = spec.split('/', 1)
+ return pkg_resources.resource_filename(package, resource_name)
+
+
+class InternalResolver(etree.Resolver):
+ """Resolver for internal absolute and relative paths (excluding protocol).
+ If the path starts with a /, it will be resolved relative to the Plone
+ site navigation root.
+ """
+
+ def resolve(self, system_url, public_id, context):
+ request = getRequest()
+ if request is None:
+ return None
+
+ # Ignore URLs with a scheme
+ if '://' in system_url:
+ return None
+
+ # Ignore the special 'diazo:' resolvers
+ if system_url.startswith('diazo:'):
+ return None
+
+ portal = getPortal()
+ if portal is None:
+ return None
+
+ response = subrequest(system_url, root=portal)
+ if response.status != 200:
+ return None
+ result = response.body or response.stdout.getvalue()
+ return self.resolve_string(result, context)
+
+
+def getPortal():
+ """Return the portal object
+ """
+ # is site ever not the portal?
+ site = getSite()
+ if site is None:
+ return None
+ portal_url = getToolByName(site, 'portal_url', None)
+ if portal_url is None:
+ return None
+ return portal_url.getPortalObject()
+
+def expandAbsolutePrefix(prefix):
+ """Prepend the Plone site URL to the prefix if it starts with /
+ """
+ if not prefix or not prefix.startswith('/'):
+ return prefix
+ portal = getPortal()
+ if portal is None:
+ return ''
+ path = portal.absolute_url_path()
+ if path and path.endswith('/'):
+ path = path[:-1]
+ return path + prefix

0 comments on commit c2d2b5a

Please sign in to comment.
Something went wrong with that request. Please try again.