1 # -*- coding: utf-8 -*-
2 # This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
6 Utilities for defining SSI variables.
8 SSI variables are a way of providing values that need to be computed
9 at request time to the prerendered templates.
12 from __future__ import unicode_literals
13 from hashlib import md5
14 from django.template import Node
18 from django.template.base import get_library
20 from importlib import import_module
21 from django.template.backends.django import get_installed_libraries
23 def get_library(taglib):
24 if not hasattr(get_library, 'libraries'):
25 get_library.libraries = get_installed_libraries()
26 if isinstance(get_library.libraries[taglib], str):
27 get_library.libraries[taglib] = import_module(get_library.libraries[taglib]).register
28 return get_library.libraries[taglib]
30 from django.utils.encoding import force_text, python_2_unicode_compatible
31 from django.utils.functional import Promise
32 from django.utils.safestring import mark_safe
33 from .exceptions import SsiVarsDependencyCycleError
36 @python_2_unicode_compatible
37 class SsiVariable(object):
39 Represents a variable computed by a template tag with given arguments.
41 Instance of this class is returned from any template tag created
42 with `decorators.ssi_variable` decorator. If renders as SSI echo
43 statement, but you can also use it as an argument to {% ssi_include %},
44 to other ssi_variable, or create SSI if statements by using
45 its `if`, `else`, `endif` properties.
47 Variable's name, as used in SSI statements, is a hash of its definition,
48 so the user never has to deal with it directly.
51 def __init__(self, tagpath=None, args=None, kwargs=None, name=None):
52 self.tagpath = tagpath
53 self.args = list(args or [])
54 self.kwargs = kwargs or {}
59 """Variable name is a hash of its definition."""
60 if self._name is None:
61 self._name = 'v' + md5(json_encode(self.definition).encode('ascii')).hexdigest()
66 Sometimes there's a need to reset the variable name.
68 Typically, this is the case after finding real values for
69 variables passed as arguments to {% ssi_include %}.
76 """Variable is defined by path to template tag and its arguments."""
78 return self.tagpath, self.args, self.kwargs
80 return self.tagpath, self.args
85 return "SsiVariable(%s: %s)" % (self.name, repr(self.definition))
87 def get_value(self, request):
88 """Computes the real value of the variable, using the request."""
89 taglib, tagname = self.tagpath.rsplit('.', 1)
90 return get_library(taglib).tags[tagname].get_value(
91 request, *self.args, **self.kwargs)
94 return mark_safe("<!--#echo var='%s' encoding='none'-->" % self.name)
97 """Returns the form that can be used in SSI include's URL."""
98 return '${%s}' % self.name
100 # If-else-endif properties for use in templates.
101 setattr(SsiVariable, 'if',
102 lambda self: mark_safe("<!--#if expr='${%s}'-->" % self.name))
103 setattr(SsiVariable, 'else',
104 staticmethod(lambda: mark_safe("<!--#else-->")))
105 setattr(SsiVariable, 'endif',
106 staticmethod(lambda: mark_safe('<!--#endif-->')))
109 class SsiExpect(object):
110 """This class says: I want the real value of this variable here."""
111 def __init__(self, name):
114 return "SsiExpect(%s)" % (self.name,)
117 def ssi_expect(var, type_):
119 Helper function for defining get_ssi_vars on ssi_included views.
121 The view needs a way of calculating all the needed variables from
122 the view args. But the args are probably the wrong type
123 (typically, str instead of int) or even are SsiVariables, not
124 resolved until request time.
126 This function provides a way to expect a real value of the needed type.
129 if isinstance(var, SsiVariable):
130 return SsiExpect(var.name)
135 class SsiVariableNode(Node):
136 """ Node for the SsiVariable tags. """
137 def __init__(self, tagpath, args, kwargs, patch_response=None, asvar=None):
138 self.tagpath = tagpath
141 self.patch_response = patch_response
145 return "<SsiVariableNode>"
147 def render(self, context):
148 """Renders the tag as SSI echo or sets the context variable."""
149 resolved_args = [var.resolve(context) for var in self.args]
150 resolved_kwargs = dict((k, v.resolve(context))
151 for k, v in self.kwargs.items())
152 var = SsiVariable(self.tagpath, resolved_args, resolved_kwargs)
154 request = context['request']
155 if not hasattr(request, 'ssi_vars_needed'):
156 request.ssi_vars_needed = {}
157 request.ssi_vars_needed[var.name] = var
158 if self.patch_response:
159 if not hasattr(request, 'ssi_patch_response'):
160 request.ssi_patch_response = []
161 request.ssi_patch_response.extend(self.patch_response)
164 context.dicts[0][self.asvar] = var
170 def ssi_set_statement(var, value):
171 """Generates an SSI set statement for a variable."""
172 if isinstance(value, Promise):
173 # Yes, this is quite brutal. But we need to know
174 # the real value now, we don't know the type,
175 # and we only want to evaluate the lazy function once.
176 value = value._proxy____cast()
177 if value is False or value is None:
179 return "<!--#set var='%s' value='%s'-->" % (
181 force_text(value).replace('\\', '\\\\').replace("'", "\\'"))
184 def provide_vars(request, ssi_vars):
186 Provides all the SSI set statements for ssi_vars variables.
188 The main purpose of this function is to by called by SsifyMiddleware.
190 def resolve_expects(var):
191 if not hasattr(var, 'hash_dirty'):
192 var.hash_dirty = False
194 for i, arg in enumerate(var.args):
195 if isinstance(arg, SsiExpect):
196 var.args[i] = resolved[arg.name]
197 var.hash_dirty = True
198 for k, arg in var.kwargs.items():
199 if isinstance(arg, SsiExpect):
200 var.kwargs[k] = resolved[arg.name]
201 var.hash_dirty = True
203 for arg in var.args + list(var.kwargs.values()):
204 if isinstance(arg, SsiVariable):
205 var.hash_dirty = resolve_expects(arg) or var.hash_dirty
207 hash_dirty = var.hash_dirty
209 # Rehash after calculating the SsiExpects with real
210 # values, because that's what the included views expect.
212 var.hash_dirty = False
216 def resolve_args(var):
218 for k, arg in var.kwargs.items():
219 kwargs[k] = resolved[arg.name] if isinstance(arg, SsiVariable) else arg
220 new_var = SsiVariable(var.tagpath,
221 [resolved[arg.name] if isinstance(arg, SsiVariable) else arg for arg in var.args],
226 queue = list(ssi_vars.values())
228 unresolved_streak = 0
233 rv = resolve_args(var)
234 except KeyError as e:
236 unresolved_streak += 1
237 if unresolved_streak > len(queue):
238 raise SsiVarsDependencyCycleError(request, queue, resolved)
241 resolved[var.name] = rv.get_value(request)
242 unresolved_streak = 0
244 output = "".join(ssi_set_statement(var, value)
245 for (var, value) in resolved.items()
250 from .serializers import json_encode