/
features.py
257 lines (194 loc) · 7.71 KB
/
features.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
"""Template tags for working with features."""
from __future__ import annotations
from typing import Dict, TYPE_CHECKING
from django import template
from django.template.base import (Node, NodeList, TemplateSyntaxError,
token_kwargs)
from djblets.features.registry import get_features_registry
if TYPE_CHECKING:
from django.template.base import (Context, FilterExpression, Parser,
Token)
register = template.Library()
class IfFeatureNode(Node):
"""Template node for feature-based if statements.
This works mostly like a standard ``{% if %}`` tag, checking whether the
given feature is enabled and rendering the content between it and the
else/end tags only if matching the desired state.
This supports a ``{% else %}``, to allow rendering content if the feature
does not match the desired state.
This is used by both the
:py:class:`{% if_feature_enabled %} <if_feature_enabled>` and
:py:class:`{% if_feature_disabled %} <if_feature_disabled>` tags.
"""
child_nodelists = ('nodelist_true', 'nodelist_false')
def __init__(
self,
nodelist_enabled: NodeList,
nodelist_disabled: NodeList,
feature_id: FilterExpression,
extra_kwargs: Dict
) -> None:
"""Initialize the template node.
Args:
nodelist_enabled (django.template.NodeList):
The nodelist to render if the feature is enabled.
nodelist_disabled (django.template.NodeList):
The nodelist to render if the feature is disabled.
feature_id (django.template.FilterExpression):
The template filter expression containing the ID of the
feature to check.
extra_kwargs (dict):
Extra keyword arguments to pass to
:py:meth:`Feature.is_enabled()
<djblets.features.feature.Feature.is_enabled>`.
"""
self.nodelist_enabled = nodelist_enabled
self.nodelist_disabled = nodelist_disabled
self.feature_id = feature_id
self.extra_kwargs = extra_kwargs
def __repr__(self) -> str:
"""Return a representation of the node.
This is mostly used for debugging output.
Returns:
str:
A representation of this node.
"""
return '<IfFeatureNode for %s>' % self.feature_id
def render(
self,
context: Context,
) -> str:
"""Render the node.
This will determine if the feature is enabled or disabled, and render
the appropriate list of nodes to a string.
Args:
context (django.template.Context):
The context provided by the template.
Returns:
unicode:
The rendered content as a string.
"""
feature_id = self.feature_id.resolve(context, True)
extra_kwargs = {
key: value.resolve(context)
for key, value in self.extra_kwargs.items()
}
feature = get_features_registry().get_feature(feature_id)
if feature:
enabled = feature.is_enabled(request=context.get('request'),
**extra_kwargs)
else:
enabled = False
if enabled:
return self.nodelist_enabled.render(context)
else:
return self.nodelist_disabled.render(context)
def _if_feature(
parser: Parser,
token: Token,
enabled_first: bool,
) -> IfFeatureNode:
"""Common implementation for feature-based if statements.
This constructs a :py:class:`IfFeatureNode` for the consuming template tag,
allowing for "if" and "if not" checks.
Args:
parser (django.template.Parser):
The parser being used to parse this template tag.
token (django.template.Token):
The token representing this template tag.
enabled_first (bool):
If ``True``, this behaves as an "if enabled" check.
If ``False``, this behaves as a "if disabled' check.
Returns:
IfFeatureNode:
The feature checker node to use for the template.
"""
bits = token.split_contents()
tag = bits[0]
end_tag = 'end%s' % tag
if len(bits) < 2:
raise TemplateSyntaxError('%r requires a feature ID argument'
% tag)
nodelist_1 = parser.parse(('else', end_tag))
token = parser.next_token()
if token.contents == 'else':
nodelist_2 = parser.parse((end_tag,))
parser.delete_first_token()
else:
nodelist_2 = NodeList()
if enabled_first:
nodelist_enabled = nodelist_1
nodelist_disabled = nodelist_2
else:
nodelist_disabled = nodelist_1
nodelist_enabled = nodelist_2
feature_id = parser.compile_filter(bits[1])
remaining_bits = bits[2:]
extra_kwargs = token_kwargs(remaining_bits, parser)
if remaining_bits:
raise TemplateSyntaxError('%r received an invalid token: %r'
% (tag, remaining_bits[0]))
return IfFeatureNode(nodelist_enabled, nodelist_disabled, feature_id,
extra_kwargs)
@register.tag
def if_feature_enabled(
parser: Parser,
token: Token,
) -> IfFeatureNode:
"""Render content only if a feature is enabled.
This works mostly like a standard ``{% if %}`` tag, checking if the
given feature is enabled before rendering the content between it and the
else or end tags.
This supports a ``{% else %}``, to allow rendering alternative content if
the feature is disabled instead.
It also accepts additional keyword arguments that can be passed to
:py:meth:`Feature.is_enabled()
<djblets.features.feature.Feature.is_enabled>`.
Args:
parser (django.template.Parser):
The parser being used to parse this template tag.
token (django.template.Token):
The token representing this template tag.
Returns:
IfFeatureNode:
The feature checker node to use for the template.
Example:
.. code-block:: html+django
{% if_feature_enabled "my-feature" user=request.user %}
This will only render if the feature is enabled for the user.
{% else %}
This will only render if the feature is disabled for the user.
{% endif_feature_enabled %}
"""
return _if_feature(parser, token, enabled_first=True)
@register.tag
def if_feature_disabled(
parser: Parser,
token: Token,
) -> IfFeatureNode:
"""Render content only if a feature is disabled.
This works mostly like a standard ``{% if %}`` tag, checking if the
given feature is disabled before rendering the content between it and the
else or end tags.
This supports a ``{% else %}``, to allow rendering alternative content if
the feature is enabled instead.
It also accepts additional keyword arguments that can be passed to
:py:meth:`Feature.is_enabled()
<djblets.features.feature.Feature.is_enabled>`.
Args:
parser (django.template.Parser):
The parser being used to parse this template tag.
token (django.template.Token):
The token representing this template tag.
Returns:
IfFeatureNode:
The feature checker node to use for the template.
Example:
.. code-block:: html+django
{% if_feature_disabled "my-feature" user=request.user %}
This will only render if the feature is disabled for the user.
{% else %}
This will only render if the feature is enabled for the user.
{% endif_feature_disabled %}
"""
return _if_feature(parser, token, enabled_first=False)