web/views/urlpublishing.py
changeset 0 b97547f5f1fa
child 661 4f61eb8a96b7
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """associate url's path to view identifier / rql queries
       
     2 
       
     3 It currently handle url's path with the forms
       
     4 
       
     5 * <publishing_method>
       
     6 
       
     7 * minimal REST publishing:
       
     8   * <eid>
       
     9   * <etype>[/<attribute name>/<attribute value>]*
       
    10 
       
    11 * folder navigation
       
    12 
       
    13 
       
    14 You can actually control URL (more exactly path) resolution using URL path
       
    15 evaluator.
       
    16 
       
    17 XXX actionpath and folderpath execute a query whose results is lost
       
    18 because of redirecting instead of direct traversal
       
    19 
       
    20 :organization: Logilab
       
    21 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
    22 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
    23 """
       
    24 
       
    25 __docformat__ = "restructuredtext en"
       
    26 
       
    27 from rql import TypeResolverException
       
    28 
       
    29 from cubicweb import RegistryException, typed_eid
       
    30 from cubicweb.web import NotFound, Redirect
       
    31 from cubicweb.web.component import SingletonComponent, Component
       
    32 
       
    33 
       
    34 class PathDontMatch(Exception):
       
    35     """exception used by url evaluators to notify they can't evaluate
       
    36     a path
       
    37     """
       
    38     
       
    39 class URLPublisherComponent(SingletonComponent):
       
    40     """associate url's path to view identifier / rql queries,
       
    41     by applying a chain of urlpathevaluator components.
       
    42 
       
    43     An evaluator is a URLPathEvaluator subclass with a .evaluate_path
       
    44     method taking the request object and the path to publish as
       
    45     argument.  It will either returns a publishing method identifier
       
    46     and a rql query on success or raises a `PathDontMatch` exception
       
    47     on failure. URL evaluators are called according to their `priority`
       
    48     attribute, with 0 as the greatest priority and greater values as
       
    49     lower priority.  The first evaluator returning a result or raising
       
    50     something else than `PathDontMatch` will stop the handlers chain.
       
    51     """
       
    52     id = 'urlpublisher'
       
    53     
       
    54     def __init__(self, default_method='view'):
       
    55         super(URLPublisherComponent, self).__init__()
       
    56         self.default_method = default_method
       
    57         evaluators = []        
       
    58         for evaluatorcls in self.vreg.registry_objects('components',
       
    59                                                        'urlpathevaluator'):
       
    60             # instantiation needed
       
    61             evaluator = evaluatorcls(self)
       
    62             evaluators.append(evaluator)
       
    63         self.evaluators = sorted(evaluators, key=lambda x: x.priority)
       
    64         
       
    65     def process(self, req, path):
       
    66         """given an url (essentialy caracterized by a path on the server,
       
    67         but additional information may be found in the request object), return
       
    68         a publishing method identifier (eg controller) and an optional result
       
    69         set
       
    70         
       
    71         :type req: `cubicweb.web.Request`
       
    72         :param req: the request object
       
    73         
       
    74         :type path: str
       
    75         :param path: the path of the resource to publish
       
    76 
       
    77         :rtype: tuple(str, `cubicweb.common.utils.ResultSet` or None)
       
    78         :return: the publishing method identifier and an optional result set
       
    79         
       
    80         :raise NotFound: if no handler is able to decode the given path
       
    81         """
       
    82         parts = [part for part in path.split('/')
       
    83                  if part != ''] or (self.default_method,)
       
    84         if req.form.get('rql'):
       
    85             if parts[0] in self.vreg.registry('controllers'):
       
    86                 return parts[0], None
       
    87             return 'view', None
       
    88         for evaluator in self.evaluators:
       
    89             try:
       
    90                 pmid, rset = evaluator.evaluate_path(req, parts[:])
       
    91                 break
       
    92             except PathDontMatch:
       
    93                 continue
       
    94         else:
       
    95             raise NotFound(path)
       
    96         if pmid is None:
       
    97             pmid = self.default_method
       
    98         return pmid, rset
       
    99 
       
   100         
       
   101 class URLPathEvaluator(Component):
       
   102     __abstract__ = True
       
   103     id = 'urlpathevaluator'
       
   104 
       
   105     def __init__(self, urlpublisher):
       
   106         self.urlpublisher = urlpublisher
       
   107 
       
   108 
       
   109 class RawPathEvaluator(URLPathEvaluator):
       
   110     """handle path of the form::
       
   111 
       
   112         <publishing_method>?parameters...
       
   113     """
       
   114     priority = 0
       
   115     def evaluate_path(self, req, parts):
       
   116         if len(parts) == 1 and parts[0] in self.vreg.registry('controllers'):
       
   117             return parts[0], None
       
   118         raise PathDontMatch()
       
   119 
       
   120 
       
   121 class EidPathEvaluator(URLPathEvaluator):
       
   122     """handle path with the form::
       
   123 
       
   124         <eid>
       
   125     """
       
   126     priority = 1
       
   127     def evaluate_path(self, req, parts):
       
   128         if len(parts) != 1:
       
   129             raise PathDontMatch()
       
   130         try:
       
   131             rset = req.execute('Any X WHERE X eid %(x)s',
       
   132                                {'x': typed_eid(parts[0])}, 'x')
       
   133         except ValueError:
       
   134             raise PathDontMatch()
       
   135         if rset.rowcount == 0:
       
   136             raise NotFound()
       
   137         return None, rset
       
   138 
       
   139         
       
   140 class RestPathEvaluator(URLPathEvaluator):
       
   141     """handle path with the form::
       
   142 
       
   143         <etype>[[/<attribute name>]/<attribute value>]*
       
   144     """
       
   145     priority = 2
       
   146     def __init__(self, urlpublisher):
       
   147         super(RestPathEvaluator, self).__init__(urlpublisher)
       
   148         self.etype_map = {}
       
   149         for etype in self.schema.entities():
       
   150             etype = str(etype)
       
   151             self.etype_map[etype.lower()] = etype
       
   152             
       
   153     def evaluate_path(self, req, parts):
       
   154         if not (0 < len(parts) < 4):
       
   155             raise PathDontMatch()
       
   156         try:
       
   157             etype = self.etype_map[parts.pop(0).lower()]
       
   158         except KeyError:
       
   159             raise PathDontMatch()
       
   160         cls = self.vreg.etype_class(etype)
       
   161         if parts:
       
   162             if len(parts) == 2:
       
   163                 attrname = parts.pop(0).lower()
       
   164                 try:
       
   165                     cls.e_schema.subject_relation(attrname)
       
   166                 except KeyError:
       
   167                     raise PathDontMatch()
       
   168             else:
       
   169                 attrname = cls._rest_attr_info()[0]
       
   170             value = req.url_unquote(parts.pop(0))
       
   171             rset = self.attr_rset(req, etype, attrname, value)
       
   172         else:
       
   173             rset = self.cls_rset(req, cls)
       
   174         if rset.rowcount == 0:
       
   175             raise NotFound()
       
   176         return None, rset
       
   177 
       
   178     def cls_rset(self, req, cls):
       
   179         return req.execute(cls.fetch_rql(req.user))
       
   180         
       
   181     def attr_rset(self, req, etype, attrname, value):
       
   182         rql = u'Any X WHERE X is %s, X %s %%(x)s' % (etype, attrname)
       
   183         if attrname == 'eid':
       
   184             try:
       
   185                 rset = req.execute(rql, {'x': typed_eid(value)}, 'x')
       
   186             except (ValueError, TypeResolverException):
       
   187                 # conflicting eid/type
       
   188                 raise PathDontMatch()
       
   189         else:
       
   190             rset = req.execute(rql, {'x': value})
       
   191         return rset
       
   192 
       
   193 
       
   194 class URLRewriteEvaluator(URLPathEvaluator):
       
   195     """tries to find a rewrite rule to apply
       
   196 
       
   197     URL rewrite rule definitions are stored in URLRewriter objects
       
   198     """
       
   199     priority = 3
       
   200     def evaluate_path(self, req, parts):
       
   201         # uri <=> req._twreq.path or req._twreq.uri
       
   202         uri = req.url_unquote('/' + '/'.join(parts))
       
   203         vobjects = sorted(self.vreg.registry_objects('urlrewriting'),
       
   204                           key=lambda x: x.priority,
       
   205                           reverse=True)
       
   206         for rewritercls in vobjects:
       
   207             rewriter = rewritercls()
       
   208             try:
       
   209                 # XXX we might want to chain url rewrites
       
   210                 return rewriter.rewrite(req, uri)
       
   211             except KeyError:
       
   212                 continue
       
   213         raise PathDontMatch()
       
   214         
       
   215 
       
   216 class ActionPathEvaluator(URLPathEvaluator):
       
   217     """handle path with the form::
       
   218 
       
   219     <any evaluator path>/<action>
       
   220     """
       
   221     priority = 4
       
   222     def evaluate_path(self, req, parts):
       
   223         if len(parts) < 2:
       
   224             raise PathDontMatch()
       
   225         # remove last part and see if this is something like an actions
       
   226         # if so, call
       
   227         try:
       
   228             requested = parts.pop(-1)
       
   229             actions = self.vreg.registry_objects('actions', requested)
       
   230         except RegistryException:
       
   231             raise PathDontMatch()
       
   232         for evaluator in self.urlpublisher.evaluators:
       
   233             if evaluator is self or evaluator.priority == 0:
       
   234                 continue
       
   235             try:
       
   236                 pmid, rset = evaluator.evaluate_path(req, parts[:])
       
   237             except PathDontMatch:
       
   238                 continue
       
   239             else:
       
   240                 try:
       
   241                     action = self.vreg.select(actions, req, rset)
       
   242                 except RegistryException:
       
   243                     raise PathDontMatch()
       
   244                 else:
       
   245                     # XXX avoid redirect
       
   246                     raise Redirect(action.url())
       
   247         raise PathDontMatch()