Commits

Chris Vigelius committed 34ec40c

initial working version

Comments (0)

Files changed (10)

+syntax:regexp
+.*\.pyc
+.*\.pyo
+.*\.egg-info/*
+compiled/.*\.js
+=================
+django-handlebars
+=================
+
+django-handlebars is a helper to use Handlebars_ client-side templates with Django.
+
+Installation
+------------
+
+::
+
+	pip install -e hg+https://bitbucket.org/chrisv/django-handlebars/
+
+Usage
+-----
+
+1. add ``django_handlebars`` to ``INSTALLED_APPS``
+
+2. put your handlebars templates in a folder named ``hbtemplates`` inside your app directory.
+   Example:
+
+:: 
+
+    myproject/
+       myapp/
+           hbtemplates/
+               myapp/
+			       forms/
+                       form1.html
+                       form2.html
+                   texts/
+                       text1.html
+                       text2.html
+               ...
+
+3. add ``{% load handlebars %}`` at the top of the template, and include the loader tag ``{% handlebars myapp %}`` into ``<head>``.
+   Note: you also need to include ``handlebars.js``, of course 
+
+4. use client-side templating like this: ::
+
+   $('#result').html(Handlebars.templates['myapp.texts.text1.html']({context: 'context'});
+   
+How it works
+------------
+django-handlebars takes everything under ``appdirectory/hbtemplates`` puts it into ``<script type="text/x-handlebars-template">``
+tags and adds these to the page. Also, a little glue script is added to compile the templates on page load and make them available
+under ``Handlebars.templates[...]``.
+
+Precompiling templates
+----------------------
+Since compiling templates on the fly inflates the DOM, works not well with browser caching and leads to increased load time, 
+django-handlebars can also use Handlebars' precompilation feature.
+
+To use it, install node.js and the Handlebars precompiler as described in Precompilation_. Then add the path to the ``handlebars``
+command to ``settings.py``: ::
+
+    HANDLEBARS_COMPILER="/path/to/handlebars"
+    
+Now, when django-handlebars encounters a ``{% handlebars %}`` tag, it will compile the templates to a single javascript file
+on the fly, and link it in the ``<head>``.
+
+Additional compilation flags like ``-k``, ``-o`` or ``-m`` can be used by adding them to ``settings.HANDLEBARS_COMPILATION_FLAGS``
+(if present, this must be in form of an array, e.g. ``settings.HANDLEBARS_COMPILATION_FLAGS=['-m', '-k each', '-k if']``).
+
+django-handlebars stores precompiled template in its own ``static`` directory, so it will work out of the box when the
+built-in development server is used.
+
+Automatic compilation at startup
+--------------------------------
+Instead of compiling the templates every time a page is loaded (which is slow and unnecessary), set ``settings.HANDLEBARS_AUTO_BUILD``
+to ``True`` to compile all the templates at server startup. Since the development server watches the project directory and restarts 
+itself automatically when a change is encountered, the compiled templates will be up to date always.
+
+Note: ``settings.HANDLEBARS_AUTO_BUILD`` is ``False`` by default only because errors in templates or in the compilation process would
+prevent the server from starting otherwise. 
+
+Precompiled templates in production
+-----------------------------------
+In production, precompiled templates can be served just like any other static file, and if you run the development server once with 
+``HANDLEBARS_AUTO_BUILD=True``, ``manage.py collectstatic`` will pick up the generated templates automatically. However, to avoid
+unneccessary runtime checks and compiler invocation, set ``settings.HANDLEBARS_PRODUCTION``. If set to ``True`` (default is ``False``) 
+it will override any other setting and ``{% handlebars %}`` will directly add the ``<script>`` headers for precompiled templates
+without further checking or overhead.
+
+License
+-------
+django-handlebars is licensed under BSD License, which basically means you can use it wherever and however you see fit.
+
+Reporting bugs
+--------------
+Use the bugtracker at https://bitbucket.org/chrisv/django-handlebars/
+
+.. _Handlebars: http://www.handlebarsjs.com/
+.. _Precompilation: http://handlebarsjs.com/precompilation.html

django_handlebars/builder.py

+'''
+Created on 10.02.2012
+
+@author: Chris P. Vigelius <me@cv.gd>
+'''
+import os, sys, tempfile, shutil, subprocess, errno
+try:
+    import cStringIO as StringIO
+except ImportError:
+    import StringIO
+    
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+from django.utils.simplejson import dumps
+
+# cache app template for later use
+# (taken from django.template.loaders.app_directories)
+
+fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
+app_hbtemplate_dirs = {}
+
+for app in settings.INSTALLED_APPS:
+    try:
+        mod = import_module(app)
+    except ImportError, e:
+        raise ImproperlyConfigured('ImportError %s: %s' % (app, e.args[0]))
+    template_dir = os.path.join(os.path.dirname(mod.__file__), 'hbtemplates')
+    if os.path.isdir(template_dir):
+        app_hbtemplate_dirs[app] = template_dir.decode(fs_encoding)
+
+class HandlebarsBuilderError(Exception):
+    """ raised when something is going wrong with the builder """
+    pass
+
+def _walk_templates_dir(app):
+    if not app in app_hbtemplate_dirs:
+        raise HandlebarsBuilderError("app '%s' does not exist (or has no 'hbtemplates' folder)" % app)
+    base = app_hbtemplate_dirs[app]
+    for folder, subfolders, files in os.walk(base):
+        packages = folder[len(base)+len(os.path.sep):].split(os.path.sep)
+        for f in files:
+            src = os.path.join(folder, f)
+            pkg = '.'.join(packages+[f,])
+            yield pkg, src
+  
+def _prepare(tmpdir, app):
+    """ copy all handlebars templates of app into tmpdir, for later compilation """
+    if not app in app_hbtemplate_dirs:
+        raise HandlebarsBuilderError("app '%s' does not exist (or has no 'hbtemplates' folder)" % app)
+    base = app_hbtemplate_dirs[app]
+    for folder, subfolders, files in os.walk(base):
+        pkg = folder[len(base)+len(os.path.sep):].split(os.path.sep)
+        for f in files: 
+            src = os.path.join(folder, f)
+            dst = '.'.join(pkg+[f,])
+            shutil.copy(src, os.path.join(tmpdir, dst))
+
+def _compile(tmpdir, out):
+    """ compile all the files in tmpdir to out """
+    args = [settings.HANDLEBARS_COMPILER,]
+    for f in os.listdir(tmpdir):
+        args.append(os.path.join(tmpdir, f))
+    if hasattr(settings, 'HANDLEBARS_COMPILER_FLAGS'):
+        args += settings.HANDLEBARS_COMPILER_FLAGS
+    args.append('-f')
+    args.append(out)
+    out,err = subprocess.Popen(args,stdout = subprocess.PIPE, stderr= subprocess.PIPE).communicate()
+    if err:
+        raise HandlebarsBuilderError("error building templates:\n\n%s" % err)
+
+def _render_tmpl_adapter(templates):
+    """ render an adapter which makes <script type="text/x-handlebars-template"> templates available
+        under Handlebars.templates """
+    out = '<script type="text/javascript">'
+    out += 'Handlebars.templates = Handlebars.templates || {};'
+    for tmpl in templates:
+        out+='Handlebars.templates["%s"]=Handlebars.compile(document.getElementById("%s").innerHTML);' % (tmpl, tmpl);
+    out += '</script>'
+    return out
+    
+def _render(app):
+    """ simply 'renders' all the templates to <script type=""> tags """
+    out = ''
+    tmpls = []
+    for pkg, src in _walk_templates_dir(app):
+        out+='<script id="%s" type="text/x-handlebars-template">' % pkg
+        out+=open(src).read()
+        out+='</script>\n'
+        tmpls.append(pkg)
+    out+=(_render_tmpl_adapter(tmpls))    
+    return out    
+    
+def _get_default_output_folder():
+    """ create/return the default output folder ("{appfolder}/static/django_handlebars/compiled") """
+    out = os.path.abspath(os.path.join(os.path.dirname(__file__), 'static', 'django_handlebars', 'compiled'))
+    try:
+        os.makedirs(out)
+    except OSError as exc:
+        if exc.errno == errno.EEXIST: pass # this is no problem
+        else: raise HandlebarsBuilderError("error creating template folder: '%s'" % out, exc)
+    return out        
+
+def _mkscripttag(app):
+    output_url = settings.STATIC_URL+'django_handlebars/compiled/'
+    return '<script type="text/javascript" src="%s"></script>' % (output_url + app + '.js')
+
+def _setting(key, default):
+    if hasattr(settings, key):
+        return getattr(settings,key)
+    else:
+        return default
+
+def get_tags(apps):
+    """ return <script> tags for the templates of apps """
+    tags = []
+    
+    # in production mode and auto build mode, just output the tags
+    if _setting('HANDLEBARS_PRODUCTION', False) or _setting('HANDLEBARS_AUTO_BUILD', False):
+        for app in apps:
+            tags.append(_mkscripttag(app))
+        return tags
+        
+    # if we don't have a compiler, just render template source    
+    if not _setting('HANDLEBARS_COMPILER', False):
+        for app in apps:
+            tags.append(_render(app))
+        return tags
+    
+    # ok, we have a compiler and need to compile
+    output_folder =  _get_default_output_folder() 
+        
+    for app in apps:
+        # prepare temp files    
+        tmp = tempfile.mkdtemp(prefix='hbbuild-')    
+        _prepare(tmp,app)
+        # build
+        _compile(tmp, os.path.join(output_folder, app+'.js'))    
+        # remove temp files    
+        shutil.rmtree(tmp, ignore_errors=True)
+        # add tags
+        tags.append(_mkscripttag(app))
+    
+    return tags
+
+if _setting('HANDLEBARS_AUTO_BUILD', False):
+    get_tags(app_hbtemplate_dirs.keys())

django_handlebars/hbtemplates/test/complex.html

+<ul>
+{{#words}}
+<li>{{.}}</li>
+{{/words}}
+</ul>
+

django_handlebars/hbtemplates/test/simple.html

+<h1>{{text}}</h1>

django_handlebars/models.py

+# there are no models, but Django requires that file to exist

django_handlebars/static/django_handlebars/compiled/.placeholder

Empty file added.

django_handlebars/templatetags/handlebars.py

+from django import template
+from django.utils.safestring import mark_safe
+
+from django_handlebars import builder
+
+register = template.Library()
+
+class HandlebarsNode(template.Node):
+    def __init__(self,apps):
+        self.apps = apps
+        self.tags = builder.get_tags(apps)
+        
+    def render(self, context):  
+        return ''.join(self.tags)
+    
+
+@register.tag('handlebars')
+def do_handlebars(parser, token):
+    args = token.split_contents()    
+    return HandlebarsNode(args[1:])

django_handlebars/tests.py

+'''
+Created on 10.02.2012
+
+@author: Chris P. Vigelius <me@cv.gd>
+'''
+import subprocess, os, shutil
+
+from django.utils import unittest
+from django.template import Template, Context
+from django.conf import settings
+#from django.test.utils import override_settings
+
+class Direct(unittest.TestCase):    
+    def test(self):
+        settings.HANDLEBARS_COMPILER = False
+        settings.HANDLEBARS_AUTO_BUILD = False
+        
+        t = Template("{% load handlebars %}{% handlebars django_handlebars %}")
+        html = t.render(Context({}))
+
+        self.assertTrue('<script id="test.simple.html"' in html)
+        self.assertTrue('Handlebars.templates["test.simple.html"]' in html)        
+
+class Compiler(unittest.TestCase):
+    
+    def test_compile(self):
+        # try to find handlebars executable
+        out,err = subprocess.Popen(['which handlebars'], shell=True,stdout = subprocess.PIPE, stderr= subprocess.PIPE).communicate()
+        self.assertTrue(len(out)>0, "command 'handlebars' not found. it is needed for this test.")        
+        # delete the compiled file
+        compiled = os.path.join(os.path.dirname(__file__), 'static/django_handlebars/compiled/django_handlebars.js')
+        try:
+            os.unlink(compiled)
+        except OSError:
+            pass
+        self.assertFalse(os.path.exists(compiled))
+        
+        # now build again
+        settings.HANDLEBARS_COMPILER = out.strip()
+        settings.HANDLEBARS_AUTO_BUILD = False        
+        t = Template("{% load handlebars %}{% handlebars django_handlebars %}")
+        html = t.render(Context({}))
+
+        # check if the compiled file exists
+        self.assertTrue('<script type="text/javascript" src="/static/django_handlebars/compiled/django_handlebars.js">' in html)
+        self.assertTrue(os.path.exists(compiled))
+                
+    def test_production(self):
+        """ test whether we can operate without compiler in production mode """
+        settings.HANDLEBARS_COMPILER = '/this/does/not/exist'
+        settings.HANDLEBARS_PRODUCTION = True
+        t = Template("{% load handlebars %}{% handlebars django_handlebars %}")
+        html = t.render(Context({}))
+        self.assertTrue('<script type="text/javascript" src="/static/django_handlebars/compiled/django_handlebars.js">' in html)
+        
+        
+    
+    
+          
                    'License :: OSI Approved :: BSD License',
                    'Operating System :: OS Independent',
                    'Programming Language :: Python',
-                   'Topic :: Utilities'],
+                   'Topic :: Internet :: WWW/HTTP :: Dynamic Content'],
       )