""" Library for enable relative pathes in django template tags 'extends' and 'include' (C) 2016 by Vitaly Bogomolov mail@vitaly-bogomolov.ru Origin: https://github.com/vb64/django.templates.relative.path """ import posixpath from django import template from django.conf import settings from django.template.base import Template as TemplateParent from django.template.base import (Lexer, Parser, StringOrigin, TemplateEncodingError, TemplateSyntaxError, token_kwargs) from django.template.loader_tags import ExtendsNode as ExtendsNodeParent from django.template.loader_tags import IncludeNode from django.template.loaders import app_directories as ad from django.template.loaders import filesystem as fs from django.utils.encoding import smart_unicode # django 1.9 try: from django.template import ( TemplateDoesNotExist, Template as Template19parent, ) from django.utils.inspect import func_supports_parameter from django.template.base import DebugLexer as DebugLexer19 from django.template.utils import get_app_template_dirs except: pass ################################################ # template loaders ################################################ def compile_string(template_string, origin, name): """ Set template name to Parser instance """ if settings.TEMPLATE_DEBUG: from django.template.debug import DebugLexer, DebugParser lexer_class, parser_class = DebugLexer, DebugParser else: lexer_class, parser_class = Lexer, Parser lexer = lexer_class(template_string, origin) parser = parser_class(lexer.tokenize()) parser.template_name = name return parser.parse() class Template(TemplateParent): """ Pass template name to Parser """ def __init__(self, template_string, origin=None, name=None, engine=None): try: template_string = smart_unicode(template_string) except UnicodeDecodeError: raise TemplateEncodingError("Templates can only be constructed " "from unicode or UTF-8 strings.") if settings.TEMPLATE_DEBUG and origin is None: origin = StringOrigin(template_string) self.nodelist = compile_string(template_string, origin, name) self.name = name self.origin = origin # !!! ATTENTION !!! # always use default engine for render try: from django.template.engine import Engine self.engine = Engine.get_default() except Exception: self.engine = None class FileSystem(fs.Loader): """ Use modified Template class """ is_usable = True def load_template(self, template_name, template_dirs=None): """ Use modified Template class and pass template name """ source, origin = self.load_template_source( template_name, template_dirs ) template = Template( source, name=template_name ) return template, origin class AppDirectories(ad.Loader): """ Use modified Template class """ is_usable = True def load_template(self, template_name, template_dirs=None): """ Use modified Template class and pass template name """ source, origin = self.load_template_source( template_name, template_dirs ) template = Template(source, name=template_name) return template, origin class Template19(Template19parent): """ Pass template name to Parser into Django 1.9 """ def compile_nodelist(self): """ Pass template name to parser instance """ if self.engine.debug: lexer = DebugLexer19(self.source) else: lexer = Lexer(self.source) tokens = lexer.tokenize() parser = Parser( tokens, self.engine.template_libraries, self.engine.template_builtins, ) parser.template_name = self.origin.template_name try: return parser.parse() except Exception as e: if self.engine.debug: e.template_debug = self.get_exception_info(e, e.token) raise class FileSystem19(fs.Loader): """ Use modified Template19 class """ def get_template(self, template_name, template_dirs=None, skip=None): """ Use modified Template19 class and pass template name to instance """ tried = [] args = [template_name] if func_supports_parameter(self.get_template_sources, 'template_dirs'): args.append(template_dirs) for origin in self.get_template_sources(*args): if skip is not None and origin in skip: tried.append((origin, 'Skipped')) continue try: contents = self.get_contents(origin) except TemplateDoesNotExist: tried.append((origin, 'Source does not exist')) continue else: return Template19( contents, origin, origin.template_name, self.engine, ) raise TemplateDoesNotExist(template_name, tried=tried) class AppDirectories19(FileSystem19): """ Use modified Template19 class """ def get_dirs(self): """ Same as core function """ return get_app_template_dirs('templates') ################################################ # template tags 'extends' and 'include' ################################################ register = template.Library() class ExtendsNode(ExtendsNodeParent): """ !!! ATTENTION !!! it disable rule, that 'extends' must be first tag in template this need for first {% load relative_path %} tag """ must_be_first = False def construct_relative_path(name, relative_name): """ Construct new path from saved into parser instance name and given relative path in extends/include handlers with posixpath functions, then handle new path by standard extends/include rules. """ if not relative_name.startswith('"./'): # argument is variable or literal, that not contain relative path return relative_name new_name = posixpath.normpath( posixpath.join( posixpath.dirname(name.lstrip('/')), relative_name.strip('"') ) ) if new_name.startswith('../'): raise TemplateSyntaxError( "Relative name '%s' have more parent folders, then given template name '%s'" % (relative_name, name) ) if name.lstrip('/') == new_name: raise TemplateSyntaxError( "Circular dependencies: relative path '%s' was translated to template name '%s'" % (relative_name, name) ) return '"%s"' % new_name @register.tag('extends') def do_extends(parser, token): """ Same as core function, with construct_relative_path call """ bits = token.split_contents() if len(bits) != 2: raise TemplateSyntaxError("'%s' takes one argument" % bits[0]) bits[1] = construct_relative_path(parser.template_name, bits[1]) parent_name = parser.compile_filter(bits[1]) nodelist = parser.parse() if nodelist.get_nodes_by_type(ExtendsNode): raise TemplateSyntaxError( "'%s' cannot appear more than once in the same template" % bits[0] ) return ExtendsNode(nodelist, parent_name) @register.tag('include') def do_include(parser, token): """ Same as core function, with construct_relative_path call """ bits = token.split_contents() if len(bits) < 2: raise TemplateSyntaxError( "%r tag takes at least one argument: " "the name of the template to be included." % bits[0] ) options = {} remaining_bits = bits[2:] while remaining_bits: option = remaining_bits.pop(0) if option in options: raise TemplateSyntaxError('The %r option was specified more ' 'than once.' % option) if option == 'with': value = token_kwargs(remaining_bits, parser, support_legacy=False) if not value: raise TemplateSyntaxError('"with" in %r tag needs at least ' 'one keyword argument.' % bits[0]) elif option == 'only': value = True else: raise TemplateSyntaxError('Unknown argument for %r tag: %r.' % (bits[0], option)) options[option] = value isolated_context = options.get('only', False) namemap = options.get('with', {}) bits[1] = construct_relative_path(parser.template_name, bits[1]) return IncludeNode(parser.compile_filter(bits[1]), extra_context=namemap, isolated_context=isolated_context)