From 0a1ed11daa78cb4aa5810f15492f12d10de477ad Mon Sep 17 00:00:00 2001 From: Chris Rossi Date: Thu, 23 Jan 2014 12:05:03 -0500 Subject: [PATCH] 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__) --- common/djangoapps/edxmako/__init__.py | 3 +- common/djangoapps/edxmako/paths.py | 51 +++++++++++++++++++ common/djangoapps/edxmako/shortcuts.py | 4 +- common/djangoapps/edxmako/startup.py | 32 +++--------- common/djangoapps/edxmako/template.py | 6 +-- common/djangoapps/edxmako/tests.py | 13 +++++ .../student/management/commands/massemail.py | 6 +-- .../management/commands/massemailtxt.py | 6 +-- .../djangoapps/student/tests/email/test.txt | 1 + .../student/tests/email/test_subject.txt | 1 + .../student/tests/emails/test_body.txt | 1 + .../student/tests/emails/test_subject.txt | 1 + .../student/tests/test_massemail.py | 50 ++++++++++++++++++ .../student/tests/test_massemail_users.txt | 2 + .../django_comment_client/tests/test.mustache | 1 + .../django_comment_client/tests/test_utils.py | 16 ++++++ lms/djangoapps/django_comment_client/utils.py | 5 +- 17 files changed, 159 insertions(+), 40 deletions(-) create mode 100644 common/djangoapps/edxmako/paths.py create mode 100644 common/djangoapps/student/tests/email/test.txt create mode 100644 common/djangoapps/student/tests/email/test_subject.txt create mode 100644 common/djangoapps/student/tests/emails/test_body.txt create mode 100644 common/djangoapps/student/tests/emails/test_subject.txt create mode 100644 common/djangoapps/student/tests/test_massemail.py create mode 100644 common/djangoapps/student/tests/test_massemail_users.txt create mode 100644 lms/djangoapps/django_comment_client/tests/test.mustache diff --git a/common/djangoapps/edxmako/__init__.py b/common/djangoapps/edxmako/__init__.py index 007b2680ae..613342b35a 100644 --- a/common/djangoapps/edxmako/__init__.py +++ b/common/djangoapps/edxmako/__init__.py @@ -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 diff --git a/common/djangoapps/edxmako/paths.py b/common/djangoapps/edxmako/paths.py new file mode 100644 index 0000000000..8601110ddd --- /dev/null +++ b/common/djangoapps/edxmako/paths.py @@ -0,0 +1,51 @@ +""" +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) diff --git a/common/djangoapps/edxmako/shortcuts.py b/common/djangoapps/edxmako/shortcuts.py index d70e2145dd..73f76b8afd 100644 --- a/common/djangoapps/edxmako/shortcuts.py +++ b/common/djangoapps/edxmako/shortcuts.py @@ -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) diff --git a/common/djangoapps/edxmako/startup.py b/common/djangoapps/edxmako/startup.py index 2b58deac2e..1783373239 100644 --- a/common/djangoapps/edxmako/startup.py +++ b/common/djangoapps/edxmako/startup.py @@ -1,33 +1,15 @@ """ 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) diff --git a/common/djangoapps/edxmako/template.py b/common/djangoapps/edxmako/template.py index 43ac057a27..209b4d6c4f 100644 --- a/common/djangoapps/edxmako/template.py +++ b/common/djangoapps/edxmako/template.py @@ -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) diff --git a/common/djangoapps/edxmako/tests.py b/common/djangoapps/edxmako/tests.py index 882d6612d4..2fc79bb348 100644 --- a/common/djangoapps/edxmako/tests.py +++ b/common/djangoapps/edxmako/tests.py @@ -1,6 +1,7 @@ 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')) diff --git a/common/djangoapps/student/management/commands/massemail.py b/common/djangoapps/student/management/commands/massemail.py index 5ce8afb41f..8e9e2ba69f 100644 --- a/common/djangoapps/student/management/commands/massemail.py +++ b/common/djangoapps/student/management/commands/massemail.py @@ -1,7 +1,7 @@ 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) diff --git a/common/djangoapps/student/management/commands/massemailtxt.py b/common/djangoapps/student/management/commands/massemailtxt.py index 1ff8557a25..e3ec851745 100644 --- a/common/djangoapps/student/management/commands/massemailtxt.py +++ b/common/djangoapps/student/management/commands/massemailtxt.py @@ -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) diff --git a/common/djangoapps/student/tests/email/test.txt b/common/djangoapps/student/tests/email/test.txt new file mode 100644 index 0000000000..11115a5a72 --- /dev/null +++ b/common/djangoapps/student/tests/email/test.txt @@ -0,0 +1 @@ +Test body. diff --git a/common/djangoapps/student/tests/email/test_subject.txt b/common/djangoapps/student/tests/email/test_subject.txt new file mode 100644 index 0000000000..6f4d1f63dc --- /dev/null +++ b/common/djangoapps/student/tests/email/test_subject.txt @@ -0,0 +1 @@ +Test subject. diff --git a/common/djangoapps/student/tests/emails/test_body.txt b/common/djangoapps/student/tests/emails/test_body.txt new file mode 100644 index 0000000000..11115a5a72 --- /dev/null +++ b/common/djangoapps/student/tests/emails/test_body.txt @@ -0,0 +1 @@ +Test body. diff --git a/common/djangoapps/student/tests/emails/test_subject.txt b/common/djangoapps/student/tests/emails/test_subject.txt new file mode 100644 index 0000000000..6f4d1f63dc --- /dev/null +++ b/common/djangoapps/student/tests/emails/test_subject.txt @@ -0,0 +1 @@ +Test subject. diff --git a/common/djangoapps/student/tests/test_massemail.py b/common/djangoapps/student/tests/test_massemail.py new file mode 100644 index 0000000000..39311a528b --- /dev/null +++ b/common/djangoapps/student/tests/test_massemail.py @@ -0,0 +1,50 @@ +""" +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') diff --git a/common/djangoapps/student/tests/test_massemail_users.txt b/common/djangoapps/student/tests/test_massemail_users.txt new file mode 100644 index 0000000000..98afe6a240 --- /dev/null +++ b/common/djangoapps/student/tests/test_massemail_users.txt @@ -0,0 +1,2 @@ +Fred +Barney diff --git a/lms/djangoapps/django_comment_client/tests/test.mustache b/lms/djangoapps/django_comment_client/tests/test.mustache new file mode 100644 index 0000000000..b69057d5ae --- /dev/null +++ b/lms/djangoapps/django_comment_client/tests/test.mustache @@ -0,0 +1 @@ +Testing 1 2 3. diff --git a/lms/djangoapps/django_comment_client/tests/test_utils.py b/lms/djangoapps/django_comment_client/tests/test_utils.py index 8acb5a057e..a35f6d8b78 100644 --- a/lms/djangoapps/django_comment_client/tests/test_utils.py +++ b/lms/djangoapps/django_comment_client/tests/test_utils.py @@ -1,4 +1,5 @@ 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') diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 4aefbc67af..94853eeb35 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -1,7 +1,6 @@ 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)