Skip to content

Commit ca3d93e

Browse files
committed
Merge pull request #42 from borgstrom/thread-safe
Implement thread safety
2 parents caa064a + 1b93075 commit ca3d93e

File tree

4 files changed

+132
-105
lines changed

4 files changed

+132
-105
lines changed

.travis.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ env:
1313
- DJANGO=Django==1.6
1414
- DJANGO=Django==1.7
1515
- DJANGO=Django==1.8
16+
- DJANGO=Django==1.9
1617

1718
matrix:
1819
exclude:
@@ -22,6 +23,12 @@ matrix:
2223
- python: 2.6
2324
env: DJANGO=Django==1.8
2425

26+
- python: 2.6
27+
env: DJANGO=Django==1.9
28+
29+
- python: 3.3
30+
env: DJANGO=Django==1.9
31+
2532
- python: 3.5
2633
env: DJANGO=Django==1.6
2734

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ https://django-simple-menu.readthedocs.org
5555
Requirements
5656
------------
5757

58-
Django 1.3+
58+
Django 1.6+

menu/menu.py

Lines changed: 44 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import copy
12
import re
3+
import sys
24

35
from django.conf import settings
6+
from django.utils.text import slugify
47

58
try:
69
from django.apps import apps
@@ -105,8 +108,9 @@ def process(c, request, name=None):
105108
if name not in c.items:
106109
return []
107110

111+
items = copy.deepcopy(c.items[name])
108112
curitem = None
109-
for item in c.items[name]:
113+
for item in items:
110114
item.process(request)
111115
if item.visible:
112116
item.selected = False
@@ -117,19 +121,12 @@ def process(c, request, name=None):
117121
if curitem is not None:
118122
curitem.selected = True
119123

120-
def filter_visible(items):
121-
return [
122-
filter_visible_children(item)
123-
for item in items
124-
if item.visible
125-
]
126-
127-
def filter_visible_children(item):
128-
item.children = filter_visible(item.children)
129-
return item
130-
131124
# return only visible items
132-
visible = filter_visible(c.items[name])
125+
visible = [
126+
item
127+
for item in items
128+
if item.visible
129+
]
133130

134131
# determine if we should apply 'selected' to parents when one of their
135132
# children is the 'selected' menu
@@ -176,10 +173,8 @@ def __init__(self, title, url, children=[], weight=1, check=None,
176173

177174
self.url = url
178175
self.title = title
179-
self._title = None
180176
self.visible = visible
181177
self.children = children
182-
self._children = None
183178
self.weight = weight
184179
self.check = check
185180
self.slug = slug
@@ -191,39 +186,57 @@ def __init__(self, title, url, children=[], weight=1, check=None,
191186
for k in kwargs:
192187
setattr(self, k, kwargs[k])
193188

194-
# if title is a callable store a reference to it for later
195-
# then we'll process it at runtime
196-
if callable(title):
197-
self.title = ""
198-
self._title = title
199-
200189
def process(self, request):
201190
"""
202191
process determines if this item should visible, if its selected, etc...
203192
"""
204-
self.check_check(request)
193+
# evaluate our check
194+
if callable(self.check):
195+
self.visible = self.check(request)
196+
197+
# if we're not visible we return since we don't need to do anymore processing
205198
if not self.visible:
206199
return
207200

208-
# evaluate title
209-
self.check_title(request)
201+
# evaluate our title
202+
if callable(self.title):
203+
self.title = self.title(request)
204+
205+
# if no title is set turn it into a slug
206+
if self.slug is None:
207+
# in python3 we don't need to convert to unicode, in python2 slugify
208+
# requires a unicode string
209+
if sys.version_info > (3, 0):
210+
self.slug = slugify(self.title)
211+
else:
212+
self.slug = slugify(unicode(self.title))
210213

211214
# evaluate children
212-
visible_children = []
213-
self.check_children(request)
215+
if callable(self.children):
216+
children = list(self.children(request))
217+
else:
218+
children = list(self.children)
214219

215-
for child in self.children:
220+
for child in children:
221+
child.parent = self
216222
child.process(request)
217-
if child.visible:
218-
visible_children.append(child)
219223

224+
self.children = [
225+
child
226+
for child in children
227+
if child.visible
228+
]
229+
self.children.sort(key=lambda child: child.weight)
230+
231+
# if we have no children and MENU_HIDE_EMPTY then we are not visible and should return
220232
hide_empty = getattr(settings, 'MENU_HIDE_EMPTY', False)
221-
if hide_empty and not self.check and not len(visible_children):
233+
if hide_empty and len(self.children) == 0:
222234
self.visible = False
223235
return
224236

237+
# find out if one of our children is selected, and mark it as such
225238
curitem = None
226-
for item in visible_children:
239+
for item in self.children:
227240
item.selected = False
228241

229242
if item.match_url(request):
@@ -244,36 +257,3 @@ def match_url(self, request):
244257
elif re.match("%s" % self.url, request.path):
245258
matched = True
246259
return matched
247-
248-
def check_children(self, request):
249-
"""
250-
Check children against the given request
251-
"""
252-
if callable(self._children):
253-
children = self._children(request)
254-
elif callable(self.children):
255-
children = self.children(request)
256-
self._children = self.children
257-
else:
258-
children = self.children
259-
260-
children = [child for child in children]
261-
children.sort(key=lambda child: child.weight)
262-
for child in children:
263-
child.parent = self
264-
265-
self.children = children
266-
267-
def check_check(self, request):
268-
"""
269-
Set our visibility based on our check against the given request
270-
"""
271-
if callable(self.check):
272-
self.visible = self.check(request)
273-
274-
def check_title(self, request):
275-
if callable(self._title):
276-
self.title = self._title(request)
277-
if self.slug is None:
278-
self.slug = re.sub(r'[^a-zA-Z0-9\-]+', '_',
279-
self.title.lower()).strip('_')

menu/tests/test_menu.py

Lines changed: 80 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
import signal
2+
import threading
13
import unittest
4+
import sys
5+
6+
if sys.version_info > (3, 0):
7+
from queue import Queue
8+
else:
9+
from Queue import Queue
210

311
from django.conf import settings
412
from django.template import Template, Context
@@ -8,7 +16,6 @@
816
from menu import Menu, MenuItem
917

1018
# XXX TODO: test MENU_HIDE_EMPTY
11-
# XXX TODO: test check_children
1219

1320
class MenuTests(TestCase):
1421
"""
@@ -52,7 +59,7 @@ def kids3_1(request):
5259
]
5360

5461
kids3 = (
55-
MenuItem("kids3-1", "/parent3/kids3-1", children=kids3_1),
62+
MenuItem("kids3-1", "/parent3/kids3-1", children=kids3_1, slug="salty"),
5663
MenuItem(kids3_2_title, "/parent3/kids3-2")
5764
)
5865

@@ -68,74 +75,107 @@ def kids3_1(request):
6875

6976
self.factory = RequestFactory()
7077

78+
def test_thread_safety_and_checks(self):
79+
"""
80+
Ensure our thread safety works, this also ensures our checks work
81+
"""
82+
# this shouldn't ever take more than 5 seconds, add a safety in case someting breaks
83+
signal.alarm(5)
84+
85+
def t1(results):
86+
"Closure for thread 1"
87+
request = self.factory.get('/kids2-2/visible')
88+
items = Menu.process(request, 'test')
89+
results.put_nowait(len(items[0].children) == 2)
90+
91+
def t2(results):
92+
"Closure for thread 2"
93+
request = self.factory.get('/kids2-2/hidden')
94+
items = Menu.process(request, 'test')
95+
results.put_nowait(len(items[0].children) == 1)
96+
97+
results = Queue()
98+
for _ in range(50):
99+
threads = [
100+
threading.Thread(target=t1, args=(results,)),
101+
threading.Thread(target=t2, args=(results,))
102+
]
103+
for thread in threads:
104+
thread.start()
105+
for thread in threads:
106+
thread.join()
107+
108+
109+
self.assertTrue(all([
110+
results.get()
111+
for _ in range(100)
112+
]))
113+
114+
def test_slug(self):
115+
"""
116+
Ensure our slugification works as expected
117+
"""
118+
request = self.factory.get('/parent3/kids3-1')
119+
items = Menu.process(request, 'test')
120+
self.assertEqual(items[1].slug, "parent-3")
121+
self.assertEqual(items[1].children[0].slug, "salty")
122+
71123
def test_exact_url(self):
72124
"""
73125
Ensure that the exact_url setting works
74126
"""
75127
# the extra stuff will still cause kids3-2 to be selected
76128
request = self.factory.get('/parent3/kids3-2/extra_stuff_here')
77-
Menu.process(request, 'test')
78-
self.assertEqual(Menu.items['test'][1].children[1].selected, True)
129+
items = Menu.process(request, 'test')
130+
self.assertEqual(items[1].children[1].selected, True)
79131

80132
# but here it won't, because exact_url is set
81133
request = self.factory.get('/parent3/kids3-1/kid1/extra_stuff_here')
82-
Menu.process(request, 'test')
83-
self.assertEqual(Menu.items['test'][1].children[0].children[0].selected, False)
134+
items = Menu.process(request, 'test')
135+
self.assertEqual(items[1].children[0].children[0].selected, False)
84136

85137
def test_callable_title(self):
86138
"""
87139
Ensure callable titles work
88140
"""
89141
self.kids3_2_desired_title = "fun"
90142
request = self.factory.get('/parent3')
91-
Menu.process(request, 'test')
92-
self.assertEqual(Menu.items['test'][1].children[1].title, "/parent3-fun")
93-
94-
def test_checks(self):
95-
"""
96-
Ensure checks on menus work
97-
"""
98-
request = self.factory.get('/kids2-2/visible')
99-
Menu.process(request, 'test')
100-
self.assertEqual(len(Menu.items['test'][0].children), 2)
101-
102-
request = self.factory.get('/kids2-2/hidden')
103-
Menu.process(request, 'test')
104-
self.assertEqual(len(Menu.items['test'][0].children), 1)
143+
items = Menu.process(request, 'test')
144+
self.assertEqual(items[1].children[1].title, "/parent3-fun")
105145

106146
def test_select_parents(self):
107147
"""
108148
Ensure the MENU_SELECT_PARENTS setting works
109149
"""
110150
settings.MENU_SELECT_PARENTS = False
111151
request = self.factory.get('/parent2/kids2-1')
112-
Menu.process(request, 'test')
113-
self.assertEqual(Menu.items['test'][0].selected, True)
114-
self.assertEqual(Menu.items['test'][0].children[1].selected, True)
115-
self.assertEqual(Menu.items['test'][1].selected, False)
152+
items = Menu.process(request, 'test')
153+
self.assertEqual(items[0].selected, True)
154+
self.assertEqual(items[0].children[1].selected, True)
155+
self.assertEqual(items[1].selected, False)
116156

117157
request = self.factory.get('/kids2-2')
118-
Menu.process(request, 'test')
119-
self.assertEqual(Menu.items['test'][0].selected, False)
120-
self.assertEqual(Menu.items['test'][0].children[0].selected, True)
121-
self.assertEqual(Menu.items['test'][1].selected, False)
158+
items = Menu.process(request, 'test')
159+
self.assertEqual(items[0].selected, False)
160+
self.assertEqual(items[0].children[0].selected, True)
161+
self.assertEqual(items[1].selected, False)
122162

123163
settings.MENU_SELECT_PARENTS = True
124164
request = self.factory.get('/kids2-2')
125-
Menu.process(request, 'test')
126-
self.assertEqual(Menu.items['test'][0].selected, True)
127-
self.assertEqual(Menu.items['test'][0].children[0].selected, True)
128-
self.assertEqual(Menu.items['test'][1].selected, False)
165+
items = Menu.process(request, 'test')
166+
self.assertEqual(items[0].selected, True)
167+
self.assertEqual(items[0].children[0].selected, True)
168+
self.assertEqual(items[1].selected, False)
129169

130170
request = self.factory.get('/parent3/kids3-1/kid1')
131-
Menu.process(request, 'test')
132-
self.assertEqual(Menu.items['test'][0].selected, False)
133-
self.assertEqual(Menu.items['test'][0].children[1].selected, False)
134-
self.assertEqual(Menu.items['test'][1].selected, True)
135-
self.assertEqual(Menu.items['test'][1].children[0].selected, True)
136-
self.assertEqual(Menu.items['test'][1].children[0].children[0].selected, True)
137-
self.assertEqual(Menu.items['test'][1].children[1].selected, False)
138-
self.assertEqual(Menu.items['test'][2].selected, False)
171+
items = Menu.process(request, 'test')
172+
self.assertEqual(items[0].selected, False)
173+
self.assertEqual(items[0].children[1].selected, False)
174+
self.assertEqual(items[1].selected, True)
175+
self.assertEqual(items[1].children[0].selected, True)
176+
self.assertEqual(items[1].children[0].children[0].selected, True)
177+
self.assertEqual(items[1].children[1].selected, False)
178+
self.assertEqual(items[2].selected, False)
139179

140180
def test_template_tag(self):
141181
"""

0 commit comments

Comments
 (0)