req.py
brancholdstable
changeset 4985 02b52bf9f5f8
parent 4892 7ee8f128be9e
child 4899 c666d265fb95
equal deleted inserted replaced
4563:c25da7573ebd 4985:02b52bf9f5f8
       
     1 """Base class for request/session
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 :license: Library General Public License version 2 - http://www.gnu.org/licenses
       
     7 """
       
     8 __docformat__ = "restructuredtext en"
       
     9 
       
    10 from urlparse import urlsplit, urlunsplit
       
    11 from urllib import quote as urlquote, unquote as urlunquote
       
    12 from datetime import time, datetime, timedelta
       
    13 from cgi import parse_qs, parse_qsl
       
    14 
       
    15 from logilab.common.decorators import cached
       
    16 from logilab.common.deprecation import deprecated
       
    17 from logilab.common.date import ustrftime, strptime, todate, todatetime
       
    18 
       
    19 from cubicweb import Unauthorized, RegistryException, typed_eid
       
    20 from cubicweb.rset import ResultSet
       
    21 
       
    22 ONESECOND = timedelta(0, 1, 0)
       
    23 CACHE_REGISTRY = {}
       
    24 
       
    25 
       
    26 class Cache(dict):
       
    27     def __init__(self):
       
    28         super(Cache, self).__init__()
       
    29         _now = datetime.now()
       
    30         self.cache_creation_date = _now
       
    31         self.latest_cache_lookup = _now
       
    32 
       
    33 
       
    34 class RequestSessionBase(object):
       
    35     """base class containing stuff shared by server session and web request
       
    36 
       
    37     request/session is the main resources accessor, mainly through it's vreg
       
    38     attribute:
       
    39     :vreg:
       
    40       the instance's registry
       
    41     :vreg.schema:
       
    42       the instance's schema
       
    43     :vreg.config:
       
    44       the instance's configuration
       
    45     """
       
    46     def __init__(self, vreg):
       
    47         self.vreg = vreg
       
    48         try:
       
    49             encoding = vreg.property_value('ui.encoding')
       
    50         except: # no vreg or property not registered
       
    51             encoding = 'utf-8'
       
    52         self.encoding = encoding
       
    53         # cache result of execution for (rql expr / eids),
       
    54         # should be emptied on commit/rollback of the server session / web
       
    55         # connection
       
    56         self.local_perm_cache = {}
       
    57         self._ = unicode
       
    58 
       
    59     def property_value(self, key):
       
    60         """return value of the property with the given key, giving priority to
       
    61         user specific value if any, else using site value
       
    62         """
       
    63         if self.user:
       
    64             return self.user.property_value(key)
       
    65         return self.vreg.property_value(key)
       
    66 
       
    67     def etype_rset(self, etype, size=1):
       
    68         """return a fake result set for a particular entity type"""
       
    69         rset = ResultSet([('A',)]*size, '%s X' % etype,
       
    70                          description=[(etype,)]*size)
       
    71         def get_entity(row, col=0, etype=etype, req=self, rset=rset):
       
    72             return req.vreg.etype_class(etype)(req, rset, row, col)
       
    73         rset.get_entity = get_entity
       
    74         return self.decorate_rset(rset)
       
    75 
       
    76     def eid_rset(self, eid, etype=None):
       
    77         """return a result set for the given eid without doing actual query
       
    78         (we have the eid, we can suppose it exists and user has access to the
       
    79         entity)
       
    80         """
       
    81         eid = typed_eid(eid)
       
    82         if etype is None:
       
    83             etype = self.describe(eid)[0]
       
    84         rset = ResultSet([(eid,)], 'Any X WHERE X eid %(x)s', {'x': eid},
       
    85                          [(etype,)])
       
    86         return self.decorate_rset(rset)
       
    87 
       
    88     def empty_rset(self):
       
    89         """return a result set for the given eid without doing actual query
       
    90         (we have the eid, we can suppose it exists and user has access to the
       
    91         entity)
       
    92         """
       
    93         return self.decorate_rset(ResultSet([], 'Any X WHERE X eid -1'))
       
    94 
       
    95     def entity_from_eid(self, eid, etype=None):
       
    96         """return an entity instance for the given eid. No query is done"""
       
    97         try:
       
    98             return self.entity_cache(eid)
       
    99         except KeyError:
       
   100             rset = self.eid_rset(eid, etype)
       
   101             entity = rset.get_entity(0, 0)
       
   102             self.set_entity_cache(entity)
       
   103             return entity
       
   104 
       
   105     def entity_cache(self, eid):
       
   106         raise KeyError
       
   107 
       
   108     def set_entity_cache(self, entity):
       
   109         pass
       
   110 
       
   111     # XXX move to CWEntityManager or even better as factory method (unclear
       
   112     # where yet...)
       
   113 
       
   114     def create_entity(self, etype, _cw_unsafe=False, **kwargs):
       
   115         """add a new entity of the given type
       
   116 
       
   117         Example (in a shell session):
       
   118 
       
   119         c = create_entity('Company', name=u'Logilab')
       
   120         create_entity('Person', works_for=c, firstname=u'John', lastname=u'Doe')
       
   121 
       
   122         """
       
   123         if _cw_unsafe:
       
   124             execute = self.unsafe_execute
       
   125         else:
       
   126             execute = self.execute
       
   127         rql = 'INSERT %s X' % etype
       
   128         relations = []
       
   129         restrictions = set()
       
   130         cachekey = []
       
   131         pending_relations = []
       
   132         for attr, value in kwargs.items():
       
   133             if isinstance(value, (tuple, list, set, frozenset)):
       
   134                 if len(value) == 1:
       
   135                     value = iter(value).next()
       
   136                 else:
       
   137                     del kwargs[attr]
       
   138                     pending_relations.append( (attr, value) )
       
   139                     continue
       
   140             if hasattr(value, 'eid'): # non final relation
       
   141                 rvar = attr.upper()
       
   142                 # XXX safer detection of object relation
       
   143                 if attr.startswith('reverse_'):
       
   144                     relations.append('%s %s X' % (rvar, attr[len('reverse_'):]))
       
   145                 else:
       
   146                     relations.append('X %s %s' % (attr, rvar))
       
   147                 restriction = '%s eid %%(%s)s' % (rvar, attr)
       
   148                 if not restriction in restrictions:
       
   149                     restrictions.add(restriction)
       
   150                 cachekey.append(attr)
       
   151                 kwargs[attr] = value.eid
       
   152             else: # attribute
       
   153                 relations.append('X %s %%(%s)s' % (attr, attr))
       
   154         if relations:
       
   155             rql = '%s: %s' % (rql, ', '.join(relations))
       
   156         if restrictions:
       
   157             rql = '%s WHERE %s' % (rql, ', '.join(restrictions))
       
   158         created = execute(rql, kwargs, cachekey).get_entity(0, 0)
       
   159         for attr, values in pending_relations:
       
   160             if attr.startswith('reverse_'):
       
   161                 restr = 'Y %s X' % attr[len('reverse_'):]
       
   162             else:
       
   163                 restr = 'X %s Y' % attr
       
   164             execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
       
   165                 restr, ','.join(str(r.eid) for r in values)),
       
   166                          {'x': created.eid}, 'x')
       
   167         return created
       
   168 
       
   169     def ensure_ro_rql(self, rql):
       
   170         """raise an exception if the given rql is not a select query"""
       
   171         first = rql.split(' ', 1)[0].lower()
       
   172         if first in ('insert', 'set', 'delete'):
       
   173             raise Unauthorized(self._('only select queries are authorized'))
       
   174 
       
   175     def get_cache(self, cachename):
       
   176         """
       
   177         NOTE: cachename should be dotted names as in :
       
   178         - cubicweb.mycache
       
   179         - cubes.blog.mycache
       
   180         - etc.
       
   181         """
       
   182         if cachename in CACHE_REGISTRY:
       
   183             cache = CACHE_REGISTRY[cachename]
       
   184         else:
       
   185             cache = CACHE_REGISTRY[cachename] = Cache()
       
   186         _now = datetime.now()
       
   187         if _now > cache.latest_cache_lookup + ONESECOND:
       
   188             ecache = self.execute(
       
   189                 'Any C,T WHERE C is CWCache, C name %(name)s, C timestamp T',
       
   190                 {'name':cachename}).get_entity(0,0)
       
   191             cache.latest_cache_lookup = _now
       
   192             if not ecache.valid(cache.cache_creation_date):
       
   193                 cache.clear()
       
   194                 cache.cache_creation_date = _now
       
   195         return cache
       
   196 
       
   197     # url generation methods ##################################################
       
   198 
       
   199     def build_url(self, *args, **kwargs):
       
   200         """return an absolute URL using params dictionary key/values as URL
       
   201         parameters. Values are automatically URL quoted, and the
       
   202         publishing method to use may be specified or will be guessed.
       
   203         """
       
   204         # use *args since we don't want first argument to be "anonymous" to
       
   205         # avoid potential clash with kwargs
       
   206         if args:
       
   207             assert len(args) == 1, 'only 0 or 1 non-named-argument expected'
       
   208             method = args[0]
       
   209         else:
       
   210             method = None
       
   211         # XXX I (adim) think that if method is passed explicitly, we should
       
   212         #     not try to process it and directly call req.build_url()
       
   213         if method is None:
       
   214             if self.from_controller() == 'view' and not '_restpath' in kwargs:
       
   215                 method = self.relative_path(includeparams=False) or 'view'
       
   216             else:
       
   217                 method = 'view'
       
   218         base_url = kwargs.pop('base_url', None)
       
   219         if base_url is None:
       
   220             base_url = self.base_url()
       
   221         if '_restpath' in kwargs:
       
   222             assert method == 'view', method
       
   223             path = kwargs.pop('_restpath')
       
   224         else:
       
   225             path = method
       
   226         if not kwargs:
       
   227             return u'%s%s' % (base_url, path)
       
   228         return u'%s%s?%s' % (base_url, path, self.build_url_params(**kwargs))
       
   229 
       
   230 
       
   231     def build_url_params(self, **kwargs):
       
   232         """return encoded params to incorporate them in an URL"""
       
   233         args = []
       
   234         for param, values in kwargs.iteritems():
       
   235             if not isinstance(values, (list, tuple)):
       
   236                 values = (values,)
       
   237             for value in values:
       
   238                 args.append(u'%s=%s' % (param, self.url_quote(value)))
       
   239         return '&'.join(args)
       
   240 
       
   241     def url_quote(self, value, safe=''):
       
   242         """urllib.quote is not unicode safe, use this method to do the
       
   243         necessary encoding / decoding. Also it's designed to quote each
       
   244         part of a url path and so the '/' character will be encoded as well.
       
   245         """
       
   246         if isinstance(value, unicode):
       
   247             quoted = urlquote(value.encode(self.encoding), safe=safe)
       
   248             return unicode(quoted, self.encoding)
       
   249         return urlquote(str(value), safe=safe)
       
   250 
       
   251     def url_unquote(self, quoted):
       
   252         """returns a unicode unquoted string
       
   253 
       
   254         decoding is based on `self.encoding` which is the encoding
       
   255         used in `url_quote`
       
   256         """
       
   257         if isinstance(quoted, unicode):
       
   258             quoted = quoted.encode(self.encoding)
       
   259         try:
       
   260             return unicode(urlunquote(quoted), self.encoding)
       
   261         except UnicodeDecodeError: # might occurs on manually typed URLs
       
   262             return unicode(urlunquote(quoted), 'iso-8859-1')
       
   263 
       
   264     def url_parse_qsl(self, querystring):
       
   265         """return a list of (key, val) found in the url quoted query string"""
       
   266         if isinstance(querystring, unicode):
       
   267             querystring = querystring.encode(self.encoding)
       
   268         for key, val in parse_qsl(querystring):
       
   269             try:
       
   270                 yield unicode(key, self.encoding), unicode(val, self.encoding)
       
   271             except UnicodeDecodeError: # might occurs on manually typed URLs
       
   272                 yield unicode(key, 'iso-8859-1'), unicode(val, 'iso-8859-1')
       
   273 
       
   274 
       
   275     def rebuild_url(self, url, **newparams):
       
   276         """return the given url with newparams inserted. If any new params
       
   277         is already specified in the url, it's overriden by the new value
       
   278 
       
   279         newparams may only be mono-valued.
       
   280         """
       
   281         if isinstance(url, unicode):
       
   282             url = url.encode(self.encoding)
       
   283         schema, netloc, path, query, fragment = urlsplit(url)
       
   284         query = parse_qs(query)
       
   285         # sort for testing predictability
       
   286         for key, val in sorted(newparams.iteritems()):
       
   287             query[key] = (self.url_quote(val),)
       
   288         query = '&'.join(u'%s=%s' % (param, value)
       
   289                          for param, values in query.items()
       
   290                          for value in values)
       
   291         return urlunsplit((schema, netloc, path, query, fragment))
       
   292 
       
   293     # bound user related methods ###############################################
       
   294 
       
   295     @cached
       
   296     def user_data(self):
       
   297         """returns a dictionnary with this user's information"""
       
   298         userinfo = {}
       
   299         if self.is_internal_session:
       
   300             userinfo['login'] = "cubicweb"
       
   301             userinfo['name'] = "cubicweb"
       
   302             userinfo['email'] = ""
       
   303             return userinfo
       
   304         user = self.actual_session().user
       
   305         userinfo['login'] = user.login
       
   306         userinfo['name'] = user.name()
       
   307         userinfo['email'] = user.get_email()
       
   308         return userinfo
       
   309 
       
   310     def is_internal_session(self):
       
   311         """overrided on the server-side"""
       
   312         return False
       
   313 
       
   314     # formating methods #######################################################
       
   315 
       
   316     def view(self, __vid, rset=None, __fallback_oid=None, __registry='views',
       
   317              initargs=None, **kwargs):
       
   318         """Select object with the given id (`__oid`) then render it.  If the
       
   319         object isn't selectable, try to select fallback object if
       
   320         `__fallback_oid` is specified.
       
   321 
       
   322         If specified `initargs` is expected to be a dictionnary containing
       
   323         arguments that should be given to selection (hence to object's __init__
       
   324         as well), but not to render(). Other arbitrary keyword arguments will be
       
   325         given to selection *and* to render(), and so should be handled by
       
   326         object's call or cell_call method..
       
   327         """
       
   328         if initargs is None:
       
   329             initargs = kwargs
       
   330         else:
       
   331             initargs.update(kwargs)
       
   332         try:
       
   333             view =  self.vreg[__registry].select(__vid, self, rset=rset, **initargs)
       
   334         except RegistryException:
       
   335             view =  self.vreg[__registry].select(__fallback_oid, self,
       
   336                                                  rset=rset, **initargs)
       
   337         return view.render(**kwargs)
       
   338 
       
   339     def format_date(self, date, date_format=None, time=False):
       
   340         """return a string for a date time according to instance's
       
   341         configuration
       
   342         """
       
   343         if date:
       
   344             if date_format is None:
       
   345                 if time:
       
   346                     date_format = self.property_value('ui.datetime-format')
       
   347                 else:
       
   348                     date_format = self.property_value('ui.date-format')
       
   349             return ustrftime(date, date_format)
       
   350         return u''
       
   351 
       
   352     def format_time(self, time):
       
   353         """return a string for a time according to instance's
       
   354         configuration
       
   355         """
       
   356         if time:
       
   357             return ustrftime(time, self.property_value('ui.time-format'))
       
   358         return u''
       
   359 
       
   360     def format_float(self, num):
       
   361         """return a string for floating point number according to instance's
       
   362         configuration
       
   363         """
       
   364         if num is not None:
       
   365             return self.property_value('ui.float-format') % num
       
   366         return u''
       
   367 
       
   368     def parse_datetime(self, value, etype='Datetime'):
       
   369         """get a datetime or time from a string (according to etype)
       
   370         Datetime formatted as Date are accepted
       
   371         """
       
   372         assert etype in ('Datetime', 'Date', 'Time'), etype
       
   373         # XXX raise proper validation error
       
   374         if etype == 'Datetime':
       
   375             format = self.property_value('ui.datetime-format')
       
   376             try:
       
   377                 return todatetime(strptime(value, format))
       
   378             except ValueError:
       
   379                 pass
       
   380         elif etype == 'Time':
       
   381             format = self.property_value('ui.time-format')
       
   382             try:
       
   383                 # (adim) I can't find a way to parse a Time with a custom format
       
   384                 date = strptime(value, format) # this returns a DateTime
       
   385                 return time(date.hour, date.minute, date.second)
       
   386             except ValueError:
       
   387                 raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
       
   388                                  % {'value': value, 'format': format})
       
   389         try:
       
   390             format = self.property_value('ui.date-format')
       
   391             dt = strptime(value, format)
       
   392             if etype == 'Datetime':
       
   393                 return todatetime(dt)
       
   394             return todate(dt)
       
   395         except ValueError:
       
   396             raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
       
   397                              % {'value': value, 'format': format})
       
   398 
       
   399     # abstract methods to override according to the web front-end #############
       
   400 
       
   401     def base_url(self):
       
   402         """return the root url of the instance"""
       
   403         raise NotImplementedError
       
   404 
       
   405     def decorate_rset(self, rset):
       
   406         """add vreg/req (at least) attributes to the given result set """
       
   407         raise NotImplementedError
       
   408 
       
   409     def describe(self, eid):
       
   410         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
       
   411         raise NotImplementedError
       
   412 
       
   413     @property
       
   414     @deprecated('[3.6] use _cw.vreg.config')
       
   415     def config(self):
       
   416         return self.vreg.config
       
   417 
       
   418     @property
       
   419     @deprecated('[3.6] use _cw.vreg.schema')
       
   420     def schema(self):
       
   421         return self.vreg.schema