merge branch master

This commit is contained in:
Helmut Merz 2013-03-23 11:32:29 +01:00
commit b5ab23a90b
82 changed files with 2000 additions and 492 deletions

View file

@ -2,8 +2,6 @@
loops - Linked Objects for Organization and Processing Services loops - Linked Objects for Organization and Processing Services
=============================================================== ===============================================================
($Id$)
The loops platform consists up of three basic types of objects: The loops platform consists up of three basic types of objects:
(1) concepts: simple interconnected objects usually representing (1) concepts: simple interconnected objects usually representing
@ -612,7 +610,7 @@ Actions
>>> view.controller = Controller(view, request) >>> view.controller = Controller(view, request)
>>> #view.setupController() >>> #view.setupController()
>>> actions = view.getActions('portlet') >>> actions = view.getAllowedActions('portlet')
>>> len(actions) >>> len(actions)
2 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 ask a view which view and edit actions it supports. We directly use the
target object's view here: target object's view here:
>>> actions = view.virtualTarget.getActions('object', page=view) >>> actions = view.virtualTarget.getAllowedActions('object', page=view)
>>> #actions[0].url >>> #actions[0].url
'http://127.0.0.1/loops/views/m1/m11/m111/.target23' 'http://127.0.0.1/loops/views/m1/m11/m111/.target23'

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -129,6 +129,7 @@ actions.register('edit_object', 'portlet', DialogAction,
viewName='edit_object.html', viewName='edit_object.html',
dialogName='edit', dialogName='edit',
prerequisites=['registerDojoEditor'], prerequisites=['registerDojoEditor'],
permission='zope.ManageContent',
) )
actions.register('edit_concept', 'portlet', DialogAction, actions.register('edit_concept', 'portlet', DialogAction,
@ -137,6 +138,7 @@ actions.register('edit_concept', 'portlet', DialogAction,
viewName='edit_concept.html', viewName='edit_concept.html',
dialogName='edit', dialogName='edit',
prerequisites=['registerDojoEditor'], prerequisites=['registerDojoEditor'],
permission='zope.ManageContent',
) )
actions.register('create_concept', 'portlet', DialogAction, actions.register('create_concept', 'portlet', DialogAction,
@ -146,6 +148,7 @@ actions.register('create_concept', 'portlet', DialogAction,
dialogName='createConcept', dialogName='createConcept',
qualifier='create_concept', qualifier='create_concept',
innerForm='inner_concept_form.html', innerForm='inner_concept_form.html',
permission='loops.AssignAsParent',
) )
actions.register('create_subtype', 'portlet', DialogAction, actions.register('create_subtype', 'portlet', DialogAction,
@ -155,4 +158,5 @@ actions.register('create_subtype', 'portlet', DialogAction,
dialogName='createConcept', dialogName='createConcept',
qualifier='subtype', qualifier='subtype',
innerForm='inner_concept_form.html', innerForm='inner_concept_form.html',
permission='loops.AssignAsParent',
) )

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -45,7 +45,7 @@ from zope.publisher.browser import applySkin
from zope.publisher.interfaces.browser import IBrowserSkinType, IBrowserView from zope.publisher.interfaces.browser import IBrowserSkinType, IBrowserView
from zope import schema from zope import schema
from zope.schema.vocabulary import SimpleTerm 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.interfaces import ForbiddenAttribute, Unauthorized
from zope.security.proxy import removeSecurityProxy from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL 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.ajax.dojo import dojoMacroTemplate
from cybertools.browser.view import GenericView from cybertools.browser.view import GenericView
from cybertools.meta.interfaces import IOptions from cybertools.meta.interfaces import IOptions
from cybertools.meta.element import Element
from cybertools.relation.interfaces import IRelationRegistry from cybertools.relation.interfaces import IRelationRegistry
from cybertools.stateful.interfaces import IStateful from cybertools.stateful.interfaces import IStateful
from cybertools.text import mimetypes from cybertools.text import mimetypes
from cybertools.typology.interfaces import IType, ITypeManager from cybertools.typology.interfaces import IType, ITypeManager
from cybertools.util.date import toLocalTime
from cybertools.util.jeep import Jeep from cybertools.util.jeep import Jeep
from loops.browser.util import normalizeForUrl from loops.browser.util import normalizeForUrl
from loops.common import adapted, baseObject 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.interfaces import IResource, IView, INode, ITypeConcept
from loops.organize.tracking import access from loops.organize.tracking import access
from loops.resource import Resource from loops.resource import Resource
from loops.security.common import checkPermission
from loops.security.common import canAccessObject, canListObject, canWriteObject from loops.security.common import canAccessObject, canListObject, canWriteObject
from loops.type import ITypeConcept from loops.type import ITypeConcept
from loops import util from loops import util
@ -131,6 +134,7 @@ class BaseView(GenericView, I18NView):
actions = {} actions = {}
portlet_actions = [] portlet_actions = []
parts = ()
icon = None icon = None
modeName = 'view' modeName = 'view'
isToplevel = False isToplevel = False
@ -184,6 +188,15 @@ class BaseView(GenericView, I18NView):
return '%s/.%s-%s' % (baseUrl, targetId, normalizeForUrl(title)) return '%s/.%s-%s' % (baseUrl, targetId, normalizeForUrl(title))
return '%s/.%s' % (baseUrl, targetId) 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 @Lazy
def principalId(self): def principalId(self):
principal = self.request.principal principal = self.request.principal
@ -258,6 +271,8 @@ class BaseView(GenericView, I18NView):
d = dc.modified or dc.created d = dc.modified or dc.created
if isinstance(d, str): if isinstance(d, str):
d = datetime(*(strptime(d, '%Y-%m-%dT%H:%M')[:6])) d = datetime(*(strptime(d, '%Y-%m-%dT%H:%M')[:6]))
else:
d = toLocalTime(d)
return d return d
@Lazy @Lazy
@ -486,7 +501,7 @@ class BaseView(GenericView, I18NView):
if text is None: if text is None:
return u'' return u''
htmlPattern = re.compile(r'<(.+)>.+</\1>') htmlPattern = re.compile(r'<(.+)>.+</\1>')
if htmlPattern.search(text): if '<br />' in text or htmlPattern.search(text):
return text return text
return self.renderText(text, 'text/restructured') return self.renderText(text, 'text/restructured')
@ -565,16 +580,29 @@ class BaseView(GenericView, I18NView):
return DummyOptions() return DummyOptions()
return component.queryAdapter(self.adapted, IOptions) or DummyOptions() return component.queryAdapter(self.adapted, IOptions) or DummyOptions()
@Lazy
def globalOptions(self):
return IOptions(self.loopsRoot)
@Lazy @Lazy
def typeOptions(self): def typeOptions(self):
if self.typeProvider is None: if self.typeProvider is None:
return DummyOptions() return DummyOptions()
return IOptions(adapted(self.typeProvider)) 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): def getPredicateOptions(self, relation):
return IOptions(adapted(relation.predicate), None) or DummyOptions() return IOptions(adapted(relation.predicate), None) or DummyOptions()
@ -648,7 +676,10 @@ class BaseView(GenericView, I18NView):
# states # states
viewStatesPermission = 'zope.ManageContent' @Lazy
def viewStatesPermission(self):
opt = self.globalOptions('organize.show_states')
return opt and opt[0] or 'zope.ManageContent'
@Lazy @Lazy
def states(self): def states(self):
@ -685,6 +716,16 @@ class BaseView(GenericView, I18NView):
""" """
return [] 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 @Lazy
def showObjectActions(self): def showObjectActions(self):
return not IUnauthenticatedPrincipal.providedBy(self.request.principal) return not IUnauthenticatedPrincipal.providedBy(self.request.principal)
@ -822,6 +863,7 @@ class BaseView(GenericView, I18NView):
def registerDojoFormAll(self): def registerDojoFormAll(self):
self.registerDojo() self.registerDojo()
self.registerDojoEditor()
cm = self.controller.macros cm = self.controller.macros
jsCall = ('dojo.require("dijit.form.Form"); ' jsCall = ('dojo.require("dijit.form.Form"); '
'dojo.require("dijit.form.DateTextBox"); ' 'dojo.require("dijit.form.DateTextBox"); '

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -200,16 +200,22 @@ class BaseRelationView(BaseView):
class ConceptView(BaseView): class ConceptView(BaseView):
template = concept_macros template = concept_macros
templateName = 'concept.standard'
macroName = 'conceptdata'
partPrefix = 'part_'
defaultParts = ('title', 'fields',
'children', 'resources', 'workitems', 'comments',)
def childViewFactory(self, *args, **kw): def childViewFactory(self, *args, **kw):
return ConceptRelationView(*args, **kw) return ConceptRelationView(*args, **kw)
@Lazy @Lazy
def macro(self): def macros(self):
return self.template.macros['conceptdata'] return self.controller.getTemplateMacros(self.templateName, self.template)
#def __init__(self, context, request): @property
# super(ConceptView, self).__init__(context, request) def macro(self):
return self.macros[self.macroName]
def setupController(self): def setupController(self):
cont = self.controller cont = self.controller
@ -222,6 +228,24 @@ class ConceptView(BaseView):
subMacro=concept_macros.macros['parents'], subMacro=concept_macros.macros['parents'],
priority=20, info=self) 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 @Lazy
def adapted(self): def adapted(self):
return adapted(self.context, self.languageInfo) return adapted(self.context, self.languageInfo)
@ -296,6 +320,10 @@ class ConceptView(BaseView):
if r.order != pos: if r.order != pos:
r.order = pos r.order = pos
@Lazy
def filterOptions(self):
return self.getOptions('filter.states')
def getChildren(self, topLevelOnly=True, sort=True, noDuplicates=True, def getChildren(self, topLevelOnly=True, sort=True, noDuplicates=True,
useFilter=True, predicates=None): useFilter=True, predicates=None):
form = self.request.form form = self.request.form
@ -337,7 +365,7 @@ class ConceptView(BaseView):
options = IOptions(adapted(r.predicate), None) options = IOptions(adapted(r.predicate), None)
if options is not None and options('hide_children'): if options is not None and options('hide_children'):
continue continue
if not fv.check(r.context): if not fv.check(r.context, self.filterOptions):
continue continue
yield r yield r
@ -693,3 +721,22 @@ class ListChildren(ConceptView):
def macro(self): def macro(self):
return concept_macros.macros['list_children'] 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

View file

@ -1,6 +1,13 @@
<html i18n:domain="loops"> <html i18n:domain="loops">
<metal:data define-macro="layout">
<tal:part repeat="part item/getParts">
<metal:part use-macro="part/macro" />
</tal:part>
</metal:data>
<metal:data define-macro="conceptdata"> <metal:data define-macro="conceptdata">
<div tal:attributes="class string:content-$level;"> <div tal:attributes="class string:content-$level;">
<metal:block use-macro="view/concept_macros/concepttitle" /> <metal:block use-macro="view/concept_macros/concepttitle" />
@ -22,6 +29,20 @@
</metal:data> </metal:data>
<metal:selection define-macro="filter_input">
<div tal:define="criteria item/filterInput"
tal:condition="criteria">
<form method="get" name="filter" id="form-filter">
<span tal:repeat="crit criteria">
<metal:input use-macro="crit/macro" />
</span>
<input type="submit" name="show" value="Show"
tal:condition="nothing" />
</form>
</div>
</metal:selection>
<metal:title define-macro="concepttitle"> <metal:title define-macro="concepttitle">
<metal:title define-macro="concepttitle_only"> <metal:title define-macro="concepttitle_only">
<tal:actions condition="view/showObjectActions"> <tal:actions condition="view/showObjectActions">
@ -41,8 +62,10 @@
string:$resourceBase/cybertools.icons/table.png" /> string:$resourceBase/cybertools.icons/table.png" />
</a> </a>
</h1> </h1>
<metal:block use-macro="view/concept_macros/filter_input" />
</metal:title> </metal:title>
<p tal:define="description description|item/renderedDescription" <p metal:define-macro="conceptdescription"
tal:define="description description|item/renderedDescription"
tal:condition="description"> tal:condition="description">
<i tal:content="structure description">Description</i></p> <i tal:content="structure description">Description</i></p>
</metal:title> </metal:title>

View file

@ -545,6 +545,14 @@
factory="loops.browser.concept.ListChildren" factory="loops.browser.concept.ListChildren"
permission="zope.View" /> permission="zope.View" />
<zope:adapter
name="list_type_instances.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.browser.concept.ListTypeInstances"
permission="zope.View" />
<!-- dialogs/forms (end-user views) --> <!-- dialogs/forms (end-user views) -->
<page <page

View file

@ -291,7 +291,8 @@
tal:condition="stTrans" tal:condition="stTrans"
tal:attributes="name string:state.$stDef"> tal:attributes="name string:state.$stDef">
<option i18n:translate="" value="-">No change</option> <option i18n:translate="" value="-">No change</option>
<option tal:repeat="trans stTrans" <option i18n:translate=""
tal:repeat="trans stTrans"
tal:attributes="value trans/name" tal:attributes="value trans/name"
tal:content="trans/title">publish</option> tal:content="trans/title">publish</option>
</select> </select>
@ -309,7 +310,7 @@
<input value="Save" type="submit" <input value="Save" type="submit"
i18n:attributes="value" i18n:attributes="value"
tal:attributes="onClick python: view.closeAction(True) or tal:attributes="onClick python: view.closeAction(True) or
'return true'"> 'submit();; return false'">
<input type="button" value="Cancel" onClick="dlg.hide();" <input type="button" value="Cancel" onClick="dlg.hide();"
i18n:attributes="value" i18n:attributes="value"
tal:condition="view/isInnerHtml" tal:condition="view/isInnerHtml"
@ -322,16 +323,15 @@
<!-- overridden field renderers --> <!-- overridden field renderers -->
<metal:html define-macro="input_html"> <metal:html define-macro="input_html">
<p>HTML</p> <div dojoType="dijit.Editor"
<div name="field" rows="3" style="width: 600px"
dojoType="dijit.Editor"
extraPlugins="['insertHorizontalRule', 'createLink', 'viewsource']" extraPlugins="['insertHorizontalRule', 'createLink', 'viewsource']"
tal:define="width field/width|nothing; tal:define="width field/width|nothing;
height field/height|python:3" height field/height|python:10"
tal:attributes="name name; id name; tal:attributes="name name; id name;
rows python: height or 3; height python:
'%spx' % (height and str(height * 15) or '150');
style python: style python:
'width: %s' % (width and str(width)+'px' or '600px');" 'width: %spx' % (width and str(width) or '600');"
tal:content="structure data/?name|string:"> tal:content="structure data/?name|string:">
</div> </div>
</metal:html> </metal:html>

View file

@ -41,18 +41,12 @@ class Base(BaseConceptView):
templateName = 'lobo.standard' templateName = 'lobo.standard'
macroName = None macroName = None
@Lazy #@Lazy
def macros(self): # better implementation in BaseView:
return self.controller.getTemplateMacros(self.templateName, self.template) # splits comma-separated list of values automatically
#def params(self):
@property # ann = self.request.annotations.get('loops.view', {})
def macro(self): # return parse_qs(ann.get('params') or '')
return self.macros[self.macroName]
@Lazy
def params(self):
ann = self.request.annotations.get('loops.view', {})
return parse_qs(ann.get('params') or '')
class ConceptView(BaseConceptView): class ConceptView(BaseConceptView):
@ -152,25 +146,8 @@ class ConceptView(BaseConceptView):
class Layout(Base, ConceptView): class Layout(Base, ConceptView):
macroName = 'layout' macroName = 'layout'
partPrefix = 'lobo_'
def getParts(self): defaultParts = ('h1', 'g3',)
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
class BasePart(Base): class BasePart(Base):
@ -192,7 +169,7 @@ class BasePart(Base):
return preds return preds
def getChildren(self): 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] subtypes = [self.conceptManager[st] for st in subtypeNames if st]
result = [] result = []
childRels = self.context.getChildRelations(self.childPredicates) childRels = self.context.getChildRelations(self.childPredicates)

View file

@ -211,6 +211,22 @@ function closeDialog(save) {
} }
function closeDataWidget(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'); var widget = dijit.byId('data');
if (widget != undefined && save) { if (widget != undefined && save) {
value = widget.getValue(); value = widget.getValue();

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -51,17 +51,18 @@ from cybertools.typology.interfaces import IType, ITypeManager
from cybertools.util.jeep import Jeep from cybertools.util.jeep import Jeep
from cybertools.xedit.browser import ExternalEditorView from cybertools.xedit.browser import ExternalEditorView
from loops.browser.action import actions, DialogAction 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.common import adapted, AdapterBase, baseObject
from loops.i18n.browser import i18n_macros, LanguageInfo from loops.i18n.browser import i18n_macros, LanguageInfo
from loops.interfaces import IConcept, IResource, IDocument, IMediaAsset, INode from loops.interfaces import IConcept, IResource, IDocument, IMediaAsset, INode
from loops.interfaces import IViewConfiguratorSchema 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.interfaces import IPresence
from loops.organize.tracking import access 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 from loops.versioning.util import getVersion
@ -150,13 +151,15 @@ class NodeView(BaseView):
priority=20) priority=20)
cm.register('portlet_left', 'navigation', title='Navigation', cm.register('portlet_left', 'navigation', title='Navigation',
subMacro=node_macros.macros['menu']) subMacro=node_macros.macros['menu'])
if canWrite(self.context, 'title') or ( if canWriteObject(self.context) or (
# TODO: is this useful in any case? # TODO: is this useful in any case?
self.virtualTargetObject is not None and self.virtualTargetObject is not None and
canWrite(self.virtualTargetObject, 'title')): canWriteObject(self.virtualTargetObject)):
# check if there are any available actions; # check if there are any available actions;
# store list of actions in macro object (evaluate only once) # 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: if actions:
cm.register('portlet_right', 'actions', title=_(u'Actions'), cm.register('portlet_right', 'actions', title=_(u'Actions'),
subMacro=node_macros.macros['actions'], subMacro=node_macros.macros['actions'],
@ -537,7 +540,7 @@ class NodeView(BaseView):
return self.makeTargetUrl(self.url, util.getUidForObject(target), return self.makeTargetUrl(self.url, util.getUidForObject(target),
target.title) target.title)
def getActions(self, category='object', target=None): def getActions(self, category='object', page=None, target=None):
actions = [] actions = []
#self.registerDojo() #self.registerDojo()
self.registerDojoFormAll() self.registerDojoFormAll()
@ -565,9 +568,11 @@ class NodeView(BaseView):
description='Open concept map editor in new window', description='Open concept map editor in new window',
url=cmeUrl, target=target)) url=cmeUrl, target=target))
if self.checkAction('create_resource', 'portlet', 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.', description='Create a new resource object.',
page=self, target=target)) page=self, target=target,
permission='zope.ManageContent'))
return actions return actions
actions = dict(portlet=getPortletActions) actions = dict(portlet=getPortletActions)

View file

@ -293,7 +293,8 @@
<metal:actions define-macro="object_actions"> <metal:actions define-macro="object_actions">
<div id="object-actions" class="object-actions" <div id="object-actions" class="object-actions"
tal:define="target nocall:target|nothing;"> tal:define="target nocall:target|nothing;">
<tal:actions repeat="action python:view.getActions('object', target=target)"> <tal:actions repeat="action python:
view.getAllowedActions('object', target=target)">
<metal:action use-macro="action/macro" /> <metal:action use-macro="action/macro" />
</tal:actions> </tal:actions>
</div> </div>
@ -322,7 +323,7 @@
<div><a href="logout.html?nextURL=login.html" <div><a href="logout.html?nextURL=login.html"
tal:attributes="href string:logout.html?nextURL=${view/menu/url}" tal:attributes="href string:logout.html?nextURL=${view/menu/url}"
i18n:translate="">Log out</a></div> i18n:translate="">Log out</a></div>
<tal:actions repeat="action python:view.getActions('personal')"> <tal:actions repeat="action python:view.getAllowedActions('personal')">
<metal:action use-macro="action/macro" /> <metal:action use-macro="action/macro" />
</tal:actions> </tal:actions>
</metal:actions> </metal:actions>

View file

@ -113,6 +113,9 @@ class AdapterBase(object):
self.context = context self.context = context
self.__parent__ = context # to get the permission stuff right self.__parent__ = context # to get the permission stuff right
def __hash__(self):
return hash(self.context)
def __getattr__(self, attr): def __getattr__(self, attr):
self.checkAttr(attr) self.checkAttr(attr)
return getattr(self.context, '_' + attr, None) return getattr(self.context, '_' + attr, None)

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
View classes for glossary and glossary items. View classes for glossary and glossary items.
$Id$
""" """
@ -27,6 +25,7 @@ import itertools
from zope import component from zope import component
from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.traversing.api import getName
from cybertools.browser.action import actions from cybertools.browser.action import actions
from cybertools.browser.member import IMemberInfoProvider from cybertools.browser.member import IMemberInfoProvider
@ -53,9 +52,36 @@ actions.register('createBlogPost', 'portlet', DialogAction,
fixedType=True, fixedType=True,
innerForm='inner_concept_form.html', innerForm='inner_concept_form.html',
prerequisites=['registerDojoDateWidget'], # +'registerDojoTextWidget'? 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): def supplyCreator(self, data):
creator = data.get('creator') creator = data.get('creator')
data['creatorId'] = creator data['creatorId'] = creator
@ -99,6 +125,8 @@ class BlogView(ConceptView):
@Lazy @Lazy
def blogOwnerId(self): def blogOwnerId(self):
if getName(self.context.conceptType) != 'blog':
return ''
pType = self.loopsRoot.getConceptManager()['person'] pType = self.loopsRoot.getConceptManager()['person']
persons = [p for p in self.context.getParents() if p.conceptType == pType] persons = [p for p in self.context.getParents() if p.conceptType == pType]
if len(persons) == 1: if len(persons) == 1:
@ -127,6 +155,8 @@ class BlogView(ConceptView):
return False return False
# blog post view
class BlogPostView(ConceptView): class BlogPostView(ConceptView):
@Lazy @Lazy
@ -152,6 +182,7 @@ class BlogPostView(ConceptView):
description=_(u'Modify blog post.'), description=_(u'Modify blog post.'),
viewName='edit_blogpost.html', viewName='edit_blogpost.html',
dialogName='editBlogPost', dialogName='editBlogPost',
permission='zope.ManageContent',
page=page, target=target)) page=page, target=target))
#self.registerDojoTextWidget() #self.registerDojoTextWidget()
self.registerDojoDateWidget() self.registerDojoDateWidget()
@ -170,6 +201,8 @@ class BlogPostView(ConceptView):
yield self.childViewFactory(r, self.request, contextIsSecond=True) yield self.childViewFactory(r, self.request, contextIsSecond=True)
# forms and form controllers
class EditBlogPostForm(EditConceptForm): class EditBlogPostForm(EditConceptForm):
title = _(u'Edit Blog Post') title = _(u'Edit Blog Post')

View file

@ -5,6 +5,19 @@
xmlns:browser="http://namespaces.zope.org/browser" xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="zope"> i18n_domain="zope">
<zope:adapter
factory="loops.compound.blog.post.SimpleBlogPost"
provides="loops.compound.blog.interfaces.ISimpleBlogPost"
trusted="True" />
<zope:class class="loops.compound.blog.post.SimpleBlogPost">
<require permission="zope.View"
interface="loops.compound.blog.interfaces.ISimpleBlogPost" />
<require permission="zope.View"
attributes="context" />
<require permission="zope.ManageContent"
set_schema="loops.compound.blog.interfaces.ISimpleBlogPost" />
</zope:class>
<zope:adapter <zope:adapter
factory="loops.compound.blog.post.BlogPost" factory="loops.compound.blog.post.BlogPost"
provides="loops.compound.blog.interfaces.IBlogPost" provides="loops.compound.blog.interfaces.IBlogPost"
@ -24,6 +37,15 @@
<!-- views --> <!-- views -->
<zope:adapter
name="bloglist.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.compound.blog.browser.BlogList"
permission="zope.View"
/>
<zope:adapter <zope:adapter
name="blog.html" name="blog.html"
for="loops.interfaces.IConcept for="loops.interfaces.IConcept

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Blogs (weblogs) and blog posts. Blogs (weblogs) and blog posts.
$Id$
""" """
from datetime import datetime from datetime import datetime
@ -27,9 +25,32 @@ from zope.interface import Interface, Attribute
from zope import interface, component, schema from zope import interface, component, schema
from loops.compound.interfaces import ICompound from loops.compound.interfaces import ICompound
from loops.interfaces import HtmlText
from loops.util import _ from loops.util import _
class ISimpleBlogPost(ICompound):
""" An item on a blog, sort of a diary item, minimal version.
"""
date = schema.Datetime(
title=_(u'Date/Time'),
description=_(u'The date and time the information '
'was posted.'),
required=True,)
date.default_method = datetime.now
creator = schema.ASCIILine(
title=_(u'Creator'),
description=_(u'The principal id of the user that created '
'the blog post.'),
readonly=True,
required=False,)
text = HtmlText(
title=_(u'Text'),
description=_(u'The text of your blog entry'),
required=False)
class IBlogPost(ICompound): class IBlogPost(ICompound):
""" An item on a blog, sort of a diary item. """ An item on a blog, sort of a diary item.
""" """

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Blogs and blog posts. Blogs and blog posts.
$Id$
""" """
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
@ -32,14 +30,31 @@ from zope.traversing.api import getName
from loops.common import adapted from loops.common import adapted
from loops.compound.base import Compound from loops.compound.base import Compound
from loops.compound.blog.interfaces import IBlogPost from loops.compound.blog.interfaces import ISimpleBlogPost, IBlogPost
from loops.resource import Resource from loops.resource import Resource
from loops.security.common import restrictView from loops.security.common import restrictView
from loops.setup import addAndConfigureObject from loops.setup import addAndConfigureObject
from loops.type import TypeInterfaceSourceList from loops.type import TypeInterfaceSourceList
TypeInterfaceSourceList.typeInterfaces += (IBlogPost,) TypeInterfaceSourceList.typeInterfaces += (ISimpleBlogPost, IBlogPost,)
class SimpleBlogPost(Compound):
implements(ISimpleBlogPost)
textContentType = 'text/html'
_adapterAttributes = Compound._adapterAttributes + ('creator',)
_contextAttributes = list(ISimpleBlogPost)
_noexportAttributes = _adapterAttributes
_textIndexAttributes = ('text',)
@property
def creator(self):
cr = IZopeDublinCore(self.context).creators
return cr and cr[0] or None
class BlogPost(Compound): class BlogPost(Compound):

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Specialized schema factories Specialized schema factories
$Id$
""" """
from zope.component import adapts from zope.component import adapts
@ -34,6 +32,6 @@ class BlogPostSchemaFactory(SchemaFactory):
def __call__(self, interface, **kw): def __call__(self, interface, **kw):
schema = super(BlogPostSchemaFactory, self).__call__(interface, **kw) schema = super(BlogPostSchemaFactory, self).__call__(interface, **kw)
schema.fields.text.height = 10 schema.fields.text.height = 15
return schema return schema

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Security settings for blogs and blog posts. Security settings for blogs and blog posts.
$Id$
""" """
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
@ -30,10 +28,10 @@ from zope.traversing.api import getName
from loops.compound.blog.interfaces import IBlogPost from loops.compound.blog.interfaces import IBlogPost
from loops.security.common import allowEditingForOwner, assignOwner, restrictView from loops.security.common import allowEditingForOwner, assignOwner, restrictView
from loops.security.common import getCurrentPrincipal from loops.security.common import getCurrentPrincipal
from loops.security.setter import BaseSecuritySetter from loops.security.setter import LoopsObjectSecuritySetter
class BlogPostSecuritySetter(BaseSecuritySetter): class BlogPostSecuritySetter(LoopsObjectSecuritySetter):
adapts(IBlogPost) adapts(IBlogPost)

View file

@ -1,5 +1,13 @@
<!-- ZPT macros for loops.compound.blog views <html i18n:domain="loops">
$Id$ -->
<metal:block define-macro="bloglist">
<div tal:define="show_headline nothing">
<metal:fields use-macro="item/template/macros/concepttitle" /><br />
<metal:fields use-macro="item/template/macros/conceptchildren" />
</div>
</metal:block>
<metal:block define-macro="blog"> <metal:block define-macro="blog">
<div> <div>
@ -7,9 +15,12 @@
<div tal:repeat="related item/blogPosts" <div tal:repeat="related item/blogPosts"
class="blog"> class="blog">
<tal:child define="data related/data"> <tal:child define="data related/data">
<tal:actions condition="view/showObjectActions"
define="target nocall:related">
<div metal:use-macro="views/node_macros/object_actions" />
</tal:actions>
<h1 class="headline"> <h1 class="headline">
<a href="#" <a tal:content="related/title"
tal:content="related/title"
tal:attributes="href python: view.getUrlForTarget(related);">Post</a> tal:attributes="href python: view.getUrlForTarget(related);">Post</a>
</h1> </h1>
<div class="info" <div class="info"
@ -51,7 +62,7 @@
<a tal:omit-tag="not:url" <a tal:omit-tag="not:url"
tal:content="data/creator" tal:content="data/creator"
tal:attributes="href url">Will Smith</a> tal:attributes="href url">Will Smith</a>
<span tal:condition="item/adapted/private"> <span tal:condition="item/adapted/private|nothing">
(<span i18n:translate="">Private</span>) (<span i18n:translate="">Private</span>)
</span> </span>
</div> </div>
@ -63,7 +74,7 @@
<div class="text" <div class="text"
tal:content="structure item/render">Here comes the text...</div> tal:content="structure item/render">Here comes the text...</div>
<div class="comment" <div class="comment"
tal:define="comment data/privateComment" tal:define="comment data/privateComment|nothing"
tal:condition="comment"> tal:condition="comment">
<h4 i18n:translate="" class="headline">Private Comment</h4> <h4 i18n:translate="" class="headline">Private Comment</h4>
<div tal:content="structure python: <div tal:content="structure python:
@ -72,3 +83,6 @@
<metal:resources use-macro="view/concept_macros/conceptresources" /> <metal:resources use-macro="view/concept_macros/conceptresources" />
<metal:block use-macro="view/comment_macros/comments" /> <metal:block use-macro="view/comment_macros/comments" />
</div> </div>
</html>

View file

@ -150,7 +150,7 @@ class PageLayout(Base, standard.Layout):
def getParts(self): def getParts(self):
parts = ['headline', 'keyquestions', 'quote', 'maintext', parts = ['headline', 'keyquestions', 'quote', 'maintext',
'story', 'usecase'] 'story', 'tip', 'usecase']
return self.getPartViews(parts) return self.getPartViews(parts)
@ -191,6 +191,11 @@ class Story(PagePart, standard.BasePart):
partName = 'story' partName = 'story'
class Tip(PagePart, standard.BasePart):
partName = 'tip'
class UseCase(PagePart, standard.BasePart): class UseCase(PagePart, standard.BasePart):
partName = 'usecase' partName = 'usecase'

View file

@ -75,6 +75,14 @@
factory="loops.compound.book.browser.Story" factory="loops.compound.book.browser.Story"
permission="zope.View" /> permission="zope.View" />
<zope:adapter
name="lobo_tip"
for="loops.compound.book.interfaces.IPage
loops.browser.skin.Lobo"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.Tip"
permission="zope.View" />
<zope:adapter <zope:adapter
name="lobo_usecase" name="lobo_usecase"
for="loops.compound.book.interfaces.IPage for="loops.compound.book.interfaces.IPage

View file

@ -24,6 +24,7 @@ concept(u'textelement', u'Textabschnitt', u'documenttype')
concept(u'textelement2', u'Textabschnitt separat', u'documenttype') concept(u'textelement2', u'Textabschnitt separat', u'documenttype')
concept(u'quote', u'Zitat', u'documenttype') concept(u'quote', u'Zitat', u'documenttype')
concept(u'story', u'Geschichte', u'documenttype') concept(u'story', u'Geschichte', u'documenttype')
concept(u'tip', u'Tipp', u'documenttype')
concept(u'usecase', u'Fallbeispiel', u'documenttype') concept(u'usecase', u'Fallbeispiel', u'documenttype')
# book structure # book structure

View file

@ -48,6 +48,7 @@ from loops.base import ParentInfo
from loops.common import adapted, AdapterBase from loops.common import adapted, AdapterBase
from loops.i18n.common import I18NValue from loops.i18n.common import I18NValue
from loops.interfaces import IConcept, IConceptRelation, IConceptView from loops.interfaces import IConcept, IConceptRelation, IConceptView
from loops.interfaces import IResource
from loops.interfaces import IConceptManager, IConceptManagerContained from loops.interfaces import IConceptManager, IConceptManagerContained
from loops.interfaces import ILoopsContained from loops.interfaces import ILoopsContained
from loops.interfaces import IIndexAttributes from loops.interfaces import IIndexAttributes
@ -199,7 +200,8 @@ class Concept(Contained, Persistent):
if sort == 'default': if sort == 'default':
sort = lambda x: (x.order, (x.second.title and x.second.title.lower())) sort = lambda x: (x.order, (x.second.title and x.second.title.lower()))
rels = (r for r in getRelations(self, child, relationships=relationships) rels = (r for r in getRelations(self, child, relationships=relationships)
if canListObject(r.second, noSecurityCheck)) if canListObject(r.second, noSecurityCheck) and
IConcept.providedBy(r.second))
return sorted(rels, key=sort) return sorted(rels, key=sort)
def getChildren(self, predicates=None, sort='default', noSecurityCheck=False): def getChildren(self, predicates=None, sort='default', noSecurityCheck=False):
@ -294,7 +296,8 @@ class Concept(Contained, Persistent):
if sort == 'default': if sort == 'default':
sort = lambda x: (x.order, x.second.title.lower()) sort = lambda x: (x.order, x.second.title.lower())
rels = (r for r in getRelations(self, resource, relationships=relationships) rels = (r for r in getRelations(self, resource, relationships=relationships)
if canListObject(r.second, noSecurityCheck)) if canListObject(r.second, noSecurityCheck) and
IResource.providedBy(r.second))
return sorted(rels, key=sort) return sorted(rels, key=sort)
def getResources(self, predicates=None, sort='default', noSecurityCheck=False): def getResources(self, predicates=None, sort='default', noSecurityCheck=False):

View file

@ -27,7 +27,7 @@ configuration):
>>> concepts, resources, views = t.setup() >>> concepts, resources, views = t.setup()
>>> len(concepts) + len(resources) >>> len(concepts) + len(resources)
37 36
>>> loopsRoot = site['loops'] >>> loopsRoot = site['loops']
@ -47,11 +47,11 @@ Type- and text-based queries
>>> from loops.expert import query >>> from loops.expert import query
>>> qu = query.Title('ty*') >>> qu = query.Title('ty*')
>>> list(qu.apply()) >>> list(qu.apply())
[0, 2, 68] [0, 2, 65]
>>> qu = query.Type('loops:*') >>> qu = query.Type('loops:*')
>>> len(list(qu.apply())) >>> len(list(qu.apply()))
37 36
>>> qu = query.Type('loops:concept:predicate') >>> qu = query.Type('loops:concept:predicate')
>>> len(list(qu.apply())) >>> len(list(qu.apply()))

View file

@ -75,4 +75,20 @@
class="loops.expert.browser.report.ResultsView" class="loops.expert.browser.report.ResultsView"
permission="zope.View" /> permission="zope.View" />
<zope:adapter
name="concept_report.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.expert.browser.report.ReportConceptView"
permission="zope.View" />
<zope:adapter
name="concept_results.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.expert.browser.report.ResultsConceptView"
permission="zope.View" />
</configure> </configure>

View file

@ -165,10 +165,20 @@ class ResultsConceptView(ConceptView):
def hasReportPredicate(self): def hasReportPredicate(self):
return self.conceptManager['hasreport'] 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 @Lazy
def report(self): def report(self):
if self.reportName: if self.reportName:
return adapted(self.conceptManager[self.reportName]) return adapted(self.conceptManager[self.reportName])
reports = self.context.getParents([self.hasReportPredicate])
if not reports:
type = self.context.conceptType type = self.context.conceptType
reports = type.getParents([self.hasReportPredicate]) reports = type.getParents([self.hasReportPredicate])
return adapted(reports[0]) return adapted(reports[0])

View file

@ -368,7 +368,8 @@
i18n:translate=""></td> i18n:translate=""></td>
<td colspan="2"> <td colspan="2">
<tal:states repeat="state def/states"> <tal:states repeat="state def/states">
<tal:state define="name string:state.$deftype.${def/name}; <tal:state define="xx_name string:state.$deftype.${def/name};
name string:state.${def/name};
value state/name"> value state/name">
<input type="checkbox" <input type="checkbox"
tal:attributes="name string:$name:list; tal:attributes="name string:$name:list;

View file

@ -196,8 +196,9 @@ class Search(ConceptView):
@Lazy @Lazy
def statesDefinitions(self): def statesDefinitions(self):
return [component.getUtility(IStatesDefinition, name=n) stdnames = (self.globalOptions('organize.stateful.resource', []) +
for n in self.globalOptions('organize.stateful.resource', ())] self.globalOptions('organize.stateful.special', []))
return [component.getUtility(IStatesDefinition, name=n) for n in stdnames]
@Lazy @Lazy
def selectedStates(self): def selectedStates(self):
@ -259,6 +260,8 @@ class Search(ConceptView):
for std, states in self.selectedStates.items(): for std, states in self.selectedStates.items():
if std.startswith('state.resource.'): if std.startswith('state.resource.'):
std = std[len('state.resource.'):] std = std[len('state.resource.'):]
elif std.startswith('state.'):
std = std[len('state.'):]
else: else:
continue continue
stf = component.queryAdapter(obj, IStateful, name=std) stf = component.queryAdapter(obj, IStateful, name=std)

View file

@ -38,6 +38,10 @@
set_schema="loops.expert.report.IReportInstance" /> set_schema="loops.expert.report.IReportInstance" />
</class> </class>
<adapter factory="loops.expert.standard.TypeInstances"
name="type_instances"
provides="loops.expert.report.IReportInstance" />
<utility <utility
provides="zope.schema.interfaces.IVocabularyFactory" provides="zope.schema.interfaces.IVocabularyFactory"
component="loops.expert.report.ReportTypeSourceList" component="loops.expert.report.ReportTypeSourceList"

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -60,6 +60,11 @@ class TextField(Field):
return text return text
class HtmlTextField(Field):
format = 'text/html'
class DecimalField(Field): class DecimalField(Field):
format = 'decimal' format = 'decimal'

View file

@ -101,9 +101,11 @@ class ReportInstance(BaseReport):
def getResults(self, dynaParams=None): def getResults(self, dynaParams=None):
crit = self.queryCriteria crit = self.queryCriteria
if crit is None:
return []
limits = self.limits limits = self.limits
if crit is None:
parts = Jeep()
#return ResultSet(self, [])
else:
if dynaParams is not None: if dynaParams is not None:
for k, v in dynaParams.items(): for k, v in dynaParams.items():
if k == 'limits': if k == 'limits':

View file

@ -66,13 +66,13 @@ zcml in real life:
>>> t = searchView.typesForSearch() >>> t = searchView.typesForSearch()
>>> len(t) >>> len(t)
16 15
>>> t.getTermByToken('loops:resource:*').title >>> t.getTermByToken('loops:resource:*').title
'Any Resource' 'Any Resource'
>>> t = searchView.conceptTypesForSearch() >>> t = searchView.conceptTypesForSearch()
>>> len(t) >>> len(t)
13 12
>>> t.getTermByToken('loops:concept:*').title >>> t.getTermByToken('loops:concept:*').title
'Any Concept' 'Any Concept'
@ -91,7 +91,7 @@ a controller attribute for the search view.
>>> searchView.submitReplacing('1.results', '1.search.form', pageView) >>> searchView.submitReplacing('1.results', '1.search.form', pageView)
'submitReplacing("1.results", "1.search.form", '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 Basic (text/title) search
------------------------- -------------------------
@ -177,7 +177,7 @@ of the concepts' titles:
>>> request = TestRequest(form=form) >>> request = TestRequest(form=form)
>>> view = Search(page, request) >>> view = Search(page, request)
>>> view.listConcepts() >>> 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 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') >>> searchView.conceptsForType('loops:concept:customer')
[{'token': 'none', 'title': u'not selected'}, [{'token': 'none', 'title': u'not selected'},
{'token': '77', 'title': u'Customer 1'}, {'token': '74', 'title': u'Customer 1'},
{'token': '79', 'title': u'Customer 2'}, {'token': '76', 'title': u'Customer 2'},
{'token': '81', 'title': u'Customer 3'}] {'token': '78', 'title': u'Customer 3'}]
Let's use this new search option for querying: 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)) >>> resultsView = SearchResults(page, TestRequest(form=form))
>>> results = list(resultsView.results) >>> results = list(resultsView.results)
>>> results[0].title >>> results[0].title

54
expert/standard.py Normal file
View file

@ -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

6
external/README.txt vendored
View file

@ -17,7 +17,7 @@ Let's set up a loops site with basic and example concepts and resources.
>>> concepts, resources, views = t.setup() >>> concepts, resources, views = t.setup()
>>> loopsRoot = site['loops'] >>> loopsRoot = site['loops']
>>> len(concepts), len(resources), len(views) >>> len(concepts), len(resources), len(views)
(34, 3, 1) (33, 3, 1)
Importing loops Objects Importing loops Objects
@ -44,7 +44,7 @@ Creating the corresponding objects
>>> loader = Loader(loopsRoot) >>> loader = Loader(loopsRoot)
>>> loader.load(elements) >>> loader.load(elements)
>>> len(concepts), len(resources), len(views) >>> len(concepts), len(resources), len(views)
(35, 3, 1) (34, 3, 1)
>>> from loops.common import adapted >>> from loops.common import adapted
>>> adMyquery = adapted(concepts['myquery']) >>> adMyquery = adapted(concepts['myquery'])
@ -131,7 +131,7 @@ Extracting elements
>>> extractor = Extractor(loopsRoot, os.path.join(dataDirectory, 'export')) >>> extractor = Extractor(loopsRoot, os.path.join(dataDirectory, 'export'))
>>> elements = list(extractor.extract()) >>> elements = list(extractor.extract())
>>> len(elements) >>> len(elements)
68 66
Writing object information to the external storage Writing object information to the external storage
-------------------------------------------------- --------------------------------------------------

View file

@ -197,6 +197,8 @@ class DirectoryCollectionProvider(object):
extFileType = extFileTypes.get(contentType.split('/')[0] + '/*') extFileType = extFileTypes.get(contentType.split('/')[0] + '/*')
if extFileType is None: if extFileType is None:
extFileType = extFileTypes['*/*'] extFileType = extFileTypes['*/*']
if extFileType is None:
extFileType = extFileTypes['image/*']
if extFileType is None: if extFileType is None:
getLogger('loops.integrator.collection.DirectoryCollectionProvider' getLogger('loops.integrator.collection.DirectoryCollectionProvider'
).warn('No external file type found for %r, ' ).warn('No external file type found for %r, '

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -30,6 +30,7 @@ from zope.app.file.interfaces import IImage as IBaseAsset
from zope.component.interfaces import IObjectEvent from zope.component.interfaces import IObjectEvent
from zope.size.interfaces import ISized from zope.size.interfaces import ISized
from cybertools.composer.schema.interfaces import FieldType
from cybertools.relation.interfaces import IDyadicRelation from cybertools.relation.interfaces import IDyadicRelation
from cybertools.tracking.interfaces import ITrackingStorage from cybertools.tracking.interfaces import ITrackingStorage
from cybertools.util.format import toStr, toUnicode from cybertools.util.format import toStr, toUnicode
@ -37,6 +38,11 @@ from loops import util
from loops.util import _ from loops.util import _
class HtmlText(schema.Text):
__typeInfo__ = ('html',)
# common interfaces # common interfaces
class ILoopsObject(Interface): class ILoopsObject(Interface):

View file

@ -170,28 +170,7 @@ For testing, we first have to provide the needed utilities and settings
Competence and Certification Management Competence and Certification Management
======================================= =======================================
>>> from cybertools.stateful.interfaces import IStatesDefinition >>> tCompetence = concepts['competence']
>>> 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
Glossaries 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 >>> 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 Fin de partie
============= =============

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -30,10 +30,7 @@ from cybertools.typology.interfaces import IType
from loops.browser.action import DialogAction from loops.browser.action import DialogAction
from loops.browser.common import BaseView from loops.browser.common import BaseView
from loops.browser.concept import ConceptView from loops.browser.concept import ConceptView
from loops.expert.browser.report import ResultsConceptView
from loops.knowledge.interfaces import IPerson, ITask 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.organize.party import getPersonForUser
from loops.util import _ from loops.util import _
@ -50,6 +47,7 @@ actions.register('createTopic', 'portlet', DialogAction,
typeToken='.loops/concepts/topic', typeToken='.loops/concepts/topic',
fixedType=True, fixedType=True,
innerForm='inner_concept_form.html', innerForm='inner_concept_form.html',
permission='loops.AssignAsParent',
) )
actions.register('editTopic', 'portlet', DialogAction, actions.register('editTopic', 'portlet', DialogAction,
@ -57,6 +55,7 @@ actions.register('editTopic', 'portlet', DialogAction,
description=_(u'Modify topic.'), description=_(u'Modify topic.'),
viewName='edit_concept.html', viewName='edit_concept.html',
dialogName='editTopic', dialogName='editTopic',
permission='zope.ManageContent',
) )
actions.register('createQualification', 'portlet', DialogAction, actions.register('createQualification', 'portlet', DialogAction,
@ -66,6 +65,7 @@ actions.register('createQualification', 'portlet', DialogAction,
dialogName='createQualification', dialogName='createQualification',
prerequisites=['registerDojoDateWidget', 'registerDojoNumberWidget', prerequisites=['registerDojoDateWidget', 'registerDojoNumberWidget',
'registerDojoTextarea'], 'registerDojoTextarea'],
permission='loops.AssignAsParent',
) )
@ -111,25 +111,3 @@ class Candidates(ConceptView):
return self.template.macros['requirement_candidates'] 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

View file

@ -1,5 +1,3 @@
<!-- $Id$ -->
<configure <configure
xmlns:zope="http://namespaces.zope.org/zope" xmlns:zope="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser" xmlns:browser="http://namespaces.zope.org/browser"
@ -66,22 +64,6 @@
interface="cybertools.knowledge.interfaces.IKnowledgeProvider" /> interface="cybertools.knowledge.interfaces.IKnowledgeProvider" />
</zope:class> </zope:class>
<!-- records -->
<zope:class class="loops.knowledge.qualification.QualificationRecord">
<require permission="zope.View"
interface="loops.knowledge.interfaces.IQualificationRecord" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.interfaces.IQualificationRecord" />
</zope:class>
<zope:adapter factory="loops.knowledge.qualification.QualificationRecords"
provides="loops.knowledge.interfaces.IQualificationRecords" />
<zope:utility factory="loops.knowledge.qualification.qualificationStates"
provides="cybertools.stateful.interfaces.IStatesDefinition"
name="knowledge.qualification" />
<!-- views --> <!-- views -->
<zope:adapter <zope:adapter
@ -100,19 +82,6 @@
factory="loops.knowledge.browser.Candidates" factory="loops.knowledge.browser.Candidates"
permission="zope.View" /> permission="zope.View" />
<browser:page
name="create_qualification.html"
for="loops.interfaces.INode"
class="loops.knowledge.browser.CreateQualificationRecordForm"
permission="zope.View" />
<zope:adapter
name="create_qualification"
for="loops.browser.node.NodeView
zope.publisher.interfaces.browser.IBrowserRequest"
factory="loops.knowledge.browser.CreateQualificationRecord"
permission="zope.View" />
<!-- other adapters --> <!-- other adapters -->
<zope:adapter factory="loops.knowledge.schema.PersonSchemaFactory" /> <zope:adapter factory="loops.knowledge.schema.PersonSchemaFactory" />
@ -120,5 +89,7 @@
<!-- sub-packages --> <!-- sub-packages -->
<include package=".glossary" /> <include package=".glossary" />
<include package=".qualification" />
<include package=".survey" />
</configure> </configure>

View file

@ -1,5 +1,6 @@
type(u'competence', u'Kompetenz', viewName=u'', 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'', type(u'person', u'Person', viewName=u'',
typeInterface=u'loops.knowledge.interfaces.IPerson', typeInterface=u'loops.knowledge.interfaces.IPerson',
options=u'action.portlet:createQualification,editPerson') options=u'action.portlet:createQualification,editPerson')
@ -9,9 +10,9 @@ type(u'task', u'Aufgabe', viewName=u'',
type(u'topic', u'Thema', viewName=u'', type(u'topic', u'Thema', viewName=u'',
typeInterface=u'loops.knowledge.interfaces.ITopic', typeInterface=u'loops.knowledge.interfaces.ITopic',
options=u'action.portlet:createTask,createTopic,editTopic') options=u'action.portlet:createTask,createTopic,editTopic')
type(u'training', u'Schulung', viewName=u'', #type(u'training', u'Schulung', viewName=u'',
typeInterface=u'loops.organize.interfaces.ITask', # typeInterface=u'loops.organize.interfaces.ITask',
options=u'action.portlet:edit_concept') # options=u'action.portlet:edit_concept')
concept(u'general', u'Allgemein', u'domain') concept(u'general', u'Allgemein', u'domain')
concept(u'system', u'System', 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'requires', u'standard')
child(u'general', u'task', u'standard') child(u'general', u'task', u'standard')
child(u'general', u'topic', 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'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
records(u'qualification', u'loops.knowledge.qualification.QualificationRecord') #records(u'qualification', u'loops.knowledge.qualification.base.QualificationRecord')

View file

@ -1,5 +1,6 @@
type(u'competence', u'Kompetenz', viewName=u'', 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'', # type(u'person', u'Person', viewName=u'',
# typeInterface=u'loops.knowledge.interfaces.IPerson', # typeInterface=u'loops.knowledge.interfaces.IPerson',
# options=u'action.portlet:editPerson') # options=u'action.portlet:editPerson')
@ -9,9 +10,9 @@ type(u'competence', u'Kompetenz', viewName=u'',
# type(u'topic', u'Thema', viewName=u'', # type(u'topic', u'Thema', viewName=u'',
# typeInterface=u'loops.knowledge.interfaces.ITopic', # typeInterface=u'loops.knowledge.interfaces.ITopic',
# options=u'action.portlet:createTask,createTopic,editTopic') # options=u'action.portlet:createTask,createTopic,editTopic')
type(u'training', u'Schulung', viewName=u'', #type(u'training', u'Schulung', viewName=u'',
typeInterface=u'loops.organize.interfaces.ITask', # typeInterface=u'loops.organize.interfaces.ITask',
options=u'action.portlet:edit_concept') # options=u'action.portlet:edit_concept')
concept(u'general', u'Allgemein', u'domain') concept(u'general', u'Allgemein', u'domain')
concept(u'system', u'System', 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'requires', u'standard')
#child(u'general', u'task', u'standard') #child(u'general', u'task', u'standard')
#child(u'general', u'topic', 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'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
records(u'qualification', u'loops.knowledge.qualification.QualificationRecord') #records(u'qualification', u'loops.knowledge.qualification.base.QualificationRecord')

View file

@ -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')

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -22,18 +22,14 @@ Interfaces for knowledge management and elearning with loops.
from zope.interface import Interface, Attribute from zope.interface import Interface, Attribute
from zope import interface, component, schema 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 IKnowing, IRequirementProfile
from cybertools.knowledge.interfaces import IKnowledgeElement from cybertools.knowledge.interfaces import IKnowledgeElement
from cybertools.organize.interfaces import IWorkItem, IWorkItems
from loops.interfaces import IConceptSchema, ILoopsAdapter from loops.interfaces import IConceptSchema, ILoopsAdapter
from loops.organize.interfaces import IPerson as IBasePerson from loops.organize.interfaces import IPerson as IBasePerson
from loops.organize.interfaces import ITask as IBaseTask from loops.organize.interfaces import ITask as IBaseTask
from loops.schema.base import Relation, RelationSet from loops.schema.base import Relation, RelationSet
from loops.util import _
_ = MessageFactory('loops')
class IPerson(IBasePerson, IKnowing): class IPerson(IBasePerson, IKnowing):
@ -62,13 +58,3 @@ class ITopic(IConceptSchema, IKnowledgeElement, ILoopsAdapter):
""" Just a topic, some general classification concept. """ 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.
"""

View file

@ -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)

View file

@ -0,0 +1 @@
'''package loops.knowledge.qualification'''

View file

@ -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)

View file

@ -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

View file

@ -0,0 +1,11 @@
<configure
xmlns:zope="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="loops">
<zope:adapter
factory="loops.knowledge.qualification.base.Competence" />
<!-- views -->
</configure>

View file

@ -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)

View file

@ -0,0 +1 @@
'''package loops.knowledge.survey'''

124
knowledge/survey/base.py Normal file
View file

@ -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

174
knowledge/survey/browser.py Normal file
View file

@ -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<br />(%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')

View file

@ -0,0 +1,76 @@
<configure
xmlns:zope="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="loops">
<!-- concept classes -->
<zope:adapter
factory="loops.knowledge.survey.base.Questionnaire"
provides="loops.knowledge.survey.interfaces.IQuestionnaire"
trusted="True" />
<zope:class class="loops.knowledge.survey.base.Questionnaire">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestionnaire" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestionnaire" />
</zope:class>
<zope:adapter
factory="loops.knowledge.survey.base.QuestionGroup"
provides="loops.knowledge.survey.interfaces.IQuestionGroup"
trusted="True" />
<zope:class class="loops.knowledge.survey.base.QuestionGroup">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestionGroup" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestionGroup" />
</zope:class>
<zope:adapter
factory="loops.knowledge.survey.base.Question"
provides="loops.knowledge.survey.interfaces.IQuestion"
trusted="True" />
<zope:class class="loops.knowledge.survey.base.Question">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestion" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestion" />
</zope:class>
<zope:adapter
factory="loops.knowledge.survey.base.FeedbackItem"
provides="loops.knowledge.survey.interfaces.IFeedbackItem"
trusted="True" />
<zope:class class="loops.knowledge.survey.base.FeedbackItem">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IFeedbackItem" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IFeedbackItem" />
</zope:class>
<!-- track -->
<zope:class class="loops.knowledge.survey.response.Response">
<require permission="zope.View"
interface="cybertools.tracking.interfaces.ITrack" />
<require permission="zope.ManageContent"
set_schema="cybertools.tracking.interfaces.ITrack" />
</zope:class>
<!-- views -->
<zope:adapter
name="survey.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.knowledge.survey.browser.SurveyView"
permission="zope.View" />
<browser:page name="survey_data.csv"
for="loops.interfaces.IView"
class="loops.knowledge.survey.browser.SurveyCsvExport"
permission="zope.View" />
</configure>

View file

@ -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.
"""

View file

@ -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'

View file

@ -0,0 +1,93 @@
<!-- ZPT macros for loops.knowledge.survey views -->
<html i18n:domain="loops">
<metal:block define-macro="survey"
tal:define="feedback item/results;
errors item/errors">
<metal:title use-macro="item/conceptMacros/concepttitle_only" />
<tal:description condition="not:feedback">
<metal:title use-macro="item/conceptMacros/conceptdescription" />
</tal:description>
<div tal:condition="feedback">
<h3 i18n:translate="">Feedback</h3>
<table class="listing">
<tr>
<th i18n:translate="">Category</th>
<th i18n:translate="">Response</th>
<th i18n:translate="">%</th>
</tr>
<tr tal:repeat="fbitem feedback">
<td tal:content="fbitem/category" />
<td tal:content="fbitem/text" />
<td tal:content="fbitem/score" />
</tr>
</table>
<div class="button" id="show_questionnaire">
<a href="" onclick="back(); return false"
i18n:translate="">
Back to Questionnaire</a>
<br />
</div>
<div tal:define="footer item/adapted/feedbackFooter"
tal:condition="footer"
tal:content="structure python:item.renderText(footer, 'text/restructured')" />
</div>
<div id="questionnaire"
tal:condition="not:feedback">
<h3 i18n:translate="">Questionnaire</h3>
<div class="error"
tal:condition="errors">
<div tal:repeat="error errors">
<span i18n:translate=""
tal:content="error" />
</div>
</div>
<form method="post">
<table class="listing">
<tal:qugroup repeat="qugroup item/adapted/questionGroups">
<tr><td colspan="6">&nbsp;</td></tr>
<tr>
<td tal:define="infoText python:item.getInfoText(qugroup)">
<b tal:content="qugroup/title" />
<div tal:condition="infoText">
<span tal:content="structure infoText" />
<br />&nbsp;
</div>
</td>
<td style="text-align: center"
i18n:translate="">No answer</td>
<td colspan="2"
i18n:translate="">Fully applies</td>
<td colspan="2"
style="text-align: right"
i18n:translate="">Does not apply</td>
</tr>
<tr tal:repeat="question qugroup/questions">
<td tal:content="question/text" />
<td style="white-space: nowrap; text-align: center"
tal:repeat="value python:item.getValues(question)">
<input type="radio"
i18n:attributes="title"
tal:condition="value/radio"
tal:attributes="
name string:question_${question/uid};
value value/value;
checked value/checked;
title string:survey_value_${value/value}" />
<span tal:condition="not:value/radio"
title="Obligatory question, must be answered"
i18n:attributes="title">***
</span>
</td>
</tr>
</tal:qugroup>
</table>
<input type="submit" name="submit" value="Evaluate Questionnaire"
i18n:attributes="value" />
</form>
</div>
</metal:block>
</html>

View file

@ -2,16 +2,31 @@
import os import os
import unittest, doctest import unittest, doctest
from zope.testing.doctestunit import DocFileSuite
from zope.app.testing import ztapi from zope.app.testing import ztapi
from zope import component
from zope.interface.verify import verifyClass 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.organize.party import Person
from loops.setup import importData as baseImportData from loops.setup import importData as baseImportData
def importData(loopsRoot):
importPath = os.path.join(os.path.dirname(__file__), 'data') importPath = os.path.join(os.path.dirname(__file__), 'data')
baseImportData(loopsRoot, importPath, 'loops_knowledge_de.dmp')
def importData(loopsRoot):
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): class Test(unittest.TestCase):

Binary file not shown.

View file

@ -1,9 +1,9 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: $Id$\n" "Project-Id-Version: 0.13.0\n"
"POT-Creation-Date: 2007-05-22 12:00 CET\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 <helmutm@cy55.de>\n" "Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
"Language-Team: loops developers <helmutm@cy55.de>\n" "Language-Team: loops developers <helmutm@cy55.de>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -170,6 +170,105 @@ msgstr "Glossareintrag anlegen..."
msgid "Create Glossary Item" msgid "Create Glossary Item"
msgstr "Glossareintrag anlegen." 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..." msgid "Create Person..."
msgstr "Person anlegen..." msgstr "Person anlegen..."
@ -212,6 +311,8 @@ msgstr "Adresse bearbeiten..."
msgid "Modify address." msgid "Modify address."
msgstr "Adresse bearbeiten." msgstr "Adresse bearbeiten."
# general
msgid "Create Concept, Type = " msgid "Create Concept, Type = "
msgstr "Begriff anlegen, Typ = " msgstr "Begriff anlegen, Typ = "
@ -230,6 +331,8 @@ msgstr "Diese Ressource bearbeiten."
msgid "Edit Concept" msgid "Edit Concept"
msgstr "Begriff bearbeiten" msgstr "Begriff bearbeiten"
# events and tasks
msgid "Create Event..." msgid "Create Event..."
msgstr "Termin anlegen..." msgstr "Termin anlegen..."
@ -443,9 +546,15 @@ msgstr "Überschrift, sprechende Bezeichnung des Begriffs"
msgid "Description" msgid "Description"
msgstr "Beschreibung" msgstr "Beschreibung"
msgid "label_description"
msgstr "Beschreibung"
msgid "A medium-length description describing the content and the purpose of the object" 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" 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" msgid "Related Items"
msgstr "Verwandte Begriffe" msgstr "Verwandte Begriffe"
@ -707,9 +816,6 @@ msgstr "Deckungsgrad"
msgid "Create loops Note" msgid "Create loops Note"
msgstr "loops-Notiz anlegen" msgstr "loops-Notiz anlegen"
msgid "State information for $definition: $title"
msgstr "Status ($definition): $title"
msgid "User ID" msgid "User ID"
msgstr "Benutzerkennung" msgstr "Benutzerkennung"
@ -886,65 +992,178 @@ msgstr "an"
msgid "move_to_task" msgid "move_to_task"
msgstr "nach" 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" msgid "new"
msgstr "neu" msgstr "neu"
msgid "planned" msgid "planned"
msgstr "geplant" msgstr "geplant"
msgid "accepted" msgid "private"
msgstr "angenommen" msgstr "privat"
msgid "delegated" msgid "published"
msgstr "delegiert" msgstr "veröffentlicht"
msgid "running" msgid "removed"
msgstr "in Arbeit" msgstr "gelöscht"
msgid "done"
msgstr "bearbeitet"
msgid "finished"
msgstr "beendet"
msgid "closed"
msgstr "abgeschlossen"
msgid "cancelled"
msgstr "abgebrochen"
msgid "moved"
msgstr "verschoben"
msgid "replaced" msgid "replaced"
msgstr "ersetzt" msgstr "ersetzt"
msgid "plan" msgid "running"
msgstr "planen" msgstr "in Arbeit"
msgid "unclassified"
msgstr "unklassifiziert"
msgid "verified"
msgstr "verifiziert"
# transitions
msgid "accept" msgid "accept"
msgstr "annehmen" msgstr "annehmen"
msgid "start working" msgid "archive"
msgstr "Arbeit beginnen" msgstr "archivieren"
msgid "work" msgid "classify"
msgstr "bearbeiten" msgstr "klassifizieren"
msgid "finish" msgid "change_classification"
msgstr "beenden" msgstr "Klassifizierung ändern"
msgid "cancel" msgid "cancel"
msgstr "abbrechen" msgstr "abbrechen"
msgid "close"
msgstr "abschließen"
msgid "delegate" msgid "delegate"
msgstr "delegieren" msgstr "delegieren"
msgid "finish"
msgstr "beenden"
msgid "finish (published)"
msgstr "beenden (zugänglich)"
msgid "hide"
msgstr "verstecken"
msgid "move" msgid "move"
msgstr "verschieben" msgstr "verschieben"
msgid "close" msgid "No change"
msgstr "abschließen" 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" msgid "Monday"
msgstr "Montag" msgstr "Montag"

View file

@ -2,8 +2,6 @@
loops - Linked Objects for Organization and Processing Services loops - Linked Objects for Organization and Processing Services
=============================================================== ===============================================================
($Id$)
Note: This packages depends on cybertools.organize. Note: This packages depends on cybertools.organize.
Let's do some basic setup 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 >>> from zope.securitypolicy.interfaces import IPrincipalRoleMap
>>> IPrincipalRoleMap(concepts['john']).getPrincipalsAndRoles() >>> IPrincipalRoleMap(concepts['john']).getPrincipalsAndRoles()
[('loops.Owner', 'users.john', PermissionSetting: Allow)] [('loops.Person', 'users.john', PermissionSetting: Allow)]
>>> IPrincipalRoleMap(concepts['person.newuser']).getPrincipalsAndRoles() >>> 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 The person ``martha`` hasn't got a user id, so there is no role assigned
to it. to it.
@ -307,9 +305,12 @@ Now we are ready to look for the real stuff - what John is allowed to do.
True True
Person objects that have an owner may be modified by this owner. Person objects that have an owner may be modified by this owner.
(Changed in 2013-01-14: Owner not set automatically)
>>> canWrite(john, 'title') >>> canWrite(john, 'title')
True False
was: True
So let's try with another user with another role setting. So let's try with another user with another role setting.
@ -409,7 +410,7 @@ Send Email to Members
>>> form.subject >>> form.subject
u"loops Notification from '$site'" u"loops Notification from '$site'"
>>> form.mailBody >>> 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 Show Presence of Other Users

View file

@ -56,6 +56,7 @@ actions.register('createEvent', 'portlet', DialogAction,
typeToken='.loops/concepts/event', typeToken='.loops/concepts/event',
fixedType=True, fixedType=True,
prerequisites=['registerDojoDateWidget'], prerequisites=['registerDojoDateWidget'],
permission='loops.AssignAsParent',
) )
actions.register('editEvent', 'portlet', DialogAction, actions.register('editEvent', 'portlet', DialogAction,
@ -383,8 +384,9 @@ class CreateFollowUpEvent(CreateConcept, BaseFollowUpController):
bevt = baseObject(self.baseEvent) bevt = baseObject(self.baseEvent)
bevt.assignChild(obj, self.followsPredicate) bevt.assignChild(obj, self.followsPredicate)
for rel in bevt.getParentRelations(): for rel in bevt.getParentRelations():
if rel.predicate != self.view.typePredicate: if rel.predicate not in (self.view.typePredicate, self.followsPredicate):
obj.assignParent(rel.first, rel.predicate) obj.assignParent(rel.first, rel.predicate,
order=rel.order, relevance=rel.relevance)
class EditFollowUpEvent(EditConcept, BaseFollowUpController): class EditFollowUpEvent(EditConcept, BaseFollowUpController):

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -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 IPerson as IBasePerson
from cybertools.organize.interfaces import ITask from cybertools.organize.interfaces import ITask
from loops.interfaces import ILoopsAdapter, IConceptSchema, IRelationAdapter from loops.interfaces import ILoopsAdapter, IConceptSchema, IRelationAdapter
from loops.interfaces import HtmlText
from loops.organize.util import getPrincipalFolder from loops.organize.util import getPrincipalFolder
from loops import util from loops import util
from loops.util import _ from loops.util import _
@ -173,26 +174,36 @@ class IEvent(ITask):
class IAgendaItem(ILoopsAdapter): class IAgendaItem(ILoopsAdapter):
description = HtmlText(
title=_(u'label_description'),
description=_(u'desc_description'),
default=u'',
missing_value=u'',
required=False)
responsible = schema.TextLine( responsible = schema.TextLine(
title=_(u'label_responsible'), title=_(u'label_responsible'),
description=_(u'desc_responsible.'), description=_(u'desc_responsible'),
default=u'', default=u'',
required=False) required=False)
discussion = schema.Text( discussion = HtmlText(
title=_(u'label_discussion'), title=_(u'label_discussion'),
description=_(u'desc_discussion.'), description=_(u'desc_discussion'),
default=u'', default=u'',
missing_value=u'', missing_value=u'',
required=False) required=False)
consequences = schema.Text( consequences = HtmlText(
title=_(u'label_consequences'), title=_(u'label_consequences'),
description=_(u'desc_consequences.'), description=_(u'desc_consequences'),
default=u'', default=u'',
missing_value=u'', missing_value=u'',
required=False) required=False)
description.height = 10
discussion.height = consequences.height = 7
# 'hasrole' predicate # 'hasrole' predicate

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -41,9 +41,11 @@ from loops.interfaces import IConcept
from loops.organize.interfaces import IAddress, IPerson, IHasRole from loops.organize.interfaces import IAddress, IPerson, IHasRole
from loops.organize.interfaces import ANNOTATION_KEY from loops.organize.interfaces import ANNOTATION_KEY
from loops.predicate import RelationAdapter 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.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 from loops import util
@ -82,6 +84,7 @@ class Person(AdapterBase, BasePerson):
def getUserId(self): def getUserId(self):
return getattr(self.context, '_userId', None) return getattr(self.context, '_userId', None)
def setUserId(self, userId): def setUserId(self, userId):
setter = ISecuritySetter(self)
if userId: if userId:
principal = self.getPrincipalForUserId(userId) principal = self.getPrincipalForUserId(userId)
if principal is None: if principal is None:
@ -97,13 +100,16 @@ class Person(AdapterBase, BasePerson):
if ann is None: # or not isinstance(ann, PersistentMapping): if ann is None: # or not isinstance(ann, PersistentMapping):
ann = pa[ANNOTATION_KEY] = PersistentMapping() ann = pa[ANNOTATION_KEY] = PersistentMapping()
ann[loopsId] = self.context ann[loopsId] = self.context
assignOwner(self.context, userId) #assignOwner(self.context, userId)
assignPersonRole(self.context, userId)
oldUserId = self.userId oldUserId = self.userId
if oldUserId and oldUserId != userId: if oldUserId and oldUserId != userId:
self.removeReferenceFromPrincipal(oldUserId) self.removeReferenceFromPrincipal(oldUserId)
removeOwner(self.context, oldUserId) removeOwner(self.context, oldUserId)
removePersonRole(self.context, oldUserId)
self.context._userId = userId self.context._userId = userId
allowEditingForOwner(self.context, revert=not userId) setter.propagateSecurity()
allowEditingForOwner(self.context, revert=not userId) # why this?
userId = property(getUserId, setUserId) userId = property(getUserId, setUserId)
def removeReferenceFromPrincipal(self, userId): def removeReferenceFromPrincipal(self, userId):

View file

@ -27,6 +27,7 @@ from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from cybertools.browser.configurator import ViewConfigurator, MacroViewProperty from cybertools.browser.configurator import ViewConfigurator, MacroViewProperty
from cybertools.stateful.interfaces import IStateful
from loops.browser.node import NodeView from loops.browser.node import NodeView
from loops.concept import Concept from loops.concept import Concept
from loops.organize.party import getPersonForUser from loops.organize.party import getPersonForUser
@ -107,7 +108,30 @@ class FilterView(NodeView):
result.setdefault(obj.getType(), set([])).add(obj) result.setdefault(obj.getType(), set([])).add(obj)
return result 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 fs = self.filterStructure
if not fs: if not fs:
return True return True

View file

@ -2,8 +2,6 @@
loops - Linked Objects for Organization and Processing Services loops - Linked Objects for Organization and Processing Services
=============================================================== ===============================================================
($Id$)
>>> from zope import component >>> from zope import component
>>> from zope.traversing.api import getName >>> 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 Fin de partie
============= =============

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,13 +18,12 @@
""" """
Views and actions for states management. Views and actions for states management.
$Id$
""" """
from zope import component from zope import component
from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.i18n import translate
from cybertools.browser.action import Action, actions from cybertools.browser.action import Action, actions
from cybertools.stateful.interfaces import IStateful, IStatesDefinition from cybertools.stateful.interfaces import IStateful, IStatesDefinition
@ -36,9 +35,12 @@ from loops.security.common import checkPermission
from loops.util import _ from loops.util import _
template = ViewPageTemplateFile('view_macros.pt')
statefulActions = ('classification_quality', statefulActions = ('classification_quality',
'simple_publishing', 'simple_publishing',
'task_states',) 'task_states',
'publishable_task',)
class StateAction(Action): class StateAction(Action):
@ -53,9 +55,11 @@ class StateAction(Action):
@Lazy @Lazy
def description(self): 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', return _(u'State information for $definition: $title',
mapping=dict(definition=self.definition, mapping=dict(definition=definition, title=title))
title=self.stateObject.title))
@Lazy @Lazy
def stateObject(self): def stateObject(self):
@ -77,7 +81,7 @@ for std in statefulActions:
#class StateQuery(ConceptView): #class StateQuery(ConceptView):
class StateQuery(BaseView): class StateQuery(BaseView):
template = ViewPageTemplateFile('view_macros.pt') template = template
form_action = 'execute_search_action' form_action = 'execute_search_action'
@ -144,3 +148,15 @@ class StateQuery(BaseView):
uids = q.apply() uids = q.apply()
return self.viewIterator(getObjects(uids, self.loopsRoot)) return self.viewIterator(getObjects(uids, self.loopsRoot))
return [] return []
class FilterAllStates(BaseView):
@Lazy
def macros(self):
return template.macros
@Lazy
def macro(self):
return self.macros['filter_allstates']

View file

@ -27,7 +27,7 @@
<zope:adapter <zope:adapter
factory="loops.organize.stateful.base.SimplePublishable" factory="loops.organize.stateful.base.SimplePublishable"
name="simple_publishing" trusted="True" /> name="simple_publishing" />
<zope:class class="loops.organize.stateful.base.SimplePublishable"> <zope:class class="loops.organize.stateful.base.SimplePublishable">
<require permission="zope.View" <require permission="zope.View"
interface="cybertools.stateful.interfaces.IStateful" /> interface="cybertools.stateful.interfaces.IStateful" />
@ -41,7 +41,7 @@
<zope:adapter <zope:adapter
factory="loops.organize.stateful.task.StatefulTask" factory="loops.organize.stateful.task.StatefulTask"
name="task_states" trusted="True" /> name="task_states" />
<zope:class class="loops.organize.stateful.task.StatefulTask"> <zope:class class="loops.organize.stateful.task.StatefulTask">
<require permission="zope.View" <require permission="zope.View"
interface="cybertools.stateful.interfaces.IStateful" /> interface="cybertools.stateful.interfaces.IStateful" />
@ -49,13 +49,27 @@
set_schema="cybertools.stateful.interfaces.IStateful" /> set_schema="cybertools.stateful.interfaces.IStateful" />
</zope:class> </zope:class>
<zope:utility
factory="loops.organize.stateful.task.publishableTask"
name="publishable_task" />
<zope:adapter
factory="loops.organize.stateful.task.PublishableTask"
name="publishable_task" />
<zope:class class="loops.organize.stateful.task.PublishableTask">
<require permission="zope.View"
interface="cybertools.stateful.interfaces.IStateful" />
<require permission="zope.ManageContent"
set_schema="cybertools.stateful.interfaces.IStateful" />
</zope:class>
<zope:utility <zope:utility
factory="loops.organize.stateful.quality.classificationQuality" factory="loops.organize.stateful.quality.classificationQuality"
name="classification_quality" /> name="classification_quality" />
<zope:adapter <zope:adapter
factory="loops.organize.stateful.quality.ClassificationQualityCheckable" factory="loops.organize.stateful.quality.ClassificationQualityCheckable"
name="classification_quality" trusted="True" /> name="classification_quality" />
<zope:class class="loops.organize.stateful.quality.ClassificationQualityCheckable"> <zope:class class="loops.organize.stateful.quality.ClassificationQualityCheckable">
<require permission="zope.View" <require permission="zope.View"
interface="cybertools.stateful.interfaces.IStateful" /> interface="cybertools.stateful.interfaces.IStateful" />
@ -71,6 +85,12 @@
class="loops.organize.stateful.browser.StateQuery" class="loops.organize.stateful.browser.StateQuery"
permission="zope.View" /> permission="zope.View" />
<browser:page
for="loops.interfaces.IConcept"
name="filter_input.allstates"
class="loops.organize.stateful.browser.FilterAllStates"
permission="zope.View" />
<!-- event handlers --> <!-- event handlers -->
<zope:subscriber handler="loops.organize.stateful.base.handleTransition" /> <zope:subscriber handler="loops.organize.stateful.base.handleTransition" />

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -20,6 +20,7 @@
Basic implementations for stateful objects and adapters. Basic implementations for stateful objects and adapters.
""" """
from zope.app.security.settings import Allow, Deny, Unset
from zope import component from zope import component
from zope.component import adapter from zope.component import adapter
from zope.interface import implementer from zope.interface import implementer
@ -30,24 +31,107 @@ from cybertools.stateful.definition import State, Transition
from cybertools.stateful.interfaces import IStatesDefinition, IStateful from cybertools.stateful.interfaces import IStatesDefinition, IStateful
from loops.common import adapted from loops.common import adapted
from loops.organize.stateful.base import StatefulLoopsObject 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) @implementer(IStatesDefinition)
def taskStates(): def taskStates():
return StatesDefinition('task_states', return StatesDefinition('task_states',
State('planned', 'planned', ('finish', 'cancel'), State('draft', 'draft', ('release', 'cancel',),
color='blue'),
State('active', 'active', ('finish', 'cancel',),
color='yellow'), color='yellow'),
State('finished', 'finished', ('reopen'), State('finished', 'finished', ('reopen', 'archive',),
color='green'), color='green'),
State('cancelled', 'cancelled', ('reopen'), State('cancelled', 'cancelled', ('reopen',),
color='x'),
State('archived', 'archived', ('reopen',),
color='grey'), color='grey'),
Transition('release', 'release', 'active'),
Transition('finish', 'finish', 'finished'), Transition('finish', 'finish', 'finished'),
Transition('cancel', 'cancel', 'cancelled'), Transition('cancel', 'cancel', 'cancelled'),
Transition('reopen', 're-open', 'planned'), Transition('reopen', 're-open', 'draft'),
initialState='planned') 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): class StatefulTask(StatefulLoopsObject):
statesDefinition = 'task_states' statesDefinition = 'task_states'
class PublishableTask(StatefulLoopsObject):
statesDefinition = 'publishable_task'

View file

@ -1,4 +1,14 @@
<!-- $Id$ --> <html i18n:domain="loops">
<metal:input define-macro="filter_allstates">
<input type="checkbox" name="filter.states" value="all" id="filter-states"
onclick="submit()"
tal:attributes="checked python:
request.form.get('filter.states') == 'all'" />
<label for="filter-states"
i18n:translate="">Show all</label>
</metal:input>
<metal:query define-macro="query"> <metal:query define-macro="query">
@ -56,3 +66,6 @@
</form> </form>
</div> </div>
</metal:query> </metal:query>
</html>

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Base class(es) for track/record managers. Base class(es) for track/record managers.
$Id$
""" """
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
@ -46,6 +44,10 @@ class BaseRecordManager(object):
def loopsRoot(self): def loopsRoot(self):
return self.context.getLoopsRoot() return self.context.getLoopsRoot()
@Lazy
def uid(self):
return util.getUidForObject(self.context)
@Lazy @Lazy
def storage(self): def storage(self):
records = self.loopsRoot.getRecordManager() records = self.loopsRoot.getRecordManager()

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -17,9 +17,7 @@
# #
""" """
View class(es) for change tracks. View classes for tracks.
$Id$
""" """
from zope import component from zope import component

View file

@ -151,6 +151,7 @@ class WorkItemDetails(TrackDetails):
addParams=dict(id=self.track.__name__)) addParams=dict(id=self.track.__name__))
actions = [info, WorkItemStateAction(self)] actions = [info, WorkItemStateAction(self)]
if self.isLastInRun and self.allowedToEditWorkItem: if self.isLastInRun and self.allowedToEditWorkItem:
#if self.allowedToEditWorkItem:
self.view.registerDojoDateWidget() self.view.registerDojoDateWidget()
self.view.registerDojoNumberWidget() self.view.registerDojoNumberWidget()
self.view.registerDojoTextarea() self.view.registerDojoTextarea()
@ -167,7 +168,10 @@ class WorkItemDetails(TrackDetails):
@Lazy @Lazy
def allowedToEditWorkItem(self): def allowedToEditWorkItem(self):
# if not canAccessObject(self.object.task):
# return False
if checkPermission('loops.ManageSite', self.object): if checkPermission('loops.ManageSite', self.object):
# or hasRole('loops.Master', self.object):
return True return True
if self.track.data.get('creator') == self.personId: if self.track.data.get('creator') == self.personId:
return True return True
@ -288,6 +292,25 @@ class TaskWorkItems(BaseWorkItemsView, ConceptView):
return sorted(self.query(**criteria), key=lambda x: x.track.timeStamp) 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): class PersonWorkItems(BaseWorkItemsView, ConceptView):
""" A query view showing work items for a person, the query's parent. """ 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()[ workItems = self.loopsRoot.getRecordManager()[
self.recordManagerName] self.recordManagerName]
return workItems.get(id) 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 @Lazy
def title(self): def title(self):
@ -379,25 +407,35 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
return time.strftime('%Y-%m-%d', time.localtime(ts)) return time.strftime('%Y-%m-%d', time.localtime(ts))
return '' return ''
@Lazy
def defaultTimeStamp(self):
if self.workItemType.prefillDate:
return getTimeStamp()
return None
@Lazy @Lazy
def date(self): def date(self):
ts = self.track.start or getTimeStamp() ts = self.track.start or self.defaultTimeStamp
if ts:
return time.strftime('%Y-%m-%d', time.localtime(ts)) return time.strftime('%Y-%m-%d', time.localtime(ts))
return ''
@Lazy @Lazy
def startTime(self): def startTime(self):
ts = self.track.start or getTimeStamp() ts = self.track.start or self.defaultTimeStamp
#return time.strftime('%Y-%m-%dT%H:%M', time.localtime(ts)) if ts:
return time.strftime('T%H:%M', time.localtime(ts)) return time.strftime('T%H:%M', time.localtime(ts))
return ''
@Lazy @Lazy
def endTime(self): def endTime(self):
if self.state == 'running': if self.state == 'running':
ts = getTimeStamp() ts = self.defaultTimeStamp
else: else:
ts = self.track.end or getTimeStamp() ts = self.track.end or self.defaultTimeStamp
#return time.strftime('%Y-%m-%dT%H:%M', time.localtime(ts)) if ts:
return time.strftime('T%H:%M', time.localtime(ts)) return time.strftime('T%H:%M', time.localtime(ts))
return ''
@Lazy @Lazy
def state(self): def state(self):

View file

@ -30,6 +30,14 @@
factory="loops.organize.work.browser.AllWorkItems" factory="loops.organize.work.browser.AllWorkItems"
permission="zope.View" /> permission="zope.View" />
<zope:adapter
name="relatedtaskworkitems.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.organize.work.browser.RelatedTaskWorkItems"
permission="zope.View" />
<zope:adapter <zope:adapter
name="taskworkitems.html" name="taskworkitems.html"
for="loops.interfaces.IConcept for="loops.interfaces.IConcept

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -35,7 +35,7 @@ from cybertools.util.jeep import Jeep
from loops.common import adapted, baseObject from loops.common import adapted, baseObject
from loops.expert.browser.report import ReportConceptView from loops.expert.browser.report import ReportConceptView
from loops.expert.field import Field, TargetField, DateField, StateField, \ from loops.expert.field import Field, TargetField, DateField, StateField, \
TextField, UrlField TextField, HtmlTextField, UrlField
from loops.expert.field import SubReport, SubReportField from loops.expert.field import SubReport, SubReportField
from loops.expert.report import ReportInstance from loops.expert.report import ReportInstance
from loops import util from loops import util
@ -279,7 +279,7 @@ class MeetingMinutesWorkRow(WorkRow):
@Lazy @Lazy
def isActive(self): def isActive(self):
return self.context.state not in ( return self.context.state not in (
'finished', 'closed', 'cancelled', 'moved') 'finished', 'finished_x', 'closed', 'cancelled', 'moved')
class MeetingMinutesWork(WorkReportInstance, SubReport): class MeetingMinutesWork(WorkReportInstance, SubReport):
@ -329,7 +329,7 @@ taskTitle = UrlField('title', u'Task Title',
description=u'The short description of the task.', description=u'The short description of the task.',
cssClass='header-1', cssClass='header-1',
executionSteps=['output']) executionSteps=['output'])
taskDescription = TextField('description', u'Description', taskDescription = HtmlTextField('description', u'Description',
description=u'The long description of the task.', description=u'The long description of the task.',
cssClass='header-2', cssClass='header-2',
executionSteps=['output']) executionSteps=['output'])
@ -337,11 +337,11 @@ responsible = TextField('responsible', u'label_responsible',
description=u'Responsible.', description=u'Responsible.',
cssClass='header-2', cssClass='header-2',
executionSteps=['output']) executionSteps=['output'])
discussion = TextField('discussion', u'label_discussion', discussion = HtmlTextField('discussion', u'label_discussion',
description=u'Discussion.', description=u'Discussion.',
cssClass='header-2', cssClass='header-2',
executionSteps=['output']) executionSteps=['output'])
consequences = TextField('consequences', u'label_consequences', consequences = HtmlTextField('consequences', u'label_consequences',
description=u'Consequences.', description=u'Consequences.',
cssClass='header-2', cssClass='header-2',
executionSteps=['output']) executionSteps=['output'])

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Specialized fields factories. Specialized fields factories.
$Id$
""" """
from zope.component import adapts from zope.component import adapts
@ -38,7 +36,7 @@ class ResourceSchemaFactory(SchemaFactory):
def __call__(self, interface, **kw): def __call__(self, interface, **kw):
schema = super(ResourceSchemaFactory, self).__call__(interface, **kw) schema = super(ResourceSchemaFactory, self).__call__(interface, **kw)
#if 'data' in schema.fields.keys(): #if 'data' in schema.fields.keys():
schema.fields.data.height = 10 schema.fields.data.height = 15
if self.context.contentType == 'text/html': if self.context.contentType == 'text/html':
schema.fields.data.fieldType = 'html' schema.fields.data.fieldType = 'html'
return schema return schema

View file

@ -79,6 +79,11 @@
<grant role="loops.Owner" permission="loops.ViewRestricted" /> <grant role="loops.Owner" permission="loops.ViewRestricted" />
<grant role="loops.Owner" permission="zope.View" /> <grant role="loops.Owner" permission="zope.View" />
<role id="loops.Person"
title="[loops-person-role] Person" />
<grant role="loops.Person" permission="zope.View" />
<grant role="loops.Person" permission="loops.ViewRestricted" />
<!-- moved to etc/securitypolicy.zcml: --> <!-- moved to etc/securitypolicy.zcml: -->
<!--<grant role="zope.ContentManager" permission="loops.AssignAsParent" />--> <!--<grant role="zope.ContentManager" permission="loops.AssignAsParent" />-->

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Security-related views. Security-related views.
$Id$
""" """
from zope.app.authentication.groupfolder import GroupInformation from zope.app.authentication.groupfolder import GroupInformation
@ -145,7 +143,7 @@ class PermissionView(object):
for e in entry: for e in entry:
value = SettingAsBoolean[e[1]] value = SettingAsBoolean[e[1]]
value = (value is False and '-') or (value and '+') or '' value = (value is False and '-') or (value and '+') or ''
result.append(value + e[0]) result.append(value + (e[0] or ''))
return ', '.join(result) return ', '.join(result)
def getPrincipalPermissions(self): def getPrincipalPermissions(self):

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Common functions and other stuff for working with permissions and roles. Common functions and other stuff for working with permissions and roles.
$Id$
""" """
from persistent import Persistent from persistent import Persistent
@ -49,12 +47,12 @@ allRolesExceptOwner = (
'zope.Anonymous', 'zope.Member', 'zope.ContentManager', 'loops.Staff', 'zope.Anonymous', 'zope.Member', 'zope.ContentManager', 'loops.Staff',
'loops.xmlrpc.ConceptManager', # relevant for local security? 'loops.xmlrpc.ConceptManager', # relevant for local security?
#'loops.SiteManager', #'loops.SiteManager',
'loops.Member', 'loops.Master',) 'loops.Person', 'loops.Member', 'loops.Master')
allRolesExceptOwnerAndMaster = tuple(allRolesExceptOwner[:-1]) allRolesExceptOwnerAndMaster = tuple(allRolesExceptOwner[:-1])
minorPrivilegedRoles = ('zope.Anonymous', 'zope.Member',) minorPrivilegedRoles = ('zope.Anonymous', 'zope.Member',)
localRoles = ('zope.Anonymous', 'zope.Member', 'zope.ContentManager', localRoles = ('zope.Anonymous', 'zope.Member', 'zope.ContentManager',
'loops.SiteManager', 'loops.Staff', 'loops.Member', 'loops.Master', 'loops.SiteManager', 'loops.Staff', 'loops.Member', 'loops.Master',
'loops.Owner') 'loops.Owner', 'loops.Person')
localPermissions = ('zope.ManageContent', 'zope.View', 'loops.ManageWorkspaces', localPermissions = ('zope.ManageContent', 'zope.View', 'loops.ManageWorkspaces',
'loops.ViewRestricted', 'loops.EditRestricted', 'loops.AssignAsParent',) 'loops.ViewRestricted', 'loops.EditRestricted', 'loops.AssignAsParent',)
@ -77,7 +75,7 @@ def canListObject(obj, noCheck=False):
return canAccess(obj, 'title') return canAccess(obj, 'title')
def canWriteObject(obj): def canWriteObject(obj):
return canWrite(obj, 'title') return canWrite(obj, 'title') or canAssignAsParent(obj)
def canEditRestricted(obj): def canEditRestricted(obj):
return checkPermission('loops.EditRestricted', obj) return checkPermission('loops.EditRestricted', obj)
@ -122,12 +120,22 @@ def setPrincipalRole(prm, r, p, setting):
def assignOwner(obj, principalId): def assignOwner(obj, principalId):
prm = IPrincipalRoleManager(obj) prm = IPrincipalRoleManager(obj, None)
if prm is not None:
prm.assignRoleToPrincipal('loops.Owner', principalId) prm.assignRoleToPrincipal('loops.Owner', principalId)
def removeOwner(obj, 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 = 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): def allowEditingForOwner(obj, deny=allRolesExceptOwner, revert=False):
@ -161,6 +169,9 @@ def setDefaultSecurity(obj, event):
aObj = adapted(obj) aObj = adapted(obj)
setter = ISecuritySetter(aObj) setter = ISecuritySetter(aObj)
setter.setDefaultSecurity() setter.setDefaultSecurity()
principal = getCurrentPrincipal()
if principal is not None:
assignOwner(obj, principal.id)
@component.adapter(IConcept, IAssignmentEvent) @component.adapter(IConcept, IAssignmentEvent)

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Interfaces for loops security management. Interfaces for loops security management.
$Id$
""" """
from zope.interface import Interface, Attribute from zope.interface import Interface, Attribute
@ -35,6 +33,10 @@ class ISecuritySetter(Interface):
context object. context object.
""" """
def setStateSecurity():
""" Set the security according to the state(s) of the object.
"""
def setDefaultRolePermissions(): def setDefaultRolePermissions():
""" Set some default role permission assignments (grants) on the """ Set some default role permission assignments (grants) on the
context object. context object.

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -19,8 +19,6 @@
""" """
Base classes for security setters, i.e. adapters that provide standardized Base classes for security setters, i.e. adapters that provide standardized
methods for setting role permissions and other security-related stuff. methods for setting role permissions and other security-related stuff.
$Id$
""" """
from zope.app.security.settings import Allow, Deny, Unset from zope.app.security.settings import Allow, Deny, Unset
@ -33,11 +31,14 @@ from zope.securitypolicy.interfaces import \
IRolePermissionMap, IRolePermissionManager, \ IRolePermissionMap, IRolePermissionManager, \
IPrincipalRoleMap, IPrincipalRoleManager IPrincipalRoleMap, IPrincipalRoleManager
from cybertools.meta.interfaces import IOptions
from cybertools.stateful.interfaces import IStateful
from loops.common import adapted, AdapterBase, baseObject from loops.common import adapted, AdapterBase, baseObject
from loops.config.base import DummyOptions
from loops.interfaces import IConceptSchema, IBaseResourceSchema, ILoopsAdapter from loops.interfaces import IConceptSchema, IBaseResourceSchema, ILoopsAdapter
from loops.organize.util import getPrincipalFolder, getGroupsFolder, getGroupId from loops.organize.util import getPrincipalFolder, getGroupsFolder, getGroupId
from loops.security.common import overrides, setRolePermission, setPrincipalRole 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.security.interfaces import ISecuritySetter
from loops.versioning.interfaces import IVersionable from loops.versioning.interfaces import IVersionable
@ -58,6 +59,17 @@ class BaseSecuritySetter(object):
def conceptManager(self): def conceptManager(self):
return self.baseObject.getLoopsRoot().getConceptManager() 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 @Lazy
def acquiringPredicates(self): def acquiringPredicates(self):
return [self.conceptManager.get(n) for n in acquiringPredicateNames] return [self.conceptManager.get(n) for n in acquiringPredicateNames]
@ -81,6 +93,9 @@ class BaseSecuritySetter(object):
def acquireRolePermissions(self): def acquireRolePermissions(self):
pass pass
def acquirePrincipalRoles(self):
pass
def copyPrincipalRoles(self, source, revert=False): def copyPrincipalRoles(self, source, revert=False):
pass pass
@ -109,6 +124,13 @@ class LoopsObjectSecuritySetter(BaseSecuritySetter):
for p, r, s in rpm.getRolesAndPermissions(): for p, r, s in rpm.getRolesAndPermissions():
setRolePermission(rpm, p, r, Unset) 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): def acquireRolePermissions(self):
settings = {} settings = {}
for p in self.parents: for p in self.parents:
@ -128,15 +150,59 @@ class LoopsObjectSecuritySetter(BaseSecuritySetter):
settings[(p, r)] = s settings[(p, r)] = s
self.setDefaultRolePermissions() self.setDefaultRolePermissions()
self.setRolePermissions(settings) self.setRolePermissions(settings)
self.setStateSecurity()
def setRolePermissions(self, settings): def setRolePermissions(self, settings):
for (p, r), s in settings.items(): for (p, r), s in settings.items():
setRolePermission(self.rolePermissionManager, p, r, s) 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): def copyPrincipalRoles(self, source, revert=False):
prm = IPrincipalRoleMap(baseObject(source.context)) prm = IPrincipalRoleMap(baseObject(source.context))
for r, p, s in prm.getPrincipalsAndRoles(): for r, p, s in prm.getPrincipalsAndRoles():
if p in self.workspacePrincipals: #if p in self.workspacePrincipals:
if r != 'loops.Owner':
if revert: if revert:
setPrincipalRole(self.principalRoleManager, r, p, Unset) setPrincipalRole(self.principalRoleManager, r, p, Unset)
else: else:
@ -155,12 +221,13 @@ class ConceptSecuritySetter(LoopsObjectSecuritySetter):
setter = ISecuritySetter(adapted(relation.second)) setter = ISecuritySetter(adapted(relation.second))
setter.setDefaultRolePermissions() setter.setDefaultRolePermissions()
setter.acquireRolePermissions() setter.acquireRolePermissions()
wi = baseObject(self.context).workspaceInformation setter.acquirePrincipalRoles()
if wi and not wi.propagateParentSecurity: #wi = baseObject(self.context).workspaceInformation
return #if wi and not wi.propagateParentSecurity:
setter.copyPrincipalRoles(self, revert) # return
if wi: #setter.copyPrincipalRoles(self, revert)
setter.copyPrincipalRoles(ISecuritySetter(wi), revert) #if wi:
# setter.copyPrincipalRoles(ISecuritySetter(wi), revert)
setter.propagateSecurity(revert, updated) setter.propagateSecurity(revert, updated)
def propagateSecurity(self, revert=False, updated=None): def propagateSecurity(self, revert=False, updated=None):
@ -186,6 +253,12 @@ class ResourceSecuritySetter(LoopsObjectSecuritySetter):
def parents(self): def parents(self):
return self.baseObject.getConcepts(self.acquiringPredicates) 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): def setRolePermissions(self, settings):
vSetters = [self] vSetters = [self]
vr = IVersionable(baseObject(self.context)) vr = IVersionable(baseObject(self.context))
@ -204,10 +277,18 @@ class ResourceSecuritySetter(LoopsObjectSecuritySetter):
vSetters = [ISecuritySetter(adapted(v)) for v in versions] vSetters = [ISecuritySetter(adapted(v)) for v in versions]
prm = IPrincipalRoleMap(baseObject(source.context)) prm = IPrincipalRoleMap(baseObject(source.context))
for r, p, s in prm.getPrincipalsAndRoles(): 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: for v in vSetters:
if revert: if revert:
setPrincipalRole(v.principalRoleManager, r, p, Unset) setPrincipalRole(v.principalRoleManager, r, p, Unset)
else: else:
setPrincipalRole(v.principalRoleManager, r, p, s) 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]

View file

@ -18,7 +18,7 @@ Let's set up a loops site with basic and example concepts and resources.
>>> concepts, resources, views = t.setup() >>> concepts, resources, views = t.setup()
>>> loopsRoot = site['loops'] >>> loopsRoot = site['loops']
>>> len(concepts), len(resources), len(views) >>> len(concepts), len(resources), len(views)
(34, 3, 1) (33, 3, 1)
>>> from cybertools.tracking.btree import TrackingStorage >>> from cybertools.tracking.btree import TrackingStorage
>>> from loops.system.job import JobRecord >>> from loops.system.job import JobRecord

View file

@ -35,7 +35,7 @@ ZCML setup):
Let's look what setup has provided us with: Let's look what setup has provided us with:
>>> len(concepts) >>> len(concepts)
23 22
Now let's add a few more concepts: 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()) >>> sorted(t['name'] for t in xrf.getConceptTypes())
[u'competence', u'customer', u'domain', u'file', u'note', u'person', [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()) >>> sorted(t['name'] for t in xrf.getPredicates())
[u'depends', u'issubtype', u'knows', u'ownedby', u'provides', u'requires', [u'depends', u'issubtype', u'knows', u'ownedby', u'provides', u'requires',
u'standard'] u'standard']
@ -96,7 +96,7 @@ All methods that retrieve one object also returns its children and parents:
u'hasType' u'hasType'
>>> sorted(c['name'] for c in ch[0]['objects']) >>> sorted(c['name'] for c in ch[0]['objects'])
[u'competence', u'customer', u'domain', u'file', u'note', u'person', [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'] >>> pa = defaultPred['parents']
>>> len(pa) >>> len(pa)
@ -115,7 +115,7 @@ We can also retrieve children and parents explicitely:
u'hasType' u'hasType'
>>> sorted(c['name'] for c in ch[0]['objects']) >>> sorted(c['name'] for c in ch[0]['objects'])
[u'competence', u'customer', u'domain', u'file', u'note', u'person', [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') >>> pa = xrf.getParents('5')
>>> len(pa) >>> len(pa)
@ -174,14 +174,14 @@ Updating the concept map
>>> topicId = xrf.getObjectByName('topic')['id'] >>> topicId = xrf.getObjectByName('topic')['id']
>>> xrf.createConcept(topicId, u'zope2', u'Zope 2') >>> 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'} 'name': u'zope2'}
The name of the concept is checked by a name chooser; if the corresponding 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. parameter is empty, the name will be generated from the title.
>>> xrf.createConcept(topicId, u'', u'Python') >>> 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'} 'name': u'python'}
If we try to deassign a ``hasType`` relation nothing will happen; a If we try to deassign a ``hasType`` relation nothing will happen; a