web/views/urlpublishing.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """Associate url's path to view identifier / rql queries.
       
    19 
       
    20 CubicWeb finds all registered URLPathEvaluators, orders them according
       
    21 to their ``priority`` attribute and calls their ``evaluate_path()``
       
    22 method. The first that returns something and doesn't raise a
       
    23 ``PathDontMatch`` exception wins.
       
    24 
       
    25 Here is the default evaluator chain:
       
    26 
       
    27 1. :class:`cubicweb.web.views.urlpublishing.RawPathEvaluator` handles
       
    28    unique url segments that match exactly one of the registered
       
    29    controller's *__regid__*. Urls such as */view?*, */edit?*, */json?*
       
    30    fall in that category;
       
    31 
       
    32 2. :class:`cubicweb.web.views.urlpublishing.EidPathEvaluator` handles
       
    33    unique url segments that are eids (e.g. */1234*);
       
    34 
       
    35 3. :class:`cubicweb.web.views.urlpublishing.URLRewriteEvaluator`
       
    36    selects all urlrewriter components, sorts them according to their
       
    37    priority, call their ``rewrite()`` method, the first one that
       
    38    doesn't raise a ``KeyError`` wins. This is where the
       
    39    :mod:`cubicweb.web.views.urlrewrite` and
       
    40    :class:`cubicweb.web.views.urlrewrite.SimpleReqRewriter` comes into
       
    41    play;
       
    42 
       
    43 4. :class:`cubicweb.web.views.urlpublishing.RestPathEvaluator` handles
       
    44    urls based on entity types and attributes : <etype>((/<attribute
       
    45    name>])?/<attribute value>)?  This is why ``cwuser/carlos`` works;
       
    46 
       
    47 5. :class:`cubicweb.web.views.urlpublishing.ActionPathEvaluator`
       
    48    handles any of the previous paths with an additional trailing
       
    49    "/<action>" segment, <action> being one of the registered actions'
       
    50    __regid__.
       
    51 
       
    52 
       
    53 .. note::
       
    54 
       
    55  Actionpath executes a query whose results is lost
       
    56  because of redirecting instead of direct traversal.
       
    57 """
       
    58 __docformat__ = "restructuredtext en"
       
    59 
       
    60 from rql import TypeResolverException
       
    61 
       
    62 from cubicweb import RegistryException
       
    63 from cubicweb.web import NotFound, Redirect, component, views
       
    64 
       
    65 
       
    66 class PathDontMatch(Exception):
       
    67     """exception used by url evaluators to notify they can't evaluate
       
    68     a path
       
    69     """
       
    70 
       
    71 class URLPublisherComponent(component.Component):
       
    72     """Associate url path to view identifier / rql queries, by
       
    73     applying a chain of urlpathevaluator components.
       
    74 
       
    75     An evaluator is a URLPathEvaluator subclass with an .evaluate_path
       
    76     method taking the request object and the path to publish as
       
    77     argument.  It will either return a publishing method identifier
       
    78     and an rql query on success or raise a `PathDontMatch` exception
       
    79     on failure. URL evaluators are called according to their
       
    80     `priority` attribute, with 0 as the greatest priority and greater
       
    81     values as lower priority. The first evaluator returning a result
       
    82     or raising something else than `PathDontMatch` will stop the
       
    83     handlers chain.
       
    84     """
       
    85     __regid__ = 'urlpublisher'
       
    86     vreg = None # XXX necessary until property for deprecation warning is on appobject
       
    87 
       
    88     def __init__(self, vreg, default_method='view'):
       
    89         super(URLPublisherComponent, self).__init__()
       
    90         self.vreg = vreg
       
    91         self.default_method = default_method
       
    92         evaluators = []
       
    93         for evaluatorcls in vreg['components']['urlpathevaluator']:
       
    94             # instantiation needed
       
    95             evaluator = evaluatorcls(self)
       
    96             evaluators.append(evaluator)
       
    97         self.evaluators = sorted(evaluators, key=lambda x: x.priority)
       
    98 
       
    99     def process(self, req, path):
       
   100         """Given a URL (essentially characterized by a path on the
       
   101         server, but additional information may be found in the request
       
   102         object), return a publishing method identifier
       
   103         (e.g. controller) and an optional result set.
       
   104 
       
   105         :type req: `cubicweb.web.request.CubicWebRequestBase`
       
   106         :param req: the request object
       
   107 
       
   108         :type path: str
       
   109         :param path: the path of the resource to publish. If empty, None or "/"
       
   110                      "view" is used as the default path.
       
   111 
       
   112         :rtype: tuple(str, `cubicweb.rset.ResultSet` or None)
       
   113         :return: the publishing method identifier and an optional result set
       
   114 
       
   115         :raise NotFound: if no handler is able to decode the given path
       
   116         """
       
   117         parts = [part for part in path.split('/')
       
   118                  if part != ''] or (self.default_method,)
       
   119         if req.form.get('rql'):
       
   120             if parts[0] in self.vreg['controllers']:
       
   121                 return parts[0], None
       
   122             return 'view', None
       
   123         for evaluator in self.evaluators:
       
   124             try:
       
   125                 pmid, rset = evaluator.evaluate_path(req, parts[:])
       
   126                 break
       
   127             except PathDontMatch:
       
   128                 continue
       
   129         else:
       
   130             raise NotFound(path)
       
   131         if pmid is None:
       
   132             pmid = self.default_method
       
   133         return pmid, rset
       
   134 
       
   135 
       
   136 class URLPathEvaluator(component.Component):
       
   137     __abstract__ = True
       
   138     __regid__ = 'urlpathevaluator'
       
   139     vreg = None # XXX necessary until property for deprecation warning is on appobject
       
   140 
       
   141     def __init__(self, urlpublisher):
       
   142         self.urlpublisher = urlpublisher
       
   143         self.vreg = urlpublisher.vreg
       
   144 
       
   145 
       
   146 class RawPathEvaluator(URLPathEvaluator):
       
   147     """handle path of the form::
       
   148 
       
   149         <publishing_method>?parameters...
       
   150     """
       
   151     priority = 0
       
   152     def evaluate_path(self, req, parts):
       
   153         if len(parts) == 1 and parts[0] in self.vreg['controllers']:
       
   154             return parts[0], None
       
   155         raise PathDontMatch()
       
   156 
       
   157 
       
   158 class EidPathEvaluator(URLPathEvaluator):
       
   159     """handle path with the form::
       
   160 
       
   161         <eid>
       
   162     """
       
   163     priority = 1
       
   164     def evaluate_path(self, req, parts):
       
   165         if len(parts) != 1:
       
   166             raise PathDontMatch()
       
   167         try:
       
   168             rset = req.execute('Any X WHERE X eid %(x)s', {'x': int(parts[0])})
       
   169         except ValueError:
       
   170             raise PathDontMatch()
       
   171         if rset.rowcount == 0:
       
   172             raise NotFound()
       
   173         return None, rset
       
   174 
       
   175 
       
   176 class RestPathEvaluator(URLPathEvaluator):
       
   177     """handle path with the form::
       
   178 
       
   179         <etype>[[/<attribute name>]/<attribute value>]*
       
   180     """
       
   181     priority = 3
       
   182 
       
   183     def evaluate_path(self, req, parts):
       
   184         if not (0 < len(parts) < 4):
       
   185             raise PathDontMatch()
       
   186         try:
       
   187             etype = self.vreg.case_insensitive_etypes[parts.pop(0).lower()]
       
   188         except KeyError:
       
   189             raise PathDontMatch()
       
   190         cls = self.vreg['etypes'].etype_class(etype)
       
   191         if parts:
       
   192             if len(parts) == 2:
       
   193                 attrname = parts.pop(0).lower()
       
   194                 try:
       
   195                     cls.e_schema.subjrels[attrname]
       
   196                 except KeyError:
       
   197                     raise PathDontMatch()
       
   198             else:
       
   199                 attrname = cls.cw_rest_attr_info()[0]
       
   200             value = req.url_unquote(parts.pop(0))
       
   201             return self.handle_etype_attr(req, cls, attrname, value)
       
   202         return self.handle_etype(req, cls)
       
   203 
       
   204     def set_vid_for_rset(self, req, cls, rset):  # cls is there to ease overriding
       
   205         if rset.rowcount == 0:
       
   206             raise NotFound()
       
   207         if 'vid' not in req.form:
       
   208             # check_table=False tells vid_from_rset not to try to use a table view if fetch_rql
       
   209             # include some non final relation
       
   210             req.form['vid'] = views.vid_from_rset(req, rset, req.vreg.schema,
       
   211                                                   check_table=False)
       
   212 
       
   213     def handle_etype(self, req, cls):
       
   214         rset = req.execute(cls.fetch_rql(req.user))
       
   215         self.set_vid_for_rset(req, cls, rset)
       
   216         return None, rset
       
   217 
       
   218     def handle_etype_attr(self, req, cls, attrname, value):
       
   219         st = cls.fetch_rqlst(req.user, ordermethod=None)
       
   220         st.add_constant_restriction(st.get_variable('X'), attrname,
       
   221                                     'x', 'Substitute')
       
   222         if attrname == 'eid':
       
   223             try:
       
   224                 rset = req.execute(st.as_string(), {'x': int(value)})
       
   225             except (ValueError, TypeResolverException):
       
   226                 # conflicting eid/type
       
   227                 raise PathDontMatch()
       
   228         else:
       
   229             rset = req.execute(st.as_string(), {'x': value})
       
   230         self.set_vid_for_rset(req, cls, rset)
       
   231         return None, rset
       
   232 
       
   233 
       
   234 class URLRewriteEvaluator(URLPathEvaluator):
       
   235     """tries to find a rewrite rule to apply
       
   236 
       
   237     URL rewrite rule definitions are stored in URLRewriter objects
       
   238     """
       
   239     priority = 2
       
   240 
       
   241     def evaluate_path(self, req, parts):
       
   242         # uri <=> req._twreq.path or req._twreq.uri
       
   243         uri = req.url_unquote('/' + '/'.join(parts))
       
   244         evaluators = sorted(self.vreg['urlrewriting'].all_objects(),
       
   245                             key=lambda x: x.priority, reverse=True)
       
   246         for rewritercls in evaluators:
       
   247             rewriter = rewritercls(req)
       
   248             try:
       
   249                 # XXX we might want to chain url rewrites
       
   250                 return rewriter.rewrite(req, uri)
       
   251             except KeyError:
       
   252                 continue
       
   253         raise PathDontMatch()
       
   254 
       
   255 
       
   256 class ActionPathEvaluator(URLPathEvaluator):
       
   257     """handle path with the form::
       
   258 
       
   259     <any evaluator path>/<action>
       
   260     """
       
   261     priority = 4
       
   262 
       
   263     def evaluate_path(self, req, parts):
       
   264         if len(parts) < 2:
       
   265             raise PathDontMatch()
       
   266         # remove last part and see if this is something like an actions
       
   267         # if so, call
       
   268         # XXX bad smell: refactor to simpler code
       
   269         try:
       
   270             actionsreg = self.vreg['actions']
       
   271             requested = parts.pop(-1)
       
   272             actions = actionsreg[requested]
       
   273         except RegistryException:
       
   274             raise PathDontMatch()
       
   275         for evaluator in self.urlpublisher.evaluators:
       
   276             if evaluator is self or evaluator.priority == 0:
       
   277                 continue
       
   278             try:
       
   279                 pmid, rset = evaluator.evaluate_path(req, parts[:])
       
   280             except PathDontMatch:
       
   281                 continue
       
   282             else:
       
   283                 try:
       
   284                     action = actionsreg._select_best(actions, req, rset=rset)
       
   285                     if action is not None:
       
   286                         raise Redirect(action.url())
       
   287                 except RegistryException:
       
   288                     pass # continue searching
       
   289         raise PathDontMatch()