218 |
209 |
219 The following attributes should be set on concret appobject classes: |
210 The following attributes should be set on concret appobject classes: |
220 :__registry__: |
211 :__registry__: |
221 name of the registry for this object (string like 'views', |
212 name of the registry for this object (string like 'views', |
222 'templates'...) |
213 'templates'...) |
223 :id: |
214 :__regid__: |
224 object's identifier in the registry (string like 'main', |
215 object's identifier in the registry (string like 'main', |
225 'primary', 'folder_box') |
216 'primary', 'folder_box') |
226 :__select__: |
217 :__select__: |
227 class'selector |
218 class'selector |
228 |
219 |
229 Moreover, the `__abstract__` attribute may be set to True to indicate |
220 Moreover, the `__abstract__` attribute may be set to True to indicate |
230 that a appobject is abstract and should not be registered. |
221 that a appobject is abstract and should not be registered. |
231 |
222 |
232 At registration time, the following attributes are set on the class: |
|
233 :vreg: |
|
234 the instance's registry |
|
235 :schema: |
|
236 the instance's schema |
|
237 :config: |
|
238 the instance's configuration |
|
239 |
|
240 At selection time, the following attributes are set on the instance: |
223 At selection time, the following attributes are set on the instance: |
241 :req: |
224 |
|
225 :_cw: |
242 current request |
226 current request |
243 :rset: |
227 :cw_extra_kwargs: |
|
228 other received arguments |
|
229 |
|
230 only if rset is found in arguments (in which case rset/row/col will be |
|
231 removed from cwextra_kwargs): |
|
232 |
|
233 :cw_rset: |
244 context result set or None |
234 context result set or None |
245 :row: |
235 :cw_row: |
246 if a result set is set and the context is about a particular cell in the |
236 if a result set is set and the context is about a particular cell in the |
247 result set, and not the result set as a whole, specify the row number we |
237 result set, and not the result set as a whole, specify the row number we |
248 are interested in, else None |
238 are interested in, else None |
249 :col: |
239 :cw_col: |
250 if a result set is set and the context is about a particular cell in the |
240 if a result set is set and the context is about a particular cell in the |
251 result set, and not the result set as a whole, specify the col number we |
241 result set, and not the result set as a whole, specify the col number we |
252 are interested in, else None |
242 are interested in, else None |
253 """ |
243 """ |
254 __registry__ = None |
244 __registry__ = None |
255 id = None |
245 __regid__ = None |
256 __select__ = yes() |
246 __select__ = yes() |
257 |
247 |
258 @classmethod |
248 @classmethod |
259 def classid(cls): |
249 def __registered__(cls, registry): |
260 """returns a unique identifier for the appobject""" |
|
261 return '%s.%s' % (cls.__module__, cls.__name__) |
|
262 |
|
263 # XXX bw compat code |
|
264 @classmethod |
|
265 def build___select__(cls): |
|
266 for klass in cls.mro(): |
|
267 if klass.__name__ == 'AppObject': |
|
268 continue # the bw compat __selector__ is there |
|
269 klassdict = klass.__dict__ |
|
270 if ('__select__' in klassdict and '__selectors__' in klassdict |
|
271 and '__selgenerated__' not in klassdict): |
|
272 raise TypeError("__select__ and __selectors__ can't be used together on class %s" % cls) |
|
273 if '__selectors__' in klassdict and '__selgenerated__' not in klassdict: |
|
274 cls.__selgenerated__ = True |
|
275 # case where __selectors__ is defined locally (but __select__ |
|
276 # is in a parent class) |
|
277 selectors = klassdict['__selectors__'] |
|
278 if len(selectors) == 1: |
|
279 # micro optimization: don't bother with AndSelector if there's |
|
280 # only one selector |
|
281 select = _instantiate_selector(selectors[0]) |
|
282 else: |
|
283 select = AndSelector(*selectors) |
|
284 cls.__select__ = select |
|
285 |
|
286 @classmethod |
|
287 def registered(cls, registry): |
|
288 """called by the registry when the appobject has been registered. |
250 """called by the registry when the appobject has been registered. |
289 |
251 |
290 It must return the object that will be actually registered (this may be |
252 It must return the object that will be actually registered (this may be |
291 the right hook to create an instance for example). By default the |
253 the right hook to create an instance for example). By default the |
292 appobject is returned without any transformation. |
254 appobject is returned without any transformation. |
293 """ |
255 """ |
294 cls.build___select__() |
256 try: # XXX < 3.6 bw compat |
295 cls.vreg = registry.vreg |
257 pdefs = cls.property_defs |
296 cls.schema = registry.schema |
258 except AttributeError: |
297 cls.config = registry.config |
259 pdefs = getattr(cls, 'cw_property_defs', {}) |
|
260 else: |
|
261 warn('property_defs is deprecated, use cw_property_defs in %s' |
|
262 % cls, DeprecationWarning) |
|
263 for propid, pdef in pdefs.items(): |
|
264 pdef = pdef.copy() # may be shared |
|
265 pdef['default'] = getattr(cls, propid, pdef['default']) |
|
266 pdef['sitewide'] = getattr(cls, 'site_wide', pdef.get('sitewide')) |
|
267 registry.vreg.register_property(cls._cwpropkey(propid), **pdef) |
|
268 assert callable(cls.__select__), cls |
298 return cls |
269 return cls |
299 |
270 |
300 @classmethod |
271 def __init__(self, req, **extra): |
301 def vreg_initialization_completed(cls): |
272 super(AppObject, self).__init__() |
302 pass |
273 self._cw = req |
303 |
274 try: |
304 # Eproperties definition: |
275 self.cw_rset = extra.pop('rset') |
305 # key: id of the property (the actual CWProperty key is build using |
276 self.cw_row = extra.pop('row', None) |
306 # <registry name>.<obj id>.<property id> |
277 self.cw_col = extra.pop('col', None) |
307 # value: tuple (property type, vocabfunc, default value, property description) |
278 except KeyError: |
308 # possible types are those used by `logilab.common.configuration` |
279 pass |
|
280 self.cw_extra_kwargs = extra |
|
281 |
|
282 # persistent class properties ############################################## |
|
283 # |
|
284 # optional `cw_property_defs` dict on a class defines available persistent |
|
285 # properties for this class: |
|
286 # |
|
287 # * key: id of the property (the actual CWProperty key is build using |
|
288 # <registry name>.<obj id>.<property id> |
|
289 # * value: tuple (property type, vocabfunc, default value, property description) |
|
290 # possible types are those used by `logilab.common.configuration` |
309 # |
291 # |
310 # notice that when it exists multiple objects with the same id (adaptation, |
292 # notice that when it exists multiple objects with the same id (adaptation, |
311 # overriding) only the first encountered definition is considered, so those |
293 # overriding) only the first encountered definition is considered, so those |
312 # objects can't try to have different default values for instance. |
294 # objects can't try to have different default values for instance. |
313 |
295 # |
314 property_defs = {} |
296 # you can then access to a property value using self.cw_propval, where self |
|
297 # is an instance of class |
315 |
298 |
316 @classmethod |
299 @classmethod |
317 def register_properties(cls): |
300 def _cwpropkey(cls, propid): |
318 for propid, pdef in cls.property_defs.items(): |
301 """return cw property key for the property of the given id for this |
319 pdef = pdef.copy() # may be shared |
302 class |
320 pdef['default'] = getattr(cls, propid, pdef['default']) |
303 """ |
321 pdef['sitewide'] = getattr(cls, 'site_wide', pdef.get('sitewide')) |
304 return '%s.%s.%s' % (cls.__registry__, cls.__regid__, propid) |
322 cls.vreg.register_property(cls.propkey(propid), **pdef) |
305 |
323 |
306 def cw_propval(self, propid): |
324 @classmethod |
307 """return cw property value associated to key |
325 def propkey(cls, propid): |
308 |
326 return '%s.%s.%s' % (cls.__registry__, cls.id, propid) |
309 <cls.__registry__>.<cls.id>.<propid> |
327 |
310 """ |
328 @classproperty |
311 return self._cw.property_value(self._cwpropkey(propid)) |
329 @deprecated('use __select__ and & or | operators') |
312 |
330 def __selectors__(cls): |
313 # deprecated ############################################################### |
331 selector = cls.__select__ |
314 |
332 if isinstance(selector, AndSelector): |
315 @property |
333 return tuple(selector.selectors) |
316 @deprecated('[3.6] use self.__regid__') |
334 if not isinstance(selector, tuple): |
317 def id(self): |
335 selector = (selector,) |
318 return self.__regid__ |
336 return selector |
319 |
337 |
320 @property |
338 def __init__(self, req=None, rset=None, row=None, col=None, **extra): |
321 @deprecated('[3.6] use self._cw.vreg') |
339 super(AppObject, self).__init__() |
322 def vreg(self): |
340 self.req = req |
323 return self._cw.vreg |
341 self.rset = rset |
324 |
342 self.row = row |
325 @property |
343 self.col = col |
326 @deprecated('[3.6] use self._cw.vreg.schema') |
344 self.extra_kwargs = extra |
327 def schema(self): |
345 |
328 return self._cw.vreg.schema |
|
329 |
|
330 @property |
|
331 @deprecated('[3.6] use self._cw.vreg.config') |
|
332 def config(self): |
|
333 return self._cw.vreg.config |
|
334 |
|
335 @property |
|
336 @deprecated('[3.6] use self._cw') |
|
337 def req(self): |
|
338 return self._cw |
|
339 |
|
340 @deprecated('[3.6] use self.cw_rset') |
|
341 def get_rset(self): |
|
342 return self.cw_rset |
|
343 @deprecated('[3.6] use self.cw_rset') |
|
344 def set_rset(self, rset): |
|
345 self.cw_rset = rset |
|
346 rset = property(get_rset, set_rset) |
|
347 |
|
348 @property |
|
349 @deprecated('[3.6] use self.cw_row') |
|
350 def row(self): |
|
351 return self.cw_row |
|
352 |
|
353 @property |
|
354 @deprecated('[3.6] use self.cw_col') |
|
355 def col(self): |
|
356 return self.cw_col |
|
357 |
|
358 @property |
|
359 @deprecated('[3.6] use self.cw_extra_kwargs') |
|
360 def extra_kwargs(self): |
|
361 return self.cw_extra_kwargs |
|
362 |
|
363 @deprecated('[3.6] use self._cw.view') |
|
364 def view(self, *args, **kwargs): |
|
365 return self._cw.view(*args, **kwargs) |
|
366 |
|
367 @property |
|
368 @deprecated('[3.6] use self._cw.varmaker') |
|
369 def varmaker(self): |
|
370 return self._cw.varmaker |
|
371 |
|
372 @deprecated('[3.6] use self._cw.get_cache') |
346 def get_cache(self, cachename): |
373 def get_cache(self, cachename): |
347 """ |
374 return self._cw.get_cache(cachename) |
348 NOTE: cachename should be dotted names as in : |
375 |
349 - cubicweb.mycache |
376 @deprecated('[3.6] use self._cw.build_url') |
350 - cubes.blog.mycache |
377 def build_url(self, *args, **kwargs): |
351 - etc. |
378 return self._cw.build_url(*args, **kwargs) |
352 """ |
379 |
353 if cachename in CACHE_REGISTRY: |
380 @deprecated('[3.6] use self.cw_rset.limited_rql') |
354 cache = CACHE_REGISTRY[cachename] |
381 def limited_rql(self): |
355 else: |
382 return self.cw_rset.limited_rql() |
356 cache = CACHE_REGISTRY[cachename] = Cache() |
383 |
357 _now = datetime.now() |
384 @deprecated('[3.6] use self.cw_rset.complete_entity(row,col) instead') |
358 if _now > cache.latest_cache_lookup + ONESECOND: |
385 def complete_entity(self, row, col=0, skip_bytes=True): |
359 ecache = self.req.execute('Any C,T WHERE C is CWCache, C name %(name)s, C timestamp T', |
386 return self.cw_rset.complete_entity(row, col, skip_bytes) |
360 {'name':cachename}).get_entity(0,0) |
387 |
361 cache.latest_cache_lookup = _now |
388 @deprecated('[3.6] use self.cw_rset.get_entity(row,col) instead') |
362 if not ecache.valid(cache.cache_creation_date): |
389 def entity(self, row, col=0): |
363 cache.clear() |
390 return self.cw_rset.get_entity(row, col) |
364 cache.cache_creation_date = _now |
391 |
365 return cache |
392 @deprecated('[3.6] use self._cw.user_rql_callback') |
366 |
393 def user_rql_callback(self, args, msg=None): |
|
394 return self._cw.user_rql_callback(args, msg) |
|
395 |
|
396 @deprecated('[3.6] use self._cw.user_callback') |
|
397 def user_callback(self, cb, args, msg=None, nonify=False): |
|
398 return self._cw.user_callback(cb, args, msg, nonify) |
|
399 |
|
400 @deprecated('[3.6] use self._cw.format_date') |
|
401 def format_date(self, date, date_format=None, time=False): |
|
402 return self._cw.format_date(date, date_format, time) |
|
403 |
|
404 @deprecated('[3.6] use self._cw.format_time') |
|
405 def format_time(self, time): |
|
406 return self._cw.format_time(time) |
|
407 |
|
408 @deprecated('[3.6] use self._cw.format_float') |
|
409 def format_float(self, num): |
|
410 return self._cw.format_float(num) |
|
411 |
|
412 @deprecated('[3.6] use self._cw.parse_datetime') |
|
413 def parse_datetime(self, value, etype='Datetime'): |
|
414 return self._cw.parse_datetime(value, etype) |
|
415 |
|
416 @deprecated('[3.6] use self.cw_propval') |
367 def propval(self, propid): |
417 def propval(self, propid): |
368 assert self.req |
418 return self._cw.property_value(self._cwpropkey(propid)) |
369 return self.req.property_value(self.propkey(propid)) |
|
370 |
|
371 def limited_rql(self): |
|
372 """return a printable rql for the result set associated to the object, |
|
373 with limit/offset correctly set according to maximum page size and |
|
374 currently displayed page when necessary |
|
375 """ |
|
376 # try to get page boundaries from the navigation component |
|
377 # XXX we should probably not have a ref to this component here (eg in |
|
378 # cubicweb.common) |
|
379 nav = self.vreg['components'].select_object('navigation', self.req, |
|
380 rset=self.rset) |
|
381 if nav: |
|
382 start, stop = nav.page_boundaries() |
|
383 rql = self._limit_offset_rql(stop - start, start) |
|
384 # result set may have be limited manually in which case navigation won't |
|
385 # apply |
|
386 elif self.rset.limited: |
|
387 rql = self._limit_offset_rql(*self.rset.limited) |
|
388 # navigation component doesn't apply and rset has not been limited, no |
|
389 # need to limit query |
|
390 else: |
|
391 rql = self.rset.printable_rql() |
|
392 return rql |
|
393 |
|
394 def _limit_offset_rql(self, limit, offset): |
|
395 rqlst = self.rset.syntax_tree() |
|
396 if len(rqlst.children) == 1: |
|
397 select = rqlst.children[0] |
|
398 olimit, ooffset = select.limit, select.offset |
|
399 select.limit, select.offset = limit, offset |
|
400 rql = rqlst.as_string(kwargs=self.rset.args) |
|
401 # restore original limit/offset |
|
402 select.limit, select.offset = olimit, ooffset |
|
403 else: |
|
404 newselect = Select() |
|
405 newselect.limit = limit |
|
406 newselect.offset = offset |
|
407 aliases = [VariableRef(newselect.get_variable(vref.name, i)) |
|
408 for i, vref in enumerate(rqlst.selection)] |
|
409 newselect.set_with([SubQuery(aliases, rqlst)], check=False) |
|
410 newunion = Union() |
|
411 newunion.append(newselect) |
|
412 rql = rqlst.as_string(kwargs=self.rset.args) |
|
413 rqlst.parent = None |
|
414 return rql |
|
415 |
|
416 def view(self, __vid, rset=None, __fallback_oid=None, __registry='views', |
|
417 **kwargs): |
|
418 """shortcut to self.vreg.view method avoiding to pass self.req""" |
|
419 return self.vreg[__registry].render(__vid, self.req, __fallback_oid, |
|
420 rset=rset, **kwargs) |
|
421 |
|
422 def initialize_varmaker(self): |
|
423 varmaker = self.req.get_page_data('rql_varmaker') |
|
424 if varmaker is None: |
|
425 varmaker = self.req.varmaker |
|
426 self.req.set_page_data('rql_varmaker', varmaker) |
|
427 self.varmaker = varmaker |
|
428 |
|
429 # url generation methods ################################################## |
|
430 |
|
431 controller = 'view' |
|
432 |
|
433 def build_url(self, *args, **kwargs): |
|
434 """return an absolute URL using params dictionary key/values as URL |
|
435 parameters. Values are automatically URL quoted, and the |
|
436 publishing method to use may be specified or will be guessed. |
|
437 """ |
|
438 # use *args since we don't want first argument to be "anonymous" to |
|
439 # avoid potential clash with kwargs |
|
440 if args: |
|
441 assert len(args) == 1, 'only 0 or 1 non-named-argument expected' |
|
442 method = args[0] |
|
443 else: |
|
444 method = None |
|
445 # XXX I (adim) think that if method is passed explicitly, we should |
|
446 # not try to process it and directly call req.build_url() |
|
447 if method is None: |
|
448 method = self.controller |
|
449 if method == 'view' and self.req.from_controller() == 'view' and \ |
|
450 not '_restpath' in kwargs: |
|
451 method = self.req.relative_path(includeparams=False) or 'view' |
|
452 return self.req.build_url(method, **kwargs) |
|
453 |
|
454 # various resources accessors ############################################# |
|
455 |
|
456 def entity(self, row, col=0): |
|
457 """short cut to get an entity instance for a particular row/column |
|
458 (col default to 0) |
|
459 """ |
|
460 return self.rset.get_entity(row, col) |
|
461 |
|
462 def complete_entity(self, row, col=0, skip_bytes=True): |
|
463 """short cut to get an completed entity instance for a particular |
|
464 row (all instance's attributes have been fetched) |
|
465 """ |
|
466 entity = self.entity(row, col) |
|
467 entity.complete(skip_bytes=skip_bytes) |
|
468 return entity |
|
469 |
|
470 def user_rql_callback(self, args, msg=None): |
|
471 """register a user callback to execute some rql query and return an url |
|
472 to call it ready to be inserted in html |
|
473 """ |
|
474 def rqlexec(req, rql, args=None, key=None): |
|
475 req.execute(rql, args, key) |
|
476 return self.user_callback(rqlexec, args, msg) |
|
477 |
|
478 def user_callback(self, cb, args, msg=None, nonify=False): |
|
479 """register the given user callback and return an url to call it ready to be |
|
480 inserted in html |
|
481 """ |
|
482 from simplejson import dumps |
|
483 self.req.add_js('cubicweb.ajax.js') |
|
484 cbname = self.req.register_onetime_callback(cb, *args) |
|
485 msg = dumps(msg or '') |
|
486 return "javascript:userCallbackThenReloadPage('%s', %s)" % ( |
|
487 cbname, msg) |
|
488 |
|
489 # formating methods ####################################################### |
|
490 |
|
491 def tal_render(self, template, variables): |
|
492 """render a precompiled page template with variables in the given |
|
493 dictionary as context |
|
494 """ |
|
495 from cubicweb.ext.tal import CubicWebContext |
|
496 context = CubicWebContext() |
|
497 context.update({'self': self, 'rset': self.rset, '_' : self.req._, |
|
498 'req': self.req, 'user': self.req.user}) |
|
499 context.update(variables) |
|
500 output = UStringIO() |
|
501 template.expand(context, output) |
|
502 return output.getvalue() |
|
503 |
|
504 def format_date(self, date, date_format=None, time=False): |
|
505 """return a string for a date time according to instance's |
|
506 configuration |
|
507 """ |
|
508 if date: |
|
509 if date_format is None: |
|
510 if time: |
|
511 date_format = self.req.property_value('ui.datetime-format') |
|
512 else: |
|
513 date_format = self.req.property_value('ui.date-format') |
|
514 return ustrftime(date, date_format) |
|
515 return u'' |
|
516 |
|
517 def format_time(self, time): |
|
518 """return a string for a time according to instance's |
|
519 configuration |
|
520 """ |
|
521 if time: |
|
522 return ustrftime(time, self.req.property_value('ui.time-format')) |
|
523 return u'' |
|
524 |
|
525 def format_float(self, num): |
|
526 """return a string for floating point number according to instance's |
|
527 configuration """ |
|
528 if num is not None: |
|
529 return self.req.property_value('ui.float-format') % num |
|
530 return u'' |
|
531 |
|
532 def parse_datetime(self, value, etype='Datetime'): |
|
533 """get a datetime or time from a string (according to etype) |
|
534 Datetime formatted as Date are accepted |
|
535 """ |
|
536 assert etype in ('Datetime', 'Date', 'Time'), etype |
|
537 # XXX raise proper validation error |
|
538 if etype == 'Datetime': |
|
539 format = self.req.property_value('ui.datetime-format') |
|
540 try: |
|
541 return todatetime(strptime(value, format)) |
|
542 except ValueError: |
|
543 pass |
|
544 elif etype == 'Time': |
|
545 format = self.req.property_value('ui.time-format') |
|
546 try: |
|
547 # (adim) I can't find a way to parse a Time with a custom format |
|
548 date = strptime(value, format) # this returns a DateTime |
|
549 return time(date.hour, date.minute, date.second) |
|
550 except ValueError: |
|
551 raise ValueError('can\'t parse %r (expected %s)' % (value, format)) |
|
552 try: |
|
553 format = self.req.property_value('ui.date-format') |
|
554 dt = strptime(value, format) |
|
555 if etype == 'Datetime': |
|
556 return todatetime(dt) |
|
557 return todate(dt) |
|
558 except ValueError: |
|
559 raise ValueError('can\'t parse %r (expected %s)' % (value, format)) |
|
560 |
|
561 # security related methods ################################################ |
|
562 |
|
563 def ensure_ro_rql(self, rql): |
|
564 """raise an exception if the given rql is not a select query""" |
|
565 first = rql.split(' ', 1)[0].lower() |
|
566 if first in ('insert', 'set', 'delete'): |
|
567 raise Unauthorized(self.req._('only select queries are authorized')) |
|
568 |
419 |
569 set_log_methods(AppObject, getLogger('cubicweb.appobject')) |
420 set_log_methods(AppObject, getLogger('cubicweb.appobject')) |