Commit 0a1ed11d by Chris Rossi

Make mako template lookups pluggable.

This code adds the ability to add Mako template lookup directories on
the fly, allowing third party add-ons to contribute their own Mako templates.
A new API function for registering Mako templates is introduced::

    from edxmako import add_lookup

    add_lookup('main', '/path/to/templates')

    # Or, specify a package to lookup using pkg_resources.  This will
    # add the 'templates' directory inside the current package:
    add_lookup('main', 'templates', package=__name__)
parent b95d71ff
......@@ -11,5 +11,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
LOOKUP = {}
lookup = None
from .paths import add_lookup, lookup_template
"""
Set up lookup paths for mako templates.
"""
import os
import pkg_resources
from django.conf import settings
from mako.lookup import TemplateLookup
from . import LOOKUP
class DynamicTemplateLookup(TemplateLookup):
"""
A specialization of the standard mako `TemplateLookup` class which allows
for adding directories progressively.
"""
def add_directory(self, directory):
"""
Add a new directory to the template lookup path.
"""
self.directories.append(os.path.normpath(directory))
def add_lookup(namespace, directory, package=None):
"""
Adds a new mako template lookup directory to the given namespace.
If `package` is specified, `pkg_resources` is used to look up the directory
inside the given package. Otherwise `directory` is assumed to be a path
in the filesystem.
"""
templates = LOOKUP.get(namespace)
if not templates:
LOOKUP[namespace] = templates = DynamicTemplateLookup(
module_directory=settings.MAKO_MODULE_DIR,
output_encoding='utf-8',
input_encoding='utf-8',
default_filters=['decode.utf8'],
encoding_errors='replace',
)
if package:
directory = pkg_resources.resource_filename(package, directory)
templates.add_directory(directory)
def lookup_template(namespace, name):
"""
Look up a Mako template by namespace and name.
"""
return LOOKUP[namespace].get_template(name)
......@@ -18,7 +18,7 @@ import logging
from microsite_configuration.middleware import MicrositeConfiguration
import edxmako
from edxmako import lookup_template
import edxmako.middleware
from django.conf import settings
from django.core.urlresolvers import reverse
......@@ -100,7 +100,7 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
if context:
context_dictionary.update(context)
# fetch and render template
template = edxmako.lookup[namespace].get_template(template_name)
template = lookup_template(namespace, template_name)
return template.render_unicode(**context_dictionary)
......
"""
Initialize the mako template lookup
"""
import tempdir
from django.conf import settings
from mako.lookup import TemplateLookup
import edxmako
from . import add_lookup
def run():
"""Setup mako variables and lookup object"""
# Set all mako variables based on django settings
"""
Setup mako lookup directories.
"""
template_locations = settings.MAKO_TEMPLATES
module_directory = getattr(settings, 'MAKO_MODULE_DIR', None)
if module_directory is None:
module_directory = tempdir.mkdtemp_clean()
lookup = {}
for location in template_locations:
lookup[location] = TemplateLookup(
directories=template_locations[location],
module_directory=module_directory,
output_encoding='utf-8',
input_encoding='utf-8',
default_filters=['decode.utf8'],
encoding_errors='replace',
)
edxmako.lookup = lookup
for namespace, directories in template_locations.items():
for directory in directories:
add_lookup(namespace, directory)
......@@ -19,7 +19,7 @@ from edxmako.shortcuts import marketing_link
import edxmako
import edxmako.middleware
django_variables = ['lookup', 'output_encoding', 'encoding_errors']
DJANGO_VARIABLES = ['output_encoding', 'encoding_errors']
# TODO: We should make this a Django Template subclass that simply has the MakoTemplate inside of it? (Intead of inheriting from MakoTemplate)
......@@ -34,8 +34,8 @@ class Template(MakoTemplate):
def __init__(self, *args, **kwargs):
"""Overrides base __init__ to provide django variable overrides"""
if not kwargs.get('no_django', False):
overrides = dict([(k, getattr(edxmako, k, None),) for k in django_variables])
overrides['lookup'] = overrides['lookup']['main']
overrides = {k: getattr(edxmako, k, None) for k in DJANGO_VARIABLES}
overrides['lookup'] = edxmako.LOOKUP['main']
kwargs.update(overrides)
super(Template, self).__init__(*args, **kwargs)
......
from django.test import TestCase
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from edxmako import add_lookup, LOOKUP
from edxmako.shortcuts import marketing_link
from mock import patch
from util.testing import UrlResetMixin
......@@ -24,3 +25,15 @@ class ShortcutsTests(UrlResetMixin, TestCase):
expected_link = reverse('login')
link = marketing_link('ABOUT')
self.assertEquals(link, expected_link)
class AddLookupTests(TestCase):
"""
Test the `add_lookup` function.
"""
@patch('edxmako.LOOKUP', {})
def test_with_package(self):
add_lookup('test', 'management', __name__)
dirs = LOOKUP['test'].directories
self.assertEqual(len(dirs), 1)
self.assertTrue(dirs[0].endswith('management'))
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
import edxmako
from edxmako import lookup_template
class Command(BaseCommand):
......@@ -15,8 +15,8 @@ body, and an _subject.txt for the subject. '''
#text = open(args[0]).read()
#subject = open(args[1]).read()
users = User.objects.all()
text = edxmako.lookup['main'].get_template('email/' + args[0] + ".txt").render()
subject = edxmako.lookup['main'].get_template('email/' + args[0] + "_subject.txt").render().strip()
text = lookup_template('main', 'email/' + args[0] + ".txt").render()
subject = lookup_template('main', 'email/' + args[0] + "_subject.txt").render().strip()
for user in users:
if user.is_active:
user.email_user(subject, text)
......@@ -4,7 +4,7 @@ import time
from django.core.management.base import BaseCommand
from django.conf import settings
import edxmako
from edxmako import lookup_template
from django.core.mail import send_mass_mail
import sys
......@@ -39,8 +39,8 @@ rate -- messages per second
users = [u.strip() for u in open(user_file).readlines()]
message = edxmako.lookup['main'].get_template('emails/' + message_base + "_body.txt").render()
subject = edxmako.lookup['main'].get_template('emails/' + message_base + "_subject.txt").render().strip()
message = lookup_template('main', 'emails/' + message_base + "_body.txt").render()
subject = lookup_template('main', 'emails/' + message_base + "_subject.txt").render().strip()
rate = int(ratestr)
self.log_file = open(logfilename, "a+", buffering=0)
......
"""
Test `massemail` and `massemailtxt` commands.
"""
import mock
import pkg_resources
from django.core import mail
from django.test import TestCase
from edxmako import add_lookup
from ..management.commands import massemail
from ..management.commands import massemailtxt
class TestMassEmailCommands(TestCase):
"""
Test `massemail` and `massemailtxt` commands.
"""
@mock.patch('edxmako.LOOKUP', {})
def test_massemailtxt(self):
"""
Test the `massemailtext` command.
"""
add_lookup('main', '', package=__name__)
userfile = pkg_resources.resource_filename(__name__, 'test_massemail_users.txt')
command = massemailtxt.Command()
command.handle(userfile, 'test', '/dev/null', 10)
self.assertEqual(len(mail.outbox), 2)
self.assertEqual(mail.outbox[0].to, ["Fred"])
self.assertEqual(mail.outbox[0].subject, "Test subject.")
self.assertEqual(mail.outbox[0].body.strip(), "Test body.")
self.assertEqual(mail.outbox[1].to, ["Barney"])
self.assertEqual(mail.outbox[1].subject, "Test subject.")
self.assertEqual(mail.outbox[1].body.strip(), "Test body.")
@mock.patch('edxmako.LOOKUP', {})
@mock.patch('student.management.commands.massemail.User')
def test_massemail(self, usercls):
"""
Test the `massemail` command.
"""
add_lookup('main', '', package=__name__)
fred = mock.Mock()
barney = mock.Mock()
usercls.objects.all.return_value = [fred, barney]
command = massemail.Command()
command.handle('test')
fred.email_user.assert_called_once_with('Test subject.', 'Test body.\n')
barney.email_user.assert_called_once_with('Test subject.', 'Test body.\n')
import json
import mock
from datetime import datetime
from pytz import UTC
from django.core.urlresolvers import reverse
......@@ -11,6 +12,7 @@ import django_comment_client.utils as utils
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from edxmako import add_lookup
class DictionaryTestCase(TestCase):
......@@ -505,3 +507,17 @@ class JsonResponseTestCase(TestCase, UnicodeTestMixin):
response = utils.JsonResponse(text)
reparsed = json.loads(response.content)
self.assertEqual(reparsed, text)
class RenderMustacheTests(TestCase):
"""
Test the `render_mustache` utility function.
"""
@mock.patch('edxmako.LOOKUP', {})
def test_it(self):
"""
Basic test.
"""
add_lookup('main', '', package=__name__)
self.assertEqual(utils.render_mustache('test.mustache', {}), 'Testing 1 2 3.\n')
import pytz
from collections import defaultdict
import logging
import urllib
from datetime import datetime
from django.contrib.auth.models import User
......@@ -12,7 +11,7 @@ from django.utils import simplejson
from django_comment_common.models import Role, FORUM_ROLE_STUDENT
from django_comment_client.permissions import check_permissions_by_view
import edxmako
from edxmako import lookup_template
import pystache_custom as pystache
from xmodule.modulestore.django import modulestore
......@@ -307,7 +306,7 @@ def get_metadata_for_threads(course_id, threads, user, user_info):
def render_mustache(template_name, dictionary, *args, **kwargs):
template = edxmako.lookup['main'].get_template(template_name).source
template = lookup_template('main', template_name).source
return pystache.render(template, dictionary)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment