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