Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 204 lines (178 sloc) 8.016 kB
9966d47 @chrisglass Source encoding changes for menus app
chrisglass authored
1 # -*- coding: utf-8 -*-
5c9aa1f @digi604 imports now working, show_menu displays nothing yet
digi604 authored
2 from django.conf import settings
03a78d9 @digi604 menu test cases added
digi604 authored
3 from django.contrib.sites.models import Site
76a0cd0 @digi604 menu now uses thread save cache for menu nodes
digi604 authored
4 from django.core.cache import cache
0bb503a @ojii Use import_module in menu_pool
ojii authored
5 from django.utils.importlib import import_module
6122aa2 @digi604 allows the menus to work under non i18n conditions
digi604 authored
6 from django.utils.translation import get_language
0bb503a @ojii Use import_module in menu_pool
ojii authored
7 from menus.exceptions import NamespaceAllreadyRegistered
f193c4d @chrisglass - First try to have a process-safe caching mechanism for menu trees
chrisglass authored
8 from menus.models import CacheKey
252d5ce @ojii added CMS_CACHE_PREFIX setting to prefix the menu cache keys
ojii authored
9 import copy
5c9aa1f @digi604 imports now working, show_menu displays nothing yet
digi604 authored
10
0a61389 @ojii started work on better menu invalidation
ojii authored
11 def lex_cache_key(key):
12 """
13 Returns the language and site ID a cache key is related to.
14 """
15 return key.rsplit('_', 2)[1:]
16
035cb53 @ojii Added test for mark_descendants
ojii authored
17 def _build_nodes_inner_for_one_menu(nodes, menu_class_name):
18 '''
19 This is an easier to test "inner loop" building the menu tree structure
20 for one menu (one language, one site)
21 '''
22 done_nodes = {} # Dict of node.id:Node
23 final_nodes = []
24
25 # This is to prevent infinite loops - we need to compare the number of
26 # times we see a specific node to "something", and for the time being,
27 # it's the total number of nodes
28 list_total_length = len(nodes)
29
30 while nodes:
31 # For when the node has a parent_id but we haven't seen it yet.
32 # We must not append it to the final list in this case!
33 should_add_to_final_list = True
34
35 node = nodes.pop(0)
36
37 # Increment the "seen" counter for this specific node.
38 node._counter = getattr(node,'_counter',0) + 1
39
40 # Implicit namespacing by menu.__name__
41 if not node.namespace:
42 node.namespace = menu_class_name
43 if node.namespace not in done_nodes:
44 # We need to create the namespace dict to avoid KeyErrors
45 done_nodes[node.namespace] = {}
46
47 # If we have seen the parent_id already...
48 if node.parent_id in done_nodes[node.namespace] :
49 # Implicit parent namespace by menu.__name__
50 if not node.parent_namespace:
51 node.parent_namespace = menu_class_name
52 parent = done_nodes[node.namespace][node.parent_id]
53 parent.children.append(node)
54 node.parent = parent
55 # If it has a parent_id but we haven't seen it yet...
56 elif node.parent_id:
57 # We check for infinite loops here, by comparing the number of
58 # times we "saw" this node to the number of nodes in the list
59 if node._counter < list_total_length:
60 nodes.append(node)
61 # Never add this node to the final list until it has a real
62 # parent (node.parent)
63 should_add_to_final_list = False
64
65 if should_add_to_final_list:
66 final_nodes.append(node)
67 # add it to the "seen" list
68 done_nodes[node.namespace][node.id] = node
69 return final_nodes
70
5c9aa1f @digi604 imports now working, show_menu displays nothing yet
digi604 authored
71 class MenuPool(object):
72 def __init__(self):
73 self.menus = {}
74 self.modifiers = []
75 self.discovered = False
76
77 def discover_menus(self):
78 if self.discovered:
bda49b2 @digi604 menu_levels working. nav-extenders correctly hiding if not assigned
digi604 authored
79 return
5c9aa1f @digi604 imports now working, show_menu displays nothing yet
digi604 authored
80 for app in settings.INSTALLED_APPS:
0bb503a @ojii Use import_module in menu_pool
ojii authored
81 try:
82 import_module('.menu', app)
83 except ImportError:
84 pass
bda49b2 @digi604 menu_levels working. nav-extenders correctly hiding if not assigned
digi604 authored
85 from menus.modifiers import register
86 register()
5c9aa1f @digi604 imports now working, show_menu displays nothing yet
digi604 authored
87 self.discovered = True
27b30c2 @digi604 show menu below id working
digi604 authored
88
0a61389 @ojii started work on better menu invalidation
ojii authored
89 def clear(self, site_id=None, language=None):
f193c4d @chrisglass - First try to have a process-safe caching mechanism for menu trees
chrisglass authored
90 '''
91 This invalidates the cache for a given menu (site_id and language)
92 '''
93 cache_keys = CacheKey.objects.get_keys(site_id, language)
94 to_be_deleted = [obj.key for obj in cache_keys]
252d5ce @ojii added CMS_CACHE_PREFIX setting to prefix the menu cache keys
ojii authored
95 cache.delete_many(to_be_deleted)
f193c4d @chrisglass - First try to have a process-safe caching mechanism for menu trees
chrisglass authored
96 cache_keys.delete()
5c9aa1f @digi604 imports now working, show_menu displays nothing yet
digi604 authored
97
1b16cd4 @digi604 monster commit for menu refactor
digi604 authored
98 def register_menu(self, menu):
5c9aa1f @digi604 imports now working, show_menu displays nothing yet
digi604 authored
99 from menus.base import Menu
100 assert issubclass(menu, Menu)
1b16cd4 @digi604 monster commit for menu refactor
digi604 authored
101 if menu.__name__ in self.menus.keys():
102 raise NamespaceAllreadyRegistered, "[%s] a menu with this name is already registered" % menu.__name__
103 self.menus[menu.__name__] = menu()
104
105 def register_modifier(self, modifier_class):
106 from menus.base import Modifier
107 assert issubclass(modifier_class, Modifier)
2d19d5c @digi604 all tests pass, nav extenders fully working now
digi604 authored
108 if not modifier_class in self.modifiers:
109 self.modifiers.append(modifier_class)
5c9aa1f @digi604 imports now working, show_menu displays nothing yet
digi604 authored
110
03a78d9 @digi604 menu test cases added
digi604 authored
111 def _build_nodes(self, request, site_id):
4c5a147 @chrisglass - Renumbered menu test methods (there were 2 "15"s)
chrisglass authored
112 """
113 This is slow. Caching must be used.
114 One menu is built per language and per site.
115
116 Namespaces: they are ID prefixes to avoid node ID clashes when plugging
117 multiple trees together.
118
119 - We iterate on the list of nodes.
120 - We store encountered nodes in a dict (with namespaces):
121 done_nodes[<namespace>][<node's id>] = node
122 - When a node has a parent defined, we lookup that parent in done_nodes
123 if it's found:
124 set the node as the node's parent's child (re-read this)
125 else:
126 the node is put at the bottom of the list
127 """
128 # Cache key management
6122aa2 @digi604 allows the menus to work under non i18n conditions
digi604 authored
129 lang = get_language()
5b90253 @digi604 made the menu app really standalone
digi604 authored
130 prefix = getattr(settings, "CMS_CACHE_PREFIX", "menu_cache_")
131 key = "%smenu_nodes_%s_%s" % (prefix, lang, site_id)
4c5a147 @chrisglass - Renumbered menu test methods (there were 2 "15"s)
chrisglass authored
132
76a0cd0 @digi604 menu now uses thread save cache for menu nodes
digi604 authored
133 cached_nodes = cache.get(key, None)
134 if cached_nodes:
135 return cached_nodes
4c5a147 @chrisglass - Renumbered menu test methods (there were 2 "15"s)
chrisglass authored
136
76a0cd0 @digi604 menu now uses thread save cache for menu nodes
digi604 authored
137 final_nodes = []
4c5a147 @chrisglass - Renumbered menu test methods (there were 2 "15"s)
chrisglass authored
138 for menu_class_name in self.menus:
139 nodes = self.menus[menu_class_name].get_nodes(request)
140 # nodes is a list of navigation nodes (page tree in cms + others)
035cb53 @ojii Added test for mark_descendants
ojii authored
141 final_nodes += _build_nodes_inner_for_one_menu(nodes, menu_class_name)
5b90253 @digi604 made the menu app really standalone
digi604 authored
142 duration = getattr(settings, "MENU_CACHE_DURATION", 60*60)
143 cache.set(key, final_nodes, duration)
f193c4d @chrisglass - First try to have a process-safe caching mechanism for menu trees
chrisglass authored
144 # We need to have a list of the cache keys for languages and sites that
145 # span several processes - so we follow the Django way and share through
146 # the database. It's still cheaper than recomputing every time!
147 # This way we can selectively invalidate per-site and per-language,
148 # since the cache shared but the keys aren't
149 CacheKey.objects.create(key=key, language=lang, site=site_id)
76a0cd0 @digi604 menu now uses thread save cache for menu nodes
digi604 authored
150 return final_nodes
5c9aa1f @digi604 imports now working, show_menu displays nothing yet
digi604 authored
151
bda49b2 @digi604 menu_levels working. nav-extenders correctly hiding if not assigned
digi604 authored
152 def apply_modifiers(self, nodes, request, namespace=None, root_id=None, post_cut=False, breadcrumb=False):
153 if not post_cut:
154 nodes = self._mark_selected(request, nodes)
2d19d5c @digi604 all tests pass, nav extenders fully working now
digi604 authored
155 for cls in self.modifiers:
156 inst = cls()
bda49b2 @digi604 menu_levels working. nav-extenders correctly hiding if not assigned
digi604 authored
157 nodes = inst.modify(request, nodes, namespace, root_id, post_cut, breadcrumb)
6e71206 @digi604 show menu working. marking of nodes working. modifiers working. cutle…
digi604 authored
158 return nodes
5c9aa1f @digi604 imports now working, show_menu displays nothing yet
digi604 authored
159
bda49b2 @digi604 menu_levels working. nav-extenders correctly hiding if not assigned
digi604 authored
160 def get_nodes(self, request, namespace=None, root_id=None, site_id=None, breadcrumb=False):
2d19d5c @digi604 all tests pass, nav extenders fully working now
digi604 authored
161 self.discover_menus()
03a78d9 @digi604 menu test cases added
digi604 authored
162 if not site_id:
163 site_id = Site.objects.get_current().pk
164 nodes = self._build_nodes(request, site_id)
6e71206 @digi604 show menu working. marking of nodes working. modifiers working. cutle…
digi604 authored
165 nodes = copy.deepcopy(nodes)
38225bb @digi604 testsuite working again
digi604 authored
166 nodes = self.apply_modifiers(nodes, request, namespace, root_id, post_cut=False, breadcrumb=breadcrumb)
6e71206 @digi604 show menu working. marking of nodes working. modifiers working. cutle…
digi604 authored
167 return nodes
168
03a78d9 @digi604 menu test cases added
digi604 authored
169 def _mark_selected(self, request, nodes):
65331d8 @digi604 softroots working
digi604 authored
170 sel = None
6e71206 @digi604 show menu working. marking of nodes working. modifiers working. cutle…
digi604 authored
171 for node in nodes:
03a78d9 @digi604 menu test cases added
digi604 authored
172 node.sibling = False
173 node.ancestor = False
174 node.descendant = False
65331d8 @digi604 softroots working
digi604 authored
175 node.selected = False
e0771cb @stefanfoulis invisible nodes can be selected, fixed menu and language chooser test…
stefanfoulis authored
176 if node.get_absolute_url() == request.path[:len(node.get_absolute_url())]:
65331d8 @digi604 softroots working
digi604 authored
177 if sel:
178 if len(node.get_absolute_url()) > len(sel.get_absolute_url()):
179 sel = node
180 else:
181 sel = node
6e71206 @digi604 show menu working. marking of nodes working. modifiers working. cutle…
digi604 authored
182 else:
183 node.selected = False
65331d8 @digi604 softroots working
digi604 authored
184 if sel:
185 sel.selected = True
6e71206 @digi604 show menu working. marking of nodes working. modifiers working. cutle…
digi604 authored
186 return nodes
bda49b2 @digi604 menu_levels working. nav-extenders correctly hiding if not assigned
digi604 authored
187
38225bb @digi604 testsuite working again
digi604 authored
188 def get_menus_by_attribute(self, name, value):
6967eea @digi604 apphooks working, nav extenders as well
digi604 authored
189 self.discover_menus()
38225bb @digi604 testsuite working again
digi604 authored
190 found = []
bda49b2 @digi604 menu_levels working. nav-extenders correctly hiding if not assigned
digi604 authored
191 for menu in self.menus.items():
38225bb @digi604 testsuite working again
digi604 authored
192 if hasattr(menu[1], name) and getattr(menu[1], name, None) == value:
193 found.append((menu[0], menu[1].name))
194 return found
195
196 def get_nodes_by_attribute(self, nodes, name, value):
197 found = []
198 for node in nodes:
199 if node.attr.get(name, None) == value:
200 found.append(node)
201 return found
03a78d9 @digi604 menu test cases added
digi604 authored
202
5c9aa1f @digi604 imports now working, show_menu displays nothing yet
digi604 authored
203 menu_pool = MenuPool()
Something went wrong with that request. Please try again.