diff --git a/README.txt b/README.txt index 3cea3e4..033a5e6 100755 --- a/README.txt +++ b/README.txt @@ -142,8 +142,12 @@ a special concept type. >>> from loops.concept import PredicateSourceList >>> predicates = PredicateSourceList(cc1) + +Note that the 'hasType' predicate is suppressed from this list as the +corresponding relation is only assigned via the conceptType attribute: + >>> sorted(t.title for t in predicates) - [u'has type', u'subconcept'] + [u'subconcept'] Concept Views ------------- @@ -209,8 +213,7 @@ types and predicates. (u'Unknown Type', '.loops/concepts/unknown')] >>> sorted((t.title, t.token) for t in view.predicates()) - [(u'has type', '.loops/concepts/hasType'), - (u'subconcept', '.loops/concepts/standard')] + [(u'subconcept', '.loops/concepts/standard')] Index attributes adapter ------------------------ diff --git a/browser/concept.py b/browser/concept.py index 29e85ba..910228d 100644 --- a/browser/concept.py +++ b/browser/concept.py @@ -272,6 +272,10 @@ class ConceptRelationView(BaseView): return ':'.join((self.loopsRoot.getLoopsUri(self.context), self.loopsRoot.getLoopsUri(self.predicate))) + @Lazy + def isProtected(self): + return zapi.getName(self.predicate) == 'hasType' + @Lazy def predicateTitle(self): return self.predicate.title diff --git a/browser/relation_macros.pt b/browser/relation_macros.pt index 9b2e3f0..925489c 100644 --- a/browser/relation_macros.pt +++ b/browser/relation_macros.pt @@ -32,6 +32,7 @@ diff --git a/concept.py b/concept.py index 2556599..fb8beae 100644 --- a/concept.py +++ b/concept.py @@ -271,11 +271,12 @@ class PredicateSourceList(object): typePred = cm.getTypePredicate() if defPred is not None and typePred is not None: result.append(defPred) - result.append(typePred) + #result.append(typePred) predType = defPred.conceptType if predType is not None and predType != cm.getTypeConcept(): result.extend(p for p in predType.getChildren([typePred]) - if p not in result) + if p not in result + and p != typePred) return result def __len__(self): diff --git a/knowledge/README.txt b/knowledge/README.txt new file mode 100644 index 0000000..bf2a566 --- /dev/null +++ b/knowledge/README.txt @@ -0,0 +1,207 @@ +=============================================================== +loops - Linked Objects for Organization and Processing Services +=============================================================== + + ($Id$) + +Note: This package depends on cybertools.knowledge and cybertools.organize. + +Let's do some basic set up + + >>> from zope.app.testing.setup import placefulSetUp, placefulTearDown + >>> site = placefulSetUp(True) + + >>> from zope import component, interface + >>> from zope.app import zapi + +and setup a simple loops site with a concept manager and some concepts +(with all the type machinery, what in real life is done via standard +ZCML setup): + + >>> from loops import Loops + >>> from loops.concept import ConceptManager, Concept + >>> from loops.resource import ResourceManager + >>> from loops.interfaces import IResource, IConcept, ITypeConcept + + >>> loopsRoot = site['loops'] = Loops() + + >>> from cybertools.relation.interfaces import IRelationRegistry + >>> from cybertools.relation.registry import DummyRelationRegistry + >>> relations = DummyRelationRegistry() + >>> component.provideUtility(relations, IRelationRegistry) + + >>> from cybertools.typology.interfaces import IType + >>> from loops.type import ConceptType, TypeConcept + >>> component.provideAdapter(ConceptType, (IConcept,), IType) + >>> component.provideAdapter(TypeConcept, (IConcept,), ITypeConcept) + + >>> concepts = loopsRoot['concepts'] = ConceptManager() + >>> resources = loopsRoot['resources'] = ResourceManager() + + >>> hasType = concepts['hasType'] = Concept(u'has type') + >>> type = concepts['type'] = Concept(u'Type') + >>> type.conceptType = type + + >>> predicate = concepts['predicate'] = Concept(u'Predicate') + >>> predicate.conceptType = type + >>> hasType.conceptType = predicate + +We need some predicates to set up the relationships between our concepts: + + >>> standard = concepts['standard'] = Concept(u'subobject') + >>> depends = concepts['depends'] = Concept(u'depends') + >>> knows = concepts['knows'] = Concept(u'knows') + >>> requires = concepts['requires'] = Concept(u'requires') + >>> provides = concepts['provides'] = Concept(u'provides') + + >>> for p in (standard, depends, knows, requires, provides): + ... p.conceptType = predicate + +And last not least we need some type concepts for controlling the +meaning of the concepts objects: + + >>> from cybertools.knowledge.interfaces import IKnowledgeElement + >>> topic = concepts['topic'] = Concept(u'Topic') + >>> topic.conceptType = type + >>> ITypeConcept(topic).typeInterface = IKnowledgeElement + + >>> from loops.knowledge.interfaces import IPerson + >>> person = concepts['person'] = Concept(u'Person') + >>> person.conceptType = type + >>> ITypeConcept(person).typeInterface = IPerson + + >>> from loops.knowledge.interfaces import ITask + >>> task = concepts['task'] = Concept(u'Task') + >>> task.conceptType = type + >>> ITypeConcept(task).typeInterface = ITask + + +Manage knowledge and knowledge requirements +=========================================== + +The classes used in this package are just adapters to IConcept. + + >>> from loops.knowledge.knowledge import Person, Topic, Task + >>> component.provideAdapter(Person, (IConcept,), IPerson) + >>> component.provideAdapter(Topic, (IConcept,), IKnowledgeElement) + >>> component.provideAdapter(Task, (IConcept,), ITask) + +First we want to set up a tree of knowledge elements (topics) and their +interdependencies. Note that in order to discern the concepts created +from their typeInterface adapters we here append a 'C' to the name of +the variables: + + >>> progLangC = concepts['progLang'] = Concept(u'Programming Language') + >>> ooProgC = concepts['ooProg'] = Concept(u'Object-oriented Programming') + >>> pythonC = concepts['python'] = Concept(u'Python') + >>> pyBasicsC = concepts['pyBasics'] = Concept(u'Python Basics') + >>> pyOoC = concepts['pyOo'] = Concept(u'OO Programming with Python') + >>> pySpecialsC = concepts['pySpecials'] = Concept(u'Python Specials') + + >>> topicConcepts = (progLangC, ooProgC, pythonC, pyBasicsC, pyOoC, pySpecialsC) + + >>> for c in topicConcepts: c.conceptType = topic + + >>> progLang, ooProg, python, pyBasics, pyOo, pySpecials = (IKnowledgeElement(c) + ... for c in topicConcepts) + + >>> python.parent = progLang + >>> pyBasics.parent = python + >>> pyOo.parent = python + >>> pySpecials.parent = python + + >>> pyOo.dependsOn(ooProg) + >>> pyOo.dependsOn(pyBasics) + +We now create a person and assign some knowledge to it: + + >>> johnC = concepts['john'] = Concept(u'John') + >>> johnC.conceptType = person + + >>> john = IPerson(johnC) + >>> john.knows(pyBasics) + >>> list(john.getKnowledge())[0].title + u'Python Basics' + +Now let's get to tasks - a task is used as a requirement profile, i.e. +it requires a certain set of knowledge elements: + + >>> task01C = concepts['task01C'] = Concept('Task: needs Python OO') + >>> task01C.conceptType = task + + >>> task01 = ITask(task01C) + >>> task01.requires(pyOo) + +Now we can ask what knowledge john is lacking if he would like to take +a position with the requirement profile: + + >>> missing = john.getMissingKnowledge(task01) + >>> [m.title for m in missing] + [u'Object-oriented Programming', u'OO Programming with Python'] + +Luckily there are a few elearning content objects out there that +provide some of the knowledge needed: + + >>> from cybertools.knowledge.interfaces import IKnowledgeProvider + >>> from loops.knowledge.knowledge import ConceptKnowledgeProvider + >>> component.provideAdapter(ConceptKnowledgeProvider, (IConcept,)) + >>> from loops.knowledge.knowledge import ResourceKnowledgeProvider + >>> component.provideAdapter(ResourceKnowledgeProvider, (IResource,)) + + >>> doc01C = concepts['doc01'] = Concept('Objectorienting Programming') + >>> doc01 = IKnowledgeProvider(doc01C) + >>> from loops.resource import Document + >>> doc02D = resources['doc02'] = Document('oopython.pdf') + >>> doc02 = IKnowledgeProvider(doc02D) + + >>> doc01.provides(ooProg) + >>> doc02.provides(pyOo) + +So that we are now able to find out what john has to study in order to +fulfill the position offered: + + >>> prov = list(john.getProvidersNeeded(task01)) + >>> len(prov) + 2 + >>> [list(d)[0].title for k, d in prov] + ['Objectorienting Programming', 'oopython.pdf'] + + +Views that make use of the knowledge management modules +------------------------------------------------------- + +One of the practical applications of this stuff is searching for missing +knowledge and corresponding knowledge providers for the user currently +logged in. + +For testing, we first have to provide the needed utilities and settings +(in real life this is all done during Zope startup): + + >>> from zope.app.security.interfaces import IAuthentication + >>> from zope.app.security.principalregistry import PrincipalRegistry + >>> auth = PrincipalRegistry() + >>> component.provideUtility(auth, IAuthentication) + + >>> from zope.app.principalannotation.interfaces import IPrincipalAnnotationUtility + >>> from zope.app.principalannotation import PrincipalAnnotationUtility + >>> principalAnnotations = PrincipalAnnotationUtility() + >>> component.provideUtility(principalAnnotations, IPrincipalAnnotationUtility) + + >>> principal = auth.definePrincipal('users.john', u'John', login='john') + >>> john.userId = 'users.john' + + >>> from zope.publisher.browser import TestRequest + >>> request = TestRequest() + >>> request.setPrincipal(principal) + + >>> from loops.knowledge.browser import MyKnowledge + >>> view = MyKnowledge(task01C, request) + >>> prov = view.myKnowledgeProvidersForTask() + + + +Fin de partie +============= + + >>> placefulTearDown() + diff --git a/knowledge/__init__.py b/knowledge/__init__.py new file mode 100644 index 0000000..4bc90fb --- /dev/null +++ b/knowledge/__init__.py @@ -0,0 +1,4 @@ +""" +$Id$ +""" + diff --git a/knowledge/browser.py b/knowledge/browser.py new file mode 100644 index 0000000..3803227 --- /dev/null +++ b/knowledge/browser.py @@ -0,0 +1,64 @@ +# +# Copyright (c) 2004 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 +# + +""" +Definition of view classes and other browser related stuff for the +loops.knowledge package. + +$Id$ +""" + +from zope import interface, component +from zope.app import zapi +from zope.app.pagetemplate import ViewPageTemplateFile +from zope.cachedescriptors.property import Lazy +from zope.formlib.namedtemplate import NamedTemplate +from zope.i18nmessageid import MessageFactory + +from cybertools.typology.interfaces import IType +from loops.browser.common import BaseView +from loops.knowledge.interfaces import IPerson, ITask +from loops.organize.browser import getPersonForLoggedInUser + +_ = MessageFactory('zope') + + +class MyKnowledge(BaseView): + + def __init__(self, context, request): + self.context = context + self.request = request + person = getPersonForLoggedInUser(request) + if person is not None: + person = IPerson(person) + self.person = person + + def myKnowledge(self): + if self.person is None: + return () + knowledge = self.person.getKnowledge() + return knowledge + + def myKnowledgeProvidersForTask(self): + if self.person is None: + return () + task = ITask(self.context) + # TODO: check correct conceptType for context! + providers = self.person.getProvidersNeeded(task) + return providers + diff --git a/knowledge/configure.zcml b/knowledge/configure.zcml new file mode 100644 index 0000000..26e038d --- /dev/null +++ b/knowledge/configure.zcml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/knowledge/interfaces.py b/knowledge/interfaces.py new file mode 100644 index 0000000..f76699f --- /dev/null +++ b/knowledge/interfaces.py @@ -0,0 +1,45 @@ +# +# Copyright (c) 2006 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 +# + +""" +Interfaces for knowledge management and elearning with loops. + +$Id$ +""" + +from zope.interface import Interface, Attribute +from zope import interface, component, schema +from zope.app import zapi +from zope.i18nmessageid import MessageFactory +from zope.security.proxy import removeSecurityProxy + +from cybertools.knowledge.interfaces import IKnowing, IRequirementProfile +from cybertools.organize.interfaces import IPerson as IBasePerson +from cybertools.organize.interfaces import ITask as IBaseTask + +_ = MessageFactory('zope') + + +class IPerson(IBasePerson, IKnowing): + """ A person, including knowledge/learning management features. + """ + + +class ITask(IBaseTask, IRequirementProfile): + """ A task, also acting as a knowledge requirement profile. + """ diff --git a/knowledge/knowledge.py b/knowledge/knowledge.py new file mode 100644 index 0000000..10a6108 --- /dev/null +++ b/knowledge/knowledge.py @@ -0,0 +1,183 @@ +# +# Copyright (c) 2006 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 +# + +""" +Adapters for IConcept providing interfaces from the +cybertools.knowledge package. + +$Id$ +""" + +from zope import interface, component +from zope.app import zapi +from zope.component import adapts +from zope.interface import implements +from zope.cachedescriptors.property import Lazy + +from cybertools.typology.interfaces import IType +from cybertools.knowledge.interfaces import IKnowledgeElement, IKnowledgeProvider +from cybertools.knowledge.knowing import Knowing +from loops.interfaces import IConcept +from loops.knowledge.interfaces import IPerson, ITask +from loops.organize.party import Person as BasePerson +from loops.organize.task import Task as BaseTask +from loops.type import TypeInterfaceSourceList, AdapterBase + + +# register type interfaces - (TODO: use a function for this) + +TypeInterfaceSourceList.typeInterfaces += (IPerson, IKnowledgeElement, ITask) + + +class KnowledgeAdapterMixin(object): + + @Lazy + def conceptManager(self): + return self.context.getLoopsRoot().getConceptManager() + + @Lazy + def standardPred(self): + return self.conceptManager.getDefaultPredicate() + + @Lazy + def dependsPred(self): + return self.conceptManager['depends'] + + @Lazy + def knowsPred(self): + return self.conceptManager['knows'] + + @Lazy + def requiresPred(self): + return self.conceptManager['requires'] + + @Lazy + def providesPred(self): + return self.conceptManager['provides'] + + def __eq__(self, other): + return self.context == other.context + + +class Person(BasePerson, Knowing, KnowledgeAdapterMixin): + """ A typeInterface adapter for concepts of type 'person', including + knowledge/learning management features. + """ + + implements(IPerson) + + def getKnowledge(self): + return (IKnowledgeElement(c) + for c in self.context.getParents((self.knowsPred,))) + + def knows(self, obj): + self.context.assignParent(obj.context, self.knowsPred) + + def removeKnowledge(self, obj): + self.context.deassignParent(obj.context, (self.knowsPred,)) + + +class Topic(AdapterBase, KnowledgeAdapterMixin): + """ A typeInterface adapter for concepts of type 'topic' that + may act as a knowledge element. + """ + + implements(IKnowledgeElement) + _attributes = ('context', '__parent__', 'parent') + + def getParent(self): + parents = self.context.getParents((self.standardPred,)) + return parents and IKnowledgeElement(parents[0]) or None + def setParent(self, obj): + old = self.getParent() + if old is not None and old.context != self.context: + self.context.deassignParent(old.context, (self.standardPred,)) + self.context.assignParent(obj.context, self.standardPred) + parent = property(getParent, setParent) + + def getDependencies(self): + return (IKnowledgeElement(c) + for c in self.context.getParents((self.dependsPred,))) + + def dependsOn(self, obj): + self.context.assignParent(obj.context, self.dependsPred) + + def removeDependency(self, obj): + self.context.deassignParent(obj.context, (self.dependsPred,)) + + def getDependents(self): + return (IKnowledgeElement(c) + for c in self.context.getChildren((self.dependsPred,))) + + def getKnowers(self): + return (IPerson(c) + for c in self.context.getChildren((self.knowsPred,))) + + def getProviders(self): + return (IKnowledgeProvider(c) + for c in self.context.getChildren((self.providesPred,)) + + self.context.getResources((self.providesPred,))) + + +class Task(BaseTask, KnowledgeAdapterMixin): + """ A typeInterface adapter for concepts of type 'task' that + may act as a knowledge requirement profile. + """ + + implements(ITask) + + def getRequirements(self): + return (IKnowledgeElement(c) + for c in self.context.getParents((self.requiresPred,))) + + def requires(self, obj): + self.context.assignParent(obj.context, self.requiresPred) + + def removeRequirement(self, obj): + self.context.deassignParent(obj.context, (self.requiresPred,)) + + +class ConceptKnowledgeProvider(AdapterBase, KnowledgeAdapterMixin): + + implements(IKnowledgeProvider) + + def getProvidedKnowledge(self): + return (IKnowledgeElement(c) + for c in self.context.getParents((self.providesPred,))) + + def provides(self, obj): + self.context.assignParent(obj.context, self.providesPred) + + def removeProvidedKnowledge(self, obj): + self.context.deassignParent(obj.context, (self.providesPred,)) + + +class ResourceKnowledgeProvider(AdapterBase, KnowledgeAdapterMixin): + + implements(IKnowledgeProvider) + + def getProvidedKnowledge(self): + return (IKnowledgeElement(c) + for c in self.context.getConcepts((self.providesPred,))) + + def provides(self, obj): + self.context.assignConcept(obj.context, self.providesPred) + + def removeProvidedKnowledge(self, obj): + self.context.deassignConcept(obj.context, (self.providesPred,)) + diff --git a/knowledge/tests.py b/knowledge/tests.py new file mode 100755 index 0000000..888ff78 --- /dev/null +++ b/knowledge/tests.py @@ -0,0 +1,24 @@ +# $Id$ + +import unittest, doctest +from zope.testing.doctestunit import DocFileSuite +from zope.app.testing import ztapi +from zope.interface.verify import verifyClass +from loops.organize.party import Person + +class Test(unittest.TestCase): + "Basic tests for the knowledge sub-package." + + def testSomething(self): + pass + + +def test_suite(): + flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS + return unittest.TestSuite(( + unittest.makeSuite(Test), + DocFileSuite('README.txt', optionflags=flags), + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/organize/browser.py b/organize/browser.py index 849eda2..18aab9c 100644 --- a/organize/browser.py +++ b/organize/browser.py @@ -44,13 +44,17 @@ from loops.organize.interfaces import IMemberRegistration _ = MessageFactory('zope') +def getPersonForLoggedInUser(request): + pa = annotations(request.principal) + return pa.get(ANNOTATION_KEY, None) + + class MyStuff(ConceptView): def __init__(self, context, request): self.context = context self.request = request - pa = annotations(request.principal) - self.person = pa.get(ANNOTATION_KEY, None) + self.person = getPersonForLoggedInUser(request) if self.person is not None: self.context = self.person diff --git a/organize/party.py b/organize/party.py index 041fdfb..165087f 100644 --- a/organize/party.py +++ b/organize/party.py @@ -41,7 +41,7 @@ from loops.organize.interfaces import IPerson, ANNOTATION_KEY from loops.type import TypeInterfaceSourceList, AdapterBase -# register IPerson as a type interface - (TODO: use a function for this) +# register type interfaces - (TODO: use a function for this) TypeInterfaceSourceList.typeInterfaces += (IPerson, IAddress) diff --git a/type.py b/type.py index f5f73ad..67cf412 100644 --- a/type.py +++ b/type.py @@ -224,7 +224,7 @@ class AdapterBase(object): adapts(IConcept) _attributes = ('context', '__parent__', ) - _schemas = (IConcept,) + _schemas = list(IConcept) def __init__(self, context): self.context = context # to get the permission stuff right