Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Version 0.2.0.

  • Loading branch information...
commit 59eda0108b67e17ce4688d60c16d3f063a0dc47d 1 parent a227a10
@carljm carljm authored
View
8 CHANGES.rst
@@ -1,6 +1,14 @@
CHANGES
=======
+0.2.0 (2011.06.26)
+------------------
+
+* Made template-finding more flexible: ``ICANHAZ_DIR`` is now ``ICANHAZ_DIRS``
+ (a list); added ``ICANHAZ_FINDERS``, ``ICANHAZ_APP_DIRNAMES``, and finding of
+ templates in installed apps.
+
+
0.1.0 (2011.06.22)
------------------
View
36 README.rst
@@ -37,22 +37,44 @@ Usage
* Add ``"icanhaz"`` to your ``INSTALLED_APPS`` setting.
-* Set the ``ICANHAZ_DIR`` setting to the full (absolute) path to a directory
- where you will store your ICanHaz templates.
+* Set the ``ICANHAZ_DIRS`` setting to a list of full (absolute) path to
+ directories where you will store your ICanHaz templates.
* ``{% load icanhaz %}`` and use ``{% icanhaz "templatename" %}`` in your
Django templates to safely embed the ICanHaz.js template at
- ``ICANHAZ_DIR/templatename.html`` into your Django template, automatically
- wrapped in ``<script id="templatename" type="text/html">``, ready for
- ``ich.templatename({...})`` in your JavaScript.
+ ``<ICANHAZ_DIRS-entry>/templatename.html`` into your Django template,
+ automatically wrapped in ``<script id="templatename" type="text/html">``,
+ ready for ``ich.templatename({...})`` in your JavaScript.
``django-icanhaz`` does not bundle `ICanHaz.js`_ or provide any JavaScript
utilities; it just helps you easily embed the templates in your HTML. Include
`ICanHaz.js`_ in your project's static assets and use it in your JS as usual.
-Philosophy
-----------
+Advanced usage
+--------------
+
+You can also bundle ICanHaz templates with Django reusable apps; by default
+``django-icanhaz`` will look for templates in a ``jstemplates`` subdirectory of
+each app in ``INSTALLED_APPS``. The app subdirectory name(s) to check can be
+configured via the ``ICANHAZ_APP_DIRNAMES`` setting, which defaults to
+``["jstemplates"]``.
+
+The finding of templates can be fully controlled via the ``ICANHAZ_FINDERS``
+setting, which is a list of dotted paths to finder classes. A finder class
+should be instantiable with no arguments, and have a ``find(name)`` method
+which returns the full absolute path to a template file, given a base-name.
+
+By default, ``ICANHAZ_FINDERS`` contains ``"icanhaz.finders.FilesystemFinder"``
+(which searches directories listed in ``ICANHAZ_DIRS``) and
+``"icanhaz.finders.AppFinder"`` (which searches subdirectories named in
+``ICANHAZ_APP_DIRNAMES`` of each app in ``INSTALLED_APPS``), in that order --
+thus templates found in ``ICANHAZ_DIRS`` take precedence over templates in
+apps.
+
+
+Rationale
+---------
The collision between Django templates' use of ``{{`` and ``}}`` as template
variable markers and `ICanHaz.js`_' use of same has spawned a variety of
View
2  icanhaz/__init__.py
@@ -1 +1 @@
-__version__ = "0.1.0"
+__version__ = "0.2.0"
View
9 icanhaz/conf.py
@@ -17,4 +17,11 @@ def __getattr__(self, k):
raise ImproperlyConfigured("%s setting is required." % k)
-conf = Configuration()
+conf = Configuration(
+ ICANHAZ_FINDERS=[
+ "icanhaz.finders.FilesystemFinder",
+ "icanhaz.finders.AppFinder",
+ ],
+ ICANHAZ_DIRS=[],
+ ICANHAZ_APP_DIRNAMES=["jstemplates"],
+ )
View
60 icanhaz/finders.py
@@ -0,0 +1,60 @@
+import os, sys
+
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+
+from .conf import conf
+
+
+
+class BaseFinder(object):
+ def find(self, name):
+ raise NotImplementedError()
+
+
+
+class FilesystemFinder(BaseFinder):
+ @property
+ def directories(self):
+ return conf.ICANHAZ_DIRS
+
+
+ def find(self, name):
+ for directory in self.directories:
+ filepath = os.path.abspath(os.path.join(
+ directory,
+ name + ".html"))
+
+ if filepath.startswith(directory) and os.path.exists(filepath):
+ return filepath
+
+ return None
+
+
+
+def _get_app_template_dirs():
+ fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
+ ret = []
+ for app in conf.INSTALLED_APPS:
+ try:
+ mod = import_module(app)
+ except ImportError, e:
+ raise ImproperlyConfigured("ImportError %s: %s" % (app, e.args[0]))
+ app_dir = os.path.dirname(mod.__file__)
+ for dirname in conf.ICANHAZ_APP_DIRNAMES:
+ template_dir = os.path.join(app_dir, dirname)
+ if os.path.isdir(template_dir):
+ ret.append(template_dir.decode(fs_encoding))
+ return ret
+
+
+
+# At import time, cache the app directories to search.
+app_template_dirs = _get_app_template_dirs()
+
+
+
+class AppFinder(FilesystemFinder):
+ @property
+ def directories(self):
+ return app_template_dirs
View
46 icanhaz/loading.py
@@ -0,0 +1,46 @@
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+
+from .conf import conf
+
+
+
+def find(name):
+ for finder in finders:
+ filepath = finder.find(name)
+ if filepath is not None:
+ return filepath
+
+ raise ICanHazTemplateNotFound(name)
+
+
+
+def _get_finders():
+ ret = []
+ for finder_path in conf.ICANHAZ_FINDERS:
+ modpath, cls_name = finder_path.rsplit(".", 1)
+ try:
+ mod = import_module(modpath)
+ except ImportError, e:
+ raise ImproperlyConfigured(
+ "ImportError %s: %s" % (modpath, e.args[0]))
+
+ try:
+ cls = getattr(mod, cls_name)
+ except AttributeError, e:
+ raise ImproperlyConfigured(
+ "AttributeError %s: %s" % (cls_name, e.args[0]))
+
+ ret.append(cls())
+
+ return ret
+
+
+
+# Instantiate finders
+finders = _get_finders()
+
+
+
+class ICanHazTemplateNotFound(Exception):
+ pass
View
25 icanhaz/templatetags/icanhaz.py
@@ -1,9 +1,7 @@
-import os.path
-
from django import template
-from django.core.exceptions import SuspiciousOperation
from ..conf import conf
+from ..loading import find, ICanHazTemplateNotFound
@@ -16,30 +14,17 @@ def __init__(self, name):
self.name = template.Variable(name)
-class ICanHazNode(template.Node):
- def __init__(self, name):
- self.name = template.Variable(name)
-
-
def render(self, context):
name = self.name.resolve(context)
- filepath = os.path.abspath(os.path.join(
- conf.ICANHAZ_DIR,
- name + ".html"))
-
- if not filepath.startswith(conf.ICANHAZ_DIR):
- raise SuspiciousOperation(
- "icanhaz tag attempting to open file at %r, outside of %r"
- % (filepath, conf.ICANHAZ_DIR))
-
try:
+ filepath = find(name)
fp = open(filepath, "r")
output = fp.read()
fp.close()
output = ('<script id="%s" type="text/html">\n'
% name) + output + "\n</script>\n"
- except IOError:
+ except (IOError, ICanHazTemplateNotFound):
output = ""
if conf.DEBUG:
raise
@@ -51,8 +36,8 @@ def render(self, context):
@register.tag
def icanhaz(parser, token):
"""
- Outputs the contents of a given file, path relative to ICANHAZ_DIR
- setting, into the page.
+ Finds the ICanHaz template for the given name and renders it surrounded by
+ the requisite ICanHaz <script> tags.
"""
bits = token.contents.split()
View
4 icanhaz/tests/__init__.py
@@ -1 +1,3 @@
-from .tests import *
+from .test_finders import *
+from .test_loading import *
+from .test_ttag import *
View
7 icanhaz/tests/mockfinders.py
@@ -0,0 +1,7 @@
+class MockFinder(object):
+ def __init__(self, retval=None):
+ self.retval = retval
+
+
+ def find(self, name):
+ return self.retval
View
0  icanhaz/tests/outside_dir.html
No changes.
View
91 icanhaz/tests/test_finders.py
@@ -0,0 +1,91 @@
+import os
+
+from django.core.exceptions import ImproperlyConfigured
+from django.test import TestCase
+
+from mock import patch
+
+from .utils import override_settings
+
+
+
+__all__ = ["BaseFinderTest", "FilesystemFinderTest", "AppFinderTest"]
+
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+
+class BaseFinderTest(TestCase):
+ @property
+ def finder(self):
+ from icanhaz.finders import BaseFinder
+ return BaseFinder()
+
+
+ def test_find_not_implemented(self):
+ with self.assertRaises(NotImplementedError):
+ self.finder.find("name")
+
+
+
+class FilesystemFinderTest(TestCase):
+ @property
+ def finder(self):
+ from icanhaz.finders import FilesystemFinder
+ return FilesystemFinder()
+
+
+ @override_settings(ICANHAZ_DIRS=["/one/path", "/another/path"])
+ def test_directories(self):
+ self.assertEqual(
+ self.finder.directories,
+ ["/one/path", "/another/path"])
+
+
+ @override_settings(ICANHAZ_DIRS=[os.path.join(here, "templates")])
+ def test_find(self):
+ self.assertEqual(
+ self.finder.find("testtemplate"),
+ os.path.join(here, "templates", "testtemplate.html"))
+
+
+ @override_settings(ICANHAZ_DIRS=[os.path.join(here, "templates")])
+ def test_find_non_existing(self):
+ self.assertEqual(self.finder.find("doesntexist"), None)
+
+
+
+class AppFinderTest(TestCase):
+ @property
+ def finder(self):
+ from icanhaz.finders import AppFinder
+ return AppFinder()
+
+
+ def test_directories(self):
+ with patch(
+ "icanhaz.finders.app_template_dirs",
+ [os.path.join(here, "templates")]):
+ dirs = self.finder.directories
+
+ self.assertEqual(dirs, [os.path.join(here, "templates")])
+
+
+ @property
+ def func(self):
+ from icanhaz.finders import _get_app_template_dirs
+ return _get_app_template_dirs
+
+
+ @override_settings(
+ INSTALLED_APPS=["icanhaz.tests"],
+ ICANHAZ_APP_DIRNAMES=["templates", "jstemplates"])
+ def test_get_app_template_dirs(self):
+ self.assertEqual(self.func(), [os.path.join(here, "templates")])
+
+
+ @override_settings(INSTALLED_APPS=["icanhaz.nonexistent"])
+ def test_bad_app(self):
+ with self.assertRaises(ImproperlyConfigured):
+ self.func()
View
67 icanhaz/tests/test_loading.py
@@ -0,0 +1,67 @@
+from django.core.exceptions import ImproperlyConfigured
+from django.test import TestCase
+
+from mock import patch
+
+from .mockfinders import MockFinder
+from .utils import override_settings
+
+
+
+__all__ = ["FindTest", "GetFindersTest"]
+
+
+
+class FindTest(TestCase):
+ @property
+ def func(self):
+ from icanhaz.loading import find
+ return find
+
+
+ @patch("icanhaz.loading.finders", [MockFinder("/path/to/a/file.html")])
+ def test_find(self):
+ self.assertEqual(self.func("file"), "/path/to/a/file.html")
+
+
+ @patch(
+ "icanhaz.loading.finders",
+ [MockFinder(), MockFinder("/path/to/a/file.html")])
+ def test_find_fallback(self):
+ self.assertEqual(self.func("file"), "/path/to/a/file.html")
+
+
+ @patch("icanhaz.loading.finders", [MockFinder()])
+ def test_none_found(self):
+ from icanhaz.loading import ICanHazTemplateNotFound
+ with self.assertRaises(ICanHazTemplateNotFound):
+ self.func("file")
+
+
+
+class GetFindersTest(TestCase):
+ @property
+ def func(self):
+ from icanhaz.loading import _get_finders
+ return _get_finders
+
+
+ @override_settings(ICANHAZ_FINDERS=["icanhaz.tests.mockfinders.MockFinder"])
+ def test_get_finders(self):
+ finders = self.func()
+
+ self.assertEqual(len(finders), 1)
+ self.assertIsInstance(finders[0], MockFinder)
+
+
+ @override_settings(ICANHAZ_FINDERS=["icanhaz.tests.doesntexist.MockFinder"])
+ def test_bad_module(self):
+ with self.assertRaises(ImproperlyConfigured):
+ self.func()
+
+
+ @override_settings(
+ ICANHAZ_FINDERS=["icanhaz.tests.mockfinders.DoesntExist"])
+ def test_bad_attribute(self):
+ with self.assertRaises(ImproperlyConfigured):
+ self.func()
View
36 icanhaz/tests/tests.py → icanhaz/tests/test_ttag.py
@@ -1,6 +1,5 @@
import os.path
-from django.core.exceptions import SuspiciousOperation
from django.template import Template, Context, TemplateSyntaxError
from django.test import TestCase
@@ -8,14 +7,14 @@
-__all__ = ["ICanHazTest"]
+__all__ = ["TemplateTagTest"]
DIR = os.path.join(os.path.dirname(__file__), "templates")
-class ICanHazTest(TestCase):
- @override_settings(ICANHAZ_DIR=DIR)
+class TemplateTagTest(TestCase):
+ @override_settings(ICANHAZ_DIRS=[DIR])
def test_simple(self):
res = Template(
"{% load icanhaz %}{% icanhaz 'testtemplate' %}"
@@ -27,7 +26,7 @@ def test_simple(self):
'<p>A template full of {{ foo }}.</p>\n\n</script>\n')
- @override_settings(ICANHAZ_DIR=DIR)
+ @override_settings(ICANHAZ_DIRS=[DIR])
def test_variable_template_name(self):
res = Template(
"{% load icanhaz %}{% icanhaz templatename %}").render(
@@ -39,7 +38,7 @@ def test_variable_template_name(self):
'<p>A template full of {{ foo }}.</p>\n\n</script>\n')
- @override_settings(ICANHAZ_DIR=DIR, DEBUG=False)
+ @override_settings(ICANHAZ_DIRS=[DIR], DEBUG=False)
def test_no_template(self):
res = Template(
"{% load icanhaz %}{% icanhaz 'notemplate' %}"
@@ -48,29 +47,32 @@ def test_no_template(self):
self.assertEqual(res, "")
- @override_settings(ICANHAZ_DIR=DIR, DEBUG=True)
+ @override_settings(ICANHAZ_DIRS=[DIR], DEBUG=True)
def test_no_template_debug(self):
- with self.assertRaises(IOError):
+ from icanhaz.loading import ICanHazTemplateNotFound
+ with self.assertRaises(ICanHazTemplateNotFound):
Template(
"{% load icanhaz %}{% icanhaz 'notemplate' %}"
).render(Context())
- @override_settings(ICANHAZ_DIR=DIR)
- def test_suspicious(self):
- with self.assertRaises(SuspiciousOperation):
- Template(
- "{% load icanhaz %}{% icanhaz '../testtemplate' %}"
+ @override_settings(ICANHAZ_DIRS=[DIR])
+ def test_no_break_out(self):
+ res = Template(
+ "{% load icanhaz %}{% icanhaz '../outside_dir' %}"
).render(Context())
+ self.assertEqual(res, "")
- @override_settings(ICANHAZ_DIR=DIR)
- def test_suspicious_absolute(self):
- with self.assertRaises(SuspiciousOperation):
- Template(
+
+ @override_settings(ICANHAZ_DIRS=[DIR])
+ def test_no_absolute(self):
+ res = Template(
"{% load icanhaz %}{% icanhaz '/testtemplate' %}"
).render(Context())
+ self.assertEqual(res, "")
+
def test_bad_args(self):
with self.assertRaises(TemplateSyntaxError):
View
4 setup.py
@@ -34,13 +34,11 @@ def get_version():
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
- "Programming Language :: Python :: 2.4",
- "Programming Language :: Python :: 2.5",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Framework :: Django",
],
zip_safe=False,
- tests_require=["Django>=1.2"],
+ tests_require=["Django>=1.2", "mock"],
test_suite="runtests.runtests"
)
View
3  tox.ini
@@ -4,14 +4,17 @@ envlist=py26,py26-trunk,py27,py27-trunk
[testenv]
deps=
django==1.3
+ mock==0.7.2
commands=python runtests.py
[testenv:py26-trunk]
basepython=python2.6
deps=
svn+http://code.djangoproject.com/svn/django/trunk#egg=django
+ mock==0.7.2
[testenv:py27-trunk]
basepython=python2.7
deps=
svn+http://code.djangoproject.com/svn/django/trunk#egg=django
+ mock==0.7.2
Please sign in to comment.
Something went wrong with that request. Please try again.