merge branch master
This commit is contained in:
commit
3c1a5ccdf4
83 changed files with 2954 additions and 418 deletions
|
@ -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
|
||||
=======
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
<div tal:define="item nocall:item/targetObjectView;
|
||||
macro item/macro">
|
||||
<div tal:attributes="class string:content-$level;
|
||||
id id;">
|
||||
<div metal:use-macro="macro" />
|
||||
</tal:concepts>
|
||||
</div>
|
||||
</div>
|
||||
</tal:body>
|
||||
</metal:body>
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -96,6 +96,7 @@
|
|||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<metal:custom define-slot="custom_info" />
|
||||
<metal:fields use-macro="view/comment_macros/comments" />
|
||||
</div>
|
||||
</metal:block>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -11,8 +11,14 @@ body {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#content {
|
||||
/* width: 100%; */
|
||||
width: 80%;
|
||||
width: auto;
|
||||
color: Black;
|
||||
}
|
||||
|
||||
|
|
14
common.py
14
common.py
|
@ -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',
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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
104
expert/browser/export.py
Normal 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 '???'
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -25,16 +25,48 @@
|
|||
</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"
|
||||
<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">
|
||||
<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:
|
||||
|
@ -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:
|
||||
|
|
|
@ -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]
|
||||
|
|
115
expert/field.py
115
expert/field.py
|
@ -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)):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
6
external/README.txt
vendored
|
@ -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
|
||||
--------------------------------------------------
|
||||
|
|
|
@ -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?')
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
133
knowledge/qualification/report.py
Normal file
133
knowledge/qualification/report.py
Normal 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)
|
||||
|
|
@ -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 = {}
|
||||
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
|
||||
data[uid] = value
|
||||
response.values[question] = value
|
||||
Responses(self.context).save(self.data)
|
||||
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 []
|
||||
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]
|
||||
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)
|
||||
|
|
|
@ -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.'),
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -4,95 +4,272 @@
|
|||
|
||||
<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>:
|
||||
<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"> </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> </td>
|
||||
<td tal:repeat="opt item/answerOptions"> </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">
|
||||
<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"
|
||||
onclick="if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false" />
|
||||
</form>
|
||||
</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> </td><td> </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:condition="value/radio"
|
||||
tal:attributes="
|
||||
name string:question_${question/uid};
|
||||
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>
|
||||
title value/title" />
|
||||
</td>
|
||||
</tr>
|
||||
</tal:qugroup>
|
||||
</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>
|
||||
<input type="submit" name="submit" value="Evaluate Questionnaire"
|
||||
i18n:attributes="value" />
|
||||
<input type="button" name="reset_responses" value="Reset Responses Entered"
|
||||
i18n:attributes="value"
|
||||
onclick="setRadioButtons('none'); return false" />
|
||||
</form>
|
||||
<p tal:define="teamData item/teamData"
|
||||
tal:condition="teamData">
|
||||
<b><span i18n:translate="">Team Size</span>:
|
||||
<span tal:content="python:len(teamData)" /></b><br />
|
||||
</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> </td>
|
||||
<td> </td>
|
||||
<!--<td> </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 />
|
||||
</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>
|
||||
|
||||
|
|
|
@ -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.
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
=============
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
26
organize/comment/interfaces.py
Normal file
26
organize/comment/interfaces.py
Normal 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
|
6
organize/comment/loops_comment_de.dmp
Normal file
6
organize/comment/loops_comment_de.dmp
Normal 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')
|
75
organize/comment/report.py
Normal file
75
organize/comment/report.py
Normal 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()
|
|
@ -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).
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
name = getName(person)
|
||||
if name:
|
||||
raise ValueError(
|
||||
'Error when creating user %s: There is already a person (%s) assigned to user %s.'
|
||||
% (getName(self.context), getName(person), userId))
|
||||
'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)
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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):
|
||||
for track in self.context.query(userName=personUid, taskId=uid):
|
||||
if track.type == type:
|
||||
changed = True
|
||||
self.context.removeTrack(t)
|
||||
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'))
|
||||
|
|
|
@ -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
|
||||
=============
|
||||
|
||||
|
|
|
@ -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,6 +124,7 @@ class ChangeStateBase(object):
|
|||
|
||||
@Lazy
|
||||
def transition(self):
|
||||
if self.action:
|
||||
return self.stateful.getStatesDefinition().transitions[self.action]
|
||||
|
||||
@Lazy
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
56
organize/stateful/contact.py
Normal file
56
organize/stateful/contact.py
Normal 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'
|
||||
|
|
@ -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'),
|
||||
|
|
|
@ -115,16 +115,33 @@
|
|||
tal:define="stateObject view/stateful/getStateObject"
|
||||
tal:content="stateObject/title" /> -
|
||||
<span i18n:translate="">Transition</span>:
|
||||
<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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
73
organize/tracking/edit_track.pt
Normal file
73
organize/tracking/edit_track.pt
Normal 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>
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
===============
|
||||
|
|
|
@ -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,6 +441,7 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
|
|||
task = self.task
|
||||
if task is None:
|
||||
task = self.target
|
||||
if IConcept.providedBy(task):
|
||||
options = IOptions(adapted(task.conceptType))
|
||||
typeNames = options.workitem_types
|
||||
if typeNames:
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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">
|
||||
<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>
|
||||
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" style="width: 6em"
|
||||
<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" style="width: 6em"
|
||||
<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" style="width: 5em"
|
||||
<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" style="width: 5em"
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,12 +235,18 @@ 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))
|
||||
if not self.noPropagateRolePermissions:
|
||||
setter.setDefaultRolePermissions()
|
||||
setter.acquireRolePermissions()
|
||||
setter.acquirePrincipalRoles()
|
||||
|
|
|
@ -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
|
||||
|
|
20
table.py
20
table.py
|
@ -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,)
|
||||
|
||||
|
|
2
type.py
2
type.py
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue