1 """Base class for dynamically loaded objects manipulated in the web interface |
1 """pre 3.2 bw compat""" |
2 |
2 # pylint: disable-msg=W0614,W0401 |
3 :organization: Logilab |
|
4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
6 """ |
|
7 __docformat__ = "restructuredtext en" |
|
8 |
|
9 from warnings import warn |
3 from warnings import warn |
10 |
4 warn('moved to cubicweb.appobject', DeprecationWarning, stacklevel=2) |
11 from mx.DateTime import now, oneSecond |
5 from cubicweb.appobject import * |
12 from simplejson import dumps |
|
13 |
|
14 from logilab.common.deprecation import obsolete |
|
15 |
|
16 from rql.nodes import VariableRef, SubQuery |
|
17 from rql.stmts import Union, Select |
|
18 |
|
19 from cubicweb import Unauthorized |
|
20 from cubicweb.vregistry import VObject |
|
21 from cubicweb.common.utils import UStringIO |
|
22 from cubicweb.common.uilib import html_escape, ustrftime |
|
23 from cubicweb.common.registerers import yes_registerer, priority_registerer |
|
24 from cubicweb.common.selectors import yes |
|
25 |
|
26 _MARKER = object() |
|
27 |
|
28 |
|
29 class Cache(dict): |
|
30 def __init__(self): |
|
31 super(Cache, self).__init__() |
|
32 self.cache_creation_date = None |
|
33 self.latest_cache_lookup = now() |
|
34 |
|
35 CACHE_REGISTRY = {} |
|
36 |
|
37 class AppRsetObject(VObject): |
|
38 """This is the base class for CubicWeb application objects |
|
39 which are selected according to a request and result set. |
|
40 |
|
41 Classes are kept in the vregistry and instantiation is done at selection |
|
42 time. |
|
43 |
|
44 At registration time, the following attributes are set on the class: |
|
45 :vreg: |
|
46 the application's registry |
|
47 :schema: |
|
48 the application's schema |
|
49 :config: |
|
50 the application's configuration |
|
51 |
|
52 At instantiation time, the following attributes are set on the instance: |
|
53 :req: |
|
54 current request |
|
55 :rset: |
|
56 result set on which the object is applied |
|
57 """ |
|
58 |
|
59 @classmethod |
|
60 def registered(cls, vreg): |
|
61 cls.vreg = vreg |
|
62 cls.schema = vreg.schema |
|
63 cls.config = vreg.config |
|
64 cls.register_properties() |
|
65 return cls |
|
66 |
|
67 @classmethod |
|
68 def selected(cls, req, rset, row=None, col=None, **kwargs): |
|
69 """by default web app objects are usually instantiated on |
|
70 selection according to a request, a result set, and optional |
|
71 row and col |
|
72 """ |
|
73 instance = cls(req, rset) |
|
74 instance.row = row |
|
75 instance.col = col |
|
76 return instance |
|
77 |
|
78 # Eproperties definition: |
|
79 # key: id of the property (the actual EProperty key is build using |
|
80 # <registry name>.<obj id>.<property id> |
|
81 # value: tuple (property type, vocabfunc, default value, property description) |
|
82 # possible types are those used by `logilab.common.configuration` |
|
83 # |
|
84 # notice that when it exists multiple objects with the same id (adaptation, |
|
85 # overriding) only the first encountered definition is considered, so those |
|
86 # objects can't try to have different default values for instance. |
|
87 |
|
88 property_defs = {} |
|
89 |
|
90 @classmethod |
|
91 def register_properties(cls): |
|
92 for propid, pdef in cls.property_defs.items(): |
|
93 pdef = pdef.copy() # may be shared |
|
94 pdef['default'] = getattr(cls, propid, pdef['default']) |
|
95 pdef['sitewide'] = getattr(cls, 'site_wide', pdef.get('sitewide')) |
|
96 cls.vreg.register_property(cls.propkey(propid), **pdef) |
|
97 |
|
98 @classmethod |
|
99 def propkey(cls, propid): |
|
100 return '%s.%s.%s' % (cls.__registry__, cls.id, propid) |
|
101 |
|
102 |
|
103 def __init__(self, req, rset): |
|
104 super(AppRsetObject, self).__init__() |
|
105 self.req = req |
|
106 self.rset = rset |
|
107 |
|
108 @property |
|
109 def cursor(self): # XXX deprecate in favor of req.cursor? |
|
110 msg = '.cursor is deprecated, use req.execute (or req.cursor if necessary)' |
|
111 warn(msg, DeprecationWarning, stacklevel=2) |
|
112 return self.req.cursor |
|
113 |
|
114 def get_cache(self, cachename): |
|
115 """ |
|
116 NOTE: cachename should be dotted names as in : |
|
117 - cubicweb.mycache |
|
118 - cubes.blog.mycache |
|
119 - etc. |
|
120 """ |
|
121 if cachename in CACHE_REGISTRY: |
|
122 cache = CACHE_REGISTRY[cachename] |
|
123 else: |
|
124 cache = Cache() |
|
125 CACHE_REGISTRY[cachename] = cache |
|
126 _now = now() |
|
127 if _now > cache.latest_cache_lookup + oneSecond: |
|
128 ecache = self.req.execute('Any C,T WHERE C is ECache, C name %(name)s, C timestamp T', |
|
129 {'name':cachename}).get_entity(0,0) |
|
130 cache.latest_cache_lookup = _now |
|
131 if not ecache.valid(cache.cache_creation_date): |
|
132 cache.empty() |
|
133 cache.cache_creation_date = _now |
|
134 return cache |
|
135 |
|
136 def propval(self, propid): |
|
137 assert self.req |
|
138 return self.req.property_value(self.propkey(propid)) |
|
139 |
|
140 |
|
141 def limited_rql(self): |
|
142 """return a printable rql for the result set associated to the object, |
|
143 with limit/offset correctly set according to maximum page size and |
|
144 currently displayed page when necessary |
|
145 """ |
|
146 # try to get page boundaries from the navigation component |
|
147 # XXX we should probably not have a ref to this component here (eg in |
|
148 # cubicweb.common) |
|
149 nav = self.vreg.select_component('navigation', self.req, self.rset) |
|
150 if nav: |
|
151 start, stop = nav.page_boundaries() |
|
152 rql = self._limit_offset_rql(stop - start, start) |
|
153 # result set may have be limited manually in which case navigation won't |
|
154 # apply |
|
155 elif self.rset.limited: |
|
156 rql = self._limit_offset_rql(*self.rset.limited) |
|
157 # navigation component doesn't apply and rset has not been limited, no |
|
158 # need to limit query |
|
159 else: |
|
160 rql = self.rset.printable_rql() |
|
161 return rql |
|
162 |
|
163 def _limit_offset_rql(self, limit, offset): |
|
164 rqlst = self.rset.syntax_tree() |
|
165 if len(rqlst.children) == 1: |
|
166 select = rqlst.children[0] |
|
167 olimit, ooffset = select.limit, select.offset |
|
168 select.limit, select.offset = limit, offset |
|
169 rql = rqlst.as_string(kwargs=self.rset.args) |
|
170 # restore original limit/offset |
|
171 select.limit, select.offset = olimit, ooffset |
|
172 else: |
|
173 newselect = Select() |
|
174 newselect.limit = limit |
|
175 newselect.offset = offset |
|
176 aliases = [VariableRef(newselect.get_variable(vref.name, i)) |
|
177 for i, vref in enumerate(rqlst.selection)] |
|
178 newselect.set_with([SubQuery(aliases, rqlst)], check=False) |
|
179 newunion = Union() |
|
180 newunion.append(newselect) |
|
181 rql = rqlst.as_string(kwargs=self.rset.args) |
|
182 rqlst.parent = None |
|
183 return rql |
|
184 |
|
185 # url generation methods ################################################## |
|
186 |
|
187 controller = 'view' |
|
188 |
|
189 def build_url(self, method=None, **kwargs): |
|
190 """return an absolute URL using params dictionary key/values as URL |
|
191 parameters. Values are automatically URL quoted, and the |
|
192 publishing method to use may be specified or will be guessed. |
|
193 """ |
|
194 # XXX I (adim) think that if method is passed explicitly, we should |
|
195 # not try to process it and directly call req.build_url() |
|
196 if method is None: |
|
197 method = self.controller |
|
198 if method == 'view' and self.req.from_controller() == 'view' and \ |
|
199 not '_restpath' in kwargs: |
|
200 method = self.req.relative_path(includeparams=False) or 'view' |
|
201 return self.req.build_url(method, **kwargs) |
|
202 |
|
203 # various resources accessors ############################################# |
|
204 |
|
205 def etype_rset(self, etype, size=1): |
|
206 """return a fake result set for a particular entity type""" |
|
207 msg = '.etype_rset is deprecated, use req.etype_rset' |
|
208 warn(msg, DeprecationWarning, stacklevel=2) |
|
209 return self.req.etype_rset(etype, size=1) |
|
210 |
|
211 def eid_rset(self, eid, etype=None): |
|
212 """return a result set for the given eid""" |
|
213 msg = '.eid_rset is deprecated, use req.eid_rset' |
|
214 warn(msg, DeprecationWarning, stacklevel=2) |
|
215 return self.req.eid_rset(eid, etype) |
|
216 |
|
217 def entity(self, row, col=0): |
|
218 """short cut to get an entity instance for a particular row/column |
|
219 (col default to 0) |
|
220 """ |
|
221 return self.rset.get_entity(row, col) |
|
222 |
|
223 def complete_entity(self, row, col=0, skip_bytes=True): |
|
224 """short cut to get an completed entity instance for a particular |
|
225 row (all instance's attributes have been fetched) |
|
226 """ |
|
227 entity = self.entity(row, col) |
|
228 entity.complete(skip_bytes=skip_bytes) |
|
229 return entity |
|
230 |
|
231 def user_rql_callback(self, args, msg=None): |
|
232 """register a user callback to execute some rql query and return an url |
|
233 to call it ready to be inserted in html |
|
234 """ |
|
235 def rqlexec(req, rql, args=None, key=None): |
|
236 req.execute(rql, args, key) |
|
237 return self.user_callback(rqlexec, args, msg) |
|
238 |
|
239 def user_callback(self, cb, args, msg=None, nonify=False): |
|
240 """register the given user callback and return an url to call it ready to be |
|
241 inserted in html |
|
242 """ |
|
243 self.req.add_js('cubicweb.ajax.js') |
|
244 if nonify: |
|
245 # XXX < 2.48.3 bw compat |
|
246 warn('nonify argument is deprecated', DeprecationWarning, stacklevel=2) |
|
247 _cb = cb |
|
248 def cb(*args): |
|
249 _cb(*args) |
|
250 cbname = self.req.register_onetime_callback(cb, *args) |
|
251 msg = dumps(msg or '') |
|
252 return "javascript:userCallbackThenReloadPage('%s', %s)" % ( |
|
253 cbname, msg) |
|
254 |
|
255 # formating methods ####################################################### |
|
256 |
|
257 def tal_render(self, template, variables): |
|
258 """render a precompiled page template with variables in the given |
|
259 dictionary as context |
|
260 """ |
|
261 from cubicweb.common.tal import CubicWebContext |
|
262 context = CubicWebContext() |
|
263 context.update({'self': self, 'rset': self.rset, '_' : self.req._, |
|
264 'req': self.req, 'user': self.req.user}) |
|
265 context.update(variables) |
|
266 output = UStringIO() |
|
267 template.expand(context, output) |
|
268 return output.getvalue() |
|
269 |
|
270 def format_date(self, date, date_format=None, time=False): |
|
271 """return a string for a mx date time according to application's |
|
272 configuration |
|
273 """ |
|
274 if date: |
|
275 if date_format is None: |
|
276 if time: |
|
277 date_format = self.req.property_value('ui.datetime-format') |
|
278 else: |
|
279 date_format = self.req.property_value('ui.date-format') |
|
280 return ustrftime(date, date_format) |
|
281 return u'' |
|
282 |
|
283 def format_time(self, time): |
|
284 """return a string for a mx date time according to application's |
|
285 configuration |
|
286 """ |
|
287 if time: |
|
288 return ustrftime(time, self.req.property_value('ui.time-format')) |
|
289 return u'' |
|
290 |
|
291 def format_float(self, num): |
|
292 """return a string for floating point number according to application's |
|
293 configuration |
|
294 """ |
|
295 if num: |
|
296 return self.req.property_value('ui.float-format') % num |
|
297 return u'' |
|
298 |
|
299 # security related methods ################################################ |
|
300 |
|
301 def ensure_ro_rql(self, rql): |
|
302 """raise an exception if the given rql is not a select query""" |
|
303 first = rql.split(' ', 1)[0].lower() |
|
304 if first in ('insert', 'set', 'delete'): |
|
305 raise Unauthorized(self.req._('only select queries are authorized')) |
|
306 |
|
307 # .accepts handling utilities ############################################# |
|
308 |
|
309 accepts = ('Any',) |
|
310 |
|
311 @classmethod |
|
312 def accept_rset(cls, req, rset, row, col): |
|
313 """apply the following rules: |
|
314 * if row is None, return the sum of values returned by the method |
|
315 for each entity's type in the result set. If any score is 0, |
|
316 return 0. |
|
317 * if row is specified, return the value returned by the method with |
|
318 the entity's type of this row |
|
319 """ |
|
320 if row is None: |
|
321 score = 0 |
|
322 for etype in rset.column_types(0): |
|
323 accepted = cls.accept(req.user, etype) |
|
324 if not accepted: |
|
325 return 0 |
|
326 score += accepted |
|
327 return score |
|
328 return cls.accept(req.user, rset.description[row][col or 0]) |
|
329 |
|
330 @classmethod |
|
331 def accept(cls, user, etype): |
|
332 """score etype, returning better score on exact match""" |
|
333 if 'Any' in cls.accepts: |
|
334 return 1 |
|
335 eschema = cls.schema.eschema(etype) |
|
336 matching_types = [e.type for e in eschema.ancestors()] |
|
337 matching_types.append(etype) |
|
338 for index, basetype in enumerate(matching_types): |
|
339 if basetype in cls.accepts: |
|
340 return 2 + index |
|
341 return 0 |
|
342 |
|
343 # .rtype handling utilities ############################################## |
|
344 |
|
345 @classmethod |
|
346 def relation_possible(cls, etype): |
|
347 """tell if a relation with etype entity is possible according to |
|
348 mixed class'.etype, .rtype and .target attributes |
|
349 |
|
350 XXX should probably be moved out to a function |
|
351 """ |
|
352 schema = cls.schema |
|
353 rtype = cls.rtype |
|
354 eschema = schema.eschema(etype) |
|
355 if hasattr(cls, 'role'): |
|
356 role = cls.role |
|
357 elif cls.target == 'subject': |
|
358 role = 'object' |
|
359 else: |
|
360 role = 'subject' |
|
361 # check if this relation is possible according to the schema |
|
362 try: |
|
363 if role == 'object': |
|
364 rschema = eschema.object_relation(rtype) |
|
365 else: |
|
366 rschema = eschema.subject_relation(rtype) |
|
367 except KeyError: |
|
368 return False |
|
369 if hasattr(cls, 'etype'): |
|
370 letype = cls.etype |
|
371 try: |
|
372 if role == 'object': |
|
373 return etype in rschema.objects(letype) |
|
374 else: |
|
375 return etype in rschema.subjects(letype) |
|
376 except KeyError, ex: |
|
377 return False |
|
378 return True |
|
379 |
|
380 |
|
381 # XXX deprecated (since 2.43) ########################## |
|
382 |
|
383 @obsolete('use req.datadir_url') |
|
384 def datadir_url(self): |
|
385 """return url of the application's data directory""" |
|
386 return self.req.datadir_url |
|
387 |
|
388 @obsolete('use req.external_resource()') |
|
389 def external_resource(self, rid, default=_MARKER): |
|
390 return self.req.external_resource(rid, default) |
|
391 |
|
392 |
|
393 class AppObject(AppRsetObject): |
|
394 """base class for application objects which are not selected |
|
395 according to a result set, only by their identifier. |
|
396 |
|
397 Those objects may not have req, rset and cursor set. |
|
398 """ |
|
399 |
|
400 @classmethod |
|
401 def selected(cls, *args, **kwargs): |
|
402 """by default web app objects are usually instantiated on |
|
403 selection |
|
404 """ |
|
405 return cls(*args, **kwargs) |
|
406 |
|
407 def __init__(self, req=None, rset=None, **kwargs): |
|
408 self.req = req |
|
409 self.rset = rset |
|
410 self.__dict__.update(kwargs) |
|
411 |
|
412 |
|
413 class ReloadableMixIn(object): |
|
414 """simple mixin for reloadable parts of UI""" |
|
415 |
|
416 def user_callback(self, cb, args, msg=None, nonify=False): |
|
417 """register the given user callback and return an url to call it ready to be |
|
418 inserted in html |
|
419 """ |
|
420 self.req.add_js('cubicweb.ajax.js') |
|
421 if nonify: |
|
422 _cb = cb |
|
423 def cb(*args): |
|
424 _cb(*args) |
|
425 cbname = self.req.register_onetime_callback(cb, *args) |
|
426 return self.build_js(cbname, html_escape(msg or '')) |
|
427 |
|
428 def build_update_js_call(self, cbname, msg): |
|
429 rql = html_escape(self.rset.printable_rql()) |
|
430 return "javascript:userCallbackThenUpdateUI('%s', '%s', '%s', '%s', '%s', '%s')" % ( |
|
431 cbname, self.id, rql, msg, self.__registry__, self.div_id()) |
|
432 |
|
433 def build_reload_js_call(self, cbname, msg): |
|
434 return "javascript:userCallbackThenReloadPage('%s', '%s')" % (cbname, msg) |
|
435 |
|
436 build_js = build_update_js_call # expect updatable component by default |
|
437 |
|
438 def div_id(self): |
|
439 return '' |
|
440 |
|
441 |
|
442 class ComponentMixIn(ReloadableMixIn): |
|
443 """simple mixin for component object""" |
|
444 __registry__ = 'components' |
|
445 __registerer__ = yes_registerer |
|
446 __selectors__ = (yes,) |
|
447 __select__ = classmethod(*__selectors__) |
|
448 |
|
449 def div_class(self): |
|
450 return '%s %s' % (self.propval('htmlclass'), self.id) |
|
451 |
|
452 def div_id(self): |
|
453 return '%sComponent' % self.id |
|
454 |
|
455 |
|
456 class Component(ComponentMixIn, AppObject): |
|
457 """base class for non displayable components |
|
458 """ |
|
459 |
|
460 class SingletonComponent(Component): |
|
461 """base class for non displayable unique components |
|
462 """ |
|
463 __registerer__ = priority_registerer |
|