1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """Set of HTML base actions""" |
|
19 |
|
20 __docformat__ = "restructuredtext en" |
|
21 from cubicweb import _ |
|
22 |
|
23 from warnings import warn |
|
24 |
|
25 from logilab.mtconverter import xml_escape |
|
26 from logilab.common.registry import objectify_predicate, yes |
|
27 |
|
28 from cubicweb.schema import display_name |
|
29 from cubicweb.predicates import (EntityPredicate, |
|
30 one_line_rset, multi_lines_rset, one_etype_rset, relation_possible, |
|
31 nonempty_rset, non_final_entity, score_entity, |
|
32 authenticated_user, match_user_groups, match_search_state, |
|
33 has_permission, has_add_permission, is_instance, debug_mode, |
|
34 ) |
|
35 from cubicweb.web import controller, action |
|
36 from cubicweb.web.views import uicfg, linksearch_select_url, vid_from_rset |
|
37 |
|
38 |
|
39 class has_editable_relation(EntityPredicate): |
|
40 """accept if some relations for an entity found in the result set is |
|
41 editable by the logged user. |
|
42 |
|
43 See `EntityPredicate` documentation for behaviour when row is not specified. |
|
44 """ |
|
45 |
|
46 def score_entity(self, entity): |
|
47 # if user has no update right but it can modify some relation, |
|
48 # display action anyway |
|
49 form = entity._cw.vreg['forms'].select('edition', entity._cw, |
|
50 entity=entity, mainform=False) |
|
51 for dummy in form.editable_relations(): |
|
52 return 1 |
|
53 for dummy in form.inlined_form_views(): |
|
54 return 1 |
|
55 for dummy in form.editable_attributes(strict=True): |
|
56 return 1 |
|
57 return 0 |
|
58 |
|
59 @objectify_predicate |
|
60 def match_searched_etype(cls, req, rset=None, **kwargs): |
|
61 return req.match_search_state(rset) |
|
62 |
|
63 @objectify_predicate |
|
64 def view_is_not_default_view(cls, req, rset=None, **kwargs): |
|
65 # interesting if it propose another view than the current one |
|
66 vid = req.form.get('vid') |
|
67 if vid and vid != vid_from_rset(req, rset, req.vreg.schema): |
|
68 return 1 |
|
69 return 0 |
|
70 |
|
71 @objectify_predicate |
|
72 def addable_etype_empty_rset(cls, req, rset=None, **kwargs): |
|
73 if rset is not None and not rset.rowcount: |
|
74 rqlst = rset.syntax_tree() |
|
75 if len(rqlst.children) > 1: |
|
76 return 0 |
|
77 select = rqlst.children[0] |
|
78 if len(select.defined_vars) == 1 and len(select.solutions) == 1: |
|
79 rset._searched_etype = next(iter(select.solutions[0].values())) |
|
80 eschema = req.vreg.schema.eschema(rset._searched_etype) |
|
81 if not (eschema.final or eschema.is_subobject(strict=True)) \ |
|
82 and eschema.has_perm(req, 'add'): |
|
83 return 1 |
|
84 return 0 |
|
85 |
|
86 class has_undoable_transactions(EntityPredicate): |
|
87 "Select entities having public (i.e. end-user) undoable transactions." |
|
88 |
|
89 def score_entity(self, entity): |
|
90 if not entity._cw.vreg.config['undo-enabled']: |
|
91 return 0 |
|
92 if entity._cw.cnx.undoable_transactions(eid=entity.eid): |
|
93 return 1 |
|
94 else: |
|
95 return 0 |
|
96 |
|
97 |
|
98 # generic 'main' actions ####################################################### |
|
99 |
|
100 class SelectAction(action.Action): |
|
101 """base class for link search actions. By default apply on |
|
102 any size entity result search it the current state is 'linksearch' |
|
103 if accept match. |
|
104 """ |
|
105 __regid__ = 'select' |
|
106 __select__ = (match_search_state('linksearch') & nonempty_rset() |
|
107 & match_searched_etype()) |
|
108 |
|
109 title = _('select') |
|
110 category = 'mainactions' |
|
111 order = 0 |
|
112 |
|
113 def url(self): |
|
114 return linksearch_select_url(self._cw, self.cw_rset) |
|
115 |
|
116 |
|
117 class CancelSelectAction(action.Action): |
|
118 __regid__ = 'cancel' |
|
119 __select__ = match_search_state('linksearch') |
|
120 |
|
121 title = _('cancel select') |
|
122 category = 'mainactions' |
|
123 order = 10 |
|
124 |
|
125 def url(self): |
|
126 target, eid, r_type, searched_type = self._cw.search_state[1] |
|
127 return self._cw.build_url(str(eid), |
|
128 vid='edition', __mode='normal') |
|
129 |
|
130 |
|
131 class ViewAction(action.Action): |
|
132 __regid__ = 'view' |
|
133 __select__ = (action.Action.__select__ & |
|
134 match_user_groups('users', 'managers') & |
|
135 view_is_not_default_view() & |
|
136 non_final_entity()) |
|
137 |
|
138 title = _('view') |
|
139 category = 'mainactions' |
|
140 order = 0 |
|
141 |
|
142 def url(self): |
|
143 params = self._cw.form.copy() |
|
144 for param in ('vid', '__message') + controller.NAV_FORM_PARAMETERS: |
|
145 params.pop(param, None) |
|
146 if self._cw.ajax_request: |
|
147 path = 'view' |
|
148 if self.cw_rset is not None: |
|
149 params = {'rql': self.cw_rset.printable_rql()} |
|
150 else: |
|
151 path = self._cw.relative_path(includeparams=False) |
|
152 return self._cw.build_url(path, **params) |
|
153 |
|
154 |
|
155 class ModifyAction(action.Action): |
|
156 __regid__ = 'edit' |
|
157 __select__ = (action.Action.__select__ |
|
158 & one_line_rset() & has_editable_relation()) |
|
159 |
|
160 title = _('modify') |
|
161 category = 'mainactions' |
|
162 order = 10 |
|
163 |
|
164 def url(self): |
|
165 entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) |
|
166 return entity.absolute_url(vid='edition') |
|
167 |
|
168 |
|
169 class MultipleEditAction(action.Action): |
|
170 __regid__ = 'muledit' # XXX get strange conflicts if id='edit' |
|
171 __select__ = (action.Action.__select__ & multi_lines_rset() & |
|
172 one_etype_rset() & has_permission('update')) |
|
173 |
|
174 title = _('modify') |
|
175 category = 'mainactions' |
|
176 order = 10 |
|
177 |
|
178 def url(self): |
|
179 return self._cw.build_url('view', rql=self.cw_rset.printable_rql(), vid='muledit') |
|
180 |
|
181 |
|
182 # generic "more" actions ####################################################### |
|
183 |
|
184 class ManagePermissionsAction(action.Action): |
|
185 __regid__ = 'managepermission' |
|
186 __select__ = (action.Action.__select__ & one_line_rset() & |
|
187 non_final_entity() & match_user_groups('managers')) |
|
188 |
|
189 title = _('manage permissions') |
|
190 category = 'moreactions' |
|
191 order = 15 |
|
192 |
|
193 def url(self): |
|
194 return self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0).absolute_url(vid='security') |
|
195 |
|
196 |
|
197 class DeleteAction(action.Action): |
|
198 __regid__ = 'delete' |
|
199 __select__ = action.Action.__select__ & has_permission('delete') |
|
200 |
|
201 title = _('delete') |
|
202 category = 'moreactions' |
|
203 order = 20 |
|
204 |
|
205 def url(self): |
|
206 if len(self.cw_rset) == 1: |
|
207 entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) |
|
208 return self._cw.build_url(entity.rest_path(), vid='deleteconf') |
|
209 return self._cw.build_url(rql=self.cw_rset.printable_rql(), vid='deleteconf') |
|
210 |
|
211 |
|
212 class CopyAction(action.Action): |
|
213 __regid__ = 'copy' |
|
214 __select__ = (action.Action.__select__ & one_line_rset() |
|
215 & has_permission('add')) |
|
216 |
|
217 title = _('copy') |
|
218 category = 'moreactions' |
|
219 order = 30 |
|
220 |
|
221 def url(self): |
|
222 entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) |
|
223 return entity.absolute_url(vid='copy') |
|
224 |
|
225 |
|
226 class AddNewAction(MultipleEditAction): |
|
227 """when we're seeing more than one entity with the same type, propose to |
|
228 add a new one |
|
229 """ |
|
230 __regid__ = 'addentity' |
|
231 __select__ = (action.Action.__select__ & |
|
232 (addable_etype_empty_rset() |
|
233 | (multi_lines_rset() & one_etype_rset() & has_add_permission())) |
|
234 ) |
|
235 |
|
236 category = 'moreactions' |
|
237 order = 40 |
|
238 |
|
239 @property |
|
240 def rsettype(self): |
|
241 if self.cw_rset: |
|
242 return self.cw_rset.description[0][0] |
|
243 return self.cw_rset._searched_etype |
|
244 |
|
245 @property |
|
246 def title(self): |
|
247 return self._cw.__('add a %s' % self.rsettype) # generated msgid |
|
248 |
|
249 def url(self): |
|
250 return self._cw.vreg["etypes"].etype_class(self.rsettype).cw_create_url(self._cw) |
|
251 |
|
252 |
|
253 class AddRelatedActions(action.Action): |
|
254 """fill 'addrelated' sub-menu of the actions box""" |
|
255 __regid__ = 'addrelated' |
|
256 __select__ = action.Action.__select__ & one_line_rset() & non_final_entity() |
|
257 |
|
258 submenu = _('addrelated') |
|
259 order = 17 |
|
260 |
|
261 def fill_menu(self, box, menu): |
|
262 # when there is only one item in the sub-menu, replace the sub-menu by |
|
263 # item's title prefixed by 'add' |
|
264 menu.label_prefix = self._cw._('add') |
|
265 super(AddRelatedActions, self).fill_menu(box, menu) |
|
266 |
|
267 def redirect_params(self, entity): |
|
268 return {'__redirectpath': entity.rest_path(), # should not be url quoted! |
|
269 '__redirectvid': self._cw.form.get('vid', '')} |
|
270 |
|
271 def actual_actions(self): |
|
272 entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) |
|
273 eschema = entity.e_schema |
|
274 params = self.redirect_params(entity) |
|
275 for rschema, teschema, role in self.add_related_schemas(entity): |
|
276 if rschema.role_rdef(eschema, teschema, role).role_cardinality(role) in '1?': |
|
277 if entity.related(rschema, role): |
|
278 continue |
|
279 if role == 'subject': |
|
280 label = 'add %s %s %s %s' % (eschema, rschema, teschema, role) |
|
281 url = self.linkto_url(entity, rschema, teschema, 'object', **params) |
|
282 else: |
|
283 label = 'add %s %s %s %s' % (teschema, rschema, eschema, role) |
|
284 url = self.linkto_url(entity, rschema, teschema, 'subject', **params) |
|
285 yield self.build_action(self._cw._(label), url) |
|
286 |
|
287 def add_related_schemas(self, entity): |
|
288 """this is actually used ui method to generate 'addrelated' actions from |
|
289 the schema. |
|
290 |
|
291 If you don't want any auto-generated actions, you should overrides this |
|
292 method to return an empty list. If you only want some, you can configure |
|
293 them by using uicfg.actionbox_appearsin_addmenu |
|
294 """ |
|
295 appearsin_addmenu = self._cw.vreg['uicfg'].select( |
|
296 'actionbox_appearsin_addmenu', self._cw, entity=entity) |
|
297 req = self._cw |
|
298 eschema = entity.e_schema |
|
299 for role, rschemas in (('subject', eschema.subject_relations()), |
|
300 ('object', eschema.object_relations())): |
|
301 for rschema in rschemas: |
|
302 if rschema.final: |
|
303 continue |
|
304 for teschema in rschema.targets(eschema, role): |
|
305 if not appearsin_addmenu.etype_get(eschema, rschema, |
|
306 role, teschema): |
|
307 continue |
|
308 rdef = rschema.role_rdef(eschema, teschema, role) |
|
309 # check the relation can be added |
|
310 # XXX consider autoform_permissions_overrides? |
|
311 if role == 'subject'and not rdef.has_perm( |
|
312 req, 'add', fromeid=entity.eid): |
|
313 continue |
|
314 if role == 'object'and not rdef.has_perm( |
|
315 req, 'add', toeid=entity.eid): |
|
316 continue |
|
317 # check the target types can be added as well |
|
318 if teschema.may_have_permission('add', req): |
|
319 yield rschema, teschema, role |
|
320 |
|
321 def linkto_url(self, entity, rtype, etype, target, **kwargs): |
|
322 return self._cw.vreg["etypes"].etype_class(etype).cw_create_url( |
|
323 self._cw, __linkto='%s:%s:%s' % (rtype, entity.eid, target), |
|
324 **kwargs) |
|
325 |
|
326 |
|
327 class ViewSameCWEType(action.Action): |
|
328 """when displaying the schema of a CWEType, offer to list entities of that type |
|
329 """ |
|
330 __regid__ = 'entitiesoftype' |
|
331 __select__ = one_line_rset() & is_instance('CWEType') & score_entity(lambda x: not x.final) |
|
332 category = 'mainactions' |
|
333 order = 40 |
|
334 |
|
335 @property |
|
336 def etype(self): |
|
337 return self.cw_rset.get_entity(0,0).name |
|
338 |
|
339 @property |
|
340 def title(self): |
|
341 return self._cw.__('view all %s') % display_name(self._cw, self.etype, 'plural').lower() |
|
342 |
|
343 def url(self): |
|
344 return self._cw.build_url(self.etype) |
|
345 |
|
346 # logged user actions ######################################################### |
|
347 |
|
348 class UserPreferencesAction(action.Action): |
|
349 __regid__ = 'myprefs' |
|
350 __select__ = authenticated_user() |
|
351 |
|
352 title = _('user preferences') |
|
353 category = 'useractions' |
|
354 order = 10 |
|
355 |
|
356 def url(self): |
|
357 return self._cw.build_url(self.__regid__) |
|
358 |
|
359 |
|
360 class UserInfoAction(action.Action): |
|
361 __regid__ = 'myinfos' |
|
362 __select__ = authenticated_user() |
|
363 |
|
364 title = _('profile') |
|
365 category = 'useractions' |
|
366 order = 20 |
|
367 |
|
368 def url(self): |
|
369 return self._cw.build_url('cwuser/%s'%self._cw.user.login, vid='edition') |
|
370 |
|
371 |
|
372 class LogoutAction(action.Action): |
|
373 __regid__ = 'logout' |
|
374 __select__ = authenticated_user() |
|
375 |
|
376 title = _('logout') |
|
377 category = 'useractions' |
|
378 order = 30 |
|
379 |
|
380 def url(self): |
|
381 return self._cw.build_url(self.__regid__) |
|
382 |
|
383 |
|
384 # site actions ################################################################ |
|
385 |
|
386 class ManagersAction(action.Action): |
|
387 __abstract__ = True |
|
388 __select__ = match_user_groups('managers') |
|
389 |
|
390 category = 'siteactions' |
|
391 |
|
392 def url(self): |
|
393 return self._cw.build_url(self.__regid__) |
|
394 |
|
395 |
|
396 class SiteConfigurationAction(ManagersAction): |
|
397 __regid__ = 'siteconfig' |
|
398 title = _('site configuration') |
|
399 order = 10 |
|
400 category = 'manage' |
|
401 |
|
402 |
|
403 class ManageAction(ManagersAction): |
|
404 __regid__ = 'manage' |
|
405 title = _('manage') |
|
406 order = 20 |
|
407 |
|
408 |
|
409 # footer actions ############################################################### |
|
410 |
|
411 class PoweredByAction(action.Action): |
|
412 __regid__ = 'poweredby' |
|
413 __select__ = yes() |
|
414 |
|
415 category = 'footer' |
|
416 order = 3 |
|
417 title = _('Powered by CubicWeb') |
|
418 |
|
419 def url(self): |
|
420 return 'http://www.cubicweb.org' |
|
421 |
|
422 ## default actions ui configuration ########################################### |
|
423 |
|
424 addmenu = uicfg.actionbox_appearsin_addmenu |
|
425 addmenu.tag_object_of(('*', 'relation_type', 'CWRType'), True) |
|
426 addmenu.tag_object_of(('*', 'from_entity', 'CWEType'), False) |
|
427 addmenu.tag_object_of(('*', 'to_entity', 'CWEType'), False) |
|
428 addmenu.tag_object_of(('*', 'in_group', 'CWGroup'), True) |
|
429 addmenu.tag_object_of(('*', 'bookmarked_by', 'CWUser'), True) |
|