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