author | sylvain.thenault@logilab.fr |
Fri, 27 Mar 2009 12:25:24 +0100 | |
branch | tls-sprint |
changeset 1178 | 8d9b705168d3 |
parent 1174 | 7dac5aeafb86 |
child 1282 | 272d8ec6f308 |
permissions | -rw-r--r-- |
0 | 1 |
"""Base class for dynamically loaded objects manipulated in the web interface |
2 |
||
3 |
:organization: Logilab |
|
447 | 4 |
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
0 | 5 |
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
6 |
""" |
|
7 |
__docformat__ = "restructuredtext en" |
|
8 |
||
1016
26387b836099
use datetime instead of mx.DateTime
sylvain.thenault@logilab.fr
parents:
875
diff
changeset
|
9 |
from datetime import datetime, timedelta |
0 | 10 |
|
11 |
from simplejson import dumps |
|
12 |
||
699 | 13 |
from logilab.common.decorators import classproperty |
0 | 14 |
from logilab.common.deprecation import obsolete |
447 | 15 |
|
16 |
from rql.nodes import VariableRef, SubQuery |
|
0 | 17 |
from rql.stmts import Union, Select |
18 |
||
1174 | 19 |
from cubicweb import Unauthorized, NoSelectableObject |
726
88a74f590986
improve __selectors__() so that it always return a tuple
Adrien Di Mascio <Adrien.DiMascio@logilab.fr>
parents:
721
diff
changeset
|
20 |
from cubicweb.vregistry import VObject, AndSelector |
692
800592b8d39b
replace deprecated cubicweb.common.selectors by its new module path (cubicweb.selectors)
Adrien Di Mascio <Adrien.DiMascio@logilab.fr>
parents:
683
diff
changeset
|
21 |
from cubicweb.selectors import yes |
709 | 22 |
from cubicweb.utils import UStringIO, ustrftime |
0 | 23 |
|
1016
26387b836099
use datetime instead of mx.DateTime
sylvain.thenault@logilab.fr
parents:
875
diff
changeset
|
24 |
ONESECOND = timedelta(0, 1, 0) |
0 | 25 |
|
26 |
class Cache(dict): |
|
27 |
def __init__(self): |
|
28 |
super(Cache, self).__init__() |
|
29 |
self.cache_creation_date = None |
|
1016
26387b836099
use datetime instead of mx.DateTime
sylvain.thenault@logilab.fr
parents:
875
diff
changeset
|
30 |
self.latest_cache_lookup = datetime.now() |
0 | 31 |
|
32 |
CACHE_REGISTRY = {} |
|
33 |
||
34 |
class AppRsetObject(VObject): |
|
35 |
"""This is the base class for CubicWeb application objects |
|
36 |
which are selected according to a request and result set. |
|
37 |
|
|
38 |
Classes are kept in the vregistry and instantiation is done at selection |
|
39 |
time. |
|
40 |
|
|
41 |
At registration time, the following attributes are set on the class: |
|
42 |
:vreg: |
|
43 |
the application's registry |
|
44 |
:schema: |
|
45 |
the application's schema |
|
46 |
:config: |
|
47 |
the application's configuration |
|
48 |
||
49 |
At instantiation time, the following attributes are set on the instance: |
|
50 |
:req: |
|
51 |
current request |
|
52 |
:rset: |
|
53 |
result set on which the object is applied |
|
54 |
""" |
|
805
6e99feeba28b
set a default selector on base app objects class
sylvain.thenault@logilab.fr
parents:
802
diff
changeset
|
55 |
__select__ = yes() |
0 | 56 |
|
57 |
@classmethod |
|
58 |
def registered(cls, vreg): |
|
721 | 59 |
super(AppRsetObject, cls).registered(vreg) |
0 | 60 |
cls.vreg = vreg |
61 |
cls.schema = vreg.schema |
|
62 |
cls.config = vreg.config |
|
63 |
cls.register_properties() |
|
64 |
return cls |
|
65 |
||
66 |
@classmethod |
|
683 | 67 |
def selected(cls, *args, **kwargs): |
0 | 68 |
"""by default web app objects are usually instantiated on |
69 |
selection according to a request, a result set, and optional |
|
70 |
row and col |
|
71 |
""" |
|
875 | 72 |
assert len(args) <= 2 |
1143
8d097defbf2c
change the way selected/__init__ are used for AppRsetObject
sylvain.thenault@logilab.fr
parents:
1130
diff
changeset
|
73 |
return cls(*args, **kwargs) |
0 | 74 |
|
75 |
# Eproperties definition: |
|
76 |
# key: id of the property (the actual EProperty key is build using |
|
77 |
# <registry name>.<obj id>.<property id> |
|
78 |
# value: tuple (property type, vocabfunc, default value, property description) |
|
79 |
# possible types are those used by `logilab.common.configuration` |
|
80 |
# |
|
81 |
# notice that when it exists multiple objects with the same id (adaptation, |
|
82 |
# overriding) only the first encountered definition is considered, so those |
|
83 |
# objects can't try to have different default values for instance. |
|
84 |
||
85 |
property_defs = {} |
|
86 |
||
87 |
@classmethod |
|
88 |
def register_properties(cls): |
|
89 |
for propid, pdef in cls.property_defs.items(): |
|
90 |
pdef = pdef.copy() # may be shared |
|
91 |
pdef['default'] = getattr(cls, propid, pdef['default']) |
|
92 |
pdef['sitewide'] = getattr(cls, 'site_wide', pdef.get('sitewide')) |
|
93 |
cls.vreg.register_property(cls.propkey(propid), **pdef) |
|
94 |
||
95 |
@classmethod |
|
96 |
def propkey(cls, propid): |
|
97 |
return '%s.%s.%s' % (cls.__registry__, cls.id, propid) |
|
699 | 98 |
|
99 |
@classproperty |
|
100 |
@obsolete('use __select__ and & or | operators') |
|
101 |
def __selectors__(cls): |
|
726
88a74f590986
improve __selectors__() so that it always return a tuple
Adrien Di Mascio <Adrien.DiMascio@logilab.fr>
parents:
721
diff
changeset
|
102 |
selector = cls.__select__ |
88a74f590986
improve __selectors__() so that it always return a tuple
Adrien Di Mascio <Adrien.DiMascio@logilab.fr>
parents:
721
diff
changeset
|
103 |
if isinstance(selector, AndSelector): |
732
45c3414ac002
__selectors__ must return a tuple
Adrien Di Mascio <Adrien.DiMascio@logilab.fr>
parents:
726
diff
changeset
|
104 |
return tuple(selector.selectors) |
726
88a74f590986
improve __selectors__() so that it always return a tuple
Adrien Di Mascio <Adrien.DiMascio@logilab.fr>
parents:
721
diff
changeset
|
105 |
if not isinstance(selector, tuple): |
88a74f590986
improve __selectors__() so that it always return a tuple
Adrien Di Mascio <Adrien.DiMascio@logilab.fr>
parents:
721
diff
changeset
|
106 |
selector = (selector,) |
88a74f590986
improve __selectors__() so that it always return a tuple
Adrien Di Mascio <Adrien.DiMascio@logilab.fr>
parents:
721
diff
changeset
|
107 |
return selector |
699 | 108 |
|
1143
8d097defbf2c
change the way selected/__init__ are used for AppRsetObject
sylvain.thenault@logilab.fr
parents:
1130
diff
changeset
|
109 |
def __init__(self, req=None, rset=None, row=None, col=None, **extra): |
0 | 110 |
super(AppRsetObject, self).__init__() |
111 |
self.req = req |
|
112 |
self.rset = rset |
|
1143
8d097defbf2c
change the way selected/__init__ are used for AppRsetObject
sylvain.thenault@logilab.fr
parents:
1130
diff
changeset
|
113 |
self.row = row |
8d097defbf2c
change the way selected/__init__ are used for AppRsetObject
sylvain.thenault@logilab.fr
parents:
1130
diff
changeset
|
114 |
self.col = col |
8d097defbf2c
change the way selected/__init__ are used for AppRsetObject
sylvain.thenault@logilab.fr
parents:
1130
diff
changeset
|
115 |
self.extra_kwargs = extra |
0 | 116 |
|
117 |
def get_cache(self, cachename): |
|
118 |
""" |
|
119 |
NOTE: cachename should be dotted names as in : |
|
120 |
- cubicweb.mycache |
|
121 |
- cubes.blog.mycache |
|
122 |
- etc. |
|
123 |
""" |
|
124 |
if cachename in CACHE_REGISTRY: |
|
125 |
cache = CACHE_REGISTRY[cachename] |
|
126 |
else: |
|
127 |
cache = Cache() |
|
128 |
CACHE_REGISTRY[cachename] = cache |
|
1016
26387b836099
use datetime instead of mx.DateTime
sylvain.thenault@logilab.fr
parents:
875
diff
changeset
|
129 |
_now = datetime.now() |
26387b836099
use datetime instead of mx.DateTime
sylvain.thenault@logilab.fr
parents:
875
diff
changeset
|
130 |
if _now > cache.latest_cache_lookup + ONESECOND: |
0 | 131 |
ecache = self.req.execute('Any C,T WHERE C is ECache, C name %(name)s, C timestamp T', |
132 |
{'name':cachename}).get_entity(0,0) |
|
133 |
cache.latest_cache_lookup = _now |
|
134 |
if not ecache.valid(cache.cache_creation_date): |
|
1130
17ff4d4bfbd0
should call clear, not empth (thanks pylint)
sylvain.thenault@logilab.fr
parents:
1129
diff
changeset
|
135 |
cache.clear() |
0 | 136 |
cache.cache_creation_date = _now |
137 |
return cache |
|
138 |
||
139 |
def propval(self, propid): |
|
140 |
assert self.req |
|
141 |
return self.req.property_value(self.propkey(propid)) |
|
142 |
||
143 |
def limited_rql(self): |
|
144 |
"""return a printable rql for the result set associated to the object, |
|
145 |
with limit/offset correctly set according to maximum page size and |
|
146 |
currently displayed page when necessary |
|
147 |
""" |
|
148 |
# try to get page boundaries from the navigation component |
|
149 |
# XXX we should probably not have a ref to this component here (eg in |
|
150 |
# cubicweb.common) |
|
151 |
nav = self.vreg.select_component('navigation', self.req, self.rset) |
|
152 |
if nav: |
|
153 |
start, stop = nav.page_boundaries() |
|
154 |
rql = self._limit_offset_rql(stop - start, start) |
|
155 |
# result set may have be limited manually in which case navigation won't |
|
156 |
# apply |
|
157 |
elif self.rset.limited: |
|
158 |
rql = self._limit_offset_rql(*self.rset.limited) |
|
159 |
# navigation component doesn't apply and rset has not been limited, no |
|
160 |
# need to limit query |
|
161 |
else: |
|
162 |
rql = self.rset.printable_rql() |
|
163 |
return rql |
|
164 |
||
165 |
def _limit_offset_rql(self, limit, offset): |
|
166 |
rqlst = self.rset.syntax_tree() |
|
167 |
if len(rqlst.children) == 1: |
|
168 |
select = rqlst.children[0] |
|
169 |
olimit, ooffset = select.limit, select.offset |
|
170 |
select.limit, select.offset = limit, offset |
|
171 |
rql = rqlst.as_string(kwargs=self.rset.args) |
|
172 |
# restore original limit/offset |
|
173 |
select.limit, select.offset = olimit, ooffset |
|
174 |
else: |
|
175 |
newselect = Select() |
|
176 |
newselect.limit = limit |
|
177 |
newselect.offset = offset |
|
178 |
aliases = [VariableRef(newselect.get_variable(vref.name, i)) |
|
179 |
for i, vref in enumerate(rqlst.selection)] |
|
180 |
newselect.set_with([SubQuery(aliases, rqlst)], check=False) |
|
181 |
newunion = Union() |
|
182 |
newunion.append(newselect) |
|
183 |
rql = rqlst.as_string(kwargs=self.rset.args) |
|
184 |
rqlst.parent = None |
|
185 |
return rql |
|
1144
654047cd0c30
move .view method on AppRsetObject, move initialize_varmaker method on View
sylvain.thenault@logilab.fr
parents:
1143
diff
changeset
|
186 |
|
654047cd0c30
move .view method on AppRsetObject, move initialize_varmaker method on View
sylvain.thenault@logilab.fr
parents:
1143
diff
changeset
|
187 |
def view(self, __vid, rset=None, __fallback_vid=None, **kwargs): |
654047cd0c30
move .view method on AppRsetObject, move initialize_varmaker method on View
sylvain.thenault@logilab.fr
parents:
1143
diff
changeset
|
188 |
"""shortcut to self.vreg.render method avoiding to pass self.req""" |
654047cd0c30
move .view method on AppRsetObject, move initialize_varmaker method on View
sylvain.thenault@logilab.fr
parents:
1143
diff
changeset
|
189 |
try: |
654047cd0c30
move .view method on AppRsetObject, move initialize_varmaker method on View
sylvain.thenault@logilab.fr
parents:
1143
diff
changeset
|
190 |
view = self.vreg.select_view(__vid, self.req, rset, **kwargs) |
654047cd0c30
move .view method on AppRsetObject, move initialize_varmaker method on View
sylvain.thenault@logilab.fr
parents:
1143
diff
changeset
|
191 |
except NoSelectableObject: |
654047cd0c30
move .view method on AppRsetObject, move initialize_varmaker method on View
sylvain.thenault@logilab.fr
parents:
1143
diff
changeset
|
192 |
if __fallback_vid is None: |
654047cd0c30
move .view method on AppRsetObject, move initialize_varmaker method on View
sylvain.thenault@logilab.fr
parents:
1143
diff
changeset
|
193 |
raise |
654047cd0c30
move .view method on AppRsetObject, move initialize_varmaker method on View
sylvain.thenault@logilab.fr
parents:
1143
diff
changeset
|
194 |
view = self.vreg.select_view(__fallback_vid, self.req, rset, **kwargs) |
654047cd0c30
move .view method on AppRsetObject, move initialize_varmaker method on View
sylvain.thenault@logilab.fr
parents:
1143
diff
changeset
|
195 |
return view.dispatch(**kwargs) |
0 | 196 |
|
197 |
# url generation methods ################################################## |
|
198 |
||
199 |
controller = 'view' |
|
200 |
||
201 |
def build_url(self, method=None, **kwargs): |
|
202 |
"""return an absolute URL using params dictionary key/values as URL |
|
203 |
parameters. Values are automatically URL quoted, and the |
|
204 |
publishing method to use may be specified or will be guessed. |
|
205 |
""" |
|
206 |
# XXX I (adim) think that if method is passed explicitly, we should |
|
207 |
# not try to process it and directly call req.build_url() |
|
208 |
if method is None: |
|
209 |
method = self.controller |
|
210 |
if method == 'view' and self.req.from_controller() == 'view' and \ |
|
211 |
not '_restpath' in kwargs: |
|
212 |
method = self.req.relative_path(includeparams=False) or 'view' |
|
213 |
return self.req.build_url(method, **kwargs) |
|
214 |
||
215 |
# various resources accessors ############################################# |
|
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 |
cbname = self.req.register_onetime_callback(cb, *args) |
|
245 |
msg = dumps(msg or '') |
|
246 |
return "javascript:userCallbackThenReloadPage('%s', %s)" % ( |
|
247 |
cbname, msg) |
|
248 |
||
249 |
# formating methods ####################################################### |
|
250 |
||
251 |
def tal_render(self, template, variables): |
|
252 |
"""render a precompiled page template with variables in the given |
|
253 |
dictionary as context |
|
254 |
""" |
|
255 |
from cubicweb.common.tal import CubicWebContext |
|
256 |
context = CubicWebContext() |
|
257 |
context.update({'self': self, 'rset': self.rset, '_' : self.req._, |
|
258 |
'req': self.req, 'user': self.req.user}) |
|
259 |
context.update(variables) |
|
260 |
output = UStringIO() |
|
261 |
template.expand(context, output) |
|
262 |
return output.getvalue() |
|
263 |
||
264 |
def format_date(self, date, date_format=None, time=False): |
|
1016
26387b836099
use datetime instead of mx.DateTime
sylvain.thenault@logilab.fr
parents:
875
diff
changeset
|
265 |
"""return a string for a date time according to application's |
0 | 266 |
configuration |
267 |
""" |
|
268 |
if date: |
|
269 |
if date_format is None: |
|
270 |
if time: |
|
271 |
date_format = self.req.property_value('ui.datetime-format') |
|
272 |
else: |
|
273 |
date_format = self.req.property_value('ui.date-format') |
|
274 |
return ustrftime(date, date_format) |
|
275 |
return u'' |
|
276 |
||
277 |
def format_time(self, time): |
|
1016
26387b836099
use datetime instead of mx.DateTime
sylvain.thenault@logilab.fr
parents:
875
diff
changeset
|
278 |
"""return a string for a time according to application's |
0 | 279 |
configuration |
280 |
""" |
|
281 |
if time: |
|
282 |
return ustrftime(time, self.req.property_value('ui.time-format')) |
|
283 |
return u'' |
|
284 |
||
285 |
def format_float(self, num): |
|
286 |
"""return a string for floating point number according to application's |
|
287 |
configuration |
|
288 |
""" |
|
289 |
if num: |
|
290 |
return self.req.property_value('ui.float-format') % num |
|
291 |
return u'' |
|
292 |
||
293 |
# security related methods ################################################ |
|
294 |
||
295 |
def ensure_ro_rql(self, rql): |
|
296 |
"""raise an exception if the given rql is not a select query""" |
|
297 |
first = rql.split(' ', 1)[0].lower() |
|
298 |
if first in ('insert', 'set', 'delete'): |
|
299 |
raise Unauthorized(self.req._('only select queries are authorized')) |
|
300 |
||
301 |
||
302 |
class AppObject(AppRsetObject): |
|
303 |
"""base class for application objects which are not selected |
|
304 |
according to a result set, only by their identifier. |
|
305 |
|
|
306 |
Those objects may not have req, rset and cursor set. |
|
307 |
""" |
|
308 |
||
309 |
@classmethod |
|
310 |
def selected(cls, *args, **kwargs): |
|
311 |
"""by default web app objects are usually instantiated on |
|
312 |
selection |
|
313 |
""" |
|
314 |
return cls(*args, **kwargs) |
|
315 |
||
316 |
def __init__(self, req=None, rset=None, **kwargs): |
|
317 |
self.req = req |
|
318 |
self.rset = rset |
|
319 |
self.__dict__.update(kwargs) |