web/views/actions.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     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)