Commit 0e7bc2af authored by sylvain.thenault@logilab.fr's avatar sylvain.thenault@logilab.fr
Browse files

major selector refactoring (mostly to avoid looking for select parameters on...

major selector refactoring (mostly to avoid looking for select parameters on the target class), start accept / interface unification)

--HG--
branch : tls-sprint
parent 66ff0b2f7d03
......@@ -304,91 +304,6 @@ class AppRsetObject(VObject):
if first in ('insert', 'set', 'delete'):
raise Unauthorized(self.req._('only select queries are authorized'))
# .accepts handling utilities #############################################
accepts = ('Any',)
@classmethod
def accept_rset(cls, req, rset, row, col):
"""apply the following rules:
* if row is None, return the sum of values returned by the method
for each entity's type in the result set. If any score is 0,
return 0.
* if row is specified, return the value returned by the method with
the entity's type of this row
"""
if row is None:
score = 0
for etype in rset.column_types(0):
accepted = cls.accept(req.user, etype)
if not accepted:
return 0
score += accepted
return score
return cls.accept(req.user, rset.description[row][col or 0])
@classmethod
def accept(cls, user, etype):
"""score etype, returning better score on exact match"""
if 'Any' in cls.accepts:
return 1
eschema = cls.schema.eschema(etype)
matching_types = [e.type for e in eschema.ancestors()]
matching_types.append(etype)
for index, basetype in enumerate(matching_types):
if basetype in cls.accepts:
return 2 + index
return 0
# .rtype handling utilities ##############################################
@classmethod
def relation_possible(cls, etype):
"""tell if a relation with etype entity is possible according to
mixed class'.etype, .rtype and .target attributes
XXX should probably be moved out to a function
"""
schema = cls.schema
rtype = cls.rtype
eschema = schema.eschema(etype)
if hasattr(cls, 'role'):
role = cls.role
elif cls.target == 'subject':
role = 'object'
else:
role = 'subject'
# check if this relation is possible according to the schema
try:
if role == 'object':
rschema = eschema.object_relation(rtype)
else:
rschema = eschema.subject_relation(rtype)
except KeyError:
return False
if hasattr(cls, 'etype'):
letype = cls.etype
try:
if role == 'object':
return etype in rschema.objects(letype)
else:
return etype in rschema.subjects(letype)
except KeyError, ex:
return False
return True
# XXX deprecated (since 2.43) ##########################
@obsolete('use req.datadir_url')
def datadir_url(self):
"""return url of the application's data directory"""
return self.req.datadir_url
@obsolete('use req.external_resource()')
def external_resource(self, rid, default=_MARKER):
return self.req.external_resource(rid, default)
class AppObject(AppRsetObject):
"""base class for application objects which are not selected
......
......@@ -10,6 +10,7 @@ from logilab.common import interface
from logilab.common.compat import all
from logilab.common.decorators import cached
from logilab.mtconverter import TransformData, TransformError
from rql.utils import rqlvar_maker
from cubicweb import Unauthorized
......
......@@ -5,7 +5,7 @@ A registerer is responsible to tell if an object should be registered according
to the application's schema or to already registered object
:organization: Logilab
:copyright: 2006-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:copyright: 2006-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
......@@ -84,13 +84,14 @@ class accepts_registerer(priority_registerer):
# remove it latter if no object is implementing accepted interfaces
if _accepts_interfaces(self.vobject):
return self.vobject
if not 'Any' in self.vobject.accepts:
for ertype in self.vobject.accepts:
if ertype in self.schema:
break
else:
self.skip()
return None
# XXX no more .accepts attribute
# if not 'Any' in self.vobject.accepts:
# for ertype in self.vobject.accepts:
# if ertype in self.schema:
# break
# else:
# self.skip()
# return None
for required in getattr(self.vobject, 'requires', ()):
if required not in self.schema:
self.skip()
......
This diff is collapsed.
"""extend the generic VRegistry with some cubicweb specific stuff
:organization: Logilab
:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
......@@ -9,6 +9,7 @@ __docformat__ = "restructuredtext en"
from warnings import warn
from logilab.common.decorators import cached, clear_cache
from logilab.common.interface import extend
from rql import RQLHelper
......@@ -129,6 +130,8 @@ class CubicWebRegistry(VRegistry):
default to a dump of the class registered for 'Any'
"""
etype = str(etype)
if etype == 'Any':
return self.select(self.registry_objects('etypes', 'Any'), 'Any')
eschema = self.schema.eschema(etype)
baseschemas = [eschema] + eschema.ancestors()
# browse ancestors from most specific to most generic and
......@@ -136,12 +139,21 @@ class CubicWebRegistry(VRegistry):
for baseschema in baseschemas:
btype = str(baseschema)
try:
return self.select(self.registry_objects('etypes', btype), etype)
cls = self.select(self.registry_objects('etypes', btype), etype)
break
except ObjectNotFound:
pass
# no entity class for any of the ancestors, fallback to the default one
return self.select(self.registry_objects('etypes', 'Any'), etype)
else:
# no entity class for any of the ancestors, fallback to the default
# one
cls = self.select(self.registry_objects('etypes', 'Any'), etype)
# add class itself to the list of implemented interfaces, as well as the
# Any entity class so we can select according to class using the
# `implements` selector
extend(cls, cls)
extend(cls, self.etype_class('Any'))
return cls
def render(self, registry, oid, req, **context):
"""select an object in a given registry and render it
......
# coding: utf-8
"""unit tests for module cubicweb.common.utils"""
from __future__ import with_statement
from logilab.common.testlib import TestCase, unittest_main
from cubicweb.devtools.apptest import EnvBasedTC
from cubicweb.common.selectors import traced_selection
from urlparse import urlsplit
from rql import parse
......
......@@ -21,7 +21,8 @@ class VRegistryTC(TestCase):
def test_load(self):
self.vreg.load_file(join(BASE, 'web', 'views'), 'euser.py')
self.vreg.load_file(join(BASE, 'web', 'views'), 'baseviews.py')
fpvc = [v for v in self.vreg.registry_objects('views', 'primary') if v.accepts[0] == 'EUser'][0]
fpvc = [v for v in self.vreg.registry_objects('views', 'primary')
i f v.__module__ == 'cubicweb.web.views.euser'][0]
fpv = fpvc(None, None)
# don't want a TypeError due to super call
self.assertRaises(AttributeError, fpv.render_entity_attributes, None, None)
......
......@@ -513,7 +513,7 @@ set_log_methods(registerer, getLogger('cubicweb.registration'))
# advanced selector building functions ########################################
def chainall(*selectors):
def chainall(*selectors, **kwargs):
"""return a selector chaining given selectors. If one of
the selectors fail, selection will fail, else the returned score
will be the sum of each selector'score
......@@ -527,9 +527,11 @@ def chainall(*selectors):
return 0
score += partscore
return score
if 'name' in kwargs:
selector.__name__ = kwargs['name']
return selector
def chainfirst(*selectors):
def chainfirst(*selectors, **kwargs):
"""return a selector chaining given selectors. If all
the selectors fail, selection will fail, else the returned score
will be the first non-zero selector score
......@@ -541,10 +543,13 @@ def chainfirst(*selectors):
if partscore:
return partscore
return 0
if 'name' in kwargs:
selector.__name__ = kwargs['name']
return selector
# selector base classes and operations ########################################
class Selector(object):
"""base class for selector classes providing implementation
for operators ``&`` and ``|``
......
"""abstract action classes for CubicWeb web client
:organization: Logilab
:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
from cubicweb import target
from cubicweb.common.appobject import AppRsetObject
from cubicweb.common.registerers import action_registerer
from cubicweb.common.selectors import add_etype_selector, \
from cubicweb.common.selectors import user_can_add_etype, \
match_search_state, searchstate_accept_one, \
searchstate_accept_one_but_etype
......@@ -21,9 +22,7 @@ class Action(AppRsetObject):
"""
__registry__ = 'actions'
__registerer__ = action_registerer
__selectors__ = (match_search_state,)
# by default actions don't appear in link search mode
search_states = ('normal',)
property_defs = {
'visible': dict(type='Boolean', default=True,
help=_('display the action or not')),
......@@ -37,53 +36,6 @@ class Action(AppRsetObject):
site_wide = True # don't want user to configuration actions eproperties
category = 'moreactions'
@classmethod
def accept_rset(cls, req, rset, row, col):
user = req.user
action = cls.schema_action
if row is None:
score = 0
need_local_check = []
geteschema = cls.schema.eschema
for etype in rset.column_types(0):
accepted = cls.accept(user, etype)
if not accepted:
return 0
if action:
eschema = geteschema(etype)
if not user.matching_groups(eschema.get_groups(action)):
if eschema.has_local_role(action):
# have to ckeck local roles
need_local_check.append(eschema)
continue
else:
# even a local role won't be enough
return 0
score += accepted
if need_local_check:
# check local role for entities of necessary types
for i, row in enumerate(rset):
if not rset.description[i][0] in need_local_check:
continue
if not cls.has_permission(rset.get_entity(i, 0), action):
return 0
score += 1
return score
col = col or 0
etype = rset.description[row][col]
score = cls.accept(user, etype)
if score and action:
if not cls.has_permission(rset.get_entity(row, col), action):
return 0
return score
@classmethod
def has_permission(cls, entity, action):
"""defined in a separated method to ease overriding (see ModifyAction
for instance)
"""
return entity.has_perm(action)
def url(self):
"""return the url associated with this action"""
raise NotImplementedError
......@@ -94,6 +46,7 @@ class Action(AppRsetObject):
if self.category:
return 'box' + self.category.capitalize()
class UnregisteredAction(Action):
"""non registered action used to build boxes. Unless you set them
explicitly, .vreg and .schema attributes at least are None.
......@@ -115,7 +68,7 @@ class AddEntityAction(Action):
"""link to the entity creation form. Concrete class must set .etype and
may override .vid
"""
__selectors__ = (add_etype_selector, match_search_state)
__selectors__ = (user_can_add_etype,)
vid = 'creation'
etype = None
......@@ -127,21 +80,9 @@ class EntityAction(Action):
"""an action for an entity. By default entity actions are only
displayable on single entity result if accept match.
"""
__selectors__ = (searchstate_accept_one,)
schema_action = None
condition = None
# XXX deprecate
@classmethod
def accept(cls, user, etype):
score = super(EntityAction, cls).accept(user, etype)
if not score:
return 0
# check if this type of entity has the necessary relation
if hasattr(cls, 'rtype') and not cls.relation_possible(etype):
return 0
return score
class LinkToEntityAction(EntityAction):
"""base class for actions consisting to create a new object
with an initial relation set to an entity.
......@@ -149,64 +90,19 @@ class LinkToEntityAction(EntityAction):
using .etype, .rtype and .target attributes to check if the
action apply and if the logged user has access to it
"""
etype = None
rtype = None
target = None
def my_selector(cls, req, rset, row=None, col=0, **kwargs):
return chainall(match_search_state('normal'),
one_line_rset, accept,
relation_possible(cls.rtype, role(cls), cls.etype,
permission='add'),
may_add_relation(cls.rtype, role(cls)))
__selectors__ = my_selector,
category = 'addrelated'
@classmethod
def accept_rset(cls, req, rset, row, col):
entity = rset.get_entity(row or 0, col or 0)
# check if this type of entity has the necessary relation
if hasattr(cls, 'rtype') and not cls.relation_possible(entity.e_schema):
return 0
score = cls.accept(req.user, entity.e_schema)
if not score:
return 0
if not cls.check_perms(req, entity):
return 0
return score
@classmethod
def check_perms(cls, req, entity):
if not cls.check_rtype_perm(req, entity):
return False
# XXX document this:
# if user can create the relation, suppose it can create the entity
# this is because we usually can't check "add" permission before the
# entity has actually been created, and schema security should be
# defined considering this
#if not cls.check_etype_perm(req, entity):
# return False
return True
@classmethod
def check_etype_perm(cls, req, entity):
eschema = cls.schema.eschema(cls.etype)
if not eschema.has_perm(req, 'add'):
#print req.user.login, 'has no add perm on etype', cls.etype
return False
#print 'etype perm ok', cls
return True
@classmethod
def check_rtype_perm(cls, req, entity):
rschema = cls.schema.rschema(cls.rtype)
# cls.target is telling us if we want to add the subject or object of
# the relation
if cls.target == 'subject':
if not rschema.has_perm(req, 'add', toeid=entity.eid):
#print req.user.login, 'has no add perm on subject rel', cls.rtype, 'with', entity
return False
elif not rschema.has_perm(req, 'add', fromeid=entity.eid):
#print req.user.login, 'has no add perm on object rel', cls.rtype, 'with', entity
return False
#print 'rtype perm ok', cls
return True
def url(self):
current_entity = self.rset.get_entity(self.row or 0, self.col or 0)
linkto = '%s:%s:%s' % (self.rtype, current_entity.eid, self.target)
linkto = '%s:%s:%s' % (self.rtype, current_entity.eid, target(self))
return self.build_url(vid='creation', etype=self.etype,
__linkto=linkto,
__redirectpath=current_entity.rest_path(), # should not be url quoted!
......@@ -217,5 +113,10 @@ class LinkToEntityAction2(LinkToEntityAction):
"""LinkToEntity action where the action is not usable on the same
entity's type as the one refered by the .etype attribute
"""
__selectors__ = (searchstate_accept_one_but_etype,)
def my_selector(cls, req, rset, row=None, col=0, **kwargs):
return chainall(match_search_state('normal'),
but_etype, one_line_rset, accept,
relation_possible(cls.rtype, role(cls), cls.etype),
may_add_relation(cls.rtype, role(cls)))
__selectors__ = my_selector,
"""Views/forms and actions for the CubicWeb web client
:organization: Logilab
:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
......@@ -67,30 +67,16 @@ def vid_from_rset(req, rset, schema):
return 'outofcontext-search'
return 'list'
return 'table'
def linksearch_match(req, rset):
"""when searching an entity to create a relation, return True if entities in
the given rset may be used as relation end
"""
try:
searchedtype = req.search_state[1][-1]
except IndexError:
return 0 # no searching for association
for etype in rset.column_types(0):
if etype != searchedtype:
return 0
return 1
def linksearch_select_url(req, rset):
"""when searching an entity to create a relation, return an url to select
entities in the given rset
"""
req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') )
target, link_eid, r_type, searchedtype = req.search_state[1]
target, eid, r_type, searchedtype = req.search_state[1]
if target == 'subject':
id_fmt = '%s:%s:%%s' % (link_eid, r_type)
id_fmt = '%s:%s:%%s' % (eid, r_type)
else:
id_fmt = '%%s:%s:%s' % (r_type, link_eid)
id_fmt = '%%s:%s:%s' % (r_type, eid)
triplets = '-'.join(id_fmt % row[0] for row in rset.rows)
return "javascript: selectForAssociation('%s', '%s');" % (triplets,
link_eid)
return "javascript: selectForAssociation('%s', '%s');" % (triplets, eid)
......@@ -6,70 +6,86 @@
"""
__docformat__ = "restructuredtext en"
from cubicweb.common.selectors import (searchstate_accept, match_user_group, yes,
one_line_rset, two_lines_rset, one_etype_rset,
authenticated_user,
match_search_state, chainfirst, chainall)
from cubicweb.web.action import Action, EntityAction, LinkToEntityAction
from cubicweb.web.views import linksearch_select_url, linksearch_match
from cubicweb.common.selectors import (
yes, one_line_rset, two_lines_rset, one_etype_rset, relation_possible,
non_final_entity,
authenticated_user, match_user_groups, match_search_state,
has_editable_relation, has_permission, has_add_permission,
)
from cubicweb.web.action import Action
from cubicweb.web.views import linksearch_select_url
from cubicweb.web.views.baseviews import vid_from_rset
_ = unicode
def match_searched_etype(cls, req, rset, row=None, col=None, **kwargs):
return req.match_search_state(rset)
def view_is_not_default_view(cls, req, rset, row, col, **kwargs):
# interesting if it propose another view than the current one
vid = req.form.get('vid')
if vid and vid != vid_from_rset(req, rset, cls.schema):
return 1
return 0
def addable_etype_empty_rset(cls, req, rset, **kwargs):
if rset is not None and not rset.rowcount:
rqlst = rset.syntax_tree()
if len(rqlst.children) > 1:
return 0
select = rqlst.children[0]
if len(select.defined_vars) == 1 and len(select.solutions) == 1:
rset._searched_etype = select.solutions[0].itervalues().next()
eschema = cls.schema.eschema(rset._searched_etype)
if not (eschema.is_final() or eschema.is_subobject(strict=True)) \
and eschema.has_perm(req, 'add'):
return 1
return 0
# generic primary actions #####################################################
class SelectAction(EntityAction):
class SelectAction(Action):
"""base class for link search actions. By default apply on
any size entity result search it the current state is 'linksearch'
if accept match.
"""
category = 'mainactions'
__selectors__ = (searchstate_accept,)
search_states = ('linksearch',)
order = 0
id = 'select'
title = _('select')
__selectors__ = (match_search_state('linksearch'),
match_searched_etype)
@classmethod
def accept_rset(cls, req, rset, row, col):
return linksearch_match(req, rset)
title = _('select')
category = 'mainactions'
order = 0
def url(self):
return linksearch_select_url(self.req, self.rset)
class CancelSelectAction(Action):
category = 'mainactions'
search_states = ('linksearch',)
order = 10
id = 'cancel'
__selectors__ = (match_search_state('linksearch'),)
title = _('cancel select')
category = 'mainactions'
order = 10
def url(self):
target, link_eid, r_type, searched_type = self.req.search_state[1]
return self.build_url(rql="Any X WHERE X eid %s" % link_eid,
target, eid, r_type, searched_type = self.req.search_state[1]
return self.build_url(str(eid),
vid='edition', __mode='normal')
class ViewAction(Action):
category = 'mainactions'
__selectors__ = (match_user_group, searchstate_accept)
require_groups = ('users', 'managers')
order = 0
id = 'view'
title = _('view')
__selectors__ = (match_search_state('normal'),
match_user_groups('users', 'managers'),
view_is_not_default_view,
non_final_entity())
@classmethod
def accept_rset(cls, req, rset, row, col):
# interesting if it propose another view than the current one
vid = req.form.get('vid')
if vid and vid != vid_from_rset(req, rset, cls.schema):
return 1
return 0
title = _('view')
category = 'mainactions'
order = 0
def url(self):
params = self.req.form.copy()
......@@ -79,45 +95,30 @@ class ViewAction(Action):
**params)