Remove unused imports, fix too long lines and indentions.
[Melange.git] / app / soc / logic / site / page.py
blobdbb027827cc256e733a758e757b5fdd8a1d24614
1 #!/usr/bin/python2.5
3 # Copyright 2008 the Melange authors.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Page properties, used to generate sidebar menus, urlpatterns, etc.
18 """
20 __authors__ = [
21 '"Todd Larsen" <tlarsen@google.com>',
25 import copy
26 import re
28 from django.conf.urls import defaults
30 from python25src import urllib
32 from soc.logic import menu
33 from soc.logic.no_overwrite_sorted_dict import NoOverwriteSortedDict
36 class Url:
37 """The components of a Django URL pattern.
38 """
40 def __init__(self, regex, view, kwargs=None, name=None, prefix=''):
41 """Collects Django urlpatterns info into a simple object.
43 The arguments to this constructor correspond directly to the items in
44 the urlpatterns tuple, which also correspond to the parameters of
45 django.conf.urls.defaults.url().
47 Args:
48 regex: a Django URL regex pattern
49 view: a Django view, either a string or a callable
50 kwargs: optional dict of extra arguments passed to the view
51 function as keyword arguments, which is copy.deepcopy()'d;
52 default is None, which supplies an empty dict {}
53 name: optional name of the view
54 prefix: optional view prefix
55 """
56 self.regex = regex
57 self.view = view
59 if kwargs:
60 self.kwargs = copy.deepcopy(kwargs)
61 else:
62 self.kwargs = {}
64 self.name = name
65 self.prefix = prefix
67 def makeDjangoUrl(self, **extra_kwargs):
68 """Returns a Django url() used by urlpatterns, or None if not a view.
69 """
70 if not self.view:
71 return None
73 kwargs = copy.deepcopy(self.kwargs)
74 kwargs.update(extra_kwargs)
75 return defaults.url(self.regex, self.view, kwargs=kwargs,
76 name=self.name, prefix=self.prefix)
78 _STR_FMT = '''%(indent)sregex: %(regex)s
79 %(indent)sview: %(view)s
80 %(indent)skwargs: %(kwargs)s
81 %(indent)sname: %(name)s
82 %(indent)sprefix: %(prefix)s
83 '''
85 def asIndentedStr(self, indent=''):
86 """Returns an indented string representation useful for logging.
88 Args:
89 indent: an indentation string that is prepended to each line present
90 in the multi-line string returned by this method.
91 """
92 return self._STR_FMT % {'indent': indent, 'regex': self.regex,
93 'view': self.view, 'kwargs': self.kwargs,
94 'name': self.name, 'prefix': self.prefix}
96 def __str__(self):
97 """Returns a string representation useful for logging.
98 """
99 return self.asIndentedStr()
102 class Page:
103 """An abstraction that combines a Django view with sidebar menu info.
106 def __init__(self, url, long_name, short_name=None, selected=False,
107 annotation=None, parent=None, link_url=None,
108 in_breadcrumb=True, force_in_menu=False):
109 """Initializes the menu item attributes from supplied arguments.
111 Args:
112 url: a Url object
113 long_name: title of the Page
114 short_name: optional menu item and breadcrumb name; default is
115 None, in which case long_name is used
116 selected: Boolean indicating if this menu item is selected;
117 default is False
118 annotation: optional annotation associated with the menu item;
119 default is None
120 parent: optional Page that is the logical "parent" of this page;
121 used to construct hierarchical menus and breadcrumb trails
122 link_url: optional alternate URL link; if supplied, it is returned
123 by makeLinkUrl(); default is None, and makeLinkUrl() attempts to
124 create a URL link from url.regex
125 in_breadcrumb: if True, the Page appears in breadcrumb trails;
126 default is True
127 force_in_menu: if True, the Page appears in menus even if it does
128 not have a usable link_url; default is False, which excludes
129 the Page if makeLinkUrl() returns None
131 self.url = url
132 self.long_name = long_name
133 self.annotation = annotation
134 self.in_breadcrumb = in_breadcrumb
135 self.force_in_menu = force_in_menu
136 self.link_url = link_url
137 self.selected = selected
138 self.parent = parent
140 # create ordered, unique mappings of URLs and view names to Pages
141 self.child_by_urls = NoOverwriteSortedDict()
142 self.child_by_views = NoOverwriteSortedDict()
144 if not short_name:
145 short_name = long_name
147 self.short_name = short_name
149 if parent:
150 # tell parent Page about parent <- child relationship
151 parent.addChild(self)
153 # TODO(tlarsen): build some sort of global Page dictionary to detect
154 # collisions sooner and to make global queries from URLs to pages
155 # and views to Pages possible (without requiring a recursive search)
157 def getChildren(self):
158 """Returns an iterator over any child Pages
160 for page in self.child_by_views.itervalues():
161 yield page
163 children = property(getChildren)
165 def getChild(self, url=None, regex=None, view=None,
166 name=None, prefix=None, request_path=None):
167 """Returns a child Page if one can be identified; or None otherwise.
169 All of the parameters to this method are optional, but at least one
170 must be supplied to return anything other than None. The parameters
171 are tried in the order they are listed in the "Args:" section, and
172 this method exits on the first "match".
174 Args:
175 url: a Url object, used to overwrite the regex, view, name, and
176 prefix parameters if present; default is None
177 regex: a regex pattern string, used to return the associated
178 child Page
179 view: a view string, used to return the associated child Page
180 name: a name string, used to return the associated child Page
181 prefix: (currently unused, see TODO below in code)
182 request_path: optional HTTP request path string (request.path)
183 with no query arguments
185 # this code is yucky; there is probably a better way...
186 if url:
187 regex = url.regex
188 view = url.view
189 name = url.name
190 prefix = url.prefix
192 if regex in self.child_by_urls:
193 # regex supplied and Page found, so return that Page
194 return self.child_by_urls[regex][0]
196 # TODO(tlarsen): make this work correctly with prefixes
198 if view in self.child_views:
199 # view supplied and Page found, so return that Page
200 return self.child_by_views[view]
202 if name in self.child_views:
203 # name supplied and Page found, so return that Page
204 return self.child_by_views[name]
206 if request_path.startswith('/'):
207 request_path = request_path[1:]
209 # attempt to match the HTTP request path with a Django URL pattern
210 for pattern, (page, regex) in self.child_by_urls:
211 if regex.match(request_path):
212 return page
214 return None
216 def addChild(self, page):
217 """Adds a unique Page as a child Page of this parent.
219 Raises:
220 ValueError if page.url.regex is not a string.
221 ValueError if page.url.view is not a string.
222 ValueError if page.url.name is supplied but is not a string.
223 KeyError if page.url.regex is already associated with another Page.
224 KeyError if page.url.view/name is already associated with another Page.
226 # TODO(tlarsen): see also TODO in __init__() about global Page dictionary
228 url = page.url
230 if url.regex:
231 if not isinstance(url.regex, basestring):
232 raise ValueError('"regex" must be a string, not a compiled regex')
234 # TODO(tlarsen): see if Django has some way exposed in its API to get
235 # the view name from the request path matched against urlpatterns;
236 # if so, there would be no need for child_by_urls, because the
237 # request path could be converted for us by Django into a view/name,
238 # and we could just use child_by_views with that string instead
239 self.child_by_urls[url.regex] = (page, re.compile(url.regex))
240 # else: NonUrl does not get indexed by regex, because it has none
242 # TODO(tlarsen): make this work correctly if url has a prefix
243 # (not sure how to make this work with include() views...)
244 if url.name:
245 if not isinstance(url.name, basestring):
246 raise ValueError('"name" must be a string if it is supplied')
248 view = url.name
249 elif isinstance(url.view, basestring):
250 view = url.view
251 else:
252 raise ValueError('"view" must be a string if "name" is not supplied')
254 self.child_by_views[view] = page
256 def delChild(self, url=None, regex=None, view=None, name=None,
257 prefix=None):
258 """Removes a child Page if one can be identified.
260 All of the parameters to this method are optional, but at least one
261 must be supplied in order to remove a child Page. The parameters
262 are tried in the order they are listed in the "Args:" section, and
263 this method uses the first "match".
265 Args:
266 url: a Url object, used to overwrite the regex, view, name, and
267 prefix parameters if present; default is None
268 regex: a regex pattern string, used to remove the associated
269 child Page
270 view: a view string, used to remove the associated child Page
271 name: a name string, used to remove the associated child Page
272 prefix: (currently unused, see TODO below in code)
274 Raises:
275 KeyError if the child Page could not be definitively identified in
276 order to delete it.
278 # this code is yucky; there is probably a better way...
279 if url:
280 regex = url.regex
281 view = url.view
282 name = url.name
283 prefix = url.prefix
285 # try to find page by regex, view, or name, in turn
286 if regex in self.child_by_urls:
287 url = self.child_by_urls[regex][0].url
288 view = url.view
289 name = url.name
290 prefix = url.prefix
291 elif view in self.child_views:
292 # TODO(tlarsen): make this work correctly with prefixes
293 regex = self.child_by_views[view].url.regex
294 elif name in self.child_views:
295 regex = self.child_by_views[name].url.regex
297 if regex:
298 # regex must refer to an existing Page at this point
299 del self.child_urls[regex]
301 if not isinstance(view, basestring):
302 # use name if view is callable() or None, etc.
303 view = name
305 # TODO(tlarsen): make this work correctly with prefixes
306 del self.child_by_views[view]
309 def makeLinkUrl(self):
310 """Makes a URL link suitable for <A HREF> use.
312 Returns:
313 self.link_url if link_url was supplied to the __init__() constructor
314 and it is a non-False value
315 -OR-
316 a suitable URL extracted from the url.regex, if possible
317 -OR-
318 None if url.regex contains quotable characters that have not already
319 been quoted (that is, % is left untouched, so quote suspect
320 characters in url.regex that would otherwise be quoted)
322 if self.link_url:
323 return self.link_url
325 link = self.url.regex
327 if not link:
328 return None
330 if link.startswith('^'):
331 link = link[1:]
333 if link.endswith('$'):
334 link = link[:-1]
336 if not link.startswith('/'):
337 link = '/' + link
339 # path separators and already-quoted characters are OK
340 if link != urllib.quote(link, safe='/%'):
341 return None
343 return link
345 def makeMenuItem(self):
346 """Returns a menu.MenuItem for the Page (and any child Pages).
348 child_items = []
350 for child in self.children:
351 child_item = child.makeMenuItem()
352 if child_item:
353 child_items.append(child_item)
355 if child_items:
356 sub_menu = menu.Menu(items=child_items)
357 else:
358 sub_menu = None
360 link_url = self.makeLinkUrl()
362 if (not sub_menu) and (not link_url) and (not self.force_in_menu):
363 # no sub-menu, no valid link URL, and not forced to be in menu
364 return None
366 return menu.MenuItem(
367 self.short_name, value=link_url, sub_menu=sub_menu)
369 def makeDjangoUrl(self):
370 """Returns the Django url() for the underlying self.url.
372 return self.url.makeDjangoUrl(page=self)
374 def makeDjangoUrls(self):
375 """Returns an ordered mapping of unique Django url() objects.
377 Raises:
378 KeyError if more than one Page has the same urlpattern.
380 TODO(tlarsen): this really needs to be detected earlier via a
381 global Page dictionary
383 return self._makeDjangoUrlsDict().values()
385 def _makeDjangoUrlsDict(self):
386 """Returns an ordered mapping of unique Django url() objects.
388 Used to implement makeDjangoUrls(). See that method for details.
390 urlpatterns = NoOverwriteSortedDict()
392 django_url = self.makeDjangoUrl()
394 if django_url:
395 urlpatterns[self.url.regex] = django_url
397 for child in self.children:
398 urlpatterns.update(child._makeDjangoUrlsDict())
400 return urlpatterns
402 _STR_FMT = '''%(indent)slong_name: %(long_name)s
403 %(indent)sshort_name: %(short_name)s
404 %(indent)sselected: %(selected)s
405 %(indent)sannotation: %(annotation)s
406 %(indent)surl: %(url)s
409 def asIndentedStr(self, indent=''):
410 """Returns an indented string representation useful for logging.
412 Args:
413 indent: an indentation string that is prepended to each line present
414 in the multi-line string returned by this method.
416 strings = [
417 self._STR_FMT % {'indent': indent, 'long_name': self.long_name,
418 'short_name': self.short_name,
419 'selected': self.selected,
420 'annotation': self.annotation,
421 'url': self.url.asIndentedStr(indent + ' ')}]
423 for child in self.children:
424 strings.extend(child.asIndentedStr(indent + ' '))
426 return ''.join(strings)
428 def __str__(self):
429 """Returns a string representation useful for logging.
431 return self.asIndentedStr()
434 class NonUrl(Url):
435 """Placeholder for when a site-map entry is not a linkable URL.
438 def __init__(self, name):
439 """Creates a non-linkable Url placeholder.
441 Args:
442 name: name of the non-view placeholder
444 Url.__init__(self, None, None, name=name)
446 def makeDjangoUrl(self, **extra_kwargs):
447 """Always returns None, since NonUrl is never a Django view.
449 return None
452 class NonPage(Page):
453 """Placeholder for when a site-map entry is not a displayable page.
456 def __init__(self, non_url_name, long_name, **page_kwargs):
459 non_url = NonUrl(non_url_name)
460 Page.__init__(self, non_url, long_name, **page_kwargs)