Login

Subdirectory and subcontext include template tag with examples

Author:
t_rybik
Posted:
March 19, 2010
Language:
Python
Version:
1.1
Score:
1 (after 1 ratings)

In principle it's a DRY code of a template node that can be used for creating project-wide template-tags. These can be used to display conceptually common blocks in templates with applications specific looks and content. E.g.:

  1. application-specific info message,

  2. model instances details in the list context,

  3. login messages like 'Please login or signup to [some app-dependent text]' with appropriate url's,

  4. standard forms (e.g with same CSS classes, displayed with uni_form).

The code is profusely commented so look it up for details. Basically, when you have your project-wide SubIncludeNode tag defined, then just define appropriately named templates in applications template subdirectories e.g. 'profiles/_show_item.html'.

Example usage: login or signup message

Let's say we are editing template rendered by the view indicated by the pattern named 'project_list' in the 'projects' app:

{# those are equivalent #}
{% login_or_signup %}
{% login_or_signup project_list %}
{% login_or_signup "project_list" %}

{# academic purposes direct sub_include usage #}

{# those two includes are also equivalent if there is no template called '_login_or_signup.html' in the 'projects' subdirectory #}
{% url acct_signup as signup_url %}
{% url acct_login as login_url %}
{% url project_list as next_url %}
{% sub_include "_login_or_signup.html" %}
{% include "_login_or_signup.html" %}
{# and those two are also equivalent if there is a template called '_login_or_signup.html' in the 'projects' subdirectory #}
{% sub_include "_login_or_signup.html" from "projects" %}
{% sub_include "_login_or_signup.html" %}
{# also equivalent include if there is no variable called 'projects' in tempalte's context #}
{% sub_include "_login_or_signup.html" from projects %}

{# those are examples of using custom subcontext #}
{% url acct_signup as signup_url %}
{% url acct_login as login_url %}
{% sub_include "_login_or_signup.html" from "default" with login_url,signup_url,next_url="/welcome" %}
{% sub_include "_login_or_signup.html" with login_url="/login",next_url="/welcome",signup_url="/signup" %}

Mored advanced usage proposal

Say you have an subclasses of some model from the original app in a separate extending apps. You want to display subclasses instances differently using the original app templates. In the original app you do not know what the set of subclasses is (as the original app is not aware of the extending apps). Subclassing the SubIncludeNode you can override the default_subdir method such that it returns the app label of a model instance that was passed as a argument of your tag. This way you just put templates like _show_instance.html in extending apps subdirectories and voila - you have implementation of the overrideable templates displaying appropriately details about the model subclass instances.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# -*- coding: utf-8 -*-
'''
Created on 30 Apr 2010

@author: trybik
'''
import os
import inspect
from warnings import warn

from django.template import Library, TemplateSyntaxError, Node, Context
from django.template.loader import select_template
from django.conf import settings
from django.utils.encoding import smart_str
from django.utils.translation import ugettext_lazy
from django.core.urlresolvers import reverse, resolve
from django.utils.safestring import mark_safe



################################################################################
# Django URL and views tools
################################################################################

def resolve_view(path, urlconf=None):
    '''
    Get the view function resolved by the URL path.
    '''
    view,_,_ = resolve(path, urlconf=urlconf)
    return getattr(view, 'view_func', view)

def view_app_label(view_func):
    '''
    Get the app label of the view function.

    Looks in the INSTALLED_APPS and if found matching app then trims the view's
    app name according to INSTALLED_APPS entry. Otherwise tries to trim the
    'views' part of the module name. If that was not successful simply return
    the view function module name.

    ATTENTION: to be sure that this function works as expected, if view is
               decorated then make sure that all of the decorators either
               - use functools.wraps (or django.utils.functional.wraps)
               or
               - explicitly copy  __module__ attribute of the decorated view.
    '''
    # Can't really use:
    #    return view_func.func_globals['__package__']
    # mainly due to the fact that update_wrapper does not copy func_globals.
    for app_label in settings.INSTALLED_APPS:
        if view_func.__module__.startswith(app_label) and\
           (len(view_func.__module__) == len(app_label) or\
            view_func.__module__[len(app_label)] == '.'):
            return app_label
    warn('Application label for %s.%s() view has not been found in INSTALLED_APPS.' % (view_func.__module__, view_func.__name__))
    modules = view_func.__module__.split('.')
    if len(modules)>1 and modules[-1] == 'views':
        return '.'.join(modules[:-1])
    return view_func.__module__



################################################################################
# Django template tools
################################################################################

def resolve_or_id(var, context):
    '''
    Resolve variable in context or return identity.
    '''
    if inspect.ismethod(getattr(var,'resolve', None)):
        return var.resolve(context)
    return var

def unquote(string):
    '''
    Removes quotes form string if found.
    '''
    if string[0] in ('"',"'"):
        if string[0] == string[-1]:
            string = string[1:-1]
        else:
            raise TemplateSyntaxError("Bad string quoting, found: %s." % string)
    return string

def context_pair(varname, value, context):
    '''
    Pair (tuple) with varname and it's value resolved form given context.

    Value can be:
    - a single value with string or any object that can be resolved
    in context (e.g. Variable or FilterExpression) variable
    or
    - a pair (value, filters), where value is as described above and filters
    is an iterable of callables to be called on the resolved value
    '''
    filters = ()
    if isinstance(value, tuple):
        value, filters = value
    value = resolve_or_id(value, context)
    for filter in filters:
        assert callable(filter), "Callable expected in filters for the context-resolved value."
        value = filter(value)
    return (smart_str(varname,'ascii'), value)

################################################################################
# TAGS
################################################################################


register = Library()



class SubIncludeNode(Node):
    '''
    Loads and renders template like include node but:
    [subdirectory]
    1. loads a template from the given templates subdirectory
       or, if not given, form the directory named as the app to which the view
       which renders the template that used SubIncludeNode belongs to
       or, if not found, directly from the TEMPLATE_DIRS;
       Note: loading from the app template folders relies on the convention of
       organizing templates in subdirectories of the TEMPLATE_DIRS.
    [subcontext]
    2. if any args or kwargs are given then only this subset of the context
       variables is passed to the rendered template (vide
       django.template.Library.inclusion_tag).
    '''

    def __init__(self, template_name, args=[], kwargs={}, subdir=None):
        '''
        If 'template_name' and 'subdir' as well as 'args' list elements and
        'kwargs' dictionary values implement method resolve() then this method
        is used with current template context passed as a first argument.

        For example, they can be instances of the FilterExpression class,
        created via compile_filter()  method of the parser in the template tag
        method or instances of the template Variable class. Note that they must
        be passed from the tag function as an instances of the Variable class if
        you want to use them as such.
        '''
        self.template_name = template_name
        self.subdir = subdir
        self.args = args
        self.kwargs = kwargs
        super(SubIncludeNode, self).__init__()

    def default_subdir(self, context):
        '''
        As the deafult subdir get the app label of the view according to the
        context's 'request.path' and the default 'urlconf'.

        Override this method if different strategy of calculating the default
        template subdirectory is required.
        '''
        assert context.has_key('request'),\
               "The subdirectory and subcontext template node requires the request context processor to be installed. Edit your TEMPLATE_CONTEXT_PROCESSORS setting to insert 'django.core.context_processors.request'."
        # assumes that the subdirectory is named after the last part of the
        # complex app label e.g. 'auth' for 'django.contrib.auth' app label
        return view_app_label(resolve_view(context['request'].path)).split('.')[-1]

    def render(self, context):
        '''
        The subcontext of the rendered template will include variables named
        according to 'self.kwargs' keys and variables named as the context
        variables in the 'args' list. If none given, then full context is passed
        to the template included from the 'self.subdir' subdirectory.
        '''
        subcontext_dict = dict([context_pair(arg, arg, context) for arg in self.args])
        subcontext_dict.update(
            dict([context_pair(k, v, context) for k, v in self.kwargs.items()]))
        template_context = Context(subcontext_dict)\
                                if bool(subcontext_dict) else\
                           context
        try:
            template_name = resolve_or_id(self.template_name, context)
            # TODO: as a template loader? how to obtain current_app?
            subdir = resolve_or_id(self.subdir, context) if self.subdir else\
                     self.default_subdir(context)
            # select template from the 'subdir' or TEMPLATE_DIRS if not found
            t = select_template([os.path.join(subdir, template_name), template_name])
            return t.render(template_context)
        except (TemplateSyntaxError, AssertionError):
            if settings.TEMPLATE_DEBUG:
                raise
            return ''
        except:
            return '' # Fail silently for invalid included templates.


def _parse_args_and_kwargs(parser, bits_iter, sep=","):
    '''
    Parses bits created form token "arg1,key1=val1, arg2 , ..." after spliting
    contents (separator is customizable). Returns list of args and dictionary of
    kwargs.
    '''
    args = []
    kwargs = {}
    for bit in bits_iter:
        for arg in bit.split(sep):
            if '=' in arg:
                k, v = arg.split('=', 1)
                k = k.strip()
                kwargs[k] = parser.compile_filter(v)
            elif arg:
                args.append(parser.compile_filter(arg))
    return args, kwargs


def sub_include(parser, token):
    '''
    Syntax::

        {% sub_include template_name from subdir with arg1,key1=val1, ... %}
        {% sub_include template_name with arg1,key1=val1, ... %}
        {% sub_include template_name from subdir %}
        {% sub_include template_name %}

    Example usage::

        {% sub_include "login_or_signup_message.html" from "projects" %}
        # which is equivalent to
        {% include "projects/login_or_signup_message.html" %}

        {% sub_include "login_or_signup_message.html" next_url='project_list' %}
        # which is equivalent to having defined an inclusion_tag that passes
        # 'next_url' argument to the "login_or_signup_message.html" template;
        # inclusion tag defined in the app which defines the view that renders
        # template that uses the above tag
    '''
    bits = token.split_contents()
    if len(bits) < 2:
        raise TemplateSyntaxError("'%s' tag takes at least one argument: '[template_name]'" % bits[0])
    template_name = parser.compile_filter(bits[1])
    subdir = None
    args = []
    kwargs = {}
    if len(bits) > 2:
        if bits[2] == 'from':
            if len(bits) < 4:
                raise TemplateSyntaxError("Expected subdirectory name following the 'from' argument in '%s' tag." % bits[0])
            subdir = parser.compile_filter(bits[3])
            i = 4
        else:
            i = 2
        if len(bits) > i:
            if bits[i] != 'with':
                raise TemplateSyntaxError("Expected argument 'with' in '%s' tag" % bits[0])
            if len(bits) < i+2:
                raise TemplateSyntaxError("Expected variables definitions 'arg1,key1=val1, ...' following the 'with' argument in '%s' tag." % bits[0])
            args, kwargs = _parse_args_and_kwargs(parser, iter(bits[(i+1):]))
    return SubIncludeNode(template_name, args, kwargs, subdir)
sub_include = register.tag(sub_include)





def info(parser, token):
    '''
    Syntax::

        {% info %}

    Example usage::

        {% info %}
    '''
    bits = token.split_contents()
    if len(bits) > 1:
        raise TemplateSyntaxError("'%s' tag takes no arguments." % bits[0])
    return SubIncludeNode('_info.html')
info = register.tag(info)



def show_item(parser, token):
    '''
    Show item; usually a model instance in a list.
    Syntax::

        {% show_item item %}

    Example usage::

        {% for user in users %}
            {% show_item user %}
        {% endfor %}
    '''
    bits = token.split_contents()
    if len(bits) < 2:
        raise TemplateSyntaxError("'%s' tag takes exactly one argument: [item]." % bits[0])
    item = parser.compile_filter(bits[1])
    return SubIncludeNode('_show_item.html', kwargs={'item': item})
show_item = register.tag(show_item)



def show_form(parser, token):
    '''
    Display form.
    Syntax::
        {% show_form form id %}
        {% show_form form id action_url=url_name classes=classes_str submit=submit_str template=template_name %}
        TODO: {% show_form form id with_inputs type=val, type=val %} problem: repeatable types

    where

        form - Form instance variable
        id - string with form id

    and optionally (in any order)

        action_url - target url (format as in the url tag); by default empty
                     string is set as an action attribute value
        classes - form CSS classes (space-separated - quoting needed in such
                  case)
        submit - submit input value; by default u"Submit"
        template - template name for customizations (e.g. via extending default,
                    '_show_form.html' template)

    Example usage::

        {% show_form form "address_form" "address_create" submit="Create &raquo;" classes="address popup" %}
    '''
    bits = token.split_contents()
    if len(bits) < 3:
        raise TemplateSyntaxError("'%s' tag takes at least two arguments: [form] [id]." % bits[0])
    form = parser.compile_filter(bits[1])
    id = parser.compile_filter(bits[2])
    template_name = '_show_form.html'
    defaults = {
        'action_url': None,
        'classes':'',
        'submit':ugettext_lazy(u"Submit"),
    }
    if len(bits) >= 3:
        __, kwargs = _parse_args_and_kwargs(parser, iter(bits[4:]))
        for key in ('action_url', 'classes'):
            if key in kwargs:
                defaults[key] = kwargs[key]
        if 'submit' in kwargs:
            defaults['submit'] = (kwargs['submit'], (mark_safe, ugettext_lazy))
        if 'template' in kwargs:
            template_name = kwargs.get('template')
    defaults.update(form=form, id=id)
    return SubIncludeNode(template_name, kwargs=defaults)
show_form = register.tag(show_form)



class LoginOrSignupNode(SubIncludeNode):
    def __init__(self, template_name='_login_or_signup.html',
                 login_view_name='acct_login', signup_view_name='acct_signup',
                 next_view_name=None):
        super(LoginOrSignupNode, self).__init__(template_name, kwargs={
                'login_url': reverse(login_view_name),
                'next_url': reverse(next_view_name) if next_view_name else None,
                'signup_url': reverse(signup_view_name)
              })

    def render(self, context):
        '''
        Get the current request path as the default 'next_url' value if not given.
        '''
        if not self.kwargs['next_url']:
            request = context.get('request', None)
            if request:
                self.kwargs['next_url'] = request.path
        return super(LoginOrSignupNode, self).render(context)

def login_or_signup(parser, token):
    '''
    Syntax::

        {% login_or_signup next_view_name %}
        {% login_or_signup %}

    Example usage::

        {% login_or_signup 'welcome_page' %}
    '''
    bits = token.split_contents()
    next_view_name = None
    if len(bits) > 1:
        next_view_name = unquote(bits[1])
    return LoginOrSignupNode(next_view_name=next_view_name)
#    # alternatively, simpler version without setting the default next_url:
#    return SubIncludeNode('_login_or_signup.html', kwargs={
#                'login_url': reverse('acct_login'),
#                'next_url': reverse(next_view_name) if next_view_name else None,
#                'signup_url': reverse('acct_signup')
#           })

login_or_signup = register.tag(login_or_signup)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
  5. Help text hyperlinks by sa2812 1 year, 8 months ago

Comments

Please login first before commenting.