Commits

Zachary Voase  committed ca16153

Initial import.

  • Participants

Comments (0)

Files changed (46)

+syntax: glob
+
+.DS_Store
+*.pyc
+build
+dist
+MANIFEST
+*.egg-info
+Copyright (c) 2009 Zachary Voase
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+# DjanJinja v0.5
+
+DjanJinja: the sound you make when you've got peanut butter stuck to the roof of your mouth. Incidentally, it also happens to be the name of a new re-usable Django app. This one, in fact.
+
+DjanJinja exists to help you leverage the power of [Jinja2](http://jinja.pocoo.org/2/) templates in your Django projects. It's simple to get started.
+
+## Installing and Using DjanJinja
+
+### Installing
+
+1. Install DjanJinja using `easy_install djanjinja`, `pip install djanjinja`, or by grabbing a copy of the Mercurial repo and running `python setup.py install`.
+2. Add `'djanjinja'` to your `INSTALLED_APPS` list.
+3. (Optionally) add `'djanjinja.middleware.RequestContextMiddleware'` to your `MIDDLEWARE_CLASSES` list.
+
+### Using
+
+* Instead of using `django.shortcuts.render_to_response`, use one of the Jinja2-based functions provided.
+* Instead of using the Django loaders to load templates, get them from the Jinja2 environment created for you by DjanJinja.
+* Instead of using Django’s provided generic views, use those contained within `djanjinja.generic` (at the moment the only one is `direct_to_template()`).
+
+## Bundles
+
+A Jinja2 environment can contain additional filters, tests and global variables which will be available in all templates rendered through that environment. Since individual Django apps will have their own set of things to add to the environment, DjanJinja adds the concept of ‘bundles’; small objects containing some global variables, filters and tests. Each app may define any number of these bundles which can then be loaded as required.
+
+### Defining Bundles
+
+It’s relatively easy to define a bundle; an example is shown below:
+    
+    from djanjinja.loader import Bundle
+    
+    foo = Bundle()
+    foo.globals['myvar'] = 12345
+    
+    @foo.envfilter
+    def myenvfilter(environment, value):
+        pass # do something here...
+    
+    @foo.ctxfunction
+    def mycontextfunction(context, value):
+        pass # do something here...
+
+Here we define a bundle called `foo`, with a global variable of `myvar` containing the value `12345`, an environment filter and a context function (for more information on each of these please consult the Jinja2 documentation). The `Bundle` class also supplies these handy decorators (the full list can be found as `djanjinja.loader.Bundle.TYPES`) to define various components.
+
+DjanJinja expects to find bundles in a `bundles` submodule of your Django app. You can lay things out in one of two ways:
+    
+    * Add a file called `bundles.py` to your app, and within this define multiple `Bundle` instances.
+    * Add a package called `bundles` to your app (i.e. a `bundles` directory containing an empty file called `__init__.py`), and within this define submodules for each of your bundles. Each submodule should have a top-level `bundle` variable which is an instance of the `Bundle` class.
+
+You can actually mix and match these; you could add some bundle instances to the `bundles/__init__.py` file with different names, in addition to having the submodules. These are loaded lazily, so DjanJinja sees no real difference. It doesn’t scour the `bundles` module for definitions, it just loads what you ask it to.
+
+### Addressing Bundles
+
+In order to use the functions, filters and tests defined in a bundle, you first have to load it into the environment. Bundles are specified in two parts: the ‘app label’ and the ‘bundle name’. The app label is simply the name of the app which contains it. For example, it may be `django.contrib.auth`, or simply `auth`, since you may just give the last part of the full name and DjanJinja will figure it out from looking at the `INSTALLED_APPS` setting.
+
+If a bundle is defined within a `bundles.py` or a `bundles/__init__.py` file, then the bundle name will be the name in the module with which it was defined. For example:
+    
+    # in the file `myapp/bundles.py`
+    foo = Bundle()
+    foo.globals['myvar'] = 12345
+
+In this case, the app label will be `myapp`, and the bundle name will be `foo`. If the bundles are defined in submodules, then the bundle name will be the name of the submodule.
+
+### Loading Bundles
+
+In order to load any bundles into the Jinja2 environment, you need to specify a `DJANJINJA_BUNDLES` setting in your `settings.py` file. This is a list or tuple of bundle specifiers in an `'app_label.bundle_name'` format. For example:
+
+    DJANJINJA_BUNDLES = (
+        'djanjinja.cache',
+        'djanjinja.humanize',
+        'djanjinja.site',
+    )
+
+You can also add bundles to the environment programmatically. This is useful when:
+
+* Your app needs to do some initial setup before a bundle is loaded.
+* Your app relies on a particular bundle being present in the environment anyway, and you don’t want the user to have to add the bundle to `DJANJINJA_BUNDLES` manually.
+* Your app needs to load bundles dynamically.
+
+You can load a bundle into an environment like this:
+
+    import djanjinja
+    env = djanjinja.get_env()
+    env.load('app_label', 'bundle_name', reload=False)
+
+This will load the bundle into the environment, passing through if it’s already loaded. If you specify `reload=True`, you can make it reload a bundle even if it’s been loaded.
+
+You should put this code somewhere where it will get executed when you want it to. If you want it to be executed immediately, as Django starts up, put it in `myapp/__init__.py`.
+
+### Caveats and Limitations
+
+Jinja2 does not yet support scoped filters and tests; as a result of this, bundles will be loaded into the global environment. It is important to make sure that definitions in your bundle do not override those in another bundle. This is especially important with threaded web applications, as multiple bundles overriding one another could cause unpredictable behavior in the templates.
+
+### Included Bundles
+
+DjanJinja provides three bundles already which either replace Django counterparts or add some useful functionality to your Jinja2 templates:
+    
+    * `djanjinja.cache`: Loading this bundle will add a global `cache` object to the environment; this is the Django cache, and allows you to carry out caching operations from within your templates (such as `cache.get(key)`, et cetera).
+    * `djanjinja.humanize`: This will add all of the filters contained within the `django.contrib.humanize` app; consult the official Django docs for more information on the filters provided.
+    * `djanjinja.site`: This will add two functions to the global environment: `url`, and `setting`. The former acts like Django’s template tag, by reversing URLconf names and views into URLs, but because Jinja2 supports a richer syntax, it can be used via `{{ url(name, *args, **kwargs) }}` instead. `setting` attempts to resolve a setting name into a value, returning an optional default instead (i.e. `setting('MEDIA_URL', '/media')`).
+
+## Extensions
+
+Jinja2 supports the concept of *environment extensions*; these are non-trivial plugins which enhance the Jinja2 templating engine itself. By default, the environment is configured with the `do` statement and the loop controls (i.e. `break` and `continue`), but if you want to add extensions to the environment then you can do so with the `JINJA_EXTENSIONS` setting. Just add this to your `settings.py` file:
+    
+    JINJA_EXTENSIONS = (
+        'jinja2.ext.i18n', # i18n Extension
+        ...
+    )
+
+For all the extensions you wish to load. This will be passed in directly to the `jinja2.Environment` constructor.
+
+If you have set `USE_I18N = True` in your settings file, then DjanJinja will automatically initialize the i18n machinery for the Jinja2 environment, loading your Django translations during the bootstrapping process. For more information on how to use the Jinja2 i18n extension, please consult the Jinja2 documentation.
+
+### Cache
+
+DjanJinja also provides an extension for fragment caching using the Django cache system. The code for this borrows heavily from the example in the Jinja2 documentation, but with a few extras thrown in. You can use the extension like this:
+    
+    {% cache (parameter1, param2, param3), timeout %}
+        ...
+    {% endcache %}
+
+The tuple of parameters is used to generate the cache key for this fragment. You can place any object here, so long as it is suitable for serialization by the standard library `marshal` module. The cache key for the fragment is generated by marshaling the parameters, hashing them and then using the digest with a prefix as the key. This allows you to specify cached fragments which vary depending on multiple variables. The timeout is optional, and should be given in seconds.
+
+## Shortcut Functions
+
+DjanJinja provides you with two shortcut functions for rendering templates, `render_to_response` and `render_to_string`. These are very similar to those provided by Django in the `django.shortcuts` module, except they use Jinja2 instead of the Django templating system. To use them from your views, just do `from djanjinja.views import render_to_response, render_to_string` at the top of your views module.
+
+## `RequestContext`
+
+One of Django's most useful features is the `RequestContext` class, which allows you to specify several context processors which each add some information to the context before templates are rendered. Luckily, this feature is template-agnostic, and is therefore fully compatible with DjanJinja.
+
+However, DjanJinja also provides you with some very helpful shortcuts for using request contexts. Usually, without DjanJinja, you would use them like this:
+    
+    from django.shortcuts import render_to_response
+    from django.template import RequestContext
+    
+    def myview(request):
+        context = {'foo': bar, 'spam': eggs}
+        return render_to_response('template_name.html',
+            context, context_instance=RequestContext())
+
+To be honest, I don't think this looks very much like a 'shortcut' at all. For this reason, DjanJinja contains a subclass of `RequestContext` specialised for Jinja2, which is used like this:
+
+    from djanjinja.views import RequestContext
+    
+    def myview(request):
+        context = RequestContext(request, {'foo': bar, 'spam': eggs})
+        return context.render_response('template_name.html')
+
+This code is much more concise, but loses none of the flexibility of the previous example. The main changes made are the addition of `render_response` and `render_string` methods to the context object itself. This is highly specialised to rendering Jinja2 templates, so it may not be a very reusable approach (indeed, other code which does not use Jinja2 will need to use the full Django syntax), but it works for the problem domain it was designed for.
+
+## Middleware
+
+One important thing to note from before is that each time I construct a `RequestContext` instance, I pass it the request. In object-oriented programming, and Python expecially, when we have functions to which we must always pass an object of a certain type, it makes sense to make that function a *method* of the type. When that function is not a function but a constructor, this seems more difficult. However, thanks to a feature of Python known as metaprogramming, we can do this very easily. Because it's not exactly obvious how to do so, DjanJinja includes a special piece of middleware which can help make your code a lot shorter yet *still* retain all the functionality and flexibility of the previous two examples.
+
+To use this middleware, simply add `'djanjinja.middleware.RequestContextMiddleware'` to your `MIDDLEWARE_CLASSES` list in the settings module of your project. Then, you can write view code like this:
+
+    def myview(request):
+        return request.Context({'foo': bar, 'spam': eggs}).render_response(
+            'template_name.html')
+
+As you can see, we've greatly reduced the verbosity of the previous code, but it's still obvious what this code does. The middleware attaches a `Context` attribute to each request object. This attribute is in fact a fully-fledged Python class, which may itself be subclassed and modified later on. When constructed, it behaves almost exactly the same as the usual `RequestContext`, only it uses the request object to which it has been attached, so you don't have to pass it in to the constructor every time.
+
+## Template Loading
+
+DjanJinja hooks directly into the Django template loader machinery to load templates. This means you can mix Jinja2 templates freely with Django templates, in your `TEMPLATE_DIRS` and your applications, and render each type independently and seamlessly. If you want more information on how it actually works, please consult the `djanjinja/environment.py` file.
+
+## License
+
+This software is licensed under the following MIT-style license:
+    
+    Copyright (c) 2009 Zachary Voase
+
+    Permission is hereby granted, free of charge, to any person
+    obtaining a copy of this software and associated documentation
+    files (the "Software"), to deal in the Software without
+    restriction, including without limitation the rights to use,
+    copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following
+    conditions:
+
+    The above copyright notice and this permission notice shall be
+    included in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+    OTHER DEALINGS IN THE SOFTWARE.
+
+## Author
+
+Zachary Voase can be found on `Twitter <http://twitter.com/zacharyvoase>`_.

File djanjinja/__init__.py

+# -*- coding: utf-8 -*-
+
+"""djanjinja - A reusable Django app to use Jinja2 templates from Django."""
+
+from djanjinja import environment
+from djanjinja.environment import is_safe
+
+
+__all__ = [
+    'bccache',
+    'bundles',
+    'environment',
+    'extensions',
+    'generic',
+    'handlers',
+    'loader',
+    'middleware',
+    'views',
+]
+
+__version__ = '0.5'
+
+
+def get_environment():
+    """Return the template environment, bootstrapping if necessary."""
+    
+    if not environment.TEMPLATE_ENVIRONMENT:
+        environment.bootstrap()
+    return environment.TEMPLATE_ENVIRONMENT
+
+# Shorthand alias for `get_environment()`.
+get_env = get_environment
+
+
+def get_template(template_name):
+    """Return the specified template."""
+    
+    return get_env().get_template(template_name)

File djanjinja/bccache.py

+# -*- coding: utf-8 -*-
+
+"""A Jinja2 bytecode cache which uses the Django caching framework."""
+
+import jinja2
+
+
+class B64CacheClient(object):
+    
+    """
+    A wrapper for the Django cache client which Base64-encodes everything.
+    
+    This wrapper is needed to use the Django cache with Jinja2. Because Django
+    tries to store/retrieve everything as Unicode, it makes it impossible to
+    store binary data. Since Jinja2 uses marshal to store bytecode, we need to
+    Base64-encode the binary data and then we can send that into and get that
+    out of the Django cache.
+    """
+    
+    def __init__(self, cache):
+        self.cache = cache
+    
+    def get(self, key):
+        """Fetch a key from the cache, base64-decoding the result."""
+        data = self.cache.get(key)
+        if data is not None:
+            return data.decode('base64')
+    
+    def set(self, key, value, timeout=None):
+        """Set a value in the cache, performing base64 encoding beforehand."""
+        if timeout is not None:
+            self.cache.set(key, value.encode('base64'), timeout)
+        else:
+            self.cache.set(key, value.encode('base64'))
+
+
+def get_cache():
+    """Get a Jinja2 bytecode cache which uses the configured Django cache."""
+    
+    from django.conf import settings
+    from django.core import cache
+    
+    cache_backend = cache.parse_backend_uri(settings.CACHE_BACKEND)[0]
+    memcached_client = None
+    
+    if cache_backend == 'memcached':
+        # We can get the actual memcached client object itself. This will
+        # avoid the Django problem of storing binary data (as Django tries to
+        # coerce everything to Unicode).
+        
+        # Here, we look for either `cache.cache._cache` or
+        # `cache.cache._client`; I believe there is some discrepancy between
+        # different versions of Django and where they put this.
+        memcached_client = getattr(
+            cache.cache, '_cache', getattr(cache.cache, '_client', None))
+    
+    memcached_client = memcached_client or B64CacheClient(cache.cache)
+    
+    return jinja2.MemcachedBytecodeCache(memcached_client)

File djanjinja/bundles/__init__.py

+"""
+A number of useful bundles, including some Django ports.
+
+``djanjinja.bundles`` contains a number of bundles, using the standard
+interface which DjanJinja expects. Some of these bundles are simple ports of
+existing Django functionality, others are DjanJinja-specific.
+"""
+
+__all__ = ['cache', 'humanize', 'site']

File djanjinja/bundles/cache.py

+# -*- coding: utf-8 -*-
+
+"""Add the Django cache object to the global template variables."""
+
+from django.core import cache
+
+from djanjinja.loader import Bundle
+
+
+bundle = Bundle()
+bundle.globals['cache'] = cache.cache

File djanjinja/bundles/humanize.py

+# -*- coding: utf-8 -*-
+
+"""Port of django.contrib.humanize to Jinja2."""
+
+from django.contrib.humanize.templatetags import humanize
+from djanjinja.loader import Bundle
+
+
+bundle = Bundle()
+# All of the humanize filters are plain functions too, and Django's way of
+# storing filters is very similar; we just update our bundle's `filters`
+# attribute using humanize's register.
+bundle.filters.update(humanize.register.filters)

File djanjinja/bundles/site.py

+# -*- coding: utf-8 -*-
+
+"""Contains definitions for working with your Django site itself."""
+
+from djanjinja.loader import Bundle
+
+
+bundle = Bundle()
+
+
+@bundle.function
+def url(name, *args, **kwargs):
+    """A simple wrapper around ``django.core.urlresolvers.reverse``."""
+    
+    from django.core.urlresolvers import reverse    
+    return reverse(name, args=args, kwargs=kwargs)
+
+
+@bundle.function
+def setting(name, default=None):
+    """Get the value of a particular setting, defaulting to ``None``."""
+    
+    from django.conf import settings
+    return getattr(settings, name, default)

File djanjinja/environment.py

+# -*- coding: utf-8 -*-
+
+"""
+Django-compatible Jinja2 Environment and helpers.
+
+This module contains a subclass of ``jinja2.Environment`` and several other
+functions which help use Jinja2 from within your Django projects.
+"""
+
+from functools import wraps
+try:
+    set
+except NameError:
+    from sets import Set as set
+
+import jinja2
+
+
+TEMPLATE_ENVIRONMENT = None
+
+
+class Environment(jinja2.Environment):
+    
+    """An environment with decorators for filters, functions and tests."""
+    
+    def load(self, app_label, bundle_name, reload=False):
+        """Load the specified bundle into this environment."""
+        
+        from djanjinja import loader
+        
+        # Returns the loaded bundle.
+        return loader.load(
+            app_label, bundle_name, environment=self, reload=reload)
+    
+    # pylint: disable-msg=C0111
+    def adder(attribute, wrapper, name, docstring):
+        
+        """
+        Generate decorator methods for adding filters, functions and tests.
+        
+        Note that this function is not a method; it is deleted before the end
+        of the class definition and is only used to generate the decorator
+        methods. It helps to remove a lot of boilerplate.
+        """
+        
+        def adder_(self, function, name=None):
+            """Add the function to the environment with wrappers, etc."""
+            
+            key = name or function.__name__
+            value = wrapper and wrapper(function) or function
+            getattr(self, attribute)[key] = value
+            return function
+        
+        def decorator(self, *args, **kwargs):
+            """Boilerplate which allows both normal calling and decoration."""
+            
+            def wrapper(*args):
+                return adder_(self, *args, **kwargs)
+            if args:
+                return wrapper(*args)
+            return wrapper
+        
+        decorator.__name__ = name
+        decorator.__doc__ = docstring
+        return decorator
+    
+    ## Simple
+    
+    filter = adder('filters', None, 'filter',
+        'Decorate a function as a simple template filter.')
+    
+    test = adder('tests', None, 'test',
+        'Decorate a function as a simple template test.')
+    
+    function = adder('globals', None, 'function',
+        'Decorate a function as a simple global template function.')
+        
+    ## Environment
+    # Note that environment- and context-tests are not supported by Jinja2.
+    
+    envfilter = adder('filters', jinja2.environmentfilter, 'envfilter',
+        'Decorate a function as an environment filter.')
+    
+    envfunction = adder('globals', jinja2.environmentfunction, 'envfunction',
+        'Decorate a function as a global environment function.')
+    
+    ## Context
+    
+    ctxfilter = adder('filters', jinja2.contextfilter, 'ctxfilter',
+        'Decorate a function as a context filter.')
+    
+    ctxfunction = adder('globals', jinja2.contextfunction, 'ctxfunction',
+        'Decorate a function as a global context function.')
+    
+    # Clean up the namespace. Also, without this, `type` will try to convert
+    # `adder()` into a method. Which it most certainly is not.
+    del adder
+
+
+def get_template_source(name):
+    
+    """
+    Interface with Django to load the source for a given template name.
+    
+    This function is a simple wrapper around
+    ``django.template.loader.find_template_source()`` to support the behaviour
+    expected by the ``jinja2.FunctionLoader`` loader class. It requires Django
+    to be configured (i.e. the settings need to be loaded).
+    """
+    
+    from django.template import loader
+    # `loader.find_template_source()` returns a 2-tuple of the source and a
+    # `LoaderOrigin` object. 
+    source = loader.find_template_source(name)[0]
+    
+    # `jinja2.FunctionLoader` expects a triple of the source of the template,
+    # the name used to load it, and a 0-ary callable which will return whether
+    # or not the template needs to be reloaded. The callable will only ever be
+    # called if auto-reload is on. In this case, we'll just assume that the
+    # template does need to be reloaded.
+    return (source, name, lambda: False)
+
+
+def bootstrap():
+    """Load the TEMPLATE_ENVIRONMENT global variable."""
+    
+    from django.conf import settings
+    if not settings.configured:
+        # At least this will make it work, even if it's using the defaults.
+        settings.configure()
+    
+    from djanjinja import bccache
+    from djanjinja.extensions.cache import CacheExtension
+    from djanjinja.extensions.load import LoadExtension
+    
+    # Get the bytecode cache object.
+    bytecode_cache = bccache.get_cache()
+    
+    default_extensions = set([
+        'jinja2.ext.do', 'jinja2.ext.loopcontrols',
+        CacheExtension, LoadExtension])
+    if getattr(settings, 'USE_I18N', False):
+        default_extensions.add('jinja2.ext.i18n')
+    
+    extensions = getattr(settings, 'JINJA_EXTENSIONS', []) + list(
+        default_extensions)
+    
+    # Set up global `TEMPLATE_ENVIRONMENT` variable.
+    global TEMPLATE_ENVIRONMENT
+    
+    TEMPLATE_ENVIRONMENT = Environment(
+        loader=jinja2.FunctionLoader(get_template_source),
+        auto_reload=getattr(settings, 'DEBUG', True),
+        bytecode_cache=bytecode_cache, extensions=extensions)
+    
+    if getattr(settings, 'USE_I18N', False):
+        # The `django.utils.translation` module behaves like a singleton of
+        # `gettext.GNUTranslations`, since it exports all the necessary
+        # methods.
+        from django.utils import translation
+        # pylint: disable-msg=E1101
+        TEMPLATE_ENVIRONMENT.install_gettext_translations(translation)
+    
+    bundles = getattr(settings, 'DJANJINJA_BUNDLES', [])
+    for bundle_specifier in bundles:
+        app_label, bundle_name = bundle_specifier.rsplit('.', 1)
+        TEMPLATE_ENVIRONMENT.load(app_label, bundle_name)
+
+
+def is_safe(function):
+    """Decorator which declares that a function returns safe markup."""
+    
+    @wraps(function)
+    def safe_wrapper(*args, **kwargs):
+        """Wraps the output of the function as safe markup."""
+        
+        # All this wrapper does is to wrap the output of the function with
+        # `jinja2.Markup`, which declares to the template that the string is
+        # safe.
+        result = function(*args, **kwargs)
+        if not isinstance(result, jinja2.Markup):
+            return jinja2.Markup(result)
+        return result
+    
+    return safe_wrapper

File djanjinja/extensions/__init__.py

+# -*- coding: utf-8 -*-
+
+"""
+Django-specific Jinja2 extensions.
+
+This module contains extensions to Jinja2 which will aid integration with
+Django projects and apps.
+"""

File djanjinja/extensions/cache.py

+# -*- coding: utf-8 -*-
+
+"""
+A Jinja2 template tag for fragment caching.
+
+This extension, copied mainly from the Jinja2 documentation on extensions,
+adds a ``{% cache %}`` tag which permits fragment caching, much like that in
+the native Django templating language.
+
+Usage is as follows:
+    
+    {% cache "cache_key", 3600 %}
+        ...
+    {% endcache %}
+
+This will cache the fragment between ``cache`` and ``endcache``, using the
+cache key ``"cache_key"`` and with a timeout of 3600 seconds.
+
+More complex cache keys can be specified by passing in a sequence (such as a
+list or tuple) of items. The entire sequence must be 'dumpable' using the
+standard ``marshal`` module in Python. For example:
+    
+    {% cache ("article", article.id), 3600 %}
+        ...
+    {% endcache %}
+
+To generate the key, the tuple is marshalled, the SHA1 hash of the resulting
+string is taken and base64-encoded, with newlines and padding stripped, and
+this is appended to the string ``jinja_frag_``. For more information, consult
+the code (located in ``djanjinja/extensions/cache.py``).
+"""
+
+import hashlib
+import marshal
+
+from jinja2 import nodes
+from jinja2.ext import Extension
+
+
+class CacheExtension(Extension):
+    
+    """Fragment caching using the Django cache system."""
+    
+    tags = set(['cache'])
+    cache_key_format = 'jinja_frag_%(hash)s'
+    
+    def __init__(self, environment):
+        super(CacheExtension, self).__init__(environment)
+        
+        # Extend the environment with the default cache key prefix.
+        environment.extend(cache_key_format=self.cache_key_format)
+        
+    def parse(self, parser):
+        """Parse a fragment cache block in a Jinja2 template."""
+        
+        # The first parsed token will be 'cache', so we ignore that but keep
+        # the line number to give to nodes we create later.
+        lineno = parser.stream.next().lineno
+        
+        # This should be the cache key.
+        args = [parser.parse_expression()]
+        
+        # This will check to see if the user provided a timeout parameter
+        # (which would be separated by a comma).
+        if parser.stream.skip_if('comma'):
+            args.append(parser.parse_expression())
+        else:
+            args.append(nodes.Const(None))
+        # Here, we parse up to {% endcache %} and drop the needle, which will
+        # be the `endcache` tag itself.
+        body = parser.parse_statements(['name:endcache'], drop_needle=True)
+        
+        # Now return a `CallBlock` node which calls the `_cache` method on the
+        # extension.
+        return nodes.CallBlock(
+            self.call_method('_cache', args), [], [], body
+        ).set_lineno(lineno)
+    
+    def _cache(self, parameters, timeout, caller):
+        """Helper method for fragment caching."""
+        
+        # This is lazily loaded so that it can be set up without Django. If
+        # you try to use it without Django, it will just render the fragment
+        # as usual.
+        try:
+            from django.core import cache
+        except ImportError:
+            # `caller()` will render whatever is between {% cache %} and
+            # {% endcache %}.
+            return caller()
+        
+        key = self._generate_key(parameters)
+        
+        # If the fragment is cached, return it. Otherwise, render it, set the
+        # key in the cache, and return it.
+        retrieved_value = cache.cache.get(key)
+        if retrieved_value is not None:
+            return retrieved_value
+        
+        value = caller()
+        cache.cache.set(key, value, timeout)
+        return value
+        
+    def _generate_key(self, parameters):
+        """Generate a cache key from some parameters (maybe a sequence)."""
+        
+        # Marshal => Hash => Prefix should generate a unique key for each
+        # set of parameters which is the same for equal parameters.
+        # Essentially, this is a 1:1 mapping.
+        serialized = marshal.dumps(parameters)
+        digest = (hashlib.sha1(serialized)
+            .digest()
+            .encode('base64')
+            .rstrip('\r\n='))
+        
+        return self.environment.cache_key_format % {'hash': digest}

File djanjinja/generic.py

+# -*- coding: utf-8 -*-
+
+"""
+Generic views for Django which use Jinja2 instead.
+
+At the moment this module only contains ``direct_to_template``. Eventually it
+may house some more useful generic views, which have been made to use Jinja2
+instead of Django's built-in template language.
+"""
+
+import mimetypes
+
+from djanjinja.middleware import RequestContextMiddleware
+from djanjinja.views import DEFAULT_CONTENT_TYPE
+
+
+def direct_to_template(request, template=None, extra_context=None,
+    mimetype=None, *args, **kwargs):
+    
+    """
+    A generic view, similar to that of the same name provided by Django.
+    
+    Please consult the documentation for Django's
+    ``django.views.generic.simple.direct_to_template`` generic view. This
+    function exports an identical calling signature, only it uses the Jinja2
+    templating system instead.
+    """
+    
+    # Ensure the request has a `Context` attribute. This means the middleware
+    # does not have to be installed.
+    if not hasattr(request, 'Context'):
+        RequestContextMiddleware.process_request(request)
+    
+    # Build the context, optionally accepting additional context values.
+    context = request.Context(dict=(extra_context or {}))
+    
+    # Build the `params` variable from the parameters passed into the view
+    # from the URLconf.
+    params = kwargs.copy()
+    for i, value in enumerate(args):
+        params[i] = value
+    context['params'] = params
+    
+    # Ensure the mimetype is sensible; if not provided, it will be inferred
+    # from the name of the template. If that fails, fall back to the default.
+    if not mimetype:
+        mimetype = mimetypes.guess_type(template)[0] or DEFAULT_CONTENT_TYPE
+    
+    return context.render_response(template, mimetype=mimetype)

File djanjinja/handlers.py

+# -*- coding: utf-8 -*-
+
+"""Replacement for the default Django 404/500 exception handlers."""
+
+from djanjinja.views import RequestContext, render_to_response
+
+
+def page_not_found(request, template_name='404.html'):
+    
+    """
+    404 (page not found) handler which uses Jinja2 to render the template.
+    
+    The default template is ``404.html``, and its context will contain
+    ``request_path`` (the path of the requested URL) and any additional
+    parameters provided by the registered context processors (this view uses
+    ``RequestContext``).
+    """
+    
+    context = RequestContext(request, {'request_path': request.path})
+    response = context.render_response(template_name)
+    response.status_code = 404
+    return response
+
+
+def server_error(request, template_name='500.html'):
+    
+    """
+    500 (server error) handler which uses Jinja2 to render the template.
+    
+    The default template is ``500.html``, and it will be rendered with a
+    completely empty context. This is to prevent further exceptions from being
+    raised.
+    """
+    
+    return render_to_response(template_name)

File djanjinja/loader.py

+# -*- coding: utf-8 -*-
+
+"""Utilities for loading definitions from reusable Django apps."""
+
+import copy
+
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+
+from djanjinja import get_env
+from djanjinja.environment import Environment
+
+
+class Bundle(object):
+    
+    """
+    Store a bunch of tests, filters and functions from a single app.
+    
+    Instances of the ``Bundle`` class store tests, filters and functions in an
+    internal register that can then be lazily merged with the Jinja2
+    environment later on. It is used when apps want to define a number of
+    utilities for lazy loading from template code, typically using the
+    ``{% load %}`` template tag defined in djanjinja.extensions.bundles.
+    
+    Essentially, a bundle is just an environment without the templating and
+    loading parts. They can be pushed onto another environment, which will
+    return a new environment with a new set of filters, globals and tests but
+    all the other attributes from the original environment.
+    """
+    
+    TYPES = (
+        'test',
+        'filter', 'ctxfilter', 'envfilter',
+        'function', 'ctxfunction', 'envfunction'
+    )
+    
+    # Set these attributes now, to prevent pylint from flagging errors later.
+    test = None
+    filter = None
+    ctxfilter = None
+    envfilter = None
+    function = None
+    ctxfunction = None
+    envfunction = None
+    
+    def __init__(self):
+        self.filters = {}
+        self.globals = {}
+        self.tests = {}
+    
+    def merge_into(self, environment=None):
+        """Push this bundle onto the environment, returning a new env."""
+        
+        if environment is None:
+            environment = get_env()
+        
+        for attr in ['globals', 'filters', 'tests']:
+            getattr(environment, attr).update(getattr(self, attr))
+        
+        return environment
+    
+    
+    # Dynamically create decorators for each type.
+    for type in TYPES:
+        # We get the attribute from the `Environment` class itself because it
+        # will be an unbound method at this point.
+        # `im_func` is the underlying function definition. Within this
+        # definition, `self` has no special meaning, so by copying it here
+        # we can essentially repurpose the same method for this class. It's
+        # actually similar to a mixin.
+        vars()[type] = copy.copy(getattr(Environment, type).im_func)
+        
+        # Clean up ``type`` from the namespace.
+        del type
+
+
+def get_bundle(app_label, bundle_name):
+    
+    """
+    Loads the bundle with a given name for a specific app.
+    
+    First, we import the app. Then, we look in the ``bundles`` sub-module for
+    the specified bundle name. If the given name is a top-level attribute of
+    ``bundles``, we use that. Otherwise, we try to import a submodule of
+    ``bundles`` with that name and look for a ``bundle`` attribute in that
+    submodule.
+    
+    Note that this function only retrieves the bundle; it does not insert it
+    into the environment. To do this in one step, use the ``load()`` function
+    in this module.
+    """
+    
+    from django.conf import settings
+    
+    # Load the app (this is a plain Python module).
+    app, app_name = None, ''
+    for full_app_name in settings.INSTALLED_APPS:
+        if app_label in (full_app_name, full_app_name.split('.')[-1]):
+            app = import_module(full_app_name)
+            app_name = full_app_name
+            break
+    if not (app and app_name):
+        raise ImproperlyConfigured(
+            'App with label %r not found' % (app_label,))
+    
+    # Try to find the bundles sub-module. Having this separate allows us to
+    # provide a more detailed exception message.
+    try:
+        bundles = import_module('.bundles', package=app_name)
+    except ImportError:
+        raise ImproperlyConfigured(
+            'App %r has no `bundles` module' % (app_name,))
+    
+    # Now load the specified bundle name. First we look to see if it is a top-
+    # level attribute of the bundles module:
+    if hasattr(bundles, bundle_name):
+        bundle = getattr(bundles, bundle_name)
+        if isinstance(bundle, Bundle):
+            return bundle
+    
+    try:
+        bundle_mod = import_module(
+            '.' + bundle_name, package=(app_name + '.bundles'))
+    except ImportError:
+        raise ImproperlyConfigured(
+            'Could not find bundle %r in app %r' % (bundle_name, app_name))
+    
+    if hasattr(bundle_mod, 'bundle'):
+        return getattr(bundle_mod, 'bundle')
+    raise ImproperlyConfigured(
+        "Module '%s.bundles.%s' has no `bundle` attribute" % (
+            app_name, bundle_name))
+
+
+def load(app_label, bundle_name, environment=None, reload=False):
+    """Load a specified bundle into an/the environment."""
+    
+    if environment is None:
+        environment = get_env()
+    
+    bundle = get_bundle(app_label, bundle_name)
+    if (bundle not in environment.loaded_bundles) or reload:
+        bundle.merge_into(environment)
+    return bundle

File djanjinja/middleware.py

+# -*- coding: utf-8 -*-
+
+"""
+djanjinja.middleware - Helpful middleware for using Jinja2 from Django.
+
+This module contains middleware which helps you use Jinja2 from within your
+views. At the moment it only contains ``RequestContextMiddleware``, but may
+expand in future.
+"""
+
+from djanjinja.views import RequestContext
+
+
+class RequestContextMiddleware(object):
+    
+    """Attach a special ``RequestContext`` class to each request object."""
+    
+    @staticmethod
+    def process_request(request):
+        
+        """
+        Attach a special ``RequestContext`` subclass to each request object.
+        
+        This is the only method in the ``RequestContextMiddleware`` Django
+        middleware class. It attaches a ``RequestContext`` subclass to each
+        request as the ``Context`` attribute. This subclass has the request
+        object pre-specified, so you only need to use ``request.Context()`` to
+        make instances of ``django.template.RequestContext``.
+        
+        Consult the documentation for ``djanjinja.views.RequestContext`` for
+        more information.
+        """
+        
+        request.Context = RequestContext.with_request(request)

File djanjinja/views.py

+# -*- coding: utf-8 -*-
+
+"""
+djanjinja.views - Utilities to help write Django views which use Jinja2.
+
+This module contains several functions and classes to make it easier to write
+views which use Jinja2. It replaces a couple of the most common Django
+template-rendering shortcuts, and features an extended ``RequestContext``.
+"""
+
+from django import template
+from django.conf import settings
+from django.http import HttpResponse
+
+from djanjinja import get_env
+
+DEFAULT_CONTENT_TYPE = getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html')
+
+
+class RequestContext(template.RequestContext):
+    
+    """A ``RequestContext`` with a pre-specified request attribute."""
+    
+    request = None
+    
+    def __init__(self, *args, **kwargs):
+        # If the class has a `request` attribute which is not `None`, use that
+        # to initialize the `RequestContext`.
+        if self.request is not None:
+            super(RequestContext, self).__init__(
+                self.request, *args, **kwargs)
+        else:
+            # Otherwise, just act as if the normal ``RequestContext``
+            # constructor was called.
+            super(RequestContext, self).__init__(*args, **kwargs)
+    
+    @classmethod
+    def with_request(cls, request):
+        """Return a `RequestContext` subclass for a specified request."""
+        # Subclasses `RequestContext` with a value for `request`, so that it
+        # does not need it as an explicit request argument for initialization.
+        return type(
+            cls.__name__, (cls,),
+            {'request': request, '__module__': cls.__module__})
+    
+    def render_string(self, filename):
+        """Render a given template name to a string, using this context."""
+        return render_to_string(filename, context=self)
+    
+    def render_response(self, filename, mimetype=DEFAULT_CONTENT_TYPE):
+        """Render a given template name to a response, using this context."""
+        return render_to_response(filename, context=self, mimetype=mimetype)
+
+
+def context_to_dict(context):
+    """Flattens a Django context into a single dictionary."""
+    if not isinstance(context, template.Context):
+        return context
+    
+    dict_out = {}
+    # This helps us handle the order of dictionaries in the context. By
+    # default, the most recent (and therefore most significant/important)
+    # sub-dictionaries are at the front of the list. This means that variables
+    # defined later on need to be processed last, hence the use of the
+    # `reversed()` built-in.
+    for sub_dict in reversed(context.dicts):
+        dict_out.update(sub_dict)
+    return dict_out
+
+
+def render_to_string(filename, context=None):
+    """Renders a given template name to a string."""
+    if context is None:
+        context = {}
+    
+    return get_env().get_template(filename).render(
+        context_to_dict(context))
+
+
+def render_to_response(filename, context=None, mimetype=DEFAULT_CONTENT_TYPE):
+    """Renders a given template name to a ``django.http.HttpResponse``."""
+    return HttpResponse(
+        render_to_string(filename, context=context), mimetype=mimetype)

File djanjinja_test/__init__.py

Empty file added.

File djanjinja_test/cache/__init__.py

Empty file added.

File djanjinja_test/cache/tests.py

+# -*- coding: utf-8 -*-
+
+"""Tests for views which render templates which use the caching extras."""
+
+from django.test import TestCase
+
+import djanjinja
+
+
+CACHE_GLOBAL_RESPONSE = u'value'
+
+
+class CacheTest(TestCase):
+    
+    def test_global(self):
+        response = self.client.get('/cache/global/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, CACHE_GLOBAL_RESPONSE)
+        
+    def test_fragment(self):
+        """Tests the fragment caching extension."""
+        
+        # A singleton which stores the number of times its `call()` method has
+        # been called.
+        # pylint: disable-msg=C0103
+        class call_state(object):
+            called = 0
+            
+            @classmethod
+            def call(cls):
+                cls.called += 1
+        
+        template = djanjinja.get_template('cache_fragment.txt')
+        # In the beginning, `called` will be 0.
+        self.assertEqual(call_state.called, 0)
+        
+        # After rendering once, `called` will be 1.
+        template.render({'call_state': call_state})
+        self.assertEqual(call_state.called, 1)
+        
+        # If fragment caching is working correctly, the output of the previous
+        # render should be stored and the `call()` method should not be called
+        # again.
+        template.render({'call_state': call_state})
+        self.assertEqual(call_state.called, 1)

File djanjinja_test/cache/urls.py

+# -*- coding: utf-8 -*-
+
+from django.conf.urls.defaults import patterns, url
+
+
+urlpatterns = patterns('djanjinja_test.cache.views',
+    url(r'^global/$', 'global_', name='cache-global'),
+)

File djanjinja_test/cache/views.py

+# -*- coding: utf-8 -*-
+
+from django.http import HttpResponse
+
+import djanjinja
+
+
+def global_(request):
+    """Renders a template which uses the global cache object."""
+    
+    template = djanjinja.get_template('cache_global.txt')
+    content = template.render().strip()
+    return HttpResponse(content=content)

File djanjinja_test/generic/__init__.py

Empty file added.

File djanjinja_test/generic/tests.py

+# -*- coding: utf-8 -*-
+
+"""Tests for views which render templates using the generic views."""
+
+from django.test import TestCase
+
+
+PLAIN_RESPONSE = 'Hello, World!'
+CONTEXT_RESPONSE = 'a = 1; b = 2'
+REQ_CONTEXT_RESPONSE = 'user.is_anonymous() => True'
+
+
+class GenericTest(TestCase):
+    
+    def test_plain(self):
+        response = self.client.get('/generic/plain/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, PLAIN_RESPONSE)
+    
+    def test_context(self):
+        response = self.client.get('/generic/context/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, CONTEXT_RESPONSE)
+    
+    def test_req_context(self):
+        response = self.client.get('/generic/req_context/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, REQ_CONTEXT_RESPONSE)

File djanjinja_test/generic/urls.py

+# -*- coding: utf-8 -*-
+
+from django.conf.urls.defaults import patterns, url
+
+
+urlpatterns = patterns('djanjinja.generic',
+    url(r'^plain/$', 'direct_to_template', {'template': 'plain.txt'},
+        name='generic-plain'),
+    url(r'^context/$', 'direct_to_template',
+        {'template': 'context.txt', 'extra_context': {'a': 1, 'b': 2}},
+        name='generic-context'),
+    url(r'^req_context/$', 'direct_to_template',
+        {'template': 'req_context.txt'}, name='generic-req_context')
+)

File djanjinja_test/manage.py

+#!/usr/bin/env python
+
+from django.core.management import execute_manager
+
+try:
+    # pylint: disable-msg=W0403
+    import settings # Assumed to be in the same directory.
+except ImportError:
+    import sys
+    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__)
+    sys.exit(1)
+
+if __name__ == "__main__":
+    execute_manager(settings)

File djanjinja_test/settings.py

+# Django settings for djanjinja_test project.
+
+import os
+
+PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+    ('Zachary Voase', 'zacharyvoase@me.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'sqlite3'
+DATABASE_NAME = 'dev.db'
+DATABASE_USER = ''
+DATABASE_PASSWORD = ''
+DATABASE_HOST = ''
+DATABASE_PORT = ''
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'Europe/Madrid'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-gb'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media')
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = '/media/'
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'vbz@n)b!xw+@qb5dqiblc&m%7u7u%r$b+qv7emz6ck^945q2!0'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.load_template_source',
+    'django.template.loaders.app_directories.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'djanjinja.middleware.RequestContextMiddleware',
+)
+
+ROOT_URLCONF = 'djanjinja_test.urls'
+
+TEMPLATE_DIRS = (
+    os.path.join(PROJECT_ROOT, 'templates'),
+)
+
+# Third-party apps
+INSTALLED_APPS = (
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'djanjinja',
+)
+
+# Local apps
+INSTALLED_APPS += (
+    'djanjinja_test.simple',
+    'djanjinja_test.shortcuts',
+    'djanjinja_test.generic',
+    'djanjinja_test.cache',
+)
+
+
+DJANJINJA_BUNDLES = (
+    'djanjinja.cache',
+    'djanjinja.humanize',
+    'djanjinja.site',
+)

File djanjinja_test/shortcuts/__init__.py

Empty file added.

File djanjinja_test/shortcuts/tests.py

+# -*- coding: utf-8 -*-
+
+"""Tests for views which render templates using DjanJinja shortcuts."""
+
+from django.test import TestCase
+
+
+PLAIN_RESPONSE = 'Hello, World!'
+CONTEXT_RESPONSE = 'a = 1; b = 2'
+REQ_CONTEXT_RESPONSE = 'user.is_anonymous() => True'
+MIDDLEWARE_RESPONSE = 'anonymous, a1, b2'
+
+
+class ShortcutsTest(TestCase):
+    
+    def test_plain(self):
+        response = self.client.get('/shortcuts/plain/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, PLAIN_RESPONSE)
+    
+    def test_context(self):
+        response = self.client.get('/shortcuts/context/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, CONTEXT_RESPONSE)
+    
+    def test_req_context(self):
+        response = self.client.get('/shortcuts/req_context/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, REQ_CONTEXT_RESPONSE)
+    
+    def test_middleware(self):
+        response = self.client.get('/shortcuts/middleware/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, MIDDLEWARE_RESPONSE)

File djanjinja_test/shortcuts/urls.py

+# -*- coding: utf-8 -*-
+
+from django.conf.urls.defaults import patterns, url
+
+
+urlpatterns = patterns('djanjinja_test.shortcuts.views',
+    url(r'^plain/$', 'plain', name='shortcuts-plain'),
+    url(r'^context/$', 'context', name='shortcuts-context'),
+    url(r'^req_context/$', 'req_context', name='shortcuts-req_context'),
+    url(r'^middleware/$', 'middleware', name='shortcuts-middleware'),
+)

File djanjinja_test/shortcuts/views.py

+# -*- coding: utf-8 -*-
+
+"""Views which use DjanJinja shortcut functions to render templates."""
+
+from django.template import RequestContext
+
+from djanjinja.views import context_to_dict, render_to_response
+
+
+def plain(request):
+    """Renders a template with no context directly to a response."""
+    
+    return render_to_response('plain.txt')
+
+
+def context(request):
+    """Renders a template with a context directly to a response."""
+    
+    return render_to_response('context.txt', {'a': 1, 'b': 2})
+
+
+def req_context(request):
+    """Renders a template with a ``RequestContext`` directly to a response."""
+    
+    return render_to_response('req_context.txt',
+        context_to_dict(RequestContext(request)))
+
+
+def middleware(request):
+    """Renders a template with ``request.Context`` using middleware."""
+    
+    return request.Context({'a': 1, 'b': 2}).render_response('middleware.txt')

File djanjinja_test/simple/__init__.py

Empty file added.

File djanjinja_test/simple/tests.py

+# -*- coding: utf-8 -*-
+
+"""Tests for simple views which render templates."""
+
+from django.test import TestCase
+
+
+PLAIN_RESPONSE = 'Hello, World!'
+CONTEXT_RESPONSE = 'a = 1; b = 2'
+REQ_CONTEXT_RESPONSE = 'user.is_anonymous() => True'
+NOT_FOUND_RESPONSE = 'NOT FOUND: /this/does/not/exist/'
+SERVER_ERROR_RESPONSE = 'ERROR OCCURRED.'
+
+
+class SimpleTest(TestCase):
+    
+    def test_plain(self):
+        response = self.client.get('/simple/plain/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, PLAIN_RESPONSE)
+    
+    def test_context(self):
+        response = self.client.get('/simple/context/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, CONTEXT_RESPONSE)
+    
+    def test_req_context(self):
+        response = self.client.get('/simple/req_context/')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, REQ_CONTEXT_RESPONSE)
+    
+    def test_404(self):
+        response = self.client.get('/this/does/not/exist/')
+        self.assertEqual(response.status_code, 404)
+        self.assertEqual(response.content, NOT_FOUND_RESPONSE)

File djanjinja_test/simple/urls.py

+# -*- coding: utf-8 -*-
+
+from django.conf.urls.defaults import patterns, url
+
+
+urlpatterns = patterns('djanjinja_test.simple.views',
+    url(r'^plain/$', 'plain', name='simple-plain'),
+    url(r'^context/$', 'context', name='simple-context'),
+    url(r'^req_context/$', 'req_context', name='simple-req_context'),
+)

File djanjinja_test/simple/views.py

+# -*- coding: utf-8 -*-
+
+from django.http import HttpResponse
+from django.template import RequestContext
+
+import djanjinja
+from djanjinja.views import context_to_dict
+
+
+def plain(request):
+    """Renders a template with no context and returns it in a response."""
+    
+    template = djanjinja.get_template('plain.txt')
+    content = template.render()
+    return HttpResponse(content=content)
+
+
+def context(request):
+    """Renders a template with a context and returns it in a response."""
+    
+    template = djanjinja.get_template('context.txt')
+    content = template.render({'a': 1, 'b': 2})
+    return HttpResponse(content=content)
+
+
+def req_context(request):
+    """Renders a template with a ``RequestContext`` and returns a repsonse."""
+    
+    template = djanjinja.get_template('req_context.txt')
+    content = template.render(context_to_dict(RequestContext(request)))
+    return HttpResponse(content=content)

File djanjinja_test/templates/404.html

+NOT FOUND: {{ request_path }}

File djanjinja_test/templates/500.html

+ERROR OCCURRED.

File djanjinja_test/templates/cache_fragment.txt

+{% cache "fragment_caching", 3600 %}
+  {{ call_state.call() }}
+{% endcache %}

File djanjinja_test/templates/cache_global.txt

+{% do cache.set('key', 'value') %}
+{{ cache.get('key') }}

File djanjinja_test/templates/context.txt

+a = {{ a }}; b = {{ b }}

File djanjinja_test/templates/middleware.txt

+{% if user.is_anonymous() %}anonymous{% endif %}, a{{ a }}, b{{ b }}

File djanjinja_test/templates/plain.txt

+Hello, World!

File djanjinja_test/templates/req_context.txt

+user.is_anonymous() => {{ user.is_anonymous() }}

File djanjinja_test/urls.py

+# -*- coding: utf-8 -*-
+
+from django.conf.urls.defaults import include, patterns
+
+
+urlpatterns = patterns('',
+    (r'^simple/', include('djanjinja_test.simple.urls')),
+    (r'^shortcuts/', include('djanjinja_test.shortcuts.urls')),
+    (r'^generic/', include('djanjinja_test.generic.urls')),
+    (r'^cache/', include('djanjinja_test.cache.urls')),
+)
+
+
+handler404 = 'djanjinja.handlers.page_not_found'
+handler500 = 'djanjinja.handlers.server_error'
+# -*- coding: utf-8 -*-
+
+from paver.easy import *
+from paver.setuputils import find_packages, setup
+
+
+setup(
+    name='DjanJinja',
+    version='0.5',
+    packages=find_packages(exclude=('djanjinja_test', 'djanjinja_test.*')),
+    url='http://bitbucket.org/zacharyvoase/djanjinja/',
+    
+    author='Zachary Voase',
+    author_email='zacharyvoase@me.com',
+)
+
+
+@task
+def lint(options):
+    """Run PyLint on the ``djanjinja`` and ``djanjinja_test`` directories."""
+    
+    import os
+    
+    os.environ['DJANGO_SETTINGS_MODULE'] = 'djanjinja_test.settings'
+    rcfile = path(__file__).abspath().dirname() / 'pylintrc'
+    
+    run_pylint('djanjinja', rcfile=rcfile)
+    run_pylint('djanjinja_test', rcfile=rcfile, disable_msg='C0111')
+
+
+def run_pylint(directory, **options):
+    """Run PyLint on a given directory, with some command-line options."""
+    
+    from pylint import lint
+    
+    rcfile = options.pop('rcfile', None)
+    if not rcfile:
+        if (path(__file__).abspath().dirname() / 'pylintrc').exists():
+            rcfile = path(__file__).abspath().dirname() / 'pylintrc'
+    if rcfile:
+        options['rcfile'] = rcfile
+    
+    arguments = []
+    for option, value in options.items():
+        arguments.append('--%s=%s' % (option.replace('_', '-'), value))
+    arguments.append(directory)
+    
+    message = 'pylint ' + ' '.join(
+        argument.replace(' ', r'\ ') for argument in arguments)
+    
+    try:
+        dry(message, lint.Run, arguments)
+    except SystemExit, exc:
+        if exc.args[0] != 0:
+            raise BuildFailure('PyLint returned with a non-zero exit code.')
+# lint Python modules using external checkers.
+# 
+# This is the main checker controlling the other ones and the reports
+# generation. It is itself both a raw checker and an astng checker in order
+# to:
+# * handle message activation / deactivation at the module level
+# * handle some basic but necessary stats'data (number of classes, methods...)
+# 
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add <file or directory> to the black list. It should be a base name, not a
+# path. You may set this option multiple times.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Set the cache size for astng objects.
+cache-size=500
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable only checker(s) with the given id(s). This option conflicts with the
+# disable-checker option
+#enable-checker=
+
+# Enable all checker(s) except those with the given id(s). This option
+# conflicts with the enable-checker option
+#disable-checker=
+
+# Enable all messages in the listed categories (IRCWEF).
+#enable-msg-cat=
+
+# Disable all messages in the listed categories (IRCWEF).
+disable-msg-cat=I
+
+# Enable the message(s) with the given id(s).
+#enable-msg=
+
+# Disable the message(s) with the given id(s).
+disable-msg=E0213,R0401,R0903,R0904,W0142,W0603,W0613,W0622
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Include message's id in output
+include-ids=yes
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells wether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectivly contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (R0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (R0004).
+comment=no
+
+# Enable the report(s) with the given id(s).
+#enable-report=
+
+# Disable the report(s) with the given id(s).
+#disable-report=
+
+
+# checks for :
+# * doc strings
+# * modules / classes / functions / methods / arguments / variables name
+# * number of arguments, local variables, branchs, returns and statements in
+# functions, methods
+# * required module attributes
+# * dangerous default values as arguments
+# * redefinition of function / method / class
+# * uses of the global statement
+# 
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|([a-z_][a-z0-9_]{2,30}))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=apply,input
+
+
+# try to find bugs in the code using type inference
+# 
+[TYPECHECK]
+
+# Tells wether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed.
+generated-members=REQUEST,acl_users,aq_parent
+
+
+# checks for
+# * unused variables / imports
+# * undefined variables
+# * redefinition of variable from builtins or from an outer scope
+# * use of variable before assigment
+# 
+[VARIABLES]
+
+# Tells wether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching names used for dummy variables (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+# checks for :
+# * methods without self as first argument
+# * overridden methods signature
+# * access only to existant members via self
+# * attributes not defined in the __init__ method
+# * supported interfaces implementation
+# * unreachable code
+# 
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+
+# checks for sign of poor/misdesign:
+# * number of methods, attributes, local variables...
+# * size, complexity of functions, methods
+# 
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branchs=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+
+# checks for
+# * external modules dependencies
+# * relative / wildcard imports
+# * cyclic imports
+# * uses of deprecated modules
+# 
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report R0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report R0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report R0402 must
+# not be disabled)
+int-import-graph=
+
+
+# checks for :
+# * unauthorized constructions
+# * strict indentation
+# * line length
+# * use of <> instead of !=
+# 
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+
+# checks for:
+# * warning notes in the code like FIXME, XXX
+# * PEP 263: source code with non ascii character but no encoding declaration
+# 
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+# checks for similarities and duplicated code. This computation may be
+# memory / CPU intensive, so you should disable it if you experiments some
+# problems.
+# 
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes