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 import template
15 from django.utils.encoding import force_text, python_2_unicode_compatible
16 from django.utils.functional import Promise
17 from django.utils.safestring import mark_safe
18 from .exceptions import SsiVarsDependencyCycleError
21 @python_2_unicode_compatible
22 class SsiVariable(object):
24 Represents a variable computed by a template tag with given arguments.
26 Instance of this class is returned from any template tag created
27 with `decorators.ssi_variable` decorator. If renders as SSI echo
28 statement, but you can also use it as an argument to {% ssi_include %},
29 to other ssi_variable, or create SSI if statements by using
30 its `if`, `else`, `endif` properties.
32 Variable's name, as used in SSI statements, is a hash of its definition,
33 so the user never has to deal with it directly.
36 def __init__(self, tagpath=None, args=None, kwargs=None, name=None):
37 self.tagpath = tagpath
38 self.args = list(args or [])
39 self.kwargs = kwargs or {}
44 """Variable name is a hash of its definition."""
45 if self._name is None:
46 self._name = 'v' + md5(json_encode(self.definition).encode('ascii')).hexdigest()
51 Sometimes there's a need to reset the variable name.
53 Typically, this is the case after finding real values for
54 variables passed as arguments to {% ssi_include %}.
61 """Variable is defined by path to template tag and its arguments."""
63 return self.tagpath, self.args, self.kwargs
65 return self.tagpath, self.args
70 return "SsiVariable(%s: %s)" % (self.name, repr(self.definition))
72 def get_value(self, request):
73 """Computes the real value of the variable, using the request."""
74 taglib, tagname = self.tagpath.rsplit('.', 1)
75 return template.get_library(taglib).tags[tagname].get_value(
76 request, *self.args, **self.kwargs)
79 return mark_safe("<!--#echo var='%s' encoding='none'-->" % self.name)
82 """Returns the form that can be used in SSI include's URL."""
83 return '${%s}' % self.name
85 # If-else-endif properties for use in templates.
86 setattr(SsiVariable, 'if',
87 lambda self: mark_safe("<!--#if expr='${%s}'-->" % self.name))
88 setattr(SsiVariable, 'else',
89 staticmethod(lambda: mark_safe("<!--#else-->")))
90 setattr(SsiVariable, 'endif',
91 staticmethod(lambda: mark_safe('<!--#endif-->')))
94 class SsiExpect(object):
95 """This class says: I want the real value of this variable here."""
96 def __init__(self, name):
99 return "SsiExpect(%s)" % (self.name,)
102 def ssi_expect(var, type_):
104 Helper function for defining get_ssi_vars on ssi_included views.
106 The view needs a way of calculating all the needed variables from
107 the view args. But the args are probably the wrong type
108 (typically, str instead of int) or even are SsiVariables, not
109 resolved until request time.
111 This function provides a way to expect a real value of the needed type.
114 if isinstance(var, SsiVariable):
115 return SsiExpect(var.name)
120 class SsiVariableNode(template.Node):
121 """ Node for the SsiVariable tags. """
122 def __init__(self, tagpath, args, kwargs, patch_response=None, asvar=None):
123 self.tagpath = tagpath
126 self.patch_response = patch_response
130 return "<SsiVariableNode>"
132 def render(self, context):
133 """Renders the tag as SSI echo or sets the context variable."""
134 resolved_args = [var.resolve(context) for var in self.args]
135 resolved_kwargs = dict((k, v.resolve(context))
136 for k, v in self.kwargs.items())
137 var = SsiVariable(self.tagpath, resolved_args, resolved_kwargs)
139 request = context['request']
140 if not hasattr(request, 'ssi_vars_needed'):
141 request.ssi_vars_needed = {}
142 request.ssi_vars_needed[var.name] = var
143 if self.patch_response:
144 if not hasattr(request, 'ssi_patch_response'):
145 request.ssi_patch_response = []
146 request.ssi_patch_response.extend(self.patch_response)
149 context.dicts[0][self.asvar] = var
155 def ssi_set_statement(var, value):
156 """Generates an SSI set statement for a variable."""
157 if isinstance(value, Promise):
158 # Yes, this is quite brutal. But we need to know
159 # the real value now, we don't know the type,
160 # and we only want to evaluate the lazy function once.
161 value = value._proxy____cast()
162 if value is False or value is None:
164 return "<!--#set var='%s' value='%s'-->" % (
166 force_text(value).replace('\\', '\\\\').replace("'", "\\'"))
169 def provide_vars(request, ssi_vars):
171 Provides all the SSI set statements for ssi_vars variables.
173 The main purpose of this function is to by called by SsifyMiddleware.
175 def resolve_expects(var):
176 if not hasattr(var, 'hash_dirty'):
177 var.hash_dirty = False
179 for i, arg in enumerate(var.args):
180 if isinstance(arg, SsiExpect):
181 var.args[i] = resolved[arg.name]
182 var.hash_dirty = True
183 for k, arg in var.kwargs.items():
184 if isinstance(arg, SsiExpect):
185 var.kwargs[k] = resolved[arg.name]
186 var.hash_dirty = True
188 for arg in var.args + list(var.kwargs.values()):
189 if isinstance(arg, SsiVariable):
190 var.hash_dirty = resolve_expects(arg) or var.hash_dirty
192 hash_dirty = var.hash_dirty
194 # Rehash after calculating the SsiExpects with real
195 # values, because that's what the included views expect.
197 var.hash_dirty = False
201 def resolve_args(var):
203 for k, arg in var.kwargs.items():
204 kwargs[k] = resolved[arg.name] if isinstance(arg, SsiVariable) else arg
205 new_var = SsiVariable(var.tagpath,
206 [resolved[arg.name] if isinstance(arg, SsiVariable) else arg for arg in var.args],
211 queue = list(ssi_vars.values())
213 unresolved_streak = 0
218 rv = resolve_args(var)
219 except KeyError as e:
221 unresolved_streak += 1
222 if unresolved_streak > len(queue):
223 raise SsiVarsDependencyCycleError(request, queue, resolved)
226 resolved[var.name] = rv.get_value(request)
227 unresolved_streak = 0
229 output = "".join(ssi_set_statement(var, value)
230 for (var, value) in resolved.items()
235 from .serializers import json_encode