# -*- 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)