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