From 5271981119191480308b3cabb8d057f2f0a0cb6a Mon Sep 17 00:00:00 2001 From: helmutm Date: Sun, 10 Feb 2008 09:56:42 +0000 Subject: [PATCH] merged Dojo 1.0 branch git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@2388 fd906abe-77d9-0310-91a1-e0d9ade77398 --- README.txt | 4 +- browser/action.py | 40 +---- browser/action_macros.pt | 22 --- browser/common.py | 69 ++++++-- browser/concept.py | 223 ++++++++++++++++--------- browser/concept_macros.pt | 22 ++- browser/configure.zcml | 2 - browser/external.py | 9 +- browser/form.py | 69 +++++--- browser/form_macros.pt | 57 ++++--- browser/loops.css | 194 ++++++++++++++++++---- browser/loops.js | 222 +++++++++++++------------ browser/loops1.js | 148 ----------------- browser/node.pt | 2 +- browser/node.py | 59 ++++--- browser/node_macros.pt | 19 ++- browser/resource.py | 52 +++--- browser/skin/body.pt | 5 + compound/README.txt | 244 ++++++++++++++++++++++++++++ compound/base.py | 20 ++- compound/blog/__init__.py | 4 + compound/blog/browser.py | 190 ++++++++++++++++++++++ compound/blog/configure.zcml | 74 +++++++++ compound/blog/interfaces.py | 62 +++++++ compound/blog/post.py | 94 +++++++++++ compound/blog/schema.py | 39 +++++ compound/blog/security.py | 66 ++++++++ compound/blog/view_macros.pt | 71 ++++++++ compound/interfaces.py | 3 +- concept.py | 89 +++++++--- configure.zcml | 69 +++----- expert/README.txt | 8 +- expert/testsetup.py | 41 +---- external/README.txt | 90 ++++++++++ external/__init__.py | 5 + external/base.py | 143 ++++++++++++++++ external/browser.py | 83 ++++++++++ external/configure.zcml | 25 +++ external/element.py | 107 ++++++++++++ external/exportimport.pt | 60 +++++++ external.py => external/external.py | 10 +- external/interfaces.py | 85 ++++++++++ external/pyfunc.py | 73 +++++++++ external/tests.py | 23 +++ i18n/browser.py | 3 +- i18n/i18n_macros.pt | 6 +- interfaces.py | 26 ++- knowledge/glossary/browser.py | 3 +- knowledge/glossary/view_macros.pt | 20 ++- locales/de/LC_MESSAGES/loops.mo | Bin 4006 -> 6027 bytes locales/de/LC_MESSAGES/loops.po | 76 ++++++++- organize/README.txt | 114 ++++++++++--- organize/configure.zcml | 7 + organize/interfaces.py | 5 +- organize/member.py | 60 +++++-- organize/memberinfo.py | 57 +++++++ organize/party.py | 10 +- organize/tests.py | 27 +++ organize/util.py | 27 ++- query.py | 8 +- resource.py | 27 +-- schema.py | 2 + search/README.txt | 15 +- search/browser.py | 69 +++++--- search/search.pt | 22 ++- security.zcml | 49 ++++++ security/__init__.py | 3 + security/common.py | 133 +++++++++++++++ security/configure.zcml | 37 +++++ security/interfaces.py | 54 ++++++ security/manage_permissionform.pt | 115 +++++++++++++ security/perm.py | 139 ++++++++++++++++ security/policy.py | 96 +++++++++++ security/setter.py | 52 ++++++ setup.py | 104 +++++++++++- tests/auth.py | 60 +++++++ tests/setup.py | 48 +++++- type.py | 20 ++- util.py | 12 +- versioning/versionable.py | 1 + view.py | 5 +- xmlrpc/README.txt | 88 +++++----- xmlrpc/common.py | 2 +- 83 files changed, 3726 insertions(+), 872 deletions(-) delete mode 100644 browser/action_macros.pt delete mode 100644 browser/loops1.js create mode 100644 compound/blog/__init__.py create mode 100755 compound/blog/browser.py create mode 100644 compound/blog/configure.zcml create mode 100644 compound/blog/interfaces.py create mode 100644 compound/blog/post.py create mode 100644 compound/blog/schema.py create mode 100644 compound/blog/security.py create mode 100755 compound/blog/view_macros.pt create mode 100644 external/README.txt create mode 100644 external/__init__.py create mode 100644 external/base.py create mode 100644 external/browser.py create mode 100644 external/configure.zcml create mode 100644 external/element.py create mode 100644 external/exportimport.pt rename external.py => external/external.py (98%) create mode 100644 external/interfaces.py create mode 100644 external/pyfunc.py create mode 100755 external/tests.py create mode 100644 organize/memberinfo.py create mode 100644 security.zcml create mode 100644 security/__init__.py create mode 100644 security/common.py create mode 100644 security/configure.zcml create mode 100644 security/interfaces.py create mode 100644 security/manage_permissionform.pt create mode 100644 security/perm.py create mode 100644 security/policy.py create mode 100644 security/setter.py create mode 100644 tests/auth.py diff --git a/README.txt b/README.txt index dbe613f..b6da1e7 100755 --- a/README.txt +++ b/README.txt @@ -593,7 +593,7 @@ Actions >>> request = TestRequest() >>> view = NodeView(m112, request) >>> view.controller = Controller(view, request) - >>> view.setupController() + >>> #view.setupController() >>> actions = view.getActions('portlet') >>> len(actions) @@ -758,7 +758,7 @@ The new technique uses the ``fields`` and ``data`` attributes... linkUrl textline False None >>> view.data - {'linkUrl': u'', 'contentType': u'', 'data': u'', + {'linkUrl': u'http://', 'contentType': 'text/restructured', 'data': u'', 'title': u'Test Note'} The object is changed via a FormController adapter created for diff --git a/browser/action.py b/browser/action.py index feac08a..4bf3106 100644 --- a/browser/action.py +++ b/browser/action.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 @@ -27,43 +27,7 @@ from zope import component from zope.app.pagetemplate import ViewPageTemplateFile from zope.cachedescriptors.property import Lazy -action_macros = ViewPageTemplateFile('action_macros.pt') - - -class Action(object): - - template = action_macros - macroName = 'action' - condition = True - permission = None - url = '.' - viewName = '' - targetWindow = '' - title = '' - description = '' - icon = '' - cssClass = '' - onClick = '' - innerHtmlId = '' - - def __init__(self, view, **kw): - self.view = view - for k, v in kw.items(): - setattr(self, k, v) - - @Lazy - def macro(self): - return self.template.macros[self.macroName] - - @Lazy - def url(self): - return self.getActionUrl(self.view.url) - - def getActionUrl(self, baseUrl): - if self.viewName: - return '/'.join((baseUrl, self.viewName)) - else: - return baseUrl +from cybertools.browser.action import Action class TargetAction(Action): diff --git a/browser/action_macros.pt b/browser/action_macros.pt deleted file mode 100644 index 10e4313..0000000 --- a/browser/action_macros.pt +++ /dev/null @@ -1,22 +0,0 @@ - - - -
- iconAction Title -
- -
diff --git a/browser/common.py b/browser/common.py index 095cfab..d710383 100644 --- a/browser/common.py +++ b/browser/common.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 @@ -22,7 +22,6 @@ Common base class for loops browser view classes. $Id$ """ -from zope.app import zapi from zope import component from zope.app.form.browser.interfaces import ITerms from zope.app.i18n.interfaces import ITranslationDomain @@ -35,24 +34,27 @@ from zope.formlib import form from zope.formlib.form import FormFields from zope.formlib.namedtemplate import NamedTemplate from zope.interface import Interface, implements +from zope.proxy import removeAllProxies from zope.publisher.browser import applySkin from zope.publisher.interfaces.browser import IBrowserSkinType from zope import schema from zope.schema.vocabulary import SimpleTerm -from zope.security import canAccess, canWrite, checkPermission +from zope.security import canAccess, checkPermission from zope.security.interfaces import ForbiddenAttribute, Unauthorized from zope.security.proxy import removeSecurityProxy from zope.traversing.browser import absoluteURL -from zope.traversing.api import getName +from zope.traversing.api import getName, getParent +from cybertools.ajax.dojo import dojoMacroTemplate from cybertools.browser.view import GenericView from cybertools.relation.interfaces import IRelationRegistry from cybertools.text import mimetypes from cybertools.typology.interfaces import IType, ITypeManager from loops.common import adapted from loops.i18n.browser import I18NView -from loops.interfaces import IView +from loops.interfaces import IView, INode from loops.resource import Resource +from loops.security.common import canAccessObject, canListObject, canWriteObject from loops.type import ITypeConcept from loops import util from loops.util import _ @@ -90,29 +92,35 @@ class EditForm(form.EditForm): template = NamedTemplate('loops.pageform') def deleteObjectAction(self): - return None # better not to show the edit button at the moment - parent = zapi.getParent(self.context) + return None # better not to show the delete button at the moment + parent = getParent(self.context) parentUrl = absoluteURL(parent, self.request) return parentUrl + '/contents.html' class BaseView(GenericView, I18NView): - actions = {} # default only, don't update + actions = {} def __init__(self, context, request): super(BaseView, self).__init__(context, request) - # TODO: get rid of removeSecurityProxy() call + # TODO: get rid of removeSecurityProxy() call - not yet... self.context = removeSecurityProxy(context) + #self.context = context #self.setSkin(self.loopsRoot.skinName) - self.checkLanguage() + #self.checkLanguage() try: - if not canAccess(context, 'title'): + if not canAccessObject(context): raise Unauthorized #request.response.redirect('login.html') except ForbiddenAttribute: # ignore when testing pass + def update(self): + result = super(BaseView, self).update() + self.checkLanguage() + return result + @Lazy def target(self): # allow for having a separate object the view acts upon @@ -242,6 +250,16 @@ class BaseView(GenericView, I18NView): for o in objs: yield BaseView(o, request) + def renderText(self, text, contentType): + typeKey = util.renderingFactories.get(contentType, None) + if typeKey is None: + if contentType == u'text/html': + return text + return u'
%s
' % util.html_quote(text) + source = component.createObject(typeKey, text) + view = component.getMultiAdapter((removeAllProxies(source), self.request)) + return view.render() + # type listings def listTypes(self, include=None, exclude=None, sortOn='title'): @@ -340,7 +358,7 @@ class BaseView(GenericView, I18NView): @Lazy def editable(self): - return canWrite(self.context, 'title') + return canWriteObject(self.context) def getActions(self, category='object', page=None): """ Return a list of actions that provide the view and edit actions @@ -364,7 +382,7 @@ class BaseView(GenericView, I18NView): return False if ct.startswith('text/') and ct != 'text/rtf': return checkPermission('loops.ManageSite', self.context) - return canWrite(self.context, 'title') + return canWriteObject(self.context) @Lazy def inlineEditingActive(self): @@ -385,8 +403,27 @@ class BaseView(GenericView, I18NView): def registerDojo(self): cm = self.controller.macros - cm.register('js', 'dojo.js', resourceName='ajax.dojo/dojo.js') - #cm.register('js', 'dojo.js', resourceName='ajax.dojo1/dojo/dojo.js') + cm.register('js', 'dojo.js', template=dojoMacroTemplate, name='main', + position=0, + #djConfig='isDebug: true, parseOnLoad: true, usePlainJson: true, ' + djConfig='parseOnLoad: true, usePlainJson: true, ' + 'locale: "%s"' % self.languageInfo.language) + jsCall = 'dojo.require("dojo.parser");' + cm.register('js-execute', jsCall, jsCall=jsCall) + cm.register('css', identifier='tundra.css', position=0, + resourceName='ajax.dojo/dijit/themes/tundra/tundra.css', media='all') + cm.register('css', identifier='dojo.css', position=1, + resourceName='ajax.dojo/dojo/resources/dojo.css', media='all') + + def registerDojoDateWidget(self): + self.registerDojo() + jsCall = 'dojo.require("dijit.form.DateTextBox");' + self.controller.macros.register('js-execute', jsCall, jsCall=jsCall) + + def registerDojoTextWidget(self): + self.registerDojo() + jsCall = 'dojo.require("dijit.form.ValidationTextBox");' + self.controller.macros.register('js-execute', jsCall, jsCall=jsCall) # vocabulary stuff @@ -411,7 +448,7 @@ class LoopsTerms(object): def getTerm(self, value): #if value is None: # return SimpleTerm(None, '', u'not assigned') - title = value.title or zapi.getName(value) + title = value.title or getName(value) token = self.loopsRoot.getLoopsUri(value) return SimpleTerm(value, token, title) diff --git a/browser/concept.py b/browser/concept.py index 863e9c7..ca9e730 100644 --- a/browser/concept.py +++ b/browser/concept.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 @@ -44,7 +44,11 @@ from zope.schema.interfaces import IIterableSource from zope.security.proxy import removeSecurityProxy from zope.traversing.api import getName +from cybertools.browser.action import actions +from cybertools.composer.interfaces import IInstance +from cybertools.composer.schema.interfaces import ISchemaFactory from cybertools.typology.interfaces import IType, ITypeManager +from cybertools.util.jeep import Jeep from loops.browser.common import EditForm, BaseView, LoopsTerms, conceptMacrosTemplate from loops.common import adapted from loops.concept import Concept, ConceptTypeSourceList, PredicateSourceList @@ -56,8 +60,14 @@ from loops.versioning.util import getVersion class ConceptEditForm(EditForm, I18NView): + """ Classic-style (zope.formlib-based) form for editing concepts. + """ - @Lazy + #@Lazy # zope.formlib does not issue a redirect after changes, so that + # it tries to redisplay the old form even after a type change that + # changes the set of available attributes. So the typeInterface + # must be recalculated e.g. after an update of the context object. + @property def typeInterface(self): return IType(self.context).typeInterface @@ -79,7 +89,8 @@ class ConceptEditForm(EditForm, I18NView): def setUpWidgets(self, ignore_request=False): # TODO: get rid of removeSecurityProxy(): use ConceptSchema in interfaces - adapter = removeSecurityProxy(adapted(self.context, self.languageInfo)) + #adapter = removeSecurityProxy(adapted(self.context, self.languageInfo)) + adapter = adapted(self.context, self.languageInfo) self.adapters = {self.typeInterface: adapter, IConceptSchema: adapter} self.widgets = setUpEditWidgets( @@ -89,9 +100,87 @@ class ConceptEditForm(EditForm, I18NView): if desc: desc.height = 2 + +class ConceptRelationView(BaseView): + """ For displaying children and resources lists, used by ConceptView. + """ + + def __init__(self, relation, request, contextIsSecond=False): + if contextIsSecond: + self.context = relation.second + self.other = relation.first + else: + self.context = relation.first + self.other = relation.second + self.context = getVersion(self.context, request) + self.predicate = relation.predicate + self.relation = relation + self.request = request + + @Lazy + def adapted(self): + return adapted(self.context, self.languageInfo) + + @Lazy + def data(self): + return self.instance.applyTemplate() + + @Lazy + def instance(self): + instance = IInstance(self.adapted) + instance.template = self.schema + instance.view = self + return instance + + @Lazy + def schema(self): + ti = self.typeInterface or IConceptSchema + schemaFactory = component.getAdapter(self.adapted, ISchemaFactory) + return schemaFactory(ti, manager=self, request=self.request) + + @Lazy + def title(self): + return self.adapted.title or getName(self.context) + + @Lazy + def description(self): + return self.adapted.description + + @Lazy + def token(self): + return ':'.join((self.loopsRoot.getLoopsUri(self.context), + self.loopsRoot.getLoopsUri(self.predicate))) + + @Lazy + def uidToken(self): + return ':'.join((util.getUidForObject(self.context), + util.getUidForObject(self.predicate))) + + @Lazy + def isProtected(self): + return getName(self.predicate) == 'hasType' + + @Lazy + def predicateTitle(self): + return self.predicate.title + + @Lazy + def predicateUrl(self): + return zapi.absoluteURL(self.predicate, self.request) + + @Lazy + def relevance(self): + return self.relation.relevance + + @Lazy + def order(self): + return self.relation.order + + class ConceptView(BaseView): template = ViewPageTemplateFile('concept_macros.pt') + childViewFactory = ConceptRelationView @Lazy def macro(self): @@ -108,7 +197,7 @@ class ConceptView(BaseView): self.request.principal)): cont.macros.register('portlet_right', 'parents', title=_(u'Parents'), subMacro=self.template.macros['parents'], - position=0, info=self) + priority=20, info=self) @Lazy def adapted(self): @@ -123,7 +212,8 @@ class ConceptView(BaseView): return self.adapted.description def fieldData(self): - # TODO: use cybertools.composer.schema.instance, see loops.browser.form + # obsolete - use getData() instead + # TODO: change view macros accordingly ti = IType(self.context).typeInterface if not ti: return @@ -140,12 +230,35 @@ class ConceptView(BaseView): widget.setRenderedValue(value) yield dict(title=f.title, value=value, id=n, widget=widget) - def children(self, topLevelOnly=True, sort=True): + def getData(self, omit=('title', 'description')): + data = self.instance.applyTemplate() + for k in omit: + if k in data: + del data[k] + return data + + @Lazy + def data(self): + return self.getData() + + @Lazy + def instance(self): + instance = IInstance(self.adapted) + instance.template = self.schema + instance.view = self + return instance + + @Lazy + def schema(self): + ti = self.typeInterface or IConceptSchema + schemaFactory = component.getAdapter(self.adapted, ISchemaFactory) + return schemaFactory(ti, manager=self, request=self.request) + + def getChildren(self, topLevelOnly=True, sort=True): cm = self.loopsRoot.getConceptManager() hasType = cm.getTypePredicate() standard = cm.getDefaultPredicate() - #rels = self.context.getChildRelations() - rels = (ConceptRelationView(r, self.request, contextIsSecond=True) + rels = (self.childViewFactory(r, self.request, contextIsSecond=True) for r in self.context.getChildRelations(sort=None)) if sort: rels = sorted(rels, key=lambda r: (r.order, r.title.lower())) @@ -160,27 +273,37 @@ class ConceptView(BaseView): if skip: continue yield r + # Override in subclass to control what is displayd in listings: + children = getChildren + def childrenAlphaGroups(self): - letters = [] - relations = {} - rels = self.children(topLevelOnly=False, sort=False) + result = Jeep() + rels = self.getChildren(topLevelOnly=False, sort=False) rels = sorted(rels, key=lambda r: r.title.lower()) for letter, group in groupby(rels, lambda r: r.title.lower()[0]): letter = letter.upper() - letters.append(letter) - relations[letter] = list(group) - return dict(letters=letters, relations=relations) + result[letter] = list(group) + return result + + def childrenByType(self): + result = Jeep() + rels = self.getChildren(topLevelOnly=False, sort=False) + rels = sorted(rels, key=lambda r: (r.typeTitle.lower(), r.title.lower())) + for type, group in groupby(rels, lambda r: r.type): + typeName = getName(type.typeProvider) + result[typeName] = list(group) + return result def parents(self): rels = sorted(self.context.getParentRelations(), key=(lambda x: x.first.title.lower())) for r in rels: - yield ConceptRelationView(r, self.request) + yield self.childViewFactory(r, self.request) def resources(self): rels = self.context.getResourceRelations() for r in rels: - yield ConceptRelationView(r, self.request, contextIsSecond=True) + yield self.childViewFactory(r, self.request, contextIsSecond=True) @Lazy def view(self): @@ -213,6 +336,14 @@ class ConceptView(BaseView): for node in self.context.getClients(): yield NodeView(node, self.request) + def getActions(self, category='object', page=None): + t = IType(self.context) + actInfo = t.optionsDict.get('action.' + category, '') + actNames = [n.strip() for n in actInfo.split(',')] + if actNames: + return actions.get(category, actNames, view=self, page=page) + return [] + class ConceptConfigureView(ConceptView): @@ -321,7 +452,7 @@ class ConceptConfigureView(ConceptView): else: start = end = searchType criteria['loops_type'] = (start, end) - cat = zapi.getUtility(ICatalog) + cat = component.getUtility(ICatalog) result = cat.searchResults(**criteria) # TODO: can this be done in a faster way? result = [r for r in result if r.getLoopsRoot() == self.loopsRoot] @@ -333,64 +464,8 @@ class ConceptConfigureView(ConceptView): def predicates(self): preds = PredicateSourceList(self.context) - terms = zapi.getMultiAdapter((preds, self.request), ITerms) + terms = component.getMultiAdapter((preds, self.request), ITerms) for pred in preds: yield terms.getTerm(pred) -class ConceptRelationView(BaseView): - - def __init__(self, relation, request, contextIsSecond=False): - if contextIsSecond: - self.context = relation.second - self.other = relation.first - else: - self.context = relation.first - self.other = relation.second - self.context = getVersion(self.context, request) - self.predicate = relation.predicate - self.relation = relation - self.request = request - - @Lazy - def adapted(self): - return adapted(self.context, self.languageInfo) - - @Lazy - def title(self): - return self.adapted.title or getName(self.context) - - @Lazy - def description(self): - return self.adapted.description - - @Lazy - def token(self): - return ':'.join((self.loopsRoot.getLoopsUri(self.context), - self.loopsRoot.getLoopsUri(self.predicate))) - - @Lazy - def uidToken(self): - return ':'.join((util.getUidForObject(self.context), - util.getUidForObject(self.predicate))) - - @Lazy - def isProtected(self): - return zapi.getName(self.predicate) == 'hasType' - - @Lazy - def predicateTitle(self): - return self.predicate.title - - @Lazy - def predicateUrl(self): - return zapi.absoluteURL(self.predicate, self.request) - - @Lazy - def relevance(self): - return self.relation.relevance - - @Lazy - def order(self): - return self.relation.order - diff --git a/browser/concept_macros.pt b/browser/concept_macros.pt index 3c5f0ba..bd63fa1 100644 --- a/browser/concept_macros.pt +++ b/browser/concept_macros.pt @@ -1,8 +1,8 @@
-
-
-
+ + +
@@ -10,7 +10,8 @@

- Title + Title

@@ -19,13 +20,16 @@ +

- - + +
:
:
+
@@ -47,7 +51,7 @@ ondblclick python: item.openEditWindow('configure.html')" tal:define="children python: list(item.children())" tal:condition="children"> -

Children


+

Children

@@ -81,7 +85,7 @@ ondblclick python: item.openEditWindow('resources.html')" tal:define="resources python: list(item.resources())" tal:condition="resources"> -

Resources


+

Resources

Title
diff --git a/browser/configure.zcml b/browser/configure.zcml index 85ac99d..f6e8db2 100644 --- a/browser/configure.zcml +++ b/browser/configure.zcml @@ -15,8 +15,6 @@ - - diff --git a/browser/external.py b/browser/external.py index 3d420a2..a9e2728 100644 --- a/browser/external.py +++ b/browser/external.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 @@ -29,8 +29,8 @@ from zope.cachedescriptors.property import Lazy from zope.security.proxy import removeSecurityProxy from cStringIO import StringIO -from loops.external import IExternalContentSource -from loops.external import ILoader, IExporter +from loops.external.external import IExternalContentSource +from loops.external.external import ILoader, IExporter class NodesExportImport(object): @@ -38,7 +38,8 @@ class NodesExportImport(object): """ def __init__(self, context, request): - self.context = removeSecurityProxy(context) + #self.context = removeSecurityProxy(context) + self.context = context self.request = request self.message = u'' diff --git a/browser/form.py b/browser/form.py index c921577..b061bfa 100644 --- a/browser/form.py +++ b/browser/form.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 @@ -30,11 +30,9 @@ from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent from zope.app.container.interfaces import INameChooser from zope.app.container.contained import NameChooser -from zope.app.form.browser.textwidgets import FileWidget, TextAreaWidget from zope.app.pagetemplate import ViewPageTemplateFile from zope.cachedescriptors.property import Lazy from zope.contenttype import guess_content_type -#from zope.formlib.form import Form, EditForm, FormFields from zope.publisher.browser import FileUpload from zope.publisher.interfaces import BadRequest from zope.security.proxy import isinstance, removeSecurityProxy @@ -81,7 +79,8 @@ class ObjectForm(NodeView): def closeAction(self, submit=False): if self.isInnerHtml: - return 'dialogs["%s"].hide()' % self.dialog_name + return ("closeDataWidget(%s); dialog.hide();" % + (submit and 'true' or 'false')) if submit: return "xhrSubmitPopup('dialog_form', '%s'); return false" % (self.request.URL) return 'window.close()' @@ -101,7 +100,10 @@ class ObjectForm(NodeView): @Lazy def fieldRenderers(self): - return schema_macros.macros + renderers = dict(schema_macros.macros) + # replace HTML edit widget with Dojo Editor + renderers['input_html'] = self.template.macros['input_html'] + return renderers @Lazy def fieldEditRenderers(self): @@ -109,19 +111,17 @@ class ObjectForm(NodeView): @Lazy def schema(self): - #ti = self.typeInterface or Interface #IConcept ti = self.typeInterface or IConceptSchema schemaFactory = component.getAdapter(self.adapted, ISchemaFactory) return schemaFactory(ti, manager=self, request=self.request) @Lazy def fields(self): - return self.schema.fields + return [f for f in self.schema.fields if not f.readonly] @Lazy def data(self): instance = self.instance - instance.template = self.schema data = instance.applyTemplate(mode='edit') form = self.request.form for k, v in data.items(): @@ -132,11 +132,15 @@ class ObjectForm(NodeView): @Lazy def instance(self): - return IInstance(self.adapted) + instance = IInstance(self.adapted) + instance.template = self.schema + instance.view = self + return instance def __call__(self): if self.isInnerHtml: response = self.request.response + #response.setHeader('Content-Type', 'text/html; charset=UTF-8') response.setHeader('Expires', 'Sat, 1 Jan 2000 00:00:00 GMT') response.setHeader('Pragma', 'no-cache') return innerHtml(self) @@ -240,11 +244,16 @@ class CreateObjectForm(ObjectForm): @Lazy def adapted(self): - return self.typeInterface(Resource()) + ad = self.typeInterface(Resource()) + ad.storageName = 'unknown' # hack for file objects: don't try to retrieve data + return (ad) @Lazy def instance(self): - return IInstance(Resource()) + instance = IInstance(self.adapted) + instance.template = self.schema + instance.view = self + return instance @Lazy def typeInterface(self): @@ -278,8 +287,9 @@ class CreateObjectPopup(CreateObjectForm): cm = self.controller.macros cm.register('css', identifier='popup.css', resourceName='popup.css', media='all', position=4) - jsCall = ('dojo.require("dojo.widget.Dialog");' - 'dojo.require("dojo.widget.ComboBox");') + jsCall = ('dojo.require("dojo.parser");' + 'dojo.require("dijit.form.FilteringSelect");' + 'dojo.require("dojox.data.QueryReadStore");') cm.register('js-execute', jsCall, jsCall=jsCall) return True @@ -298,14 +308,19 @@ class CreateConceptForm(CreateObjectForm): @Lazy def adapted(self): + c = Concept() ti = self.typeInterface if ti is None: - return Concept() - return ti(Concept()) + return c + ad = ti(c) + return ad @Lazy def instance(self): - return IInstance(Concept()) + instance = IInstance(self.adapted) + instance.template = self.schema + instance.view = self + return instance @Lazy def typeInterface(self): @@ -355,9 +370,6 @@ class EditObject(FormController, I18NView): prefix = 'form.' conceptPrefix = 'assignments.' - old = None - selected = None - @Lazy def adapted(self): return adapted(self.object, self.languageInfoForUpdate) @@ -377,7 +389,10 @@ class EditObject(FormController, I18NView): @Lazy def instance(self): - return component.getAdapter(self.adapted, IInstance, name='editor') + instance = component.getAdapter(self.adapted, IInstance, name='editor') + instance.template = self.schema + instance.view = self.view + return instance @Lazy def loopsRoot(self): @@ -406,14 +421,17 @@ class EditObject(FormController, I18NView): obj = self.object form = self.request.form instance = self.instance - instance.template = self.schema + #instance.template = self.schema formState = instance.applyTemplate(data=form, fieldHandlers=self.fieldHandlers) + self.selected = [] + self.old = [] for k in form.keys(): if k.startswith(self.prefix): fn = k[len(self.prefix):] value = form[k] if fn.startswith(self.conceptPrefix) and value: self.collectConcepts(fn[len(self.conceptPrefix):], value) + self.collectAutoConcepts() if self.old or self.selected: self.assignConcepts(obj) notify(ObjectModifiedEvent(obj)) @@ -441,14 +459,15 @@ class EditObject(FormController, I18NView): def collectConcepts(self, fieldName, value): if self.old is None: self.old = [] - if self.selected is None: - self.selected = [] for v in value: if fieldName == 'old': self.old.append(v) elif fieldName == 'selected' and v not in self.selected: self.selected.append(v) + def collectAutoConcepts(self): + pass + def assignConcepts(self, obj): for v in self.old: if v not in self.selected: @@ -532,6 +551,10 @@ class CreateObject(EditObject): class EditConcept(EditObject): + @Lazy + def typeInterface(self): + return IType(self.object).typeInterface or IConceptSchema + def getConceptRelations(self, obj, predicates, concept): return obj.getParentRelations(predicates=predicates, parent=concept) diff --git a/browser/form_macros.pt b/browser/form_macros.pt index 7e6f491..1cd0d86 100644 --- a/browser/form_macros.pt +++ b/browser/form_macros.pt @@ -2,7 +2,8 @@ $Id$ --> -
- @@ -44,7 +45,7 @@ + i18n:translate="">Assign Parent Concepts @@ -61,7 +62,8 @@ -
Title

+
Edit Information Object
Concept Assignments
- + + + + + + + + diff --git a/browser/loops.css b/browser/loops.css index 97d4c3b..a61fbaa 100644 --- a/browser/loops.css +++ b/browser/loops.css @@ -11,71 +11,111 @@ a[href]:hover { } pre { + background-color: #f4f4f4; overflow: scroll; max-height: 35em; } +ul, p { + margin-top: 0.4em; + margin-bottom: 0.5em; +} + table.listing td { white-space: normal; } -.box div.body div.even { - background-color: #f4f4f4; +fieldset.box { + margin: 1em 0 0.5em 0; + padding: 0.5em; + border: 1px solid #ccc; } -.box { - margin: 12px; +fieldset.box td { + padding: 0.2em 0.2em 0.2em 0; } #body { margin-left: 5px; } -.top-actions{ +.top-actions { position: absolute; right: 2em; top: 1em; } -/*.content-1 h1 { */ -h1 { +.top image { + margin-top: -1px; +} + +.content-1 h1, h1 { font-size: 160%; - font-weight: bold; + margin-top: 0.8em; + padding-bottom: 3px; } -.content-2 h1, h2 { +.content-2 h1, .content-1 h2, h2 { font-size: 140%; + font-weight: normal; + margin-top: 0.7em; } -.content-3 h1, .content-2 h2, h3 { +.content-3 h1, .content-2 h2, .content-1 h3, h3 { font-size: 130%; - font-weight: bold; + font-weight: normal; + margin-top: 0.7em; } -.content-4 h1, .content-3 h2, .content-2 h3, .content-1 h4 { +.content-4 h1, .content-3 h2, .content-2 h3, .content-1 h4, h4 { font-size: 120%; + font-weight: normal; + margin-top: 0.7em; } .content-5 h1, .content-4 h2, .content-3 h3, content-2 h4 { font-size: 100%; border: none; -} - -.subcolumn { - display: inline; - float: left; + margin-top: 0.7em; } .box { - margin: 5px; + margin: 12px; + /*margin: 5px;*/ padding: 6px; padding-top: 0; } +div.box { + margin: 12px 12px 8px 12px; + border: 1px solid #ccc; + border-bottom: none; +} + +div.box h4 { + font: 110% Verdana, Tahoma, Arial, Helvetica, sans-serif; + color: #000040; + border: none; + border-bottom: 1px solid #ccc; + padding: 4px; + padding-top: 1px; + padding-bottom: 3px; + background-color: #ddd; + height: auto; +} + .box h1, .box h2, .box h3 { border-bottom: None; } +.box div.body div.even { + background-color: #f4f4f4; +} + +.box div.body div { + padding: 0.2em 0.2em 0.2em 0.3em; +} + div.menu-1, div.menu-2 { border-top: 1px solid #eeeeee; font-weight: bold; @@ -83,7 +123,7 @@ div.menu-1, div.menu-2 { .box div.body div.menu-3 { border-top: none; - padding-left: 1.5em; + padding: 0.1em 0 0.2em 1.5em; } .box div.body div.menu-4 { @@ -91,6 +131,19 @@ div.menu-1, div.menu-2 { font-size: 90% } +.subcolumn { + display: inline; + float: left; +} + +.footer { + text-align: center; + border-top: 1px solid #ccc; + border-bottom: none; + margin-top: 12px; + padding-top: 6px; +} + .flow-left { float: left; } @@ -128,6 +181,45 @@ img.notselected { margin: 1em 0 0.5em 0; } +.button { + margin: 1em 0 1em 0; +} + +.button a, .button a:visited { + padding: 2px 4px 2px 4px; + background-color: #e8e8e8; + text-decoration: None; + color: Black; + border-width: 2px; + border-style: solid; + border-color: #f4f4f4 #989898 #989898 #f4f4f4; +} + +.button a:active { + border-color: #989898 #f4f4f4 #f4f4f4 #989898; +} + +table.listing { + margin: 1px; + margin-top: 6px; +} + +table.listing th { + font-family: Verdana, Tahoma, Arial, Helvetica, sans-serif; + color: #000040; +} + +.itemViews { + border-bottom-width: 2px; +} + +.dialog .headline { + font-size: 120%; + font-weight: bold; + text-align: center; + padding: 1em 0 1em 0; +} + /* search stuff */ @@ -139,34 +231,70 @@ img.notselected { font-weight: bold; } -/* dojo stuff */ +/* blog */ -/*.dojoComboBox { - width: 200px; -}*/ - -.dojoDialog { - background: #eee; - border: 1px solid #999; - -moz-border-radius: 5px; - padding: 4px; +.blog .description { + font-size: 90%; + color: #666666; } -.dojoDialog th { +.blogpost .description { + font-weight: bold; + font-size: 90%; + color: #666666; + padding-top: 0.4em; +} + +.blog .info, .blogpost .info { + font-style: italic; + font-size: 90%; + color: #666666; + padding-top: 0.4em; +} + +/* dojo stuff */ + +.dijitDialog { + background-color: #aaaaaaa; + border: 1px solid #999; + padding: 5px; +} + +.dijitDialogPaneContent { + background-color: #aaaaaaa; +} + +.dijitDialog th { font-size: 120%; + font-weight: bold; + text-align: center; padding: 0 5px 8px 5px; } -.dojoDialog .headline { +.dijitDialog td { + padding: 2px; +} + +.dijitDialog .headline { font-weight: bold; } -.dojoDialog input.text { +.dijitDialog input.text { width: 100%; margin-right: 10px; } -.dojoDialog input.submit { +.dijitDialog input.submit { font-weight: bold; } +.dijitDialogUnderlay { + background-color: White; +} + +div.RichTextEditable { + border-top: 2px solid grey; + border-left: 2px solid grey; + border-right: 2px solid #eeeeee; + border-bottom: 2px solid #eeeeee; +} diff --git a/browser/loops.js b/browser/loops.js index 10c0f65..a0ff1e6 100644 --- a/browser/loops.js +++ b/browser/loops.js @@ -13,144 +13,122 @@ function focusOpener() { } } +function validate(nodeName, required) { + // (work in progress) - may be used for onBlur event handler + var w = dojo.byId(nodeName); + if (required && w.value == '') { + w.setAttribute('style','background-color: #ffff00'); + w.focus(); + return false; + } +} + +function destroyWidgets(node) { + dojo.forEach(dojo.query('[widgetId]', node), function(n) { + w = dijit.byNode(n); + if (w != undefined) { + w.destroyRecursive(); + } + }); +} + +function replaceNode(html, nodeName) { + var newNode = document.createElement('div'); + newNode.innerHTML = html; + if (nodeName == undefined) { + nodeName = newNode.firstChild.getAttribute('id'); + } + if (nodeName != undefined) { + newNode.id = nodeName; + var node = dojo.byId(nodeName); + destroyWidgets(node); + var parent = node.parentNode; + parent.replaceChild(newNode, node); + dojo.parser.parse(newNode); + } else { + window.location.href = url; + } +} + +function updateNode(url, nodeName) { + dojo.xhrGet({ + url: url, + handleAs: 'text', + load: function(response, ioArgs) { + replaceNode(response, nodeName); + } + }); +} + function replaceFieldsNode(targetId, typeId, url) { token = dojo.byId(typeId).value; uri = url + '?form.type=' + token; - dojo.io.updateNode(targetId, uri); + updateNode(uri, 'form.fields'); } function replaceFieldsNodeForLanguage(targetId, langId, url) { lang = dojo.byId(langId).value; uri = url + '?loops.language=' + lang; - dojo.io.updateNode(targetId, uri); + updateNode(uri, 'form.fields'); } -function submitReplacing(targetId, formId, actionUrl) { - dojo.io.updateNode(targetId, { - url: actionUrl, - formNode: dojo.byId(formId), - method: 'post' - }); - return false; -} - -function xhrSubmitPopup(formId, actionUrl) { - dojo.io.bind({ - url: actionUrl, - formNode: dojo.byId(formId), - method: 'post', +function submitReplacing(targetId, formId, url) { + dojo.xhrPost({ + url: url, + form: dojo.byId(formId), mimetype: "text/html", - load: function(t, d, e) { + load: function(response, ioArgs) { + replaceNode(response, targetId); + } + }) +} + +function xhrSubmitPopup(formId, url) { + dojo.xhrPost({ + url: url, + form: dojo.byId(formId), + mimetype: "text/html", + load: function(response, ioArgs) { window.close(); } }); } -function submitReplacingOrReloading(targetId, formId, actionUrl) { - node = dojo.byId(targetId); - var args = { - url: actionUrl, - formNode: dojo.byId(formId), - method: 'post', - mimetype: "text/html" - }; - args.load = function (t, d, e) { - if (d.length < 10) { - document.location.reload(false); - } else { - while (node.firstChild) { - dojo.dom.destroyNode(node.firstChild); - } - node.innerHTML = d; - } - }; - dojo.io.bind(args); - return false; -} - -function inlineEdit(id, saveUrl) { - var iconNode = dojo.byId('inlineedit_icon'); - iconNode.style.visibility = 'hidden'; - editor = dojo.widget.createWidget('Editor', - {items: ['save', '|', 'formatblock', '|', - 'insertunorderedlist', 'insertorderedlist', '|', - 'bold', 'italic', '|', 'createLink', 'insertimage'], - saveUrl: saveUrl, - //closeOnSave: true, - htmlEditing: true - //onClose: function() { - /* onSave: function() { - this.disableToolbar(true); - iconNode.style.visibility = 'visible'; - //window.location.reload(); - }*/ - }, dojo.byId(id)); - editor._save = function (e) { - if (!this._richText.isClosed) { - if (this.saveUrl.length) { - var content = {}; - this._richText.contentFilters = []; - content[this.saveArgName] = this.getHtml(); - content['version'] = 'this'; - dojo.io.bind({method:this.saveMethod, - url:this.saveUrl, - content:content, - handle:function(type, data, ti, kwargs) { - location.reload(false); - } - }); //alert('save'); - } else { - dojo.debug("please set a saveUrl for the editor"); - } - if (this.closeOnSave) { - this._richText.close(e.getName().toLowerCase() == "save"); - } - } - } - return false; -} - function setConceptTypeForComboBox(typeId, cbId) { var t = dojo.byId(typeId).value; - var cb = dojo.widget.manager.getWidgetById(cbId); - var dp = cb.dataProvider; - var baseUrl = dp.searchUrl.split('&')[0]; - var newUrl = baseUrl + '&searchType=' + t; - dp.searchUrl = newUrl; - cb.setValue(''); + var cb = dijit.byId(cbId); + var dp = cb.store; + var baseUrl = dp.url.split('?')[0]; + var newUrl = baseUrl + '?searchType=' + t; + dp.url = newUrl; + cb.setDisplayedValue(''); } -var dialogs = {} +var dialog; +var dialogName function objectDialog(dlgName, url) { - dojo.require('dojo.widget.Dialog'); - dojo.require('dojo.widget.ComboBox'); - dlg = dialogs[dlgName]; - if (!dlg) { - //dlg = dojo.widget.fromScript('Dialog', - dlg = dojo.widget.createWidget('Dialog', - {bgColor: 'white', bgOpacity: 0.5, toggle: 'fade', - toggleDuration: 250, - executeScripts: true, - href: url - }, dojo.byId('dialog.' + dlgName)); - dialogs[dlgName] = dlg; + dojo.require('dijit.Dialog'); + dojo.require('dojo.parser'); + dojo.require('dijit.form.FilteringSelect'); + dojo.require('dojox.data.QueryReadStore'); + if (dialogName == undefined || dialogName != dlgName) { + if (dialog != undefined) { + dialog.destroyRecursive(); + } + dialogName = dlgName; + dialog = new dijit.Dialog({ + href: url + }, dojo.byId('dialog.' + dlgName)); } - dlg.show(); + dialog.show(); } function addConceptAssignment(prefix, suffix) { - dojo.require('dojo.html') node = dojo.byId('form.' + suffix); - els = document.forms[0].elements; - for (var i=0; i' + title + ''; + td.innerHTML = ' ' + title + ''; var tr = document.createElement('tr'); tr.appendChild(td); node.appendChild(tr); } +function closeDataWidget(save) { + var widget = dijit.byId('data'); + if (widget != undefined && save) { + value = widget.getValue(); + //widget.close(save); + form = dojo.byId('dialog_form'); + var ta = document.createElement('textarea'); + ta.name = 'data'; + ta.value = value; + form.appendChild(ta); + } +} + +var editor; + +function inlineEdit(id) { + if (editor == undefined) { + editor = new dijit.Editor({}, dojo.byId(id)); + } +} diff --git a/browser/loops1.js b/browser/loops1.js deleted file mode 100644 index eb76aa6..0000000 --- a/browser/loops1.js +++ /dev/null @@ -1,148 +0,0 @@ -/* $Id: loops.js 1965 2007-08-27 17:33:07Z helmutm $ */ - -function openEditWindow(url) { - zmi = window.open(url, 'zmi'); - zmi.focus(); - return false; -} - -function focusOpener() { - if (typeof(opener) != 'undefined' && opener != null) { - opener.location.reload(); - opener.focus(); - } -} - -function replaceFieldsNode(targetId, typeId, url) { - token = dojo.byId(typeId).value; - uri = url + '?form.type=' + token; - dojo.io.updateNode(targetId, uri); -} - -function submitReplacing(targetId, formId, actionUrl) { - dojo.io.updateNode(targetId, { - url: actionUrl, - formNode: dojo.byId(formId), - method: 'post' - }); - return false; -} - -function submitReplacingOrReloading(targetId, formId, actionUrl) { - node = dojo.byId(targetId); - var args = { - url: actionUrl, - formNode: dojo.byId(formId), - method: 'post', - mimetype: "text/html" - }; - args.load = function (t, d, e) { - if (d.length < 10) { - document.location.reload(false); - } else { - while (node.firstChild) { - dojo.dom.destroyNode(node.firstChild); - } - node.innerHTML = d; - } - }; - dojo.io.bind(args); - return false; -} - -function inlineEdit(id, saveUrl) { - var iconNode = dojo.byId('inlineedit_icon'); - iconNode.style.visibility = 'hidden'; - editor = dojo.widget.createWidget('Editor', - {items: ['save', '|', 'formatblock', '|', - 'insertunorderedlist', 'insertorderedlist', '|', - 'bold', 'italic', '|', 'createLink', 'insertimage'], - saveUrl: saveUrl, - //closeOnSave: true, - htmlEditing: true - //onClose: function() { - /* onSave: function() { - this.disableToolbar(true); - iconNode.style.visibility = 'visible'; - //window.location.reload(); - }*/ - }, dojo.byId(id)); - editor._save = function (e) { - if (!this._richText.isClosed) { - if (this.saveUrl.length) { - var content = {}; - this._richText.contentFilters = []; - content[this.saveArgName] = this.getHtml(); - content['version'] = 'this'; - dojo.io.bind({method:this.saveMethod, - url:this.saveUrl, - content:content, - handle:function(type, data, ti, kwargs) { - location.reload(false); - } - }); //alert('save'); - } else { - dojo.debug("please set a saveUrl for the editor"); - } - if (this.closeOnSave) { - this._richText.close(e.getName().toLowerCase() == "save"); - } - } - } - return false; -} - -function setConceptTypeForComboBox(typeId, cbId) { - var t = dojo.byId(typeId).value; - var cb = dijit.byId(cbId) - var dp = cb.store; - var baseUrl = dp.url.split('&')[0]; - var newUrl = baseUrl + '&searchType=' + t; - dp.url = newUrl; - cb.setValue(''); -} -x -var dialogs = {} - -function objectDialog(dlgName, url) { - dojo.require('dijit.Dialog'); - dojo.require('dijit.form.ComboBox'); - dojo.require('dojox.data.QueryReadStore'); - dlg = dialogs[dlgName]; - if (!dlg) { - dlg = new dijit.Dialog( - {bgColor: 'white', bgOpacity: 0.5, toggle: 'fade', toggleDuration: 250, - executeScripts: true, - href: url - }, dojo.byId('dialog.' + dlgName)); - dialogs[dlgName] = dlg; - } - dlg.show(); -} - -function addConceptAssignment() { - dojo.require('dojo.html') - node = dojo.byId('form.assignments'); - els = document.forms[0].elements; - for (var i=0; i' + title + ''; - var tr = document.createElement('tr'); - tr.appendChild(td); - node.appendChild(tr); -} - diff --git a/browser/node.pt b/browser/node.pt index 9302b0f..1edc365 100644 --- a/browser/node.pt +++ b/browser/node.pt @@ -16,7 +16,7 @@ diff --git a/browser/node.py b/browser/node.py index bff8fbe..49c4bae 100644 --- a/browser/node.py +++ b/browser/node.py @@ -44,16 +44,17 @@ from zope.security.proxy import removeSecurityProxy from cybertools.ajax import innerHtml from cybertools.browser import configurator +from cybertools.browser.action import Action from cybertools.browser.view import GenericView from cybertools.typology.interfaces import IType, ITypeManager from cybertools.xedit.browser import ExternalEditorView +from loops.browser.action import DialogAction from loops.i18n.browser import i18n_macros from loops.interfaces import IConcept, IResource, IDocument, IMediaAsset, INode from loops.interfaces import IViewConfiguratorSchema from loops.resource import MediaAsset from loops import util from loops.util import _ -from loops.browser.action import Action, DialogAction, TargetAction from loops.browser.common import BaseView from loops.browser.concept import ConceptView from loops.versioning.util import getVersion @@ -65,8 +66,8 @@ node_macros = ViewPageTemplateFile('node_macros.pt') class NodeView(BaseView): _itemNum = 0 - template = node_macros + nextUrl = None def __init__(self, context, request): super(NodeView, self).__init__(context, request) @@ -80,23 +81,31 @@ class NodeView(BaseView): def setupController(self): cm = self.controller.macros cm.register('css', identifier='loops.css', resourceName='loops.css', - media='all', position=3) - cm.register('js', 'loops.js', resourceName='loops.js') - #cm.register('js', 'loops.js', resourceName='loops1.js') + media='all', priority=60) + cm.register('js', 'loops.js', resourceName='loops.js', priority=60) cm.register('top_actions', 'top_actions', name='multi_actions', subMacros=[i18n_macros.macros['language_switch']]) cm.register('portlet_left', 'navigation', title='Navigation', subMacro=node_macros.macros['menu']) - #if not IUnauthenticatedPrincipal.providedBy(self.request.principal): if canWrite(self.context, 'title'): #cm.register('portlet_right', 'clipboard', title='Clipboard', # subMacro=self.template.macros['clipboard']) - # this belongs to loops.organize; how to register portlets - # from sub- (other) packages? - # see controller / configurator: use multiple configurators; - # register additional configurators (adapters) from within package. + # this belongs to loops.organize cm.register('portlet_right', 'actions', title=_(u'Actions'), - subMacro=node_macros.macros['actions']) + subMacro=node_macros.macros['actions'], + priority=100) + if not IUnauthenticatedPrincipal.providedBy(self.request.principal): + mi = self.controller.memberInfo + title = mi.title.value or _(u'Personal Informations') + obj = mi.get('object') + url = obj is not None and self.getUrlForTarget(obj.value) or None + cm.register('portlet_right', 'personal', title=title, + subMacro=node_macros.macros['personal'], + icon='cybertools.icons/user.png', + url=url, + priority=10) + # force early portlet registrations by target by setting up target view + self.virtualTarget # force early portlet registrations by target by setting up target view self.virtualTarget @@ -117,7 +126,7 @@ class NodeView(BaseView): #target = self.virtualTargetObject # ignores page even for direktly assignd target target = self.request.annotations.get('loops.view', {}).get('target') if target is not None: - basicView = zapi.getMultiAdapter((target, self.request), name=viewName) + basicView = component.getMultiAdapter((target, self.request), name=viewName) # xxx: obsolete when self.targetObject is virtual target: return basicView.view return self.page @@ -154,7 +163,7 @@ class NodeView(BaseView): if text.startswith('<'): # seems to be HTML return text source = zapi.createObject(self.context.contentType, text) - view = zapi.getMultiAdapter((removeAllProxies(source), self.request)) + view = component.getMultiAdapter((removeAllProxies(source), self.request)) return view.render() @Lazy @@ -170,7 +179,7 @@ class NodeView(BaseView): def targetObjectView(self): obj = self.targetObject if obj is not None: - basicView = zapi.getMultiAdapter((obj, self.request)) + basicView = component.getMultiAdapter((obj, self.request)) basicView._viewName = self.context.viewName return basicView.view @@ -315,7 +324,7 @@ class NodeView(BaseView): def virtualTarget(self): obj = self.virtualTargetObject if obj is not None: - basicView = zapi.getMultiAdapter((obj, self.request)) + basicView = component.getMultiAdapter((obj, self.request)) if obj == self.targetObject: basicView._viewName = self.context.viewName return basicView.view @@ -376,8 +385,6 @@ class NodeView(BaseView): actions = dict(portlet=getPortletActions) - nextUrl = None - @Lazy def popupCreateObjectForm(self): return ("javascript:function%%20openDialog(url){" @@ -401,11 +408,19 @@ class NodeView(BaseView): def inlineEdit(self, id): self.registerDojo() cm = self.controller.macros - jsCall = 'dojo.require("dojo.widget.Editor")' + jsCall = 'dojo.require("dijit.Editor")' cm.register('js-execute', jsCall, jsCall=jsCall) return ('return inlineEdit("%s", "%s/inline_save")' % (id, self.virtualTargetUrl)) + def checkRTE(self): + target = self.virtualTarget + if target and target.inlineEditable: + self.registerDojo() + cm = self.controller.macros + jsCall = 'dojo.require("dijit.Editor")' + cm.register('js-execute', jsCall, jsCall=jsCall) + def externalEdit(self): target = self.virtualTargetObject if target is None: @@ -423,7 +438,7 @@ class NodeView(BaseView): def registerDojoDialog(self): self.registerDojo() cm = self.controller.macros - jsCall = 'dojo.require("dojo.widget.Dialog")' + jsCall = 'dojo.require("dijit.Dialog")' cm.register('js-execute', jsCall, jsCall=jsCall) @@ -527,7 +542,7 @@ class ConfigureView(NodeView): def target(self): obj = self.targetObject if obj is not None: - return zapi.getMultiAdapter((obj, self.request)) + return component.getMultiAdapter((obj, self.request)) def update(self): request = self.request @@ -604,7 +619,7 @@ class ConfigureView(NodeView): else: start = end = searchType criteria['loops_type'] = (start, end) - cat = zapi.getUtility(ICatalog) + cat = component.getUtility(ICatalog) result = cat.searchResults(**criteria) # TODO: can this be done in a faster way? result = [r for r in result if r.getLoopsRoot() == self.loopsRoot] @@ -664,7 +679,7 @@ class ViewPropertiesConfigurator(object): options = property(getOptions, setOptions) -class NodeViewConfigurator(configurator.ViewConfigurator): +class NodeViewConfigurator(configurator.AnnotationViewConfigurator): """ Take properties from next menu item... """ diff --git a/browser/node_macros.pt b/browser/node_macros.pt index 0a53a44..5179785 100644 --- a/browser/node_macros.pt +++ b/browser/node_macros.pt @@ -27,24 +27,25 @@ tal:condition="nocall:target">
+ style="float: right;"> - Edit + xtal:condition="item/inlineEditable" + tal:condition="nothing"> Edit +

-
+ Create Information Object - -
-
- -
+
+
+
-
`{RMai^;hAU@IUZ<@C;s>`ds)ycmbS&J0V@n15opS4SobZ0yo0%!4JV- zLaqCIsCoY4+h2j||0et>JZ*z9XTkH}$KVxE^BnT+4sN3U1^8k32s{No37NwD07{?Z zQ0qJmCEv5~1MqpMaj!th^%m6p8yF)ZQKcE1z88{QAK{#T*; zKMFPfx1jX?4wSx6L6$H-hv&d&p!&ZG)$etves97{;HgY1du)T6Zx7UZH~9KAlzwxt z2HQ~cJ_yzCDX4w?k*_}kncDmgYQ8^1+4&{k{x8TDm{So#a&3g_cMjYFEj$-aLD{DV zwNCDNmv6rZYMn1Z&GQi4^$uemgS)9ekMXXAn=rP%AAx)_bzi>&O7CSTfBXuR|9=xo z-XBBl`)@s;hy0np@wyfM1EPwV!8npLfU-jfW&bbw`U6mQe+;VMx1s#~yYOuIE2wq< z3O^3t^xTMWnr{o-49B42z6B-ky-@Q$3?;`mpzQGk)H*-$?Z1Fp=UFJdUV~aslW4uO zpyu5QwcY_J`5I7mzYAUs?}4)8&!Fu1G*mu$&bPk+<*zS6#qk?Z@|@13n&)h&aTZ<* zhoR)S1*%^HHUHi40K6Yczvmz#n-`(%IYZ!hb{Mz3ps<+HZnd=ZNR6zTH7w#`K`{x*HM(g)E1m zWYqdMQ1mD^wC?99n*WoO&r^mdqZB>YP%ffiVsjHk{xL&Y`|P43d!0|wnIzrxj8GIO z)uX+wQN}2rrrb!;vxlO%*E3hb%tGm+M?NI`>zSfpc5{Sskg|iKNBg#gay?C?-ms1oQhbVe3q+CncuZn+^ixhw3lk z`?D0~9_^Q6=N8ImD0-$#6fq`0f2XhSgP-!h6(925D=5k}+W+dAC#3L_OmdK{O@`gwQbgMwh>2}i!vKT&7$7V zlYX2!8@GyTT)*8lGA7L2of$1bX2aA*ab_37G^}@>ZN-U=nJKY(>XLNGj5V}QYR0-< zyFg1>iI&V*v}7mZsNwod$nQfgjq{}8%vhR+?Z{4a!fvzb!T7RuI!Gj+A4qeab>ifr z^kOp+Fjv=1bb_eu>~xT(i*eGV?i%xmikz*`F=JS&Qt!rXi*A`sJ8@2X-Pxega4aJ| z;;6e+C==Lj*b6h)v_aks9cv6RC!!&TspR*xrI0-&Osj9jBr`5Z8XYyY!lY;W1L3^6 zXP5fUOe8MID%f$!H;sJ0eYIg|2nmY5c5gRM(;!*0`!d%%Y2%vCm4UZdEoZN`)qv{5 zn!bUJ+@c-WXs}?}p7^b@r9TY5cH_98+5<6K><+R(FE>0J_NcD9oY@`sf-nM$Q8x~n zW=}KBR<%2!Dw&E7m5HZeJp)91;s>&zq`dq*DPBn=r=L4mj9 z(=HwEhS7X#>v25a3li@|YBQ5L?yMj%>((^;`Do`3?Kk_`y*SUzRNM?(VWXT^_9$DH z*4p*}ruLy>3gaEb9S1&guW1dtNGsxVZi5dBCn$Q5sBS2=0{iArbK0es!A9y#kpM!$Oc(l?j7R_ zn^eXuM-Hb&^p)E+6D}iPCcS2Zbl%LehqZc;qDk4!%#sG1d@pk#&Rb-@Fv7UrOp8HU zWDeIHDtZ^m5T8Mwq}*4`go$Ru4F4+LRRZ-&n%TG?Hq2}iL@C-AIkCtAdXdbek(Zif z&0I7e5iLdBn!{xh)gJqwsW}|FMRP>U$7@^%^$&-4jt=G zepX~p^lIdEWwU$1?Fi5dBfaCqDQuMQ(_(L2)3AL`=EF^59S*g9&6<~M)NbC_+`M7> z;B2kPSEyN22#nfoBO{mBMt0P;ZL?dqjgE}ii9NIQs~vJWN7i=RRU6q_+qQj8xBWqy z)hf%y$*8^Fb$fYcr(CjZuj&+)-gS+o9Xp1a?wSqzIS%rm?P{|w=#APkMK#@m>&AU7 zd6E%R`=<7+s<3rvWWz+6rfb@r(ZY|0`(1L_m9kfoy6oz?*~!|j)qdL7mP=}Tq6YCF zwMXr)dYEn4>mrw6fM#ucY1GayCY&o9%$_j9+48ita=ept!v=?^v*RSpPU6CP&dJPy zZTCf;fGDy#C-c!o&a|{>KFEnYPjE4bl`n6hHL*;U$bZY3qC8usF=A>qPVaho8B4VaiqarGg`K{kh&Hxm=_hG*4on zI$65T*iwdKD_5H&K2cblf!qK{R)fwbnCJibvfg%UMy#4`wNagwwL&wgs4A4lfQ2(g zwm9&Yb2}L<#0uwVct1{x4dl?a6GqE9t|-GECe(_vZ@s`qw}r8K52vQn%6)QtVm{>v z%6o6~A|Dt-=74s{hpH|O;WE+7mBm&uRGF)WxoWx{Rab`G^$4LTWh3m$u~thVdNl9h z>6J+b&1)lf;PhrUp;)a_ySa)0<*76ad%es@AeUz1vuAPVHW5)L_eFzocI7x(flouG zUtBY)TSj7UEUy_sf>l}sw9_%iG2dJpjf0IcGyTHWBnw3-x)exQFbdPOjN8KOE040` zAf9dNdxwtmKA>WX4w36{$-!ldRG_rHxbk>M@jV`9NT7RQk&%1`QeHw0SAxt6dLixw)C5mw}XA7w@QD}pOy zH@n^Ps7N>}26q)M-dqVo{4Xm`mg01R|iruiqS(5h4%-mEWESx&3 z&LeLj#&ujT@Z(&z%y;zVq%1eWMnR)v%TtQ|Ux<6U#c-pkyZIneHYn=elTnxP+HlRviUE&wizn` delta 1769 zcmZwHTWkzb9LMp~+R}EH*6wQEyH%}QSG6uJbyu_reNe3s5vyHnjIA(D@Uo&Lf|r_9 zN|AV|hekr`5(x>>gh+)4iAy3hA|%8EB7H#O``gY#ADqnpe9oMiIk!18r%JBmCEocx z?S@oMOeD6Z8WX__9x_sBxG^IzAAL9jhv5SB;v%3#%|OAu3;hGLpA&XIm}Nob@&_Spofz*oR8|b5_P?XOzmwz z4RjNZ!foiKf3ugu1U!TsrkzYTUP9g2g){LcYK7jT8vKFkaKJtPi<(F((^GxWbv$Y! zA=GnW)T=7Pgf3K5$i#Z24YM6}V+@Nij(OOLn)zK+2feOO-1_IJ_Fkdd`G^%M#(Z|# zNgtychXK@mVO0G}AM39X*N~~922{h_Py?}0542(~cA#c{9W{g7sITEZYIpY{hj~G! z`}$Ec{)Dse7pmQSI-zJP5){=qDKA{)$j|{^B+(v_7(Li zQ@E&=@S)lni;HmX>o&Y@;-)wKsX%pEe#_%UkJ_Mv9}8R^>$ zpaz^qr8>yM1(=JIu-2`&koFVC9xRx{?u8?$U3>yHgY(E9N+!)fooK*|2;E12W*VW! zbw9T!JJf1uB~}wlh%!P;ucW=E!OhY4KQt9`GLO)^*6fv55&HbJZ25#zHBpo-{Xbhe zq z1ah6mK+xF{$eY;EY+1=J)Ozo+cvEzLOMKUXrik6L*YO4WZhQ}BrKD|)*-^_moAc`* DL|UxP diff --git a/locales/de/LC_MESSAGES/loops.po b/locales/de/LC_MESSAGES/loops.po index 4a842f0..ba77769 100644 --- a/locales/de/LC_MESSAGES/loops.po +++ b/locales/de/LC_MESSAGES/loops.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: $Id$\n" "POT-Creation-Date: 2007-05-22 12:00 CET\n" -"PO-Revision-Date: 2007-12-17 12:00 CET\n" +"PO-Revision-Date: 2008-01-23 12:00 CET\n" "Last-Translator: Helmut Merz \n" "Language-Team: loops developers \n" "MIME-Version: 1.0\n" @@ -17,12 +17,54 @@ msgstr "Begriff" msgid "Resource" msgstr "Ressource" +msgid "Log out" +msgstr "Abmelden" + +msgid "Create" +msgstr "Anlegen" + msgid "Edit Concept Map" msgstr "Concept Map bearbeiten" msgid "Create Resource..." msgstr "Ressource anlegen..." +msgid "Create a new resource object." +msgstr "Eine neue Ressource erzeugen" + +msgid "Edit Blog Post..." +msgstr "Eintrag bearbeiten..." + +msgid "Edit Blog Post" +msgstr "Tagebucheintrag bearbeiten" + +msgid "Modify blog post." +msgstr "Tagebucheintrag ändern." + +msgid "Create Blog Post..." +msgstr "Tagebucheintrag anlegen..." + +msgid "Create a new blog post." +msgstr "Einen neuen Tagebucheintrag erzeugen" + +msgid "Create Blog Post" +msgstr "Tagebucheintrag anlegen" + +msgid "Glossary Item" +msgstr "Glossareintrag" + +msgid "Edit Glossary Item..." +msgstr "Glossareintrag bearbeiten..." + +msgid "Edit Glossary Item" +msgstr "Glossareintrag bearbeiten" + +msgid "Create Glossary Item..." +msgstr "Glossareintrag anlegen..." + +msgid "Create Glossary Item" +msgstr "Glossareintrag anlegen" + msgid "Actions" msgstr "Aktionen" @@ -47,9 +89,15 @@ msgstr "Unterbegriffe" msgid "Title" msgstr "Titel" +msgid "Title of the concept" +msgstr "Überschrift, sprechende Bezeichnung des Begriffs" + msgid "Description" msgstr "Beschreibung" +msgid "A medium-length description describing the content and the purpose of the object" +msgstr "Eine ausführlichere Beschreibung des Inhalts und Zwecks des Objekts" + msgid "Related Items" msgstr "Verwandte Begriffe" @@ -224,4 +272,30 @@ msgstr "Die Passwort-Wiederholung stimmt nicht mit dem eingegebenen Passwort üb msgid "Your password has been changed." msgstr "Ihr Passwort wurde geändert." +msgid "Date/Time" +msgstr "Datum/Uhrzeit" + +msgid "The date and time the information was posted." +msgstr "Datum und Uhrzeit der Bereitstellung der Information" + +msgid "Private" +msgstr "privat" + +msgid "Check this field if the blog post should be accessible only for a limited audience." +msgstr "Markieren Sie dieses Feld, wenn der Tagebucheintrag nicht für alle Benutzer sichtbar sein soll" + +msgid "The text of your blog entry" +msgstr "Der eigentliche Text des Tagebucheintrags" + +msgid "Private Comment" +msgstr "Privater Kommentar" + +msgid "A text that is not visible for other users." +msgstr "Ein Text, der für andere Benutzer nicht sichtbar sein soll" + +msgid "For quick creation of notes/links bookmark this link" +msgstr "Für Notizen diesen Link zu Favoriten/Lesezeichen hinzufügen" + +msgid "Create loops Note" +msgstr "loops-Notiz anlegen" diff --git a/organize/README.txt b/organize/README.txt index b508254..fa8cd18 100644 --- a/organize/README.txt +++ b/organize/README.txt @@ -26,6 +26,9 @@ ZCML setup): >>> loopsRoot = site['loops'] >>> loopsId = util.getUidForObject(loopsRoot) + >>> from loops.organize.tests import setupUtilitiesAndAdapters + >>> setupData = setupUtilitiesAndAdapters(loopsRoot) + >>> type = concepts['type'] >>> person = concepts['person'] @@ -39,10 +42,7 @@ Organizations: Persons (and Users), Institutions, Addresses... The classes used in this package are just adapters to IConcept. - >>> from loops.interfaces import IConcept >>> from loops.organize.interfaces import IPerson - >>> from loops.organize.party import Person - >>> component.provideAdapter(Person, (IConcept,), IPerson) >>> john = IPerson(johnC) >>> john.title @@ -74,15 +74,8 @@ the person(s) belonging to a user/principal. 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) + >>> auth = setupData.auth + >>> principalAnnotations = setupData.principalAnnotations >>> principal = auth.definePrincipal('users.john', u'John', login='john') >>> john.userId = 'users.john' @@ -116,6 +109,7 @@ principal annotation: >>> from zope.app.container.contained import ObjectRemovedEvent >>> from zope.event import notify >>> from zope.interface import Interface + >>> from loops.interfaces import IConcept >>> from loops.organize.party import removePersonReferenceFromPrincipal >>> from zope.app.testing import ztapi >>> ztapi.subscribe([IConcept, IObjectRemovedEvent], None, @@ -157,6 +151,7 @@ with a principal folder: >>> from zope.app.appsetup.bootstrap import ensureUtility >>> from zope.app.authentication.authentication import PluggableAuthentication + >>> from zope.app.security.interfaces import IAuthentication >>> ensureUtility(site, IAuthentication, '', PluggableAuthentication, ... copy_to_zlog=False, asObject=True) <...PluggableAuthentication...> @@ -212,7 +207,6 @@ Now we can also retrieve it from the authentication utility: >>> pau.getPrincipal('loops.newuser').title u'Tom Sawyer' - Change Password --------------- @@ -229,18 +223,100 @@ We need a principal for testing the login stuff: >>> principal = InternalPrincipal('scott', 'tiger', 'Scotty') >>> request.setPrincipal(principal) - >>> from cybertools.composer.schema.factory import SchemaFactory - >>> from cybertools.composer.schema.field import FieldInstance - >>> component.provideAdapter(SchemaFactory) - >>> component.provideAdapter(FieldInstance) - >>> from loops.organize.browser import PasswordChange >>> pwcView = PasswordChange(menu, request) >>> pwcView.update() False + +Security +======== + +Automatic security settings on persons +-------------------------------------- + + >>> from zope.traversing.api import getName + >>> list(sorted(getName(c) for c in concepts['person'].getChildren())) + [u'john', u'martha', u'person.newuser'] + +Person objects that have a user assigned to them receive this user +(principal) as their owner. + + >>> from zope.app.securitypolicy.interfaces import IPrincipalRoleMap + >>> IPrincipalRoleMap(concepts['john']).getPrincipalsAndRoles() + [('loops.Owner', 'users.john', PermissionSetting: Allow)] + >>> IPrincipalRoleMap(concepts['person.newuser']).getPrincipalsAndRoles() + [('loops.Owner', u'loops.newuser', PermissionSetting: Allow)] + +The person ``martha`` hasn't got a user id, so there is no role assigned +to it. + + >>> IPrincipalRoleMap(concepts['martha']).getPrincipalsAndRoles() + [] + +Only the owner (and a few other privileged people) should be able +to edit a person object. + +We also need an interaction with a participation based on the principal +whose permissions we want to check. + + >>> from zope.app.authentication.principalfolder import Principal + >>> pJohn = Principal('users.john', 'xxx', u'John') + + >>> from loops.tests.auth import login + >>> login(pJohn) + +We also want to grant some global permissions and roles, i.e. on the site or +loops root level. + + >>> rolePermissions = setupData.rolePermissions + >>> rolePermissions.grantPermissionToRole('zope.View', 'zope.Member') + + >>> principalRoles = setupData.principalRoles + >>> principalRoles.assignRoleToPrincipal('zope.Member', 'users.john') + +Now we are ready to look for the real stuff - what John is allowed to do. + + >>> from zope.security import canAccess, canWrite, checkPermission + >>> john = concepts['john'] + + >>> canAccess(john, 'title') + True + +Person objects that have an owner may be modified by this owner. + + >>> canWrite(john, 'title') + True + +So let's try with another user with another role setting. + + >>> rolePermissions.grantPermissionToRole('zope.ManageContent', 'loops.Staff') + >>> principalRoles.assignRoleToPrincipal('loops.Staff', 'users.martha') + >>> principalRoles.assignRoleToPrincipal('zope.Member', 'users.martha') + + >>> pMartha = Principal('users.martha', 'xxx', u'Martha') + >>> login(pMartha) + + >>> canAccess(john, 'title') + True + >>> canWrite(john, 'title') + False + +If we clear the userId attribute from a person object others may be allowed +again to edit it... + + >>> adapted(john).userId = '' + >>> canWrite(john, 'title') + True + +... but John no more... + + >>> login(pJohn) + >>> canWrite(john, 'title') + False + + Fin de partie ============= >>> placefulTearDown() - diff --git a/organize/configure.zcml b/organize/configure.zcml index b4054e9..3bf9431 100644 --- a/organize/configure.zcml +++ b/organize/configure.zcml @@ -93,6 +93,13 @@ + + diff --git a/organize/interfaces.py b/organize/interfaces.py index 84bf6e1..5c1347f 100644 --- a/organize/interfaces.py +++ b/organize/interfaces.py @@ -30,7 +30,8 @@ from zope.app.security.interfaces import IAuthentication, PrincipalLookupError from zope.security.proxy import removeSecurityProxy from cybertools.organize.interfaces import IPerson as IBasePerson -from loops.organize.util import getPrincipalFolder, authPluginId +from loops.interfaces import IConceptSchema +from loops.organize.util import getPrincipalFolder from loops.util import _ ANNOTATION_KEY = 'loops.organize.person' @@ -83,7 +84,7 @@ class LoginName(schema.TextLine): mapping=dict(userId=userId))) -class IPerson(IBasePerson): +class IPerson(IConceptSchema, IBasePerson): """ Resembles a human being with a name (first and last name), a birth date, and a set of addresses. This interface only lists fields used in addition to those provided by the diff --git a/organize/member.py b/organize/member.py index 60835cf..b9cbd9a 100644 --- a/organize/member.py +++ b/organize/member.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 @@ -36,10 +36,13 @@ from zope.i18nmessageid import MessageFactory from zope.cachedescriptors.property import Lazy from cybertools.typology.interfaces import IType -from loops.interfaces import ILoops +from loops.common import adapted from loops.concept import Concept +from loops.interfaces import ILoops from loops.organize.interfaces import IMemberRegistrationManager -from loops.organize.util import getPrincipalFolder, authPluginId, getInternalPrincipal +from loops.organize.util import getPrincipalFolder, getGroupsFolder +from loops.organize.util import getInternalPrincipal +from loops.type import getOptionsDict from loops.util import _ @@ -51,29 +54,54 @@ class MemberRegistrationManager(object): def __init__(self, context): self.context = context - def register(self, userId, password, lastName, firstName=u'', **kw): + def register(self, userId, password, lastName, firstName=u'', + groups=[], useExisting=False, **kw): # step 1: create an internal principal in the loops principal folder: pFolder = getPrincipalFolder(self.context) title = firstName and ' '.join((firstName, lastName)) or lastName principal = InternalPrincipal(userId, password, title) - pFolder[userId] = principal - # step 2: create a corresponding person concept: + if useExisting: + if userId not in pFolder: + pFolder[userId] = principal + else: + pFolder[userId] = principal + # step 2 (optional): assign to group(s) + personType = self.context.getLoopsRoot().getConceptManager()['person'] + od = getOptionsDict(adapted(personType).options) + groupInfo = od.get('group') + if groupInfo: + gfName, groupNames = groupInfo.split(':') + gFolder = getGroupsFolder(gfName) + if not groups: + groups = groupNames.split(',') + else: + gFolder = getGroupsFolder() + if gFolder is not None: + for g in groups: + group = gFolder.get(g) + if group is not None: + members = list(group.principals) + members.append(pFolder.prefix + userId) + group.principals = members + # step 3: create a corresponding person concept: cm = self.context.getConceptManager() id = baseId = 'person.' + userId # TODO: use NameChooser - num = 0 - while id in cm: - num +=1 - id = baseId + str(num) - person = cm[id] = Concept(title) - # TODO: the name of the person type object must be kept flexible! - # So we have to search for a type concept that has IPerson as - # its typeInterface... + if useExisting and id in cm: + person = cm[id] + else: + num = 0 + while id in cm: + num +=1 + id = baseId + str(num) + person = cm[id] = Concept(title) person.conceptType = cm['person'] - personAdapter = IType(person).typeInterface(person) + personAdapter = adapted(person) personAdapter.firstName = firstName personAdapter.lastName = lastName - personAdapter.userId = '.'.join((authPluginId, userId)) + personAdapter.userId = pFolder.prefix + userId + for k, v in kw.items(): + setattr(personAdapter, k, v) notify(ObjectCreatedEvent(person)) notify(ObjectModifiedEvent(person)) return personAdapter diff --git a/organize/memberinfo.py b/organize/memberinfo.py new file mode 100644 index 0000000..273f981 --- /dev/null +++ b/organize/memberinfo.py @@ -0,0 +1,57 @@ +# +# 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 +# + +""" +Provide member properties based on person data. + +$Id$ +""" + +from zope import component +from zope.app.security.interfaces import IAuthentication +from zope.cachedescriptors.property import Lazy + +from cybertools.browser.member import MemberInfoProvider as BaseMemberInfoProvider +from cybertools.browser.member import MemberProperty +from cybertools.util.jeep import Jeep +from loops.common import adapted +from loops.organize.party import getPersonForUser +from loops import util + + +class MemberInfoProvider(BaseMemberInfoProvider): + + def getData(self, principalId=None): + if principalId is None: + principal = self.request.principal + else: + #auth = component.getUtility(IAuthentication, self.context) + auth = component.getUtility(IAuthentication) + principal = auth.getPrincipal(principalId) + person = getPersonForUser(self.context, self.request, principal) + if person is None: + return super(MemberInfoProvider, self).getData(principalId) + aPerson = adapted(person) + return Jeep((MemberProperty('id', principal.id, u'ID'), + MemberProperty('title', aPerson.title, u'Title'), + MemberProperty('description', aPerson.description, + u'Description'), + MemberProperty('object', person, u'Object'), + MemberProperty('adapted', aPerson, u'Adapted object'), + )) + diff --git a/organize/party.py b/organize/party.py index def0701..e394aad 100644 --- a/organize/party.py +++ b/organize/party.py @@ -38,10 +38,11 @@ from cybertools.organize.interfaces import IAddress from cybertools.organize.party import Person as BasePerson from cybertools.relation.interfaces import IRelationRegistry from cybertools.typology.interfaces import IType +from loops.common import AdapterBase from loops.concept import Concept from loops.interfaces import IConcept from loops.organize.interfaces import IPerson, ANNOTATION_KEY -from loops.common import AdapterBase +from loops.security.common import assignOwner, removeOwner, allowEditingForOwner from loops.type import TypeInterfaceSourceList from loops import util @@ -54,6 +55,8 @@ TypeInterfaceSourceList.typeInterfaces += (IPerson, IAddress) def getPersonForUser(context, request=None, principal=None): if principal is None: principal = request.principal + if principal is None: + return None loops = context.getLoopsRoot() pa = annotations(principal).get(ANNOTATION_KEY, None) if pa is None: @@ -88,13 +91,16 @@ class Person(AdapterBase, BasePerson): pa = annotations(principal) loopsId = util.getUidForObject(self.context.getLoopsRoot()) ann = pa.get(ANNOTATION_KEY) - if ann is None: + if ann is None: # or not isinstance(ann, PersistentMapping): ann = pa[ANNOTATION_KEY] = PersistentMapping() ann[loopsId] = self.context + assignOwner(self.context, userId) oldUserId = self.userId if oldUserId and oldUserId != userId: self.removeReferenceFromPrincipal(oldUserId) + removeOwner(self.context, oldUserId) self.context._userId = userId + allowEditingForOwner(self.context, revert=not userId) userId = property(getUserId, setUserId) def removeReferenceFromPrincipal(self, userId): diff --git a/organize/tests.py b/organize/tests.py index a4759a2..e6210ca 100755 --- a/organize/tests.py +++ b/organize/tests.py @@ -4,8 +4,35 @@ import unittest, doctest from zope.testing.doctestunit import DocFileSuite from zope.app.testing import ztapi from zope.interface.verify import verifyClass + +from zope import component +from zope.app.principalannotation.interfaces import IPrincipalAnnotationUtility +from zope.app.principalannotation import PrincipalAnnotationUtility +from zope.app.principalannotation import annotations +from zope.app.security.interfaces import IAuthentication +from zope.app.security.principalregistry import PrincipalRegistry +from zope.app.securitypolicy.interfaces import IRolePermissionManager +from zope.app.securitypolicy.interfaces import IPrincipalRoleManager + +from cybertools.util.jeep import Jeep +from loops.organize.interfaces import IPerson from loops.organize.party import Person + +def setupUtilitiesAndAdapters(loopsRoot): + auth = PrincipalRegistry() + component.provideUtility(auth, IAuthentication) + principalAnnotations = PrincipalAnnotationUtility() + component.provideUtility(principalAnnotations, IPrincipalAnnotationUtility) + component.provideAdapter(Person, provides=IPerson) + return Jeep(( + ('auth', auth), + ('principalAnnotations', principalAnnotations), + ('rolePermissions', IRolePermissionManager(loopsRoot)), + ('principalRoles', IPrincipalRoleManager(loopsRoot)), + )) + + class Test(unittest.TestCase): "Basic tests for the organize sub-package." diff --git a/organize/util.py b/organize/util.py index 6bf8fc4..6189896 100644 --- a/organize/util.py +++ b/organize/util.py @@ -27,22 +27,39 @@ from zope.app.authentication.interfaces import IPluggableAuthentication from zope.app.authentication.interfaces import IAuthenticatorPlugin from zope.app.security.interfaces import IAuthentication -authPluginId = 'loops' +from loops.common import adapted +from loops.type import getOptionsDict + +defaultAuthPluginId = 'loops' -def getPrincipalFolder(context=None): +def getPrincipalFolder(context=None, authPluginId=None, ignoreErrors=False): pau = component.getUtility(IAuthentication, context=context) if not IPluggableAuthentication.providedBy(pau): + if ignoreErrors: + return None raise ValueError(u'There is no pluggable authentication ' 'utility available.') - if not authPluginId in pau.authenticatorPlugins: - raise ValueError(u'There is no loops authenticator ' - 'plugin available.') + if authPluginId is None and context is not None: + person = context.getLoopsRoot().getConceptManager()['person'] + od = getOptionsDict(adapted(person).options) + authPluginId = od.get('principalfolder', defaultAuthPluginId) + if authPluginId is None: + authPluginId = defaultAuthPluginId + if authPluginId not in pau.authenticatorPlugins: + if ignoreErrors: + return None + raise ValueError(u"There is no loops authenticator " + "plugin '%s' available." % authPluginId) for name, plugin in pau.getAuthenticatorPlugins(): if name == authPluginId: return plugin +def getGroupsFolder(context=None, name='gloops'): + return getPrincipalFolder(authPluginId=name, ignoreErrors=True) + + def getInternalPrincipal(id, context=None): pau = component.getUtility(IAuthentication, context=context) if not IPluggableAuthentication.providedBy(pau): diff --git a/query.py b/query.py index 701d32b..eddcaa2 100644 --- a/query.py +++ b/query.py @@ -24,13 +24,13 @@ $Id$ from zope import schema, component from zope.interface import Interface, Attribute, implements -from zope import traversing from zope.app.catalog.interfaces import ICatalog from zope.cachedescriptors.property import Lazy from cybertools.typology.interfaces import IType -from loops.interfaces import IConcept, IConceptSchema from loops.common import AdapterBase +from loops.interfaces import IConcept, IConceptSchema +from loops.security.common import canListObject from loops.type import TypeInterfaceSourceList from loops.versioning.util import getVersion from loops import util @@ -72,7 +72,8 @@ class BaseQuery(object): result = cat.searchResults(loops_type=(start, end), loops_title=title) else: result = cat.searchResults(loops_type=(start, end)) - result = set(r for r in result if r.getLoopsRoot() == self.loopsRoot) + result = set(r for r in result if r.getLoopsRoot() == self.loopsRoot + and canListObject(r)) if 'exclude' in kw: r1 = set() for r in result: @@ -139,6 +140,7 @@ class FullQuery(BaseQuery): result = rc result = set(r for r in result if r.getLoopsRoot() == self.loopsRoot + and canListObject(r) and getVersion(r) == r) return result diff --git a/resource.py b/resource.py index 079be8b..5600785 100644 --- a/resource.py +++ b/resource.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2005 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 @@ -22,6 +22,8 @@ Definition of the Concept class. $Id$ """ +from cStringIO import StringIO +from persistent import Persistent from zope import component, schema from zope.app import zapi from zope.app.container.btree import BTreeContainer @@ -38,9 +40,6 @@ from zope.interface import implements from zope.size.interfaces import ISized from zope.security.proxy import removeSecurityProxy from zope.traversing.api import getName, getParent -from persistent import Persistent -from cStringIO import StringIO - from zope.lifecycleevent import ObjectModifiedEvent, Attributes from zope.event import notify @@ -50,7 +49,6 @@ from cybertools.storage.interfaces import IExternalStorage from cybertools.text.interfaces import ITextTransform from cybertools.typology.interfaces import IType, ITypeManager from cybertools.util.jeep import Jeep - from loops.base import ParentInfo from loops.common import ResourceAdapterBase, adapted from loops.concept import ResourceRelation @@ -62,6 +60,7 @@ from loops.interfaces import IResourceManager, IResourceManagerContained from loops.interfaces import ITypeConcept from loops.interfaces import ILoopsContained from loops.interfaces import IIndexAttributes +from loops.security.common import canListObject from loops import util from loops.versioning.util import getMaster from loops.view import TargetRelation @@ -189,20 +188,24 @@ class Resource(Image, Contained): relationships = [TargetRelation] obj = getMaster(self) # use the master version for relations rels = getRelations(second=obj, relationships=relationships) - return [r.first for r in rels] + return [r.first for r in rels if canListObject(r.first)] - def getConceptRelations (self, predicates=None, concept=None, sort='default'): + def getConceptRelations (self, predicates=None, concept=None, sort='default', + noSecurityCheck=False): predicates = predicates is None and ['*'] or predicates obj = getMaster(self) relationships = [ResourceRelation(None, obj, p) for p in predicates] if sort == 'default': sort = lambda x: (x.order, x.first.title.lower()) - return sorted(getRelations(first=concept, second=obj, relationships=relationships), - key=sort) + rels = (r for r in getRelations(first=concept, second=obj, + relationships=relationships) + if canListObject(r.first, noSecurityCheck)) + return sorted(rels, key=sort) - def getConcepts(self, predicates=None): + def getConcepts(self, predicates=None, noSecurityCheck=False): obj = getMaster(self) - return [r.first for r in obj.getConceptRelations(predicates)] + return [r.first for r in obj.getConceptRelations(predicates, + noSecurityCheck=noSecurityCheck)] def assignConcept(self, concept, predicate=None, order=0, relevance=1.0): obj = getMaster(self) @@ -372,6 +375,8 @@ class ExternalFileAdapter(FileAdapter): self.storageName = storageName def getData(self): + if self.storageName == 'unknown': # object not set up yet + return '' storage = component.getUtility(IExternalStorage, name=self.storageName) return storage.getData(self.externalAddress, params=self.storageParams) diff --git a/schema.py b/schema.py index 4e2842d..4138236 100644 --- a/schema.py +++ b/schema.py @@ -35,6 +35,8 @@ class ResourceSchemaFactory(SchemaFactory): def __call__(self, interface, **kw): schema = super(ResourceSchemaFactory, self).__call__(interface, **kw) schema.fields.data.height = 10 + if self.context.contentType == 'text/html': + schema.fields.data.fieldType = 'html' return schema diff --git a/search/README.txt b/search/README.txt index 9768427..c50d64e 100755 --- a/search/README.txt +++ b/search/README.txt @@ -90,8 +90,8 @@ a controller attribute for the search view. >>> searchView.controller = Controller(searchView, request) >>> searchView.submitReplacing('1.results', '1.search.form', pageView) - 'return submitReplacing("1.results", "1.search.form", - "http://127.0.0.1/loops/views/page/.target29/@@searchresults.html")' + 'submitReplacing("1.results", "1.search.form", + "http://127.0.0.1/loops/views/page/.target29/@@searchresults.html");...' Basic (text/title) search ------------------------- @@ -164,7 +164,9 @@ Now we can fill our search form and execute the query; note that all concepts found are listed, plus all their children and all resources associated with them: - >>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': u'zope'} + >>> from loops import util + >>> uid = util.getUidForObject(concepts['zope']) + >>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': uid} >>> request = TestRequest(form=form) >>> resultsView = SearchResults(page, request) >>> results = list(resultsView.results) @@ -173,7 +175,8 @@ with them: >>> results[0].context.__name__ u'plone' - >>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': u'zope3'} + >>> uid = util.getUidForObject(concepts['zope3']) + >>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': uid} >>> request = TestRequest(form=form) >>> resultsView = SearchResults(page, request) >>> results = list(resultsView.results) @@ -186,11 +189,11 @@ To support easy entry of concepts to search for we can preselect the available concepts (optionally restricted to a certain type) by entering text parts of the concepts' titles: - >>> form = {'searchType': 'loops:concept:topic', 'searchString': u'zope'} + >>> form = {'searchType': 'loops:concept:topic', 'name': u'zope'} >>> request = TestRequest(form=form) >>> view = Search(page, request) >>> view.listConcepts() - "[['Zope (Topic)', '33']]" + u"{identifier: 'id', items: [{label: 'Zope (Topic)', name: 'Zope', id: '33'}]}" Preset Concept Types on Search Forms ------------------------------------ diff --git a/search/browser.py b/search/browser.py index 99c04b8..b5ba843 100644 --- a/search/browser.py +++ b/search/browser.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2004 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 @@ -83,30 +83,56 @@ class Search(BaseView): def initDojo(self): self.registerDojo() cm = self.controller.macros - jsCall = 'dojo.require("dojo.widget.ComboBox")' + jsCall = ('dojo.require("dojo.parser");' + 'dojo.require("dijit.form.FilteringSelect");' + 'dojo.require("dojox.data.QueryReadStore");') cm.register('js-execute', jsCall, jsCall=jsCall) def listConcepts(self): - """ Used for dojo.widget.ComboBox. + """ Used for dijit.FilteringSelect. """ request = self.request request.response.setHeader('Content-Type', 'text/plain; charset=UTF-8') - title = request.get('searchString', '').replace('(', ' ').replace(')', ' ') - type = request.get('searchType') or 'loops:concept:*' - result = ConceptQuery(self).query(title=title, type=type, exclude=('system',)) - #registry = component.getUtility(IRelationRegistry) - # simple way to provide JSON format: - return str(sorted([[`adapted(o, self.languageInfo).title`[2:-1] - + ' (%s)' % `o.conceptType.title`[2:-1], - `int(util.getUidForObject(o))`] - for o in result - if o.getLoopsRoot() == self.loopsRoot])).replace('\\\\x', '\\x') - #return str(sorted([[`o.title`[2:-1], `traversing.api.getName(o)`[2:-1]] - # for o in result])).replace('\\\\x', '\\x') + title = request.get('name') + if title == '*': + title = None + type = request.get('searchType') + data = [] + if title or type: + if title is not None: + title = title.replace('(', ' ').replace(')', ' ').replace(' -', ' ') + #title = title.split(' ', 1)[0] + if not type: + type = 'loops:concept:*' + result = ConceptQuery(self).query(title=title or None, type=type, + exclude=('system',)) + for o in result: + if o.getLoopsRoot() == self.loopsRoot: + name = adapted(o, self.languageInfo).title + if title and title.endswith('*'): + title = title[:-1] + sort = ((title and name.startswith(title) and '0' or '1') + + name.lower()) + if o.conceptType is None: + raise ValueError('Concept Type missing for %r.' % name) + data.append({'label': '%s (%s)' % (name, o.conceptType.title), + 'name': name, + 'id': util.getUidForObject(o), + 'sort': sort}) + data.sort(key=lambda x: x['sort']) + if not title: + data.insert(0, {'label': '', 'name': '', 'id': ''}) + json = [] + for item in data: + json.append("{label: '%s', name: '%s', id: '%s'}" % + (item['label'], item['name'], item['id'])) + json = "{identifier: 'id', items: [%s]}" % ', '.join(json) + #print '***', json + return json def submitReplacing(self, targetId, formId, view): self.registerDojo() - return 'return submitReplacing("%s", "%s", "%s")' % ( + return 'submitReplacing("%s", "%s", "%s"); return false;' % ( targetId, formId, '%s/.target%s/@@searchresults.html' % (view.url, self.uniqueId)) @@ -131,13 +157,14 @@ class SearchResults(BaseView): useTitle = form.get('search.2.title') useFull = form.get('search.2.full') conceptType = form.get('search.3.type', 'loops:concept:*') - conceptTitle = form.get('search.3.text') - if conceptTitle is not None: - conceptTitle = util.toUnicode(conceptTitle, encoding='ISO8859-15') - conceptUid = form.get('search.3.text_selected') + #conceptTitle = form.get('search.3.text') + #if conceptTitle is not None: + # conceptTitle = util.toUnicode(conceptTitle, encoding='ISO8859-15') + conceptUid = form.get('search.3.text') result = FullQuery(self).query(text=text, type=type, useTitle=useTitle, useFull=useFull, - conceptTitle=conceptTitle, conceptUid=conceptUid, + #conceptTitle=conceptTitle, + conceptUid=conceptUid, conceptType=conceptType) rowNum = 4 while rowNum < 10: diff --git a/search/search.pt b/search/search.pt index d9f014d..3d71260 100644 --- a/search/search.pt +++ b/search/search.pt @@ -4,11 +4,11 @@ idPrefix string:${view/itemNum}.search; formId string:$idPrefix.form; resultsId string:$idPrefix.results"> -

Search -

+
@@ -46,7 +46,7 @@ i18n:domain="loops">
- Search results +

Search results

@@ -113,7 +113,7 @@ @@ -144,7 +144,7 @@ @@ -185,7 +185,7 @@ - + + + id string:$idPrefix.text" /> diff --git a/security.zcml b/security.zcml new file mode 100644 index 0000000..000a95e --- /dev/null +++ b/security.zcml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/security/__init__.py b/security/__init__.py new file mode 100644 index 0000000..38314f3 --- /dev/null +++ b/security/__init__.py @@ -0,0 +1,3 @@ +""" +$Id$ +""" diff --git a/security/common.py b/security/common.py new file mode 100644 index 0000000..5ff3ef3 --- /dev/null +++ b/security/common.py @@ -0,0 +1,133 @@ +# +# 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 +# + +""" +Common functions and other stuff for working with permissions and roles. + +$Id$ +""" + +from zope import component +from zope.app.securitypolicy.interfaces import IPrincipalRoleManager +from zope.app.securitypolicy.interfaces import IRolePermissionManager +from zope.cachedescriptors.property import Lazy +from zope.interface import implements +from zope.lifecycleevent import IObjectCreatedEvent +from zope.security import canAccess, canWrite +from zope.security import checkPermission as baseCheckPermission +from zope.security.management import getInteraction + +from loops.common import adapted +from loops.interfaces import ILoopsObject, IConcept +from loops.interfaces import IAssignmentEvent, IDeassignmentEvent +from loops.security.interfaces import ISecuritySetter + + +allRolesExceptOwner = ( + #'zope.SiteManager' - no, not this one... + 'zope.Anonymous', 'zope.Member', 'zope.ContentManager', 'loops.Staff', + 'loops.xmlrpc.ConceptManager', # relevant for local security? + #'loops.SiteManager', + 'loops.Master',) +allRolesExceptOwnerAndMaster = tuple(allRolesExceptOwner[:-1]) +minorPrivilegedRoles = ('zope.Anonymous', 'zope.Member',) + + +# checking and querying functions + +def canAccessObject(obj): + return canAccess(obj, 'title') + +def canListObject(obj, noCheck=False): + if noCheck: + return True + return canAccess(obj, 'title') + +def canWriteObject(obj): + return canWrite(obj, 'title') + +def checkPermission(permission, obj): + return baseCheckPermission(permission, obj) + + +def getCurrentPrincipal(): + interaction = getInteraction() + if interaction is not None: + parts = interaction.participations + if parts: + return parts[0].principal + return None + + +# functions for setting security properties + +def assignOwner(obj, principalId): + prm = IPrincipalRoleManager(obj) + prm.assignRoleToPrincipal('loops.Owner', principalId) + +def removeOwner(obj, principalId): + prm = IPrincipalRoleManager(obj) + prm.removeRoleFromPrincipal('loops.Owner', principalId) + + +def allowEditingForOwner(obj, deny=allRolesExceptOwner, revert=False): + rpm = IRolePermissionManager(obj) + if revert: + for role in deny: + rpm.unsetPermissionFromRole('zope.ManageContent', role) + rpm.unsetPermissionFromRole('zope.ManageContent', 'loops.Owner') + else: + for role in deny: + rpm.denyPermissionToRole('zope.ManageContent', role) + rpm.grantPermissionToRole('zope.ManageContent', 'loops.Owner') + + +def restrictView(obj, roles=allRolesExceptOwnerAndMaster, revert=False): + rpm = IRolePermissionManager(obj) + if revert: + for role in roles: + rpm.unsetPermissionFromRole('zope.View', role) + else: + for role in roles: + rpm.denyPermissionToRole('zope.View', role) + + +# event handlers + +@component.adapter(ILoopsObject, IObjectCreatedEvent) +def setDefaultSecurity(obj, event): + aObj = adapted(obj) + setter = ISecuritySetter(aObj) + setter.setDefaultRolePermissions() + setter.setDefaultPrincipalRoles() + + +@component.adapter(IConcept, IAssignmentEvent) +def grantAcquiredSecurity(obj, event): + aObj = adapted(obj) + setter = ISecuritySetter(aObj) + setter.setAcquiredRolePermissions(event.relation) + setter.setAcquiredPrincipalRoles(event.relation) + + +@component.adapter(IConcept, IDeassignmentEvent) +def revokeAcquiredSecurity(obj, event): + aObj = adapted(obj) + setter = ISecuritySetter(aObj) + setter.setAcquiredRolePermissions(event.relation, revert=True) + setter.setAcquiredPrincipalRoles(event.relation, revert=True) diff --git a/security/configure.zcml b/security/configure.zcml new file mode 100644 index 0000000..0cc142d --- /dev/null +++ b/security/configure.zcml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + diff --git a/security/interfaces.py b/security/interfaces.py new file mode 100644 index 0000000..c9f6d0f --- /dev/null +++ b/security/interfaces.py @@ -0,0 +1,54 @@ +# +# 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 +# + +""" +Interfaces for loops security management. + +$Id$ +""" + +from zope.interface import Interface, Attribute +from zope import interface, component, schema + +from loops.util import _ + + +class ISecuritySetter(Interface): + + def setDefaultRolePermissions(): + """ Set some default role permission assignments (grants) on the + context object. + """ + + def setDefaultPrincipalRoles(): + """ Assign default roles (e.g. owner) to certain principals + (e.g. the user that created the object). + """ + + def setAcquiredRolePermissions(relation, revert=False): + """ Grant role permissions on children/resources for the relation given. + + If the ``revert`` argument is true unset the corresponding settings. + """ + + def setAcquiredPrincipalRoles(relation, revert=False): + """ Assign roles on children/resources for the relation given. + + If the ``revert`` argument is true unset the corresponding settings. + """ + diff --git a/security/manage_permissionform.pt b/security/manage_permissionform.pt new file mode 100644 index 0000000..76a45c2 --- /dev/null +++ b/security/manage_permissionform.pt @@ -0,0 +1,115 @@ + + + + + + +
+

+ +

+
+ + + +

+ Roles assigned to the permission + Change DTML Methods + (id: Zope.Some.Permission) +

+ +
+ + +
+ +
-

Type(s) to search for

+

Type(s) to search for

-

Text-based search

+

Text-based search

-

Search via related concepts

+

Search via related concepts

+ + + + + + + + + + + + + + + + + + + + +
RoleUsers/GroupsAcquired SettingSetting
+ Manager + + + User xy + + + + + +
+ Direct Settings + +xyz
+ +
+ +
+
+ +
+ + + + + + diff --git a/security/perm.py b/security/perm.py new file mode 100644 index 0000000..ca402b8 --- /dev/null +++ b/security/perm.py @@ -0,0 +1,139 @@ +# +# 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 +# + +""" +Authentication view. + +$Id$ +""" + +from zope import component +from zope.interface import implements +from zope.cachedescriptors.property import Lazy +from zope.app.security.interfaces import IPermission +from zope.app.securitypolicy.browser.rolepermissionview import RolePermissionView +from zope.app.securitypolicy.interfaces import IPrincipalRoleManager, IRolePermissionMap +from zope.app.securitypolicy.interfaces import IPrincipalPermissionManager, \ + IPrincipalPermissionMap +from zope.app.securitypolicy.zopepolicy import SettingAsBoolean +from zope.publisher.browser import BrowserView +from zope.security.proxy import removeSecurityProxy +from zope.traversing.api import getParents + + +class PermissionView(object): + """ View for permission editing. + """ + + def __init__(self, context, request): + self.context = context + # make sure the real view (delegate) updates our context when + # talking about the context's parent: + self.__parent__ = context + self.request = request + self.delegate = RolePermissionView() + self.delegate.context = self + self.delegate.request = request + self.permissionId = request.get('permission_to_manage') or 'zope.View' + + def pagetip(self): + return self.delegate.pagetip() + + def roles(self): + return self.delegate.roles() + + def permissions(self): + return self.delegate.permissions() + + def availableSettings(self, noacquire=False): + return self.delegate.availableSettings(noacquire) + + def permissionRoles(self): + return self.delegate.permissionRoles() + + def permissionForID(self, pid): + return self.delegate.permissionForID(pid) + + @Lazy + def permission(self): + return self.permissionForID(self.permissionId) + + def roleForID(self, rid): + return self.delegate.roleForID(rid) + + def update(self, testing=None): + return self.delegate.update(testing) + + def getAcquiredPermissionSetting(self, role, perm): + for obj in getParents(self.context): + rpm = IRolePermissionMap(obj, None) + if rpm is not None: + setting = rpm.getSetting(perm, role) + setting = SettingAsBoolean[setting] + if setting is not None: + return setting and '+' or '-' + return '' + + def listUsersForRole(self, rid): + result = '' + direct = IPrincipalRoleManager(self.context).getPrincipalsForRole(rid) + if direct: + result = '' + self.renderEntry(direct) + '' + acquired = [] + for obj in getParents(self.context): + prm = IPrincipalRoleManager(obj, None) + if prm is not None: + entry = prm.getPrincipalsForRole(rid) + if entry: + acquired.append(self.renderEntry(entry)) + if acquired: + if result: + result += '
' + result += '
'.join(acquired) + return result + + def renderEntry(self, entry): + result = [] + for e in entry: + value = SettingAsBoolean[e[1]] + value = (value is False and '-') or (value and '+') or '' + result.append(value + e[0]) + return ', '.join(result) + + def getPrincipalPermissions(self): + result = '' + ppm = IPrincipalPermissionMap(self.context) + direct = ppm.getPrincipalsForPermission(self.permissionId) + if direct: + result = '' + self.renderEntry(direct) + '' + acquired = [] + for obj in getParents(self.context): + ppm = IPrincipalPermissionMap(obj, None) + if ppm is not None: + entry = ppm.getPrincipalsForPermission(self.permissionId) + if entry: + acquired.append(self.renderEntry(entry)) + if acquired: + if result: + result += '
' + result += '
'.join(acquired) + return result + + def getPermissions(self): + return sorted(name for name, perm in component.getUtilitiesFor(IPermission)) + diff --git a/security/policy.py b/security/policy.py new file mode 100644 index 0000000..6f58706 --- /dev/null +++ b/security/policy.py @@ -0,0 +1,96 @@ +# +# 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 +# + +""" +A loops-specific security policy. Intended mainly to deal with checking +concept map parents in addition to containers for collecting principal roles. + +$Id$ +""" + +from zope.app.securitypolicy.interfaces import IPrincipalRoleMap +from zope.app.securitypolicy.zopepolicy import ZopeSecurityPolicy +from zope.app.securitypolicy.zopepolicy import SettingAsBoolean, globalRolesForPrincipal +from zope import component +from zope.component import adapts +from zope.cachedescriptors.property import Lazy +from zope.interface import classProvides +from zope.security.interfaces import ISecurityPolicy +from zope.security.proxy import removeSecurityProxy + +from loops.interfaces import IConcept, IResource + + +class LoopsSecurityPolicy(ZopeSecurityPolicy): + + classProvides(ISecurityPolicy) + + def cached_principal_roles(self, obj, principal, checked=None): + if checked is None: + checked = [] + obj = removeSecurityProxy(obj) + cache = self.cache(obj) + try: + cache_principal_roles = cache.principal_roles + except AttributeError: + cache_principal_roles = cache.principal_roles = {} + try: + return cache_principal_roles[principal] + except KeyError: + pass + if obj is None: + roles = dict([(role, SettingAsBoolean[setting]) + for (role, setting) + in globalRolesForPrincipal(principal)]) + roles['zope.Anonymous'] = True # Everybody has Anonymous + cache_principal_roles[principal] = roles + return roles + roles = {} + for p in self.getParents(obj, checked): + # TODO: care for correct combination if there is more than + # one parent + roles.update(self.cached_principal_roles(p, principal)) + prinrole = IPrincipalRoleMap(obj, None) + if prinrole: + roles = roles.copy() + for role, setting in prinrole.getRolesForPrincipal(principal): + roles[role] = SettingAsBoolean[setting] + cache_principal_roles[principal] = roles + #print '***', roles + return roles + + def getParents(self, obj, checked): + if obj in checked: # cycle - leave the concept map + return [getattr(obj, '__parent__', None)] + # keep concept parents in cache + cache = self.cache(obj) + try: + parents = cache.parents + except AttributeError: + parents = [] + if IConcept.providedBy(obj): + parents = [p for p in obj.getParents(noSecurityCheck=True) + if p != obj] + elif IResource.providedBy(obj): + parents = [p for p in obj.getConcepts(noSecurityCheck=True) + if p != obj] + cache.parents = parents + if not parents: + parents = [getattr(obj, '__parent__', None)] + checked.append(obj) + return parents diff --git a/security/setter.py b/security/setter.py new file mode 100644 index 0000000..dd07df3 --- /dev/null +++ b/security/setter.py @@ -0,0 +1,52 @@ +# +# 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 +# + +""" +Base classes for security setters, i.e. adapters that provide standardized +methods for setting role permissions and other security-related stuff. + +$Id$ +""" + +from zope import component +from zope.component import adapts +from zope.cachedescriptors.property import Lazy +from zope.interface import implements, Interface + +from loops.security.interfaces import ISecuritySetter + + +class BaseSecuritySetter(object): + + implements(ISecuritySetter) + adapts(Interface) + + def __init__(self, context): + self.context = context + + def setDefaultRolePermissions(self): + pass + + def setDefaultPrincipalRoles(self): + pass + + def setAcquiredRolePermissions(self, relation, revert=False): + pass + + def setAcquiredPrincipalRoles(self, relation, revert=False): + pass diff --git a/setup.py b/setup.py index 7555dc3..12f07ef 100644 --- a/setup.py +++ b/setup.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 @@ -25,13 +25,16 @@ $Id$ from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent from zope.event import notify from zope import component +from zope.cachedescriptors.property import Lazy from zope.component import adapts from zope.interface import implements, Interface +from zope.traversing.api import getName from cybertools.typology.interfaces import IType +from loops.common import adapted +from loops.concept import ConceptManager, Concept from loops.interfaces import ILoops, ITypeConcept from loops.interfaces import IFile, IImage, ITextDocument, INote -from loops.concept import ConceptManager, Concept from loops.query import IQueryConcept from loops.resource import ResourceManager, Resource from loops.view import ViewManager, Node @@ -79,7 +82,6 @@ class SetupManager(object): domain = self.addObject(conceptManager, Concept, 'domain', title=u'Domain') query = self.addObject(conceptManager, Concept, 'query', title=u'Query') file = self.addObject(conceptManager, Concept, 'file', title=u'File') - #image = self.addObject(conceptManager, Concept, 'image', title=u'Image') textdocument = self.addObject(conceptManager, Concept, 'textdocument', title=u'Text') note = self.addObject(conceptManager, Concept, 'note', title=u'Note') @@ -89,13 +91,105 @@ class SetupManager(object): ITypeConcept(typeConcept).typeInterface = ITypeConcept ITypeConcept(query).typeInterface = IQueryConcept ITypeConcept(file).typeInterface = IFile - #ITypeConcept(image).typeInterface = IImage ITypeConcept(textdocument).typeInterface = ITextDocument ITypeConcept(note).typeInterface = INote - ITypeConcept(note).viewName = 'note.html' # leads to error in DocTest + ITypeConcept(note).viewName = 'note.html' hasType.conceptType = predicate standard.conceptType = predicate + # standard properties and methods + + @Lazy + def concepts(self): + return self.context.getConceptManager() + + @Lazy + def resources(self): + return self.context.getResourceManager() + + @Lazy + def views(self): + return self.context.getViewManager() + + @Lazy + def typeConcept(self): + return self.concepts.getTypeConcept() + + @Lazy + def predicateType(self): + return self.concepts.getPredicateType() + + def addType(self, name, title, typeInterface=None, **kw): + c = self.addConcept(name, title, self.typeConcept, + typeInterface=typeInterface, **kw) + return c + + def addPredicate(self, name, title, **kw): + c = self.addConcept(name, title, self.predicateType, **kw) + return c + + def addConcept(self, name, title, conceptType, description=u'', + parentName=None, **kw): + if name in self.concepts: + self.log("Concept '%s' ('%s') already exists." % (name, title)) + c = self.concepts[name] + if c.conceptType != conceptType: + self.log("Wrong concept type for '%s': '%s' instead of '%s'." % + (name, getName(c.conceptType), getName(conceptType))) + else: + c = addAndConfigureObject(self.concepts, Concept, name, title=title, + description=description, + conceptType=conceptType, **kw) + self.log("Concept '%s' ('%s') created." % (name, title)) + if parentName is not None: + self.assignChild(parentName, name) + return c + + def setConceptAttribute(self, concept, attr, value): + setattr(adapted(concept), attr, value) + self.log("Setting Attribute '%s' of '%s' to '%s'" % + (attr, getName(concept), repr(value))) + + def assignChild(self, conceptName, childName, predicate=None): + if predicate is None: + predicate = self.concepts.getDefaultPredicate() + if isinstance(predicate, str): + predicate = self.concepts[predicate] + concept = self.concepts[conceptName] + child = self.concepts[childName] + if child in concept.getChildren([predicate]): + self.log("Concept '%s' is already a child of '%s with predicate '%s'.'" % + (childName, conceptName, getName(predicate))) + else: + concept.assignChild(child, predicate) + + def addNode(self, name, title, container=None, nodeType='page', + description=u'', body=u'', targetName=None, **kw): + if container is None: + container = self.views + nodeType = 'menu' + if name in container: + self.log("Node '%s' ('%s') already exists in '%s'." % + (name, title, getName(container))) + n = container[name] + if n.nodeType != nodeType: + self.log("Wrong node type for '%s': '%s' instead of '%s'." % + (name, n.nodeType, nodeType)) + else: + n = addAndConfigureObject(container, Node, name, title=title, + description=description, body=body, + nodeType=nodeType, **kw) + self.log("Node '%s' ('%s') created." % (name, title)) + if targetName is not None: + if targetName in self.concepts: + n.target = self.concepts[targetName] + return n + + def log(self, message): + if isinstance(message, unicode): + message = message.encode('UTF-8') + print >> self.logger, message + def addObject(self, container, class_, name, **kw): return addObject(container, class_, name, **kw) diff --git a/tests/auth.py b/tests/auth.py new file mode 100644 index 0000000..d03de7e --- /dev/null +++ b/tests/auth.py @@ -0,0 +1,60 @@ +# +# 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 +# + +""" +Elements helpful for testing with authenticated users. + +$Id$ +""" + + +from zope.cachedescriptors.property import Lazy +from zope.publisher.browser import TestRequest as BaseTestRequest +from zope.security.management import getInteraction, newInteraction, endInteraction + + +class Participation(object): + """ Dummy Participation class for testing. + """ + + interaction = None + + def __init__(self, principal): + self.principal = principal + + +class TestRequest(BaseTestRequest): + + basePrincipal = BaseTestRequest.principal + + @Lazy + def principal(self): + interaction = getInteraction() + if interaction is not None: + parts = interaction.participations + if parts: + prin = parts[0].principal + if prin is not None: + return prin + return self.basePrincipal + + +def login(principal): + endInteraction() + newInteraction(Participation(principal)) + diff --git a/tests/setup.py b/tests/setup.py index 54132db..c085193 100644 --- a/tests/setup.py +++ b/tests/setup.py @@ -6,21 +6,31 @@ $Id$ from zope import component from zope.annotation.attribute import AttributeAnnotations +from zope.annotation.interfaces import IAnnotatable from zope.app.catalog.catalog import Catalog from zope.app.catalog.interfaces import ICatalog from zope.app.catalog.field import FieldIndex from zope.app.catalog.text import TextIndex from zope.app.container.interfaces import IObjectRemovedEvent +from zope.app.principalannotation import PrincipalAnnotationUtility +from zope.app.principalannotation.interfaces import IPrincipalAnnotationUtility from zope.app.security.principalregistry import principalRegistry from zope.app.security.interfaces import IAuthentication +from zope.app.securitypolicy.zopepolicy import ZopeSecurityPolicy +from zope.app.securitypolicy.principalrole import AnnotationPrincipalRoleManager +from zope.app.securitypolicy.rolepermission import AnnotationRolePermissionManager from zope.app.session.interfaces import IClientIdManager, ISessionDataContainer from zope.app.session import session from zope.dublincore.annotatableadapter import ZDCAnnotatableAdapter from zope.dublincore.interfaces import IZopeDublinCore -from zope.interface import implements +from zope.interface import Interface, implements +from zope.publisher.interfaces.browser import IBrowserRequest, IBrowserView +from zope.security.checker import Checker, defineChecker +from cybertools.browser.controller import Controller from cybertools.composer.schema.factory import SchemaFactory from cybertools.composer.schema.field import FieldInstance, NumberFieldInstance +from cybertools.composer.schema.field import DateFieldInstance, BooleanFieldInstance from cybertools.composer.schema.instance import Instance, Editor from cybertools.relation.tests import IntIdsStub from cybertools.relation.registry import RelationRegistry @@ -34,14 +44,21 @@ from loops.base import Loops from loops import util from loops.browser.node import ViewPropertiesConfigurator from loops.common import NameChooser -from loops.interfaces import ILoopsObject, IIndexAttributes -from loops.interfaces import IDocument, IFile, ITextDocument from loops.concept import Concept from loops.concept import IndexAttributes as ConceptIndexAttributes +from loops.interfaces import ILoopsObject, IIndexAttributes +from loops.interfaces import IDocument, IFile, ITextDocument +from loops.organize.memberinfo import MemberInfoProvider +from loops.query import QueryConcept from loops.query import QueryConcept from loops.resource import Resource, FileAdapter, TextDocumentAdapter +from loops.resource import Document, MediaAsset from loops.resource import IndexAttributes as ResourceIndexAttributes from loops.schema import ResourceSchemaFactory, FileSchemaFactory, NoteSchemaFactory +from loops.security.common import grantAcquiredSecurity, revokeAcquiredSecurity +from zope.security.management import setSecurityPolicy +from loops.security.policy import LoopsSecurityPolicy +from loops.security.setter import BaseSecuritySetter from loops.setup import SetupManager, addObject from loops.type import LoopsType, ConceptType, ResourceType, TypeConcept from loops.view import NodeAdapter @@ -62,14 +79,25 @@ class TestSite(object): def baseSetup(self): site = self.site + #oldPolicy = setSecurityPolicy(ZopeSecurityPolicy) + oldPolicy = setSecurityPolicy(LoopsSecurityPolicy) + checker = Checker(dict(title='zope.View', data='zope.View'), + dict(title='zope.ManageContent')) + defineChecker(Concept, checker) + defineChecker(Resource, checker) + defineChecker(Document, checker) + component.provideUtility(IntIdsStub()) relations = RelationRegistry() relations.setupIndexes() component.provideUtility(relations, IRelationRegistry) - component.provideAdapter(IndexableRelationAdapter) + component.provideUtility(PrincipalAnnotationUtility(), IPrincipalAnnotationUtility) + component.provideAdapter(IndexableRelationAdapter) component.provideAdapter(ZDCAnnotatableAdapter, (ILoopsObject,), IZopeDublinCore) component.provideAdapter(AttributeAnnotations, (ILoopsObject,)) + component.provideAdapter(AnnotationPrincipalRoleManager, (ILoopsObject,)) + component.provideAdapter(AnnotationRolePermissionManager, (ILoopsObject,)) component.provideUtility(principalRegistry, IAuthentication) component.provideAdapter(session.ClientId) component.provideAdapter(session.Session) @@ -86,16 +114,26 @@ class TestSite(object): component.provideAdapter(NodeAdapter) component.provideAdapter(ViewPropertiesConfigurator) component.provideAdapter(NameChooser) + component.provideHandler(grantAcquiredSecurity) + component.provideHandler(revokeAcquiredSecurity) + component.provideAdapter(BaseSecuritySetter) + component.provideAdapter(Instance) component.provideAdapter(Editor, name='editor') component.provideAdapter(FieldInstance) component.provideAdapter(NumberFieldInstance, name='number') - + component.provideAdapter(DateFieldInstance, name='date') + component.provideAdapter(BooleanFieldInstance, name='boolean') component.provideAdapter(SchemaFactory) component.provideAdapter(ResourceSchemaFactory) component.provideAdapter(FileSchemaFactory) component.provideAdapter(NoteSchemaFactory) + component.provideAdapter(Controller, (Interface, IBrowserRequest), + IBrowserView, name='controller') + component.provideAdapter(MemberInfoProvider, + (ILoopsObject, IBrowserRequest)) + component.getSiteManager().registerHandler(invalidateRelations, (ILoopsObject, IObjectRemovedEvent)) component.getSiteManager().registerHandler(removeRelation, diff --git a/type.py b/type.py index 4149f98..51260d3 100644 --- a/type.py +++ b/type.py @@ -88,7 +88,6 @@ class LoopsType(BaseType): addQualifiers = self.optionsDict.get('qualifier') if addQualifiers: qu.extend(addQualifiers.split(',')) - # how to set a type to 'hidden'? return tuple(qu) @Lazy @@ -112,14 +111,7 @@ class LoopsType(BaseType): @Lazy def optionsDict(self): - result = {'default': []} - for opt in self.options: - if ':' in opt: - key, value = opt.split(':', 1) - result[key] = value - else: - result['default'].append(opt) - return result + return getOptionsDict(self.options) # general infos @@ -291,3 +283,13 @@ class TypeInterfaceSourceList(object): def __len__(self): return len(self.typeInterfaces) + +def getOptionsDict(options): + result = {'default': []} + for opt in options: + if ':' in opt: + key, value = opt.split(':', 1) + result[key] = value + else: + result['default'].append(opt) + return result diff --git a/util.py b/util.py index 43da7de..4a08105 100644 --- a/util.py +++ b/util.py @@ -27,12 +27,22 @@ from zope.app.intid.interfaces import IIntIds from zope.interface import directlyProvides, directlyProvidedBy from zope.i18nmessageid import MessageFactory from zope.schema import vocabulary -#from view import TargetRelation + +from loops.browser.util import html_quote _ = MessageFactory('loops') #_ = MessageFactory('zope') # it's easier not use a special i18n domain? +renderingFactories = { + 'text/plain': 'zope.source.plaintext', + 'text/stx': 'zope.source.stx', + 'text/structured': 'zope.source.stx', + 'text/rest': 'zope.source.rest', + 'text/restructured': 'zope.source.rest', +} + + class KeywordVocabulary(vocabulary.SimpleVocabulary): def __init__(self, items, *interfaces): diff --git a/versioning/versionable.py b/versioning/versionable.py index cdd5474..8d95fae 100644 --- a/versioning/versionable.py +++ b/versioning/versionable.py @@ -48,6 +48,7 @@ class VersionableResource(object): def __init__(self, context): self.context = context + self.__parent__ = context def getVersioningAttribute(self, attr, default): attrName = attrPattern % attr diff --git a/view.py b/view.py index 991c40e..e005a55 100644 --- a/view.py +++ b/view.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2005 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 @@ -27,13 +27,12 @@ from zope.app import zapi from zope.app.container.btree import BTreeContainer from zope.app.container.contained import Contained from zope.app.container.ordered import OrderedContainer -from zope.app.container.traversal import ContainerTraverser, ItemTraverser -from zope.app.container.traversal import ContainerTraversable from zope.app.intid.interfaces import IIntIds from zope.cachedescriptors.property import Lazy, readproperty from zope.component import adapts from zope.interface import implements from zope.interface import alsoProvides, directlyProvides, directlyProvidedBy +from zope.publisher.browser import applySkin from zope.security.proxy import removeSecurityProxy from persistent import Persistent from cybertools.relation import DyadicRelation diff --git a/xmlrpc/README.txt b/xmlrpc/README.txt index c09ce84..762d01e 100755 --- a/xmlrpc/README.txt +++ b/xmlrpc/README.txt @@ -8,57 +8,39 @@ 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.publisher.browser import TestRequest >>> from loops.concept import Concept >>> from loops.resource import Resource + >>> -and setup a simple loops site with a concept manager and some concepts +and set up 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 cybertools.relation.registry import DummyRelationRegistry - >>> component.provideUtility(DummyRelationRegistry()) - >>> from cybertools.relation.tests import IntIdsStub - >>> intIds = IntIdsStub() - >>> component.provideUtility(intIds) - - >>> from loops.type import LoopsType, ConceptType, TypeConcept - >>> component.provideAdapter(LoopsType) - >>> component.provideAdapter(ConceptType) - >>> component.provideAdapter(TypeConcept) - >>> from loops.common import NameChooser - >>> component.provideAdapter(NameChooser) - - >>> from loops.base import Loops - >>> loopsRoot = site['loops'] = Loops() - - >>> from loops.setup import SetupManager + >>> from loops.setup import addObject >>> from loops.organize.setup import SetupManager as OrganizeSetupManager >>> component.provideAdapter(OrganizeSetupManager, name='organize') - >>> setup = SetupManager(loopsRoot) - >>> concepts, resources, views = setup.setup() + >>> from loops.knowledge.setup import SetupManager as KnowledgeSetupManager + >>> component.provideAdapter(KnowledgeSetupManager, name='knowledge') + >>> from loops.tests.setup import TestSite + >>> t = TestSite(site) + >>> concepts, resources, views = t.setup() + + >>> from loops import util + >>> loopsRoot = site['loops'] + >>> loopsId = util.getUidForObject(loopsRoot) Let's look what setup has provided us with: - >>> sorted(concepts) - [u'domain', u'file', u'hasType', u'note', u'ownedby', u'person', - u'predicate', u'query', u'standard', u'textdocument', u'type'] + >>> len(concepts) + 19 Now let's add a few more concepts: - >>> topic = concepts[u'topic'] = Concept(u'Topic') - >>> intIds.register(topic) - 11 - >>> zope = concepts[u'zope'] = Concept(u'Zope') - >>> zope.conceptType = topic - >>> intIds.register(zope) - 12 - >>> zope3 = concepts[u'zope3'] = Concept(u'Zope 3') - >>> zope3.conceptType = topic - >>> intIds.register(zope3) - 13 + >>> topic = concepts[u'topic'] + >>> zope = addObject(concepts, Concept, 'zope', title=u'Zope', conceptType=topic) + >>> zope3 = addObject(concepts, Concept, 'zope3', title=u'Zope 3', conceptType=topic) Navigation typically starts at a start object, which by default ist the domain concept (if present, otherwise the top-level type concept): @@ -70,16 +52,16 @@ domain concept (if present, otherwise the top-level type concept): ['children', 'description', 'id', 'name', 'parents', 'resources', 'title', 'type', 'viewName'] >>> startObj['id'], startObj['name'], startObj['title'], startObj['type'] - ('1', u'domain', u'Domain', '0') + ('3', u'domain', u'Domain', '0') There are a few standard objects we can retrieve directly: >>> defaultPred = xrf.getDefaultPredicate() >>> defaultPred['id'], defaultPred['name'] - ('8', u'standard') + ('16', u'standard') >>> typePred = xrf.getTypePredicate() >>> typePred['id'], typePred['name'] - ('7', u'hasType') + ('1', u'hasType') >>> typeConcept = xrf.getTypeConcept() >>> typeConcept['id'], typeConcept['name'] ('0', u'type') @@ -89,19 +71,19 @@ note that the 'hasType' predicate is not shown as it should not be applied in an explicit assignment. >>> sorted(t['name'] for t in xrf.getConceptTypes()) - [u'domain', u'file', u'note', u'person', u'predicate', u'query', - u'textdocument', u'type'] + [u'customer', u'domain', u'file', u'note', u'person', u'predicate', u'query', + u'task', u'textdocument', u'topic', u'type'] >>> sorted(t['name'] for t in xrf.getPredicates()) - [u'ownedby', u'standard'] + [u'depends', u'knows', u'ownedby', u'provides', u'requires', u'standard'] We can also retrieve a certain object by its id or its name: - >>> obj2 = xrf.getObjectById('2') + >>> obj2 = xrf.getObjectById('5') >>> obj2['id'], obj2['name'] - ('2', u'query') + ('5', u'query') >>> textdoc = xrf.getObjectByName(u'textdocument') >>> textdoc['id'], textdoc['name'] - ('5', u'textdocument') + ('11', u'textdocument') All methods that retrieve one object also returns its children and parents: @@ -111,8 +93,8 @@ All methods that retrieve one object also returns its children and parents: >>> ch[0]['name'] u'hasType' >>> sorted(c['name'] for c in ch[0]['objects']) - [u'domain', u'file', u'note', u'person', u'predicate', u'query', - u'textdocument', u'type'] + [u'customer', u'domain', u'file', u'note', u'person', u'predicate', + u'query', u'task', u'textdocument', u'topic', u'type'] >>> pa = defaultPred['parents'] >>> len(pa) @@ -130,8 +112,8 @@ We can also retrieve children and parents explicitely: >>> ch[0]['name'] u'hasType' >>> sorted(c['name'] for c in ch[0]['objects']) - [u'domain', u'file', u'note', u'person', u'predicate', u'query', - u'textdocument', u'type'] + [u'customer', u'domain', u'file', u'note', u'person', u'predicate', + u'query', u'task', u'textdocument', u'topic', u'type'] >>> pa = xrf.getParents('7') >>> len(pa) @@ -139,7 +121,7 @@ We can also retrieve children and parents explicitely: >>> pa[0]['name'] u'hasType' >>> sorted(p['name'] for p in pa[0]['objects']) - [u'predicate'] + [u'type'] Resources --------- @@ -184,19 +166,23 @@ Updating the concept map >>> topicId = xrf.getObjectByName('topic')['id'] >>> xrf.createConcept(topicId, u'zope2', u'Zope 2') - {'description': u'', 'title': u'Zope 2', 'type': '11', 'id': '17', + {'description': u'', 'title': u'Zope 2', 'type': '22', 'id': '56', 'name': u'zope2'} The name of the concept is checked by a name chooser; if the corresponding parameter is empty, the name will be generated from the title. >>> xrf.createConcept(topicId, u'', u'Python') - {'description': u'', 'title': u'Python', 'type': '11', 'id': '18', + {'description': u'', 'title': u'Python', 'type': '22', 'id': '58', 'name': u'python'} Changing the attributes of a concept ------------------------------------ + >>> from loops.knowledge.knowledge import Person + >>> from loops.knowledge.interfaces import IPerson + >>> component.provideAdapter(Person, provides=IPerson) + >>> xrf.editConcept(john['id'], 'firstName', u'John') 'OK' >>> john = xrf.getObjectById(john['id']) diff --git a/xmlrpc/common.py b/xmlrpc/common.py index 6e52161..f46f6b3 100644 --- a/xmlrpc/common.py +++ b/xmlrpc/common.py @@ -162,7 +162,7 @@ def objectAsDict(obj, langInfo=None): 'title': adapter.title, 'description': adapter.description, 'type': getUidForObject(objType.typeProvider)} ti = objType.typeInterface - if ti is not None: + if ti is not None and adapter != obj: #for attr in (list(adapter._adapterAttributes) + list(ti)): for attr in list(ti): if attr not in ('__parent__', 'context', 'id', 'name',