diff --git a/README.txt b/README.txt index dc019b3..4068bfd 100755 --- a/README.txt +++ b/README.txt @@ -2,8 +2,6 @@ loops - Linked Objects for Organization and Processing Services =============================================================== - ($Id$) - The loops platform consists up of three basic types of objects: (1) concepts: simple interconnected objects usually representing @@ -612,7 +610,7 @@ Actions >>> view.controller = Controller(view, request) >>> #view.setupController() - >>> actions = view.getActions('portlet') + >>> actions = view.getAllowedActions('portlet') >>> len(actions) 2 @@ -849,7 +847,7 @@ In order to provide suitable links for viewing or editing a target you may ask a view which view and edit actions it supports. We directly use the target object's view here: - >>> actions = view.virtualTarget.getActions('object', page=view) + >>> actions = view.virtualTarget.getAllowedActions('object', page=view) >>> #actions[0].url 'http://127.0.0.1/loops/views/m1/m11/m111/.target23' diff --git a/browser/action.py b/browser/action.py index 68176d9..06600c5 100644 --- a/browser/action.py +++ b/browser/action.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -129,6 +129,7 @@ actions.register('edit_object', 'portlet', DialogAction, viewName='edit_object.html', dialogName='edit', prerequisites=['registerDojoEditor'], + permission='zope.ManageContent', ) actions.register('edit_concept', 'portlet', DialogAction, @@ -137,6 +138,7 @@ actions.register('edit_concept', 'portlet', DialogAction, viewName='edit_concept.html', dialogName='edit', prerequisites=['registerDojoEditor'], + permission='zope.ManageContent', ) actions.register('create_concept', 'portlet', DialogAction, @@ -146,6 +148,7 @@ actions.register('create_concept', 'portlet', DialogAction, dialogName='createConcept', qualifier='create_concept', innerForm='inner_concept_form.html', + permission='loops.AssignAsParent', ) actions.register('create_subtype', 'portlet', DialogAction, @@ -155,4 +158,5 @@ actions.register('create_subtype', 'portlet', DialogAction, dialogName='createConcept', qualifier='subtype', innerForm='inner_concept_form.html', + permission='loops.AssignAsParent', ) diff --git a/browser/common.py b/browser/common.py index 990093b..c7e0528 100644 --- a/browser/common.py +++ b/browser/common.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -45,7 +45,7 @@ from zope.publisher.browser import applySkin from zope.publisher.interfaces.browser import IBrowserSkinType, IBrowserView from zope import schema from zope.schema.vocabulary import SimpleTerm -from zope.security import canAccess, checkPermission +from zope.security import canAccess from zope.security.interfaces import ForbiddenAttribute, Unauthorized from zope.security.proxy import removeSecurityProxy from zope.traversing.browser import absoluteURL @@ -54,10 +54,12 @@ from zope.traversing.api import getName, getParent from cybertools.ajax.dojo import dojoMacroTemplate from cybertools.browser.view import GenericView from cybertools.meta.interfaces import IOptions +from cybertools.meta.element import Element from cybertools.relation.interfaces import IRelationRegistry from cybertools.stateful.interfaces import IStateful from cybertools.text import mimetypes from cybertools.typology.interfaces import IType, ITypeManager +from cybertools.util.date import toLocalTime from cybertools.util.jeep import Jeep from loops.browser.util import normalizeForUrl from loops.common import adapted, baseObject @@ -66,6 +68,7 @@ from loops.i18n.browser import I18NView from loops.interfaces import IResource, IView, INode, ITypeConcept from loops.organize.tracking import access from loops.resource import Resource +from loops.security.common import checkPermission from loops.security.common import canAccessObject, canListObject, canWriteObject from loops.type import ITypeConcept from loops import util @@ -131,6 +134,7 @@ class BaseView(GenericView, I18NView): actions = {} portlet_actions = [] + parts = () icon = None modeName = 'view' isToplevel = False @@ -184,6 +188,15 @@ class BaseView(GenericView, I18NView): return '%s/.%s-%s' % (baseUrl, targetId, normalizeForUrl(title)) return '%s/.%s' % (baseUrl, targetId) + def filterInput(self): + result = [] + for name in self.getOptions('filter_input'): + view = component.queryMultiAdapter( + (self.context, self.request), name='filter_input.' + name) + if view is not None: + result.append(view) + return result + @Lazy def principalId(self): principal = self.request.principal @@ -258,6 +271,8 @@ class BaseView(GenericView, I18NView): d = dc.modified or dc.created if isinstance(d, str): d = datetime(*(strptime(d, '%Y-%m-%dT%H:%M')[:6])) + else: + d = toLocalTime(d) return d @Lazy @@ -486,7 +501,7 @@ class BaseView(GenericView, I18NView): if text is None: return u'' htmlPattern = re.compile(r'<(.+)>.+') - if htmlPattern.search(text): + if '
' in text or htmlPattern.search(text): return text return self.renderText(text, 'text/restructured') @@ -565,16 +580,29 @@ class BaseView(GenericView, I18NView): return DummyOptions() return component.queryAdapter(self.adapted, IOptions) or DummyOptions() - @Lazy - def globalOptions(self): - return IOptions(self.loopsRoot) - @Lazy def typeOptions(self): if self.typeProvider is None: return DummyOptions() return IOptions(adapted(self.typeProvider)) + @Lazy + def globalOptions(self): + return IOptions(self.loopsRoot) + + def getOptions(self, keys): + for opt in (self.options, self.typeOptions, self.globalOptions): + if isinstance(opt, DummyOptions): + continue + #import pdb; pdb.set_trace() + v = opt + for key in keys.split('.'): + if isinstance(v, list): + break + v = getattr(v, key) + if not isinstance(v, DummyOptions): + return v + def getPredicateOptions(self, relation): return IOptions(adapted(relation.predicate), None) or DummyOptions() @@ -648,7 +676,10 @@ class BaseView(GenericView, I18NView): # states - viewStatesPermission = 'zope.ManageContent' + @Lazy + def viewStatesPermission(self): + opt = self.globalOptions('organize.show_states') + return opt and opt[0] or 'zope.ManageContent' @Lazy def states(self): @@ -685,6 +716,16 @@ class BaseView(GenericView, I18NView): """ return [] + def getAllowedActions(self, category='object', page=None, target=None): + result = [] + for act in self.getActions(category, page=page, target=target): + if act.permission is not None: + ctx = (target is not None and target.context) or self.context + if not checkPermission(act.permission, ctx): + continue + result.append(act) + return result + @Lazy def showObjectActions(self): return not IUnauthenticatedPrincipal.providedBy(self.request.principal) @@ -822,6 +863,7 @@ class BaseView(GenericView, I18NView): def registerDojoFormAll(self): self.registerDojo() + self.registerDojoEditor() cm = self.controller.macros jsCall = ('dojo.require("dijit.form.Form"); ' 'dojo.require("dijit.form.DateTextBox"); ' diff --git a/browser/concept.py b/browser/concept.py index 0aa03e3..50ff0a5 100644 --- a/browser/concept.py +++ b/browser/concept.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2011 Helmut Merz helmutm@cy55.de +# Copyright (c) 2012 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 @@ -200,16 +200,22 @@ class BaseRelationView(BaseView): class ConceptView(BaseView): template = concept_macros + templateName = 'concept.standard' + macroName = 'conceptdata' + partPrefix = 'part_' + defaultParts = ('title', 'fields', + 'children', 'resources', 'workitems', 'comments',) def childViewFactory(self, *args, **kw): return ConceptRelationView(*args, **kw) @Lazy - def macro(self): - return self.template.macros['conceptdata'] + def macros(self): + return self.controller.getTemplateMacros(self.templateName, self.template) - #def __init__(self, context, request): - # super(ConceptView, self).__init__(context, request) + @property + def macro(self): + return self.macros[self.macroName] def setupController(self): cont = self.controller @@ -222,6 +228,24 @@ class ConceptView(BaseView): subMacro=concept_macros.macros['parents'], priority=20, info=self) + def getParts(self): + parts = (self.params.get('parts') or []) # deprecated! + if not parts: + parts = (self.options('parts') or self.typeOptions('parts') or + self.defaultParts) + return self.getPartViews(parts) + + def getPartViews(self, parts): + result = [] + for p in parts: + viewName = self.partPrefix + p + view = component.queryMultiAdapter((self.adapted, self.request), + name=viewName) + if view is not None: + view.parent = self + result.append(view) + return result + @Lazy def adapted(self): return adapted(self.context, self.languageInfo) @@ -296,6 +320,10 @@ class ConceptView(BaseView): if r.order != pos: r.order = pos + @Lazy + def filterOptions(self): + return self.getOptions('filter.states') + def getChildren(self, topLevelOnly=True, sort=True, noDuplicates=True, useFilter=True, predicates=None): form = self.request.form @@ -337,7 +365,7 @@ class ConceptView(BaseView): options = IOptions(adapted(r.predicate), None) if options is not None and options('hide_children'): continue - if not fv.check(r.context): + if not fv.check(r.context, self.filterOptions): continue yield r @@ -693,3 +721,22 @@ class ListChildren(ConceptView): def macro(self): return concept_macros.macros['list_children'] + +class ListTypeInstances(ListChildren): + + @Lazy + def targets(self): + targetPredicate = self.conceptManager['querytarget'] + for c in self.context.getChildren([targetPredicate]): + # TODO: use type-specific view + yield ConceptView(c, self.request) + + def children(self, topLevelOnly=True, sort=True, noDuplicates=True, + useFilter=True, predicates=None): + # TODO: use filter options of query for selection of children + for tv in self.targets: + tv.filterOptions = self.filterOptions + for c in tv.getChildren(topLevelOnly, sort, + noDuplicates, useFilter, [self.typePredicate]): + yield c + diff --git a/browser/concept_macros.pt b/browser/concept_macros.pt index 49b99d3..86844e1 100644 --- a/browser/concept_macros.pt +++ b/browser/concept_macros.pt @@ -1,6 +1,13 @@ + + + + + + +
@@ -22,6 +29,20 @@ + +
+
+ + + + +
+
+
+ + @@ -41,8 +62,10 @@ string:$resourceBase/cybertools.icons/table.png" /> + -

Description

diff --git a/browser/configure.zcml b/browser/configure.zcml index d8e1014..97782d8 100644 --- a/browser/configure.zcml +++ b/browser/configure.zcml @@ -545,6 +545,14 @@ factory="loops.browser.concept.ListChildren" permission="zope.View" /> + + - @@ -309,7 +310,7 @@ + 'submit();; return false'"> -

HTML

-
+
diff --git a/browser/lobo/standard.py b/browser/lobo/standard.py index c29484a..5c4fbff 100644 --- a/browser/lobo/standard.py +++ b/browser/lobo/standard.py @@ -41,18 +41,12 @@ class Base(BaseConceptView): templateName = 'lobo.standard' macroName = None - @Lazy - def macros(self): - return self.controller.getTemplateMacros(self.templateName, self.template) - - @property - def macro(self): - return self.macros[self.macroName] - - @Lazy - def params(self): - ann = self.request.annotations.get('loops.view', {}) - return parse_qs(ann.get('params') or '') + #@Lazy + # better implementation in BaseView: + # splits comma-separated list of values automatically + #def params(self): + # ann = self.request.annotations.get('loops.view', {}) + # return parse_qs(ann.get('params') or '') class ConceptView(BaseConceptView): @@ -152,25 +146,8 @@ class ConceptView(BaseConceptView): class Layout(Base, ConceptView): macroName = 'layout' - - def getParts(self): - parts = (self.params.get('parts') or [''])[0].split(',') # obsolete - if not parts or not parts[0]: - parts = (self.options('parts') or - self.typeOptions('parts') or - ['h1', 'g3']) - return self.getPartViews(parts) - - def getPartViews(self, parts): - result = [] - for p in parts: - viewName = 'lobo_' + p - view = component.queryMultiAdapter((self.adapted, self.request), - name=viewName) - if view is not None: - view.parent = self - result.append(view) - return result + partPrefix = 'lobo_' + defaultParts = ('h1', 'g3',) class BasePart(Base): @@ -192,7 +169,7 @@ class BasePart(Base): return preds def getChildren(self): - subtypeNames = (self.params.get('subtypes') or [''])[0].split(',') + subtypeNames = (self.params.get('subtypes') or []) subtypes = [self.conceptManager[st] for st in subtypeNames if st] result = [] childRels = self.context.getChildRelations(self.childPredicates) diff --git a/browser/loops.js b/browser/loops.js index f7310ec..5072186 100644 --- a/browser/loops.js +++ b/browser/loops.js @@ -211,6 +211,22 @@ function closeDialog(save) { } function closeDataWidget(save) { + form = dojo.byId('dialog_form'); + dojo.query('.dijitEditor').forEach(function(item, index) { + console.log(item); + var name = item.id; + var widget = dijit.byId(name); + value = widget.getValue(); + var ta = document.createElement('input'); + ta.type = 'hidden'; + ta.name = name; + ta.value = value; + form.appendChild(ta); + }); +} + + +function xx_closeDataWidget(save) { var widget = dijit.byId('data'); if (widget != undefined && save) { value = widget.getValue(); diff --git a/browser/node.py b/browser/node.py index cc82db9..5bed9b3 100644 --- a/browser/node.py +++ b/browser/node.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -51,17 +51,18 @@ from cybertools.typology.interfaces import IType, ITypeManager from cybertools.util.jeep import Jeep from cybertools.xedit.browser import ExternalEditorView from loops.browser.action import actions, DialogAction +from loops.browser.common import BaseView +from loops.browser.concept import ConceptView from loops.common import adapted, AdapterBase, baseObject from loops.i18n.browser import i18n_macros, LanguageInfo 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.common import BaseView -from loops.browser.concept import ConceptView from loops.organize.interfaces import IPresence from loops.organize.tracking import access +from loops.resource import MediaAsset +from loops.security.common import canWriteObject +from loops import util +from loops.util import _ from loops.versioning.util import getVersion @@ -150,13 +151,15 @@ class NodeView(BaseView): priority=20) cm.register('portlet_left', 'navigation', title='Navigation', subMacro=node_macros.macros['menu']) - if canWrite(self.context, 'title') or ( + if canWriteObject(self.context) or ( # TODO: is this useful in any case? self.virtualTargetObject is not None and - canWrite(self.virtualTargetObject, 'title')): + canWriteObject(self.virtualTargetObject)): # check if there are any available actions; # store list of actions in macro object (evaluate only once) - actions = [act for act in self.getActions('portlet') if act.condition] + actions = [act for act in self.getAllowedActions('portlet', + target=self.virtualTarget) + if act.condition] if actions: cm.register('portlet_right', 'actions', title=_(u'Actions'), subMacro=node_macros.macros['actions'], @@ -537,7 +540,7 @@ class NodeView(BaseView): return self.makeTargetUrl(self.url, util.getUidForObject(target), target.title) - def getActions(self, category='object', target=None): + def getActions(self, category='object', page=None, target=None): actions = [] #self.registerDojo() self.registerDojoFormAll() @@ -565,9 +568,11 @@ class NodeView(BaseView): description='Open concept map editor in new window', url=cmeUrl, target=target)) if self.checkAction('create_resource', 'portlet', target): - actions.append(DialogAction(self, title='Create Resource...', + actions.append(DialogAction(self, name='create_resource', + title='Create Resource...', description='Create a new resource object.', - page=self, target=target)) + page=self, target=target, + permission='zope.ManageContent')) return actions actions = dict(portlet=getPortletActions) diff --git a/browser/node_macros.pt b/browser/node_macros.pt index 7f65b19..b67f1fb 100644 --- a/browser/node_macros.pt +++ b/browser/node_macros.pt @@ -293,7 +293,8 @@
- +
@@ -322,7 +323,7 @@ - +
diff --git a/common.py b/common.py index 0b00e65..a585a12 100644 --- a/common.py +++ b/common.py @@ -113,6 +113,9 @@ class AdapterBase(object): self.context = context self.__parent__ = context # to get the permission stuff right + def __hash__(self): + return hash(self.context) + def __getattr__(self, attr): self.checkAttr(attr) return getattr(self.context, '_' + attr, None) diff --git a/compound/blog/browser.py b/compound/blog/browser.py index 14dc259..adffef0 100755 --- a/compound/blog/browser.py +++ b/compound/blog/browser.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -18,8 +18,6 @@ """ View classes for glossary and glossary items. - -$Id$ """ @@ -27,6 +25,7 @@ import itertools from zope import component from zope.app.pagetemplate import ViewPageTemplateFile from zope.cachedescriptors.property import Lazy +from zope.traversing.api import getName from cybertools.browser.action import actions from cybertools.browser.member import IMemberInfoProvider @@ -53,9 +52,36 @@ actions.register('createBlogPost', 'portlet', DialogAction, fixedType=True, innerForm='inner_concept_form.html', prerequisites=['registerDojoDateWidget'], # +'registerDojoTextWidget'? + permission='loops.AssignAsParent', ) +# blog lists + +class BlogList(ConceptView): + + @Lazy + def macro(self): + return view_macros.macros['bloglist'] + + def children(self, topLevelOnly=True, sort=True, noDuplicates=True, + useFilter=True, predicates=None): + rels = self.getChildren(topLevelOnly, sort, + noDuplicates, useFilter, predicates) + return [rel for rel in rels if self.notEmpty(rel)] + + def notEmpty(self, rel): + if self.request.form.get('filter.states') == 'all': + return True + # TODO: use type-specific view + view = ConceptView(rel.relation.second, self.request) + for c in view.children(): + return True + return False + + +# blog view + def supplyCreator(self, data): creator = data.get('creator') data['creatorId'] = creator @@ -99,6 +125,8 @@ class BlogView(ConceptView): @Lazy def blogOwnerId(self): + if getName(self.context.conceptType) != 'blog': + return '' pType = self.loopsRoot.getConceptManager()['person'] persons = [p for p in self.context.getParents() if p.conceptType == pType] if len(persons) == 1: @@ -127,6 +155,8 @@ class BlogView(ConceptView): return False +# blog post view + class BlogPostView(ConceptView): @Lazy @@ -152,6 +182,7 @@ class BlogPostView(ConceptView): description=_(u'Modify blog post.'), viewName='edit_blogpost.html', dialogName='editBlogPost', + permission='zope.ManageContent', page=page, target=target)) #self.registerDojoTextWidget() self.registerDojoDateWidget() @@ -170,6 +201,8 @@ class BlogPostView(ConceptView): yield self.childViewFactory(r, self.request, contextIsSecond=True) +# forms and form controllers + class EditBlogPostForm(EditConceptForm): title = _(u'Edit Blog Post') diff --git a/compound/blog/configure.zcml b/compound/blog/configure.zcml index 6c8673c..ec324b9 100644 --- a/compound/blog/configure.zcml +++ b/compound/blog/configure.zcml @@ -5,6 +5,19 @@ xmlns:browser="http://namespaces.zope.org/browser" i18n_domain="zope"> + + + + + + + + + + + + + +
+
+ +
+
+
@@ -7,9 +15,12 @@
+ +
+

- Post

Will Smith - + (Private)
@@ -63,7 +74,7 @@
Here comes the text...

Private Comment

+ + + diff --git a/compound/book/browser.py b/compound/book/browser.py index 6b55e5f..d71a89a 100644 --- a/compound/book/browser.py +++ b/compound/book/browser.py @@ -150,7 +150,7 @@ class PageLayout(Base, standard.Layout): def getParts(self): parts = ['headline', 'keyquestions', 'quote', 'maintext', - 'story', 'usecase'] + 'story', 'tip', 'usecase'] return self.getPartViews(parts) @@ -191,6 +191,11 @@ class Story(PagePart, standard.BasePart): partName = 'story' +class Tip(PagePart, standard.BasePart): + + partName = 'tip' + + class UseCase(PagePart, standard.BasePart): partName = 'usecase' diff --git a/compound/book/configure.zcml b/compound/book/configure.zcml index 9c5a628..4ff408f 100644 --- a/compound/book/configure.zcml +++ b/compound/book/configure.zcml @@ -75,6 +75,14 @@ factory="loops.compound.book.browser.Story" permission="zope.View" /> + + + + + + diff --git a/expert/browser/report.py b/expert/browser/report.py index aba8f14..2d27be3 100644 --- a/expert/browser/report.py +++ b/expert/browser/report.py @@ -165,12 +165,22 @@ class ResultsConceptView(ConceptView): def hasReportPredicate(self): return self.conceptManager['hasreport'] + @Lazy + def reportName(self): + return (self.getOptions('report_name') or [None])[0] + + @Lazy + def reportType(self): + return (self.getOptions('report_type') or [None])[0] + @Lazy def report(self): if self.reportName: return adapted(self.conceptManager[self.reportName]) - type = self.context.conceptType - reports = type.getParents([self.hasReportPredicate]) + reports = self.context.getParents([self.hasReportPredicate]) + if not reports: + type = self.context.conceptType + reports = type.getParents([self.hasReportPredicate]) return adapted(reports[0]) @Lazy diff --git a/expert/browser/search.pt b/expert/browser/search.pt index ac45dd4..2a83202 100644 --- a/expert/browser/search.pt +++ b/expert/browser/search.pt @@ -368,7 +368,8 @@ i18n:translate=""> - + + >> t = searchView.typesForSearch() >>> len(t) - 16 + 15 >>> t.getTermByToken('loops:resource:*').title 'Any Resource' >>> t = searchView.conceptTypesForSearch() >>> len(t) - 13 + 12 >>> t.getTermByToken('loops:concept:*').title 'Any Concept' @@ -91,7 +91,7 @@ a controller attribute for the search view. >>> searchView.submitReplacing('1.results', '1.search.form', pageView) 'submitReplacing("1.results", "1.search.form", - "http://127.0.0.1/loops/views/page/.target99/@@searchresults.html");...' + "http://127.0.0.1/loops/views/page/.target96/@@searchresults.html");...' Basic (text/title) search ------------------------- @@ -177,7 +177,7 @@ of the concepts' titles: >>> request = TestRequest(form=form) >>> view = Search(page, request) >>> view.listConcepts() - u"{identifier: 'id', items: [{label: 'Zope (Thema)', name: 'Zope', id: '104'}, {label: 'Zope 2 (Thema)', name: 'Zope 2', id: '106'}, {label: 'Zope 3 (Thema)', name: 'Zope 3', id: '108'}]}" + u"{identifier: 'id', items: [{label: 'Zope (Thema)', name: 'Zope', id: '101'}, {label: 'Zope 2 (Thema)', name: 'Zope 2', id: '103'}, {label: 'Zope 3 (Thema)', name: 'Zope 3', id: '105'}]}" Preset Concept Types on Search Forms ------------------------------------ @@ -219,13 +219,13 @@ and thus include the customer type in the preset search types. >>> searchView.conceptsForType('loops:concept:customer') [{'token': 'none', 'title': u'not selected'}, - {'token': '77', 'title': u'Customer 1'}, - {'token': '79', 'title': u'Customer 2'}, - {'token': '81', 'title': u'Customer 3'}] + {'token': '74', 'title': u'Customer 1'}, + {'token': '76', 'title': u'Customer 2'}, + {'token': '78', 'title': u'Customer 3'}] Let's use this new search option for querying: - >>> form = {'search.4.text_selected': u'77'} + >>> form = {'search.4.text_selected': u'74'} >>> resultsView = SearchResults(page, TestRequest(form=form)) >>> results = list(resultsView.results) >>> results[0].title diff --git a/expert/standard.py b/expert/standard.py new file mode 100644 index 0000000..4814019 --- /dev/null +++ b/expert/standard.py @@ -0,0 +1,54 @@ +# +# Copyright (c) 2013 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 +# + +""" +loops standard reports. +""" + +from zope.cachedescriptors.property import Lazy + +from cybertools.util.jeep import Jeep +from loops.browser.concept import ConceptView +from loops.expert.field import Field, TargetField, DateField, StateField, \ + TextField, HtmlTextField, UrlField +from loops.expert.report import ReportInstance + + +title = UrlField('title', u'Title', + description=u'A short descriptive text.', + executionSteps=['output']) + + +class TypeInstances(ReportInstance): + + fields = Jeep((title,)) + defaultOutputFields = fields + + @Lazy + def targets(self): + targetPredicate = self.view.conceptManager['querytarget'] + return self.view.context.getChildren([targetPredicate]) + + def selectObjects(self, parts): + result = [] + for t in self.targets: + for c in t.getChildren([self.view.typePredicate]): + result.append(c) + print '***', self.targets, result + return result + diff --git a/external/README.txt b/external/README.txt index cfbdcb8..bcc1cf5 100644 --- a/external/README.txt +++ b/external/README.txt @@ -17,7 +17,7 @@ Let's set up a loops site with basic and example concepts and resources. >>> concepts, resources, views = t.setup() >>> loopsRoot = site['loops'] >>> len(concepts), len(resources), len(views) - (34, 3, 1) + (33, 3, 1) Importing loops Objects @@ -44,7 +44,7 @@ Creating the corresponding objects >>> loader = Loader(loopsRoot) >>> loader.load(elements) >>> len(concepts), len(resources), len(views) - (35, 3, 1) + (34, 3, 1) >>> from loops.common import adapted >>> adMyquery = adapted(concepts['myquery']) @@ -131,7 +131,7 @@ Extracting elements >>> extractor = Extractor(loopsRoot, os.path.join(dataDirectory, 'export')) >>> elements = list(extractor.extract()) >>> len(elements) - 68 + 66 Writing object information to the external storage -------------------------------------------------- diff --git a/integrator/collection.py b/integrator/collection.py index 35d4a9c..167974b 100644 --- a/integrator/collection.py +++ b/integrator/collection.py @@ -197,6 +197,8 @@ class DirectoryCollectionProvider(object): extFileType = extFileTypes.get(contentType.split('/')[0] + '/*') if extFileType is None: extFileType = extFileTypes['*/*'] + if extFileType is None: + extFileType = extFileTypes['image/*'] if extFileType is None: getLogger('loops.integrator.collection.DirectoryCollectionProvider' ).warn('No external file type found for %r, ' diff --git a/interfaces.py b/interfaces.py index a552743..1330a6a 100644 --- a/interfaces.py +++ b/interfaces.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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,6 +30,7 @@ from zope.app.file.interfaces import IImage as IBaseAsset from zope.component.interfaces import IObjectEvent from zope.size.interfaces import ISized +from cybertools.composer.schema.interfaces import FieldType from cybertools.relation.interfaces import IDyadicRelation from cybertools.tracking.interfaces import ITrackingStorage from cybertools.util.format import toStr, toUnicode @@ -37,6 +38,11 @@ from loops import util from loops.util import _ +class HtmlText(schema.Text): + + __typeInfo__ = ('html',) + + # common interfaces class ILoopsObject(Interface): diff --git a/knowledge/README.txt b/knowledge/README.txt index 994b7ae..1617034 100644 --- a/knowledge/README.txt +++ b/knowledge/README.txt @@ -170,28 +170,7 @@ For testing, we first have to provide the needed utilities and settings Competence and Certification Management ======================================= - >>> from cybertools.stateful.interfaces import IStatesDefinition - >>> from loops.knowledge.qualification import qualificationStates - >>> from loops.knowledge.interfaces import IQualificationRecords - >>> from loops.knowledge.qualification import QualificationRecords - >>> component.provideUtility(qualificationStates, - ... provides=IStatesDefinition, - ... name='knowledge.qualification') - >>> component.provideAdapter(QualificationRecords, - ... provides=IQualificationRecords) - - >>> qurecs = loopsRoot.getRecordManager()['qualification'] - -We first create a training that provides knowledge in Python specials. - - >>> trainingPySpecC = concepts['trpyspec'] = Concept( - ... u'Python Specials Training') - >>> trainingPySpecC.assignParent(pySpecialsC) - -Then we record the need for John to acquire this knowledge. - - >>> from loops.knowledge.browser import CreateQualificationRecordForm - >>> from loops.knowledge.browser import CreateQualificationRecord + >>> tCompetence = concepts['competence'] Glossaries @@ -205,6 +184,15 @@ Glossary items are topic-like concepts that may be edited by end users. >>> from loops.knowledge.glossary.browser import EditGlossaryItem +Survey +====== + + >>> from loops.knowledge.tests import importSurvey + >>> importSurvey(loopsRoot) + + >>> from loops.knowledge.survey.browser import SurveyView + + Fin de partie ============= diff --git a/knowledge/browser.py b/knowledge/browser.py index fd9a8ed..6126661 100644 --- a/knowledge/browser.py +++ b/knowledge/browser.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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,10 +30,7 @@ from cybertools.typology.interfaces import IType from loops.browser.action import DialogAction from loops.browser.common import BaseView from loops.browser.concept import ConceptView -from loops.expert.browser.report import ResultsConceptView from loops.knowledge.interfaces import IPerson, ITask -from loops.knowledge.qualification import QualificationRecord -from loops.organize.work.browser import CreateWorkItemForm, CreateWorkItem from loops.organize.party import getPersonForUser from loops.util import _ @@ -50,6 +47,7 @@ actions.register('createTopic', 'portlet', DialogAction, typeToken='.loops/concepts/topic', fixedType=True, innerForm='inner_concept_form.html', + permission='loops.AssignAsParent', ) actions.register('editTopic', 'portlet', DialogAction, @@ -57,6 +55,7 @@ actions.register('editTopic', 'portlet', DialogAction, description=_(u'Modify topic.'), viewName='edit_concept.html', dialogName='editTopic', + permission='zope.ManageContent', ) actions.register('createQualification', 'portlet', DialogAction, @@ -66,6 +65,7 @@ actions.register('createQualification', 'portlet', DialogAction, dialogName='createQualification', prerequisites=['registerDojoDateWidget', 'registerDojoNumberWidget', 'registerDojoTextarea'], + permission='loops.AssignAsParent', ) @@ -111,25 +111,3 @@ class Candidates(ConceptView): return self.template.macros['requirement_candidates'] -# qualification stuff - -class PersonQualificationView(ResultsConceptView): - - pass - - -class CreateQualificationRecordForm(CreateWorkItemForm): - - macros = knowledge_macros - recordManagerName = 'qualification' - trackFactory = QualificationRecord - - @Lazy - def macro(self): - return self.macros['create_qualification'] - - -class CreateQualificationRecord(CreateWorkItem): - - pass - diff --git a/knowledge/configure.zcml b/knowledge/configure.zcml index da2bb30..50d67c4 100644 --- a/knowledge/configure.zcml +++ b/knowledge/configure.zcml @@ -1,5 +1,3 @@ - - - - - - - - - - - - - - - - - @@ -120,5 +89,7 @@ + + diff --git a/knowledge/data/loops_knowledge_de.dmp b/knowledge/data/knowledge_de.dmp similarity index 71% rename from knowledge/data/loops_knowledge_de.dmp rename to knowledge/data/knowledge_de.dmp index 3b2ce7b..d1a0e36 100644 --- a/knowledge/data/loops_knowledge_de.dmp +++ b/knowledge/data/knowledge_de.dmp @@ -1,5 +1,6 @@ type(u'competence', u'Kompetenz', viewName=u'', - typeInterface=u'', options=u'action.portlet:create_subtype,edit_concept') + typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence', + options=u'action.portlet:create_subtype,edit_concept') type(u'person', u'Person', viewName=u'', typeInterface=u'loops.knowledge.interfaces.IPerson', options=u'action.portlet:createQualification,editPerson') @@ -9,9 +10,9 @@ type(u'task', u'Aufgabe', viewName=u'', type(u'topic', u'Thema', viewName=u'', typeInterface=u'loops.knowledge.interfaces.ITopic', options=u'action.portlet:createTask,createTopic,editTopic') -type(u'training', u'Schulung', viewName=u'', - typeInterface=u'loops.organize.interfaces.ITask', - options=u'action.portlet:edit_concept') +#type(u'training', u'Schulung', viewName=u'', +# typeInterface=u'loops.organize.interfaces.ITask', +# options=u'action.portlet:edit_concept') concept(u'general', u'Allgemein', u'domain') concept(u'system', u'System', u'domain') @@ -34,11 +35,12 @@ child(u'general', u'provides', u'standard') child(u'general', u'requires', u'standard') child(u'general', u'task', u'standard') child(u'general', u'topic', u'standard') -child(u'general', u'training', u'standard') +#child(u'general', u'training', u'standard') child(u'system', u'issubtype', u'standard') -child(u'competence', u'training', u'issubtype', usePredicate=u'provides') +child(u'competence', u'competence', u'issubtype') +#child(u'competence', u'training', u'issubtype', usePredicate=u'provides') # records -records(u'qualification', u'loops.knowledge.qualification.QualificationRecord') +#records(u'qualification', u'loops.knowledge.qualification.base.QualificationRecord') diff --git a/knowledge/data/loops_knowledge_update_de.dmp b/knowledge/data/knowledge_update_de.dmp similarity index 71% rename from knowledge/data/loops_knowledge_update_de.dmp rename to knowledge/data/knowledge_update_de.dmp index 4080f43..403589f 100644 --- a/knowledge/data/loops_knowledge_update_de.dmp +++ b/knowledge/data/knowledge_update_de.dmp @@ -1,5 +1,6 @@ type(u'competence', u'Kompetenz', viewName=u'', - typeInterface=u'', options=u'action.portlet:create_subtype,edit_concept') + typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence', + options=u'action.portlet:create_subtype,edit_concept') # type(u'person', u'Person', viewName=u'', # typeInterface=u'loops.knowledge.interfaces.IPerson', # options=u'action.portlet:editPerson') @@ -9,9 +10,9 @@ type(u'competence', u'Kompetenz', viewName=u'', # type(u'topic', u'Thema', viewName=u'', # typeInterface=u'loops.knowledge.interfaces.ITopic', # options=u'action.portlet:createTask,createTopic,editTopic') -type(u'training', u'Schulung', viewName=u'', - typeInterface=u'loops.organize.interfaces.ITask', - options=u'action.portlet:edit_concept') +#type(u'training', u'Schulung', viewName=u'', +# typeInterface=u'loops.organize.interfaces.ITask', +# options=u'action.portlet:edit_concept') concept(u'general', u'Allgemein', u'domain') concept(u'system', u'System', u'domain') @@ -34,11 +35,12 @@ child(u'general', u'provides', u'standard') child(u'general', u'requires', u'standard') #child(u'general', u'task', u'standard') #child(u'general', u'topic', u'standard') -child(u'general', u'training', u'standard') +#child(u'general', u'training', u'standard') child(u'system', u'issubtype', u'standard') -child(u'competence', u'training', u'issubtype', usePredicate=u'provides') +child(u'competence', u'competence', u'issubtype') +#child(u'competence', u'training', u'issubtype', usePredicate=u'provides') # records -records(u'qualification', u'loops.knowledge.qualification.QualificationRecord') +#records(u'qualification', u'loops.knowledge.qualification.base.QualificationRecord') diff --git a/knowledge/data/survey_de.dmp b/knowledge/data/survey_de.dmp new file mode 100644 index 0000000..a094261 --- /dev/null +++ b/knowledge/data/survey_de.dmp @@ -0,0 +1,25 @@ +# survey types +type(u'questionnaire', u'Fragebogen', viewName=u'survey.html', + typeInterface=u'loops.knowledge.survey.interfaces.IQuestionnaire', + options=u'action.portlet:create_subtype,edit_concept') +type(u'questiongroup', u'Fragengruppe', viewName=u'', + typeInterface=u'loops.knowledge.survey.interfaces.IQuestionGroup', + options=u'action.portlet:create_subtype,edit_concept\nchildren_append\nshow_navigation') +type(u'question', u'Frage', viewName=u'', + typeInterface=u'loops.knowledge.survey.interfaces.IQuestion', + options=u'action.portlet:edit_concept\nshow_navigation') + #options=u'action.portlet:create_subtype,edit_concept') +type(u'feedbackitem', u'Feedback-Element', viewName=u'', + typeInterface=u'loops.knowledge.survey.interfaces.IFeedbackItem', + options=u'action.portlet:edit_concept\nshow_navigation') + +# subtypes +#child(u'questionnaire', u'questionnaire', u'issubtype') +#child(u'questionnaire', u'question', u'issubtype') +child(u'questionnaire', u'questiongroup', u'issubtype') +child(u'questiongroup', u'question', u'issubtype') +child(u'questiongroup', u'feedbackitem', u'issubtype') +#child(u'question', u'feedbackitem', u'issubtype') + +# records +records(u'survey_responses', u'loops.knowledge.survey.response.Response') diff --git a/knowledge/interfaces.py b/knowledge/interfaces.py index f1aef6b..5233653 100644 --- a/knowledge/interfaces.py +++ b/knowledge/interfaces.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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,18 +22,14 @@ Interfaces for knowledge management and elearning with loops. from zope.interface import Interface, Attribute from zope import interface, component, schema -from zope.i18nmessageid import MessageFactory -from zope.security.proxy import removeSecurityProxy from cybertools.knowledge.interfaces import IKnowing, IRequirementProfile from cybertools.knowledge.interfaces import IKnowledgeElement -from cybertools.organize.interfaces import IWorkItem, IWorkItems from loops.interfaces import IConceptSchema, ILoopsAdapter from loops.organize.interfaces import IPerson as IBasePerson from loops.organize.interfaces import ITask as IBaseTask from loops.schema.base import Relation, RelationSet - -_ = MessageFactory('loops') +from loops.util import _ class IPerson(IBasePerson, IKnowing): @@ -62,13 +58,3 @@ class ITopic(IConceptSchema, IKnowledgeElement, ILoopsAdapter): """ Just a topic, some general classification concept. """ - -class IQualificationRecord(IWorkItem): - """ Records needs for qualification (acqusition of competence) - and corresponding participations in training events etc. - """ - - -class IQualificationRecords(IWorkItems): - """ Container for qualification records. - """ diff --git a/knowledge/qualification.py b/knowledge/qualification.py deleted file mode 100644 index f123c39..0000000 --- a/knowledge/qualification.py +++ /dev/null @@ -1,103 +0,0 @@ -# -# Copyright (c) 2012 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 -# - -""" -Controlling qualification activities of persons. - -Central part of CCM competence and certification management framework. -""" - -from zope.component import adapts -from zope.interface import implementer, implements - -from cybertools.stateful.base import Stateful -from cybertools.stateful.definition import StatesDefinition -from cybertools.stateful.definition import State, Transition -from cybertools.stateful.interfaces import IStatesDefinition -from cybertools.tracking.interfaces import ITrackingStorage -from loops.knowledge.interfaces import IQualificationRecord, \ - IQualificationRecords -from loops.organize.work.base import WorkItem, WorkItems - - -@implementer(IStatesDefinition) -def qualificationStates(): - return StatesDefinition('qualification', - State('new', 'new', ('assign',), - color='grey'), - State('open', 'open', - ('register', - #'pass', 'fail', - 'cancel', 'modify'), - color='red'), - State('registered', 'registered', - ('register', 'pass', 'fail', 'unregister', 'cancel', 'modify'), - color='yellow'), - State('passed', 'passed', - ('cancel', 'close', 'modify', 'open', 'expire'), - color='green'), - State('failed', 'failed', - ('register', 'cancel', 'modify', 'open'), - color='green'), - State('expired', 'expired', - ('register', 'cancel', 'modify', 'open'), - color='red'), - State('cancelled', 'cancelled', ('modify', 'open'), - color='grey'), - State('closed', 'closed', ('modify', 'open'), - color='lightblue'), - # not directly reachable states: - State('open_x', 'open', ('modify',), color='red'), - State('registered_x', 'registered', ('modify',), color='yellow'), - # transitions: - Transition('assign', 'assign', 'open'), - Transition('register', 'register', 'registered'), - Transition('pass', 'pass', 'passed'), - Transition('fail', 'fail', 'failed'), - Transition('unregister', 'unregister', 'open'), - Transition('cancel', 'cancel', 'cancelled'), - Transition('modify', 'modify', 'open'), - Transition('close', 'close', 'closed'), - Transition('open', 'open', 'open'), - #initialState='open') - initialState='new') # TODO: handle assignment to competence - - -class QualificationRecord(WorkItem): - - implements(IQualificationRecord) - - typeName = 'QualificationRecord' - typeInterface = IQualificationRecord - statesDefinition = 'knowledge.qualification' - - def doAction(self, action, userName, **kw): - new = self.createNew(action, userName, **kw) - new.userName = self.userName - new.doTransition(action) - new.reindex() - return new - - -class QualificationRecords(WorkItems): - """ A tracking storage adapter managing qualification records. - """ - - implements(IQualificationRecords) - adapts(ITrackingStorage) - diff --git a/knowledge/qualification/__init__.py b/knowledge/qualification/__init__.py new file mode 100644 index 0000000..36a440f --- /dev/null +++ b/knowledge/qualification/__init__.py @@ -0,0 +1 @@ +'''package loops.knowledge.qualification''' \ No newline at end of file diff --git a/knowledge/qualification/base.py b/knowledge/qualification/base.py new file mode 100644 index 0000000..32c9910 --- /dev/null +++ b/knowledge/qualification/base.py @@ -0,0 +1,42 @@ +# +# Copyright (c) 2013 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 +# + +""" +Controlling qualification activities of persons. + +Central part of CCM competence and certification management framework. +""" + +from zope.component import adapts +from zope.interface import implementer, implements + +from loops.common import AdapterBase +from loops.knowledge.qualification.interfaces import ICompetence +from loops.type import TypeInterfaceSourceList + + +TypeInterfaceSourceList.typeInterfaces += (ICompetence,) + + +class Competence(AdapterBase): + + implements(ICompetence) + + _contextAttributes = list(ICompetence) + + diff --git a/knowledge/qualification/browser.py b/knowledge/qualification/browser.py new file mode 100644 index 0000000..29b53d6 --- /dev/null +++ b/knowledge/qualification/browser.py @@ -0,0 +1,36 @@ +# +# Copyright (c) 2013 Helmut Merz helmutm@cy55.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +Definition of view classes and other browser related stuff for the +loops.knowledge package. +""" + +from zope import interface, component +from zope.app.pagetemplate import ViewPageTemplateFile +from zope.cachedescriptors.property import Lazy + +from loops.expert.browser.report import ResultsConceptView +from loops.knowledge.browser import template, knowledge_macros +from loops.knowledge.qualification.base import QualificationRecord + + +class PersonQualificationView(ResultsConceptView): + + pass + diff --git a/knowledge/qualification/configure.zcml b/knowledge/qualification/configure.zcml new file mode 100644 index 0000000..d8cbc74 --- /dev/null +++ b/knowledge/qualification/configure.zcml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/knowledge/qualification/interfaces.py b/knowledge/qualification/interfaces.py new file mode 100644 index 0000000..85a002a --- /dev/null +++ b/knowledge/qualification/interfaces.py @@ -0,0 +1,44 @@ +# +# Copyright (c) 2013 Helmut Merz helmutm@cy55.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +Interfaces for knowledge management and elearning with loops. +""" + +from zope.interface import Interface, Attribute +from zope import interface, component, schema + +from loops.interfaces import IConceptSchema +from loops.util import _ + + +class ICompetence(IConceptSchema): + """ The competence of a person. + + Maybe assigned to the person via a 'knows' relation or + work items of type 'checkup'. + """ + + validityPeriod = schema.Int( + title=_(u'Validity Period (Months)'), + description=_(u'Number of months the competence remains valid. ' + u'Zero means unlimited validity.'), + default=0, + required=False) + + diff --git a/knowledge/survey/__init__.py b/knowledge/survey/__init__.py new file mode 100644 index 0000000..d227614 --- /dev/null +++ b/knowledge/survey/__init__.py @@ -0,0 +1 @@ +'''package loops.knowledge.survey''' diff --git a/knowledge/survey/base.py b/knowledge/survey/base.py new file mode 100644 index 0000000..cacdcc7 --- /dev/null +++ b/knowledge/survey/base.py @@ -0,0 +1,124 @@ +# +# Copyright (c) 2013 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 +# + +""" +Surveys used in knowledge management. +""" + +from zope.component import adapts +from zope.interface import implementer, implements + +from cybertools.knowledge.survey.questionnaire import Questionnaire, \ + QuestionGroup, Question, FeedbackItem +from loops.common import adapted, AdapterBase +from loops.knowledge.survey.interfaces import IQuestionnaire, \ + IQuestionGroup, IQuestion, IFeedbackItem +from loops.type import TypeInterfaceSourceList + + +TypeInterfaceSourceList.typeInterfaces += (IQuestionnaire, + IQuestionGroup, IQuestion, IFeedbackItem) + + +class Questionnaire(AdapterBase, Questionnaire): + + implements(IQuestionnaire) + + _contextAttributes = list(IQuestionnaire) + _adapterAttributes = AdapterBase._adapterAttributes + ( + 'questionGroups', 'questions', 'responses',) + _noexportAttributes = _adapterAttributes + + @property + def questionGroups(self): + return [adapted(c) for c in self.context.getChildren()] + + @property + def questions(self): + for qug in self.questionGroups: + for qu in qug.questions: + #qu.questionnaire = self + yield qu + + +class QuestionGroup(AdapterBase, QuestionGroup): + + implements(IQuestionGroup) + + _contextAttributes = list(IQuestionGroup) + _adapterAttributes = AdapterBase._adapterAttributes + ( + 'questionnaire', 'questions', 'feedbackItems') + _noexportAttributes = _adapterAttributes + + @property + def questionnaire(self): + for p in self.context.getParents(): + ap = adapted(p) + if IQuestionnaire.providedBy(ap): + return ap + + @property + def subobjects(self): + return [adapted(c) for c in self.context.getChildren()] + + @property + def questions(self): + return [obj for obj in self.subobjects if IQuestion.providedBy(obj)] + + @property + def feedbackItems(self): + return [obj for obj in self.subobjects if IFeedbackItem.providedBy(obj)] + + +class Question(AdapterBase, Question): + + implements(IQuestion) + + _contextAttributes = list(IQuestion) + _adapterAttributes = AdapterBase._adapterAttributes + ( + 'text', 'questionnaire', 'answerRange', 'feedbackItems',) + _noexportAttributes = _adapterAttributes + + @property + def text(self): + return self.context.description + + @property + def questionGroup(self): + for p in self.context.getParents(): + ap = adapted(p) + if IQuestionGroup.providedBy(ap): + return ap + + @property + def questionnaire(self): + return self.questionGroup.questionnaire + + +class FeedbackItem(AdapterBase, FeedbackItem): + + implements(IFeedbackItem) + + _contextAttributes = list(IFeedbackItem) + _adapterAttributes = AdapterBase._adapterAttributes + ( + 'text',) + _noexportAttributes = _adapterAttributes + + @property + def text(self): + return self.context.description diff --git a/knowledge/survey/browser.py b/knowledge/survey/browser.py new file mode 100644 index 0000000..6b6c767 --- /dev/null +++ b/knowledge/survey/browser.py @@ -0,0 +1,174 @@ +# +# Copyright (c) 2013 Helmut Merz helmutm@cy55.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +Definition of view classes and other browser related stuff for +surveys and self-assessments. +""" + +import csv +from cStringIO import StringIO +from zope.app.pagetemplate import ViewPageTemplateFile +from zope.cachedescriptors.property import Lazy +from zope.i18n import translate + +from cybertools.knowledge.survey.questionnaire import Response +from cybertools.util.date import formatTimeStamp +from loops.browser.concept import ConceptView +from loops.browser.node import NodeView +from loops.common import adapted +from loops.knowledge.survey.response import Responses +from loops.organize.party import getPersonForUser +from loops.util import getObjectForUid +from loops.util import _ + + +template = ViewPageTemplateFile('view_macros.pt') + +class SurveyView(ConceptView): + + tabview = 'index.html' + data = None + errors = None + + @Lazy + def macro(self): + return template.macros['survey'] + + def results(self): + result = [] + response = None + form = self.request.form + if 'submit' in form: + self.data = {} + response = Response(self.adapted, None) + for key, value in form.items(): + if key.startswith('question_'): + uid = key[len('question_'):] + question = adapted(self.getObjectForUid(uid)) + if value != 'none': + value = int(value) + self.data[uid] = value + response.values[question] = value + Responses(self.context).save(self.data) + self.errors = self.check(response) + if self.errors: + return [] + if response is not None: + result = response.getGroupedResult() + return [dict(category=r[0].title, text=r[1].text, + score=int(round(r[2] * 100))) + for r in result] + + def check(self, response): + errors = [] + values = response.values + for qu in self.adapted.questions: + if qu.required and qu not in values: + errors.append('Please answer the obligatory questions.') + break + qugroups = {} + for qugroup in self.adapted.questionGroups: + qugroups[qugroup] = 0 + for qu in values: + qugroups[qu.questionGroup] += 1 + for qugroup, count in qugroups.items(): + minAnswers = qugroup.minAnswers + if minAnswers in (u'', None): + minAnswers = len(qugroup.questions) + if count < minAnswers: + errors.append('Please answer the minimum number of questions.') + break + return errors + + def getInfoText(self, qugroup): + lang = self.languageInfo.language + text = qugroup.description + info = None + if qugroup.minAnswers in (u'', None): + info = translate(_(u'Please answer all questions.'), target_language=lang) + elif qugroup.minAnswers > 0: + info = translate(_(u'Please answer at least $minAnswers questions.', + mapping=dict(minAnswers=qugroup.minAnswers)), + target_language=lang) + if info: + text = u'%s
(%s)' % (text, info) + return text + + def getValues(self, question): + setting = None + if self.data is None: + self.data = Responses(self.context).load() + if self.data: + setting = self.data.get(question.uid) + noAnswer = [dict(value='none', checked=(setting == None), + radio=(not question.required))] + return noAnswer + [dict(value=i, checked=(setting == i), radio=True) + for i in reversed(range(question.answerRange))] + + +class SurveyCsvExport(NodeView): + + encoding = 'ISO8859-15' + + def encode(self, text): + text.encode(self.encoding) + + @Lazy + def questions(self): + result = [] + for idx1, qug in enumerate(adapted(self.virtualTargetObject).questionGroups): + for idx2, qu in enumerate(qug.questions): + result.append((idx1, idx2, qug, qu)) + return result + + @Lazy + def columns(self): + infoCols = ['Name', 'Timestamp'] + dataCols = ['%02i-%02i' % (item[0], item[1]) for item in self.questions] + return infoCols + dataCols + + def getRows(self): + for tr in Responses(self.virtualTargetObject).getAllTracks(): + p = adapted(getObjectForUid(tr.userName)) + name = p and p.title or u'???' + ts = formatTimeStamp(tr.timeStamp) + cells = [tr.data.get(qu.uid, -1) + for (idx1, idx2, qug, qu) in self.questions] + yield [name, ts] + cells + + def __call__(self): + f = StringIO() + writer = csv.writer(f, delimiter=',') + writer.writerow(self.columns) + for row in self.getRows(): + writer.writerow(row) + text = f.getvalue() + self.setDownloadHeader(text) + return text + + def setDownloadHeader(self, text): + response = self.request.response + filename = 'survey_data.csv' + response.setHeader('Content-Disposition', + 'attachment; filename=%s' % filename) + response.setHeader('Cache-Control', '') + response.setHeader('Pragma', '') + response.setHeader('Content-Length', len(text)) + response.setHeader('Content-Type', 'text/csv') + diff --git a/knowledge/survey/configure.zcml b/knowledge/survey/configure.zcml new file mode 100644 index 0000000..423889d --- /dev/null +++ b/knowledge/survey/configure.zcml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/knowledge/survey/interfaces.py b/knowledge/survey/interfaces.py new file mode 100644 index 0000000..7ce5849 --- /dev/null +++ b/knowledge/survey/interfaces.py @@ -0,0 +1,93 @@ +# +# Copyright (c) 2013 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 surveys used in knowledge management. +""" + +from zope.interface import Interface, Attribute +from zope import interface, component, schema + +from cybertools.knowledge.survey import interfaces +from loops.interfaces import IConceptSchema, ILoopsAdapter +from loops.util import _ + + +class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire): + """ A collection of questions for setting up a survey. + """ + + defaultAnswerRange = schema.Int( + title=_(u'Answer Range'), + description=_(u'Number of items (answer options) to select from.'), + default=4, + required=True) + + feedbackFooter = schema.Text( + title=_(u'Feedback Footer'), + description=_(u'Text that will appear at the end of the feedback page.'), + default=u'', + missing_value=u'', + required=False) + + +class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup): + """ A group of questions within a questionnaire. + """ + + minAnswers = schema.Int( + title=_(u'Minimum Number of Answers'), + description=_(u'Minumum number of questions that have to be answered. ' + 'Empty means all questions have to be answered.'), + default=None, + required=False) + + +class IQuestion(IConceptSchema, interfaces.IQuestion): + """ A single question within a questionnaire. + """ + + required = schema.Bool( + title=_(u'Required'), + description=_(u'Question must be answered.'), + default=False, + required=False) + + revertAnswerOptions = schema.Bool( + title=_(u'Negative'), + description=_(u'Value inversion: High selection means low value.'), + default=False, + required=False) + + +class IFeedbackItem(IConceptSchema, interfaces.IFeedbackItem): + """ Some text (e.g. a recommendation) or some other kind of information + that may be deduced from the res)ponses to a questionnaire. + """ + + +class IResponse(interfaces.IResponse): + """ A set of response values given to the questions of a questionnaire + by a single person or party. + """ + + +class IResponses(Interface): + """ A container or manager of survey responses. + """ + diff --git a/knowledge/survey/response.py b/knowledge/survey/response.py new file mode 100644 index 0000000..b27979e --- /dev/null +++ b/knowledge/survey/response.py @@ -0,0 +1,63 @@ +# +# Copyright (c) 2013 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 +# + +""" +Handling survey responses. +""" + +from zope.component import adapts +from zope.interface import implements + +from cybertools.tracking.btree import Track +from cybertools.tracking.interfaces import ITrackingStorage +from loops.knowledge.survey.interfaces import IResponse, IResponses +from loops.organize.tracking.base import BaseRecordManager + + +class Responses(BaseRecordManager): + + implements(IResponses) + + storageName = 'survey_responses' + + def __init__(self, context): + self.context = context + + def save(self, data): + if not self.personId: + return + self.storage.saveUserTrack(self.uid, 0, self.personId, data, + update=True, overwrite=True) + + def load(self): + if self.personId: + tracks = self.storage.getUserTracks(self.uid, 0, self.personId) + if tracks: + return tracks[0].data + return {} + + def getAllTracks(self): + return self.storage.query(taskId=self.uid) + + +class Response(Track): + + implements(IResponse) + + typeName = 'Response' + diff --git a/knowledge/survey/view_macros.pt b/knowledge/survey/view_macros.pt new file mode 100644 index 0000000..49731dc --- /dev/null +++ b/knowledge/survey/view_macros.pt @@ -0,0 +1,93 @@ + + + + + + + + + +
+

Feedback

+ + + + + + + + +
CategoryResponse%
+ + +
+ +
+
+
+

Questionnaire

+
+
+ +
+
+
+ + + + + + + + + + + + + +
 
+ +
+ +
  +
+
No answerFully appliesDoes not apply
+ + + *** + +
+ +
+
+ + + + diff --git a/knowledge/tests.py b/knowledge/tests.py index baa95b0..8340a79 100755 --- a/knowledge/tests.py +++ b/knowledge/tests.py @@ -2,16 +2,31 @@ import os import unittest, doctest -from zope.testing.doctestunit import DocFileSuite from zope.app.testing import ztapi +from zope import component from zope.interface.verify import verifyClass +from zope.testing.doctestunit import DocFileSuite + +from loops.knowledge.qualification.base import Competence +from loops.knowledge.survey.base import Questionnaire, Question, FeedbackItem +from loops.knowledge.survey.interfaces import IQuestionnaire, IQuestion, \ + IFeedbackItem from loops.organize.party import Person from loops.setup import importData as baseImportData +importPath = os.path.join(os.path.dirname(__file__), 'data') + + def importData(loopsRoot): - importPath = os.path.join(os.path.dirname(__file__), 'data') - baseImportData(loopsRoot, importPath, 'loops_knowledge_de.dmp') + baseImportData(loopsRoot, importPath, 'knowledge_de.dmp') + +def importSurvey(loopsRoot): + component.provideAdapter(Competence) + component.provideAdapter(Questionnaire, provides=IQuestionnaire) + component.provideAdapter(Question, provides=IQuestion) + component.provideAdapter(FeedbackItem, provides=IFeedbackItem) + baseImportData(loopsRoot, importPath, 'survey_de.dmp') class Test(unittest.TestCase): diff --git a/locales/de/LC_MESSAGES/loops.mo b/locales/de/LC_MESSAGES/loops.mo index d079c1e..4520427 100644 Binary files a/locales/de/LC_MESSAGES/loops.mo and b/locales/de/LC_MESSAGES/loops.mo differ diff --git a/locales/de/LC_MESSAGES/loops.po b/locales/de/LC_MESSAGES/loops.po index 282a6e4..92ac3cf 100644 --- a/locales/de/LC_MESSAGES/loops.po +++ b/locales/de/LC_MESSAGES/loops.po @@ -1,9 +1,9 @@ msgid "" msgstr "" -"Project-Id-Version: $Id$\n" +"Project-Id-Version: 0.13.0\n" "POT-Creation-Date: 2007-05-22 12:00 CET\n" -"PO-Revision-Date: 2012-09-17 12:00 CET\n" +"PO-Revision-Date: 2013-03-21 12:00 CET\n" "Last-Translator: Helmut Merz \n" "Language-Team: loops developers \n" "MIME-Version: 1.0\n" @@ -170,6 +170,105 @@ msgstr "Glossareintrag anlegen..." msgid "Create Glossary Item" msgstr "Glossareintrag anlegen." +# survey / questionnaire + +msgid "Answer Range" +msgstr "Abstufung Bewertungen" + +msgid "Feedback Footer" +msgstr "Auswertungs-Hinweis" + +msgid "Text that will appear at the end of the feedback page." +msgstr "Text, der am Ende der Auswertungsseite erscheinen soll." + +msgid "Number of items (answer options) to select from." +msgstr "Anzahl der Abstufungen, aus denen bei der Antwort gewählt werden kann." + +msgid "Minimum Number of Answers" +msgstr "Mindestanzahl an Antworten" + +msgid "Minumum number of questions that have to be answered. Empty means all questions have to be answered." +msgstr "Anzahl der Fragen, die mindestens beantwortet werden müssen. Keine Angabe: Es müssen alle Fragen beantwortet werden." + +msgid "Required" +msgstr "Pflichtfrage" + +msgid "Question must be answered." +msgstr "Frage muss unbedingt beantwortet werden." + +msgid "Negative" +msgstr "Negative Polarität" + +msgid "Value inversion: High selection means low value." +msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert." + +msgid "Questionnaire" +msgstr "Fragebogen" + +msgid "Feedback" +msgstr "Auswertung" + +msgid "Category" +msgstr "Kategorie" + +msgid "Response" +msgstr "Beurteilung" + +msgid "No answer" +msgstr "Keine Antwort" + +msgid "Does not apply" +msgstr "Trifft nicht zu" + +msgid "Fully applies" +msgstr "Trifft voll zu" + +msgid "survey_value_none" +msgstr "Keine Antwort" + +msgid "survey_value_0" +msgstr "Trifft für unser Unternehmen überhaupt nicht zu" + +msgid "survey_value_1" +msgstr "Trifft eher nicht zu" + +msgid "survey_value_2" +msgstr "Trifft eher zu" + +msgid "survey_value_3" +msgstr "Trifft für unser Unternehmen voll und ganz zu" + +msgid "Evaluate Questionnaire" +msgstr "Fragebogen auswerten" + +msgid "Back to Questionnaire" +msgstr "Zurück zum Fragebogen" + +msgid "Please answer at least $minAnswers questions." +msgstr "Bitte beantworten Sie mindestens $minAnswers Fragen." + +msgid "Please answer all questions." +msgstr "Bitte beantworten Sie alle Fragen." + +msgid "Please answer the obligatory questions." +msgstr "Bitte beantworten Sie die Pflichtfragen." + +msgid "Please answer the minimum number of questions." +msgstr "Bitte beantworten Sie die angegebene Mindestanzahl an Fragen je Fragengruppe." + +msgid "Obligatory question, must be answered" +msgstr "Pflichtfrage, muss beantwortet werden" + +# competence (qualification) + +msgid "Validity Period (Months)" +msgstr "Gültigkeitszeitraum (Monate)" + +msgid "Number of months the competence remains valid. Zero means unlimited validity." +msgstr "Anzahl der Monate, die diese Kompetenz gültig bleibt. Null bedeutet unbegrenzte Gültigkeit" + +# organize + msgid "Create Person..." msgstr "Person anlegen..." @@ -212,6 +311,8 @@ msgstr "Adresse bearbeiten..." msgid "Modify address." msgstr "Adresse bearbeiten." +# general + msgid "Create Concept, Type = " msgstr "Begriff anlegen, Typ = " @@ -230,6 +331,8 @@ msgstr "Diese Ressource bearbeiten." msgid "Edit Concept" msgstr "Begriff bearbeiten" +# events and tasks + msgid "Create Event..." msgstr "Termin anlegen..." @@ -443,9 +546,15 @@ msgstr "Überschrift, sprechende Bezeichnung des Begriffs" msgid "Description" msgstr "Beschreibung" +msgid "label_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 "desc_description" +msgstr "Eine ausführlichere Beschreibung des Inhalts und Zwecks des Objekts" + msgid "Related Items" msgstr "Verwandte Begriffe" @@ -707,9 +816,6 @@ msgstr "Deckungsgrad" msgid "Create loops Note" msgstr "loops-Notiz anlegen" -msgid "State information for $definition: $title" -msgstr "Status ($definition): $title" - msgid "User ID" msgstr "Benutzerkennung" @@ -886,65 +992,178 @@ msgstr "an" msgid "move_to_task" msgstr "nach" +# state definitions + +msgid "Show all" +msgstr "Alle anzeigen" + +msgid "Restrict to objects with certain states" +msgstr "Auf Objekte mit bestimmtem Status beschränken" + +msgid "Workflow" +msgstr "Statusdefinition/Workflow" + +msgid "States" +msgstr "Statuswerte" + +msgid "State information for $definition: $title" +msgstr "Status ($definition): $title" + +msgid "classification_quality" +msgstr "Klassifizierung" + +msgid "simple_publishing" +msgstr "Veröffentlichung" + +msgid "task_states" +msgstr "Aufgabe" + +msgid "publishable_task" +msgstr "Aufgabe/Zugriff" + +# state names + +msgid "accepted" +msgstr "angenommen" + +msgid "active" +msgstr "aktiv" + +msgid "active (published)" +msgstr "aktiv (zugänglich)" + +msgid "archived" +msgstr "Archiv" + +msgid "cancelled" +msgstr "abgebrochen" + +msgid "classified" +msgstr "klassifiziert" + +msgid "closed" +msgstr "abgeschlossen" + +msgid "delegated" +msgstr "delegiert" + +msgid "done" +msgstr "bearbeitet" + +msgid "draft" +msgstr "Entwurf" + +msgid "finished" +msgstr "erledigt" + +msgid "finished (published)" +msgstr "erledigt (zugänglich)" + +msgid "moved" +msgstr "verschoben" + msgid "new" msgstr "neu" msgid "planned" msgstr "geplant" -msgid "accepted" -msgstr "angenommen" +msgid "private" +msgstr "privat" -msgid "delegated" -msgstr "delegiert" +msgid "published" +msgstr "veröffentlicht" -msgid "running" -msgstr "in Arbeit" - -msgid "done" -msgstr "bearbeitet" - -msgid "finished" -msgstr "beendet" - -msgid "closed" -msgstr "abgeschlossen" - -msgid "cancelled" -msgstr "abgebrochen" - -msgid "moved" -msgstr "verschoben" +msgid "removed" +msgstr "gelöscht" msgid "replaced" msgstr "ersetzt" -msgid "plan" -msgstr "planen" +msgid "running" +msgstr "in Arbeit" + +msgid "unclassified" +msgstr "unklassifiziert" + +msgid "verified" +msgstr "verifiziert" + +# transitions msgid "accept" msgstr "annehmen" -msgid "start working" -msgstr "Arbeit beginnen" +msgid "archive" +msgstr "archivieren" -msgid "work" -msgstr "bearbeiten" +msgid "classify" +msgstr "klassifizieren" -msgid "finish" -msgstr "beenden" +msgid "change_classification" +msgstr "Klassifizierung ändern" msgid "cancel" msgstr "abbrechen" +msgid "close" +msgstr "abschließen" + msgid "delegate" msgstr "delegieren" +msgid "finish" +msgstr "beenden" + +msgid "finish (published)" +msgstr "beenden (zugänglich)" + +msgid "hide" +msgstr "verstecken" + msgid "move" msgstr "verschieben" -msgid "close" -msgstr "abschließen" +msgid "No change" +msgstr "keine Änderung" + +msgid "plan" +msgstr "planen" + +msgid "publish" +msgstr "veröffentlichen" + +msgid "remove" +msgstr "entfernen" + +msgid "release" +msgstr "freigeben" + +msgid "release, publish" +msgstr "freigeben (zugänglich)" + +msgid "re-open" +msgstr "zurücksetzen" + +msgid "remove_classification" +msgstr "Klassifizierung entfernen" + +msgid "retract" +msgstr "einschränken" + +msgid "show" +msgstr "anzeigen" + +msgid "start working" +msgstr "Arbeit beginnen" + +msgid "verify" +msgstr "verifizieren" + +msgid "work" +msgstr "bearbeiten" + +# calendar msgid "Monday" msgstr "Montag" diff --git a/organize/README.txt b/organize/README.txt index 349eb44..f1742df 100644 --- a/organize/README.txt +++ b/organize/README.txt @@ -2,8 +2,6 @@ loops - Linked Objects for Organization and Processing Services =============================================================== - ($Id$) - Note: This packages depends on cybertools.organize. Let's do some basic setup @@ -267,9 +265,9 @@ Person objects that have a user assigned to them receive this user >>> from zope.securitypolicy.interfaces import IPrincipalRoleMap >>> IPrincipalRoleMap(concepts['john']).getPrincipalsAndRoles() - [('loops.Owner', 'users.john', PermissionSetting: Allow)] + [('loops.Person', 'users.john', PermissionSetting: Allow)] >>> IPrincipalRoleMap(concepts['person.newuser']).getPrincipalsAndRoles() - [('loops.Owner', u'loops.newuser', PermissionSetting: Allow)] + [('loops.Person', u'loops.newuser', PermissionSetting: Allow)] The person ``martha`` hasn't got a user id, so there is no role assigned to it. @@ -307,9 +305,12 @@ Now we are ready to look for the real stuff - what John is allowed to do. True Person objects that have an owner may be modified by this owner. +(Changed in 2013-01-14: Owner not set automatically) >>> canWrite(john, 'title') - True + False + +was: True So let's try with another user with another role setting. @@ -409,7 +410,7 @@ Send Email to Members >>> form.subject u"loops Notification from '$site'" >>> form.mailBody - u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.116\n\n' + u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.113\n\n' Show Presence of Other Users diff --git a/organize/browser/event.py b/organize/browser/event.py index 90f0f76..8f43548 100644 --- a/organize/browser/event.py +++ b/organize/browser/event.py @@ -56,6 +56,7 @@ actions.register('createEvent', 'portlet', DialogAction, typeToken='.loops/concepts/event', fixedType=True, prerequisites=['registerDojoDateWidget'], + permission='loops.AssignAsParent', ) actions.register('editEvent', 'portlet', DialogAction, @@ -383,8 +384,9 @@ class CreateFollowUpEvent(CreateConcept, BaseFollowUpController): bevt = baseObject(self.baseEvent) bevt.assignChild(obj, self.followsPredicate) for rel in bevt.getParentRelations(): - if rel.predicate != self.view.typePredicate: - obj.assignParent(rel.first, rel.predicate) + if rel.predicate not in (self.view.typePredicate, self.followsPredicate): + obj.assignParent(rel.first, rel.predicate, + order=rel.order, relevance=rel.relevance) class EditFollowUpEvent(EditConcept, BaseFollowUpController): diff --git a/organize/interfaces.py b/organize/interfaces.py index 4dd3aa0..3581128 100644 --- a/organize/interfaces.py +++ b/organize/interfaces.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -31,6 +31,7 @@ from cybertools.organize.interfaces import IAddress as IBaseAddress from cybertools.organize.interfaces import IPerson as IBasePerson from cybertools.organize.interfaces import ITask from loops.interfaces import ILoopsAdapter, IConceptSchema, IRelationAdapter +from loops.interfaces import HtmlText from loops.organize.util import getPrincipalFolder from loops import util from loops.util import _ @@ -173,26 +174,36 @@ class IEvent(ITask): class IAgendaItem(ILoopsAdapter): + description = HtmlText( + title=_(u'label_description'), + description=_(u'desc_description'), + default=u'', + missing_value=u'', + required=False) + responsible = schema.TextLine( title=_(u'label_responsible'), - description=_(u'desc_responsible.'), + description=_(u'desc_responsible'), default=u'', required=False) - discussion = schema.Text( + discussion = HtmlText( title=_(u'label_discussion'), - description=_(u'desc_discussion.'), + description=_(u'desc_discussion'), default=u'', missing_value=u'', required=False) - consequences = schema.Text( + consequences = HtmlText( title=_(u'label_consequences'), - description=_(u'desc_consequences.'), + description=_(u'desc_consequences'), default=u'', missing_value=u'', required=False) + description.height = 10 + discussion.height = consequences.height = 7 + # 'hasrole' predicate diff --git a/organize/party.py b/organize/party.py index 88d5b9b..bac9fb0 100644 --- a/organize/party.py +++ b/organize/party.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -41,9 +41,11 @@ from loops.interfaces import IConcept from loops.organize.interfaces import IAddress, IPerson, IHasRole from loops.organize.interfaces import ANNOTATION_KEY from loops.predicate import RelationAdapter -from loops.security.common import assignOwner, removeOwner, allowEditingForOwner -from loops.type import TypeInterfaceSourceList from loops.predicate import PredicateInterfaceSourceList +from loops.security.common import assignOwner, removeOwner, allowEditingForOwner +from loops.security.common import assignPersonRole, removePersonRole +from loops.security.interfaces import ISecuritySetter +from loops.type import TypeInterfaceSourceList from loops import util @@ -82,6 +84,7 @@ class Person(AdapterBase, BasePerson): def getUserId(self): return getattr(self.context, '_userId', None) def setUserId(self, userId): + setter = ISecuritySetter(self) if userId: principal = self.getPrincipalForUserId(userId) if principal is None: @@ -97,13 +100,16 @@ class Person(AdapterBase, BasePerson): if ann is None: # or not isinstance(ann, PersistentMapping): ann = pa[ANNOTATION_KEY] = PersistentMapping() ann[loopsId] = self.context - assignOwner(self.context, userId) + #assignOwner(self.context, userId) + assignPersonRole(self.context, userId) oldUserId = self.userId if oldUserId and oldUserId != userId: self.removeReferenceFromPrincipal(oldUserId) removeOwner(self.context, oldUserId) + removePersonRole(self.context, oldUserId) self.context._userId = userId - allowEditingForOwner(self.context, revert=not userId) + setter.propagateSecurity() + allowEditingForOwner(self.context, revert=not userId) # why this? userId = property(getUserId, setUserId) def removeReferenceFromPrincipal(self, userId): diff --git a/organize/personal/browser/filter.py b/organize/personal/browser/filter.py index 38d21d9..cedf846 100644 --- a/organize/personal/browser/filter.py +++ b/organize/personal/browser/filter.py @@ -27,6 +27,7 @@ from zope.app.pagetemplate import ViewPageTemplateFile from zope.cachedescriptors.property import Lazy from cybertools.browser.configurator import ViewConfigurator, MacroViewProperty +from cybertools.stateful.interfaces import IStateful from loops.browser.node import NodeView from loops.concept import Concept from loops.organize.party import getPersonForUser @@ -107,7 +108,30 @@ class FilterView(NodeView): result.setdefault(obj.getType(), set([])).add(obj) return result - def check(self, obj): + def checkOptions(self, obj, options): + if isinstance(options, list): + return True + form = self.request.form + if form.get('filter.states') == 'all': + return True + filterStates = options + for std in filterStates.keys(): + formStates = form.get('filter.states.' + std) + if formStates == 'all': + continue + stf = component.getAdapter(obj, IStateful, name=std) + if formStates: + if stf.state not in formStates.split(','): + return False + else: + if stf.state not in getattr(filterStates, std): + return False + return True + + def check(self, obj, options=None): + if options is not None: + if not self.checkOptions(obj, options): + return False fs = self.filterStructure if not fs: return True diff --git a/organize/stateful/README.txt b/organize/stateful/README.txt index e6445eb..11182e4 100644 --- a/organize/stateful/README.txt +++ b/organize/stateful/README.txt @@ -2,8 +2,6 @@ loops - Linked Objects for Organization and Processing Services =============================================================== - ($Id$) - >>> from zope import component >>> from zope.traversing.api import getName @@ -183,6 +181,12 @@ Querying objects by state [<...>] +Task States +=========== + + >>> from loops.organize.stateful.task import taskStates, publishableTask + + Fin de partie ============= diff --git a/organize/stateful/browser.py b/organize/stateful/browser.py index 1e4c49b..ae57c87 100644 --- a/organize/stateful/browser.py +++ b/organize/stateful/browser.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2012 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 @@ -18,13 +18,12 @@ """ Views and actions for states management. - -$Id$ """ from zope import component from zope.app.pagetemplate import ViewPageTemplateFile from zope.cachedescriptors.property import Lazy +from zope.i18n import translate from cybertools.browser.action import Action, actions from cybertools.stateful.interfaces import IStateful, IStatesDefinition @@ -36,9 +35,12 @@ from loops.security.common import checkPermission from loops.util import _ +template = ViewPageTemplateFile('view_macros.pt') + statefulActions = ('classification_quality', 'simple_publishing', - 'task_states',) + 'task_states', + 'publishable_task',) class StateAction(Action): @@ -53,9 +55,11 @@ class StateAction(Action): @Lazy def description(self): + lang = self.view.languageInfo.language + definition = translate(_(self.definition), target_language=lang) + title = translate(_(self.stateObject.title), target_language=lang) return _(u'State information for $definition: $title', - mapping=dict(definition=self.definition, - title=self.stateObject.title)) + mapping=dict(definition=definition, title=title)) @Lazy def stateObject(self): @@ -77,7 +81,7 @@ for std in statefulActions: #class StateQuery(ConceptView): class StateQuery(BaseView): - template = ViewPageTemplateFile('view_macros.pt') + template = template form_action = 'execute_search_action' @@ -144,3 +148,15 @@ class StateQuery(BaseView): uids = q.apply() return self.viewIterator(getObjects(uids, self.loopsRoot)) return [] + + +class FilterAllStates(BaseView): + + @Lazy + def macros(self): + return template.macros + + @Lazy + def macro(self): + return self.macros['filter_allstates'] + diff --git a/organize/stateful/configure.zcml b/organize/stateful/configure.zcml index e5cd542..7833208 100644 --- a/organize/stateful/configure.zcml +++ b/organize/stateful/configure.zcml @@ -27,7 +27,7 @@ + name="simple_publishing" /> @@ -41,7 +41,7 @@ + name="task_states" /> @@ -49,13 +49,27 @@ set_schema="cybertools.stateful.interfaces.IStateful" /> + + + + + + + + + name="classification_quality" /> @@ -71,6 +85,12 @@ class="loops.organize.stateful.browser.StateQuery" permission="zope.View" /> + + diff --git a/organize/stateful/task.py b/organize/stateful/task.py index 7a99273..0721bc4 100644 --- a/organize/stateful/task.py +++ b/organize/stateful/task.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -20,6 +20,7 @@ Basic implementations for stateful objects and adapters. """ +from zope.app.security.settings import Allow, Deny, Unset from zope import component from zope.component import adapter from zope.interface import implementer @@ -30,24 +31,107 @@ from cybertools.stateful.definition import State, Transition from cybertools.stateful.interfaces import IStatesDefinition, IStateful from loops.common import adapted from loops.organize.stateful.base import StatefulLoopsObject +from loops.security.interfaces import ISecuritySetter + + +def setPermissionsForRoles(settings): + def setSecurity(obj): + setter = ISecuritySetter(obj.context) + setter.setRolePermissions(settings) + setter.propagateSecurity() + return setSecurity @implementer(IStatesDefinition) def taskStates(): return StatesDefinition('task_states', - State('planned', 'planned', ('finish', 'cancel'), + State('draft', 'draft', ('release', 'cancel',), + color='blue'), + State('active', 'active', ('finish', 'cancel',), color='yellow'), - State('finished', 'finished', ('reopen'), + State('finished', 'finished', ('reopen', 'archive',), color='green'), - State('cancelled', 'cancelled', ('reopen'), + State('cancelled', 'cancelled', ('reopen',), + color='x'), + State('archived', 'archived', ('reopen',), color='grey'), + Transition('release', 'release', 'active'), Transition('finish', 'finish', 'finished'), Transition('cancel', 'cancel', 'cancelled'), - Transition('reopen', 're-open', 'planned'), - initialState='planned') + Transition('reopen', 're-open', 'draft'), + initialState='draft') + + +@implementer(IStatesDefinition) +def publishableTask(): + return StatesDefinition('publishable_task', + State('draft', 'draft', ('release', 'release_publish', 'cancel',), + color='yellow', + setSecurity=setPermissionsForRoles({ + ('zope.View', 'zope.Member'): Deny, + ('zope.View', 'loops.Member'): Deny, + ('zope.View', 'loops.Person'): Deny, + ('zope.View', 'loops.Staff'): Deny,})), + State('active', 'active', ('reopen', 'finish', 'publish', 'cancel',), + color='lightblue', + setSecurity=setPermissionsForRoles({ + ('zope.View', 'zope.Member'): Deny, + ('zope.View', 'loops.Member'): Deny, + ('zope.View', 'loops.Person'): Allow, + ('zope.View', 'loops.Staff'): Deny,})), + State('active_published', 'active (published)', + ('reopen', 'finish_published', 'retract', 'cancel',), color='blue', + setSecurity=setPermissionsForRoles({ + ('zope.View', 'zope.Member'): Allow, + ('zope.View', 'loops.Member'): Allow, + ('zope.View', 'loops.Person'): Allow, + ('zope.View', 'loops.Staff'): Allow,})), + State('finished', 'finished', ('reopen', 'archive',), + color='lightgreen', + setSecurity=setPermissionsForRoles({ + ('zope.View', 'zope.Member'): Deny, + ('zope.View', 'loops.Member'): Deny, + ('zope.View', 'loops.Person'): Allow, + ('zope.View', 'loops.Staff'): Deny,})), + State('finished_published', 'finished (published)', ('reopen', 'archive',), + color='green', + setSecurity=setPermissionsForRoles({ + ('zope.View', 'zope.Member'): Allow, + ('zope.View', 'loops.Member'): Allow, + ('zope.View', 'loops.Person'): Allow, + ('zope.View', 'loops.Staff'): Allow,})), + State('cancelled', 'cancelled', ('reopen',), + color='x', + setSecurity=setPermissionsForRoles({ + ('zope.View', 'zope.Member'): Deny, + ('zope.View', 'loops.Member'): Deny, + ('zope.View', 'loops.Person'): Deny, + ('zope.View', 'loops.Staff'): Deny,})), + State('archived', 'archived', ('reopen',), + color='grey', + setSecurity=setPermissionsForRoles({ + ('zope.View', 'zope.Member'): Deny, + ('zope.View', 'loops.Member'): Deny, + ('zope.View', 'loops.Person'): Deny, + ('zope.View', 'loops.Staff'): Deny,})), + Transition('release', 'release', 'active'), + Transition('release_publish', 'release, publish', 'active_published'), + Transition('publish', 'publish', 'active_published'), + Transition('retract', 'retract', 'draft'), + Transition('finish', 'finish', 'finished'), + Transition('finish_published', 'finish (published)', 'finished_published'), + Transition('cancel', 'cancel', 'cancelled'), + Transition('reopen', 're-open', 'draft'), + Transition('archive', 'archive', 'archived'), + initialState='draft') class StatefulTask(StatefulLoopsObject): statesDefinition = 'task_states' + +class PublishableTask(StatefulLoopsObject): + + statesDefinition = 'publishable_task' + diff --git a/organize/stateful/view_macros.pt b/organize/stateful/view_macros.pt index c9473c2..6dfad17 100644 --- a/organize/stateful/view_macros.pt +++ b/organize/stateful/view_macros.pt @@ -1,4 +1,14 @@ - + + + + + + + @@ -32,7 +42,7 @@ tal:attributes="name string:$name:list; value value; checked python: - value in item.selectedStates.get(name, ()); + value in item.selectedStates.get(name, ()); id string:$name.$value" /> 
+ + + diff --git a/organize/tracking/base.py b/organize/tracking/base.py index ab3c2c6..f8a1323 100644 --- a/organize/tracking/base.py +++ b/organize/tracking/base.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -18,8 +18,6 @@ """ Base class(es) for track/record managers. - -$Id$ """ from zope.cachedescriptors.property import Lazy @@ -46,6 +44,10 @@ class BaseRecordManager(object): def loopsRoot(self): return self.context.getLoopsRoot() + @Lazy + def uid(self): + return util.getUidForObject(self.context) + @Lazy def storage(self): records = self.loopsRoot.getRecordManager() diff --git a/organize/tracking/browser.py b/organize/tracking/browser.py index 9f33af1..d6b7604 100644 --- a/organize/tracking/browser.py +++ b/organize/tracking/browser.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,9 +17,7 @@ # """ -View class(es) for change tracks. - -$Id$ +View classes for tracks. """ from zope import component diff --git a/organize/work/browser.py b/organize/work/browser.py index de90159..564f90b 100644 --- a/organize/work/browser.py +++ b/organize/work/browser.py @@ -151,6 +151,7 @@ class WorkItemDetails(TrackDetails): addParams=dict(id=self.track.__name__)) actions = [info, WorkItemStateAction(self)] if self.isLastInRun and self.allowedToEditWorkItem: + #if self.allowedToEditWorkItem: self.view.registerDojoDateWidget() self.view.registerDojoNumberWidget() self.view.registerDojoTextarea() @@ -167,7 +168,10 @@ class WorkItemDetails(TrackDetails): @Lazy def allowedToEditWorkItem(self): + # if not canAccessObject(self.object.task): + # return False if checkPermission('loops.ManageSite', self.object): + # or hasRole('loops.Master', self.object): return True if self.track.data.get('creator') == self.personId: return True @@ -288,6 +292,25 @@ class TaskWorkItems(BaseWorkItemsView, ConceptView): return sorted(self.query(**criteria), key=lambda x: x.track.timeStamp) +class RelatedTaskWorkItems(AllWorkItems): + """ Show work items for all instances of a concept type assigned to + the query as query target. + """ + + @Lazy + def isQueryTarget(self): + return self.conceptManager['querytarget'] + + def listWorkItems(self): + criteria = self.baseCriteria + tasks = [] + for parent in self.context.getChildren([self.isQueryTarget]): + for task in parent.getChildren([self.typePredicate]): + tasks.append(util.getUidForObject(task)) + criteria['task'] = tasks + return sorted(self.query(**criteria), key=lambda x: x.track.timeStamp) + + class PersonWorkItems(BaseWorkItemsView, ConceptView): """ A query view showing work items for a person, the query's parent. """ @@ -343,7 +366,12 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView): workItems = self.loopsRoot.getRecordManager()[ self.recordManagerName] return workItems.get(id) - return self.trackFactory(None, 0, None, {}) + self.task = self.target + track = self.trackFactory(None, 0, None, {}) + types = self.workItemTypes + if len(types) == 1: + track.workItemType = types[0].name + return track @Lazy def title(self): @@ -379,25 +407,35 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView): return time.strftime('%Y-%m-%d', time.localtime(ts)) return '' + @Lazy + def defaultTimeStamp(self): + if self.workItemType.prefillDate: + return getTimeStamp() + return None + @Lazy def date(self): - ts = self.track.start or getTimeStamp() - return time.strftime('%Y-%m-%d', time.localtime(ts)) + ts = self.track.start or self.defaultTimeStamp + if ts: + return time.strftime('%Y-%m-%d', time.localtime(ts)) + return '' @Lazy def startTime(self): - ts = self.track.start or getTimeStamp() - #return time.strftime('%Y-%m-%dT%H:%M', time.localtime(ts)) - return time.strftime('T%H:%M', time.localtime(ts)) + ts = self.track.start or self.defaultTimeStamp + if ts: + return time.strftime('T%H:%M', time.localtime(ts)) + return '' @Lazy def endTime(self): if self.state == 'running': - ts = getTimeStamp() + ts = self.defaultTimeStamp else: - ts = self.track.end or getTimeStamp() - #return time.strftime('%Y-%m-%dT%H:%M', time.localtime(ts)) - return time.strftime('T%H:%M', time.localtime(ts)) + ts = self.track.end or self.defaultTimeStamp + if ts: + return time.strftime('T%H:%M', time.localtime(ts)) + return '' @Lazy def state(self): diff --git a/organize/work/configure.zcml b/organize/work/configure.zcml index 5dbdc5b..bbd83f7 100644 --- a/organize/work/configure.zcml +++ b/organize/work/configure.zcml @@ -30,6 +30,14 @@ factory="loops.organize.work.browser.AllWorkItems" permission="zope.View" /> + + + + + + diff --git a/security/browser/admin.py b/security/browser/admin.py index 0679ec1..d4e020c 100644 --- a/security/browser/admin.py +++ b/security/browser/admin.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2010 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -18,8 +18,6 @@ """ Security-related views. - -$Id$ """ from zope.app.authentication.groupfolder import GroupInformation @@ -145,7 +143,7 @@ class PermissionView(object): for e in entry: value = SettingAsBoolean[e[1]] value = (value is False and '-') or (value and '+') or '' - result.append(value + e[0]) + result.append(value + (e[0] or '')) return ', '.join(result) def getPrincipalPermissions(self): diff --git a/security/common.py b/security/common.py index e280aed..1a5fb58 100644 --- a/security/common.py +++ b/security/common.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2011 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -18,8 +18,6 @@ """ Common functions and other stuff for working with permissions and roles. - -$Id$ """ from persistent import Persistent @@ -49,12 +47,12 @@ allRolesExceptOwner = ( 'zope.Anonymous', 'zope.Member', 'zope.ContentManager', 'loops.Staff', 'loops.xmlrpc.ConceptManager', # relevant for local security? #'loops.SiteManager', - 'loops.Member', 'loops.Master',) + 'loops.Person', 'loops.Member', 'loops.Master') allRolesExceptOwnerAndMaster = tuple(allRolesExceptOwner[:-1]) minorPrivilegedRoles = ('zope.Anonymous', 'zope.Member',) localRoles = ('zope.Anonymous', 'zope.Member', 'zope.ContentManager', 'loops.SiteManager', 'loops.Staff', 'loops.Member', 'loops.Master', - 'loops.Owner') + 'loops.Owner', 'loops.Person') localPermissions = ('zope.ManageContent', 'zope.View', 'loops.ManageWorkspaces', 'loops.ViewRestricted', 'loops.EditRestricted', 'loops.AssignAsParent',) @@ -77,7 +75,7 @@ def canListObject(obj, noCheck=False): return canAccess(obj, 'title') def canWriteObject(obj): - return canWrite(obj, 'title') + return canWrite(obj, 'title') or canAssignAsParent(obj) def canEditRestricted(obj): return checkPermission('loops.EditRestricted', obj) @@ -122,12 +120,22 @@ def setPrincipalRole(prm, r, p, setting): def assignOwner(obj, principalId): - prm = IPrincipalRoleManager(obj) - prm.assignRoleToPrincipal('loops.Owner', principalId) + prm = IPrincipalRoleManager(obj, None) + if prm is not None: + prm.assignRoleToPrincipal('loops.Owner', principalId) def removeOwner(obj, principalId): + prm = IPrincipalRoleManager(obj, None) + if prm is not None: + prm.unsetRoleForPrincipal('loops.Owner', principalId) + +def assignPersonRole(obj, principalId): prm = IPrincipalRoleManager(obj) - prm.removeRoleFromPrincipal('loops.Owner', principalId) + prm.assignRoleToPrincipal('loops.Person', principalId) + +def removePersonRole(obj, principalId): + prm = IPrincipalRoleManager(obj) + prm.unsetRoleForPrincipal('loops.Person', principalId) def allowEditingForOwner(obj, deny=allRolesExceptOwner, revert=False): @@ -161,6 +169,9 @@ def setDefaultSecurity(obj, event): aObj = adapted(obj) setter = ISecuritySetter(aObj) setter.setDefaultSecurity() + principal = getCurrentPrincipal() + if principal is not None: + assignOwner(obj, principal.id) @component.adapter(IConcept, IAssignmentEvent) diff --git a/security/interfaces.py b/security/interfaces.py index ef4876e..283db83 100644 --- a/security/interfaces.py +++ b/security/interfaces.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2009 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -18,8 +18,6 @@ """ Interfaces for loops security management. - -$Id$ """ from zope.interface import Interface, Attribute @@ -35,6 +33,10 @@ class ISecuritySetter(Interface): context object. """ + def setStateSecurity(): + """ Set the security according to the state(s) of the object. + """ + def setDefaultRolePermissions(): """ Set some default role permission assignments (grants) on the context object. diff --git a/security/setter.py b/security/setter.py index 373e41e..a26be15 100644 --- a/security/setter.py +++ b/security/setter.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2011 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -19,8 +19,6 @@ """ Base classes for security setters, i.e. adapters that provide standardized methods for setting role permissions and other security-related stuff. - -$Id$ """ from zope.app.security.settings import Allow, Deny, Unset @@ -33,11 +31,14 @@ from zope.securitypolicy.interfaces import \ IRolePermissionMap, IRolePermissionManager, \ IPrincipalRoleMap, IPrincipalRoleManager +from cybertools.meta.interfaces import IOptions +from cybertools.stateful.interfaces import IStateful from loops.common import adapted, AdapterBase, baseObject +from loops.config.base import DummyOptions from loops.interfaces import IConceptSchema, IBaseResourceSchema, ILoopsAdapter from loops.organize.util import getPrincipalFolder, getGroupsFolder, getGroupId from loops.security.common import overrides, setRolePermission, setPrincipalRole -from loops.security.common import acquiringPredicateNames +from loops.security.common import allRolesExceptOwner, acquiringPredicateNames from loops.security.interfaces import ISecuritySetter from loops.versioning.interfaces import IVersionable @@ -58,6 +59,17 @@ class BaseSecuritySetter(object): def conceptManager(self): return self.baseObject.getLoopsRoot().getConceptManager() + @Lazy + def typeOptions(self): + type = self.baseObject.getType() + if type is None: + return DummyOptions() + return IOptions(adapted(type), DummyOptions()) + + @Lazy + def globalOptions(self): + return IOptions(self.baseObject.getLoopsRoot()) + @Lazy def acquiringPredicates(self): return [self.conceptManager.get(n) for n in acquiringPredicateNames] @@ -81,6 +93,9 @@ class BaseSecuritySetter(object): def acquireRolePermissions(self): pass + def acquirePrincipalRoles(self): + pass + def copyPrincipalRoles(self, source, revert=False): pass @@ -109,6 +124,13 @@ class LoopsObjectSecuritySetter(BaseSecuritySetter): for p, r, s in rpm.getRolesAndPermissions(): setRolePermission(rpm, p, r, Unset) + def setStateSecurity(self): + statesDefs = (self.globalOptions('organize.stateful.concept', []) + + (self.typeOptions('organize.stateful') or [])) + for std in statesDefs: + stf = component.getAdapter(self.baseObject, IStateful, name=std) + stf.getStateObject().setSecurity(stf) + def acquireRolePermissions(self): settings = {} for p in self.parents: @@ -128,15 +150,59 @@ class LoopsObjectSecuritySetter(BaseSecuritySetter): settings[(p, r)] = s self.setDefaultRolePermissions() self.setRolePermissions(settings) + self.setStateSecurity() def setRolePermissions(self, settings): for (p, r), s in settings.items(): setRolePermission(self.rolePermissionManager, p, r, s) + def acquirePrincipalRoles(self): + #if baseObject(self.context).workspaceInformation: + # return # do not remove/overwrite workspace settings + settings = {} + for parent in self.parents: + if parent == self.baseObject: + continue + wi = parent.workspaceInformation + if wi: + if not wi.propagateParentSecurity: + continue + prm = IPrincipalRoleMap(wi) + for r, p, s in prm.getPrincipalsAndRoles(): + current = settings.get((r, p)) + if current is None or overrides(s, current): + settings[(r, p)] = s + prm = IPrincipalRoleMap(parent) + for r, p, s in prm.getPrincipalsAndRoles(): + current = settings.get((r, p)) + if current is None or overrides(s, current): + settings[(r, p)] = s + self.setDefaultPrincipalRoles() + for setter in self.versionSetters: + setter.setPrincipalRoles(settings) + + @Lazy + def versionSetters(self): + return [self] + + def setDefaultPrincipalRoles(self): + prm = self.principalRoleManager + # TODO: set loops.Person roles for Person + for r, p, s in prm.getPrincipalsAndRoles(): + if r in allRolesExceptOwner: + setPrincipalRole(prm, r, p, Unset) + + def setPrincipalRoles(self, settings): + prm = self.principalRoleManager + for (r, p), s in settings.items(): + if r != 'loops.Owner': + setPrincipalRole(prm, r, p, s) + def copyPrincipalRoles(self, source, revert=False): prm = IPrincipalRoleMap(baseObject(source.context)) for r, p, s in prm.getPrincipalsAndRoles(): - if p in self.workspacePrincipals: + #if p in self.workspacePrincipals: + if r != 'loops.Owner': if revert: setPrincipalRole(self.principalRoleManager, r, p, Unset) else: @@ -155,12 +221,13 @@ class ConceptSecuritySetter(LoopsObjectSecuritySetter): setter = ISecuritySetter(adapted(relation.second)) setter.setDefaultRolePermissions() setter.acquireRolePermissions() - wi = baseObject(self.context).workspaceInformation - if wi and not wi.propagateParentSecurity: - return - setter.copyPrincipalRoles(self, revert) - if wi: - setter.copyPrincipalRoles(ISecuritySetter(wi), revert) + setter.acquirePrincipalRoles() + #wi = baseObject(self.context).workspaceInformation + #if wi and not wi.propagateParentSecurity: + # return + #setter.copyPrincipalRoles(self, revert) + #if wi: + # setter.copyPrincipalRoles(ISecuritySetter(wi), revert) setter.propagateSecurity(revert, updated) def propagateSecurity(self, revert=False, updated=None): @@ -186,6 +253,12 @@ class ResourceSecuritySetter(LoopsObjectSecuritySetter): def parents(self): return self.baseObject.getConcepts(self.acquiringPredicates) + def setStateSecurity(self): + statesDefs = (self.globalOptions('organize.stateful.resource', [])) + for std in statesDefs: + stf = component.getAdapter(self.baseObject, IStateful, name=std) + stf.getStateObject().setSecurity(self.context) + def setRolePermissions(self, settings): vSetters = [self] vr = IVersionable(baseObject(self.context)) @@ -204,10 +277,18 @@ class ResourceSecuritySetter(LoopsObjectSecuritySetter): vSetters = [ISecuritySetter(adapted(v)) for v in versions] prm = IPrincipalRoleMap(baseObject(source.context)) for r, p, s in prm.getPrincipalsAndRoles(): - if p in self.workspacePrincipals: + #if p in self.workspacePrincipals: + if r != 'loops.Owner' and p in self.workspacePrincipals: for v in vSetters: if revert: setPrincipalRole(v.principalRoleManager, r, p, Unset) else: setPrincipalRole(v.principalRoleManager, r, p, s) + @Lazy + def versionSetters(self): + vr = IVersionable(baseObject(self.context)) + versions = list(vr.versions.values()) + if versions: + return [ISecuritySetter(adapted(v)) for v in versions] + return [self] diff --git a/system/sync/README.txt b/system/sync/README.txt index c18c601..5ed477f 100644 --- a/system/sync/README.txt +++ b/system/sync/README.txt @@ -18,7 +18,7 @@ Let's set up a loops site with basic and example concepts and resources. >>> concepts, resources, views = t.setup() >>> loopsRoot = site['loops'] >>> len(concepts), len(resources), len(views) - (34, 3, 1) + (33, 3, 1) >>> from cybertools.tracking.btree import TrackingStorage >>> from loops.system.job import JobRecord diff --git a/xmlrpc/README.txt b/xmlrpc/README.txt index 9b2e033..3ea6557 100755 --- a/xmlrpc/README.txt +++ b/xmlrpc/README.txt @@ -35,7 +35,7 @@ ZCML setup): Let's look what setup has provided us with: >>> len(concepts) - 23 + 22 Now let's add a few more concepts: @@ -73,7 +73,7 @@ applied in an explicit assignment. >>> sorted(t['name'] for t in xrf.getConceptTypes()) [u'competence', u'customer', u'domain', u'file', u'note', u'person', - u'predicate', u'task', u'textdocument', u'topic', u'training', u'type'] + u'predicate', u'task', u'textdocument', u'topic', u'type'] >>> sorted(t['name'] for t in xrf.getPredicates()) [u'depends', u'issubtype', u'knows', u'ownedby', u'provides', u'requires', u'standard'] @@ -96,7 +96,7 @@ All methods that retrieve one object also returns its children and parents: u'hasType' >>> sorted(c['name'] for c in ch[0]['objects']) [u'competence', u'customer', u'domain', u'file', u'note', u'person', - u'predicate', u'task', u'textdocument', u'topic', u'training', u'type'] + u'predicate', u'task', u'textdocument', u'topic', u'type'] >>> pa = defaultPred['parents'] >>> len(pa) @@ -115,7 +115,7 @@ We can also retrieve children and parents explicitely: u'hasType' >>> sorted(c['name'] for c in ch[0]['objects']) [u'competence', u'customer', u'domain', u'file', u'note', u'person', - u'predicate', u'task', u'textdocument', u'topic', u'training', u'type'] + u'predicate', u'task', u'textdocument', u'topic', u'type'] >>> pa = xrf.getParents('5') >>> len(pa) @@ -174,14 +174,14 @@ Updating the concept map >>> topicId = xrf.getObjectByName('topic')['id'] >>> xrf.createConcept(topicId, u'zope2', u'Zope 2') - {'description': u'', 'title': u'Zope 2', 'type': '36', 'id': '75', + {'description': u'', 'title': u'Zope 2', 'type': '36', 'id': '72', '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': '36', 'id': '77', + {'description': u'', 'title': u'Python', 'type': '36', 'id': '74', 'name': u'python'} If we try to deassign a ``hasType`` relation nothing will happen; a