added options attribute to query, e.g. for controlling autoassignment; work in progress: loops.expert - starting to implement filters

git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@2422 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2008-02-29 09:57:41 +00:00
parent 3b5132b0f6
commit 8660f12f5e
14 changed files with 415 additions and 43 deletions

View file

@ -679,6 +679,15 @@ been created during setup.
>>> form.presetTypesForAssignment >>> form.presetTypesForAssignment
[{'token': 'loops:concept:customer', 'title': u'Customer'}] [{'token': 'loops:concept:customer', 'title': u'Customer'}]
If the node's target is a type concept we don't get any assignments because
it does not make much sense to assign resources or other concepts as
children to type concepts.
>>> m112.target = customer
>>> form = CreateObjectForm(m112, TestRequest())
>>> form.assignments
()
OK, so much about the form - now we want to create a new object based OK, so much about the form - now we want to create a new object based
on data provided in this form: on data provided in this form:

View file

@ -52,7 +52,7 @@ from loops.interfaces import IFile, IExternalFile, INote, ITextDocument
from loops.browser.node import NodeView from loops.browser.node import NodeView
from loops.browser.concept import ConceptRelationView from loops.browser.concept import ConceptRelationView
from loops.i18n.browser import I18NView from loops.i18n.browser import I18NView
from loops.query import ConceptQuery from loops.query import ConceptQuery, IQueryConcept
from loops.resource import Resource from loops.resource import Resource
from loops.type import ITypeConcept from loops.type import ITypeConcept
from loops import util from loops import util
@ -265,13 +265,23 @@ class CreateObjectForm(ObjectForm):
@property @property
def assignments(self): def assignments(self):
target = self.virtualTargetObject target = self.virtualTargetObject
if (IConcept.providedBy(target) and if self.maybeAssignedAsParent(target):
target.conceptType !=
self.loopsRoot.getConceptManager().getTypeConcept()):
rv = ConceptRelationView(ResourceRelation(target, None), self.request) rv = ConceptRelationView(ResourceRelation(target, None), self.request)
return (rv,) return (rv,)
return () return ()
def maybeAssignedAsParent(self, obj):
if not IConcept.providedBy(obj):
return False
if obj.conceptType == self.loopsRoot.getConceptManager().getTypeConcept():
return False
if 'noassign' in IType(obj).qualifiers:
return False
adap = adapted(obj)
if 'noassign' in getattr(adap, 'options', []):
return False
return True
class CreateObjectPopup(CreateObjectForm): class CreateObjectPopup(CreateObjectForm):
@ -329,9 +339,7 @@ class CreateConceptForm(CreateObjectForm):
@property @property
def assignments(self): def assignments(self):
target = self.virtualTargetObject target = self.virtualTargetObject
if (IConcept.providedBy(target) and if self.maybeAssignedAsParent(target):
target.conceptType !=
self.loopsRoot.getConceptManager().getTypeConcept()):
rv = ConceptRelationView(ConceptRelation(target, None), self.request) rv = ConceptRelationView(ConceptRelation(target, None), self.request)
return (rv,) return (rv,)
return () return ()

View file

@ -38,7 +38,11 @@ from loops.interfaces import ILoopsObject, ILoopsContained, IConcept, IResource
from loops.interfaces import IResourceAdapter from loops.interfaces import IResourceAdapter
# convenience functions
def adapted(obj, langInfo=None): def adapted(obj, langInfo=None):
""" Return adapter based on the object type's type interface.
"""
t = IType(obj, None) t = IType(obj, None)
if t is not None: if t is not None:
ti = t.typeInterface ti = t.typeInterface
@ -52,6 +56,33 @@ def adapted(obj, langInfo=None):
return obj return obj
# helper functions for specifying automatic attribute handling
def adapterAttributes(*args):
attrs = []
for arg in args:
if isinstance(arg, basestring):
attrs.append(arg)
elif isinstance(arg, type):
attrs.extend(list(arg._adapterAttributes))
else:
raise ValueError("Argument must be string or class, '%s' is '%s'." %
(arg, type(arg)))
return tuple(attrs)
def contextAttributes(*args):
attrs = []
for arg in args:
if isinstance(arg, basestring):
attrs.append(arg)
elif isinstance(arg, type):
attrs.extend(list(arg._contextAttributes))
else:
raise ValueError("Argument must be string or class, '%s' is '%s'." %
(arg, type(arg)))
return attrs
# type interface adapters # type interface adapters
class AdapterBase(object): class AdapterBase(object):

View file

@ -27,7 +27,7 @@ from zope.cachedescriptors.property import Lazy
from zope.interface import implements from zope.interface import implements
from zope.traversing.api import getName from zope.traversing.api import getName
from loops.common import AdapterBase from loops.common import AdapterBase, adapterAttributes, contextAttributes
from loops.constraint.interfaces import IStaticConstraint from loops.constraint.interfaces import IStaticConstraint
from loops.type import TypeInterfaceSourceList from loops.type import TypeInterfaceSourceList
@ -42,8 +42,8 @@ hasChildConstraint = 'haschildconstraint'
class StaticConstraint(AdapterBase): class StaticConstraint(AdapterBase):
_contextAttributes = AdapterBase._contextAttributes + ['relationType', 'cardinality'] _contextAttributes = contextAttributes(AdapterBase, 'relationType', 'cardinality')
_adapterAttributes = AdapterBase._adapterAttributes + ('predicates', 'types',) _adapterAttributes = adapterAttributes(AdapterBase, 'predicates', 'types')
implements(IStaticConstraint) implements(IStaticConstraint)

View file

@ -29,23 +29,32 @@ configuration):
>>> t = TestSite(site) >>> t = TestSite(site)
>>> concepts, resources, views = t.setup() >>> concepts, resources, views = t.setup()
>>> #sorted(concepts)
>>> #sorted(resources)
>>> len(concepts) + len(resources) >>> len(concepts) + len(resources)
38 36
>>> loopsRoot = site['loops']
Type- and Text-based Queries Queries
============================ =======
Queries search the loops database (typically but not necessarily the
catalog) for objects fulfilling the criteria given. A query returns
a set of integer UIDs; thus the results of a query may be efficiently combined
with those of other queries using logical operations.
Type- and text-based queries
----------------------------
>>> from loops.expert import query >>> from loops.expert import query
>>> qu = query.Title('ty*') >>> qu = query.Title('ty*')
>>> list(qu.apply()) >>> list(qu.apply())
[0, 1, 50] [0, 1, 47]
>>> qu = query.Type('loops:*') >>> qu = query.Type('loops:*')
>>> len(list(qu.apply())) >>> len(list(qu.apply()))
38 36
>>> qu = query.Type('loops:concept:predicate') >>> qu = query.Type('loops:concept:predicate')
>>> len(list(qu.apply())) >>> len(list(qu.apply()))
@ -55,9 +64,8 @@ Type- and Text-based Queries
>>> list(qu.apply()) >>> list(qu.apply())
[1] [1]
Relationship-based queries
Relationship-based Queries --------------------------
==========================
In addition to the simple methods of concepts and resources for accessing In addition to the simple methods of concepts and resources for accessing
relations to other objects the expert package provides methods relations to other objects the expert package provides methods
@ -67,7 +75,77 @@ syntax (that in turn is based on hurry.query).
>>> stateNew = concepts['new'] >>> stateNew = concepts['new']
>>> qu = query.Resources(stateNew) >>> qu = query.Resources(stateNew)
>>> list(qu.apply()) >>> list(qu.apply())
[66, 71] [25, 27]
Getting objects
---------------
When a query (or a combination of query terms) has been applied we
want to get at the objects resulting from the query.
The ``getObjects()`` function returns an iterable with the objects.
If the ``root`` argument is supplied only objects belonging to the
corresponding loops site are returned. In addition a ``checkPermission``
argument may be supplied with a function that should be checked for
filtering the results; this defaults to ``canListObjects``.
>>> from loops.expert.query import getObjects
>>> objs = getObjects(query.Title('ty*').apply(), root=loopsRoot)
>>> sorted(o.title for o in objs)
[u'Document Type', u'Type', u'has Type']
Filters
=======
Basically there are two kinds of filters: One is in fact just a query
term that is joined ``and`` (``&``) operation to another query;
the other one is applied to the objects resulting from applying a
query by checking certain attributes or other conditions, thus reducing
the number of the resulting objects.
Which kind of filtering will be used depends on the implementation - this
may be an efficiency issue; there are also filters that don't have an
equivalent query.
Example 1: "My Items"
---------------------
Let's assume that jim is the person that corresponds to the logged-in user.
We now want to set up a filter that lets pass only objects (resources and
concepts) that are direct or indirect children of jim.
>>> jim = concepts['jim']
>>> qu = query.Type('loops:resource:textdocument')
>>> objs = getObjects(qu.apply())
>>> sorted(o.title for o in objs)
[u'Doc 001', u'Doc 002', u'Doc 003']
>>> from loops.expert import filter
>>> fltr = filter.Children(jim, recursive=True, includeResources=True)
>>> sorted(o.title for o in getObjects((qu & fltr.query()).apply()))
[u'Doc 001', u'Doc 003']
>>> #fltr.check(concepts['d001.txt'])
>>> #fltr.check(concepts['d002.txt'])
>>> #objs = fltr.apply(objs)
>>> #sorted(o.title for o in objs.values())
Organizing Queries and Filters with Query Instances
===================================================
A query instance consists of
- a base query (a composition of terms)
- one or more query filter that will be joined with the base query
- a result filter that will be applied to the result set of the
preceding steps
>>> from loops.expert.instance import QueryInstance
>>> qi = QueryInstance(qu, fltr)
>>> #qi.apply()
Fin de partie Fin de partie

58
expert/filter.py Normal file
View file

@ -0,0 +1,58 @@
#
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
Filter query results.
$Id$
"""
from hurry.query.query import And
from BTrees.IOBTree import IOBTree
from zope.interface import implements
from loops.expert.interfaces import IFilter
from loops.expert.query import Children as ChildrenQuery
class Filter(object):
implements(IFilter)
def __init__(self, **kw):
self.kwargs = kw
def apply(self, objects, queryInstance=None):
result = IOBTree()
for uid, obj in objects:
if self.check(obj, queryInstance):
result[uid] = obj
return result
class Children(Filter):
implements(IFilter)
def __init__(self, parent, **kw):
self.parent = parent
super(Children, self).__init__(**kw)
def query(self):
return ChildrenQuery(self.parent, **self.kwargs)

44
expert/instance.py Normal file
View file

@ -0,0 +1,44 @@
#
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
Filter query results.
$Id$
"""
from zope.interface import implements
from loops.expert.interfaces import IQueryInstance
class QueryInstance(object):
implements(IQueryInstance)
def __init__(self, query, *filters, **kw):
self.query = query
self.filters = filters
self.filterQueries = {}
for k, v in kw.items():
setattr(self, k, v)
def apply(self, uidsOnly=False):
result = self.query.apply()
return result

76
expert/interfaces.py Normal file
View file

@ -0,0 +1,76 @@
#
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
Filtering query result sets.
$Id$
"""
from zope.interface import Interface, Attribute
class IQuery(Interface):
""" A query or
"""
def apply():
""" Return the result set for this query.
"""
class IQueryInstance(Interface):
""" A top-level query instance that allows caching of intermediate
results when the underlying queries and filters are applied.
"""
query = Attribute('The top-level query (query term).')
filters = Attribute('A collection of filters that will be applied '
'to the result of the query.')
def apply(uidsOnly=False):
""" Execute the query and apply the filters; return a mapping
of UIDs to objects or a set of UIDs only if the ``uidsOnly``
argument is set to True.
"""
class IFilter(Interface):
""" A filter is a query that will be ``and``-connected to a query.
"""
def query():
""" Return the query that corresponds to this filter; that will then
typically be ``and``-joined to another query.
May return None; in this case the ``apply()`` method must be used.
"""
def apply(objects, queryInstance=None):
""" Apply the filter to the set of objects specified as a mapping
of UIDs to objects.
If a query instance is given it may be used for caching
intermediate results.
"""
def check(obj, queryInstance=None):
""" Return True if the object given should be included in the
filter's result set, False otherwise.
If a query instance is given it may be used for caching.
"""

View file

@ -1,5 +1,5 @@
# #
# Copyright (c) 2007 Helmut Merz helmutm@cy55.de # Copyright (c) 2008 Helmut Merz helmutm@cy55.de
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -17,23 +17,25 @@
# #
""" """
Generic query functionality for retrieving stuff from a loops database Generic query functionality for retrieving stuff from a loops database.
$Id$ $Id$
""" """
from BTrees.IIBTree import IITreeSet from BTrees.IIBTree import IITreeSet
from BTrees.IFBTree import IFBTree, IFTreeSet from BTrees.IFBTree import IFBTree, IFTreeSet
from BTrees.IOBTree import IOBTree
from zope import interface, component from zope import interface, component
from zope.app.intid.interfaces import IIntIds
from zope.component import adapts from zope.component import adapts
from zope.interface import implements from zope.interface import implements, implementer
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from hurry.query.query import Term from hurry.query.query import Term
from hurry.query.query import Text as BaseText from hurry.query.query import Text as BaseText
from hurry.query.query import Eq, Between from hurry.query.query import Eq, Between
from loops.expert.interfaces import IQuery
from loops.security.common import canListObject
from loops import util from loops import util
titleIndex = ('', 'loops_title') titleIndex = ('', 'loops_title')
@ -41,12 +43,15 @@ textIndex = ('', 'loops_text')
typeIndex = ('', 'loops_type') typeIndex = ('', 'loops_type')
@implementer(IQuery)
def Title(value): def Title(value):
return BaseText(titleIndex, value) return BaseText(titleIndex, value)
@implementer(IQuery)
def Text(value): def Text(value):
return BaseText(textIndex, value) return BaseText(textIndex, value)
@implementer(IQuery)
def Type(value): def Type(value):
if value.endswith('*'): if value.endswith('*'):
v1 = value[:-1] v1 = value[:-1]
@ -55,16 +60,56 @@ def Type(value):
return Eq(typeIndex, value) return Eq(typeIndex, value)
class Resources(Term): class ConceptMapTerm(Term):
implements(IQuery)
def __init__(self, concept, **kw): def __init__(self, concept, **kw):
self.context = concept self.context = concept
self.kwargs = kw for k, v in kw.items():
setattr(self, k, v)
class Resources(ConceptMapTerm):
predicates = None
def apply(self): def apply(self):
result = IFTreeSet() result = IFTreeSet()
#result = IFBTree()
for r in self.context.getResources(): for r in self.context.getResources():
result.insert(int(util.getUidForObject(r))) result.insert(int(util.getUidForObject(r)))
#result[int(util.getUidForObject(r))] = 1.0
return result return result
class Children(ConceptMapTerm):
includeResources = False
recursive = False
predicates = None
def apply(self):
result = IFTreeSet()
self.getRecursive(self.context, result)
return result
def getRecursive(self, c, result):
if self.includeResources:
for r in c.getResources():
uid = int(util.getUidForObject(r))
if uid not in result:
result.insert(uid)
for c in c.getChildren():
uid = int(util.getUidForObject(c))
if uid not in result:
result.insert(uid)
if self.recursive:
self.getRecursive(c, result)
def getObjects(uids, root=None, checkPermission=canListObject):
intIds = component.getUtility(IIntIds)
for uid in uids:
obj = util.getObjectForUid(uid, intIds)
if ((root is None or obj.getLoopsRoot() == root) and
(checkPermission is None or checkPermission(obj))):
yield obj

View file

@ -11,6 +11,8 @@ from cybertools.typology.interfaces import IType
from loops import util from loops import util
from loops.concept import Concept from loops.concept import Concept
from loops.resource import Resource from loops.resource import Resource
from loops.knowledge.interfaces import IPerson
from loops.knowledge.knowledge import Person
from loops.knowledge.setup import SetupManager as KnowledgeSetupManager from loops.knowledge.setup import SetupManager as KnowledgeSetupManager
from loops.setup import SetupManager, addObject from loops.setup import SetupManager, addObject
from loops.tests.setup import TestSite as BaseTestSite from loops.tests.setup import TestSite as BaseTestSite
@ -27,6 +29,8 @@ class TestSite(BaseTestSite):
site = self.site site = self.site
loopsRoot = site['loops'] loopsRoot = site['loops']
component.provideAdapter(Person, provides=IPerson)
component.provideAdapter(KnowledgeSetupManager, name='knowledge') component.provideAdapter(KnowledgeSetupManager, name='knowledge')
setup = SetupManager(loopsRoot) setup = SetupManager(loopsRoot)
concepts, resources, views = setup.setup() concepts, resources, views = setup.setup()
@ -34,6 +38,7 @@ class TestSite(BaseTestSite):
tType = concepts.getTypeConcept() tType = concepts.getTypeConcept()
tDomain = concepts['domain'] tDomain = concepts['domain']
tTextDocument = concepts['textdocument'] tTextDocument = concepts['textdocument']
tPerson = concepts['person']
tCountry = addObject(concepts, Concept, 'country', title=u'Country', tCountry = addObject(concepts, Concept, 'country', title=u'Country',
type=tType) type=tType)
@ -76,18 +81,18 @@ class TestSite(BaseTestSite):
dtNote = addObject(concepts, Concept, 'dt_note', dtNote = addObject(concepts, Concept, 'dt_note',
title=u'Note', type=tDocumentType) title=u'Note', type=tDocumentType)
d001 = addObject(resources, Resource, 'd001', jim = addObject(concepts, Concept, 'jim', title=u'Jim', type=tPerson)
title=u'Doc 001', type=tTextDocument) jim.assignChild(cust1)
d001 = resources['d001.txt']
d001.assignConcept(cust1) d001.assignConcept(cust1)
d001.assignConcept(stateReleased) d001.assignConcept(stateReleased)
d001.assignConcept(dtNote) d001.assignConcept(dtNote)
d002 = addObject(resources, Resource, 'd002', d002 = resources['d002.txt']
title=u'Doc 002', type=tTextDocument)
d002.assignConcept(cust3) d002.assignConcept(cust3)
d002.assignConcept(stateNew) d002.assignConcept(stateNew)
d002.assignConcept(dtNote) d002.assignConcept(dtNote)
d003 = addObject(resources, Resource, 'd003', d003 = resources['d003.txt']
title=u'Doc 003', type=tTextDocument)
d003.assignConcept(cust1) d003.assignConcept(cust1)
d003.assignConcept(stateNew) d003.assignConcept(stateNew)
d003.assignConcept(dtStudy) d003.assignConcept(dtStudy)

2
external/README.txt vendored
View file

@ -75,7 +75,7 @@ Writing object information to the external storage
type(u'customer', u'Customer', options=u'', viewName=u'')... type(u'customer', u'Customer', options=u'', viewName=u'')...
type(u'query', u'Query', options=u'', typeInterface='loops.query.IQueryConcept', type(u'query', u'Query', options=u'', typeInterface='loops.query.IQueryConcept',
viewName=u'')... viewName=u'')...
concept(u'myquery', u'My Query', u'query', viewName='mystuff.html')... concept(u'myquery', u'My Query', u'query', options=u'', viewName='mystuff.html')...
child(u'projects', u'customer', u'standard')... child(u'projects', u'customer', u'standard')...

View file

@ -1,5 +1,5 @@
# #
# Copyright (c) 2006 Helmut Merz helmutm@cy55.de # Copyright (c) 2008 Helmut Merz helmutm@cy55.de
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -169,12 +169,26 @@ class IQueryConcept(IConceptSchema):
default=u'', default=u'',
required=True) required=True)
options = schema.List(
title=_(u'Options'),
description=_(u'Additional settings.'),
value_type=schema.TextLine(),
default=[],
required=False)
class QueryConcept(AdapterBase): class QueryConcept(AdapterBase):
implements(IQueryConcept) implements(IQueryConcept)
_contextAttributes = list(IQueryConcept) + list(IConcept) _contextAttributes = AdapterBase._contextAttributes + ['viewName']
_adapterAttributes = AdapterBase._adapterAttributes + ('options',)
def getOptions(self):
return getattr(self.context, '_options', [])
def setOptions(self, value):
self.context._options = value
options = property(getOptions, setOptions)
TypeInterfaceSourceList.typeInterfaces += (IQueryConcept,) TypeInterfaceSourceList.typeInterfaces += (IQueryConcept,)

View file

@ -203,8 +203,11 @@ def addObject(container, class_, name, **kw):
if name in container: if name in container:
return container[name] return container[name]
obj = container[name] = class_() obj = container[name] = class_()
for attr in kw: for attr, value in kw.items():
setattr(obj, attr, kw[attr]) if attr == 'type':
obj.setType(value)
else:
setattr(obj, attr, value)
notify(ObjectCreatedEvent(obj)) notify(ObjectCreatedEvent(obj))
notify(ObjectModifiedEvent(obj)) notify(ObjectModifiedEvent(obj))
return obj return obj

View file

@ -81,10 +81,11 @@ def toUnicode(text, encoding='UTF-8'):
return text return text
def getObjectForUid(uid): def getObjectForUid(uid, intIds=None):
if uid == '*': # wild card if uid == '*': # wild card
return '*' return '*'
intIds = component.getUtility(IIntIds) if intIds is None:
intIds = component.getUtility(IIntIds)
return intIds.getObject(int(uid)) return intIds.getObject(int(uid))
def getUidForObject(obj): def getUidForObject(obj):