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
15 from django.template.base import get_library
16 from django.utils.encoding import force_text, python_2_unicode_compatible
17 from django.utils.functional import Promise
18 from django.utils.safestring import mark_safe
19 from .exceptions import SsiVarsDependencyCycleError
22 @python_2_unicode_compatible
23 class SsiVariable(object):
25 Represents a variable computed by a template tag with given arguments.
27 Instance of this class is returned from any template tag created
28 with `decorators.ssi_variable` decorator. If renders as SSI echo
29 statement, but you can also use it as an argument to {% ssi_include %},
30 to other ssi_variable, or create SSI if statements by using
31 its `if`, `else`, `endif` properties.
33 Variable's name, as used in SSI statements, is a hash of its definition,
34 so the user never has to deal with it directly.
37 def __init__(self, tagpath=None, args=None, kwargs=None, name=None):
38 self.tagpath = tagpath
39 self.args = list(args or [])
40 self.kwargs = kwargs or {}
45 """Variable name is a hash of its definition."""
46 if self._name is None:
47 self._name = 'v' + md5(json_encode(self.definition).encode('ascii')).hexdigest()
52 Sometimes there's a need to reset the variable name.
54 Typically, this is the case after finding real values for
55 variables passed as arguments to {% ssi_include %}.
62 """Variable is defined by path to template tag and its arguments."""
64 return self.tagpath, self.args, self.kwargs
66 return self.tagpath, self.args
71 return "SsiVariable(%s: %s)" % (self.name, repr(self.definition))
73 def get_value(self, request):
74 """Computes the real value of the variable, using the request."""
75 taglib, tagname = self.tagpath.rsplit('.', 1)
76 return get_library(taglib).tags[tagname].get_value(
77 request, *self.args, **self.kwargs)
80 return mark_safe("<!--#echo var='%s' encoding='none'-->" % self.name)
83 """Returns the form that can be used in SSI include's URL."""
84 return '${%s}' % self.name
86 # If-else-endif properties for use in templates.
87 setattr(SsiVariable, 'if',
88 lambda self: mark_safe("<!--#if expr='${%s}'-->" % self.name))
89 setattr(SsiVariable, 'else',
90 staticmethod(lambda: mark_safe("<!--#else-->")))
91 setattr(SsiVariable, 'endif',
92 staticmethod(lambda: mark_safe('<!--#endif-->')))
95 class SsiExpect(object):
96 """This class says: I want the real value of this variable here."""
97 def __init__(self, name):
100 return "SsiExpect(%s)" % (self.name,)
103 def ssi_expect(var, type_):
105 Helper function for defining get_ssi_vars on ssi_included views.
107 The view needs a way of calculating all the needed variables from
108 the view args. But the args are probably the wrong type
109 (typically, str instead of int) or even are SsiVariables, not
110 resolved until request time.
112 This function provides a way to expect a real value of the needed type.
115 if isinstance(var, SsiVariable):
116 return SsiExpect(var.name)
121 class SsiVariableNode(Node):
122 """ Node for the SsiVariable tags. """
123 def __init__(self, tagpath, args, kwargs, patch_response=None, asvar=None):
124 self.tagpath = tagpath
127 self.patch_response = patch_response
131 return "<SsiVariableNode>"
133 def render(self, context):
134 """Renders the tag as SSI echo or sets the context variable."""
135 resolved_args = [var.resolve(context) for var in self.args]
136 resolved_kwargs = dict((k, v.resolve(context))
137 for k, v in self.kwargs.items())
138 var = SsiVariable(self.tagpath, resolved_args, resolved_kwargs)
140 request = context['request']
141 if not hasattr(request, 'ssi_vars_needed'):
142 request.ssi_vars_needed = {}
143 request.ssi_vars_needed[var.name] = var
144 if self.patch_response:
145 if not hasattr(request, 'ssi_patch_response'):
146 request.ssi_patch_response = []
147 request.ssi_patch_response.extend(self.patch_response)
150 context.dicts[0][self.asvar] = var
156 def ssi_set_statement(var, value):
157 """Generates an SSI set statement for a variable."""
158 if isinstance(value, Promise):
159 # Yes, this is quite brutal. But we need to know
160 # the real value now, we don't know the type,
161 # and we only want to evaluate the lazy function once.
162 value = value._proxy____cast()
163 if value is False or value is None:
165 return "<!--#set var='%s' value='%s'-->" % (
167 force_text(value).replace('\\', '\\\\').replace("'", "\\'"))
170 def provide_vars(request, ssi_vars):
172 Provides all the SSI set statements for ssi_vars variables.
174 The main purpose of this function is to by called by SsifyMiddleware.
176 def resolve_expects(var):
177 if not hasattr(var, 'hash_dirty'):
178 var.hash_dirty = False
180 for i, arg in enumerate(var.args):
181 if isinstance(arg, SsiExpect):
182 var.args[i] = resolved[arg.name]
183 var.hash_dirty = True
184 for k, arg in var.kwargs.items():
185 if isinstance(arg, SsiExpect):
186 var.kwargs[k] = resolved[arg.name]
187 var.hash_dirty = True
189 for arg in var.args + list(var.kwargs.values()):
190 if isinstance(arg, SsiVariable):
191 var.hash_dirty = resolve_expects(arg) or var.hash_dirty
193 hash_dirty = var.hash_dirty
195 # Rehash after calculating the SsiExpects with real
196 # values, because that's what the included views expect.
198 var.hash_dirty = False
202 def resolve_args(var):
204 for k, arg in var.kwargs.items():
205 kwargs[k] = resolved[arg.name] if isinstance(arg, SsiVariable) else arg
206 new_var = SsiVariable(var.tagpath,
207 [resolved[arg.name] if isinstance(arg, SsiVariable) else arg for arg in var.args],
212 queue = list(ssi_vars.values())
214 unresolved_streak = 0
219 rv = resolve_args(var)
220 except KeyError as e:
222 unresolved_streak += 1
223 if unresolved_streak > len(queue):
224 raise SsiVarsDependencyCycleError(request, queue, resolved)
227 resolved[var.name] = rv.get_value(request)
228 unresolved_streak = 0
230 output = "".join(ssi_set_statement(var, value)
231 for (var, value) in resolved.items()
236 from .serializers import json_encode