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 |
|