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.
21 '"Todd Larsen" <tlarsen@google.com>',
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
37 """The components of a Django URL pattern.
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().
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
60 self
.kwargs
= copy
.deepcopy(kwargs
)
67 def makeDjangoUrl(self
, **extra_kwargs
):
68 """Returns a Django url() used by urlpatterns, or None if not a view.
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
85 def asIndentedStr(self
, indent
=''):
86 """Returns an indented string representation useful for logging.
89 indent: an indentation string that is prepended to each line present
90 in the multi-line string returned by this method.
92 return self
._STR
_FMT
% {'indent': indent
, 'regex': self
.regex
,
93 'view': self
.view
, 'kwargs': self
.kwargs
,
94 'name': self
.name
, 'prefix': self
.prefix
}
97 """Returns a string representation useful for logging.
99 return self
.asIndentedStr()
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.
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;
118 annotation: optional annotation associated with the menu item;
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;
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
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
140 # create ordered, unique mappings of URLs and view names to Pages
141 self
.child_by_urls
= NoOverwriteSortedDict()
142 self
.child_by_views
= NoOverwriteSortedDict()
145 short_name
= long_name
147 self
.short_name
= short_name
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():
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".
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
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...
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
):
216 def addChild(self
, page
):
217 """Adds a unique Page as a child Page of this parent.
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
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...)
245 if not isinstance(url
.name
, basestring
):
246 raise ValueError('"name" must be a string if it is supplied')
249 elif isinstance(url
.view
, basestring
):
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,
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".
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
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)
275 KeyError if the child Page could not be definitively identified in
278 # this code is yucky; there is probably a better way...
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
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
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.
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.
313 self.link_url if link_url was supplied to the __init__() constructor
314 and it is a non-False value
316 a suitable URL extracted from the url.regex, if possible
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)
325 link
= self
.url
.regex
330 if link
.startswith('^'):
333 if link
.endswith('$'):
336 if not link
.startswith('/'):
339 # path separators and already-quoted characters are OK
340 if link
!= urllib
.quote(link
, safe
='/%'):
345 def makeMenuItem(self
):
346 """Returns a menu.MenuItem for the Page (and any child Pages).
350 for child
in self
.children
:
351 child_item
= child
.makeMenuItem()
353 child_items
.append(child_item
)
356 sub_menu
= menu
.Menu(items
=child_items
)
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
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.
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()
395 urlpatterns
[self
.url
.regex
] = django_url
397 for child
in self
.children
:
398 urlpatterns
.update(child
._makeDjangoUrlsDict
())
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.
413 indent: an indentation string that is prepended to each line present
414 in the multi-line string returned by this method.
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
)
429 """Returns a string representation useful for logging.
431 return self
.asIndentedStr()
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.
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.
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
)