req.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2012 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 """Base class for request/session"""
       
    19 
       
    20 __docformat__ = "restructuredtext en"
       
    21 
       
    22 from warnings import warn
       
    23 from datetime import time, datetime, timedelta
       
    24 
       
    25 from six import PY2, PY3, text_type
       
    26 from six.moves.urllib.parse import parse_qs, parse_qsl, quote as urlquote, unquote as urlunquote, urlsplit, urlunsplit
       
    27 
       
    28 from logilab.common.decorators import cached
       
    29 from logilab.common.deprecation import deprecated
       
    30 from logilab.common.date import ustrftime, strptime, todate, todatetime
       
    31 
       
    32 from rql.utils import rqlvar_maker
       
    33 
       
    34 from cubicweb import (Unauthorized, NoSelectableObject, NoResultError,
       
    35                       MultipleResultsError, uilib)
       
    36 from cubicweb.rset import ResultSet
       
    37 
       
    38 ONESECOND = timedelta(0, 1, 0)
       
    39 CACHE_REGISTRY = {}
       
    40 
       
    41 class FindEntityError(Exception):
       
    42     """raised when find_one_entity() can not return one and only one entity"""
       
    43 
       
    44 class Cache(dict):
       
    45     def __init__(self):
       
    46         super(Cache, self).__init__()
       
    47         _now = datetime.now()
       
    48         self.cache_creation_date = _now
       
    49         self.latest_cache_lookup = _now
       
    50 
       
    51 
       
    52 class RequestSessionBase(object):
       
    53     """base class containing stuff shared by server session and web request
       
    54 
       
    55     request/session is the main resources accessor, mainly through it's vreg
       
    56     attribute:
       
    57 
       
    58     :attribute vreg: the instance's registry
       
    59     :attribute vreg.schema: the instance's schema
       
    60     :attribute vreg.config: the instance's configuration
       
    61     """
       
    62     is_request = True # False for repository session
       
    63 
       
    64     def __init__(self, vreg):
       
    65         self.vreg = vreg
       
    66         try:
       
    67             encoding = vreg.property_value('ui.encoding')
       
    68         except Exception: # no vreg or property not registered
       
    69             encoding = 'utf-8'
       
    70         self.encoding = encoding
       
    71         # cache result of execution for (rql expr / eids),
       
    72         # should be emptied on commit/rollback of the server session / web
       
    73         # connection
       
    74         self.user = None
       
    75         self.local_perm_cache = {}
       
    76         self._ = text_type
       
    77 
       
    78     def _set_user(self, orig_user):
       
    79         """set the user for this req_session_base
       
    80 
       
    81         A special method is needed to ensure the linked user is linked to the
       
    82         connection too.
       
    83         """
       
    84         rset = self.eid_rset(orig_user.eid, 'CWUser')
       
    85         user_cls = self.vreg['etypes'].etype_class('CWUser')
       
    86         user = user_cls(self, rset, row=0, groups=orig_user.groups,
       
    87                         properties=orig_user.properties)
       
    88         user.cw_attr_cache['login'] = orig_user.login # cache login
       
    89         self.user = user
       
    90         self.set_entity_cache(user)
       
    91         self.set_language(user.prefered_language())
       
    92 
       
    93 
       
    94     def set_language(self, lang):
       
    95         """install i18n configuration for `lang` translation.
       
    96 
       
    97         Raises :exc:`KeyError` if translation doesn't exist.
       
    98         """
       
    99         self.lang = lang
       
   100         gettext, pgettext = self.vreg.config.translations[lang]
       
   101         # use _cw.__ to translate a message without registering it to the catalog
       
   102         self._ = self.__ = gettext
       
   103         self.pgettext = pgettext
       
   104 
       
   105     def get_option_value(self, option):
       
   106         raise NotImplementedError
       
   107 
       
   108     def property_value(self, key):
       
   109         """return value of the property with the given key, giving priority to
       
   110         user specific value if any, else using site value
       
   111         """
       
   112         if self.user:
       
   113             val = self.user.property_value(key)
       
   114             if val is not None:
       
   115                 return val
       
   116         return self.vreg.property_value(key)
       
   117 
       
   118     def etype_rset(self, etype, size=1):
       
   119         """return a fake result set for a particular entity type"""
       
   120         rset = ResultSet([('A',)]*size, '%s X' % etype,
       
   121                          description=[(etype,)]*size)
       
   122         def get_entity(row, col=0, etype=etype, req=self, rset=rset):
       
   123             return req.vreg['etypes'].etype_class(etype)(req, rset, row, col)
       
   124         rset.get_entity = get_entity
       
   125         rset.req = self
       
   126         return rset
       
   127 
       
   128     def eid_rset(self, eid, etype=None):
       
   129         """return a result set for the given eid without doing actual query
       
   130         (we have the eid, we can suppose it exists and user has access to the
       
   131         entity)
       
   132         """
       
   133         eid = int(eid)
       
   134         if etype is None:
       
   135             etype = self.entity_metas(eid)['type']
       
   136         rset = ResultSet([(eid,)], 'Any X WHERE X eid %(x)s', {'x': eid},
       
   137                          [(etype,)])
       
   138         rset.req = self
       
   139         return rset
       
   140 
       
   141     def empty_rset(self):
       
   142         """ return a guaranteed empty result """
       
   143         rset = ResultSet([], 'Any X WHERE X eid -1')
       
   144         rset.req = self
       
   145         return rset
       
   146 
       
   147     def entity_from_eid(self, eid, etype=None):
       
   148         """return an entity instance for the given eid. No query is done"""
       
   149         try:
       
   150             return self.entity_cache(eid)
       
   151         except KeyError:
       
   152             rset = self.eid_rset(eid, etype)
       
   153             entity = rset.get_entity(0, 0)
       
   154             self.set_entity_cache(entity)
       
   155             return entity
       
   156 
       
   157     def entity_cache(self, eid):
       
   158         raise KeyError
       
   159 
       
   160     def set_entity_cache(self, entity):
       
   161         pass
       
   162 
       
   163     def create_entity(self, etype, **kwargs):
       
   164         """add a new entity of the given type
       
   165 
       
   166         Example (in a shell session):
       
   167 
       
   168         >>> c = create_entity('Company', name=u'Logilab')
       
   169         >>> create_entity('Person', firstname=u'John', surname=u'Doe',
       
   170         ...               works_for=c)
       
   171 
       
   172         """
       
   173         cls = self.vreg['etypes'].etype_class(etype)
       
   174         return cls.cw_instantiate(self.execute, **kwargs)
       
   175 
       
   176     @deprecated('[3.18] use find(etype, **kwargs).entities()')
       
   177     def find_entities(self, etype, **kwargs):
       
   178         """find entities of the given type and attribute values.
       
   179 
       
   180         >>> users = find_entities('CWGroup', name=u'users')
       
   181         >>> groups = find_entities('CWGroup')
       
   182         """
       
   183         return self.find(etype, **kwargs).entities()
       
   184 
       
   185     @deprecated('[3.18] use find(etype, **kwargs).one()')
       
   186     def find_one_entity(self, etype, **kwargs):
       
   187         """find one entity of the given type and attribute values.
       
   188         raise :exc:`FindEntityError` if can not return one and only one entity.
       
   189 
       
   190         >>> users = find_one_entity('CWGroup', name=u'users')
       
   191         >>> groups = find_one_entity('CWGroup')
       
   192         Exception()
       
   193         """
       
   194         try:
       
   195             return self.find(etype, **kwargs).one()
       
   196         except (NoResultError, MultipleResultsError) as e:
       
   197             raise FindEntityError("%s: (%s, %s)" % (str(e), etype, kwargs))
       
   198 
       
   199     def find(self, etype, **kwargs):
       
   200         """find entities of the given type and attribute values.
       
   201 
       
   202         :returns: A :class:`ResultSet`
       
   203 
       
   204         >>> users = find('CWGroup', name=u"users").one()
       
   205         >>> groups = find('CWGroup').entities()
       
   206         """
       
   207         parts = ['Any X WHERE X is %s' % etype]
       
   208         varmaker = rqlvar_maker(defined='X')
       
   209         eschema = self.vreg.schema.eschema(etype)
       
   210         for attr, value in kwargs.items():
       
   211             if isinstance(value, list) or isinstance(value, tuple):
       
   212                 raise NotImplementedError("List of values are not supported")
       
   213             if hasattr(value, 'eid'):
       
   214                 kwargs[attr] = value.eid
       
   215             if attr.startswith('reverse_'):
       
   216                 attr = attr[8:]
       
   217                 assert attr in eschema.objrels, \
       
   218                     '%s not in %s object relations' % (attr, eschema)
       
   219                 parts.append(
       
   220                     '%(varname)s %(attr)s X, '
       
   221                     '%(varname)s eid %%(reverse_%(attr)s)s'
       
   222                     % {'attr': attr, 'varname': next(varmaker)})
       
   223             else:
       
   224                 assert attr in eschema.subjrels, \
       
   225                     '%s not in %s subject relations' % (attr, eschema)
       
   226                 parts.append('X %(attr)s %%(%(attr)s)s' % {'attr': attr})
       
   227 
       
   228         rql = ', '.join(parts)
       
   229 
       
   230         return self.execute(rql, kwargs)
       
   231 
       
   232     def ensure_ro_rql(self, rql):
       
   233         """raise an exception if the given rql is not a select query"""
       
   234         first = rql.split(None, 1)[0].lower()
       
   235         if first in ('insert', 'set', 'delete'):
       
   236             raise Unauthorized(self._('only select queries are authorized'))
       
   237 
       
   238     def get_cache(self, cachename):
       
   239         """cachename should be dotted names as in :
       
   240 
       
   241         - cubicweb.mycache
       
   242         - cubes.blog.mycache
       
   243         - etc.
       
   244         """
       
   245         warn.warning('[3.19] .get_cache will disappear soon. '
       
   246                      'Distributed caching mechanisms are being introduced instead.'
       
   247                      'Other caching mechanism can be used more reliably '
       
   248                      'to the same effect.',
       
   249                      DeprecationWarning)
       
   250         if cachename in CACHE_REGISTRY:
       
   251             cache = CACHE_REGISTRY[cachename]
       
   252         else:
       
   253             cache = CACHE_REGISTRY[cachename] = Cache()
       
   254         _now = datetime.now()
       
   255         if _now > cache.latest_cache_lookup + ONESECOND:
       
   256             ecache = self.execute(
       
   257                 'Any C,T WHERE C is CWCache, C name %(name)s, C timestamp T',
       
   258                 {'name':cachename}).get_entity(0,0)
       
   259             cache.latest_cache_lookup = _now
       
   260             if not ecache.valid(cache.cache_creation_date):
       
   261                 cache.clear()
       
   262                 cache.cache_creation_date = _now
       
   263         return cache
       
   264 
       
   265     # url generation methods ##################################################
       
   266 
       
   267     def build_url(self, *args, **kwargs):
       
   268         """return an absolute URL using params dictionary key/values as URL
       
   269         parameters. Values are automatically URL quoted, and the
       
   270         publishing method to use may be specified or will be guessed.
       
   271 
       
   272         if ``__secure__`` argument is True, the request will try to build a
       
   273         https url.
       
   274 
       
   275         raises :exc:`ValueError` if None is found in arguments
       
   276         """
       
   277         # use *args since we don't want first argument to be "anonymous" to
       
   278         # avoid potential clash with kwargs
       
   279         method = None
       
   280         if args:
       
   281             assert len(args) == 1, 'only 0 or 1 non-named-argument expected'
       
   282             method = args[0]
       
   283         if method is None:
       
   284             method = 'view'
       
   285         # XXX I (adim) think that if method is passed explicitly, we should
       
   286         #     not try to process it and directly call req.build_url()
       
   287         base_url = kwargs.pop('base_url', None)
       
   288         if base_url is None:
       
   289             secure = kwargs.pop('__secure__', None)
       
   290             base_url = self.base_url(secure=secure)
       
   291         if '_restpath' in kwargs:
       
   292             assert method == 'view', repr(method)
       
   293             path = kwargs.pop('_restpath')
       
   294         else:
       
   295             path = method
       
   296         if not kwargs:
       
   297             return u'%s%s' % (base_url, path)
       
   298         return u'%s%s?%s' % (base_url, path, self.build_url_params(**kwargs))
       
   299 
       
   300     def build_url_params(self, **kwargs):
       
   301         """return encoded params to incorporate them in a URL"""
       
   302         args = []
       
   303         for param, values in kwargs.items():
       
   304             if not isinstance(values, (list, tuple)):
       
   305                 values = (values,)
       
   306             for value in values:
       
   307                 assert value is not None
       
   308                 args.append(u'%s=%s' % (param, self.url_quote(value)))
       
   309         return '&'.join(args)
       
   310 
       
   311     def url_quote(self, value, safe=''):
       
   312         """urllib.quote is not unicode safe, use this method to do the
       
   313         necessary encoding / decoding. Also it's designed to quote each
       
   314         part of a url path and so the '/' character will be encoded as well.
       
   315         """
       
   316         if PY2 and isinstance(value, unicode):
       
   317             quoted = urlquote(value.encode(self.encoding), safe=safe)
       
   318             return unicode(quoted, self.encoding)
       
   319         return urlquote(str(value), safe=safe)
       
   320 
       
   321     def url_unquote(self, quoted):
       
   322         """returns a unicode unquoted string
       
   323 
       
   324         decoding is based on `self.encoding` which is the encoding
       
   325         used in `url_quote`
       
   326         """
       
   327         if PY3:
       
   328             return urlunquote(quoted)
       
   329         if isinstance(quoted, unicode):
       
   330             quoted = quoted.encode(self.encoding)
       
   331         try:
       
   332             return unicode(urlunquote(quoted), self.encoding)
       
   333         except UnicodeDecodeError: # might occurs on manually typed URLs
       
   334             return unicode(urlunquote(quoted), 'iso-8859-1')
       
   335 
       
   336     def url_parse_qsl(self, querystring):
       
   337         """return a list of (key, val) found in the url quoted query string"""
       
   338         if PY3:
       
   339             for key, val in parse_qsl(querystring):
       
   340                 yield key, val
       
   341             return
       
   342         if isinstance(querystring, unicode):
       
   343             querystring = querystring.encode(self.encoding)
       
   344         for key, val in parse_qsl(querystring):
       
   345             try:
       
   346                 yield unicode(key, self.encoding), unicode(val, self.encoding)
       
   347             except UnicodeDecodeError: # might occurs on manually typed URLs
       
   348                 yield unicode(key, 'iso-8859-1'), unicode(val, 'iso-8859-1')
       
   349 
       
   350 
       
   351     def rebuild_url(self, url, **newparams):
       
   352         """return the given url with newparams inserted. If any new params
       
   353         is already specified in the url, it's overriden by the new value
       
   354 
       
   355         newparams may only be mono-valued.
       
   356         """
       
   357         if PY2 and isinstance(url, unicode):
       
   358             url = url.encode(self.encoding)
       
   359         schema, netloc, path, query, fragment = urlsplit(url)
       
   360         query = parse_qs(query)
       
   361         # sort for testing predictability
       
   362         for key, val in sorted(newparams.items()):
       
   363             query[key] = (self.url_quote(val),)
       
   364         query = '&'.join(u'%s=%s' % (param, value)
       
   365                          for param, values in sorted(query.items())
       
   366                          for value in values)
       
   367         return urlunsplit((schema, netloc, path, query, fragment))
       
   368 
       
   369     # bound user related methods ###############################################
       
   370 
       
   371     @cached
       
   372     def user_data(self):
       
   373         """returns a dictionary with this user's information.
       
   374 
       
   375         The keys are :
       
   376 
       
   377         login
       
   378             The user login
       
   379 
       
   380         name
       
   381             The user name, returned by user.name()
       
   382 
       
   383         email
       
   384             The user principal email
       
   385 
       
   386         """
       
   387         userinfo = {}
       
   388         user = self.user
       
   389         userinfo['login'] = user.login
       
   390         userinfo['name'] = user.name()
       
   391         userinfo['email'] = user.cw_adapt_to('IEmailable').get_email()
       
   392         return userinfo
       
   393 
       
   394     # formating methods #######################################################
       
   395 
       
   396     def view(self, __vid, rset=None, __fallback_oid=None, __registry='views',
       
   397              initargs=None, w=None, **kwargs):
       
   398         """Select object with the given id (`__oid`) then render it.  If the
       
   399         object isn't selectable, try to select fallback object if
       
   400         `__fallback_oid` is specified.
       
   401 
       
   402         If specified `initargs` is expected to be a dictionary containing
       
   403         arguments that should be given to selection (hence to object's __init__
       
   404         as well), but not to render(). Other arbitrary keyword arguments will be
       
   405         given to selection *and* to render(), and so should be handled by
       
   406         object's call or cell_call method..
       
   407         """
       
   408         if initargs is None:
       
   409             initargs = kwargs
       
   410         else:
       
   411             initargs.update(kwargs)
       
   412         try:
       
   413             view =  self.vreg[__registry].select(__vid, self, rset=rset, **initargs)
       
   414         except NoSelectableObject:
       
   415             if __fallback_oid is None:
       
   416                 raise
       
   417             view =  self.vreg[__registry].select(__fallback_oid, self,
       
   418                                                  rset=rset, **initargs)
       
   419         return view.render(w=w, **kwargs)
       
   420 
       
   421     def printable_value(self, attrtype, value, props=None, displaytime=True,
       
   422                         formatters=uilib.PRINTERS):
       
   423         """return a displayablye value (i.e. unicode string)"""
       
   424         if value is None:
       
   425             return u''
       
   426         try:
       
   427             as_string = formatters[attrtype]
       
   428         except KeyError:
       
   429             self.error('given bad attrtype %s', attrtype)
       
   430             return unicode(value)
       
   431         return as_string(value, self, props, displaytime)
       
   432 
       
   433     def format_date(self, date, date_format=None, time=False):
       
   434         """return a string for a date time according to instance's
       
   435         configuration
       
   436         """
       
   437         if date is not None:
       
   438             if date_format is None:
       
   439                 if time:
       
   440                     date_format = self.property_value('ui.datetime-format')
       
   441                 else:
       
   442                     date_format = self.property_value('ui.date-format')
       
   443             return ustrftime(date, date_format)
       
   444         return u''
       
   445 
       
   446     def format_time(self, time):
       
   447         """return a string for a time according to instance's
       
   448         configuration
       
   449         """
       
   450         if time is not None:
       
   451             return ustrftime(time, self.property_value('ui.time-format'))
       
   452         return u''
       
   453 
       
   454     def format_float(self, num):
       
   455         """return a string for floating point number according to instance's
       
   456         configuration
       
   457         """
       
   458         if num is not None:
       
   459             return self.property_value('ui.float-format') % num
       
   460         return u''
       
   461 
       
   462     def parse_datetime(self, value, etype='Datetime'):
       
   463         """get a datetime or time from a string (according to etype)
       
   464         Datetime formatted as Date are accepted
       
   465         """
       
   466         assert etype in ('Datetime', 'Date', 'Time'), etype
       
   467         # XXX raise proper validation error
       
   468         if etype == 'Datetime':
       
   469             format = self.property_value('ui.datetime-format')
       
   470             try:
       
   471                 return todatetime(strptime(value, format))
       
   472             except ValueError:
       
   473                 pass
       
   474         elif etype == 'Time':
       
   475             format = self.property_value('ui.time-format')
       
   476             try:
       
   477                 # (adim) I can't find a way to parse a Time with a custom format
       
   478                 date = strptime(value, format) # this returns a DateTime
       
   479                 return time(date.hour, date.minute, date.second)
       
   480             except ValueError:
       
   481                 raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
       
   482                                  % {'value': value, 'format': format})
       
   483         try:
       
   484             format = self.property_value('ui.date-format')
       
   485             dt = strptime(value, format)
       
   486             if etype == 'Datetime':
       
   487                 return todatetime(dt)
       
   488             return todate(dt)
       
   489         except ValueError:
       
   490             raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
       
   491                              % {'value': value, 'format': format})
       
   492 
       
   493     def _base_url(self, secure=None):
       
   494         if secure:
       
   495             return self.vreg.config.get('https-url') or self.vreg.config['base-url']
       
   496         return self.vreg.config['base-url']
       
   497 
       
   498     def base_url(self, secure=None):
       
   499         """return the root url of the instance
       
   500         """
       
   501         url = self._base_url(secure=secure)
       
   502         return url if url is None else url.rstrip('/') + '/'
       
   503 
       
   504     # abstract methods to override according to the web front-end #############
       
   505 
       
   506     def describe(self, eid, asdict=False):
       
   507         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
       
   508         raise NotImplementedError