app/soc/logic/site/page.py
changeset 281 7caa8951cbc9
child 372 8595c1129c74
equal deleted inserted replaced
280:ce9b10bbdd42 281:7caa8951cbc9
       
     1 #!/usr/bin/python2.5
       
     2 #
       
     3 # Copyright 2008 the Melange authors.
       
     4 #
       
     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
       
     8 #
       
     9 #   http://www.apache.org/licenses/LICENSE-2.0
       
    10 #
       
    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.
       
    16 
       
    17 """Page properties, used to generate sidebar menus, urlpatterns, etc.
       
    18 """
       
    19 
       
    20 __authors__ = [
       
    21   '"Todd Larsen" <tlarsen@google.com>',
       
    22   ]
       
    23 
       
    24 
       
    25 import copy
       
    26 import re
       
    27 
       
    28 from google.appengine.api import users
       
    29 
       
    30 from django.conf.urls import defaults
       
    31 
       
    32 from python25src import urllib
       
    33 
       
    34 from soc.logic import menu
       
    35 from soc.logic.no_overwrite_sorted_dict import NoOverwriteSortedDict
       
    36 
       
    37 
       
    38 class Url:
       
    39   """The components of a Django URL pattern.
       
    40   """
       
    41    
       
    42   def __init__(self, regex, view, kwargs=None, name=None, prefix=''):
       
    43     """Collects Django urlpatterns info into a simple object.
       
    44     
       
    45     The arguments to this constructor correspond directly to the items in
       
    46     the urlpatterns tuple, which also correspond to the parameters of
       
    47     django.conf.urls.defaults.url().
       
    48 
       
    49     Args:
       
    50       regex: a Django URL regex pattern
       
    51       view: a Django view, either a string or a callable
       
    52       kwargs: optional dict of extra arguments passed to the view
       
    53         function as keyword arguments, which is copy.deepcopy()'d;
       
    54         default is None, which supplies an empty dict {}
       
    55       name: optional name of the view
       
    56       prefix: optional view prefix
       
    57     """
       
    58     self.regex = regex
       
    59     self.view = view
       
    60     
       
    61     if kwargs:
       
    62       self.kwargs = copy.deepcopy(kwargs)
       
    63     else:
       
    64       self.kwargs = {}
       
    65 
       
    66     self.name = name
       
    67     self.prefix = prefix
       
    68 
       
    69   def makeDjangoUrl(self):
       
    70     """Returns a Django url() used by urlpatterns.
       
    71     """
       
    72     return defaults.url(self.regex, self.view, kwargs=self.kwargs,
       
    73                         name=self.name, prefix=self.prefix)
       
    74 
       
    75   _STR_FMT = '''%(indent)sregex: %(regex)s
       
    76 %(indent)sview: %(view)s
       
    77 %(indent)skwargs: %(kwargs)s
       
    78 %(indent)sname: %(name)s
       
    79 %(indent)sprefix: %(prefix)s
       
    80 '''
       
    81 
       
    82   def asIndentedStr(self, indent=''):
       
    83     """Returns an indented string representation useful for logging.
       
    84     
       
    85     Args:
       
    86       indent: an indentation string that is prepended to each line present
       
    87         in the multi-line string returned by this method.
       
    88     """
       
    89     return self._STR_FMT % {'indent': indent, 'regex': self.regex,
       
    90                             'view': self.view, 'kwargs': self.kwargs,
       
    91                             'name': self.name, 'prefix': self.prefix}
       
    92 
       
    93   def __str__(self):
       
    94     """Returns a string representation useful for logging.
       
    95     """
       
    96     return self.asIndentedStr()
       
    97 
       
    98 
       
    99 class Page:
       
   100   """An abstraction that combines a Django view with sidebar menu info.
       
   101   """
       
   102   
       
   103   def __init__(self, url, long_name, short_name=None, selected=False,
       
   104                annotation=None, parent=None, link_url=None,
       
   105                in_breadcrumb=True, force_in_menu=False):
       
   106     """Initializes the menu item attributes from supplied arguments.
       
   107     
       
   108     Args:
       
   109       url: a Url object
       
   110       long_name: title of the Page
       
   111       short_name: optional menu item and breadcrumb name; default is
       
   112         None, in which case long_name is used
       
   113       selected: Boolean indicating if this menu item is selected;
       
   114         default is False
       
   115       annotation: optional annotation associated with the menu item;
       
   116         default is None
       
   117       parent: optional Page that is the logical "parent" of this page;
       
   118         used to construct hierarchical menus and breadcrumb trails
       
   119       link_url: optional alternate URL link; if supplied, it is returned
       
   120         by makeLinkUrl(); default is None, and makeLinkUrl() attempts to
       
   121         create a URL link from url.regex 
       
   122       in_breadcrumb: if True, the Page appears in breadcrumb trails;
       
   123         default is True
       
   124       force_in_menu: if True, the Page appears in menus even if it does
       
   125         not have a usable link_url; default is False, which excludes
       
   126         the Page if makeLinkUrl() returns None
       
   127     """
       
   128     self.url = url
       
   129     self.long_name = long_name
       
   130     self.annotation = annotation
       
   131     self.in_breadcrumb = in_breadcrumb
       
   132     self.force_in_menu = force_in_menu
       
   133     self.link_url = link_url
       
   134     self.selected = selected
       
   135     self.parent = parent
       
   136 
       
   137     # create ordered, unique mappings of URLs and view names to Pages
       
   138     self.child_by_urls = NoOverwriteSortedDict()
       
   139     self.child_by_views = NoOverwriteSortedDict()
       
   140     
       
   141     if not short_name:
       
   142       short_name = long_name
       
   143       
       
   144     self.short_name = short_name
       
   145     
       
   146     if parent:
       
   147       # tell parent Page about parent <- child relationship
       
   148       parent.addChild(self)
       
   149       
       
   150     # TODO(tlarsen): build some sort of global Page dictionary to detect
       
   151     #   collisions sooner and to make global queries from URLs to pages
       
   152     #   and views to Pages possible (without requiring a recursive search)
       
   153 
       
   154   def getChildren(self):
       
   155     """Returns an iterator over any child Pages 
       
   156     """
       
   157     for page, _ in self.child_by_urls.itervalues():
       
   158       yield page
       
   159 
       
   160   children = property(getChildren)
       
   161 
       
   162   def getChild(self, url=None, regex=None, view=None,
       
   163                name=None, prefix=None, request_path=None):
       
   164     """Returns a child Page if one can be identified; or None otherwise.
       
   165     
       
   166     All of the parameters to this method are optional, but at least one
       
   167     must be supplied to return anything other than None.  The parameters
       
   168     are tried in the order they are listed in the "Args:" section, and
       
   169     this method exits on the first "match".
       
   170     
       
   171     Args:
       
   172       url: a Url object, used to overwrite the regex, view, name, and
       
   173         prefix parameters if present; default is None
       
   174       regex: a regex pattern string, used to return the associated
       
   175         child Page
       
   176       view: a view string, used to return the associated child Page
       
   177       name: a name string, used to return the associated child Page
       
   178       prefix: (currently unused, see TODO below in code)
       
   179       request_path: optional HTTP request path string (request.path)
       
   180         with no query arguments
       
   181     """
       
   182     # this code is yucky; there is probably a better way...
       
   183     if url:
       
   184       regex = url.regex
       
   185       view = url.view
       
   186       name = url.name
       
   187       prefix = url.prefix
       
   188 
       
   189     if regex in self.child_by_urls:
       
   190       # regex supplied and Page found, so return that Page
       
   191       return self.child_by_urls[regex][0]
       
   192   
       
   193     # TODO(tlarsen): make this work correctly with prefixes
       
   194 
       
   195     if view in self.child_views:
       
   196       # view supplied and Page found, so return that Page
       
   197       return self.child_by_views[view]
       
   198   
       
   199     if name in self.child_views:
       
   200       # name supplied and Page found, so return that Page
       
   201       return self.child_by_views[name]
       
   202 
       
   203     if request_path.startswith('/'):
       
   204       request_path = request_path[1:]
       
   205 
       
   206     # attempt to match the HTTP request path with a Django URL pattern
       
   207     for pattern, (page, regex) in self.child_by_urls:
       
   208       if regex.match(request_path):
       
   209         return page
       
   210 
       
   211     return None
       
   212 
       
   213   def addChild(self, page):
       
   214     """Adds a unique Page as a child Page of this parent.
       
   215     
       
   216     Raises:
       
   217       ValueError if page.url.regex is not a string.
       
   218       ValueError if page.url.view is not a string.
       
   219       ValueError if page.url.name is supplied but is not a string.
       
   220       KeyError if page.url.regex is already associated with another Page.
       
   221       KeyError if page.url.view/name is already associated with another Page.
       
   222     """
       
   223     # TODO(tlarsen): see also TODO in __init__() about global Page dictionary
       
   224 
       
   225     url = page.url
       
   226     
       
   227     if not isinstance(url.regex, basestring):
       
   228       raise ValueError('"regex" must be a string, not a compiled regex')
       
   229 
       
   230     # TODO(tlarsen): see if Django has some way exposed in its API to get
       
   231     #   the view name from the request path matched against urlpatterns;
       
   232     #   if so, there would be no need for child_by_urls, because the
       
   233     #   request path could be converted for us by Django into a view/name,
       
   234     #   and we could just use child_by_views with that string instead
       
   235     self.child_by_urls[url.regex] = (page, re.compile(url.regex))
       
   236 
       
   237     # TODO(tlarsen): make this work correctly if url has a prefix
       
   238     #   (not sure how to make this work with include() views...)
       
   239     if url.name:
       
   240       if not isinstance(url.name, basestring):
       
   241         raise ValueError('"name" must be a string if it is supplied')
       
   242 
       
   243       view = url.name
       
   244     elif isinstance(url.view, basestring):
       
   245       view = url.view
       
   246     else:
       
   247       raise ValueError('"view" must be a string if "name" is not supplied')
       
   248 
       
   249     self.child_by_views[view] = page
       
   250 
       
   251   def delChild(self, url=None, regex=None, view=None, name=None,
       
   252                prefix=None):
       
   253     """Removes a child Page if one can be identified.
       
   254     
       
   255     All of the parameters to this method are optional, but at least one
       
   256     must be supplied in order to remove a child Page.  The parameters
       
   257     are tried in the order they are listed in the "Args:" section, and
       
   258     this method uses the first "match".
       
   259     
       
   260     Args:
       
   261       url: a Url object, used to overwrite the regex, view, name, and
       
   262         prefix parameters if present; default is None
       
   263       regex: a regex pattern string, used to remove the associated
       
   264         child Page
       
   265       view: a view string, used to remove the associated child Page
       
   266       name: a name string, used to remove the associated child Page
       
   267       prefix: (currently unused, see TODO below in code)
       
   268       
       
   269     Raises:
       
   270       KeyError if the child Page could not be definitively identified in
       
   271       order to delete it.
       
   272     """
       
   273     # this code is yucky; there is probably a better way...
       
   274     if url:
       
   275       regex = url.regex
       
   276       view = url.view
       
   277       name = url.name
       
   278       prefix = url.prefix
       
   279 
       
   280     # try to find page by regex, view, or name, in turn
       
   281     if regex in self.child_by_urls:
       
   282       url = self.child_by_urls[regex][0].url
       
   283       view = url.view
       
   284       name = url.name
       
   285       prefix = url.prefix
       
   286     elif view in self.child_views:
       
   287       # TODO(tlarsen): make this work correctly with prefixes
       
   288       regex = self.child_by_views[view].url.regex
       
   289     elif name in self.child_views:
       
   290       regex = self.child_by_views[name].url.regex
       
   291 
       
   292     # regex must refer to an existing Page at this point
       
   293     del self.child_urls[regex]
       
   294 
       
   295     if not isinstance(view, basestring):
       
   296       # use name if view is callable() or None, etc.
       
   297       view = name
       
   298 
       
   299     # TODO(tlarsen): make this work correctly with prefixes
       
   300     del self.child_by_views[view]
       
   301     
       
   302 
       
   303   def makeLinkUrl(self):
       
   304     """Makes a URL link suitable for <A HREF> use.
       
   305     
       
   306     Returns:
       
   307       self.link_url if link_url was supplied to the __init__() constructor
       
   308         and it is a non-False value
       
   309        -OR-
       
   310       a suitable URL extracted from the url.regex, if possible
       
   311        -OR-
       
   312       None if url.regex contains quotable characters that have not already
       
   313         been quoted (that is, % is left untouched, so quote suspect
       
   314         characters in url.regex that would otherwise be quoted)
       
   315     """
       
   316     if self.link_url:
       
   317       return self.link_url
       
   318 
       
   319     link = self.url.regex
       
   320     
       
   321     if link.startswith('^'):
       
   322       link = link[1:]
       
   323     
       
   324     if link.endswith('$'):
       
   325       link = link[:-1]
       
   326 
       
   327     if not link.startswith('/'):
       
   328       link = '/' + link
       
   329     
       
   330     # path separators and already-quoted characters are OK
       
   331     if link != urllib.quote(link, safe='/%'):
       
   332       return None
       
   333 
       
   334     return link
       
   335 
       
   336   def makeMenuItem(self):
       
   337     """Returns a menu.MenuItem for the Page (and any child Pages).
       
   338     """
       
   339     child_items = []
       
   340     
       
   341     for child in self.children:
       
   342       child_item = child.makeMenuItem()
       
   343       if child_item:
       
   344         child_items.append(child_item)
       
   345     
       
   346     if child_items:
       
   347       sub_menu = menu.Menu(items=child_items)
       
   348     else:
       
   349       sub_menu = None
       
   350     
       
   351     link_url = self.makeLinkUrl()
       
   352     
       
   353     if (not sub_menu) and (not link_url) and (not self.force_in_menu):
       
   354       # no sub-menu, no valid link URL, and not forced to be in menu
       
   355       return None
       
   356     
       
   357     return menu.MenuItem(
       
   358       self.short_name, value=link_url, sub_menu=sub_menu)
       
   359 
       
   360   def makeDjangoUrl(self):
       
   361     """Returns the Django url() for the underlying self.url.
       
   362     """
       
   363     return self.url.makeDjangoUrl()
       
   364 
       
   365   def makeDjangoUrls(self):
       
   366     """Returns an ordered mapping of unique Django url() objects.
       
   367     
       
   368     Raises:
       
   369       KeyError if more than one Page has the same urlpattern.
       
   370       
       
   371     TODO(tlarsen): this really needs to be detected earlier via a
       
   372       global Page dictionary
       
   373     """
       
   374     return self._makeDjangoUrlsDict().values()
       
   375 
       
   376   def _makeDjangoUrlsDict(self):
       
   377     """Returns an ordered mapping of unique Django url() objects.
       
   378     
       
   379     Used to implement makeDjangoUrls().  See that method for details.
       
   380     """
       
   381     urlpatterns = NoOverwriteSortedDict()
       
   382     
       
   383     if self.url.view:
       
   384       urlpatterns[self.url.regex] = self.makeDjangoUrl()
       
   385     
       
   386     for child in self.children:
       
   387       urlpatterns.update(child._makeDjangoUrlsDict())
       
   388     
       
   389     return urlpatterns
       
   390 
       
   391   _STR_FMT = '''%(indent)slong_name: %(long_name)s
       
   392 %(indent)sshort_name: %(short_name)s
       
   393 %(indent)sselected: %(selected)s
       
   394 %(indent)sannotation: %(annotation)s
       
   395 %(indent)surl: %(url)s
       
   396 '''
       
   397 
       
   398   def asIndentedStr(self, indent=''):
       
   399     """Returns an indented string representation useful for logging.
       
   400     
       
   401     Args:
       
   402       indent: an indentation string that is prepended to each line present
       
   403         in the multi-line string returned by this method.
       
   404     """
       
   405     strings = [ 
       
   406         self._STR_FMT % {'indent': indent, 'long_name': self.long_name,
       
   407                          'short_name': self.short_name,
       
   408                          'selected': self.selected,
       
   409                          'annotation': self.annotation,
       
   410                          'url': self.url.asIndentedStr(indent + ' ')}]
       
   411 
       
   412     for child in self.children:
       
   413       strings.extend(child.asIndentedStr(indent + '  '))
       
   414 
       
   415     return ''.join(strings)
       
   416 
       
   417   def __str__(self):
       
   418     """Returns a string representation useful for logging.
       
   419     """
       
   420     return self.asIndentedStr()