merge branch master

This commit is contained in:
Helmut Merz 2015-06-12 08:35:07 +02:00
commit 3c1a5ccdf4
83 changed files with 2954 additions and 418 deletions

View file

@ -913,6 +913,12 @@ relates ISO country codes with the full name of the country.
>>> sorted(adapted(concepts['countries']).data.items())
[('at', ['Austria']), ('de', ['Germany'])]
>>> countries.dataAsRecords()
[{'value': 'Austria', 'key': 'at'}, {'value': 'Germany', 'key': 'de'}]
>>> countries.getRowsByValue('value', 'Germany')
[{'value': 'Germany', 'key': 'de'}]
Caching
=======

View file

@ -92,6 +92,8 @@ class DialogAction(Action):
urlParams['fixed_type'] = 'yes'
if self.viewTitle:
urlParams['view_title'] = self.viewTitle
#for k, v in self.page.sortInfo.items():
# urlParams['sortinfo_' + k] = v['fparam']
urlParams.update(self.addParams)
if self.target is not None:
url = self.page.getUrlForTarget(self.target)

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -22,7 +22,7 @@ Common base class for loops browser view classes.
from cgi import parse_qs, parse_qsl
#import mimetypes # use more specific assignments from cybertools.text
from datetime import datetime
from datetime import date, datetime
import re
from time import strptime
from urllib import urlencode
@ -61,17 +61,21 @@ from cybertools.stateful.interfaces import IStateful
from cybertools.text import mimetypes
from cybertools.typology.interfaces import IType, ITypeManager
from cybertools.util.date import toLocalTime
from cybertools.util.format import formatDate
from cybertools.util.jeep import Jeep
from loops.browser.util import normalizeForUrl
from loops.common import adapted, baseObject
from loops.config.base import DummyOptions
from loops.i18n.browser import I18NView
from loops.interfaces import IResource, IView, INode, ITypeConcept
from loops.organize.personal import favorite
from loops.organize.party import getPersonForUser
from loops.organize.tracking import access
from loops.organize.util import getRolesForPrincipal
from loops.resource import Resource
from loops.security.common import checkPermission
from loops.security.common import canAccessObject, canListObject, canWriteObject
from loops.security.common import canEditRestricted
from loops.type import ITypeConcept, LoopsTypeInfo
from loops import util
from loops.util import _, saveRequest
@ -132,7 +136,58 @@ class EditForm(form.EditForm):
return parentUrl + '/contents.html'
class BaseView(GenericView, I18NView):
class SortableMixin(object):
@Lazy
def sortInfo(self):
result = {}
for k, v in self.request.form.items():
if k.startswith('sortinfo_'):
tableName = k[len('sortinfo_'):]
if ',' in v:
fn, dir = v.split(',')
else:
fn = v
dir = 'asc'
result[tableName] = dict(
colName=fn, ascending=(dir=='asc'), fparam=v)
result = favorite.updateSortInfo(getPersonForUser(
self.context, self.request), self.target, result)
return result
def isSortableColumn(self, tableName, colName):
return False # overwrite in subclass
def getSortUrl(self, tableName, colName):
url = str(self.request.URL)
paramChar = '?' in url and '&' or '?'
si = self.sortInfo.get(tableName)
if si is not None and si.get('colName') == colName:
dir = si['ascending'] and 'desc' or 'asc'
else:
dir = 'asc'
return '%s%ssortinfo_%s=%s,%s' % (url, paramChar, tableName, colName, dir)
def getSortParams(self, tableName):
url = str(self.request.URL)
paramChar = '?' in url and '&' or '?'
si = self.sortInfo.get(tableName)
if si is not None:
colName = si['colName']
dir = si['ascending'] and 'asc' or 'desc'
return '%ssortinfo_%s=%s,%s' % (paramChar, tableName, colName, dir)
return ''
def getSortImage(self, tableName, colName):
si = self.sortInfo.get(tableName)
if si is not None and si.get('colName') == colName:
if si['ascending']:
return '/@@/cybertools.icons/arrowdown.gif'
else:
return '/@@/cybertools.icons/arrowup.gif'
class BaseView(GenericView, I18NView, SortableMixin):
actions = {}
portlet_actions = []
@ -153,6 +208,10 @@ class BaseView(GenericView, I18NView):
pass
saveRequest(request)
def todayFormatted(self):
return formatDate(date.today(), 'date', 'short',
self.languageInfo.language)
def checkPermissions(self):
return canAccessObject(self.context)
@ -204,6 +263,16 @@ class BaseView(GenericView, I18NView):
result.append(view)
return result
@Lazy
def urlParamString(self):
return self.getUrlParamString()
def getUrlParamString(self):
qs = self.request.get('QUERY_STRING')
if qs:
return '?' + qs
return ''
@Lazy
def principalId(self):
principal = self.request.principal
@ -337,6 +406,10 @@ class BaseView(GenericView, I18NView):
def isPartOfPredicate(self):
return self.conceptManager.get('ispartof')
@Lazy
def queryTargetPredicate(self):
return self.conceptManager.get('querytarget')
@Lazy
def memberPredicate(self):
return self.conceptManager.get('ismember')
@ -732,6 +805,8 @@ class BaseView(GenericView, I18NView):
return result
def checkState(self):
if checkPermission('loops.ManageSite', self.context):
return True
if not self.allStates:
return True
for stf in self.allStates:
@ -806,6 +881,10 @@ class BaseView(GenericView, I18NView):
def canAccessRestricted(self):
return checkPermission('loops.ViewRestricted', self.context)
@Lazy
def canEditRestricted(self):
return canEditRestricted(self.context)
def openEditWindow(self, viewName='edit.html'):
if self.editable:
if checkPermission('loops.ManageSite', self.context):
@ -928,6 +1007,12 @@ class BaseView(GenericView, I18NView):
jsCall = 'dojo.require("dojox.image.Lightbox");'
self.controller.macros.register('js-execute', jsCall, jsCall=jsCall)
def registerDojoComboBox(self):
self.registerDojo()
jsCall = ('dojo.require("dijit.form.ComboBox");')
self.controller.macros.register('js-execute',
'dojo.require.ComboBox', jsCall=jsCall)
def registerDojoFormAll(self):
self.registerDojo()
self.registerDojoEditor()

View file

@ -282,8 +282,17 @@ class ConceptView(BaseView):
def breadcrumbsTitle(self):
return self.title
@Lazy
def showInBreadcrumbs(self):
return (self.options('show_in_breadcrumbs') or
self.typeOptions('show_in_breadcrumbs'))
@Lazy
def breadcrumbsParent(self):
for p in self.context.getParents([self.defaultPredicate]):
view = self.nodeView.getViewForTarget(p)
if view.showInBreadcrumbs:
return view
return None
def getData(self, omit=('title', 'description')):
@ -449,7 +458,7 @@ class ConceptView(BaseView):
if r.order != pos:
r.order = pos
def getResources(self):
def getResources(self, relView=None, sort='default'):
form = self.request.form
#if form.get('loops.viewName') == 'index.html' and self.editable:
if self.editable:
@ -458,13 +467,17 @@ class ConceptView(BaseView):
tokens = form.get('resources_tokens')
if tokens:
self.reorderResources(tokens)
from loops.browser.resource import ResourceRelationView
if relView is None:
from loops.browser.resource import ResourceRelationView
relView = ResourceRelationView
from loops.organize.personal.browser.filter import FilterView
fv = FilterView(self.context, self.request)
rels = self.context.getResourceRelations()
rels = self.context.getResourceRelations(sort=sort)
for r in rels:
if fv.check(r.first):
yield ResourceRelationView(r, self.request, contextIsSecond=True)
view = relView(r, self.request, contextIsSecond=True)
if view.checkState():
yield view
def resources(self):
return self.getResources()

View file

@ -51,7 +51,7 @@
<h1 tal:define="tabview item/tabview|nothing"
tal:attributes="ondblclick item/openEditWindow">
<a tal:omit-tag="python: level > 1"
tal:attributes="href request/URL"
tal:attributes="href string:${request/URL}${item/urlParamString}"
tal:content="item/title">Title</a>
<a title="Show tabular view"
i18n:attributes="title"
@ -362,4 +362,21 @@
</metal:actions>
<metal:sortable define-macro="sortable_column_header"
tal:define="tableName tableName|nothing">
<a title="tooltip_sort_column"
tal:define="colName col/name"
tal:omit-tag="python:not item.isSortableColumn(tableName, colName)"
tal:attributes="href python:item.getSortUrl(tableName, colName)"
i18n:attributes="title">
<span tal:content="col/title"
tal:attributes="class col/cssClass|nothing"
i18n:translate="" />
<img tal:define="src python:item.getSortImage(tableName, colName)"
tal:condition="src"
tal:attributes="src src" />
</a>
</metal:sortable>
</html>

View file

@ -561,6 +561,14 @@
factory="loops.browser.concept.TabbedPage"
permission="zope.View" />
<!-- delete object action -->
<page
name="delete_object"
for="loops.interfaces.INode"
class="loops.browser.form.DeleteObject"
permission="zope.ManageContent" />
<!-- dialogs/forms (end-user views) -->
<page

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2012 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -20,12 +20,13 @@
Classes for form presentation and processing.
"""
from urllib import urlencode
from zope.app.container.contained import ObjectRemovedEvent
from zope import component, interface, schema
from zope.component import adapts
from zope.event import notify
from zope.interface import Interface
from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
from zope.app.container.interfaces import INameChooser
from zope.app.container.contained import ObjectAddedEvent
from zope.app.pagetemplate import ViewPageTemplateFile
@ -35,7 +36,7 @@ from zope.publisher.browser import FileUpload
from zope.publisher.interfaces import BadRequest
from zope.security.interfaces import ForbiddenAttribute, Unauthorized
from zope.security.proxy import isinstance, removeSecurityProxy
from zope.traversing.api import getName
from zope.traversing.api import getName, getParent
from cybertools.ajax import innerHtml
from cybertools.browser.form import FormController
@ -68,6 +69,25 @@ from loops.util import _
from loops.versioning.interfaces import IVersionable
# delete object
class DeleteObject(NodeView):
isTopLevel = True
def __call__(self):
# todo: check permission; check security code
form = self.request.form
obj = util.getObjectForUid(form['uid'])
container = getParent(obj)
notify(ObjectRemovedEvent(obj))
del container[getName(obj)]
message = 'The object requested has been deleted.'
params = [('loops.message', message.encode('UTF-8'))]
nextUrl = '%s?%s' % (self.request.URL[-1], urlencode(params))
return self.request.response.redirect(nextUrl)
# forms
class ObjectForm(NodeView):
@ -196,15 +216,37 @@ class ObjectForm(NodeView):
def typeManager(self):
return ITypeManager(self.target)
@Lazy
def targetType(self):
return self.target.getType()
@Lazy
def presetTypesForAssignment(self):
types = list(self.typeManager.listTypes(include=('assign',)))
types = []
tn = getName(self.targetType)
for t in self.typeManager.listTypes(include=('assign',)):
# check if type is appropriate for the object to be created
opt = IOptions(adapted(t.context))('qualifier_assign_to')
#print '***', t.context.__name__, opt, tn
if not opt or tn in opt:
types.append(t)
assigned = [r.context.conceptType for r in self.assignments]
types = [t for t in types if t.typeProvider not in assigned]
return [dict(title=t.title, token=t.tokenForSearch) for t in types]
def conceptsForType(self, token):
result = ConceptQuery(self).query(type=token)
# check typeOption: include only matching instances
include = []
type = self.conceptManager[token.split(':')[-1]]
#print '###', token, repr(type)
opt = IOptions(adapted(type))('qualifier_assign_check_parents')
if opt:
for p in self.target.getAllParents([self.defaultPredicate]):
for c in p.object.getChildren([self.defaultPredicate]):
include.append(c)
if include:
result = [c for c in result if c in include]
fv = FilterView(self.context, self.request)
result = fv.apply(result)
result.sort(key=lambda x: x.title)
@ -288,8 +330,11 @@ class CreateObjectForm(ObjectForm):
@Lazy
def defaultTypeToken(self):
return (self.controller.params.get('form.create.defaultTypeToken')
or '.loops/concepts/textdocument')
setting = self.controller.params.get('form.create.defaultTypeToken')
if setting:
return setting
opt = self.globalOptions('form.create.default_type_token')
return opt and opt[0] or '.loops/concepts/textdocument'
@Lazy
def typeToken(self):
@ -310,6 +355,10 @@ class CreateObjectForm(ObjectForm):
if typeToken:
return self.loopsRoot.loopsTraverse(typeToken)
@Lazy
def targetType(self):
return self.typeConcept
@Lazy
def adapted(self):
ad = self.typeInterface(Resource())
@ -423,6 +472,7 @@ class CreateConceptForm(CreateObjectForm):
return c
ad = ti(c)
ad.__is_dummy__ = True
ad.__type__ = adapted(self.typeConcept)
return ad
@Lazy

View file

@ -238,18 +238,21 @@ fieldset.box td {
font-weight: bold;
color: #444;
padding-top: 0.4em;
border-bottom: none;
}
.content-4 h1, .content-3 h2, .content-2 h3, .content-1 h4, h4 {
font-size: 130%;
font-weight: normal;
padding-top: 0.3em;
border-bottom: none;
}
.content-5 h1, .content-4 h2, .content-3 h3, content-2 h4, h5 {
font-size: 120%;
/* border: none; */
padding-top: 0.2em;
border-bottom: none;
}
.box {

View file

@ -47,6 +47,15 @@ function showIfIn(node, conditions) {
})
}
function setIfIn(node, conditions) {
dojo.forEach(conditions, function(cond) {
if (node.value == cond[0]) {
target = dijit.byId(cond[1]);
target.setValue(cond[2]);
}
})
}
function destroyWidgets(node) {
dojo.forEach(dojo.query('[widgetId]', node), function(n) {
w = dijit.byNode(n);

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 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
@ -110,7 +110,7 @@ class NodeView(BaseView):
return []
menu = self.menu
data = [dict(label=menu.title, url=menu.url)]
menuItem = self.nearestMenuItem
menuItem = self.getNearestMenuItem(all=True)
if menuItem != menu.context:
data.append(dict(label=menuItem.title,
url=absoluteURL(menuItem, self.request)))
@ -121,6 +121,9 @@ class NodeView(BaseView):
url=absoluteURL(p, self.request)))
if self.virtualTarget:
data.extend(self.virtualTarget.breadcrumbs())
if data and not '?' in data[-1]['url']:
if self.urlParamString:
data[-1]['url'] += self.urlParamString
return data
def viewModes(self):
@ -401,10 +404,13 @@ class NodeView(BaseView):
@Lazy
def nearestMenuItem(self):
return self.getNearestMenuItem()
def getNearestMenuItem(self, all=False):
menu = self.menuObject
menuItem = None
for p in [self.context] + self.parents:
if not p.isMenuItem():
if not all and not p.isMenuItem():
menuItem = None
elif menuItem is None:
menuItem = p
@ -441,7 +447,7 @@ class NodeView(BaseView):
def targetView(self, name='index.html', methodName='show'):
if name == 'index.html': # only when called for default view
tv = self.viewAnnotations.get('targetView')
if tv is not None:
if tv is not None and callable(tv):
return tv()
if '?' in name:
name, params = name.split('?', 1)

View file

@ -30,7 +30,7 @@
item nocall:target"
tal:attributes="class string:content-$level;
id id;
ondblclick python: target.openEditWindow('configure.html')">
ondblclick python:target.openEditWindow('configure.html')">
<metal:body use-macro="item/macro">
The body
</metal:body>
@ -41,17 +41,22 @@
<metal:body define-macro="conceptbody">
<tal:body define="body item/body;">
<tal:body define="body item/body;
itemNum view/itemNum;
id string:$itemNum.body">
<div class="content-1" id="1"
tal:attributes="class string:content-$level;
id string:${view/itemNum}.body;
ondblclick python: item.openEditWindow('configure.html')">
ondblclick python:item.openEditWindow('configure.html')">
<span tal:content="structure body">Node Body</span>
</div>
<tal:concepts define="item nocall:item/targetObjectView;
macro item/macro">
<div metal:use-macro="macro" />
</tal:concepts>
<div tal:define="item nocall:item/targetObjectView;
macro item/macro">
<div tal:attributes="class string:content-$level;
id id;">
<div metal:use-macro="macro" />
</div>
</div>
</tal:body>
</metal:body>

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -20,6 +20,7 @@
View class for resource objects.
"""
import os.path
import urllib
from zope.cachedescriptors.property import Lazy
from zope import component
@ -47,7 +48,7 @@ from loops.browser.common import EditForm, BaseView
from loops.browser.concept import BaseRelationView, ConceptRelationView
from loops.browser.concept import ConceptConfigureView
from loops.browser.node import NodeView, node_macros
from loops.common import adapted, NameChooser, normalizeName
from loops.common import adapted, baseObject, NameChooser, normalizeName
from loops.interfaces import IBaseResource, IDocument, ITextDocument
from loops.interfaces import IMediaAsset as legacy_IMediaAsset
from loops.interfaces import ITypeConcept
@ -216,6 +217,16 @@ class ResourceView(BaseView):
if filename is None:
filename = (adapted(self.context).localFilename or
getName(self.context))
if self.typeOptions('use_title_for_download_filename'):
base, ext = os.path.splitext(filename)
filename = context.title
vr = IVersionable(baseObject(context))
if len(vr.versions) > 0:
filename = vr.generateName(filename, ext, vr.versionId)
else:
if not filename.endswith(ext):
filename += ext
filename = filename.encode('UTF-8')
if self.typeOptions('no_normalize_download_filename'):
filename = '"%s"' % filename
else:

View file

@ -96,6 +96,7 @@
</a>
</span>
</div>
<metal:custom define-slot="custom_info" />
<metal:fields use-macro="view/comment_macros/comments" />
</div>
</metal:block>

View file

@ -20,6 +20,7 @@ body {
#portlets {
margin-top: 1em;
background-color: #ffffff;
}
ul.view-modes {
@ -108,6 +109,14 @@ thead th {
background: none;
}
/* printing */
@media print {
.noprint {
display: none;
}
}
/* class-specific */
.breadcrumbs td {

View file

@ -11,8 +11,14 @@ body {
display: none;
}
.container {
width: auto;
margin: 0;
border: 0;
}
#content {
/* width: 100%; */
width: 80%;
width: auto;
color: Black;
}

View file

@ -231,17 +231,19 @@ class NameChooser(BaseNameChooser):
return name
def generateNameFromTitle(self, obj):
title = obj.title
if len(title) > 15:
words = title.split()
if len(words) > 1:
title = '_'.join((words[0], words[-1]))
return self.normalizeName(title)
return generateNameFromTitle(obj.title)
def normalizeName(self, baseName):
return normalizeName(baseName)
def generateNameFromTitle(title):
if len(title) > 15:
words = title.split()
if len(words) > 1:
title = '_'.join((words[0], words[-1]))
return normalizeName(title)
def normalizeName(baseName):
specialCharacters = {
'\xc4': 'Ae', '\xe4': 'ae', '\xd6': 'Oe', '\xf6': 'oe',

View file

@ -107,6 +107,7 @@ class Base(object):
@Lazy
def textResources(self):
self.images = [[]]
self.otherResources = []
result = []
idx = 0
for rv in self.getResources():
@ -115,7 +116,7 @@ class Base(object):
idx += 1
result.append(rv)
self.images.append([])
else:
elif rv.context.contentType.startswith('image/'):
self.registerDojoLightbox()
url = self.nodeView.getUrlForTarget(rv.context)
src = '%s/mediaasset.html?v=small' % url
@ -123,6 +124,8 @@ class Base(object):
img = dict(src=src, fullImageUrl=fullSrc, title=rv.title,
description=rv.description, url=url, object=rv)
self.images[idx].append(img)
else:
self.otherResources.append(rv)
return result
def getDocumentTypeForResource(self, r):

View file

@ -130,7 +130,8 @@
<metal:topic define-macro="topic"
tal:define="children children|python:list(item.children());
textResources textResources|item/textResources">
textResources textResources|item/textResources;
resources item/otherResources">
<metal:info use-macro="view/concept_macros/concepttitle" />
<h2 i18n:translate=""
tal:condition="children">Children</h2>
@ -151,8 +152,18 @@
<a i18n:translate=""
tal:attributes="href python:view.getUrlForTarget(related.context)">
more...</a></p>
<div tal:repeat="image python:
item.images[repeat['related'].index() + 1]">
<a dojoType="dojox.image.Lightbox" group="mediasset"
i18n:attributes="title"
tal:attributes="href image/fullImageUrl;
title image/title">
<img tal:attributes="src image/src;
alt image/title" /></a>
</div>
</div>
</div>
<metal:info use-macro="view/concept_macros/conceptresources" />
</metal:topic>

View file

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

View file

@ -91,4 +91,18 @@
factory="loops.expert.browser.report.ResultsConceptView"
permission="zope.View" />
<zope:adapter
name="concept_results_embedded.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.expert.browser.report.EmbeddedResultsConceptView"
permission="zope.View" />
<browser:page
name="concept_results.csv"
for="loops.organize.interfaces.IConceptSchema"
class="loops.expert.browser.export.ResultsConceptCSVExport"
permission="zope.View" />
</configure>

104
expert/browser/export.py Normal file
View file

@ -0,0 +1,104 @@
#
# Copyright (c) 2015 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
#
"""
View classes for export of report results.
"""
import csv
from cStringIO import StringIO
from zope.cachedescriptors.property import Lazy
from zope.i18n import translate
from loops.common import normalizeName
from loops.expert.browser.report import ResultsConceptView
from loops.interfaces import ILoopsObject
from loops.util import _
class ResultsConceptCSVExport(ResultsConceptView):
isToplevel = True
reportMode = 'export'
delimiter = ';'
#encoding = 'UTF-8'
#encoding = 'ISO8859-15'
#encoding = 'CP852'
@Lazy
def encoding(self):
enc = self.globalOptions('csv_encoding')
if enc:
return enc[0]
return 'UTF-8'
def getFileName(self):
return normalizeName(self.context.title)
def getColumnTitle(self, field):
lang = self.languageInfo.language
return translate(_(field.title), target_language=lang)
def __call__(self):
fields = self.displayedColumns
fieldNames = [f.name for f in fields]
output = StringIO()
writer = csv.DictWriter(output, fieldNames, delimiter=self.delimiter)
output.write(self.delimiter.join(
[self.getColumnTitle(f) for f in fields]) + '\n')
results = self.reportInstance.getResults()
for row in results:
data = {}
for f in fields:
value = f.getValue(row)
if ILoopsObject.providedBy(value):
value = value.title
value = encode(value, self.encoding)
data[f.name] = value
writer.writerow(data)
text = output.getvalue()
self.setDownloadHeader(text)
return text
def setDownloadHeader(self, text):
response = self.request.response
response.setHeader('Content-Disposition',
'attachment; filename=%s.csv' %
self.getFileName())
response.setHeader('Cache-Control', '')
response.setHeader('Pragma', '')
response.setHeader('Content-Type', 'text/csv')
response.setHeader('Content-Length', len(text))
def encode(text, encoding):
if not isinstance(text, unicode):
return text
try:
return text.encode(encoding)
except UnicodeEncodeError:
result = []
for c in text:
try:
result.append(c.encode(encoding))
except UnicodeEncodeError:
result.append('?')
return ''.join(result)
return '???'

View file

@ -3,7 +3,8 @@
<div metal:define-macro="main">
<div tal:define="report item/reportInstance;
reportView nocall:item"
reportView nocall:item;
renderer item/resultsRenderer"
tal:attributes="class string:content-$level;">
<div metal:use-macro="item/report_macros/header" />
<div metal:use-macro="item/resultsRenderer" />
@ -37,6 +38,7 @@
<div metal:define-macro="buttons">
<input type="submit" name="report_execute" value="Execute Report"
tal:attributes="value item/reportExecuteTitle|string:Execute Report"
tal:condition="item/queryFields"
i18n:attributes="value" />
<input type="submit"
tal:condition="item/reportDownload"
@ -46,6 +48,13 @@
</div>
<br />
</form>
<tal:list condition="renderer">
<div metal:use-macro="renderer" />
</tal:list>
<tal:list condition="not:renderer">
<div metal:use-macro="view/concept_macros/conceptchildren" />
</tal:list>
</div>
</div>

View file

@ -46,6 +46,8 @@ class ReportView(ConceptView):
""" A view for defining (editing) a report.
"""
resultsRenderer = None # to be defined by subclass
@Lazy
def report_macros(self):
return self.controller.mergeTemplateMacros('report', report_template)
@ -59,6 +61,25 @@ class ReportView(ConceptView):
def dynamicParams(self):
return self.request.form
@Lazy
def report(self):
return self.adapted
@Lazy
def reportInstance(self):
instance = component.getAdapter(self.report, IReportInstance,
name=self.report.reportType)
instance.view = self
return instance
@Lazy
def queryFields(self):
ri = self.reportInstance
qf = ri.getAllQueryFields()
if ri.userSettings:
return [f for f in qf if f in ri.userSettings]
return qf
class ResultsView(NodeView):
@ -107,13 +128,6 @@ class ResultsView(NodeView):
def report(self):
return adapted(self.virtualTargetObject)
@Lazy
def reportInstance(self):
instance = component.getAdapter(self.report, IReportInstance,
name=self.report.reportType)
instance.view = self
return instance
#@Lazy
def results(self):
return self.reportInstance.getResults(self.params)
@ -193,6 +207,13 @@ class ResultsConceptView(ConceptView):
ri = component.getAdapter(self.report, IReportInstance,
name=reportType)
ri.view = self
if not ri.sortCriteria:
si = self.sortInfo.get('results')
if si is not None:
fnames = (si['colName'],)
ri.sortCriteria = [f for f in ri.getSortFields()
if f.name in fnames]
ri.sortDescending = not si['ascending']
return ri
def results(self):
@ -207,6 +228,31 @@ class ResultsConceptView(ConceptView):
def getColumnRenderer(self, col):
return self.result_macros[col.renderer]
@Lazy
def downloadLink(self, format='csv'):
opt = self.options('download_' + format)
if not opt:
opt = self.typeOptions('download_' + format)
if opt:
return opt[0]
def isSortableColumn(self, tableName, colName):
if tableName == 'results':
if colName in [f.name for f in self.reportInstance.getSortFields()]:
return True
return False
class EmbeddedResultsConceptView(ResultsConceptView):
@Lazy
def macro(self):
return self.result_macros['embedded_content']
@Lazy
def title(self):
return self.report.title
class ReportConceptView(ResultsConceptView, ReportView):
""" View on a concept using a report.

View file

@ -25,29 +25,61 @@
</div>
<div metal:define-macro="results">
<div metal:define-macro="embedded_content"
tal:define="report item/reportInstance;
reportView nocall:item">
<div tal:attributes="class string:content-$level;">
<metal:block use-macro="view/concept_macros/concepttitle_only" />
</div>
<div metal:use-macro="item/resultsRenderer" />
</div>
<div metal:define-macro="results"
tal:define="tableName string:results">
<br />
<tal:download condition="item/downloadLink">
<div class="button">
<a i18n:translate=""
tal:define="params python:item.getSortParams(tableName)"
tal:attributes="href string:${item/downloadLink}$params">Download Data</a>
</div>
<br />
</tal:download>
<table class="report"
tal:define="results reportView/results">
<tr>
<th tal:repeat="col results/displayedColumns"
tal:content="col/title"
tal:attributes="class col/cssClass"
i18n:translate="" />
<th style="white-space: nowrap"
tal:repeat="col results/displayedColumns">
<a title="tooltip_sort_column"
tal:define="colName col/name"
tal:omit-tag="python:not item.isSortableColumn(tableName, colName)"
tal:attributes="href python:item.getSortUrl(tableName, colName)"
i18n:attributes="title">
<span tal:content="col/title"
tal:attributes="class col/cssClass"
i18n:translate="" />
<img tal:define="src python:item.getSortImage(tableName, colName)"
tal:condition="src"
tal:attributes="src src" />
</a>
</th>
</tr>
<tr tal:repeat="row results">
<td tal:repeat="col results/displayedColumns"
tal:attributes="class col/cssClass">
<metal:column use-macro="python:
reportView.getColumnRenderer(col)" />
</td>
<tr tal:repeat="row results"
tal:attributes="class python:(repeat['row'].index() % 2) and 'even' or 'odd'">
<td tal:repeat="col results/displayedColumns"
tal:attributes="class col/cssClass">
<metal:column use-macro="python:
reportView.getColumnRenderer(col)" />
</td>
</tr>
<tr tal:define="row nocall:results/totals"
tal:condition="nocall:row">
<td tal:repeat="col results/displayedColumns"
tal:attributes="class col/cssClass">
<metal:column use-macro="python:
reportView.getColumnRenderer(col)" />
</td>
<td tal:repeat="col results/displayedColumns"
tal:attributes="class col/cssClass">
<metal:column use-macro="python:
reportView.getColumnRenderer(col)" />
</td>
</tr>
</table>
</div>
@ -78,6 +110,17 @@
</metal:state>
<metal:state define-macro="workitem_state">
<tal:column define="value python:col.getDisplayValue(row)"
condition="value">
<tal:action repeat="action value/actions">
<metal:action tal:condition="action"
use-macro="action/macro" />
</tal:action>
</tal:column>
</metal:state>
<metal:target define-macro="target">
<tal:column define="value python:col.getDisplayValue(row)">
<a tal:omit-tag="python:

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 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
@ -91,7 +91,8 @@ class Search(ConceptView):
@Lazy
def showActions(self):
return checkPermission('loops.ManageSite', self.context)
perm = (self.globalOptions('delete_permission') or ['loops.ManageSite'])[0]
return checkPermission(perm, self.context)
#return canWriteObject(self.context)
@property
@ -166,12 +167,10 @@ class Search(ConceptView):
title = request.get('name')
if title == '*':
title = None
types = request.get('searchType')
#types = request.get('searchType')
data = []
types = self.getTypes()
if title or types:
#if title or (types and types not in
# (u'loops:concept:*', 'loops:concept:account')):
if title or (types and types != u'loops:concept:*'):
if title is not None:
title = title.replace('(', ' ').replace(')', ' ').replace(' -', ' ')
#title = title.split(' ', 1)[0]
@ -302,8 +301,8 @@ class Search(ConceptView):
for state in states:
if stf.state == state:
break
else:
return False
else:
return False
return True

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -22,6 +22,7 @@ Field definitions for reports.
from zope.app.form.browser.interfaces import ITerms
from zope import component
from zope.i18n import translate
from zope.i18n.locales import locales
from zope.schema.interfaces import IVocabularyFactory, IContextSourceBinder
@ -29,18 +30,32 @@ from cybertools.composer.report.field import Field as BaseField
from cybertools.composer.report.field import TableCellStyle
from cybertools.composer.report.result import ResultSet
from cybertools.stateful.interfaces import IStateful, IStatesDefinition
from cybertools.util.date import timeStamp2Date
from cybertools.util.date import timeStamp2Date, timeStamp2ISO
from cybertools.util.format import formatDate
from loops.common import baseObject
from loops.expert.report import ReportInstance
from loops.organize.work.browser import WorkItemDetails
from loops import util
class Field(BaseField):
def getContext(self, row):
return row.context
def getSelectValue(self, row):
return self.getValue(row)
class StringField(Field):
def getSelectValue(self, row):
return self.getValue(row).strip()
def getSortValue(self, row):
return self.getValue(row).strip()
class TextField(Field):
format = 'text/restructured'
@ -109,6 +124,11 @@ class DateField(Field):
renderer = cssClass = 'center'
dbtype = 'date'
def getValue(self, row):
if getattr(row.parent.context.view, 'reportMode', None) == 'export':
return self.getDisplayValue(row)
super(DateField, self).getValue(row)
def getDisplayValue(self, row):
value = self.getRawValue(row)
if not value:
@ -127,16 +147,17 @@ class DateField(Field):
class StateField(Field):
statesDefinition = 'workItemStates'
statesDefinition = None
renderer = 'state'
def getDisplayValue(self, row):
if IStateful.providedBy(row.context):
stf = row.context
elif row.context is None:
context = self.getContext(row)
if IStateful.providedBy(context):
stf = context
elif context is None:
return None
else:
stf = component.getAdapter(baseObject(row.context), IStateful,
stf = component.getAdapter(context, IStateful,
name=self.statesDefinition)
stateObject = stf.getStateObject()
icon = stateObject.icon or 'led%s.png' % stateObject.color
@ -147,6 +168,27 @@ class StateField(Field):
return util._(text)
class WorkItemStateField(Field):
statesDefinition = 'workItemStates'
renderer = 'workitem_state'
def getValue(self, row):
view = row.parent.context.view
if getattr(view, 'reportMode', None) == 'export':
stateObject = row.context.getStateObject()
lang = view.languageInfo.language
return translate(util._(stateObject.title), target_language=lang)
return super(WorkItemStateField, self).getValue(row)
def getDisplayValue(self, row):
if row.context is None:
return None
details = WorkItemDetails(row.parent.context.view, row.context)
return dict(actions=details.actions())
class VocabularyField(Field):
vocabulary = None
@ -233,6 +275,14 @@ class RelationField(Field):
class TargetField(RelationField):
def getSortValue(self, row):
value = self.getRawValue(row)
if value is not None:
value = util.getObjectForUid(value)
if value is not None:
if value.title is not None:
return value.title.split()
def getValue(self, row):
value = self.getRawValue(row)
if value is None:
@ -247,6 +297,57 @@ class MultiLineField(Field):
def getValue(self, row):
return self.getRawValue(row)
# track fields
class TrackDateField(Field):
fieldType = 'date'
part = 'date'
format = 'short'
descending = False
cssClass = 'right'
def getValue(self, row):
reportMode = getattr(row.parent.context.view, 'reportMode', None)
if reportMode == 'export':
return self.getDisplayValue(row)
value = self.getRawValue(row)
if not value:
return None
return timeStamp2Date(value)
def getDisplayValue(self, row):
value = self.getRawValue(row)
if value:
value = timeStamp2Date(value)
view = row.parent.context.view
return formatDate(value, self.part, self.format,
view.languageInfo.language)
return u''
def getSelectValue(self, row):
value = self.getRawValue(row)
if not value:
return ''
return timeStamp2ISO(value)[:10]
def getSortValue(self, row):
value = self.getRawValue(row)
if value and self.descending:
return -value
return value or None
class TrackDateTimeField(TrackDateField):
part = 'dateTime'
class TrackTimeField(TrackDateField):
part = 'time'
def getDisplayValues(self, row):
value = self.getValue(row)
if not isinstance(value, (list, tuple)):

View file

@ -88,6 +88,7 @@ class ReportInstance(BaseReport):
#headerRowFactory = Row
view = None # set upon creation
#headerRowFactory = Row
def __init__(self, context):
self.context = context
@ -120,7 +121,9 @@ class ReportInstance(BaseReport):
result = list(self.selectObjects(parts)) # may modify parts
qc = CompoundQueryCriteria(parts)
return ResultSet(self, result, rowFactory=self.rowFactory,
sortCriteria=self.getSortCriteria(), queryCriteria=qc,
sortCriteria=self.getSortCriteria(),
sortDescending=self.sortDescending,
queryCriteria=qc,
limits=limits)
def selectObjects(self, parts):
@ -173,3 +176,15 @@ class DefaultConceptReportInstance(ReportInstance):
label = u'Default Concept Report'
# specialized rows
class TrackRow(Row):
@staticmethod
def getContextAttr(obj, attr):
if attr in obj.context.metadata_attributes:
return getattr(obj.context, attr)
return obj.context.data.get(attr)

View file

@ -66,13 +66,13 @@ zcml in real life:
>>> t = searchView.typesForSearch()
>>> len(t)
15
16
>>> t.getTermByToken('loops:resource:*').title
'Any Resource'
>>> t = searchView.conceptTypesForSearch()
>>> len(t)
12
13
>>> t.getTermByToken('loops:concept:*').title
'Any Concept'
@ -91,7 +91,7 @@ a controller attribute for the search view.
>>> searchView.submitReplacing('1.results', '1.search.form', pageView)
'submitReplacing("1.results", "1.search.form",
"http://127.0.0.1/loops/views/page/.target96/@@searchresults.html");...'
"http://127.0.0.1/loops/views/page/.target.../@@searchresults.html");...'
Basic (text/title) search
-------------------------
@ -177,7 +177,7 @@ of the concepts' titles:
>>> request = TestRequest(form=form)
>>> view = Search(page, request)
>>> view.listConcepts()
u"{identifier: 'id', items: [{label: 'Zope (Thema)', name: 'Zope', id: '101'}, {label: 'Zope 2 (Thema)', name: 'Zope 2', id: '103'}, {label: 'Zope 3 (Thema)', name: 'Zope 3', id: '105'}]}"
u"{identifier: 'id', items: [{label: 'Zope (Thema)', name: 'Zope', id: '...'}, {label: 'Zope 2 (Thema)', name: 'Zope 2', id: '...'}, {label: 'Zope 3 (Thema)', name: 'Zope 3', id: '...'}]}"
Preset Concept Types on Search Forms
------------------------------------
@ -219,13 +219,13 @@ and thus include the customer type in the preset search types.
>>> searchView.conceptsForType('loops:concept:customer')
[{'token': 'none', 'title': u'not selected'},
{'token': '74', 'title': u'Customer 1'},
{'token': '76', 'title': u'Customer 2'},
{'token': '78', 'title': u'Customer 3'}]
{'token': '...', 'title': u'Customer 1'},
{'token': '...', 'title': u'Customer 2'},
{'token': '...', 'title': u'Customer 3'}]
Let's use this new search option for querying:
>>> form = {'search.4.text_selected': u'74'}
>>> form = {'search.4.text_selected': u'75'}
>>> resultsView = SearchResults(page, TestRequest(form=form))
>>> results = list(resultsView.results)
>>> results[0].title

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()
>>> loopsRoot = site['loops']
>>> len(concepts), len(resources), len(views)
(33, 3, 1)
(35, 3, 1)
Importing loops Objects
@ -44,7 +44,7 @@ Creating the corresponding objects
>>> loader = Loader(loopsRoot)
>>> loader.load(elements)
>>> len(concepts), len(resources), len(views)
(34, 3, 1)
(36, 3, 1)
>>> from loops.common import adapted
>>> adMyquery = adapted(concepts['myquery'])
@ -131,7 +131,7 @@ Extracting elements
>>> extractor = Extractor(loopsRoot, os.path.join(dataDirectory, 'export'))
>>> elements = list(extractor.extract())
>>> len(elements)
66
69
Writing object information to the external storage
--------------------------------------------------

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
"""
Integrator interfaces.
$Id$
"""
from zope.interface import Interface, Attribute
@ -133,3 +131,5 @@ class IOfficeFile(IExternalFile):
It provides access to the document content and properties.
"""
documentPropertiesAccessible = Attribute(
'Are document properties accessible?')

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 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
@ -26,7 +26,7 @@ from lxml import etree
import os
import shutil
from time import strptime
from zipfile import ZipFile
from zipfile import ZipFile, BadZipfile
from zope.cachedescriptors.property import Lazy
from zope import component
from zope.component import adapts
@ -52,12 +52,22 @@ class OfficeFile(ExternalFileAdapter):
implements(IOfficeFile)
_adapterAttributes = (ExternalFileAdapter._adapterAttributes +
('documentPropertiesAccessible',))
propertyMap = {u'Revision:': 'version'}
propFileName = 'docProps/custom.xml'
corePropFileName = 'docProps/core.xml'
fileExtensions = ('.docm', '.docx', 'dotm', 'dotx', 'pptx', 'potx', 'ppsx',
'.xlsm', '.xlsx', '.xltm', '.xltx')
def getDocumentPropertiesAccessible(self):
return getattr(self.context, '_documentPropertiesAccessible', True)
def setDocumentPropertiesAccessible(self, value):
self.context._documentPropertiesAccessible = value
documentPropertiesAccessible = property(
getDocumentPropertiesAccessible, setDocumentPropertiesAccessible)
@Lazy
def logger(self):
return getLogger('loops.integrator.office.base.OfficeFile')
@ -79,14 +89,19 @@ class OfficeFile(ExternalFileAdapter):
def docPropertyDom(self):
fn = self.docFilename
result = dict(core=[], custom=[])
if not os.path.exists(fn):
# may happen before file has been created
return result
root, ext = os.path.splitext(fn)
if not ext.lower() in self.fileExtensions:
return result
try:
zf = ZipFile(fn, 'r')
except IOError, e:
self.documentPropertiesAccessible = True
except (IOError, BadZipfile), e:
from logging import getLogger
self.logger.warn(e)
self.documentPropertiesAccessible = False
return result
if self.corePropFileName not in zf.namelist():
self.logger.warn('Core properties not found in file %s.' %
@ -123,6 +138,8 @@ class OfficeFile(ExternalFileAdapter):
attributes = {}
# get dc:description from core.xml
desc = self.getCoreProperty('description')
if not self.documentPropertiesAccessible:
return
if desc is not None:
attributes['comments'] = desc
dom = self.docPropertyDom['custom']

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -30,8 +30,13 @@ from cybertools.typology.interfaces import IType
from loops.browser.action import DialogAction
from loops.browser.common import BaseView
from loops.browser.concept import ConceptView
from loops.common import adapted
from loops.knowledge.interfaces import IPerson, ITask
from loops.organize.party import getPersonForUser
from loops.organize.personal import favorite
from loops.organize.personal.interfaces import IFavorites
from loops.security.common import checkPermission
from loops import util
from loops.util import _
@ -69,6 +74,63 @@ actions.register('createQualification', 'portlet', DialogAction,
)
class InstitutionMixin(object):
knowledge_macros = knowledge_macros
adminMaySelectAllInstitutions = True
@Lazy
def institutionType(self):
return self.conceptManager['institution']
@Lazy
def institutions(self):
if self.adminMaySelectAllInstitutions:
if checkPermission('loops.ManageWorkspaces', self.context):
return self.getAllInstitutions()
result = []
p = getPersonForUser(self.context, self.request)
if p is None:
return result
for parent in p.getParents(
[self.memberPredicate, self.masterPredicate]):
if parent.conceptType == self.institutionType:
result.append(dict(
object=adapted(parent),
title=parent.title,
uid=util.getUidForObject(parent)))
return result
def getAllInstitutions(self):
insts = self.institutionType.getChildren([self.typePredicate])
return [dict(object=adapted(inst),
title=inst.title,
uid=util.getUidForObject(inst)) for inst in insts]
def setInstitution(self, uid):
inst = util.getObjectForUid(uid)
person = getPersonForUser(self.context, self.request)
favorite.setInstitution(person, inst)
self.institution = inst
return True
def getSavedInstitution(self):
person = getPersonForUser(self.context, self.request)
favorites = IFavorites(self.loopsRoot.getRecordManager()['favorites'])
for inst in favorites.list(person, type='institution'):
return adapted(util.getObjectForUid(inst))
@Lazy
def institution(self):
saved = self.getSavedInstitution()
for inst in self.institutions:
if inst['object'] == saved:
return inst['object']
if self.institutions:
return self.institutions[0]['object']
class MyKnowledge(ConceptView):
template = template

View file

@ -1,9 +1,11 @@
type(u'competence', u'Kompetenz', viewName=u'',
type(u'competence', u'Qualifikation', viewName=u'',
typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',
options=u'action.portlet:create_subtype,edit_concept')
type(u'person', u'Person', viewName=u'',
typeInterface=u'loops.knowledge.interfaces.IPerson',
options=u'action.portlet:createQualification,editPerson')
type(u'report', u'Report', viewName=u'',
typeInterface='loops.expert.report.IReport')
type(u'task', u'Aufgabe', viewName=u'',
typeInterface=u'loops.knowledge.interfaces.ITask',
options=u'action.portlet:createTask,editTask')
@ -26,6 +28,10 @@ concept(u'requires', u'requires', u'predicate')
concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children',
predicateInterface='loops.interfaces.IIsSubtype')
# reports
concept(u'qualification_overview', u'Qualification Overview', u'report',
reportType=u'qualification_overview')
# structure
child(u'general', u'competence', u'standard')
child(u'general', u'depends', u'standard')
@ -38,6 +44,7 @@ child(u'general', u'topic', u'standard')
#child(u'general', u'training', u'standard')
child(u'system', u'issubtype', u'standard')
child(u'system', u'report', u'standard')
child(u'competence', u'competence', u'issubtype')
#child(u'competence', u'training', u'issubtype', usePredicate=u'provides')

View file

@ -1,6 +1,14 @@
type(u'competence', u'Kompetenz', viewName=u'',
type(u'competence', u'Qualifikation', viewName=u'',
typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',
options=u'action.portlet:create_subtype,edit_concept')
type(u'ipskill', u'Kompetenz', viewName=u'',
options=u'action.portlet:edit_concept')
type(u'ipskillsrequired', u'Soll-Profil', viewName=u'',
options=u'action.portlet:edit_concept')
type(u'jobposition', u'Stelle', viewName=u'',
options=u'action.portlet:edit_concept')
type(u'report', u'Report', viewName=u'',
typeInterface='loops.expert.report.IReport')
# type(u'person', u'Person', viewName=u'',
# typeInterface=u'loops.knowledge.interfaces.IPerson',
# options=u'action.portlet:editPerson')
@ -26,9 +34,16 @@ concept(u'requires', u'requires', u'predicate')
concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children',
predicateInterface='loops.interfaces.IIsSubtype')
# reports
concept(u'qualification_overview', u'Qualification Overview', u'report',
reportType=u'qualification_overview')
# structure
child(u'general', u'competence', u'standard')
child(u'general', u'depends', u'standard')
child(u'general', u'ipskill', u'standard')
child(u'general', u'ipskillsrequired', u'standard')
child(u'general', u'jobposition', u'standard')
child(u'general', u'knows', u'standard')
#child(u'general', u'person', u'standard')
child(u'general', u'provides', u'standard')
@ -38,6 +53,7 @@ child(u'general', u'requires', u'standard')
#child(u'general', u'training', u'standard')
child(u'system', u'issubtype', u'standard')
child(u'system', u'report', u'standard')
child(u'competence', u'competence', u'issubtype')
#child(u'competence', u'training', u'issubtype', usePredicate=u'provides')

View file

@ -1,5 +1,3 @@
<!-- $Id$ -->
<configure
xmlns:zope="http://namespaces.zope.org/zope"
xmlns="http://namespaces.zope.org/browser"
@ -28,14 +26,14 @@
name="create_glossaryitem.html"
for="loops.interfaces.INode"
class="loops.knowledge.glossary.browser.CreateGlossaryItemForm"
permission="zope.ManageContent"
permission="zope.View"
/>
<page
name="edit_glossaryitem.html"
for="loops.interfaces.INode"
class="loops.knowledge.glossary.browser.EditGlossaryItemForm"
permission="zope.ManageContent"
permission="zope.View"
/>
<zope:adapter
@ -43,7 +41,7 @@
for="loops.browser.node.NodeView
zope.publisher.interfaces.browser.IBrowserRequest"
factory="loops.knowledge.glossary.browser.CreateGlossaryItem"
permission="zope.ManageContent"
permission="zope.View"
/>
<zope:adapter
@ -51,7 +49,7 @@
for="loops.browser.node.NodeView
zope.publisher.interfaces.browser.IBrowserRequest"
factory="loops.knowledge.glossary.browser.EditGlossaryItem"
permission="zope.ManageContent"
permission="zope.View"
/>
</configure>

View file

@ -1,6 +1,27 @@
<html i18n:domain="loops">
<metal:institution define-macro="select_institution">
<form method="post">
<div style="font-size: 120%; padding: 10px 0 10px 0">
<span i18n:translate="">Organisation/Team</span>:
<b tal:content="item/institution/title" />
<img tal:condition="python:len(item.institutions) > 1"
src="/@@/cybertools.icons/application_edit.png"
onclick="dojo.byId('select_institution').style.display = 'inline'" />
<select name="select_institution" id="select_institution"
style="display: none"
onchange="submit()">
<option tal:repeat="inst item/institutions"
tal:content="inst/title"
tal:attributes="value inst/uid;
selected python:inst['object'] == item.institution" />
</select>
</div>
</form>
</metal:institution>
<metal:providers define-macro="requirement_providers">
<metal:block use-macro="view/concept_macros/conceptdata" />
<div>

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -18,19 +18,25 @@
"""
Definition of view classes and other browser related stuff for the
loops.knowledge package.
loops.knowledge.qualification package.
"""
from zope import interface, component
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from loops.browser.concept import ConceptView
from loops.expert.browser.export import ResultsConceptCSVExport
from loops.expert.browser.report import ResultsConceptView
from loops.knowledge.browser import template, knowledge_macros
from loops.knowledge.qualification.base import QualificationRecord
from loops.organize.party import getPersonForUser
from loops.util import _
class PersonQualificationView(ResultsConceptView):
class Qualifications(ResultsConceptView):
pass
# obsolete because we can directly use ResultsConceptView
#reportName = 'qualification_overview'
pass # report assigned to query via hasReport relation

View file

@ -15,4 +15,50 @@
<!-- views -->
<zope:adapter
name="qualifications.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.knowledge.qualification.browser.Qualifications"
permission="zope.View" />
<!-- reports -->
<zope:adapter
name="qualification_overview"
factory="loops.knowledge.qualification.report.QualificationOverview"
provides="loops.expert.report.IReportInstance"
trusted="True" />
<zope:class class="loops.knowledge.qualification.report.QualificationOverview">
<require permission="zope.View"
interface="loops.expert.report.IReportInstance" />
<require permission="zope.ManageContent"
set_schema="loops.expert.report.IReportInstance" />
</zope:class>
<zope:adapter
name="qualifications"
factory="loops.knowledge.qualification.report.Qualifications"
provides="loops.expert.report.IReportInstance"
trusted="True" />
<zope:class class="loops.knowledge.qualification.report.Qualifications">
<require permission="zope.View"
interface="loops.expert.report.IReportInstance" />
<require permission="zope.ManageContent"
set_schema="loops.expert.report.IReportInstance" />
</zope:class>
<zope:adapter
name="person_qualifications"
factory="loops.knowledge.qualification.report.PersonQualifications"
provides="loops.expert.report.IReportInstance"
trusted="True" />
<zope:class class="loops.knowledge.qualification.report.PersonQualifications">
<require permission="zope.View"
interface="loops.expert.report.IReportInstance" />
<require permission="zope.ManageContent"
set_schema="loops.expert.report.IReportInstance" />
</zope:class>
</configure>

View file

@ -0,0 +1,133 @@
#
# Copyright (c) 2015 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
#
"""
Qualification management report definitions.
"""
from zope.cachedescriptors.property import Lazy
from cybertools.composer.report.base import LeafQueryCriteria, CompoundQueryCriteria
from cybertools.util.jeep import Jeep
from loops.expert.report import ReportInstance
from loops.organize.work.report import WorkRow
from loops.organize.work.report import deadline, day, task, party, state
from loops.organize.work.report import dayStart, dayEnd
from loops.organize.work.report import workTitle, workDescription
from loops.organize.work.report import partyState
from loops import util
class QualificationOverview(ReportInstance):
type = 'qualification_overview'
label = u'Qualification Overview'
rowFactory = WorkRow
fields = Jeep((task, party, workTitle, dayStart, dayEnd, state,
partyState,)) # +deadline?
defaultOutputFields = Jeep(list(fields)[:-1])
defaultSortCriteria = (party, task,)
def getOptions(self, option):
return self.view.options(option)
@Lazy
def states(self):
return self.getOptions('report_select_state' or ('planned',))
@property
def queryCriteria(self):
crit = self.context.queryCriteria or []
f = self.fields.partyState
crit.append(
LeafQueryCriteria(f.name, f.operator, 'active', f))
return CompoundQueryCriteria(crit)
def selectObjects(self, parts):
result = []
workItems = self.recordManager['work']
pred = self.conceptManager['querytarget']
types = self.view.context.getChildren([pred])
for t in types:
for c in t.getChildren([self.view.typePredicate]):
uid = util.getUidForObject(c)
for wi in workItems.query(taskId=uid, state=self.states):
result.append(wi)
return result
class Qualifications(QualificationOverview):
type = 'qualifications'
label = u'Qualifications'
taskTypeNames = ('competence',)
def getOptions(self, option):
return self.view.typeOptions(option)
def selectObjects(self, parts):
result = []
workItems = self.recordManager['work']
target = self.view.context
tasks = [target] + self.getAllSubtasks(target)
for t in tasks:
uid = util.getUidForObject(t)
for wi in workItems.query(taskId=uid, state=self.states):
result.append(wi)
return result
def getAllSubtasks(self, concept):
result = []
for c in concept.getChildren([self.view.defaultPredicate]):
if c.conceptType in self.taskTypes:
result.append(c)
result.extend(self.getAllSubtasks(c))
return result
@Lazy
def taskTypes(self):
return [c for c in [self.conceptManager.get(name)
for name in self.taskTypeNames]
if c is not None]
class PersonQualifications(QualificationOverview):
type = 'person_qualifications'
label = u'Qualifications for Person'
defaultSortCriteria = (task,)
def getOptions(self, option):
return self.view.typeOptions(option)
@property
def queryCriteria(self):
crit = self.context.queryCriteria or []
return CompoundQueryCriteria(crit)
def selectObjects(self, parts):
workItems = self.recordManager['work']
person = self.view.context
uid = util.getUidForObject(person)
return workItems.query(userName=uid, state=self.states)

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 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
@ -23,6 +23,7 @@ surveys and self-assessments.
import csv
from cStringIO import StringIO
import math
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from zope.i18n import translate
@ -31,61 +32,305 @@ 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.common import adapted, baseObject
from loops.knowledge.browser import InstitutionMixin
from loops.knowledge.survey.response import Responses
from loops.organize.party import getPersonForUser
from loops.security.common import checkPermission
from loops.util import getObjectForUid
from loops.util import _
template = ViewPageTemplateFile('view_macros.pt')
class SurveyView(ConceptView):
class SurveyView(InstitutionMixin, ConceptView):
data = None
errors = None
errors = message = None
batchSize = 12
teamData = None
template = template
adminMaySelectAllInstitutions = False
@Lazy
def macro(self):
self.registerDojo()
return template.macros['survey']
@Lazy
def title(self):
title = self.context.title
personId = self.request.form.get('person')
if personId:
person = adapted(getObjectForUid(personId))
if person is not None:
return '%s: %s' % (title, person.title)
return title
@Lazy
def tabview(self):
if self.editable:
return 'index.html'
def results(self):
def getUrlParamString(self):
qs = super(SurveyView, self).getUrlParamString()
if qs.startswith('?report='):
return ''
return qs
@Lazy
def report(self):
return self.request.form.get('report')
@Lazy
def questionnaireType(self):
return self.adapted.questionnaireType
def teamReports(self):
if self.adapted.teamBasedEvaluation:
if checkPermission('loops.ViewRestricted', self.context):
return [dict(name='standard', label='label_survey_report_standard'),
dict(name='questions',
label='label_survey_report_questions')]
def update(self):
instUid = self.request.form.get('select_institution')
if instUid:
return self.setInstitution(instUid)
@Lazy
def groups(self):
result = []
response = None
if self.questionnaireType == 'pref_selection':
groups = [g.questions for g in self.adapted.questionGroups]
questions = []
for idxg, g in enumerate(groups):
qus = []
for idxq, qu in enumerate(g):
questions.append((idxg + 3 * idxq, idxg, qu))
questions.sort()
questions = [item[2] for item in questions]
size = len(questions)
for idx in range(0, size, 3):
result.append(dict(title=u'Question', infoText=None,
questions=questions[idx:idx+3]))
return [g for g in result if len(g['questions']) == 3]
if self.adapted.noGrouping:
questions = list(self.adapted.questions)
questions.sort(key=lambda x: x.title)
size = len(questions)
bs = self.batchSize
for idx in range(0, size, bs):
result.append(dict(title=u'Question', infoText=None,
questions=questions[idx:idx+bs]))
else:
for group in self.adapted.questionGroups:
result.append(dict(title=group.title,
infoText=self.getInfoText(group),
questions=group.questions))
return result
@Lazy
def answerOptions(self):
opts = self.adapted.answerOptions
if not opts:
opts = [
dict(value='none', label=u'No answer',
description=u'survey_value_none'),
dict(value=3, label=u'Fully applies',
description=u'survey_value_3'),
dict(value=2, label=u'', description=u'survey_value_2'),
dict(value=1, label=u'', description=u'survey_value_1'),
dict(value=0, label=u'Does not apply',
description=u'survey_value_0'),]
return opts
@Lazy
def showFeedbackText(self):
sft = self.adapted.showFeedbackText
return sft is None and True or sft
@Lazy
def feedbackColumns(self):
cols = self.adapted.feedbackColumns
if not cols:
cols = [
dict(name='text', label=u'Response'),
dict(name='score', label=u'Score')]
if self.report == 'standard':
cols = [c for c in cols if c['name'] in self.teamColumns]
return cols
teamColumns = ['category', 'average', 'stddev', 'teamRank', 'text']
@Lazy
def showTeamResults(self):
for c in self.feedbackColumns:
if c['name'] in ('average', 'teamRank'):
return True
return False
def getTeamData(self, respManager):
result = []
pred = [self.conceptManager.get('ismember'),
self.conceptManager.get('ismaster')]
if None in pred:
return result
inst = self.institution
instUid = self.getUidForObject(inst)
if inst:
for c in inst.getChildren(pred):
uid = self.getUidForObject(c)
data = respManager.load(uid, instUid)
if data:
resp = Response(self.adapted, None)
for qu in self.adapted.questions:
if qu.questionType in (None, 'value_selection'):
if qu.uid in data:
value = data[qu.uid]
if isinstance(value, int) or value.isdigit():
resp.values[qu] = int(value)
else:
resp.texts[qu] = data.get(qu.uid) or u''
qgAvailable = True
for qg in self.adapted.questionGroups:
if qg.uid in data:
resp.values[qg] = data[qg.uid]
else:
qgAvailable = False
if not qgAvailable:
values = resp.getGroupedResult()
for v in values:
resp.values[v['group']] = v['score']
result.append(resp)
return result
def results(self):
if self.report:
return self.teamResults(self.report)
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_'):
action = None
for k in ('submit', 'save'):
if k in form:
action = k
break
if action is None:
return []
respManager = Responses(self.context)
respManager.personId = (self.request.form.get('person') or
respManager.getPersonId())
if self.adapted.teamBasedEvaluation and self.institution:
respManager.institutionId = self.getUidForObject(
baseObject(self.institution))
if self.adapted.questionnaireType == 'pref_selection':
return self.prefsResults(respManager, form, action)
data = {}
response = Response(self.adapted, None)
for key, value in form.items():
if key.startswith('question_'):
if value != 'none':
uid = key[len('question_'):]
question = adapted(self.getObjectForUid(uid))
if value != 'none':
if value.isdigit():
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]
data[uid] = value
response.values[question] = value
values = response.getGroupedResult()
for v in values:
data[self.getUidForObject(v['group'])] = v['score']
respManager.save(data)
if action == 'save':
self.message = u'Your data have been saved.'
return []
self.data = data
self.errors = self.check(response)
if self.errors:
return []
result = [dict(category=r['group'].title, text=r['feedback'].text,
score=int(round(r['score'] * 100)), rank=r['rank'])
for r in values]
if self.showTeamResults:
self.teamData = self.getTeamData(respManager)
groups = [r['group'] for r in values]
teamValues = response.getTeamResult(groups, self.teamData)
for idx, r in enumerate(teamValues):
result[idx]['average'] = int(round(r['average'] * 100))
result[idx]['teamRank'] = r['rank']
return result
def teamResults(self, report):
result = []
respManager = Responses(self.context)
self.teamData = self.getTeamData(respManager)
response = Response(self.adapted, None)
groups = self.adapted.questionGroups
teamValues = response.getTeamResult(groups, self.teamData)
for idx, r in enumerate(teamValues):
group = r['group']
item = dict(category=group.title,
average=int(round(r['average'] * 100)),
teamRank=r['rank'])
if group.feedbackItems:
wScore = r['average'] * len(group.feedbackItems) - 0.00001
item['text'] = group.feedbackItems[int(wScore)].text
result.append(item)
return result
def getTeamResultsForQuestion(self, question, questionnaire):
result = dict(average=0.0, stddev=0.0)
if self.teamData is None:
respManager = Responses(self.context)
self.teamData = self.getTeamData(respManager)
answerRange = question.answerRange or questionnaire.defaultAnswerRange
values = [r.values.get(question) for r in self.teamData]
values = [v for v in values if v is not None]
if values:
average = float(sum(values)) / len(values)
if question.revertAnswerOptions:
average = answerRange - average - 1
devs = [(average - v) for v in values]
stddev = math.sqrt(sum(d * d for d in devs) / len(values))
average = average * 100 / (answerRange - 1)
stddev = stddev * 100 / (answerRange - 1)
result['average'] = int(round(average))
result['stddev'] = int(round(stddev))
texts = [r.texts.get(question) for r in self.teamData]
result['texts'] = '<br />'.join([unicode(t) for t in texts if t])
return result
def prefsResults(self, respManager, form, action):
result = []
data = {}
for key, value in form.items():
if key.startswith('group_') and value:
data[value] = 1
respManager.save(data)
if action == 'save':
self.message = u'Your data have been saved.'
return []
self.data = data
#self.errors = self.check(response)
if self.errors:
return []
for group in self.adapted.questionGroups:
score = 0
for qu in group.questions:
value = data.get(qu.uid) or 0
if qu.revertAnswerOptions:
value = -value
score += value
result.append(dict(category=group.title, score=score))
return 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.')
errors.append(dict(uid=qu.uid,
text='Please answer the obligatory questions.'))
break
qugroups = {}
for qugroup in self.adapted.questionGroups:
@ -97,7 +342,12 @@ class SurveyView(ConceptView):
if minAnswers in (u'', None):
minAnswers = len(qugroup.questions)
if count < minAnswers:
errors.append('Please answer the minimum number of questions.')
if self.adapted.noGrouping:
errors.append(dict(uid=qugroup.uid,
text='Please answer the highlighted questions.'))
else:
errors.append(dict(uid=qugroup.uid,
text='Please answer the minimum number of questions.'))
break
return errors
@ -106,7 +356,8 @@ class SurveyView(ConceptView):
text = qugroup.description
info = None
if qugroup.minAnswers in (u'', None):
info = translate(_(u'Please answer all questions.'), target_language=lang)
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)),
@ -115,16 +366,46 @@ class SurveyView(ConceptView):
text = u'<i>%s</i><br />(%s)' % (text, info)
return text
def loadData(self):
if self.data is None:
respManager = Responses(self.context)
respManager.personId = (self.request.form.get('person') or
respManager.getPersonId())
if self.adapted.teamBasedEvaluation and self.institution:
respManager.institutionId = self.getUidForObject(
baseObject(self.institution))
self.data = respManager.load()
def getValues(self, question):
setting = None
if self.data is None:
self.data = Responses(self.context).load()
self.loadData()
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))]
if setting is None:
setting = 'none'
setting = str(setting)
result = []
for opt in self.answerOptions:
value = str(opt['value'])
result.append(dict(value=value, checked=(setting == value),
title=opt.get('description') or u''))
return result
def getTextValue(self, question):
self.loadData()
if self.data:
return self.data.get(question.uid)
def getPrefsValue(self, question):
self.loadData()
if self.data:
return self.data.get(question.uid)
def getCssClass(self, question):
cls = ''
if self.errors and self.data.get(question.uid) is None:
cls = 'error '
return cls + 'vpad'
class SurveyCsvExport(NodeView):
@ -132,36 +413,43 @@ class SurveyCsvExport(NodeView):
encoding = 'ISO8859-15'
def encode(self, text):
text.encode(self.encoding)
return text.encode(self.encoding)
@Lazy
def questions(self):
result = []
for idx1, qug in enumerate(adapted(self.virtualTargetObject).questionGroups):
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']
infoCols = ['Institution', 'Name', 'Timestamp']
dataCols = ['%02i-%02i' % (item[0], item[1]) for item in self.questions]
return infoCols + dataCols
def getRows(self):
memberPred = self.conceptManager.get('ismember')
for tr in Responses(self.virtualTargetObject).getAllTracks():
p = adapted(getObjectForUid(tr.userName))
name = p and p.title or u'???'
name = self.encode(p and p.title or u'???')
inst = u''
if memberPred is not None:
for i in baseObject(p).getParents([memberPred]):
inst = self.encode(i.title)
break
ts = formatTimeStamp(tr.timeStamp)
cells = [tr.data.get(qu.uid, -1)
for (idx1, idx2, qug, qu) in self.questions]
yield [name, ts] + cells
yield [inst, name, ts] + cells
def __call__(self):
f = StringIO()
writer = csv.writer(f, delimiter=',')
writer.writerow(self.columns)
for row in self.getRows():
for row in sorted(self.getRows()):
writer.writerow(row)
text = f.getvalue()
self.setDownloadHeader(text)

View file

@ -23,21 +23,78 @@ Interfaces for surveys used in knowledge management.
from zope.interface import Interface, Attribute
from zope import interface, component, schema
from cybertools.composer.schema.grid.interfaces import Records
from cybertools.knowledge.survey import interfaces
from loops.interfaces import IConceptSchema, ILoopsAdapter
from loops.util import _
from loops.util import _, KeywordVocabulary
class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire):
""" A collection of questions for setting up a survey.
"""
questionnaireHeader = schema.Text(
title=_(u'Questionnaire Header'),
description=_(u'Text that will appear at the top of the questionnaire.'),
default=u'',
missing_value=u'',
required=False)
questionnaireType = schema.Choice(
title=_(u'Questionnaire Type'),
description=_(u'Select the type of the questionnaire.'),
source=KeywordVocabulary((
('standard', _(u'Standard Questionnaire')),
('pref_selection', _(u'Preference Selection')),
)),
default='standard',
required=True)
defaultAnswerRange = schema.Int(
title=_(u'Answer Range'),
description=_(u'Number of items (answer options) to select from.'),
default=4,
required=True)
answerOptions = Records(
title=_(u'Answer Options'),
description=_(u'Values to select from with corresponding column '
u'labels and descriptions. There should be at '
u'least answer range items with numeric values.'),
default=[],
required=False)
answerOptions.column_types = [
schema.Text(__name__='value', title=u'Value',),
schema.Text(__name__='label', title=u'Label'),
schema.Text(__name__='description', title=u'Description'),
schema.Text(__name__='colspan', title=u'ColSpan'),
schema.Text(__name__='cssclass', title=u'CSS Class'),]
noGrouping = schema.Bool(
title=_(u'No Grouping of Questions'),
description=_(u'The questions should be presented in a linear manner, '
u'not grouped by categories or question groups.'),
default=False,
required=False)
teamBasedEvaluation = schema.Bool(
title=_(u'Team-based Evaluation'),
description=_(u'.'),
default=False,
required=False)
feedbackColumns = Records(
title=_(u'Feedback Columns'),
description=_(u'Column definitions for the results table '
u'on the feedback page.'),
default=[],
required=False)
feedbackColumns.column_types = [
schema.Text(__name__='name', title=u'Column Name',),
schema.Text(__name__='label', title=u'Column Label'),]
feedbackHeader = schema.Text(
title=_(u'Feedback Header'),
description=_(u'Text that will appear at the top of the feedback page.'),
@ -69,6 +126,16 @@ class IQuestion(IConceptSchema, interfaces.IQuestion):
""" A single question within a questionnaire.
"""
questionType = schema.Choice(
title=_(u'Question Type'),
description=_(u'Select the type of the question.'),
source=KeywordVocabulary((
('value_selection', _(u'Value Selection')),
('text', _(u'Text')),
)),
default='value_selection',
required=True)
required = schema.Bool(
title=_(u'Required'),
description=_(u'Question must be answered.'),

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 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
@ -34,18 +34,32 @@ class Responses(BaseRecordManager):
implements(IResponses)
storageName = 'survey_responses'
personId = None
institutionId = None
def __init__(self, context):
self.context = context
def save(self, data):
if self.personId:
self.storage.saveUserTrack(self.uid, 0, self.personId, data,
id = self.personId
if self.institutionId:
id += '.' + self.institutionId
self.storage.saveUserTrack(self.uid, 0, id, data,
update=True, overwrite=True)
def load(self):
if self.personId:
tracks = self.storage.getUserTracks(self.uid, 0, self.personId)
def load(self, personId=None, institutionId=None):
if personId is None:
personId = self.personId
if institutionId is None:
institutionId = self.institutionId
if personId:
id = personId
if institutionId:
id += '.' + institutionId
tracks = self.storage.getUserTracks(self.uid, 0, id)
if not tracks: # then try without institution
tracks = self.storage.getUserTracks(self.uid, 0, personId)
if tracks:
return tracks[0].data
return {}

View file

@ -4,96 +4,273 @@
<metal:block define-macro="survey"
tal:define="feedback item/results;
errors item/errors">
questType item/questionnaireType;
questMacro python:
'quest_' + (questType or 'standard');
report request/report|nothing;
reportMacro python:
'report_' + (report or 'standard');
errors item/errors;
message item/message;
dummy item/update">
<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>
<div tal:define="header item/adapted/feedbackHeader"
<div tal:define="header item/adapted/questionnaireHeader"
tal:condition="header"
tal:content="structure python:item.renderText(header, 'text/restructured')" />
<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')" />
tal:content="structure python:
item.renderText(header, 'text/restructured')" />
</tal:description>
<div tal:condition="feedback">
<metal:block use-macro="item/template/macros/?reportMacro" />
</div>
<div id="questionnaire"
tal:condition="not:feedback">
<metal:block use-macro="item/template/macros/?questMacro" />
</div>
</metal:block>
<metal:block define-macro="quest_standard">
<tal:inst condition="item/adapted/teamBasedEvaluation">
<metal:inst use-macro="item/knowledge_macros/select_institution" />
</tal:inst>
<div class="button"
tal:define="reports item/teamReports"
tal:condition="reports">
<b i18n:translate="label_survey_show_report">Show Report</b>: &nbsp;
<a tal:repeat="report reports"
tal:attributes="href string:${request/URL}?report=${report/name}"
i18n:translate=""
tal:content="report/label" />
<br /><br />
</div>
<h3 i18n:translate="">Questionnaire</h3>
<div class="error"
tal:condition="errors">
<div tal:repeat="error errors">
<span i18n:translate=""
tal:content="error" />
tal:content="error/text" />
</div>
</div>
<div class="message"
tal:condition="message"
i18n:translate=""
tal:content="message" />
<form method="post">
<table class="listing">
<tal:qugroup repeat="qugroup item/adapted/questionGroups">
<tr><td colspan="6">&nbsp;</td></tr>
<input type="hidden" name="person"
tal:define="personId request/person|nothing"
tal:condition="personId"
tal:attributes="value personId" />
<tal:group repeat="group item/groups">
<tr>
<td>&nbsp;</td>
<td tal:repeat="opt item/answerOptions">&nbsp;</td></tr>
<tr class="vpad">
<td tal:define="infoText python:item.getInfoText(qugroup)">
<b tal:content="qugroup/title" />
<td tal:define="infoText group/infoText">
<b i18n:translate=""
tal:content="group/title" />
<div class="infotext"
tal:condition="infoText">
<span tal:content="structure infoText" />
</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>
<td tal:repeat="opt python:[opt for opt in item.answerOptions
if opt.get('colspan') != '0']"
i18n:translate=""
i18n:attributes="title"
tal:attributes="title opt/description|string:;
class python:opt.get('cssclass') or 'center';
colspan python:opt.get('colspan')"
tal:content="opt/label|string:" />
</tr>
<tr class="vpad"
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>
<tal:question repeat="question group/questions">
<tal:question define="qutype python:
question.questionType or 'value_selection'">
<metal:question use-macro="item/template/macros/?qutype" />
</tal:question>
</tal:question>
</tal:group>
</table>
<input type="submit" name="submit" value="Evaluate Questionnaire"
i18n:attributes="value" />
<input type="submit" name="save" value="Save Data"
i18n:attributes="value" />
<input type="button" name="reset_responses" value="Reset Responses Entered"
i18n:attributes="value"
onclick="setRadioButtons('none'); return false" />
i18n:attributes="value; onclick"
onclick="if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false" />
</form>
</div>
</metal:block>
<metal:block define-macro="quest_pref_selection">
<h3 i18n:translate="">Questionnaire</h3>
<div class="error"
tal:condition="errors">
<div tal:repeat="error errors">
<span i18n:translate=""
tal:content="error/text" />
</div>
</div>
<div class="message"
tal:condition="message"
i18n:translate=""
tal:content="message" />
<form method="post">
<table class="listing">
<input type="hidden" name="person"
tal:define="personId request/person|nothing"
tal:condition="personId"
tal:attributes="value personId" />
<tal:group repeat="group item/groups">
<tr><td>&nbsp;</td><td>&nbsp;</td></tr>
<tal:question repeat="question group/questions">
<tr tal:attributes="class python:item.getCssClass(question)">
<td tal:content="question/text" />
<td tal:define="value python:item.getPrefsValue(question)">
<input type="radio"
tal:attributes="name string:group_${repeat/group/index};
value question/uid;
checked value" />
</td>
</tr>
</tal:question>
</tal:group>
</table>
<input type="submit" name="submit" value="Evaluate Questionnaire"
i18n:attributes="value" />
<input type="submit" name="save" value="Save Data"
i18n:attributes="value" />
<input type="button" name="reset_responses" value="Reset Responses Entered"
i18n:attributes="value; onclick"
onclick="if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false" />
</form>
</metal:block>
<metal:block define-macro="value_selection">
<tr tal:attributes="class python:item.getCssClass(question)">
<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:attributes="name string:question_${question/uid};
value value/value;
checked value/checked;
title value/title" />
</td>
</tr>
</metal:block>
<metal:block define-macro="text">
<tr tal:attributes="class python:item.getCssClass(question)">
<td>
<div tal:content="question/text" />
<textarea style="width: 90%; margin-left: 20px"
tal:content="python:item.getTextValue(question)"
tal:attributes="name string:question_${question/uid}">
</textarea>
</td>
<td tal:repeat="opt item/answerOptions" />
</tr>
</metal:block>
<metal:block define-macro="report_standard">
<h3 i18n:translate="">Feedback</h3>
<div tal:define="header item/adapted/feedbackHeader"
tal:condition="header"
tal:content="structure python:
item.renderText(header, 'text/restructured')" />
<table class="listing">
<tr>
<th i18n:translate="">Category</th>
<th tal:repeat="col item/feedbackColumns"
i18n:translate=""
tal:attributes="class python:
col['name'] != 'text' and 'center' or None"
tal:content="col/label" />
</tr>
<tr style="vertical-align: top"
tal:repeat="fbitem feedback">
<td style="vertical-align: top"
tal:content="fbitem/category" />
<tal:cols repeat="col item/feedbackColumns">
<td style="vertical-align: top"
tal:define="name col/name"
tal:attributes="class python:name != 'text' and 'center' or None"
tal:content="fbitem/?name|string:" />
</tal:cols>
</tr>
</table>
<p tal:define="teamData item/teamData"
tal:condition="teamData">
<b><span i18n:translate="">Team Size</span>:
<span tal:content="python:len(teamData)" /></b><br />&nbsp;
</p>
<div class="button" id="show_questionnaire">
<a i18n:translate=""
tal:attributes="href string:${request/URL}${item/urlParamString}">
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')" />
</metal:block>
<metal:block define-macro="report_questions">
<h3 i18n:translate="label_survey_report_questions"></h3>
<div>
<table class="listing">
<tal:group repeat="group item/groups">
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<!--<td>&nbsp;</td>-->
</tr>
<tr class="vpad">
<td><b tal:content="group/title" /></td>
<td i18n:translate="">Average</td>
<!--<td i18n:translate="">Deviation</td>-->
</tr>
<tr tal:repeat="question group/questions">
<tal:question
define="qutype python:
question.questionType or 'value_selection';
data python:
item.getTeamResultsForQuestion(question, item.adapted)">
<td>
<div tal:content="question/text" />
<div style="width: 90%; margin-left: 20px"
tal:condition="python:qutype == 'text'"
tal:content="structure data/texts" />
</td>
<td class="center">
<span tal:condition="python:qutype == 'value_selection'"
tal:content="data/average" /></td>
<!--<td class="center">
<span tal:condition="python:qutype == 'value_selection'"
tal:content="data/stddev" /></td>-->
</tal:question>
</tr>
</tal:group>
</table>
<p tal:define="teamData item/teamData"
tal:condition="teamData">
<b><span i18n:translate="">Team Size</span>:
<span tal:content="python:len(teamData)" /></b><br />&nbsp;
</p>
<div class="button" id="show_questionnaire">
<a i18n:translate=""
tal:attributes="href string:${request/URL}${item/urlParamString}">
Back to Questionnaire</a></div>
</div>
</metal:block>

View file

@ -7,6 +7,7 @@ from zope import component
from zope.interface.verify import verifyClass
from zope.testing.doctestunit import DocFileSuite
from loops.expert.report import IReport, Report
from loops.knowledge.qualification.base import Competence
from loops.knowledge.survey.base import Questionnaire, Question, FeedbackItem
from loops.knowledge.survey.interfaces import IQuestionnaire, IQuestion, \
@ -19,6 +20,7 @@ importPath = os.path.join(os.path.dirname(__file__), 'data')
def importData(loopsRoot):
component.provideAdapter(Report, provides=IReport)
baseImportData(loopsRoot, importPath, 'knowledge_de.dmp')
def importSurvey(loopsRoot):

Binary file not shown.

View file

@ -1,9 +1,9 @@
msgid ""
msgstr ""
"Project-Id-Version: 0.13.0\n"
"Project-Id-Version: 0.13.1\n"
"POT-Creation-Date: 2007-05-22 12:00 CET\n"
"PO-Revision-Date: 2013-07-15 12:00 CET\n"
"PO-Revision-Date: 2015-06-06 12:00 CET\n"
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
"Language-Team: loops developers <helmutm@cy55.de>\n"
"MIME-Version: 1.0\n"
@ -89,6 +89,14 @@ msgstr "Thema ändern"
msgid "Please correct the indicated errors."
msgstr "Bitte berichtigen Sie die angezeigten Fehler."
msgid "tooltip_sort_column"
msgstr "Nach dieser Spalte sortieren"
# expert (reporting)
msgid "Download Data"
msgstr "Download als Excel-Datei"
# blog
msgid "Edit Blog Post..."
@ -178,6 +186,30 @@ msgstr "Glossareintrag anlegen."
msgid "Answer Range"
msgstr "Abstufung Bewertungen"
msgid "Answer Options"
msgstr "Antwortmöglichkeiten"
msgid "Values to select from with corresponding column labels and descriptions. There should be at least answer range items with numeric values."
msgstr "Auszuwählende Werte mit zugehörigen Spaltenüberschriften und Beschreibungen. Es sollte mindestens so viele Einträge mit numerischen Werten geben wie durch das Feld 'Abstufung Bewertungen' vorgegeben."
msgid "No Grouping of Questions"
msgstr "Keine Gruppierung der Fragen"
msgid "The questions should be presented in a linear manner, not grouped by categories or question groups."
msgstr "Die Fragen sollen in linearer Reihenfolge ausgegeben und nicht nach Fragengruppen bzw. Kategorien gruppiert werden."
msgid "Questionnaire Header"
msgstr "Infotext zum Fragebogen"
msgid "Text that will appear at the top of the questionnaire."
msgstr "Text, der vor dem Fragebogen erscheinen soll"
msgid "Feedback Header"
msgstr "Infotext zur Auswertung"
msgid "Text that will appear at the top of the feedback page."
msgstr "Text, der oben auf der Auswertungsseite erscheinen soll."
msgid "Feedback Footer"
msgstr "Auswertungs-Hinweis"
@ -193,6 +225,15 @@ 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 "Question Type"
msgstr "Fragentyp"
msgid "Select the type of the question."
msgstr "Bitte den Typ der Frage auswählen."
msgid "Value Selection"
msgstr "Auswahl Bewertung"
msgid "Required"
msgstr "Pflichtfrage"
@ -205,6 +246,9 @@ msgstr "Negative Polarität"
msgid "Value inversion: High selection means low value."
msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert."
msgid "Question"
msgstr "Frage"
msgid "Questionnaire"
msgstr "Fragebogen"
@ -241,15 +285,30 @@ msgstr "Trifft eher zu"
msgid "survey_value_3"
msgstr "Trifft für unser Unternehmen voll und ganz zu"
msgid "label_survey_show_report"
msgstr "Auswertung anzeigen"
msgid "label_survey_report_standard"
msgstr "Standard-Auswertung"
msgid "label_survey_report_questions"
msgstr "Einzelfragen-Auswertung"
msgid "Evaluate Questionnaire"
msgstr "Fragebogen auswerten"
msgid "Save Data"
msgstr "Daten speichern"
msgid "Reset Responses Entered"
msgstr "Eingaben zurücksetzen"
msgid "Back to Questionnaire"
msgstr "Zurück zum Fragebogen"
msgid "Your data have been saved."
msgstr "Ihre Daten wurden gespeichert."
msgid "Please answer at least $minAnswers questions."
msgstr "Bitte beantworten Sie mindestens $minAnswers Fragen."
@ -262,10 +321,37 @@ 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 "Please answer the highlighted questions."
msgstr "Bitte beantworten Sie die markierten Fragen."
msgid "Obligatory question, must be answered"
msgstr "Pflichtfrage, muss beantwortet werden"
# competence (qualification)
msgid "Score"
msgstr "Ergebnis %"
msgid "Team Score"
msgstr "Durchschnitt Team %"
msgid "Rank"
msgstr "Rang"
msgid "Team Rank"
msgstr "Rang Team"
msgid "Average"
msgstr "Durchschnitt"
msgid "Deviation"
msgstr "Abweichung"
msgid "Team Size"
msgstr "Anzahl der vom Team ausgefüllten Fragebögen"
msgid "if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false"
msgstr "if (confirm('Wollen Sie wirklich alle eingegebenen Daten zurücksetzen?')) setRadioButtons('none'); return false"
# compentence and qualification management
msgid "Validity Period (Months)"
msgstr "Gültigkeitszeitraum (Monate)"
@ -471,6 +557,8 @@ msgstr "Wer?"
msgid "When?"
msgstr "Wann?"
# personal stuff
msgid "Favorites"
msgstr "Lesezeichen"
@ -495,6 +583,8 @@ msgstr "Anmelden"
msgid "Presence"
msgstr "Anwesenheit"
# general
msgid "Actions"
msgstr "Aktionen"
@ -819,6 +909,12 @@ msgstr "Beginn"
msgid "End date"
msgstr "Ende"
msgid "Start Day"
msgstr "Beginn"
msgid "End Day"
msgstr "Ende"
msgid "Knowledge"
msgstr "Kompetenzen"
@ -891,6 +987,9 @@ msgstr "Kommentare"
msgid "Add Comment"
msgstr "Kommentar hinzufügen"
msgid "Email Address"
msgstr "E-Mail-Adresse"
msgid "Subject"
msgstr "Thema"
@ -965,6 +1064,21 @@ msgstr "Kalender"
msgid "Work Items"
msgstr "Aktivitäten"
msgid "Work Item Type"
msgstr "Art der Aktivität"
msgid "Unit of Work"
msgstr "Standard-Aktivität"
msgid "Scheduled Event"
msgstr "Termin"
msgid "Deadline"
msgstr "Deadline"
msgid "Check-up"
msgstr "Überprüfung"
msgid "Work Items for $title"
msgstr "Aktivitäten für $title"
@ -995,6 +1109,12 @@ msgstr "Dauer/Aufwand"
msgid "Duration / Effort (hh:mm)"
msgstr "Dauer / Aufwand (hh:mm)"
msgid "Priority"
msgstr "Priorität"
msgid "Activity"
msgstr "Leistungsart"
msgid "Action"
msgstr "Aktion"
@ -1069,6 +1189,9 @@ msgstr "Bemerkung"
msgid "desc_transition_comments"
msgstr "Notizen zum Statusübergang."
msgid "contact_states"
msgstr "Kontaktstatus"
# state names
msgid "accepted"
@ -1137,6 +1260,12 @@ msgstr "unklassifiziert"
msgid "verified"
msgstr "verifiziert"
msgid "prospective"
msgstr "künftig"
msgid "inactive"
msgstr "inaktiv"
# transitions
msgid "accept"
@ -1211,6 +1340,15 @@ msgstr "verifizieren"
msgid "work"
msgstr "bearbeiten"
msgid "activate"
msgstr "aktivieren"
msgid "inactivate"
msgstr "inaktiv setzen"
msgid "reset"
msgstr "zurücksetzen"
# calendar
msgid "Monday"

View file

@ -228,6 +228,23 @@ We need a principal for testing the login stuff:
>>> pwcView.update()
False
Reset Password
--------------
Invalidates the user account by generating a new password. A mail ist sent to
the email address of the person with a link for re-activating the account
and enter a new password.
>>> data = {'loginName': u'dummy',
... 'action': 'update'}
>>> request = TestRequest(form=data)
>>> from loops.organize.browser.member import PasswordReset
>>> pwrView = PasswordReset(menu, request)
>>> pwrView.update()
True
Pure Person-based Authentication
================================
@ -410,7 +427,7 @@ Send Email to Members
>>> form.subject
u"loops Notification from '$site'"
>>> form.mailBody
u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.113\n\n'
u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.118\n\n'
Show Presence of Other Users

View file

@ -45,6 +45,12 @@
class="loops.organize.browser.member.PasswordChange"
permission="zope.View" />
<browser:page
for="loops.interfaces.INode"
name="reset_password.html"
class="loops.organize.browser.member.PasswordReset"
permission="zope.View" />
<zope:adapter
name="task.html"
for="loops.interfaces.IConcept
@ -146,4 +152,12 @@
permission="zope.ManageServices"
menu="zmi_views" title="Prefix" />
<!-- utilities -->
<browser:page
for="loops.interfaces.ILoops"
name="fix_person_roles"
class="loops.organize.browser.member.FixPersonRoles"
permission="zope.ManageServices" />
</configure>

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -25,6 +25,7 @@ from datetime import datetime
from email.MIMEText import MIMEText
from zope import interface, component
from zope.app.authentication.principalfolder import InternalPrincipal
from zope.app.authentication.principalfolder import PrincipalInfo
from zope.app.form.browser.textwidgets import PasswordWidget as BasePasswordWidget
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.app.principalannotation import annotations
@ -47,9 +48,11 @@ from loops.browser.node import NodeView
from loops.common import adapted
from loops.concept import Concept
from loops.organize.interfaces import ANNOTATION_KEY, IMemberRegistrationManager
from loops.organize.interfaces import IMemberRegistration, IPasswordChange
from loops.organize.interfaces import IMemberRegistration, IPasswordEntry
from loops.organize.interfaces import IPasswordChange, IPasswordReset
from loops.organize.party import getPersonForUser, Person
from loops.organize.util import getInternalPrincipal, getPrincipalForUserId
from loops.organize.util import getPrincipalFolder
import loops.browser.util
from loops.util import _
@ -86,7 +89,8 @@ class BaseMemberRegistration(NodeView):
template = form_macros
formErrors = dict(
confirm_nomatch=FormError(_(u'Password and password confirmation do not match.')),
confirm_nomatch=FormError(_(u'Password and password confirmation '
u'do not match.')),
duplicate_loginname=FormError(_('Login name already taken.')),
)
@ -187,7 +191,9 @@ class MemberRegistration(BaseMemberRegistration, CreateForm):
result = regMan.register(login, pw,
form.get('lastName'), form.get('firstName'),
email=form.get('email'),
phoneNumbers=[x for x in phoneNumbers if x])
phoneNumbers=[x for x in phoneNumbers if x],
salutation=form.get('salutation'),
academicTitle=form.get('academicTitle'))
if isinstance(result, dict):
fi = formState.fieldInstances[result['fieldName']]
fi.setError(result['error'], self.formErrors)
@ -210,6 +216,8 @@ class SecureMemberRegistration(BaseMemberRegistration, CreateForm):
@Lazy
def schema(self):
schema = super(SecureMemberRegistration, self).schema
schema.fields.remove('salutation')
schema.fields.remove('academicTitle')
schema.fields.remove('birthDate')
schema.fields.remove('password')
schema.fields.remove('passwordConfirm')
@ -366,7 +374,8 @@ class PasswordChange(NodeView, Form):
message = _(u'Your password has been changed.')
formErrors = dict(
confirm_nomatch=FormError(_(u'Password and password confirmation do not match.')),
confirm_nomatch=FormError(_(u'Password and password confirmation '
u'do not match.')),
wrong_oldpw=FormError(_(u'Your old password was not entered correctly.')),
)
@ -422,3 +431,84 @@ class PasswordChange(NodeView, Form):
formState.severity = max(formState.severity, fi.severity)
return formState
class PasswordReset(PasswordChange):
interface = IPasswordReset
message = _(u'Password Reset: You will receive an email with '
u'a link to change your password.')
formErrors = dict(
confirm_notfound=FormError(_(u'Invalid user account.')),
)
label = label_submit = _(u'Reset Password')
@Lazy
def data(self):
data = dict(loginName=u'')
return data
def update(self):
form = self.request.form
if not form.get('action'):
return True
formState = self.formState = self.validate(form)
if formState.severity > 0:
return True
loginName = form.get('loginName')
person = principal = None
regMan = IMemberRegistrationManager(self.context.getLoopsRoot())
authenticator = regMan.getPrincipalFolderFromOption()
if authenticator is not None:
userId = authenticator.prefix + loginName
principal = getPrincipalForUserId(userId)
if principal is not None:
person = getPersonForUser(self.context, principal=principal)
if person is None:
fi = formState.fieldInstances['loginName']
fi.setError('confirm_notfound', self.formErrors)
formState.severity = max(formState.severity, fi.severity)
return True
person = adapted(person)
pa = self.getPrincipalAnnotation(principal)
pa['id'] = generateName()
pa['timestamp'] = datetime.utcnow()
self.notifyEmail(loginName, person.email, pa['id'])
url = '%s?messsage=%s' % (self.url, self.message)
self.request.response.redirect(url)
return False
def getPrincipalAnnotation(self, principal):
return annotations(principal).get(ANNOTATION_KEY, None)
def notifyEmail(self, userid, recipient, id):
baseUrl = absoluteURL(self.context.getMenu(), self.request)
url = u'%s/selfservice_confirmation.html?login=%s&id=%s' % (
baseUrl, userid, id,)
recipients = [recipient]
subject = _(u'password_reset_mail_subject')
message = _(u'password_reset_mail_text') + u':\n\n'
message = (message + url).encode('UTF-8')
senderInfo = self.globalOptions('email.sender')
sender = senderInfo and senderInfo[0] or 'info@loops.cy55.de'
sender = sender.encode('UTF-8')
msg = MIMEText(message, 'plain', 'utf-8')
msg['Subject'] = subject.encode('UTF-8')
msg['From'] = sender
msg['To'] = ', '.join(recipients)
mailhost = component.getUtility(IMailDelivery, 'Mail')
mailhost.send(sender, recipients, msg.as_string())
class FixPersonRoles(object):
def __call__(self):
concepts = self.context['concepts']
for p in concepts['person'].getChildren([concepts['hasType']]):
person = adapted(p)
userId = person.userId
print '***', userId
person.userId = userId
return 'blubb'

View file

@ -45,6 +45,9 @@ to assign comments to this document.
>>> home = views['home']
>>> home.target = resources['d001.txt']
>>> from loops.organize.comment.base import commentStates
>>> component.provideUtility(commentStates(), name='organize.commentStates')
Creating comments
-----------------
@ -75,6 +78,12 @@ Viewing comments
('My comment', u'... ...', u'john')
Reporting
=========
>>> from loops.organize.comment.report import CommentsOverview
Fin de partie
=============

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -18,24 +18,55 @@
"""
Base classes for comments/discussions.
$Id$
"""
from zope.component import adapts
from zope.interface import implements
from zope.interface import implementer, implements
from zope.traversing.api import getParent
from cybertools.stateful.definition import StatesDefinition
from cybertools.stateful.definition import State, Transition
from cybertools.stateful.interfaces import IStatesDefinition
from cybertools.tracking.btree import Track
from cybertools.tracking.interfaces import ITrackingStorage
from cybertools.tracking.comment.interfaces import IComment
from loops.organize.comment.interfaces import IComment
from loops.organize.stateful.base import Stateful
from loops import util
class Comment(Track):
@implementer(IStatesDefinition)
def commentStates():
return StatesDefinition('commentStates',
State('new', 'new', ('accept', 'reject'), color='red'),
State('public', 'public', ('retract', 'reject'), color='green'),
State('rejected', 'rejected', ('accept',), color='grey'),
Transition('accept', 'accept', 'public'),
Transition('reject', 'reject', 'rejected'),
Transition('retract', 'retract', 'new'),
initialState='new')
class Comment(Stateful, Track):
implements(IComment)
metadata_attributes = Track.metadata_attributes + ('state',)
index_attributes = metadata_attributes
typeName = 'Comment'
typeInterface = IComment
statesDefinition = 'organize.commentStates'
contentType = 'text/restructured'
def __init__(self, taskId, runId, userName, data):
super(Comment, self).__init__(taskId, runId, userName, data)
self.state = self.getState() # make initial state persistent
@property
def title(self):
return self.data['subject']
def doTransition(self, action):
super(Comment, self).doTransition(action)
getParent(self).indexTrack(None, self, 'state')

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2012 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 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
@ -23,15 +23,17 @@ Definition of view classes and other browser related stuff for comments.
from zope import interface, component
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from zope.security import checkPermission
from cybertools.browser.action import actions
from cybertools.tracking.btree import TrackingStorage
from loops.browser.action import DialogAction
from loops.browser.action import Action, DialogAction
from loops.browser.common import BaseView
from loops.browser.form import ObjectForm, EditObject
from loops.browser.node import NodeView
from loops.organize.comment.base import Comment
from loops.organize.party import getPersonForUser
from loops.organize.stateful.browser import StateAction
from loops.organize.tracking.report import TrackDetails
from loops.security.common import canAccessObject
from loops.setup import addObject
@ -50,10 +52,17 @@ class CommentsView(NodeView):
@Lazy
def allowed(self):
if self.isAnonymous:
if self.virtualTargetObject is None:
return False
return (self.virtualTargetObject is not None and
self.globalOptions('organize.allowComments'))
opts = (self.globalOptions('organize.allowComments') or
self.typeOptions('organize.allowComments'))
if not opts:
return False
if opts is True:
opts = []
if self.isAnonymous and not 'all' in opts:
return False
return True
@Lazy
def addUrl(self):
@ -76,9 +85,47 @@ class CommentsView(NodeView):
result.append(CommentDetails(self, tr))
return result
def getActionsFor(self, comment):
if not self.globalOptions('organize.showCommentState'):
return []
if not checkPermission('loops.ViewRestricted', self.context):
return []
trackUid = util.getUidForObject(comment.track)
url = '%s/.%s/change_state.html' % (
self.page.virtualTargetUrl, trackUid)
onClick = ("objectDialog('change_state', "
"'%s?dialog=change_state"
"&target_uid=%s'); return false;" % (url, trackUid))
stateAct = StateAction(self,
definition='organize.commentStates',
stateful=comment.track,
url=url,
onClick=onClick)
actions = [stateAct]
if not checkPermission('loops.EditRestricted', self.context):
return actions
baseUrl = self.page.virtualTargetUrl
url = '%s/delete_object?uid=%s' % (baseUrl, trackUid)
onClick = _("return confirm('Do you really want to delete this object?')")
delAct = Action(self,
url=url,
description=_('Delete Comment'),
icon='cybertools.icons/delete.png',
cssClass='icon-action',
onClick=onClick)
actions.append(delAct)
return actions
class CommentDetails(TrackDetails):
@Lazy
def poster(self):
name = self.track.data.get('name')
if name:
return name
return self.user['title']
@Lazy
def subject(self):
return self.track.data['subject']
@ -108,6 +155,8 @@ class CreateComment(EditObject):
@Lazy
def personId(self):
if self.view.isAnonymous:
return self.request.form.get('email')
p = getPersonForUser(self.context, self.request)
if p is not None:
return util.getUidForObject(p)
@ -129,8 +178,11 @@ class CreateComment(EditObject):
if ts is None:
ts = addObject(rm, TrackingStorage, 'comments', trackFactory=Comment)
uid = util.getUidForObject(self.object)
ts.saveUserTrack(uid, 0, self.personId, dict(
subject=subject, text=text))
data = dict(subject=subject, text=text)
for k in ('name', 'email'):
if k in form:
data[k] = form[k]
ts.saveUserTrack(uid, 0, self.personId, data)
url = self.view.virtualTargetUrl + '?version=this'
self.request.response.redirect(url)
return False

View file

@ -14,10 +14,17 @@
<tal:comment tal:repeat="comment items">
<br />
<div class="comment">
<div class="object-actions"
tal:define="actions python:comments.getActionsFor(comment)"
tal:condition="actions">
<tal:actions repeat="action actions">
<metal:action use-macro="action/macro" />
</tal:actions>
</div>
<h3>
<span tal:content="comment/subject">Subject</span></h3>
<div class="info">
<span tal:replace="comment/user/title">John</span>,
<span tal:replace="comment/poster">John</span>,
<span tal:replace="comment/timeStamp">2007-03-30</span>
</div>
<p class="content"
@ -44,6 +51,18 @@
<input type="hidden" name="contentType" value="text/restructured" />
<div class="heading" i18n:translate="">Add Comment</div>
<div>
<tal:anonymous condition="view/isAnonymous">
<label i18n:translate=""
for="comment_name">Name</label>
<div><input type="text" name="name" id="comment_name"
dojoType="dijit.form.ValidationTextBox" required="true"
style="width: 60em" /></div>
<label i18n:translate=""
for="comment_email">Email Address</label>
<div><input type="text" name="email" id="comment_email"
dojoType="dijit.form.ValidationTextBox" required="true"
style="width: 60em" /></div>
</tal:anonymous>
<label i18n:translate=""
for="comment_subject">Subject</label>
<div><input type="text" name="subject" id="comment_subject"

View file

@ -12,6 +12,10 @@
set_schema="cybertools.tracking.comment.interfaces.IComment" />
</zope:class>
<zope:utility
factory="loops.organize.comment.base.commentStates"
name="organize.commentStates" />
<!-- views -->
<browser:page
@ -33,4 +37,26 @@
factory="loops.organize.comment.browser.CreateComment"
permission="zope.View" />
<!-- reporting -->
<zope:adapter
name="list_comments.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.organize.comment.report.CommentsOverview"
permission="zope.View" />
<zope:adapter
name="comments_overview"
factory="loops.organize.comment.report.CommentsReportInstance"
provides="loops.expert.report.IReportInstance"
trusted="True" />
<zope:class class="loops.organize.comment.report.CommentsReportInstance">
<require permission="zope.View"
interface="loops.expert.report.IReportInstance" />
<require permission="zope.ManageContent"
set_schema="loops.expert.report.IReportInstance" />
</zope:class>
</configure>

View file

@ -0,0 +1,26 @@
#
# Copyright (c) 2014 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
#
"""
Interface definitions for comments - discussions - forums.
"""
from zope.interface import Interface, Attribute
from zope import schema
from cybertools.tracking.comment.interfaces import IComment

View file

@ -0,0 +1,6 @@
type(u'report', u'Report', options=u'',
typeInterface='loops.expert.report.IReport', viewName=u'')
concept(u'comments_overview', u'\xdcbersicht Kommentare', u'report',
reportType=u'comments_overview')
concept(u'comments', u'Kommentare', u'query', options=u'',
viewName=u'list_comments.html')

View file

@ -0,0 +1,75 @@
#
# Copyright (c) 2014 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
#
"""
Report views and definitions for comments listings and similar stuff.
"""
from cybertools.util.jeep import Jeep
from loops.expert.browser.report import ReportConceptView
from loops.expert.field import Field, StateField, TargetField
from loops.expert.field import TrackDateField
from loops.expert.report import ReportInstance, TrackRow
class CommentsOverview(ReportConceptView):
reportName = 'comments_overview'
timeStamp = TrackDateField('timeStamp', u'Timestamp',
description=u'The date and time the comment was posted.',
part='dateTime', descending=True,
executionSteps=['sort', 'output'])
target = TargetField('taskId', u'Target',
description=u'The resource or concept the comment was posted at.',
executionSteps=['output'])
name = Field('name', u'Name',
description=u'The name addres of the poster.',
executionSteps=['output'])
email = Field('email', u'E-Mail Address',
description=u'The email addres of the poster.',
executionSteps=['output'])
subject = Field('subject', u'Subject',
description=u'The subject of the comment.',
executionSteps=['output'])
state = StateField('state', u'State',
description=u'The state of the comment.',
cssClass='center',
statesDefinition='organize.commentStates',
executionSteps=['query', 'sort', 'output'])
class CommentsRow(TrackRow):
pass
class CommentsReportInstance(ReportInstance):
type = "comments_overview"
label = u'Comments Overview'
rowFactory = CommentsRow
fields = Jeep((timeStamp, target, name, email, subject, state))
defaultOutputFields = fields
defaultSortCriteria = (state, timeStamp)
def selectObjects(self, parts):
return self.recordManager['comments'].values()

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 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
@ -82,7 +82,7 @@ class LoginName(schema.TextLine):
super(LoginName, self)._validate(userId)
if userId in getPrincipalFolder(self.context):
raiseValidationError(
_(u'There is alread a user with ID $userId.',
_(u'There is already a user with ID $userId.',
mapping=dict(userId=userId)))
@ -124,6 +124,14 @@ class IPasswordChange(IPasswordEntry):
required=True,)
class IPasswordReset(Interface):
loginName = schema.TextLine(title=_(u'User ID'),
description=_(u'Your login name.'),
required=True,)
loginName.nostore = True
class IMemberRegistration(IBasePerson, IPasswordEntry):
""" Schema for registering a new member (user + person).
"""

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 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
@ -57,6 +57,8 @@ PredicateInterfaceSourceList.predicateInterfaces += (IHasRole,)
def getPersonForUser(context, request=None, principal=None):
if context is None:
return None
if principal is None:
if request is None:
principal = getCurrentPrincipal()
@ -95,9 +97,11 @@ class Person(AdapterBase, BasePerson):
return
person = getPersonForUser(self.context, principal=principal)
if person is not None and person != self.context:
raise ValueError(
'Error when creating user %s: There is already a person (%s) assigned to user %s.'
% (getName(self.context), getName(person), userId))
name = getName(person)
if name:
raise ValueError(
'There is already a person (%s) assigned to user %s.'
% (getName(person), userId))
pa = annotations(principal)
loopsId = util.getUidForObject(self.context.getLoopsRoot())
ann = pa.get(ANNOTATION_KEY)

View file

@ -75,7 +75,7 @@ So we are now ready to query the favorites.
>>> favs = list(favorites.query(userName=johnCId))
>>> favs
[<Favorite ['27', 1, '33', '...']: {}>]
[<Favorite ['27', 1, '33', '...']: {'type': 'favorite'}>]
>>> list(favAdapted.list(johnC))
['27']

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2010 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
"""
Base classes for a notification framework.
$Id$
"""
from zope.component import adapts
@ -39,17 +37,20 @@ class Favorites(object):
def __init__(self, context):
self.context = context
def list(self, person, sortKey=None):
for item in self.listTracks(person, sortKey):
def list(self, person, sortKey=None, type='favorite'):
for item in self.listTracks(person, sortKey, type):
yield item.taskId
def listTracks(self, person, sortKey=None):
def listTracks(self, person, sortKey=None, type='favorite'):
if person is None:
return
personUid = util.getUidForObject(person)
if sortKey is None:
sortKey = lambda x: -x.timeStamp
for item in sorted(self.context.query(userName=personUid), key=sortKey):
if type is not None:
if item.type != type:
continue
yield item
def add(self, obj, person, data=None):
@ -57,21 +58,23 @@ class Favorites(object):
return False
uid = util.getUidForObject(obj)
personUid = util.getUidForObject(person)
if self.context.query(userName=personUid, taskId=uid):
return False
if data is None:
data = {}
data = {'type': 'favorite'}
for track in self.context.query(userName=personUid, taskId=uid):
if track.type == data['type']: # already present
return False
return self.context.saveUserTrack(uid, 0, personUid, data)
def remove(self, obj, person):
def remove(self, obj, person, type='favorite'):
if None in (obj, person):
return False
uid = util.getUidForObject(obj)
personUid = util.getUidForObject(person)
changed = False
for t in self.context.query(userName=personUid, taskId=uid):
changed = True
self.context.removeTrack(t)
for track in self.context.query(userName=personUid, taskId=uid):
if track.type == type:
changed = True
self.context.removeTrack(track)
return changed
@ -81,3 +84,46 @@ class Favorite(Track):
typeName = 'Favorite'
@property
def type(self):
return self.data.get('type') or 'favorite'
def updateSortInfo(person, task, data):
if person is not None:
favorites = task.getLoopsRoot().getRecordManager().get('favorites')
if favorites is None:
return data
personUid = util.getUidForObject(person)
taskUid = util.getUidForObject(task)
for fav in favorites.query(userName=personUid, taskId=taskUid):
if fav.data.get('type') == 'sort':
fdata = fav.data['sortInfo']
if not data:
data = fdata
else:
if data != fdata:
newData = fav.data
newData['sortInfo'] = data
fav.data = newData
break
else:
if data:
Favorites(favorites).add(task, person,
dict(type='sort', sortInfo=data))
return data
def setInstitution(person, inst):
if person is not None:
favorites = inst.getLoopsRoot().getRecordManager().get('favorites')
if favorites is None:
return
personUid = util.getUidForObject(person)
taskUid = util.getUidForObject(inst)
for fav in favorites.query(userName=personUid):
if fav.type == 'institution':
fav.taskId = taskUid
favorites.indexTrack(None, fav, 'taskId')
else:
Favorites(favorites).add(inst, person, dict(type='institution'))

View file

@ -187,6 +187,12 @@ Task States
>>> from loops.organize.stateful.task import taskStates, publishableTask
Contact States
===========
>>> from loops.organize.stateful.contact import contactStates
Fin de partie
=============

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 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
@ -37,6 +37,7 @@ from loops.common import adapted
from loops.expert.query import And, Or, State, Type, getObjects
from loops.expert.browser.search import search_template
from loops.security.common import checkPermission
from loops import util
from loops.util import _
@ -45,7 +46,8 @@ template = ViewPageTemplateFile('view_macros.pt')
statefulActions = ('classification_quality',
'simple_publishing',
'task_states',
'publishable_task',)
'publishable_task',
'contact_states',)
def registerStatesPortlet(controller, view, statesDefs,
@ -65,6 +67,7 @@ class StateAction(Action):
url = None
definition = None
msgFactory = _
cssClass = 'icon-action'
@Lazy
def stateful(self):
@ -106,8 +109,10 @@ class ChangeStateBase(object):
@Lazy
def stateful(self):
return component.getAdapter(self.view.virtualTargetObject, IStateful,
name=self.definition)
target = self.view.virtualTargetObject
if IStateful.providedBy(target):
return target
return component.getAdapter(target, IStateful, name=self.definition)
@Lazy
def definition(self):
@ -119,7 +124,8 @@ class ChangeStateBase(object):
@Lazy
def transition(self):
return self.stateful.getStatesDefinition().transitions[self.action]
if self.action:
return self.stateful.getStatesDefinition().transitions[self.action]
@Lazy
def stateObject(self):
@ -152,9 +158,17 @@ class ChangeStateForm(ChangeStateBase, ObjectForm):
class ChangeState(ChangeStateBase, EditObject):
@Lazy
def stateful(self):
target = self.target
if IStateful.providedBy(target):
return target
return component.getAdapter(target, IStateful, name=self.definition)
def update(self):
self.stateful.doTransition(self.action)
formData = self.request.form
if 'target_uid' in formData:
self.target = util.getObjectForUid(formData['target_uid'])
# store data in target object (unless field.nostore)
self.object = self.target
formState = self.instance.applyTemplate(data=formData)
@ -169,8 +183,9 @@ class ChangeState(ChangeStateBase, EditObject):
fi = formState.fieldInstances[name]
rawValue = fi.getRawValue(formData, name, u'')
trackData[name] = fi.unmarshall(rawValue)
notify(ObjectModifiedEvent(self.view.virtualTargetObject, trackData))
self.request.response.redirect(self.request.getURL())
self.stateful.doTransition(self.action)
notify(ObjectModifiedEvent(self.target, trackData))
#self.request.response.redirect(self.request.getURL())
return True

View file

@ -77,6 +77,20 @@
set_schema="cybertools.stateful.interfaces.IStateful" />
</zope:class>
<zope:utility
factory="loops.organize.stateful.contact.contactStates"
name="contact_states" />
<zope:adapter
factory="loops.organize.stateful.contact.StatefulContact"
name="contact_states" />
<zope:class class="loops.organize.stateful.contact.StatefulContact">
<require permission="zope.View"
interface="cybertools.stateful.interfaces.IStateful" />
<require permission="zope.ManageContent"
set_schema="cybertools.stateful.interfaces.IStateful" />
</zope:class>
<!-- views and form controllers -->
<browser:page

View file

@ -0,0 +1,56 @@
#
# Copyright (c) 2015 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
#
"""
States definition for contacts (persons, customers, ...).
"""
from zope import component
from zope.component import adapter
from zope.interface import implementer
from zope.traversing.api import getName
from cybertools.stateful.definition import StatesDefinition
from cybertools.stateful.definition import State, Transition
from cybertools.stateful.interfaces import IStatesDefinition, IStateful
from loops.common import adapted
from loops.organize.stateful.base import commentsField
from loops.organize.stateful.base import StatefulLoopsObject
from loops.security.interfaces import ISecuritySetter
from loops.util import _
@implementer(IStatesDefinition)
def contactStates():
return StatesDefinition('contact_states',
State('prospective', 'prospective', ('activate', 'inactivate',),
color='blue'),
State('active', 'active', ('reset', 'inactivate',),
color='green'),
State('inactive', 'inactive', ('activate', 'reset'),
color='x'),
Transition('activate', 'activate', 'active'),
Transition('reset', 'reset', 'prospective'),
Transition('inactivate', 'inactivate', 'inactive'),
initialState='active')
class StatefulContact(StatefulLoopsObject):
statesDefinition = 'contact_states'

View file

@ -54,7 +54,7 @@ def taskStates():
return StatesDefinition('task_states',
State('draft', 'draft', ('release', 'cancel',),
color='blue'),
State('active', 'active', ('finish', 'cancel',),
State('active', 'active', ('finish', 'reopen', 'cancel',),
color='yellow'),
State('finished', 'finished', ('reopen', 'archive',),
color='green'),

View file

@ -115,16 +115,33 @@
tal:define="stateObject view/stateful/getStateObject"
tal:content="stateObject/title" /> -
<span i18n:translate="">Transition</span>:
<span i18n:translate=""
tal:content="view/transition/title" />
<tal:transition condition="view/transition">
<span i18n:translate=""
tal:content="view/transition/title" />
<input type="hidden" name="action"
tal:attributes="value request/form/action|nothing">
</tal:transition>
<tal:transition condition="not:view/transition">
<tal:trans repeat="trans view/stateful/getAvailableTransitions">
<label i18n:translate=""
tal:attributes="for string:transition.${trans/name}"
tal:content="trans/title" />
<input type="radio" name="action"
tal:attributes="value trans/name;
id string:transition.${trans/name}" />
</tal:trans>
</tal:transition>
</div>
<input type="hidden" name="form.action" value="change_state">
<input type="hidden" name="stdef"
tal:attributes="value request/form/stdef|nothing">
<input type="hidden" name="action"
tal:attributes="value request/form/action|nothing">
<input type="hidden" name="target_uid"
tal:define="uid request/target_uid|nothing"
tal:condition="uid"
tal:attributes="value uid" />
</div>
<div dojoType="dijit.layout.ContentPane" region="center">
<div dojoType="dijit.layout.ContentPane" region="center"
tal:condition="view/transition">
<table cellpadding="3" class="form">
<tbody><tr><td colspan="5" style="padding-right: 15px">
<div id="form.fields">

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 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
@ -21,9 +21,11 @@ View classes for tracks.
"""
from zope import component
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.app.security.interfaces import IAuthentication, PrincipalLookupError
from zope.cachedescriptors.property import Lazy
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL
from zope.traversing.api import getName
@ -34,6 +36,8 @@ from loops.browser.form import ObjectForm, EditObject
from loops.organize.party import getPersonForUser
from loops import util
track_edit_template = ViewPageTemplateFile('edit_track.pt')
class BaseTrackView(TrackView):
@ -62,7 +66,15 @@ class BaseTrackView(TrackView):
obj = util.getObjectForUid(uid)
if obj is not None:
return obj
return uid
result = []
for id in uid.split('.'):
if id.isdigit():
obj = util.getObjectForUid(id)
if obj is not None:
result.append(obj.title)
continue
result.append(id)
return ' / '.join(result)
@Lazy
def authentication(self):
@ -102,6 +114,29 @@ class BaseTrackView(TrackView):
return self.request.principal.id
class EditForm(BaseTrackView):
template = track_edit_template
def update(self):
form = self.request.form
if not form.get('form_submitted'):
return True
data = {}
for row in form.get('data') or []:
key = row['key']
if not key:
continue
value = row['value']
# TODO: unmarshall value if necessary
data[key] = value
context = removeSecurityProxy(self.context)
context.data = data
return True
# specialized views
class ChangeView(BaseTrackView):
pass

View file

@ -61,7 +61,7 @@
for="cybertools.tracking.interfaces.ITrackingStorage"
name="index.html"
class="cybertools.tracking.browser.TrackingStorageView"
permission="zope.View" />
permission="loops.ManageSite" />
<browser:menuItem
menu="zmi_views"
@ -74,19 +74,26 @@
for="cybertools.tracking.interfaces.ITrack"
name="index.html"
class="loops.organize.tracking.browser.BaseTrackView"
permission="zope.View" />
permission="loops.ManageSite" />
<browser:page
for="loops.organize.tracking.change.IChangeRecord"
name="index.html"
class="loops.organize.tracking.browser.ChangeView"
permission="zope.View" />
permission="loops.ManageSite" />
<browser:page
name="edit.html"
for="cybertools.tracking.interfaces.ITrack"
class="loops.organize.tracking.browser.EditForm"
permission="loops.ManageSite"
menu="zmi_views" title="Edit" />
<browser:page
for="loops.organize.tracking.access.IAccessRecord"
name="index.html"
class="loops.organize.tracking.browser.AccessView"
permission="zope.View" />
permission="loops.ManageSite" />
<browser:menuItem
menu="zmi_views"

View file

@ -0,0 +1,73 @@
<tal:tag condition="view/update">
<html metal:use-macro="context/@@standard_macros/view"
i18n:domain="loops">
<body>
<div metal:fill-slot="body">
<form action="." tal:attributes="action request/URL" method="post">
<input type="hidden" name="form_submitted" value="true" />
<h1>Edit Track <span tal:content="view/id" /></h1>
<div class="row">
<table>
<tr>
<td>Task:</td>
<td><a tal:omit-tag="not:view/taskUrl"
tal:attributes="href view/taskUrl"
tal:content="view/taskTitle" /></td></tr>
<tr>
<td>Run:</td>
<td tal:content="view/run"></td></tr>
<tr>
<td>User:</td>
<td><a tal:define="userUrl view/userUrl|nothing"
tal:omit-tag="not:userUrl"
tal:attributes="href userUrl"
tal:content="view/userTitle" /></td></tr>
<tr>
<td>Timestamp:</td>
<td tal:content="view/timeStamp"></td></tr>
<tr tal:repeat="key view/additionalMetadataFields">
<td><span tal:replace="key" />:</td>
<td><a tal:define="target python: view.getMetadataTarget(key)"
tal:omit-tag="not:target/url"
tal:attributes="href target/url"
tal:content="python: target['title'] or '???'" /></td></tr>
</table>
</div>
<h2>Data</h2>
<div class="row">
<table width="100%">
<tr>
<th>Key</th>
<th>Value</th>
</tr>
<tr tal:repeat="row python:sorted(context.data.items())">
<td>
<input name="data.key:records"
tal:attributes="value python:row[0]" /></td>
<td style="width: 100%">
<input name="data.value:records"
style="width: 100%"
tal:attributes="value python:row[1]" /></td>
</tr>
<tr>
<td>
<input name="data.key:records" /></td>
<td style="width: 100%">
<input name="data.value:records"
style="width: 100%" /></td>
</tr>
</table>
<div class="row">
<div class="controls">
<input type="submit" name="UPDATE_SUBMIT" value="Change"
i18n:attributes="value submit-button;" />
</div>
</div>
</div>
</form>
</div>
</body>
</html>
</tal:tag>

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 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
@ -264,6 +264,9 @@ class TrackDetails(BaseView):
@Lazy
def objectData(self):
obj = self.object
if obj is None:
return dict(object=None, title='-', type='-', url='',
version=None, canAccess=False)
node = self.view.nodeView
url = node is not None and node.getUrlForTarget(obj) or ''
view = self.view.getViewForObject(obj)

View file

@ -229,11 +229,23 @@ The user interface is a ReportConceptView subclass that is directly associated w
08/12/28 19:00 20:15
{'url': '.../home/.36', 'title': u'loops Development'}
{'url': '.../home/.33', 'title': u'john'} 01:15 00:15
{'icon': 'cybertools.icons/ledgreen.png', 'title': u'finished'}
{'actions': [...]}
>>> results.totals.data
{'effort': 900}
Export of work data
-------------------
>>> from loops.organize.work.report import WorkStatementCSVExport
>>> reportView = WorkStatementCSVExport(task01, TestRequest())
>>> reportView.nodeView = nodeView
>>> output = reportView()
>>> print output
Day;Start;End;Task;Party;Title;Duration;Effort;State
08/12/28;19:00;20:15;loops Development;john;;1.25;0.25;finished
Meeting Minutes
===============

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2012 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -22,12 +22,14 @@ View class(es) for work items.
from datetime import date
import time
from urllib import urlencode
from zope import component
from zope.app.security.interfaces import IAuthentication, PrincipalLookupError
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from zope.event import notify
from zope.lifecycleevent import ObjectModifiedEvent
from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL
from zope.traversing.api import getName, getParent
@ -43,6 +45,7 @@ from loops.browser.concept import ConceptView
from loops.browser.form import ObjectForm, EditObject
from loops.browser.node import NodeView
from loops.common import adapted
from loops.interfaces import IConcept
from loops.organize.interfaces import IPerson
from loops.organize.party import getPersonForUser
from loops.organize.stateful.browser import StateAction
@ -84,6 +87,14 @@ class WorkItemDetails(TrackDetails):
def deadline(self):
return self.formatTimeStamp(self.track.deadline, 'date')
@Lazy
def deadlineTime(self):
return self.formatTimeStamp(self.track.deadline, 'time')
@Lazy
def deadlineWithTime(self):
return self.globalOptions('organize.work.deadline_with_time')
@Lazy
def start(self):
result = self.formatTimeStamp(self.track.start, 'time')
@ -106,6 +117,11 @@ class WorkItemDetails(TrackDetails):
def startDay(self):
return self.formatTimeStamp(self.track.timeStamp, 'date')
@Lazy
def endDay(self):
endDay = self.formatTimeStamp(self.track.end, 'date')
return endDay != self.startDay and endDay or ''
@Lazy
def created(self):
return self.formatTimeStamp(self.track.created, 'dateTime')
@ -151,8 +167,8 @@ class WorkItemDetails(TrackDetails):
target=self.object,
addParams=dict(id=self.track.__name__))
actions = [info, WorkItemStateAction(self)]
if self.isLastInRun and self.allowedToEditWorkItem:
#if self.allowedToEditWorkItem:
#if self.isLastInRun and self.allowedToEditWorkItem:
if self.allowedToEditWorkItem:
self.view.registerDojoDateWidget()
self.view.registerDojoNumberWidget()
self.view.registerDojoTextarea()
@ -169,12 +185,11 @@ class WorkItemDetails(TrackDetails):
@Lazy
def allowedToEditWorkItem(self):
# if not canAccessObject(self.object.task):
# return False
if checkPermission('loops.ManageSite', self.object):
# or hasRole('loops.Master', self.object):
#if checkPermission('loops.ManageSite', self.object):
if (self.object is None and
checkPermission('zope.ManageContent', self.view.node)):
return True
if self.track.data.get('creator') == self.personId:
if checkPermission('zope.ManageContent', self.object):
return True
return self.user['object'] == getPersonForUser(self.object, self.view.request)
@ -342,6 +357,10 @@ class PersonWorkItems(BaseWorkItemsView, ConceptView):
class UserWorkItems(PersonWorkItems):
@Lazy
def title(self):
return self.adapted.title
def listWorkItems(self):
criteria = self.getCriteria()
p = getPersonForUser(self.context, self.request)
@ -361,6 +380,10 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
def checkPermissions(self):
return canAccessObject(self.task or self.target)
def setupView(self):
self.setupController()
self.registerDojoComboBox()
@Lazy
def macro(self):
return self.template.macros['create_workitem']
@ -385,6 +408,22 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
track.workItemType = types[0].name
return track
@Lazy
def titleSelection(self):
result = []
if self.title:
return result
dt = adapted(self.conceptManager.get('organize.work.texts'))
if dt is None or not dt.data:
return result
names = ([getName(self.target)] +
[getName(p.object)
for p in self.target.getAllParents(ignoreTypes=True)])
for name, text in dt.data.values():
if not name or name in names:
result.append(text)
return result
@Lazy
def title(self):
return self.track.title or u''
@ -402,10 +441,11 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
task = self.task
if task is None:
task = self.target
options = IOptions(adapted(task.conceptType))
typeNames = options.workitem_types
if typeNames:
return [workItemTypes[name] for name in typeNames]
if IConcept.providedBy(task):
options = IOptions(adapted(task.conceptType))
typeNames = options.workitem_types
if typeNames:
return [workItemTypes[name] for name in typeNames]
return workItemTypes
@Lazy
@ -419,12 +459,27 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
return time.strftime('%Y-%m-%d', time.localtime(ts))
return ''
@Lazy
def deadlineTime(self):
ts = self.track.deadline# or getTimeStamp()
if ts:
return time.strftime('T%H:%M', time.localtime(ts))
return ''
@Lazy
def deadlineWithTime(self):
return self.globalOptions('organize.work.deadline_with_time')
@Lazy
def defaultTimeStamp(self):
if self.workItemType.prefillDate:
return getTimeStamp()
return None
@Lazy
def defaultDate(self):
return time.strftime('%Y-%m-%dT%H:%M', time.localtime(getTimeStamp()))
@Lazy
def date(self):
ts = self.track.start or self.defaultTimeStamp
@ -432,6 +487,13 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
return time.strftime('%Y-%m-%d', time.localtime(ts))
return ''
@Lazy
def endDate(self):
ts = self.track.end or self.defaultTimeStamp
if ts:
return time.strftime('%Y-%m-%d', time.localtime(ts))
return ''
@Lazy
def startTime(self):
ts = self.track.start or self.defaultTimeStamp
@ -468,6 +530,8 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
task = self.task
if task is None:
task = self.target
if not IConcept.providedBy(task):
return []
options = IOptions(adapted(task.conceptType))
return options.hidden_workitem_actions or []
@ -486,7 +550,10 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
return [dict(name=util.getUidForObject(p), title=p.title)
for p in persons]
taskTypes = ['task', 'event', 'agendaitem']
@Lazy
def taskTypes(self):
return (self.globalOptions('organize.work.task_types') or
['task', 'event', 'agendaitem'])
@Lazy
def followUpTask(self):
@ -506,6 +573,22 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
return [dict(name=util.getUidForObject(t), title=t.title)
for t in tasks]
@Lazy
def priorities(self):
if 'priority' in self.workItemType.fields:
prio = self.conceptManager.get('organize.work.priorities')
if prio:
return adapted(prio).dataAsRecords()
return []
@Lazy
def activities(self):
if 'activity' in self.workItemType.fields:
act = self.conceptManager.get('organize.work.activities')
if act:
return adapted(act).dataAsRecords()
return []
@Lazy
def duration(self):
if self.state == 'running':
@ -564,13 +647,23 @@ class CreateWorkItem(EditObject, BaseTrackView):
setValue('party')
if action == 'move':
setValue('task')
result['deadline'] = parseDate(form.get('deadline'))
#result['deadline'] = parseDate(form.get('deadline'))
deadline = form.get('deadline')
if deadline:
deadlineTime = (form.get('deadline_time', '').
strip().replace('T', '') or '00:00:00')
result['deadline'] = parseDateTime('T'.join((deadline, deadlineTime)))
else:
result['deadline'] = None
result['priority'] = form.get('priority')
result['activity'] = form.get('activity')
startDate = form.get('start_date', '').strip()
endDate = form.get('end_date', '').strip() or startDate
startTime = form.get('start_time', '').strip().replace('T', '') or '00:00:00'
endTime = form.get('end_time', '').strip().replace('T', '') or '00:00:00'
if startDate:
result['start'] = parseDateTime('T'.join((startDate, startTime)))
result['end'] = parseDateTime('T'.join((startDate, endTime)))
result['end'] = parseDateTime('T'.join((endDate, endTime)))
result['duration'] = parseTime(form.get('duration'))
result['effort'] = parseTime(form.get('effort'))
return action, result
@ -589,6 +682,12 @@ class CreateWorkItem(EditObject, BaseTrackView):
#notify(ObjectModifiedEvent(obj))
url = self.view.virtualTargetUrl
#url = self.request.URL
# append sortinfo parameters:
#urlParams = {}
#for k, v in self.view.sortInfo.items():
# urlParams['sortinfo_' + k] = v['fparam']
#if urlParams:
# url = '%s?%s' % (url, urlencode(urlParams))
self.request.response.redirect(url)
return False
@ -660,5 +759,31 @@ def formatTimeDelta(value):
if not value:
return u''
h, m = divmod(int(value) / 60, 60)
if h > 24:
#d, h = divmod(h / 24, 24)
#return u'%id %02i:%02i' % (d, h, m)
return str(int(round(h / 24.0)))
return u'%02i:%02i' % (h, m)
class FixCheckupWorkItems(object):
def __call__(self):
context = removeSecurityProxy(self.context)
rm = context['records']['work']
count = 0
workItems = list(rm.values())
for wi in workItems:
if wi.state in ('done',):
if wi.workItemType != 'checkup':
print '*** done, but not checkup', wi.__name__
continue
wi.state = 'running'
wi.reindex('state')
if wi.end == wi.start:
del wi.data['end']
count += 1
msg = '*** checked: %i, updated: %i.' % (len(workItems), count)
print msg
return msg

View file

@ -97,10 +97,16 @@
<browser:page
name="work.html"
for="loops.organize.interfaces.ITask"
for="loops.organize.interfaces.IConceptSchema"
class="loops.organize.work.report.WorkStatementView"
permission="zope.View" />
<browser:page
name="work.csv"
for="loops.organize.interfaces.IConceptSchema"
class="loops.organize.work.report.WorkStatementCSVExport"
permission="zope.View" />
<zope:adapter
name="meeting_minutes"
factory="loops.organize.work.report.MeetingMinutes"
@ -126,6 +132,14 @@
attribute="embed"
permission="zope.View" />
<!-- repair -->
<browser:page
name="fix_checkup_workitems.fix"
for="loops.interfaces.ILoops"
class="loops.organize.work.browser.FixCheckupWorkItems"
permission="zope.View" />
<!-- setup -->
<zope:adapter factory="loops.organize.work.setup.SetupManager"

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -22,21 +22,25 @@ Work report definitions.
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from zope.component import adapter
from zope.component import adapter, getAdapter
from cybertools.composer.report.base import Report
from cybertools.composer.report.base import LeafQueryCriteria, CompoundQueryCriteria
from cybertools.composer.report.field import CalculatedField
from cybertools.composer.report.result import ResultSet, Row as BaseRow
from cybertools.meta.interfaces import IOptions
from cybertools.organize.interfaces import IWorkItems
from cybertools.stateful.interfaces import IStateful
from cybertools.util.date import timeStamp2Date, timeStamp2ISO
from cybertools.util.format import formatDate
from cybertools.util.jeep import Jeep
from loops.common import adapted, baseObject
from loops.expert.browser.export import ResultsConceptCSVExport
from loops.expert.browser.report import ReportConceptView
from loops.expert.field import Field, TargetField, DateField, StateField, \
TextField, HtmlTextField, UrlField
StringField, TextField, HtmlTextField, UrlField
from loops.expert.field import SubReport, SubReportField
from loops.expert.field import TrackDateField, TrackTimeField, TrackDateTimeField
from loops.expert.field import WorkItemStateField
from loops.expert.report import ReportInstance
from loops import util
@ -48,46 +52,13 @@ class WorkStatementView(ReportConceptView):
reportName = 'work_statement'
class WorkStatementCSVExport(ResultsConceptCSVExport):
reportName = 'work_statement'
# fields
class TrackDateField(Field):
fieldType = 'date'
part = 'date'
format = 'short'
cssClass = 'right'
def getValue(self, row):
value = self.getRawValue(row)
if not value:
return None
return timeStamp2Date(value)
def getDisplayValue(self, row):
value = self.getValue(row)
if value:
view = row.parent.context.view
return formatDate(value, self.part, self.format,
view.languageInfo.language)
return u''
def getSelectValue(self, row):
value = self.getRawValue(row)
if not value:
return ''
return timeStamp2ISO(value)[:10]
class TrackDateTimeField(TrackDateField):
part = 'dateTime'
class TrackTimeField(TrackDateField):
part = 'time'
class DurationField(Field):
cssClass = 'right'
@ -108,6 +79,31 @@ class DurationField(Field):
return u'%02i:%02i' % divmod(value * 60, 60)
class PartyStateField(StateField):
def getValue(self, row):
context = row.context
if context is None:
return None
party = util.getObjectForUid(context.party)
ptype = adapted(party.conceptType)
stdefs = IOptions(ptype)('organize.stateful') or []
if self.statesDefinition in stdefs:
stf = getAdapter(party, IStateful,
name=self.statesDefinition)
return stf.state
def getContext(self, row):
if row.context is None:
return None
party = util.getObjectForUid(row.context.party)
ptype = adapted(party.conceptType)
stdefs = IOptions(ptype)('organize.stateful') or []
if self.statesDefinition in stdefs:
return party
return None
# common fields
tasks = Field('tasks', u'Tasks',
@ -135,6 +131,14 @@ day = TrackDateField('day', u'Day',
description=u'The day the work was done.',
cssClass='center',
executionSteps=['sort', 'output'])
dayStart = TrackDateField('dayStart', u'Start Day',
description=u'The day the unit of work was started.',
cssClass='center',
executionSteps=['sort', 'output'])
dayEnd = TrackDateField('dayEnd', u'End Day',
description=u'The day the unit of work was finished.',
cssClass='center',
executionSteps=['sort', 'output'])
timeStart = TrackTimeField('start', u'Start',
description=u'The time the unit of work was started.',
executionSteps=['sort', 'output'])
@ -143,15 +147,15 @@ timeEnd = TrackTimeField('end', u'End',
executionSteps=['output'])
task = TargetField('taskId', u'Task',
description=u'The task to which work items belong.',
executionSteps=['output'])
executionSteps=['sort', 'output'])
party = TargetField('userName', u'Party',
description=u'The party (usually a person) who did the work.',
fieldType='selection',
executionSteps=['query', 'sort', 'output'])
workTitle = Field('title', u'Title',
workTitle = StringField('title', u'Title',
description=u'The short description of the work.',
executionSteps=['output'])
workDescription = Field('description', u'Description',
executionSteps=['sort', 'output'])
workDescription = StringField('description', u'Description',
description=u'The long description of the work.',
executionSteps=['output'])
duration = DurationField('duration', u'Duration',
@ -160,11 +164,16 @@ duration = DurationField('duration', u'Duration',
effort = DurationField('effort', u'Effort',
description=u'The effort of the work.',
executionSteps=['output', 'totals'])
state = StateField('state', u'State',
state = WorkItemStateField('state', u'State',
description=u'The state of the work.',
cssClass='center',
statesDefinition='workItemStates',
executionSteps=['query', 'output'])
partyState = PartyStateField('partyState', u'Party State',
description=u'State of the party, mainly for selection.',
cssClass='center',
statesDefinition='contact_states',
executionSteps=['query', 'output'])
# basic definitions and work report instance
@ -182,6 +191,12 @@ class WorkRow(BaseRow):
def getDay(self, attr):
return self.context.timeStamp
def getStart(self, attr):
return self.context.start
def getEnd(self, attr):
return self.context.end
def getDuration(self, attr):
value = self.context.data.get('duration')
if value is None:
@ -194,8 +209,10 @@ class WorkRow(BaseRow):
value = self.getDuration(attr)
return value
attributeHandlers = dict(day=getDay, dayFrom=getDay, dayTo=getDay,
duration=getDuration, effort=getEffort)
attributeHandlers = dict(day=getDay,
dayStart=getStart, dayEnd=getEnd,
dayFrom=getDay, dayTo=getDay,
duration=getDuration, effort=getEffort,)
class WorkReportInstance(ReportInstance):

View file

@ -36,8 +36,9 @@
<td class="nowrap center" tal:content="row/end">20:00</td>
<td class="nowrap center" tal:content="row/duration">2:30</td>
<td tal:condition="python: 'Task' in work.columns">
<a tal:attributes="href row/objectData/url"
tal:content="row/objectData/title">Task</a></td>
<a tal:define="data row/objectData"
tal:attributes="href data/url"
tal:content="data/title">Task</a></td>
<td tal:condition="python: 'User' in work.columns">
<a tal:attributes="href row/user/url"
tal:content="row/user/title">John</a></td>
@ -69,10 +70,13 @@
<form method="post" id="addWorkitem_form" class="dialog"
xx_dojoType="dijit.form.Form"
tal:define="workItemTypes view/workItemTypes;
workItemType view/workItemType">
workItemType view/workItemType;
dummy view/setupView">
<input type="hidden" name="form.action" value="create_workitem" />
<input type="hidden" name="id"
tal:attributes="value request/form/id|nothing" />
<!--<input type="hidden" name="sortinfo_results"
tal:attributes="value view/sortInfo/results/fparam|nothing" />-->
<div class="heading" i18n:translate="">Add Work Item</div>
<div>
<tal:type condition="view/showTypes">
@ -91,8 +95,17 @@
tal:attributes="value python:workItemTypes[0].name" />
</tal:type>
<label i18n:translate="" for="title">Title</label>
<div>
<input name="title" id="title" style="width: 60em"
<div tal:define="titleSelection view/titleSelection">
<select tal:condition="titleSelection"
data-dojo-type="dijit/form/ComboBox" required
name="title" id="title" style="width: 100%"
tal:attributes="value view/title" >
<option selected></option>
<option tal:repeat="text view/titleSelection"
tal:content="text"></option>
</select>
<input tal:condition="not:titleSelection"
name="title" id="title" style="width: 60em"
dojoType="dijit.form.ValidationTextBox" required
tal:attributes="value view/title" /></div>
</div>
@ -107,12 +120,21 @@
<label i18n:translate="" for="action">Action</label>
<select name="workitem.action" id="action"
onChange="showIfIn(this, [['move', 'target_task'],
['delegate', 'target_party']])">
['delegate', 'target_party']]);
setIfIn(this, [['start', 'start_date',
this.form.default_date.value],
['start', 'start_time',
this.form.default_date.value],
['start', 'end_time', null],
['start', 'duration', ''],
['start', 'effort', '']])">
<option tal:repeat="action view/actions"
tal:attributes="value action/name"
tal:content="action/title"
i18n:translate="" />
</select>
<input type="hidden" name="default_date" id="default_date"
tal:attributes="value view/defaultDate" />
<span id="target_party" style="display: none">&nbsp;
<label i18n:translate="delegate_to_party" for="input_party"
style="display: inline">to</label>
@ -149,45 +171,92 @@
view.getUidForObject(view.followUpTask)" />
</span>
</div>
<div>
<div id="deadline"
tal:condition="python:'deadline' in workItemType.fields">
<label i18n:translate="" for="deadline-input">Deadline</label>
<div id="deadline-input">
<input type="text" name="deadline" style="width: 8em"
dojoType="dijit.form.DateTextBox"
tal:attributes="value view/deadline" /></div>
</div>
<div id="start-end"
tal:condition="python:'start-end' in workItemType.fields">
<label i18n:translate="" for="start-end-input">Start - End</label>
<div id="start-end-input">
<input type="text" name="start_date" style="width: 8em"
dojoType="dijit.form.DateTextBox"
tal:attributes="value view/date" />
<input type="text" name="start_time" style="width: 6em"
dojoType="dijit.form.TimeTextBox"
tal:attributes="value view/startTime" /> -
<input type="text" name="end_time" style="width: 6em"
dojoType="dijit.form.TimeTextBox"
tal:attributes="value view/endTime" /></div>
</div>
<div id="duration-effort"
tal:condition="python:
'duration-effort' in workItemType.fields">
<label i18n:translate=""
for="duration-effort-input">Duration / Effort (hh:mm)</label>
<div id="duration-effort-input">
<input type="text" name="duration" style="width: 5em"
dojoType="dijit.form.ValidationTextBox"
regexp="-{0,1}[0-9]{1,2}(:[0-5][0-9]){0,1}"
tal:attributes="value view/duration" /> /
<input type="text" name="effort" style="width: 5em"
dojoType="dijit.form.ValidationTextBox"
regexp="-{0,1}[0-9]{1,2}(:[0-5][0-9]){0,1}"
tal:attributes="value view/effort" /></div>
<div id="deadline"
tal:condition="python:'deadline' in workItemType.fields">
<label i18n:translate="" for="deadline-input">Deadline</label>
<div id="deadline-input">
<input type="text" name="deadline" style="width: 8em"
dojoType="dijit.form.DateTextBox"
tal:attributes="value view/deadline" />
<input type="text" name="deadline_time" style="width: 6em"
dojoType="dijit.form.TimeTextBox"
tal:condition="view/deadlineWithTime"
tal:attributes="value view/deadlineTime" />
</div>
</div>
<div id="priority-activity"
tal:define="priorities view/priorities;
activities view/activities"
tal:condition="python:priorities or activities">
<table style="width: auto">
<tr>
<td tal:condition="priorities">
<label i18n:translate="" for="priority">Priority</label></td>
<td tal:condition="activities">
<label i18n:translate="" for="activity">Activity</label></td>
</tr>
<tr>
<td tal:condition="priorities">
<select name="priority" id="priority"
tal:define="value view/track/data/priority|nothing">
<option tal:repeat="prio priorities"
tal:attributes="value prio/name;
selected python:prio['name'] == value"
tal:content="prio/title" />
</select>
</td>
<td tal:condition="activities">
<select name="activity" id="activity"
tal:define="value view/track/data/activity|nothing">
<option tal:repeat="act activities"
tal:attributes="value act/name;
selected python:act['name'] == value"
tal:content="act/title" />
</select>
</td>
</table>
</div>
<div id="start-end"
tal:condition="python:'start-end' in workItemType.fields">
<label i18n:translate="" for="start-end-input">Start - End</label>
<div id="start-end-input">
<input type="text" name="start_date" style="width: 8em"
id="start_date"
dojoType="dijit.form.DateTextBox"
tal:attributes="value view/date" />
<input type="text" name="start_time" id="start_time" style="width: 6em"
dojoType="dijit.form.TimeTextBox"
tal:attributes="value view/startTime" /> -
<input type="text" name="end_time" id="end_time" style="width: 6em"
dojoType="dijit.form.TimeTextBox"
tal:attributes="value view/endTime" /></div>
</div>
<div id="daterange"
tal:condition="python:'daterange' in workItemType.fields">
<label i18n:translate="" for="daterange-input">Start - End</label>
<div id="daterange-input">
<input type="text" name="start_date" style="width: 8em"
dojoType="dijit.form.DateTextBox"
tal:attributes="value view/date" />
<input type="text" name="end_date" style="width: 8em"
dojoType="dijit.form.DateTextBox"
tal:attributes="value view/endDate" /></div>
</div>
<div id="duration-effort"
tal:condition="python:
'duration-effort' in workItemType.fields">
<label i18n:translate=""
for="duration-effort-input">Duration / Effort (hh:mm)</label>
<div id="duration-effort-input">
<input type="text" name="duration" id="duration" style="width: 5em"
dojoType="dijit.form.ValidationTextBox"
regexp="-{0,1}[0-9]{1,2}(:[0-5][0-9]){0,1}"
tal:attributes="value view/duration" /> /
<input type="text" name="effort" id="effort" style="width: 5em"
dojoType="dijit.form.ValidationTextBox"
regexp="-{0,1}[0-9]{1,2}(:[0-5][0-9]){0,1}"
tal:attributes="value view/effort" /></div>
</div>
<div>
<label i18n:translate="" for="comment">Comment</label>
<div>
@ -227,17 +296,23 @@
</tr>
<tr>
<td><span i18n:translate="">Task</span>:</td>
<td tal:content="item/object/title"></td>
<td tal:content="item/object/title|nothing"></td>
</tr>
<tr>
<td><span i18n:translate="">Deadline</span>:</td>
<td tal:content="item/deadline"></td>
<td>
<span tal:content="item/deadline" />
<span tal:condition="item/deadlineWithTime|nothing"
tal:content="item/deadlineTime" />
</td>
</tr>
<tr>
<td><span i18n:translate="">Start - End</span>:</td>
<td><span tal:content="item/startDay" />
<span tal:content="item/start" /> -
<span tal:content="item/end" /></td>
<span tal:content="item/endDay" />
<span tal:content="item/end" />
</td>
</tr>
<tr>
<td><span i18n:translate="">Duration/Effort</span>:</td>

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -36,6 +36,7 @@ from zope.securitypolicy.interfaces import IRolePermissionManager
from zope.traversing.api import getName
from zope.traversing.interfaces import IPhysicallyLocatable
from cybertools.meta.interfaces import IOptions
from loops.common import adapted
from loops.interfaces import ILoopsObject, IConcept
from loops.interfaces import IAssignmentEvent, IDeassignmentEvent
@ -66,13 +67,39 @@ workspaceGroupsFolderName = 'gloops_ws'
# checking and querying functions
def getOption(obj, option, checkType=True):
opts = component.queryAdapter(adapted(obj), IOptions)
if opts is not None:
opt = opts(option, None)
if opt is True:
return opt
if opt:
return opt[0]
if not checkType:
return None
typeMethod = getattr(obj, 'getType', None)
if typeMethod is not None:
opts = component.queryAdapter(adapted(typeMethod()), IOptions)
if opts is not None:
opt = opts(option, None)
if opt is True:
return opt
if opt:
return opt[0]
return None
def canAccessObject(obj):
return canAccess(obj, 'title')
if not canAccess(obj, 'title'):
return False
perm = getOption(obj, 'access_permission')
if not perm:
return True
return checkPermission(perm, obj)
def canListObject(obj, noCheck=False):
if noCheck:
return True
return canAccess(obj, 'title')
return canAccessObject(obj)
def canAccessRestricted(obj):
return checkPermission('loops.ViewRestricted', obj)

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 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
@ -21,6 +21,7 @@ Base classes for security setters, i.e. adapters that provide standardized
methods for setting role permissions and other security-related stuff.
"""
from logging import getLogger
from zope.app.security.settings import Allow, Deny, Unset
from zope import component
from zope.component import adapts
@ -39,9 +40,12 @@ from loops.interfaces import IConceptSchema, IBaseResourceSchema, ILoopsAdapter
from loops.organize.util import getPrincipalFolder, getGroupsFolder, getGroupId
from loops.security.common import overrides, setRolePermission, setPrincipalRole
from loops.security.common import allRolesExceptOwner, acquiringPredicateNames
from loops.security.common import getOption
from loops.security.interfaces import ISecuritySetter
from loops.versioning.interfaces import IVersionable
logger = getLogger('loops.security')
class BaseSecuritySetter(object):
@ -55,10 +59,18 @@ class BaseSecuritySetter(object):
def baseObject(self):
return baseObject(self.context)
@Lazy
def adapted(self):
return adapted(self.context)
@Lazy
def conceptManager(self):
return self.baseObject.getLoopsRoot().getConceptManager()
@Lazy
def options(self):
return IOptions(self.adapted)
@Lazy
def typeOptions(self):
type = self.baseObject.getType()
@ -133,11 +145,17 @@ class LoopsObjectSecuritySetter(BaseSecuritySetter):
def acquireRolePermissions(self):
settings = {}
for p in self.parents:
if p == self.baseObject:
#rpm = IRolePermissionMap(self.baseObject)
#for p, r, s in rpm.getRolesAndPermissions():
# settings[(p, r)] = s
for parent in self.parents:
if parent == self.baseObject:
continue
secProvider = p
wi = p.workspaceInformation
if getOption(parent, 'security.no_propagate_rolepermissions',
checkType=False):
continue
secProvider = parent
wi = parent.workspaceInformation
if wi:
if wi.propagateRolePermissions == 'none':
continue
@ -147,6 +165,10 @@ class LoopsObjectSecuritySetter(BaseSecuritySetter):
for p, r, s in rpm.getRolesAndPermissions():
current = settings.get((p, r))
if current is None or overrides(s, current):
if self.globalOptions('security.log_acquired_setting'):
logger.info('*** %s: %s, %s: current %s; new from %s: %s' %
(self.baseObject.__name__, p, r, current,
parent.__name__, s))
settings[(p, r)] = s
self.setDefaultRolePermissions()
self.setRolePermissions(settings)
@ -213,14 +235,20 @@ class ConceptSecuritySetter(LoopsObjectSecuritySetter):
adapts(IConceptSchema)
@Lazy
def noPropagateRolePermissions(self):
return getOption(self.baseObject, 'security.no_propagate_rolepermissions',
checkType=False)
def setAcquiredSecurity(self, relation, revert=False, updated=None):
if updated and relation.second in updated:
return
if relation.predicate not in self.acquiringPredicates:
return
setter = ISecuritySetter(adapted(relation.second))
setter.setDefaultRolePermissions()
setter.acquireRolePermissions()
if not self.noPropagateRolePermissions:
setter.setDefaultRolePermissions()
setter.acquireRolePermissions()
setter.acquirePrincipalRoles()
#wi = baseObject(self.context).workspaceInformation
#if wi and not wi.propagateParentSecurity:

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()
>>> loopsRoot = site['loops']
>>> len(concepts), len(resources), len(views)
(33, 3, 1)
(35, 3, 1)
>>> from cybertools.tracking.btree import TrackingStorage
>>> from loops.system.job import JobRecord

View file

@ -73,7 +73,10 @@ class DataTable(AdapterBase):
_adapterAttributes = AdapterBase._adapterAttributes + ('columns', 'data')
def getColumns(self):
return getattr(self.context, '_columns', ['key', 'value'])
cols = getattr(self.context, '_columns', None)
if not cols:
cols = getattr(baseObject(self.type), '_columns', None)
return cols or ['key', 'value']
def setColumns(self, value):
self.context._columns = value
columns = property(getColumns, setColumns)
@ -90,6 +93,21 @@ class DataTable(AdapterBase):
self.context._data = OOBTree(data)
data = property(getData, setData)
def dataAsRecords(self):
result = []
for k, v in sorted(self.data.items()):
item = {}
for idx, c in enumerate(self.columns):
if idx == 0:
item[c] = k
else:
item[c] = v[idx-1]
result.append(item)
return result
def getRowsByValue(self, column, value):
return [r for r in self.dataAsRecords() if r[column] == value]
TypeInterfaceSourceList.typeInterfaces += (IDataTable,)

View file

@ -115,7 +115,7 @@ class LoopsType(BaseType):
@Lazy
def typeProvider(self):
# TODO: unify this type attribute naming...
return self.context.resourceType
return getattr(self.context, 'resourceType', None)
@Lazy
def options(self):

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2007 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
"""
View classes for versioning.
$Id$
"""
from zope import interface, component
@ -51,8 +49,11 @@ class ListVersions(BaseView):
def versions(self):
versionable = IVersionable(self.context)
versions = versionable.versions
cls = getattr(self.controller, 'versionViewClass', None)
for v in sorted(versions):
if isinstance(versions[v], Resource):
if cls is not None:
yield(cls(versions[v], self.request))
elif isinstance(versions[v], Resource):
from loops.browser.resource import ResourceView
yield ResourceView(versions[v], self.request)
else:

View file

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