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.:
-
application-specific info message,
-
model instances details in the list context,
-
login messages like 'Please login or signup to [some app-dependent text]' with appropriate url's,
-
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 »" 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
- Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
- Help text hyperlinks by sa2812 1 year, 8 months ago
Comments
Please login first before commenting.