├── demoapp ├── __init__.py ├── xsltdemo │ ├── __init__.py │ ├── views.py │ ├── models.py │ └── tests.py ├── db ├── manage.py ├── urls.py ├── transforms │ ├── testtransform_errorpage.xslt │ ├── testtransform_errorvalue.xslt │ └── testtransform_simplepage.xslt └── settings.py ├── AUTHORS ├── src └── djangoxslt │ ├── __init__.py │ └── xslt │ ├── __init__.py │ ├── urls.py │ ├── models.py │ ├── testhelp.py │ ├── views.py │ ├── managers.py │ ├── tests.py │ └── engine.py ├── .hgignore ├── MANIFEST.in ├── .hgtags ├── LICENSE ├── .veh.conf ├── setup.py └── README.creole /demoapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demoapp/xsltdemo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Nic Ferrier 2 | -------------------------------------------------------------------------------- /src/djangoxslt/__init__.py: -------------------------------------------------------------------------------- 1 | # Nothing here 2 | -------------------------------------------------------------------------------- /demoapp/xsltdemo/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax:glob 2 | *.pyc 3 | *~ 4 | .venv* 5 | build* 6 | dist* -------------------------------------------------------------------------------- /demoapp/db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicferrier/django-xslt/master/demoapp/db -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.creole 4 | include src 5 | -------------------------------------------------------------------------------- /demoapp/xsltdemo/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /src/djangoxslt/xslt/__init__.py: -------------------------------------------------------------------------------- 1 | # XSLT 2 | 3 | """ 4 | An XSLT template engine for Django. 5 | """ 6 | 7 | from engine import * 8 | 9 | # End 10 | -------------------------------------------------------------------------------- /src/djangoxslt/xslt/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns( 4 | 'djangoxslt.xslt.views', 5 | url(r'^testtransform/(?P[A-Z-a-z0-9_-]+)/$', 'page', {"namespace": "testtransform_"}), 6 | ) 7 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 9ea2c75ac259d939f90068d6809d111e57f746c3 release_0_1 2 | 621f61eb949f7161a599067e4e0762c96293dedf release_0_2 3 | 170f108cf59bf04754e2684e3a71731ece015bcf release_0_4 4 | d0b0e2912576b2b28fa473de3e4933fa933dfb14 release_0_4_1 5 | 6f80f95959465564a8dba2bfd67fa5047df3dfca release_0_42 6 | bd67315e89d7aa2c63101a443abc56c8b02f563c release_0_4_3 7 | 610b6983b647f4a07b08bf8fb78c0c538c605b40 release_0_4_4 8 | 51016a43ecf24aae9c307e70553b00a8a08d92b3 release_0_4_5 9 | -------------------------------------------------------------------------------- /demoapp/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /demoapp/xsltdemo/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates two different styles of tests (one doctest and one 3 | unittest). These will both pass when you run "manage.py test". 4 | 5 | Replace these with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | class SimpleTest(TestCase): 11 | def test_basic_addition(self): 12 | """ 13 | Tests that 1 + 1 always equals 2. 14 | """ 15 | self.failUnlessEqual(1 + 1, 2) 16 | 17 | __test__ = {"doctest": """ 18 | Another way to test that 1 + 1 is equal to 2. 19 | 20 | >>> 1 + 1 == 2 21 | True 22 | """} 23 | 24 | -------------------------------------------------------------------------------- /src/djangoxslt/xslt/models.py: -------------------------------------------------------------------------------- 1 | """Models for XSLT. 2 | 3 | These are purely for testing the XSLT code. 4 | """ 5 | 6 | from django.db import models 7 | from managers import RenderingManager 8 | 9 | # A test manager 10 | class XSLTTestManager(RenderingManager): 11 | pass 12 | 13 | # A test model 14 | class XSLTTestModel(models.Model): 15 | name = models.CharField(max_length=50) 16 | about = models.TextField() 17 | count = models.IntegerField() 18 | 19 | # Setup the manager to be a rendering manager 20 | objects = XSLTTestManager() 21 | 22 | def __xml__(self): 23 | return "%s" % self.name 24 | 25 | # End 26 | -------------------------------------------------------------------------------- /demoapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | 7 | urlpatterns = patterns( 8 | '', 9 | # Example: 10 | # (r'^demoapp/', include('demoapp.foo.urls')), 11 | 12 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 13 | # to INSTALLED_APPS to enable admin documentation: 14 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')), 15 | 16 | # Uncomment the next line to enable the admin: 17 | # (r'^admin/', include(admin.site.urls)), 18 | 19 | (r'', include('djangoxslt.xslt.urls')), 20 | ) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) WooMedia Inc. 2 | All rights reserved. 3 | 4 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 5 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 6 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 7 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 8 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 9 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 10 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 11 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 12 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 13 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /.veh.conf: -------------------------------------------------------------------------------- 1 | [packages] 2 | # enter package names here like 3 | # packagelabel = packagename 4 | # 5 | # The package name on the right are passed directly to pip. 6 | # The package label is not used right now. 7 | # 8 | # This makes it easy to use either a pypi name: 9 | # packagelabel = packagename 10 | # or if the package name is the same as the option label: 11 | # packagelabel = 12 | # 13 | # or a url: 14 | # packagelabel = http://pypi.python.org/packages/source/v/virtualenv/virtualenv-1.4.9.tar.gz#md5=c49067cab242b5ff8c7b681a5a99533a 15 | # 16 | # or a vc reference: 17 | # packagelabel = hg+http://domain/repo 18 | django = file:///home/nferrier/woome/django-hg-1.1 19 | lxml = lxml==2.3 20 | 21 | [pip] 22 | # supported options for pip: 23 | always-upgrade: true 24 | # supply the --upgrade option to pip when building the virtualenv 25 | 26 | # End 27 | -------------------------------------------------------------------------------- /demoapp/transforms/testtransform_errorpage.xslt: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

This is a test error page

20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 |
28 | -------------------------------------------------------------------------------- /demoapp/transforms/testtransform_errorvalue.xslt: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

This is a test error page

20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 |
28 | -------------------------------------------------------------------------------- /demoapp/transforms/testtransform_simplepage.xslt: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 21 | 22 | 23 | 24 | WooMe Test 25 | 26 | 27 |

This is a test page

28 | 29 | 30 |
31 | 32 |
33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #from setuptools import setup 2 | #from setuptools import find_packages 3 | from setuptools import setup 4 | 5 | classifiers = [ 6 | 'Development Status :: 3 - Alpha', 7 | 'Intended Audience :: Developers', 8 | 'License :: OSI Approved :: BSD License', 9 | 'Operating System :: OS Independent', 10 | 'Programming Language :: Python', 11 | 'Topic :: Utilities', 12 | 'Topic :: Communications :: Email', 13 | ] 14 | 15 | # Depends: 16 | # django 17 | setup( 18 | name = "django-xslt", 19 | version = "0.4.5", 20 | description = "an XSLT template system for Django", 21 | long_description = """A replacment for Django's template system based on XSLT.""", 22 | license = "BSD", 23 | author = "Nic Ferrier", 24 | author_email = "nic@woome.com", 25 | url = "http://github.com/woome/django-xslt", 26 | download_url="http://github.com/woome/django-xslt/downloads", 27 | platforms = ["unix"], 28 | packages = ["djangoxslt", "djangoxslt.xslt"], 29 | package_dir = {"":"src"}, 30 | # Not sure we need a script, it would be nice to ship a django command line xsltproc? 31 | # scripts=['src/md'], 32 | classifiers = classifiers 33 | ) 34 | -------------------------------------------------------------------------------- /src/djangoxslt/xslt/testhelp.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | from StringIO import StringIO 3 | 4 | # This is used as a parsed-XML cache 5 | # We put all the content we parse here keyed by content 6 | XMLCACHE={} 7 | 8 | def assertXpath(xml, xpr, assertion_message="", namespaces=None, html=False): 9 | """Assert the Xpath 'xpr' evals against the 'xml' document. 10 | 11 | assertion_message can be specified as an alternative error message from a failure. 12 | namespaces can be specified as a list of namespace key:url pairs to be passed to XSLT 13 | html is boolean to specify whether to parse the document as HTML or not. 14 | """ 15 | 16 | namespaces = {} if not namespaces else namespaces 17 | try: 18 | doc = XMLCACHE.get(xml) 19 | if not doc: 20 | parser = etree.HTMLParser() if html else etree.XMLParser() 21 | doc = etree.parse(StringIO(xml), parser) 22 | XMLCACHE[xml] = doc 23 | except Exception, e: 24 | raise e 25 | else: 26 | ret = doc.xpath(xpr, namespaces=namespaces) if namespaces else doc.xpath(xpr) 27 | if not ret: 28 | assertion_message = assertion_message \ 29 | if assertion_message \ 30 | else "{%s} did not evaluate with the specified document" % xpr 31 | raise AssertionError(assertion_message) 32 | 33 | # End 34 | -------------------------------------------------------------------------------- /src/djangoxslt/xslt/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.template import RequestContext 3 | from django.conf import settings 4 | from os.path import join 5 | 6 | import logging 7 | 8 | from engine import TransformerFile 9 | from engine import EMPTYDOC 10 | from django.conf import settings 11 | 12 | DEFAULT_PAGE_NAMESPACE="" # WooMe's page namespace is "woome" 13 | DEFAULT_PAGE_PATTERN="%s%s.xslt" # WooMe's page pattern is "%s_%s.xslt" 14 | 15 | def page(request, page="index", namespace=DEFAULT_PAGE_NAMESPACE, **kwargs): 16 | """A generic XSLT view which just runs a page name derived XSLT file. 17 | 18 | Pass in a page to be rendered (this could come from a urls 19 | statement) and optionally a namespace. 20 | 21 | The 'namespace' allows you to namespace all the XSLT files that 22 | will be used like this in your transforms directory. For example, 23 | to serve a page ^main/$ you could write an XSLT called 24 | static_main.xslt. To use that you would declare your page url 25 | like: 26 | 27 | '(r'(?P.*)/$', 'djangoxslt.xslt.views.page', {"namespace": "static"}), 28 | 29 | In order to make the namespace work we also allow a page pattern 30 | to be specified in the settings file. The page pattern is used to 31 | define how the namespace and the page name combine. The above 32 | example would require the following declared in settings.py: 33 | 34 | XSLT_PAGE_PATTERN="%s_%s.xslt" 35 | """ 36 | logger = logging.getLogger("xslt.views.page") 37 | logger.info("page = %s namespace = %s" % (page, namespace)) 38 | page_pattern = getattr(settings, "XSLT_PAGE_PATTERN", DEFAULT_PAGE_PATTERN) 39 | p = page_pattern % (namespace, page) 40 | t = TransformerFile(join(settings.TRANSFORMS, p)) 41 | c = RequestContext(request, {}) 42 | c.update(kwargs) 43 | out = t(EMPTYDOC, context=c) 44 | return HttpResponse(out) 45 | 46 | 47 | # End 48 | -------------------------------------------------------------------------------- /demoapp/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for demoapp project. 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ADMINS = ( 7 | # ('Your Name', 'your_email@domain.com'), 8 | ) 9 | 10 | MANAGERS = ADMINS 11 | 12 | DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 13 | DATABASE_NAME = 'db' # Or path to database file if using sqlite3. 14 | DATABASE_USER = '' # Not used with sqlite3. 15 | DATABASE_PASSWORD = '' # Not used with sqlite3. 16 | DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. 17 | DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. 18 | 19 | # Local time zone for this installation. Choices can be found here: 20 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 21 | # although not all choices may be available on all operating systems. 22 | # If running in a Windows environment this must be set to the same as your 23 | # system time zone. 24 | TIME_ZONE = 'America/Chicago' 25 | 26 | # Language code for this installation. All choices can be found here: 27 | # http://www.i18nguy.com/unicode/language-identifiers.html 28 | LANGUAGE_CODE = 'en-us' 29 | 30 | SITE_ID = 1 31 | 32 | # If you set this to False, Django will make some optimizations so as not 33 | # to load the internationalization machinery. 34 | USE_I18N = True 35 | 36 | # Absolute path to the directory that holds media. 37 | # Example: "/home/media/media.lawrence.com/" 38 | MEDIA_ROOT = '' 39 | 40 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 41 | # trailing slash if there is a path component (optional in other cases). 42 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 43 | MEDIA_URL = '' 44 | 45 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 46 | # trailing slash. 47 | # Examples: "http://foo.com/media/", "/media/". 48 | ADMIN_MEDIA_PREFIX = '/media/' 49 | 50 | # Make this unique, and don't share it with anybody. 51 | SECRET_KEY = 'l$g+jqw)vzy(vdguzz0z_(#j)q_b7#nsaqn_b8re3($rbf@s)&' 52 | 53 | MIDDLEWARE_CLASSES = ( 54 | 'django.middleware.common.CommonMiddleware', 55 | 'django.contrib.sessions.middleware.SessionMiddleware', 56 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 57 | ) 58 | 59 | ROOT_URLCONF = 'urls' 60 | 61 | INSTALLED_APPS = ( 62 | 'django.contrib.auth', 63 | 'django.contrib.contenttypes', 64 | 'django.contrib.sessions', 65 | 'djangoxslt.xslt', # you don't need to include the app, we do so we can see the tests run 66 | 'xsltdemo', 67 | ) 68 | 69 | TRANSFORMS="transforms" 70 | -------------------------------------------------------------------------------- /README.creole: -------------------------------------------------------------------------------- 1 | This is an XSLT template engine for Django. 2 | 3 | XSLT is a powerful templating language and this package extends it 4 | further with the ability to render to XML any Django context object 5 | (including querysets). 6 | 7 | == Very simple use == 8 | 9 | djangoxslt includes a view for mapping requests to XSLT pages 10 | directly. This is very easy to use. For example: 11 | 12 | {{{ 13 | (r'^(?P[A-Za-z0-9_.-]+)/*$', 'djangoxslt.xslt.views.page'), 14 | }}} 15 | 16 | will try to load an XSLT for any top level page, eg: /main.html or /top/ 17 | 18 | For more infromation see the {{{djangoxslt.xslt.views.page}}} method. 19 | 20 | 21 | == Some XSLT examples == 22 | 23 | The djangoxslt system causes Django {{{RequestContext}}} variables to 24 | be mapped into an XSLT function namespace. You can render Django 25 | values by calling them as XSLT functions. 26 | 27 | This requires that you declare the xdjango namespace in your XSLT like 28 | this: 29 | 30 | {{{ 31 | 32 | 37 | . 38 | . 39 | . 40 | }}} 41 | 42 | === Render simple values === 43 | 44 | Spitting out the {{{MEDIA_ROOT}}} from the Django settings file: 45 | 46 | {{{ 47 | 48 | }}} 49 | 50 | this will be possible if you have added the Django builtin context 51 | processor {{{django.core.context_processors.media}}} to the 52 | {{{TEMPLATE_CONTEXT_PROCESSORS}}} in the settings file. 53 | 54 | 55 | === Render more complex objects === 56 | 57 | Rendering a form: 58 | 59 | {{{ 60 | 65 | }}} 66 | 67 | this will work if you attach the form in the obvious way: 68 | 69 | {{{ 70 | ctx = RequestContext(request, { 71 | "comment": CommentForm(), 72 | }) 73 | return render_to_response("episode.xslt", ctx) 74 | }}} 75 | 76 | note that we had to specifically declare the namespace on the nodes 77 | coming out of the xdjango function. This is because the xdjango 78 | namespace is presumed to be the namepsace of the result 79 | fragment. {{{parsehtml}}} will always produce XHTML tho so we have to 80 | specify the namespace. 81 | 82 | This would require an XSLT declaration something like this: 83 | 84 | {{{ 85 | 86 | 93 | . 94 | . 95 | . 96 | }}} 97 | 98 | Note the double declaration of the XHTML namespace. 99 | 100 | 101 | === Display Query Sets == 102 | 103 | First, iterating over a queryset context object and rendering the username 104 | and the id: 105 | 106 | {{{ 107 | 108 | 109 | 110 | }}} 111 | 112 | this renders a queryset attached to a context - something like this: 113 | 114 | {{{ 115 | from djangoxslt.xslt.managers import xmlify 116 | qs = Users.objects.filter(gender="F") 117 | ctx = RequestContext(request, { 118 | "users": xmlify(qs, 119 | username="username", 120 | id="id" 121 | ) 122 | }) 123 | return render_to_response("myxslt.xslt", ctx) 124 | }}} 125 | 126 | == Project structure == 127 | 128 | This project is {{{veh}}} enabled. See 129 | [[http://github.com/nicferrier/veh|here]] for more information about 130 | {{{veh}}}. 131 | 132 | This project comes with a demoapp which is included to illustrate how 133 | to add xslt to any project but also to facilitate unit testing of the 134 | current code. 135 | 136 | -------------------------------------------------------------------------------- /src/djangoxslt/xslt/managers.py: -------------------------------------------------------------------------------- 1 | # Model Managers. 2 | 3 | from django.db import models 4 | from django.template import Template 5 | from django.template import Context 6 | import types 7 | 8 | class XPathRenderer(object): 9 | """This is an interface for objects that have __xml__""" 10 | def __xml__(self, *args): 11 | return "" 12 | 13 | def xmlify(qs, use_values=True, **kwargs): 14 | """XML serializer for queryset qs using the template described in kwargs. 15 | 16 | This operates a little like django values (indeed it uses django 17 | values if use_values is True) but allows you to specify the 18 | resulting XML name. 19 | 20 | So this call: 21 | 22 | xmlify(qs, **{ 23 | "xml_field_name": "django_queryset__model__accessor", 24 | "other_xml_field": "django_queryset__deep__reference__field", 25 | "xml_field": "django_queryset__model__field|upper", 26 | }).__xml__() 27 | 28 | produces: 29 | 30 | [ "xml_field_name='value from django_queryset.model.accessor row 1' 31 | other_xml_field='value from django_queryset.deep.reference.field row 1' 32 | xml_field='value to caps from django_queryset.model.field row 1'", 33 | 34 | "xml_field_name='value from django_queryset.model.accessor row 2' 35 | other_xml_field='value from django_queryset.deep.reference.field row 2' 36 | xml_field='value to caps from django_queryset.model.field row 2'", 37 | . 38 | . 39 | . 40 | ] 41 | 42 | Any valid django template may be used on the django field name. 43 | 44 | If use_values is False then a normal queryset is used instead of a 45 | values queryset. 46 | 47 | The best way to use this with XSLT is to attach xmlify-ed 48 | querysets to a request context and then call render_to_response 49 | with the context object. 'xdjango:contextobject()' can then 50 | retrieve the queryset. 51 | """ 52 | captured_qs = qs 53 | class XML(XPathRenderer): 54 | def __init__(self): 55 | self._cached = None 56 | 57 | def __xml__(self, *args): 58 | if self._cached == None: 59 | self._cached = self.__evalxml__(*args) 60 | return self._cached 61 | 62 | def __evalxml__(self, *args): 63 | template_list = [(name, Template('{{%s}}' % value)) \ 64 | for name,value in kwargs.iteritems()] 65 | django_fields = [template.split("|")[0].split(".")[0] for template in kwargs.values()] 66 | 67 | # Do the query that gets the data to XML 68 | # Ordinarily we use 'values' but we can use an ordinary query if necessary 69 | rows = [] 70 | text_fields = [] 71 | if use_values: 72 | rows = captured_qs.values(*django_fields) 73 | else: 74 | for row in captured_qs: 75 | row_result = {} 76 | if django_fields: 77 | for field in django_fields: 78 | value = getattr(row, field) 79 | row_result[field] = value() if isinstance(value, types.MethodType) else value 80 | if getattr(value, 'is_text', False): 81 | text_fields.append(field) 82 | else: 83 | row_result = row.__xml__() 84 | 85 | rows += [row_result] 86 | 87 | # Make a nice list of template outputed rows 88 | from lxml import etree 89 | xmlname = captured_qs.model.__name__ 90 | xmlroot = etree.Element("%ss" % xmlname.lower()) 91 | #import pdb 92 | #pdb.set_trace() 93 | for record in rows: 94 | c = Context() 95 | c.update(record) 96 | child = etree.SubElement(xmlroot, xmlname.lower()) 97 | if template_list: 98 | for name,template in template_list: 99 | if name in text_fields: 100 | elem = etree.SubElement(child, name) 101 | elem.text = template.render(c) 102 | else: 103 | child.attrib[name] = template.render(c) 104 | else: 105 | parsed = etree.XML(record) 106 | elem = child.append(parsed) 107 | 108 | return xmlroot 109 | 110 | return XML() 111 | 112 | def xmlifyiter(iterator, name, **kwargs): 113 | """XML serializer to elements of 'name'. 114 | 115 | The kwargs specifies the XML -> iterator values mapping. 116 | 117 | At the moment the iterator values are expected to be dictionary 118 | type objects. 119 | 120 | The kwargs specifies: 121 | 122 | an xml result attribute name = "an iterator value object dictionary key" 123 | 124 | for example: 125 | 126 | xmlifyiter(iteratorobject, "Iterable", name="username", age="age") 127 | 128 | => 129 | 130 | 131 | 132 | 133 | 134 | """ 135 | captured_iter = iterator 136 | captured_element_name = name 137 | class XML(XPathRenderer): 138 | def __init__(self): 139 | self._cached = None 140 | 141 | def __xml__(self, *args): 142 | if self._cached == None: 143 | self._cached = self.__evalxml__(*args) 144 | return self._cached 145 | 146 | def __evalxml__(self, *args): 147 | template_list = [(name, Template('{{%s}}' % value)) \ 148 | for name,value in kwargs.iteritems()] 149 | dict_keys = [template.split("|")[0].split(".")[0] for template in kwargs.values()] 150 | 151 | rows = [] 152 | text_fields = [] 153 | for row in captured_iter: 154 | row_result = {} 155 | for field in dict_keys: 156 | value = row.get(field) 157 | row_result[field] = value 158 | if getattr(value, 'is_text', False): 159 | text_fields.append(field) 160 | rows += [row_result] 161 | 162 | # Make a nice list of template outputed rows 163 | from lxml import etree 164 | xmlname = captured_element_name 165 | xmlroot = etree.Element("%ss" % xmlname.lower()) 166 | #import pdb 167 | #pdb.set_trace() 168 | for record in rows: 169 | c = Context() 170 | c.update(record) 171 | child = etree.SubElement(xmlroot, xmlname.lower()) 172 | for name,template in template_list: 173 | if name in text_fields: 174 | elem = etree.SubElement(child, name) 175 | elem.text = template.render(c) 176 | else: 177 | child.attrib[name] = template.render(c) 178 | return xmlroot 179 | 180 | return XML() 181 | 182 | 183 | from django.db.models.query import QuerySet 184 | class XmlQuerySet(QuerySet): 185 | """A queryset that does xmlifying""" 186 | def xml(self, **kwargs): 187 | return xmlify(self, use_values=getattr(self, "use_values", True), **kwargs) 188 | 189 | def xml_objects(self, **kwargs): 190 | """Don't do a values query. Use the full object instead.""" 191 | return xmlify(self, use_values=getattr(self, "use_values", False), **kwargs) 192 | 193 | 194 | def monkey_qs(qs, use_values=True): 195 | """Clone the queryset adding an 'xml' method. 196 | 197 | The 'xml' method works like 'xmlify', pass kwargs for rendering 198 | arguments and it returns an object which supports the __xml__ 199 | protocol. 200 | 201 | The objects returned from the 'xml' object attached with 202 | 'monkey_qs' are also querysets which can be further cloned. 203 | 204 | For example: 205 | 206 | qs1 = Person.objects.filter(user__last_name="smith") 207 | qs2 = monkey_qs(qs1) 208 | qs3 = qs2.xml(username="user__username", firstname="user__firstname") 209 | qs4 = qs3.filter(age__gte=18) 210 | xmldata = qs4.__xml__() 211 | """ 212 | capturedclone = qs._clone 213 | 214 | def baseclone(xml_closure): 215 | newclone = capturedclone() 216 | newclone.__xml__ = lambda *args: xml_closure.__xml__(*args) 217 | newclone._clone = lambda: baseclone(xml_closure) 218 | return newclone 219 | 220 | def adapt(*args, **kwargs): 221 | newclone = capturedclone(*args, **kwargs) 222 | xml_protocol_obj = xmlify(qs, use_values=use_values, **kwargs) 223 | newclone.__dict__["__xml__"] = lambda *args: xml_protocol_obj.__xml__(*args) 224 | newclone.__dict__["_clone"] = lambda: baseclone(xml_protocol_obj) 225 | return newclone 226 | 227 | def xmlclone(selfarg, *args, **kwargs): 228 | newclone = capturedclone(*args, **kwargs) 229 | newclone.__dict__["xml"] = lambda *args,**kwargs: adapt(*args, **kwargs) 230 | newclone.__dict__["use_values"] = use_values 231 | newclone.__dict__["_clone"] = types.MethodType(xmlclone, selfarg, selfarg.__class__) 232 | return newclone 233 | return xmlclone(qs) 234 | 235 | 236 | class RenderingManager(models.Manager): 237 | """Use monkey_qs to decorate the queryset. 238 | 239 | This makes querysets that use values calls by default. To get a 240 | queryset that will render without using values create the manager 241 | with: 242 | 243 | use_values=False 244 | """ 245 | def __init__(self, use_values=True): 246 | models.Manager.__init__(self) 247 | self.use_values = use_values 248 | 249 | def get_query_set(self): 250 | qs = super(RenderingManager, self).get_query_set() 251 | return monkey_qs(qs) 252 | 253 | # End 254 | -------------------------------------------------------------------------------- /src/djangoxslt/xslt/tests.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | """Tests for xslt""" 4 | 5 | import time 6 | import re 7 | from django.template import Context 8 | from testhelp import assertXpath 9 | from unittest import TestCase 10 | from djangoxslt import xslt 11 | import logging 12 | logging.basicConfig() 13 | 14 | BLANK = """ 15 | 21 | 22 | 23 | %s 24 | 25 | 26 | """ 27 | 28 | class DjangoContextFuncTest(TestCase): 29 | def setUp(self): 30 | super(DjangoContextFuncTest, self).setUp() 31 | self.func = xslt.DjangoContextFunc("testfunc") 32 | c = Context({'testfunc': 'a value'}) 33 | xslt.djangothread.context = c 34 | 35 | def test_parse_str(self): 36 | result = self.func.parse("no") 37 | 38 | def test_parse_unicode(self): 39 | result = self.func.parse(u"") 40 | 41 | def test_parsehtml_str(self): 42 | result = self.func.parsehtml("no") 43 | 44 | def test_parsehtml_unicode(self): 45 | result = self.func.parsehtml(u"") 46 | 47 | def test_call_str(self): 48 | result = self.func("testval") 49 | assert result == "a value" 50 | 51 | def test_call_unicode(self): 52 | c = Context({'testfunc': u'☃☃☃☃☃'}) 53 | xslt.djangothread.context = c 54 | result = self.func(u"téstv☃al") 55 | assert result == u'☃☃☃☃☃' 56 | 57 | def tearDown(self): 58 | xslt.djangothread.context = None 59 | 60 | 61 | from django.test.client import Client 62 | 63 | class XSLTTest(TestCase): 64 | """Test the xslt system.. 65 | """ 66 | def setUp(self): 67 | self.client = Client() 68 | 69 | def test_simple_page(self): 70 | """Test that we can retrieve the simple page.""" 71 | response = self.client.get("/testtransform/simplepage/") 72 | self.assertEquals(response.status_code, 200) 73 | # We should really assert some xpath things about it. 74 | 75 | from djangoxslt.xslt import managers as xsltmanagers 76 | 77 | def _qs_eval_helper(qs): 78 | """This is a useful little bit of code to eval the xml dom result. 79 | 80 | The qs MUST have the __xml__ method, ie: it must result from an 81 | .xml(...) somethere in the chain.""" 82 | 83 | from lxml import etree 84 | data1 = qs.__xml__() 85 | strresult = etree.tostring(data1) 86 | # print strresult 87 | return strresult 88 | 89 | class IterableTest(TestCase): 90 | def setUp(self): 91 | super(IterableTest, self).setUp() 92 | self.time = int(time.time() * 1000) 93 | 94 | def test_iterable_render(self): 95 | tmpl = BLANK % """ 96 | 97 | """ % self.time 98 | transformer = xslt.Transformer(tmpl) 99 | 100 | dictlist = [ 101 | {"a": 10, "b": 20, "c": 40}, 102 | {"a": 11, "b": 21, "c": 41}, 103 | {"a": 12, "b": 22, "c": 42}, 104 | ] 105 | 106 | xmlobj = xsltmanagers.xmlifyiter( 107 | dictlist, 108 | "Simple", 109 | attriba="a", 110 | otherattrib="c" 111 | ) 112 | 113 | # Use objects and NOT values 114 | context_key = 'foo%d' % self.time 115 | c = Context({ 116 | context_key: xmlobj, 117 | }) 118 | 119 | res = transformer(context=c) 120 | assertXpath( 121 | res, 122 | '//simples/simple[@attriba="%s"]' % 10 123 | ) 124 | 125 | class QSRenderTestCase(TestCase): 126 | def setUp(self): 127 | super(QSRenderTestCase, self).setUp() 128 | self.time = int(time.time() * 1000) 129 | 130 | def test_queryset_render_non_values(self): 131 | """Tests that querysets can be rendered without using 'values'. 132 | 133 | Takes a django User object and monkey_qs's it then test the 134 | xml is returned correctly when we use a dynamic method and 135 | therefore specify no to 'use_values' 136 | """ 137 | tmpl = BLANK % """ 138 | 139 | """ % self.time 140 | transformer = xslt.Transformer(tmpl) 141 | 142 | from django.contrib.auth.models import User 143 | user = User( 144 | username="user%s" % self.time, 145 | password="password%s" % self.time, 146 | first_name="first%s" % self.time, 147 | last_name="last%s" % self.time 148 | ) 149 | user.save() 150 | 151 | # Use objects and NOT values 152 | qs = xsltmanagers.monkey_qs( 153 | User.objects.filter(username="user%s" % self.time), 154 | use_values=False 155 | ) 156 | 157 | context_key = 'foo%d' % self.time 158 | xml = qs.xml(full_name="get_full_name", username="username") 159 | c = Context({ context_key: xml }) 160 | res = transformer(context=c) 161 | assertXpath( 162 | res, 163 | '//users/user[@full_name="first%s last%s"]' % (self.time, self.time) 164 | ) 165 | 166 | def test_queryset_xmlify_user(self): 167 | """Tests that querysets can be rendered with the xmlify method. 168 | """ 169 | tmpl = BLANK % """ 170 | 171 | """ % self.time 172 | transformer = xslt.Transformer(tmpl) 173 | 174 | from django.contrib.auth.models import User 175 | user = User( 176 | username="user%s" % self.time, 177 | password="password%s" % self.time, 178 | first_name="first%s" % self.time 179 | ) 180 | user.save() 181 | 182 | # Make a queryset 183 | qs = User.objects.filter(username="user%s" % self.time) 184 | 185 | # Make an xml object from it 186 | xml_list = xsltmanagers.xmlify( 187 | qs, 188 | first_name="first_name", 189 | username="username" 190 | ) 191 | 192 | context_key = 'foo%d' % self.time 193 | c = Context({ context_key: xml_list }) 194 | res = transformer(context=c) 195 | assertXpath( 196 | res, 197 | '//users//user[@first_name="first%s"]' % self.time, 198 | ) 199 | 200 | 201 | def test_queryset_render_user(self): 202 | """Tests that querysets can be rendered. 203 | 204 | Takes a django User object and monkey_qs's it then test the 205 | xml is returned correctly. 206 | """ 207 | tmpl = BLANK % """ 208 | 209 | """ % self.time 210 | transformer = xslt.Transformer(tmpl) 211 | 212 | from django.contrib.auth.models import User 213 | user = User( 214 | username="user%s" % self.time, 215 | password="password%s" % self.time, 216 | first_name="first%s" % self.time 217 | ) 218 | user.save() 219 | qs = xsltmanagers.monkey_qs( 220 | User.objects.filter(username="user%s" % self.time) 221 | ) 222 | 223 | context_key = 'foo%d' % self.time 224 | xml = qs.xml(first_name="first_name", username="username") 225 | c = Context({ context_key: xml }) 226 | res = transformer(context=c) 227 | assertXpath( 228 | res, 229 | '//users/user[@first_name="first%s"]' % self.time, 230 | ) 231 | 232 | def test_queryset_render_testmodel(self): 233 | """Tests that querysets can be rendered. 234 | 235 | Takes our own test model and uses the model's built in manager 236 | which monkey patches all querysets coming out of it. 237 | """ 238 | tmpl = BLANK % """ 239 | 240 | """ % self.time 241 | transformer = xslt.Transformer(tmpl) 242 | 243 | from models import XSLTTestModel 244 | testobject = XSLTTestModel( 245 | name = "name%s" % self.time, 246 | about = "about%s" % self.time, 247 | count = 10 248 | ) 249 | testobject.save() 250 | 251 | # Do the query and pull back the xml 252 | xml = XSLTTestModel.objects.filter( 253 | name="name%s" % self.time 254 | ).xml( 255 | name="name", 256 | about_text="about", 257 | count="count" 258 | ) 259 | c = Context({ 'foo%d' % self.time: xml }) 260 | res = transformer(context=c) 261 | assertXpath( 262 | res, 263 | '//xslttestmodels/xslttestmodel[@name="name%s"]' % self.time, 264 | ) 265 | assertXpath( 266 | res, 267 | '//xslttestmodels/xslttestmodel[@about_text="about%s"]' % self.time, 268 | ) 269 | 270 | 271 | def test_queryset_render_testmodel_no_kwargs(self): 272 | """Tests that querysets can be rendered without kwargs. 273 | 274 | Takes our own test model and xmlify it with no kwargs causing 275 | it's own __xml__ method to be used to render the XML. 276 | """ 277 | tmpl = BLANK % """ 278 | 279 | """ % self.time 280 | transformer = xslt.Transformer(tmpl) 281 | 282 | from models import XSLTTestModel 283 | testobject = XSLTTestModel( 284 | name = "name%s" % self.time, 285 | about = "about%s" % self.time, 286 | count = 10 287 | ) 288 | testobject.save() 289 | 290 | # Do the query and pull back the xml 291 | xml = XSLTTestModel.objects.filter( 292 | name="name%s" % self.time 293 | ) 294 | 295 | xmlified = xsltmanagers.xmlify(xml, use_values=False) 296 | 297 | c = Context({ 'foo%d' % self.time: xmlified }) 298 | res = transformer(context=c) 299 | assertXpath( 300 | res, 301 | '//xslttestmodels/xslttestmodel/name[text()="name%s"]' % self.time, 302 | ) 303 | 304 | 305 | 306 | def test_queryset_render_persists(self): 307 | """Tests that queryset __xml__ method attachment works. 308 | 309 | The __xml__ method, attached by using the xml() method, should 310 | 'stick' to a queryset through further child-querysets. 311 | """ 312 | tmpl = BLANK % """ 313 | 314 | """ % self.time 315 | transformer = xslt.Transformer(tmpl) 316 | 317 | from models import XSLTTestModel 318 | for i in range(1,20): 319 | testobject = XSLTTestModel( 320 | name = "name%s" % (int(self.time) * i), 321 | about = "about%s" % (int(self.time) * i), 322 | count = i 323 | ) 324 | testobject.save() 325 | 326 | # Do the query - this qs should have an 'xml' method 327 | base_qs = XSLTTestModel.objects.filter( 328 | name="name%s" % self.time 329 | ) 330 | 331 | # Make a first sub-qs to check the xml method is being cloned 332 | qs = base_qs.filter(count__lte=12) 333 | 334 | # Check it has the xml method 335 | self.assert_("xml" in qs.__dict__) 336 | 337 | # Call the 'xml' method to store the xml to be generated and return a new qs 338 | qs_from_xml = qs.xml(name="name", about_text="about", count="count") 339 | 340 | # Check it has the __xml__ method 341 | self.assert_("__xml__" in qs_from_xml.__dict__) 342 | 343 | # Make another qs from the 'xml' decorated one 344 | selected = qs_from_xml.filter(count__lte=3) 345 | 346 | # Check the sub-queryset has the __xml__ method 347 | self.assert_("__xml__" in selected.__dict__) 348 | 349 | xml_result = _qs_eval_helper(selected) 350 | self.assert_(re.search(""" name="name%s"[ /]""" % self.time, xml_result)) 351 | 352 | c = Context({ 'foo%d' % self.time: selected }) 353 | res = transformer(context=c) 354 | assertXpath( 355 | res, 356 | '//xslttestmodels/xslttestmodel[@name="name%s"]' % self.time, 357 | ) 358 | assertXpath( 359 | res, 360 | '//xslttestmodels/xslttestmodel[@about_text="about%s"]' % self.time, 361 | ) 362 | 363 | ##### TODO!!!! 364 | ### need to assert we have the RIGHT number of xslttestmodel objects 365 | ### it should be 12 and not 3 because we do the .xml(...) before the lte=3 366 | 367 | 368 | 369 | class AVTTestCase(TestCase): 370 | def setUp(self): 371 | super(AVTTestCase, self).setUp() 372 | self.time = int(time.time() * 1000) 373 | 374 | def test_variable_usage(self): 375 | tmpl = BLANK % """ 376 | 377 | 378 | """ % self.time 379 | t = xslt.Transformer(tmpl) 380 | c = Context({'foo%d' % self.time: 'hello world'}) 381 | assert t(context=c) == 'hello world\n' 382 | 383 | def test_simple(self): 384 | tmpl = BLANK % """ 385 | 386 | """ % self.time 387 | t = xslt.Transformer(tmpl) 388 | c = Context({'foo%d' % self.time: 'hello world'}) 389 | assert t(context=c) == 'hello world\n' 390 | 391 | def test_rooted_avt(self): 392 | tmpl = BLANK % """ 393 | ! 394 | """ % self.time 395 | t = xslt.Transformer(tmpl) 396 | c = Context({'foo%d' % self.time: 'some-location'}) 397 | res = t(context=c) 398 | assertXpath( 399 | res, 400 | '//xhtml:a[@href="some-location/foo"]', 401 | namespaces={ 402 | "xhtml": "http://www.w3.org/1999/xhtml", 403 | } 404 | ) 405 | 406 | def test_nonroot_avt(self): 407 | tmpl = BLANK % """ 408 | ! 409 | """ % self.time 410 | t = xslt.Transformer(tmpl) 411 | c = Context({'foo%d' % self.time: 'some-location'}) 412 | res = t(context=c) 413 | assertXpath( 414 | res, 415 | '//xhtml:a[@href="/some-location/foo"]', 416 | namespaces={ 417 | "xhtml": "http://www.w3.org/1999/xhtml", 418 | } 419 | ) 420 | 421 | def test_nonroot_avt_method(self): 422 | tmpl = BLANK % """ 423 | ! 424 | """ % self.time 425 | t = xslt.Transformer(tmpl) 426 | c = Context({'foo%d' % self.time: 'some-location'}) 427 | res = t(context=c) 428 | assertXpath( 429 | res, 430 | '//xhtml:a[@href="/SOME-LOCATION/foo"]', 431 | namespaces={ 432 | "xhtml": "http://www.w3.org/1999/xhtml", 433 | } 434 | ) 435 | 436 | def test_method(self): 437 | tmpl = BLANK % """ 438 | 439 | """ % self.time 440 | t = xslt.Transformer(tmpl) 441 | c = Context({'foo%d' % self.time: 'hello world'}) 442 | assert t(context=c) == 'HELLO WORLD\n' 443 | 444 | def test_multiple_avt(self): 445 | tmpl = BLANK % """ 446 | ! 447 | """ % (self.time, self.time) 448 | t = xslt.Transformer(tmpl) 449 | c = Context({ 450 | 'foo%da' % self.time: 'some-location', 451 | 'foo%db' % self.time: 'more' 452 | }) 453 | res = t(context=c) 454 | assertXpath( 455 | res, 456 | '//xhtml:a[@href="/some-location/more/foo"]', 457 | namespaces={ 458 | "xhtml": "http://www.w3.org/1999/xhtml", 459 | } 460 | ) 461 | 462 | # End 463 | -------------------------------------------------------------------------------- /src/djangoxslt/xslt/engine.py: -------------------------------------------------------------------------------- 1 | # XSLT helper 2 | from __future__ import with_statement 3 | 4 | """ 5 | A Django template engine for XSLT. 6 | 7 | This isn't a real template engine, in the sense that it does not 8 | provide any Template class. We'll work towards that. 9 | 10 | It does allow Django to use XSLT templates and to include context 11 | variables in your templates with an xpath syntax. 12 | 13 | In order to do this we implement an extension system that makes 14 | context variables map to xpath functions. 15 | 16 | A context variable called as a function with no arguments returns the 17 | string rendering of the context variable. 18 | 19 | However, a context variable called with an argument is intended to 20 | allow abstracted rendering behaviour. The argument is called the 21 | ''mapper key'' and provides a string -> callable mapping which will be 22 | used to render the of the context variable to a legal XPath value. The 23 | callable mapped to is called the ''renderer''. 24 | 25 | Three renderers are defined by default: 26 | 27 | * xml 28 | 29 | returns the context variable string evaluated but wrapped in a DIV 30 | element. 31 | 32 | * parse 33 | 34 | attempts to XML parse the context variable string evaluation. 35 | 36 | * parsehtml 37 | 38 | attempts to HTML parse the context variable string evaluation, the 39 | parsed document is normalized to XML before being returned so that 40 | it can be succesfully evaluated by XSLT. 41 | 42 | Currently, we fix any HTML parsed document with the XHTML namespace. 43 | 44 | 45 | Further renderers may be specified in settings with the variable 46 | XSLT_MAPPER: 47 | 48 | import project.render 49 | XSLT_MAPPER = { 50 | "project_special_render": project.render.do_render 51 | } 52 | 53 | mapping keys defined in settings may override the default renderers. 54 | """ 55 | 56 | from django.conf import settings 57 | 58 | from lxml import etree 59 | from lxml.builder import E 60 | from StringIO import StringIO 61 | import re 62 | import logging 63 | import traceback 64 | 65 | # This is a simple empty document you can pass into Transformer.__call__ if you need to. 66 | EMPTYDOC = etree.Element("empty") 67 | 68 | # DO NOT change these without running the xslt unit-tests in this module. 69 | FUNC_MATCH_RE1 = re.compile(r"{?xdjango:[^}]+}?") 70 | FUNC_MATCH_RE2 = re.compile(r"{?xdjango:([^(]+)\((.*?)\)}?") 71 | 72 | # taken from drivel.config 73 | def dotted_import(name): 74 | mod, attr = name.split('.'), [] 75 | obj = None 76 | while mod: 77 | try: 78 | obj = __import__('.'.join(mod), {}, {}, ['']) 79 | except ImportError, e: 80 | attr.insert(0, mod.pop()) 81 | else: 82 | for a in attr: 83 | try: 84 | obj = getattr(obj, a) 85 | except AttributeError, e: 86 | raise AttributeError('could not get attribute %s from %s -> %s (%r)' % ( 87 | a, '.'.join(mod), '.'.join(attr), obj)) 88 | return obj 89 | raise ImportError('could not import %s' % name) 90 | 91 | 92 | class Exml(object): 93 | """A simple document builder for lxml""" 94 | def __init__(self, element_name, **attribs): 95 | self.el = None 96 | self.element_name = element_name 97 | self.attribs = attribs 98 | 99 | def append(self, element_name, **attribs): 100 | if self.el == None: 101 | self.el = etree.Element(self.element_name) 102 | for name,value in self.attribs.iteritems(): 103 | self.el.set(name,str(value)) 104 | 105 | ne=etree.SubElement(self.el, element_name) 106 | for name,value in attribs.iteritems(): 107 | ne.set(name,str(value)) 108 | 109 | ec = Exml(element_name, **attribs) 110 | ec.el = ne 111 | return ec 112 | 113 | 114 | class Xml(object): 115 | def __init__(self, data, base_url=None, parser=None): 116 | self.parser = parser if parser else etree.XMLParser() 117 | self.data = data 118 | self.base_url = base_url 119 | 120 | def __call__(self): 121 | doc = etree.fromstring( 122 | self.data, 123 | parser=self.parser, 124 | base_url=self.base_url) 125 | return doc 126 | 127 | class Html(Xml): 128 | def __init__(self, data, base_url=None, parser=None): 129 | super(Html,self).__init__(data, base_url, parser if parser else etree.HTMLParser()) 130 | 131 | XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml" 132 | DJANGO_NAMESPACE="http://djangoproject.com/template/xslt" 133 | 134 | from django.template import Variable 135 | from django.template import VariableDoesNotExist 136 | import types 137 | import threading 138 | djangothread = threading.local() 139 | 140 | class DjangoContextFunc(object): 141 | """Implements an extension function for django contexts""" 142 | def __init__(self, name, context=None): 143 | self.logger = logging.getLogger("xslt.DjangoContextFunc.%s" % name) 144 | self.logger.debug("creating %s" % name) 145 | self.name = name 146 | # Define some default mappers and extend with settings 147 | self.mappers = { 148 | "xml": self.xml, 149 | "parsehtml": self.parsehtml, 150 | "parse": self.parse 151 | } 152 | self._context = context 153 | try: 154 | self.mappers.update(settings.XSLT_MAPPER) 155 | except AttributeError: 156 | pass 157 | 158 | @property 159 | def context(self): 160 | if self._context is None: 161 | return djangothread.context 162 | return self._context 163 | 164 | def _django_eval(self): 165 | """ 166 | When the variable doesn't resolve, we return an empty string, 167 | this allows for tests such as: 168 | 169 | 170 | 171 | 172 | Where form.errors is a dict, and form.errors['fieldname'] 173 | may not exist, returning an empty string will allow evaluation 174 | to continue. 175 | """ 176 | try: 177 | e = Variable(self.name).resolve(self.context) 178 | return e 179 | except VariableDoesNotExist, e: 180 | # Nic says: I think there should be some debug setting 181 | # here to make this report the error 182 | errmsg = "problem evaling variable %s %s %s" % ( 183 | self.name, 184 | e.__class__.__name__, 185 | e 186 | ) 187 | self.logger.error(errmsg) 188 | return "" 189 | except Exception, e: 190 | errmsg = "problem evaling variable %s %s %s" % ( 191 | self.name, 192 | e.__class__.__name__, 193 | e 194 | ) 195 | self.logger.error(errmsg) 196 | if settings.DEBUG: 197 | return [E.error(errmsg)] 198 | return "" 199 | 200 | def parsehtml(self, ctx_value, *args): 201 | try: 202 | # First make it HTML 203 | htmldoc = etree.HTML(ctx_value) 204 | xmldoc = etree.XML(re.sub( 205 | "", 206 | """""" % XHTML_NAMESPACE, 207 | etree.tostring(htmldoc) 208 | )) 209 | self.logger.debug(etree.tostring(xmldoc)) 210 | doc= [xmldoc] 211 | return doc 212 | except etree.XMLSyntaxError, e: 213 | self.logger.debug(ctx_value) 214 | for i in e.error_log: 215 | self.logger.error("couldn't transform %s" % i) 216 | self.logger.debug(traceback.format_exc()) 217 | if settings.DEBUG: 218 | errors = [E.li(str(error)) for error in e.error_log] 219 | return [E.ol(*errors)] 220 | return "" 221 | 222 | def parse(self, ctx_value, *args): 223 | try: 224 | ## Not sure if it's better to return EMPTYDOC from here if nothing is passed in 225 | if ctx_value: 226 | xmldoc = etree.fromstring(unicode(ctx_value)) 227 | doc= [xmldoc] 228 | return doc 229 | else: 230 | return EMPTYDOC 231 | except etree.XMLSyntaxError, e: 232 | self.logger.debug(ctx_value) 233 | for i in e.error_log: 234 | self.logger.error("couldn't transform %s via %s %s" % (i, ctx_value, args)) 235 | self.logger.debug(traceback.format_exc()) 236 | if settings.DEBUG: 237 | errors = [E.li(str(error)) for error in e.error_log] 238 | return [E.ol(*errors)] 239 | return "" 240 | 241 | def xml(self, ctx_value, *args): 242 | e = etree.Element("div" if len(args) < 2 else args[1]) 243 | e.text = ctx_value 244 | return [e] 245 | 246 | def __call__(self, ctx, *args): 247 | """Treat a django context variable as an XSLT callable. 248 | 249 | If the context object supports the __xml__ protocol then the 250 | method is called (with any arguments) and the return value is 251 | expected to be some XML value like an lxml DOM or an XPath 252 | value, like a string. 253 | 254 | If the call specifies a renderer name as arg #1 then the 255 | renderer name is looked up in settings.XSLT_MAPPER and the 256 | resulting callable name evaled and then passed the context 257 | variable and the rest of the args. 258 | 259 | If the context object does not support __xml__ and the call 260 | does not specify a renderer then it is just str(evaled). 261 | 262 | The arguments to this function are the normal xpath extension 263 | function arguments: 'ctx' is an opaque xpath context value and 264 | 'args' is (mostly) the list of arguments supplied to the xpath 265 | function. 266 | """ 267 | self.logger.debug("called %s" % args[0] if len(args) > 0 else "") 268 | try: 269 | ctx_value = self._django_eval() 270 | self.logger.debug("context value %s" % ctx_value) 271 | 272 | # If the context object supports the render protocol use it 273 | try: 274 | # We just EXPECT the value to be XML 275 | if len(args): 276 | return ctx_value.__xml__(*args) 277 | return ctx_value.__xml__() 278 | except AttributeError: 279 | pass 280 | 281 | # If there are no args there is no defined renderer 282 | if not len(args): 283 | if isinstance(ctx_value, basestring): 284 | return ctx_value 285 | return str(ctx_value) 286 | 287 | # If the argument are xpath evaled they are general complex 288 | # lxml returns them as lists... python is expecting something simpler 289 | # not sure the best way to do this... maybe a higher level API 290 | # passing a context to the function. 291 | a = [str(x[0]) if isinstance(x,type([])) else x \ 292 | for x in args] 293 | fn = a.pop(0) 294 | 295 | # This is a useful place to put trace breaks 296 | # you can test fn for your renderer name 297 | renderer_mapped = self.mappers[fn] 298 | if isinstance(renderer_mapped, basestring): 299 | try: 300 | renderer = renderer_mapped.split(".") 301 | self.logger.debug("found renderer %s" % ".".join(renderer)) 302 | r = dotted_import(renderer_mapped) 303 | value = r(ctx_value, *a) 304 | except Exception, e: 305 | errmsg = "renderer %s had error %s" % ( 306 | ".".join(renderer), 307 | e) 308 | self.logger.error(errmsg, exc_info=True) 309 | return [E.error(errmsg)] 310 | else: 311 | return value 312 | elif isinstance(renderer_mapped, types.FunctionType): 313 | value = renderer_mapped(ctx_value, *a) 314 | self.logger.debug("%s returning %s", fn, value) 315 | return value 316 | elif isinstance(renderer_mapped, types.MethodType): 317 | value = renderer_mapped(ctx_value, *a) 318 | self.logger.debug("%s returning %s", fn, value) 319 | return value 320 | except Exception, e: 321 | errmsg = "resolving context call %s had error %s %s" % ( 322 | self.name, 323 | e.__class__.__name__, 324 | e) 325 | self.logger.error(errmsg, exc_info=True) 326 | return [E.error(errmsg + traceback.format_exc())] 327 | 328 | 329 | class DjangoResolver(etree.Resolver): 330 | """A base django resolver. 331 | 332 | This is necessary to discover functions used in every XSLT document. 333 | 334 | The resolver connects xpath expressions of the kind: 335 | 336 | xdjango:contextvarname(args) 337 | 338 | to context variable mapping implementations; eg: 339 | 340 | xdjango:contextvar('xml') 341 | 342 | will cause the Django context variable 'contextvar' to be rendered 343 | with the XML context variable renderer, while: 344 | 345 | xdjango:contextvar('reported') 346 | 347 | will cause the Django context variable 'contextvar' to be rendered 348 | with the renderer 'reported', presumably a custom renderer for that 349 | data type. 350 | """ 351 | 352 | def __init__(self): 353 | self.parser = etree.XMLParser() 354 | 355 | def _resolve(self, content, context, base_url=None): 356 | xml = etree.parse(StringIO(content), self.parser) 357 | 358 | # We should check this is an xslt document 359 | results = xml.xpath( 360 | "//@*", 361 | namespaces={ 362 | # The tag used here is relied upon in the following regexing 363 | "xdjango": DJANGO_NAMESPACE, 364 | }) 365 | # Not a perfect regex here, {} should wrap, or not. 366 | djangocalls = [r for r in results if FUNC_MATCH_RE1.search(r)] 367 | 368 | fns = etree.FunctionNamespace(DJANGO_NAMESPACE) 369 | for call in djangocalls: 370 | offset = 0 371 | while True: 372 | m = FUNC_MATCH_RE2.search(call, offset) 373 | if m is None: 374 | break 375 | name = m.group(1) 376 | offset = m.end() 377 | if name not in fns: 378 | fns[name] = DjangoContextFunc(name) 379 | # We want to call the actual super here 380 | return super(DjangoResolver, self).resolve_string( 381 | content, 382 | context, 383 | base_url=base_url) 384 | 385 | def resolve(self, url, pubid, context): 386 | """Implement resolver for django transformers. 387 | 388 | At the moment this seems to only be able to do file based resolving. 389 | That's because I'm not sure how to differentiate on the url. 390 | """ 391 | if url.partition(':')[0] not in ['http', 'django', 'querydirect']: 392 | # FIXME 393 | # We need a decent error here to say we couldn't find it. 394 | with open(url) as fd: 395 | content = fd.read() 396 | return self._resolve(content, context, base_url=url) 397 | 398 | def resolve_file(self, f, context, base_url=None): 399 | content = f.read() 400 | return self._resolve(content, context, base_url=base_url) 401 | 402 | def resolve_filename(self, filename, context): 403 | with open(filename) as f: 404 | content = f.read() 405 | return self._resolve(content, context, base_url=filename) 406 | 407 | def resolve_string(self, content, context, base_url=None): 408 | return self._resolve(content, context, base_url=base_url) 409 | 410 | 411 | # Hook management 412 | 413 | _transformer_init_hook_list = [] 414 | _transformer_percall_hook_list = [] 415 | 416 | def _transformer_init_hook(transformer_object): 417 | """Purely backward stuff 418 | 419 | This links our two xslt systems. 420 | """ 421 | global _transformer_init_hook_list 422 | for hook_func in _transformer_init_hook_list: 423 | hook_func(transformer_object) 424 | 425 | def _transformer_percall_hook(transformer_object, doc, context, **params): 426 | for hook_func in _transformer_percall_hook_list: 427 | hook_func(transformer_object, doc, context, **params) 428 | 429 | def add_init_hook(hookfunc): 430 | """Add the specified function to the list of functions called when we init a transformer.""" 431 | global _transformer_init_hook_list 432 | if hookfunc not in _transformer_init_hook_list: 433 | _transformer_init_hook_list += [hookfunc] 434 | 435 | def add_percall_hook(hookfunc): 436 | """Add the specified function to the list of functions called when we call a transformer. 437 | 438 | The hook must be a function like this: 439 | 440 | function(transformer_object, doc, context, **params) 441 | 442 | the parameters should be obvious. 443 | """ 444 | global _transformer_percall_hook_list 445 | if hookfunc not in _transformer_init_hook_list: 446 | _transformer_percall_hook_list += [hookfunc] 447 | 448 | 449 | # Transformers 450 | 451 | class Transformer(object): 452 | def __init__(self, 453 | content, 454 | resolv=lambda c,p: etree.fromstring(c,p), 455 | parser=None, 456 | context=None): 457 | """Make a transformer object. 458 | 459 | The transformer wraps all the django specific functionality. 460 | 461 | Params: 462 | content is the XML content, this could be a string or a url 463 | the exact semantics of content are defined by resolver 464 | 465 | resolv is a function that is called to return the XSLT 466 | document, it is called like this: 467 | 468 | xsltdoc = resolv(content, parser) 469 | 470 | where content and parser are both the args from __init__. 471 | The default value of resolv is: 472 | 473 | lambda c,p: etree.fromstring(c,p) 474 | 475 | parser is the XMLParser to use. a default is supplied. 476 | """ 477 | context = context if context else {} 478 | 479 | global DJANGO_NAMESPACE 480 | self.logger = logging.getLogger("xslt.Transformer") 481 | fns = etree.FunctionNamespace(DJANGO_NAMESPACE) 482 | 483 | # Setup the rest of the environment 484 | self.parser = parser if parser else etree.XMLParser() 485 | 486 | # We call out here to anything that's defined 487 | _transformer_init_hook(self) 488 | 489 | # Setup the djangoxslt resolver 490 | self.resolver = DjangoResolver() 491 | self.parser.resolvers.add(self.resolver) 492 | 493 | # lxml doesn't seem to use the parser's resolver for this 494 | # Hence we need to do the great big hack below 495 | self.xslt_doc = resolv(content, self.parser) 496 | 497 | ## Great big hack 498 | # We should check this is an xslt document 499 | xml = self.xslt_doc 500 | results = xml.xpath( 501 | "//@*", 502 | namespaces={ 503 | # The tag used here is relied upon in the following regexing 504 | "xdjango": DJANGO_NAMESPACE, 505 | }) 506 | # Not a perfect regex here, {} should wrap, or not. 507 | djangocalls = [r for r in results if FUNC_MATCH_RE1.search(r)] 508 | for call in djangocalls: 509 | offset = 0 510 | while True: 511 | m = FUNC_MATCH_RE2.search(call, offset) 512 | if m is None: 513 | break 514 | name = m.group(1) 515 | offset = m.end() 516 | if name not in fns: 517 | fns[name] = DjangoContextFunc(name) 518 | # End Great big hack 519 | 520 | qs_extension = QuerySetTemplateElement() 521 | extensions = {(DJANGO_NAMESPACE, 'queryset'): qs_extension} 522 | self.xslt = etree.XSLT(self.xslt_doc, extensions=extensions) 523 | 524 | def __xslt_error__(self, errorlist): 525 | """Format an errorlist. 526 | 527 | Override this if you want your errors looking different. 528 | """ 529 | errors = [E.li(str(error)) for error in errorlist] 530 | errordoc = E.html( 531 | E.h1("an error occurred"), 532 | E.ol(*errors) 533 | ) 534 | return errordoc 535 | 536 | def __call__(self, 537 | doc=None, 538 | context=None, 539 | **params): 540 | from django.template import Context 541 | global djangothread 542 | djangothread.context = context if context != None else Context() 543 | doc = doc if doc is not None else EMPTYDOC 544 | 545 | # Call out to the percall hooks 546 | _transformer_percall_hook(self, doc, context, **params) 547 | 548 | # accept a string for document as well 549 | if isinstance(doc, basestring): 550 | doc = etree.fromstring(doc) 551 | 552 | try: 553 | return str(self.xslt(doc, **params)) 554 | except etree.XSLTApplyError, e: 555 | self.logger.error("couldn't transform %s" % e.error_log) 556 | self.logger.error("couldn't transform %s" % e) 557 | self.logger.debug(self.__xslt_error__(e.error_log)) 558 | 559 | if settings.DEBUG: 560 | return etree.tostring(self.__xslt_error__(e.error_log)) 561 | else: 562 | raise 563 | except etree.XMLSyntaxError, e: 564 | for i in e.error_log: 565 | self.logger.error("couldn't transform %s" % i) 566 | self.logger.debug(traceback.format_exc()) 567 | if settings.DEBUG: 568 | return etree.tostring(self.__xslt_error__(e.error_log)) 569 | else: 570 | raise 571 | 572 | 573 | from os.path import join as joinpath 574 | 575 | def transformer_file_resolv_callback(c, p): 576 | """A resolver function for Transformer. 577 | 578 | c is the content which should be a filename 579 | p is the parser which we'll use. 580 | """ 581 | logger = logging.getLogger("djangoxslt.xslt.transformer_file_resolv_callback") 582 | try: 583 | stylesheet = joinpath(*c) 584 | return etree.parse(stylesheet, p) 585 | except etree.XMLSyntaxError: 586 | logger.error("stylesheet %s" % stylesheet) 587 | raise 588 | 589 | class TransformerFile(Transformer): 590 | """Make a transformer from an XSLT file. 591 | 592 | You can pass through a list of filename parts that will be joined 593 | to construct a filename.""" 594 | 595 | def __init__(self, *filename_parts, **kwargs): 596 | try: 597 | stylesheet = joinpath(filename_parts) 598 | self.stylesheet = stylesheet 599 | super(TransformerFile, self).__init__( 600 | stylesheet, 601 | resolv=transformer_file_resolv_callback, 602 | **kwargs) 603 | except Exception, e: 604 | e.stylesheet = stylesheet 605 | e.message = "%s {%s}" % (e.message, e.stylesheet) 606 | raise 607 | 608 | from django.http import HttpResponse 609 | def render_to_response(xslt, context, mimetype="text/html"): 610 | t = TransformerFile(settings.TRANSFORMS, xslt) 611 | return HttpResponse(t(context=context), mimetype="text/html") 612 | 613 | class QuerySetTemplateElement(etree.XSLTExtension): 614 | def execute(self, context, self_node, input_node, output_parent): 615 | ctx = djangothread.context 616 | key = self_node.get('key') 617 | dest = self_node.get('dest') 618 | if '.' in key: 619 | qs = DjangoContextFunc(key, context=ctx)(None, 'pass') 620 | else: 621 | qs = ctx[key] 622 | for item in qs: 623 | ctx[dest] = item 624 | el = etree.Element('{%s}%s' % (DJANGO_NAMESPACE, dest)) 625 | #self.apply_templates(context, el, output_parent) 626 | results = self.apply_templates(context, el) 627 | content = results[0] 628 | if isinstance(content, basestring): 629 | output_parent.text = content 630 | else: 631 | output_parent.append(content) 632 | #try: 633 | #self.process_children(context, output_parent) 634 | #except AttributeError, e: 635 | #raise RuntimeError('template not specified') 636 | del ctx[dest] 637 | 638 | # End 639 | --------------------------------------------------------------------------------