diff --git a/README.txt b/README.txt index 2ee1439..3dfe836 100755 --- a/README.txt +++ b/README.txt @@ -679,6 +679,15 @@ been created during setup. >>> form.presetTypesForAssignment [{'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 on data provided in this form: diff --git a/browser/form.py b/browser/form.py index 62e8b1b..e5bf62b 100644 --- a/browser/form.py +++ b/browser/form.py @@ -52,7 +52,7 @@ from loops.interfaces import IFile, IExternalFile, INote, ITextDocument from loops.browser.node import NodeView from loops.browser.concept import ConceptRelationView from loops.i18n.browser import I18NView -from loops.query import ConceptQuery +from loops.query import ConceptQuery, IQueryConcept from loops.resource import Resource from loops.type import ITypeConcept from loops import util @@ -265,13 +265,23 @@ class CreateObjectForm(ObjectForm): @property def assignments(self): target = self.virtualTargetObject - if (IConcept.providedBy(target) and - target.conceptType != - self.loopsRoot.getConceptManager().getTypeConcept()): + if self.maybeAssignedAsParent(target): rv = ConceptRelationView(ResourceRelation(target, None), self.request) return (rv,) 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): @@ -329,9 +339,7 @@ class CreateConceptForm(CreateObjectForm): @property def assignments(self): target = self.virtualTargetObject - if (IConcept.providedBy(target) and - target.conceptType != - self.loopsRoot.getConceptManager().getTypeConcept()): + if self.maybeAssignedAsParent(target): rv = ConceptRelationView(ConceptRelation(target, None), self.request) return (rv,) return () diff --git a/common.py b/common.py index 202a2e4..374be95 100644 --- a/common.py +++ b/common.py @@ -38,7 +38,11 @@ from loops.interfaces import ILoopsObject, ILoopsContained, IConcept, IResource from loops.interfaces import IResourceAdapter +# convenience functions + def adapted(obj, langInfo=None): + """ Return adapter based on the object type's type interface. + """ t = IType(obj, None) if t is not None: ti = t.typeInterface @@ -52,6 +56,33 @@ def adapted(obj, langInfo=None): 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 class AdapterBase(object): diff --git a/constraint/base.py b/constraint/base.py index ca8fb7d..39bd458 100644 --- a/constraint/base.py +++ b/constraint/base.py @@ -27,7 +27,7 @@ from zope.cachedescriptors.property import Lazy from zope.interface import implements 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.type import TypeInterfaceSourceList @@ -42,8 +42,8 @@ hasChildConstraint = 'haschildconstraint' class StaticConstraint(AdapterBase): - _contextAttributes = AdapterBase._contextAttributes + ['relationType', 'cardinality'] - _adapterAttributes = AdapterBase._adapterAttributes + ('predicates', 'types',) + _contextAttributes = contextAttributes(AdapterBase, 'relationType', 'cardinality') + _adapterAttributes = adapterAttributes(AdapterBase, 'predicates', 'types') implements(IStaticConstraint) diff --git a/expert/README.txt b/expert/README.txt index f87c57a..ad1d3d5 100644 --- a/expert/README.txt +++ b/expert/README.txt @@ -29,23 +29,32 @@ configuration): >>> t = TestSite(site) >>> concepts, resources, views = t.setup() - >>> #sorted(concepts) - >>> #sorted(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 >>> qu = query.Title('ty*') >>> list(qu.apply()) - [0, 1, 50] + [0, 1, 47] >>> qu = query.Type('loops:*') >>> len(list(qu.apply())) - 38 + 36 >>> qu = query.Type('loops:concept:predicate') >>> len(list(qu.apply())) @@ -55,9 +64,8 @@ Type- and Text-based Queries >>> list(qu.apply()) [1] - -Relationship-based Queries -========================== +Relationship-based queries +-------------------------- In addition to the simple methods of concepts and resources for accessing 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'] >>> qu = query.Resources(stateNew) >>> 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 diff --git a/expert/filter.py b/expert/filter.py new file mode 100644 index 0000000..2cd1601 --- /dev/null +++ b/expert/filter.py @@ -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) + diff --git a/expert/instance.py b/expert/instance.py new file mode 100644 index 0000000..403f63b --- /dev/null +++ b/expert/instance.py @@ -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 + diff --git a/expert/interfaces.py b/expert/interfaces.py new file mode 100644 index 0000000..e9c1912 --- /dev/null +++ b/expert/interfaces.py @@ -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. + """ diff --git a/expert/query.py b/expert/query.py index 3d2c07f..c286e4a 100644 --- a/expert/query.py +++ b/expert/query.py @@ -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 # 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$ """ from BTrees.IIBTree import IITreeSet from BTrees.IFBTree import IFBTree, IFTreeSet - +from BTrees.IOBTree import IOBTree from zope import interface, component +from zope.app.intid.interfaces import IIntIds from zope.component import adapts -from zope.interface import implements +from zope.interface import implements, implementer from zope.cachedescriptors.property import Lazy - from hurry.query.query import Term from hurry.query.query import Text as BaseText from hurry.query.query import Eq, Between +from loops.expert.interfaces import IQuery +from loops.security.common import canListObject from loops import util titleIndex = ('', 'loops_title') @@ -41,12 +43,15 @@ textIndex = ('', 'loops_text') typeIndex = ('', 'loops_type') +@implementer(IQuery) def Title(value): return BaseText(titleIndex, value) +@implementer(IQuery) def Text(value): return BaseText(textIndex, value) +@implementer(IQuery) def Type(value): if value.endswith('*'): v1 = value[:-1] @@ -55,16 +60,56 @@ def Type(value): return Eq(typeIndex, value) -class Resources(Term): +class ConceptMapTerm(Term): + + implements(IQuery) def __init__(self, concept, **kw): self.context = concept - self.kwargs = kw + for k, v in kw.items(): + setattr(self, k, v) + + +class Resources(ConceptMapTerm): + + predicates = None def apply(self): result = IFTreeSet() - #result = IFBTree() for r in self.context.getResources(): result.insert(int(util.getUidForObject(r))) - #result[int(util.getUidForObject(r))] = 1.0 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 diff --git a/expert/testsetup.py b/expert/testsetup.py index 3a5051e..a961439 100644 --- a/expert/testsetup.py +++ b/expert/testsetup.py @@ -11,6 +11,8 @@ from cybertools.typology.interfaces import IType from loops import util from loops.concept import Concept 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.setup import SetupManager, addObject from loops.tests.setup import TestSite as BaseTestSite @@ -27,6 +29,8 @@ class TestSite(BaseTestSite): site = self.site loopsRoot = site['loops'] + component.provideAdapter(Person, provides=IPerson) + component.provideAdapter(KnowledgeSetupManager, name='knowledge') setup = SetupManager(loopsRoot) concepts, resources, views = setup.setup() @@ -34,6 +38,7 @@ class TestSite(BaseTestSite): tType = concepts.getTypeConcept() tDomain = concepts['domain'] tTextDocument = concepts['textdocument'] + tPerson = concepts['person'] tCountry = addObject(concepts, Concept, 'country', title=u'Country', type=tType) @@ -76,18 +81,18 @@ class TestSite(BaseTestSite): dtNote = addObject(concepts, Concept, 'dt_note', title=u'Note', type=tDocumentType) - d001 = addObject(resources, Resource, 'd001', - title=u'Doc 001', type=tTextDocument) + jim = addObject(concepts, Concept, 'jim', title=u'Jim', type=tPerson) + jim.assignChild(cust1) + + d001 = resources['d001.txt'] d001.assignConcept(cust1) d001.assignConcept(stateReleased) d001.assignConcept(dtNote) - d002 = addObject(resources, Resource, 'd002', - title=u'Doc 002', type=tTextDocument) + d002 = resources['d002.txt'] d002.assignConcept(cust3) d002.assignConcept(stateNew) d002.assignConcept(dtNote) - d003 = addObject(resources, Resource, 'd003', - title=u'Doc 003', type=tTextDocument) + d003 = resources['d003.txt'] d003.assignConcept(cust1) d003.assignConcept(stateNew) d003.assignConcept(dtStudy) diff --git a/external/README.txt b/external/README.txt index b6f0017..1e55046 100644 --- a/external/README.txt +++ b/external/README.txt @@ -75,7 +75,7 @@ Writing object information to the external storage type(u'customer', u'Customer', options=u'', viewName=u'')... type(u'query', u'Query', options=u'', typeInterface='loops.query.IQueryConcept', 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')... diff --git a/query.py b/query.py index eddcaa2..5ec2377 100644 --- a/query.py +++ b/query.py @@ -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 # it under the terms of the GNU General Public License as published by @@ -169,12 +169,26 @@ class IQueryConcept(IConceptSchema): default=u'', required=True) + options = schema.List( + title=_(u'Options'), + description=_(u'Additional settings.'), + value_type=schema.TextLine(), + default=[], + required=False) + class QueryConcept(AdapterBase): 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,) diff --git a/setup.py b/setup.py index 958c983..0054cd7 100644 --- a/setup.py +++ b/setup.py @@ -203,8 +203,11 @@ def addObject(container, class_, name, **kw): if name in container: return container[name] obj = container[name] = class_() - for attr in kw: - setattr(obj, attr, kw[attr]) + for attr, value in kw.items(): + if attr == 'type': + obj.setType(value) + else: + setattr(obj, attr, value) notify(ObjectCreatedEvent(obj)) notify(ObjectModifiedEvent(obj)) return obj diff --git a/util.py b/util.py index 4a08105..373d2e1 100644 --- a/util.py +++ b/util.py @@ -81,10 +81,11 @@ def toUnicode(text, encoding='UTF-8'): return text -def getObjectForUid(uid): +def getObjectForUid(uid, intIds=None): if uid == '*': # wild card return '*' - intIds = component.getUtility(IIntIds) + if intIds is None: + intIds = component.getUtility(IIntIds) return intIds.getObject(int(uid)) def getUidForObject(obj):