-
Notifications
You must be signed in to change notification settings - Fork 29
/
utils.py
282 lines (242 loc) · 9.47 KB
/
utils.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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# -*- coding: utf-8 -*-
'''template engine independent utils'''
from __future__ import absolute_import, unicode_literals
from hashlib import md5
from django.core.cache import cache
from django.utils.http import urlquote
from django.utils.translation import get_language
from lxml.etree import ParserError
from lxml.html import fragment_fromstring, tostring
from .__about__ import __version__
from .conf import settings
try:
from django.utils.six.moves.urllib import parse as urlparse
except ImportError:
from urllib import parse as urlparse
class ImproperlyConfigured(Exception):
'''django-like ImproperlyConfigured exception'''
pass
class Configuration(object):
'''abstract configuration'''
def load_configuration(self, **kwargs):
'''load configuration, merge with default settings'''
# update passed arguments with default values
for key in settings.ACTIVE_URL_KWARGS:
kwargs.setdefault(key, settings.ACTIVE_URL_KWARGS[key])
# "active" html tag css class
self.css_class = kwargs['css_class']
# "active" html tag
self.parent_tag = kwargs['parent_tag']
# flipper for menu support
self.menu = kwargs['menu']
# whether to ignore / chomp get_params
self.ignore_params = kwargs['ignore_params']
def get_cache_key(content, **kwargs):
'''generate cache key'''
cache_key = ''
for key in sorted(kwargs.keys()):
cache_key = '{cache_key}.{key}:{value}'.format(
cache_key=cache_key,
key=key,
value=kwargs[key],
)
cache_key = '{content}{cache_key}'.format(
content=content,
cache_key=cache_key,
)
# fix for non ascii symbols, ensure encoding, python3 hashlib fix
cache_key = cache_key.encode('utf-8', 'ignore')
cache_key = md5(cache_key).hexdigest()
cache_key = '{prefix}.{version}.{language}.{cache_key}'.format(
prefix=settings.ACTIVE_URL_CACHE_PREFIX,
version=__version__,
language=get_language(),
cache_key=cache_key
)
return cache_key
def yesno_to_bool(value, varname):
"""Return True/False from "yes"/"no".
:param value: template keyword argument value
:type value: string
:param varname: name of the variable, for use on exception raising
:type varname: string
:raises: :exc:`ImproperlyConfigured`
Django > 1.5 template boolean/None variables feature.
"""
if isinstance(value, bool):
if value:
value = 'yes'
else:
value = 'no'
elif value is None:
value = 'no'
# check value configuration, set boolean value
if value.lower() in ('yes', 'true'):
value = True
elif value.lower() in ('no', 'false'):
value = False
else:
raise ImproperlyConfigured(
'activeurl: malformed param value for %s' % varname
)
return value
def check_active(url, element, **kwargs):
'''check "active" url, apply css_class'''
menu = yesno_to_bool(kwargs['menu'], 'menu')
ignore_params = yesno_to_bool(kwargs['ignore_params'], 'ignore_params')
# check missing href parameter
if not url.attrib.get('href', None) is None:
# get href attribute
href = url.attrib['href'].strip()
# href="#" is often used when links shouldn't be handled by browsers.
# For example, Bootstrap uses this for expandable menus on
# small screens, see
# https://getbootstrap.com/docs/4.0/components/navs/#using-dropdowns
if href == '#':
return False
# split into urlparse object
href = urlparse.urlsplit(href)
# cut off hashtag (anchor)
href = href._replace(fragment='')
# cut off get params (?key=var&etc=var2)
if ignore_params:
href = href._replace(query='')
kwargs['full_path'] = urlparse.urlunsplit(
urlparse.urlsplit(
kwargs['full_path']
)._replace(query='')
)
# build urlparse object back into string
href = urlparse.urlunsplit(href)
# check empty href
if href == '':
# replace href with current location
href = kwargs['full_path']
# compare full_path with href according to menu configuration
if menu:
# try mark "root" (/) url as "active", in equals way
if href == '/' == kwargs['full_path']:
logic = True
# skip "root" (/) url, otherwise it will be always "active"
elif href != '/':
# start with logic
logic = (
kwargs['full_path'].startswith(href)
or
# maybe an urlquoted href was supplied
urlquote(kwargs['full_path']).startswith(href)
or
kwargs['full_path'].startswith(urlquote(href))
)
else:
logic = False
else:
# equals logic
logic = (
kwargs['full_path'] == href
or
# maybe an urlquoted href was supplied
urlquote(kwargs['full_path']) == href
or
kwargs['full_path'] == urlquote(href)
)
# "active" url found
if logic:
# check parent tag has "class" attribute or it is empty
if element.attrib.get('class'):
# prevent multiple "class" attribute adding
if kwargs['css_class'] not in element.attrib['class']:
# append "active" class
element.attrib['class'] += ' {css_class}'.format(
css_class=kwargs['css_class'],
)
else:
# create or set (if empty) "class" attribute
element.attrib['class'] = kwargs['css_class']
return True
# no "active" urls found
return False
def check_content(content, **kwargs):
'''check content for "active" urls'''
# valid html root tag
try:
# render elements tree from content
tree = fragment_fromstring(content)
# flag for prevent content rerendering, when no "active" urls found
processed = False
# django > 1.5 template boolean\None variables feature
if isinstance(kwargs['parent_tag'], bool):
if not kwargs['parent_tag']:
kwargs['parent_tag'] = 'self'
else:
raise ImproperlyConfigured('''
parent_tag=True is not allowed
''')
elif kwargs['parent_tag'] is None:
kwargs['parent_tag'] = 'self'
# if parent_tag is False\None\''\a\self
# "active" status will be applied directly to "<a>"
if kwargs['parent_tag'].lower() in ('a', 'self', ''):
# xpath query to get all "<a>"
urls = tree.xpath('.//a')
# check "active" status for all urls
for url in urls:
if check_active(url, url, **kwargs):
# mark flag for rerendering content
processed = True
# otherwise css_class must be applied to parent_tag
else:
# xpath query to get all parent tags
elements = tree.xpath('.//{parent_tag}'.format(
parent_tag=kwargs['parent_tag'],
))
# check all elements for "active" "<a>"
for element in elements:
# xpath query to get all "<a>"
urls = element.xpath('.//a')
# check "active" status for all urls
for url in urls:
if check_active(url, element, **kwargs):
# flag for rerendering content tree
processed = True
# stop checking other "<a>"
break
# do not rerender content if no "active" urls found
if processed:
# render content from tree
return tostring(tree, encoding='unicode')
# not valid html root tag
except ParserError:
# raise an exception with configuration example
raise ImproperlyConfigured('''
content of {% activeurl %} must have valid html root tag
for example
{% activeurl %}
<ul>
<li>
<a href="/page/">page</a>
</li>
<li>
<a href="/other_page/">other_page</a>
</li>
</ul>
{% endactiveurl %}
in this case <ul> is valid content root tag
''')
return content
def render_content(content, **kwargs):
'''check content for "active" urls, store results to django cache'''
# try to take pre rendered content from django cache, if caching is enabled
if settings.ACTIVE_URL_CACHE:
cache_key = get_cache_key(content, **kwargs)
# get cached content from django cache backend
from_cache = cache.get(cache_key)
# return pre rendered content if it exist in cache
if from_cache is not None:
return from_cache
# render content with "active" logic
content = check_content(content, **kwargs)
# write rendered content to django cache backend, if caching is enabled
if settings.ACTIVE_URL_CACHE:
cache.set(cache_key, content, settings.ACTIVE_URL_CACHE_TIMEOUT)
return content