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())
|
>>> sorted(adapted(concepts['countries']).data.items())
|
||||||
[('at', ['Austria']), ('de', ['Germany'])]
|
[('at', ['Austria']), ('de', ['Germany'])]
|
||||||
|
|
||||||
|
>>> countries.dataAsRecords()
|
||||||
|
[{'value': 'Austria', 'key': 'at'}, {'value': 'Germany', 'key': 'de'}]
|
||||||
|
|
||||||
|
>>> countries.getRowsByValue('value', 'Germany')
|
||||||
|
[{'value': 'Germany', 'key': 'de'}]
|
||||||
|
|
||||||
|
|
||||||
Caching
|
Caching
|
||||||
=======
|
=======
|
||||||
|
|
|
@ -92,6 +92,8 @@ class DialogAction(Action):
|
||||||
urlParams['fixed_type'] = 'yes'
|
urlParams['fixed_type'] = 'yes'
|
||||||
if self.viewTitle:
|
if self.viewTitle:
|
||||||
urlParams['view_title'] = self.viewTitle
|
urlParams['view_title'] = self.viewTitle
|
||||||
|
#for k, v in self.page.sortInfo.items():
|
||||||
|
# urlParams['sortinfo_' + k] = v['fparam']
|
||||||
urlParams.update(self.addParams)
|
urlParams.update(self.addParams)
|
||||||
if self.target is not None:
|
if self.target is not None:
|
||||||
url = self.page.getUrlForTarget(self.target)
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -22,7 +22,7 @@ Common base class for loops browser view classes.
|
||||||
|
|
||||||
from cgi import parse_qs, parse_qsl
|
from cgi import parse_qs, parse_qsl
|
||||||
#import mimetypes # use more specific assignments from cybertools.text
|
#import mimetypes # use more specific assignments from cybertools.text
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
import re
|
import re
|
||||||
from time import strptime
|
from time import strptime
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
|
@ -61,17 +61,21 @@ from cybertools.stateful.interfaces import IStateful
|
||||||
from cybertools.text import mimetypes
|
from cybertools.text import mimetypes
|
||||||
from cybertools.typology.interfaces import IType, ITypeManager
|
from cybertools.typology.interfaces import IType, ITypeManager
|
||||||
from cybertools.util.date import toLocalTime
|
from cybertools.util.date import toLocalTime
|
||||||
|
from cybertools.util.format import formatDate
|
||||||
from cybertools.util.jeep import Jeep
|
from cybertools.util.jeep import Jeep
|
||||||
from loops.browser.util import normalizeForUrl
|
from loops.browser.util import normalizeForUrl
|
||||||
from loops.common import adapted, baseObject
|
from loops.common import adapted, baseObject
|
||||||
from loops.config.base import DummyOptions
|
from loops.config.base import DummyOptions
|
||||||
from loops.i18n.browser import I18NView
|
from loops.i18n.browser import I18NView
|
||||||
from loops.interfaces import IResource, IView, INode, ITypeConcept
|
from loops.interfaces import IResource, IView, INode, ITypeConcept
|
||||||
|
from loops.organize.personal import favorite
|
||||||
|
from loops.organize.party import getPersonForUser
|
||||||
from loops.organize.tracking import access
|
from loops.organize.tracking import access
|
||||||
from loops.organize.util import getRolesForPrincipal
|
from loops.organize.util import getRolesForPrincipal
|
||||||
from loops.resource import Resource
|
from loops.resource import Resource
|
||||||
from loops.security.common import checkPermission
|
from loops.security.common import checkPermission
|
||||||
from loops.security.common import canAccessObject, canListObject, canWriteObject
|
from loops.security.common import canAccessObject, canListObject, canWriteObject
|
||||||
|
from loops.security.common import canEditRestricted
|
||||||
from loops.type import ITypeConcept, LoopsTypeInfo
|
from loops.type import ITypeConcept, LoopsTypeInfo
|
||||||
from loops import util
|
from loops import util
|
||||||
from loops.util import _, saveRequest
|
from loops.util import _, saveRequest
|
||||||
|
@ -132,7 +136,58 @@ class EditForm(form.EditForm):
|
||||||
return parentUrl + '/contents.html'
|
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 = {}
|
actions = {}
|
||||||
portlet_actions = []
|
portlet_actions = []
|
||||||
|
@ -153,6 +208,10 @@ class BaseView(GenericView, I18NView):
|
||||||
pass
|
pass
|
||||||
saveRequest(request)
|
saveRequest(request)
|
||||||
|
|
||||||
|
def todayFormatted(self):
|
||||||
|
return formatDate(date.today(), 'date', 'short',
|
||||||
|
self.languageInfo.language)
|
||||||
|
|
||||||
def checkPermissions(self):
|
def checkPermissions(self):
|
||||||
return canAccessObject(self.context)
|
return canAccessObject(self.context)
|
||||||
|
|
||||||
|
@ -204,6 +263,16 @@ class BaseView(GenericView, I18NView):
|
||||||
result.append(view)
|
result.append(view)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def urlParamString(self):
|
||||||
|
return self.getUrlParamString()
|
||||||
|
|
||||||
|
def getUrlParamString(self):
|
||||||
|
qs = self.request.get('QUERY_STRING')
|
||||||
|
if qs:
|
||||||
|
return '?' + qs
|
||||||
|
return ''
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def principalId(self):
|
def principalId(self):
|
||||||
principal = self.request.principal
|
principal = self.request.principal
|
||||||
|
@ -337,6 +406,10 @@ class BaseView(GenericView, I18NView):
|
||||||
def isPartOfPredicate(self):
|
def isPartOfPredicate(self):
|
||||||
return self.conceptManager.get('ispartof')
|
return self.conceptManager.get('ispartof')
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def queryTargetPredicate(self):
|
||||||
|
return self.conceptManager.get('querytarget')
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def memberPredicate(self):
|
def memberPredicate(self):
|
||||||
return self.conceptManager.get('ismember')
|
return self.conceptManager.get('ismember')
|
||||||
|
@ -732,6 +805,8 @@ class BaseView(GenericView, I18NView):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def checkState(self):
|
def checkState(self):
|
||||||
|
if checkPermission('loops.ManageSite', self.context):
|
||||||
|
return True
|
||||||
if not self.allStates:
|
if not self.allStates:
|
||||||
return True
|
return True
|
||||||
for stf in self.allStates:
|
for stf in self.allStates:
|
||||||
|
@ -806,6 +881,10 @@ class BaseView(GenericView, I18NView):
|
||||||
def canAccessRestricted(self):
|
def canAccessRestricted(self):
|
||||||
return checkPermission('loops.ViewRestricted', self.context)
|
return checkPermission('loops.ViewRestricted', self.context)
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def canEditRestricted(self):
|
||||||
|
return canEditRestricted(self.context)
|
||||||
|
|
||||||
def openEditWindow(self, viewName='edit.html'):
|
def openEditWindow(self, viewName='edit.html'):
|
||||||
if self.editable:
|
if self.editable:
|
||||||
if checkPermission('loops.ManageSite', self.context):
|
if checkPermission('loops.ManageSite', self.context):
|
||||||
|
@ -928,6 +1007,12 @@ class BaseView(GenericView, I18NView):
|
||||||
jsCall = 'dojo.require("dojox.image.Lightbox");'
|
jsCall = 'dojo.require("dojox.image.Lightbox");'
|
||||||
self.controller.macros.register('js-execute', jsCall, jsCall=jsCall)
|
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):
|
def registerDojoFormAll(self):
|
||||||
self.registerDojo()
|
self.registerDojo()
|
||||||
self.registerDojoEditor()
|
self.registerDojoEditor()
|
||||||
|
|
|
@ -282,8 +282,17 @@ class ConceptView(BaseView):
|
||||||
def breadcrumbsTitle(self):
|
def breadcrumbsTitle(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def showInBreadcrumbs(self):
|
||||||
|
return (self.options('show_in_breadcrumbs') or
|
||||||
|
self.typeOptions('show_in_breadcrumbs'))
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def breadcrumbsParent(self):
|
def breadcrumbsParent(self):
|
||||||
|
for p in self.context.getParents([self.defaultPredicate]):
|
||||||
|
view = self.nodeView.getViewForTarget(p)
|
||||||
|
if view.showInBreadcrumbs:
|
||||||
|
return view
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def getData(self, omit=('title', 'description')):
|
def getData(self, omit=('title', 'description')):
|
||||||
|
@ -449,7 +458,7 @@ class ConceptView(BaseView):
|
||||||
if r.order != pos:
|
if r.order != pos:
|
||||||
r.order = pos
|
r.order = pos
|
||||||
|
|
||||||
def getResources(self):
|
def getResources(self, relView=None, sort='default'):
|
||||||
form = self.request.form
|
form = self.request.form
|
||||||
#if form.get('loops.viewName') == 'index.html' and self.editable:
|
#if form.get('loops.viewName') == 'index.html' and self.editable:
|
||||||
if self.editable:
|
if self.editable:
|
||||||
|
@ -458,13 +467,17 @@ class ConceptView(BaseView):
|
||||||
tokens = form.get('resources_tokens')
|
tokens = form.get('resources_tokens')
|
||||||
if tokens:
|
if tokens:
|
||||||
self.reorderResources(tokens)
|
self.reorderResources(tokens)
|
||||||
from loops.browser.resource import ResourceRelationView
|
if relView is None:
|
||||||
|
from loops.browser.resource import ResourceRelationView
|
||||||
|
relView = ResourceRelationView
|
||||||
from loops.organize.personal.browser.filter import FilterView
|
from loops.organize.personal.browser.filter import FilterView
|
||||||
fv = FilterView(self.context, self.request)
|
fv = FilterView(self.context, self.request)
|
||||||
rels = self.context.getResourceRelations()
|
rels = self.context.getResourceRelations(sort=sort)
|
||||||
for r in rels:
|
for r in rels:
|
||||||
if fv.check(r.first):
|
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):
|
def resources(self):
|
||||||
return self.getResources()
|
return self.getResources()
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
<h1 tal:define="tabview item/tabview|nothing"
|
<h1 tal:define="tabview item/tabview|nothing"
|
||||||
tal:attributes="ondblclick item/openEditWindow">
|
tal:attributes="ondblclick item/openEditWindow">
|
||||||
<a tal:omit-tag="python: level > 1"
|
<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>
|
tal:content="item/title">Title</a>
|
||||||
<a title="Show tabular view"
|
<a title="Show tabular view"
|
||||||
i18n:attributes="title"
|
i18n:attributes="title"
|
||||||
|
@ -362,4 +362,21 @@
|
||||||
</metal:actions>
|
</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>
|
</html>
|
||||||
|
|
|
@ -561,6 +561,14 @@
|
||||||
factory="loops.browser.concept.TabbedPage"
|
factory="loops.browser.concept.TabbedPage"
|
||||||
permission="zope.View" />
|
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) -->
|
<!-- dialogs/forms (end-user views) -->
|
||||||
|
|
||||||
<page
|
<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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -20,12 +20,13 @@
|
||||||
Classes for form presentation and processing.
|
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 import component, interface, schema
|
||||||
from zope.component import adapts
|
from zope.component import adapts
|
||||||
from zope.event import notify
|
from zope.event import notify
|
||||||
from zope.interface import Interface
|
from zope.interface import Interface
|
||||||
from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
|
from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
|
||||||
|
|
||||||
from zope.app.container.interfaces import INameChooser
|
from zope.app.container.interfaces import INameChooser
|
||||||
from zope.app.container.contained import ObjectAddedEvent
|
from zope.app.container.contained import ObjectAddedEvent
|
||||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
|
@ -35,7 +36,7 @@ from zope.publisher.browser import FileUpload
|
||||||
from zope.publisher.interfaces import BadRequest
|
from zope.publisher.interfaces import BadRequest
|
||||||
from zope.security.interfaces import ForbiddenAttribute, Unauthorized
|
from zope.security.interfaces import ForbiddenAttribute, Unauthorized
|
||||||
from zope.security.proxy import isinstance, removeSecurityProxy
|
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.ajax import innerHtml
|
||||||
from cybertools.browser.form import FormController
|
from cybertools.browser.form import FormController
|
||||||
|
@ -68,6 +69,25 @@ from loops.util import _
|
||||||
from loops.versioning.interfaces import IVersionable
|
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
|
# forms
|
||||||
|
|
||||||
class ObjectForm(NodeView):
|
class ObjectForm(NodeView):
|
||||||
|
@ -196,15 +216,37 @@ class ObjectForm(NodeView):
|
||||||
def typeManager(self):
|
def typeManager(self):
|
||||||
return ITypeManager(self.target)
|
return ITypeManager(self.target)
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def targetType(self):
|
||||||
|
return self.target.getType()
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def presetTypesForAssignment(self):
|
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]
|
assigned = [r.context.conceptType for r in self.assignments]
|
||||||
types = [t for t in types if t.typeProvider not in assigned]
|
types = [t for t in types if t.typeProvider not in assigned]
|
||||||
return [dict(title=t.title, token=t.tokenForSearch) for t in types]
|
return [dict(title=t.title, token=t.tokenForSearch) for t in types]
|
||||||
|
|
||||||
def conceptsForType(self, token):
|
def conceptsForType(self, token):
|
||||||
result = ConceptQuery(self).query(type=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)
|
fv = FilterView(self.context, self.request)
|
||||||
result = fv.apply(result)
|
result = fv.apply(result)
|
||||||
result.sort(key=lambda x: x.title)
|
result.sort(key=lambda x: x.title)
|
||||||
|
@ -288,8 +330,11 @@ class CreateObjectForm(ObjectForm):
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def defaultTypeToken(self):
|
def defaultTypeToken(self):
|
||||||
return (self.controller.params.get('form.create.defaultTypeToken')
|
setting = self.controller.params.get('form.create.defaultTypeToken')
|
||||||
or '.loops/concepts/textdocument')
|
if setting:
|
||||||
|
return setting
|
||||||
|
opt = self.globalOptions('form.create.default_type_token')
|
||||||
|
return opt and opt[0] or '.loops/concepts/textdocument'
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def typeToken(self):
|
def typeToken(self):
|
||||||
|
@ -310,6 +355,10 @@ class CreateObjectForm(ObjectForm):
|
||||||
if typeToken:
|
if typeToken:
|
||||||
return self.loopsRoot.loopsTraverse(typeToken)
|
return self.loopsRoot.loopsTraverse(typeToken)
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def targetType(self):
|
||||||
|
return self.typeConcept
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def adapted(self):
|
def adapted(self):
|
||||||
ad = self.typeInterface(Resource())
|
ad = self.typeInterface(Resource())
|
||||||
|
@ -423,6 +472,7 @@ class CreateConceptForm(CreateObjectForm):
|
||||||
return c
|
return c
|
||||||
ad = ti(c)
|
ad = ti(c)
|
||||||
ad.__is_dummy__ = True
|
ad.__is_dummy__ = True
|
||||||
|
ad.__type__ = adapted(self.typeConcept)
|
||||||
return ad
|
return ad
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
|
|
|
@ -238,18 +238,21 @@ fieldset.box td {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #444;
|
color: #444;
|
||||||
padding-top: 0.4em;
|
padding-top: 0.4em;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-4 h1, .content-3 h2, .content-2 h3, .content-1 h4, h4 {
|
.content-4 h1, .content-3 h2, .content-2 h3, .content-1 h4, h4 {
|
||||||
font-size: 130%;
|
font-size: 130%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
padding-top: 0.3em;
|
padding-top: 0.3em;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-5 h1, .content-4 h2, .content-3 h3, content-2 h4, h5 {
|
.content-5 h1, .content-4 h2, .content-3 h3, content-2 h4, h5 {
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
/* border: none; */
|
/* border: none; */
|
||||||
padding-top: 0.2em;
|
padding-top: 0.2em;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.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) {
|
function destroyWidgets(node) {
|
||||||
dojo.forEach(dojo.query('[widgetId]', node), function(n) {
|
dojo.forEach(dojo.query('[widgetId]', node), function(n) {
|
||||||
w = dijit.byNode(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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -110,7 +110,7 @@ class NodeView(BaseView):
|
||||||
return []
|
return []
|
||||||
menu = self.menu
|
menu = self.menu
|
||||||
data = [dict(label=menu.title, url=menu.url)]
|
data = [dict(label=menu.title, url=menu.url)]
|
||||||
menuItem = self.nearestMenuItem
|
menuItem = self.getNearestMenuItem(all=True)
|
||||||
if menuItem != menu.context:
|
if menuItem != menu.context:
|
||||||
data.append(dict(label=menuItem.title,
|
data.append(dict(label=menuItem.title,
|
||||||
url=absoluteURL(menuItem, self.request)))
|
url=absoluteURL(menuItem, self.request)))
|
||||||
|
@ -121,6 +121,9 @@ class NodeView(BaseView):
|
||||||
url=absoluteURL(p, self.request)))
|
url=absoluteURL(p, self.request)))
|
||||||
if self.virtualTarget:
|
if self.virtualTarget:
|
||||||
data.extend(self.virtualTarget.breadcrumbs())
|
data.extend(self.virtualTarget.breadcrumbs())
|
||||||
|
if data and not '?' in data[-1]['url']:
|
||||||
|
if self.urlParamString:
|
||||||
|
data[-1]['url'] += self.urlParamString
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def viewModes(self):
|
def viewModes(self):
|
||||||
|
@ -401,10 +404,13 @@ class NodeView(BaseView):
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def nearestMenuItem(self):
|
def nearestMenuItem(self):
|
||||||
|
return self.getNearestMenuItem()
|
||||||
|
|
||||||
|
def getNearestMenuItem(self, all=False):
|
||||||
menu = self.menuObject
|
menu = self.menuObject
|
||||||
menuItem = None
|
menuItem = None
|
||||||
for p in [self.context] + self.parents:
|
for p in [self.context] + self.parents:
|
||||||
if not p.isMenuItem():
|
if not all and not p.isMenuItem():
|
||||||
menuItem = None
|
menuItem = None
|
||||||
elif menuItem is None:
|
elif menuItem is None:
|
||||||
menuItem = p
|
menuItem = p
|
||||||
|
@ -441,7 +447,7 @@ class NodeView(BaseView):
|
||||||
def targetView(self, name='index.html', methodName='show'):
|
def targetView(self, name='index.html', methodName='show'):
|
||||||
if name == 'index.html': # only when called for default view
|
if name == 'index.html': # only when called for default view
|
||||||
tv = self.viewAnnotations.get('targetView')
|
tv = self.viewAnnotations.get('targetView')
|
||||||
if tv is not None:
|
if tv is not None and callable(tv):
|
||||||
return tv()
|
return tv()
|
||||||
if '?' in name:
|
if '?' in name:
|
||||||
name, params = name.split('?', 1)
|
name, params = name.split('?', 1)
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
item nocall:target"
|
item nocall:target"
|
||||||
tal:attributes="class string:content-$level;
|
tal:attributes="class string:content-$level;
|
||||||
id id;
|
id id;
|
||||||
ondblclick python: target.openEditWindow('configure.html')">
|
ondblclick python:target.openEditWindow('configure.html')">
|
||||||
<metal:body use-macro="item/macro">
|
<metal:body use-macro="item/macro">
|
||||||
The body
|
The body
|
||||||
</metal:body>
|
</metal:body>
|
||||||
|
@ -41,17 +41,22 @@
|
||||||
|
|
||||||
|
|
||||||
<metal:body define-macro="conceptbody">
|
<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"
|
<div class="content-1" id="1"
|
||||||
tal:attributes="class string:content-$level;
|
tal:attributes="class string:content-$level;
|
||||||
id string:${view/itemNum}.body;
|
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>
|
<span tal:content="structure body">Node Body</span>
|
||||||
</div>
|
</div>
|
||||||
<tal:concepts define="item nocall:item/targetObjectView;
|
<div tal:define="item nocall:item/targetObjectView;
|
||||||
macro item/macro">
|
macro item/macro">
|
||||||
<div metal:use-macro="macro" />
|
<div tal:attributes="class string:content-$level;
|
||||||
</tal:concepts>
|
id id;">
|
||||||
|
<div metal:use-macro="macro" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</tal:body>
|
</tal:body>
|
||||||
</metal: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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -20,6 +20,7 @@
|
||||||
View class for resource objects.
|
View class for resource objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os.path
|
||||||
import urllib
|
import urllib
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
from zope import component
|
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 BaseRelationView, ConceptRelationView
|
||||||
from loops.browser.concept import ConceptConfigureView
|
from loops.browser.concept import ConceptConfigureView
|
||||||
from loops.browser.node import NodeView, node_macros
|
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 IBaseResource, IDocument, ITextDocument
|
||||||
from loops.interfaces import IMediaAsset as legacy_IMediaAsset
|
from loops.interfaces import IMediaAsset as legacy_IMediaAsset
|
||||||
from loops.interfaces import ITypeConcept
|
from loops.interfaces import ITypeConcept
|
||||||
|
@ -216,6 +217,16 @@ class ResourceView(BaseView):
|
||||||
if filename is None:
|
if filename is None:
|
||||||
filename = (adapted(self.context).localFilename or
|
filename = (adapted(self.context).localFilename or
|
||||||
getName(self.context))
|
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'):
|
if self.typeOptions('no_normalize_download_filename'):
|
||||||
filename = '"%s"' % filename
|
filename = '"%s"' % filename
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -96,6 +96,7 @@
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<metal:custom define-slot="custom_info" />
|
||||||
<metal:fields use-macro="view/comment_macros/comments" />
|
<metal:fields use-macro="view/comment_macros/comments" />
|
||||||
</div>
|
</div>
|
||||||
</metal:block>
|
</metal:block>
|
||||||
|
|
|
@ -20,6 +20,7 @@ body {
|
||||||
|
|
||||||
#portlets {
|
#portlets {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.view-modes {
|
ul.view-modes {
|
||||||
|
@ -108,6 +109,14 @@ thead th {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* printing */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.noprint {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* class-specific */
|
/* class-specific */
|
||||||
|
|
||||||
.breadcrumbs td {
|
.breadcrumbs td {
|
||||||
|
|
|
@ -11,8 +11,14 @@ body {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
/* width: 100%; */
|
width: auto;
|
||||||
width: 80%;
|
|
||||||
color: Black;
|
color: Black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
common.py
14
common.py
|
@ -231,17 +231,19 @@ class NameChooser(BaseNameChooser):
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def generateNameFromTitle(self, obj):
|
def generateNameFromTitle(self, obj):
|
||||||
title = obj.title
|
return generateNameFromTitle(obj.title)
|
||||||
if len(title) > 15:
|
|
||||||
words = title.split()
|
|
||||||
if len(words) > 1:
|
|
||||||
title = '_'.join((words[0], words[-1]))
|
|
||||||
return self.normalizeName(title)
|
|
||||||
|
|
||||||
def normalizeName(self, baseName):
|
def normalizeName(self, baseName):
|
||||||
return normalizeName(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):
|
def normalizeName(baseName):
|
||||||
specialCharacters = {
|
specialCharacters = {
|
||||||
'\xc4': 'Ae', '\xe4': 'ae', '\xd6': 'Oe', '\xf6': 'oe',
|
'\xc4': 'Ae', '\xe4': 'ae', '\xd6': 'Oe', '\xf6': 'oe',
|
||||||
|
|
|
@ -107,6 +107,7 @@ class Base(object):
|
||||||
@Lazy
|
@Lazy
|
||||||
def textResources(self):
|
def textResources(self):
|
||||||
self.images = [[]]
|
self.images = [[]]
|
||||||
|
self.otherResources = []
|
||||||
result = []
|
result = []
|
||||||
idx = 0
|
idx = 0
|
||||||
for rv in self.getResources():
|
for rv in self.getResources():
|
||||||
|
@ -115,7 +116,7 @@ class Base(object):
|
||||||
idx += 1
|
idx += 1
|
||||||
result.append(rv)
|
result.append(rv)
|
||||||
self.images.append([])
|
self.images.append([])
|
||||||
else:
|
elif rv.context.contentType.startswith('image/'):
|
||||||
self.registerDojoLightbox()
|
self.registerDojoLightbox()
|
||||||
url = self.nodeView.getUrlForTarget(rv.context)
|
url = self.nodeView.getUrlForTarget(rv.context)
|
||||||
src = '%s/mediaasset.html?v=small' % url
|
src = '%s/mediaasset.html?v=small' % url
|
||||||
|
@ -123,6 +124,8 @@ class Base(object):
|
||||||
img = dict(src=src, fullImageUrl=fullSrc, title=rv.title,
|
img = dict(src=src, fullImageUrl=fullSrc, title=rv.title,
|
||||||
description=rv.description, url=url, object=rv)
|
description=rv.description, url=url, object=rv)
|
||||||
self.images[idx].append(img)
|
self.images[idx].append(img)
|
||||||
|
else:
|
||||||
|
self.otherResources.append(rv)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def getDocumentTypeForResource(self, r):
|
def getDocumentTypeForResource(self, r):
|
||||||
|
|
|
@ -130,7 +130,8 @@
|
||||||
|
|
||||||
<metal:topic define-macro="topic"
|
<metal:topic define-macro="topic"
|
||||||
tal:define="children children|python:list(item.children());
|
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" />
|
<metal:info use-macro="view/concept_macros/concepttitle" />
|
||||||
<h2 i18n:translate=""
|
<h2 i18n:translate=""
|
||||||
tal:condition="children">Children</h2>
|
tal:condition="children">Children</h2>
|
||||||
|
@ -151,8 +152,18 @@
|
||||||
<a i18n:translate=""
|
<a i18n:translate=""
|
||||||
tal:attributes="href python:view.getUrlForTarget(related.context)">
|
tal:attributes="href python:view.getUrlForTarget(related.context)">
|
||||||
more...</a></p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
<metal:info use-macro="view/concept_macros/conceptresources" />
|
||||||
</metal:topic>
|
</metal:topic>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ configuration):
|
||||||
>>> concepts, resources, views = t.setup()
|
>>> concepts, resources, views = t.setup()
|
||||||
|
|
||||||
>>> len(concepts) + len(resources)
|
>>> len(concepts) + len(resources)
|
||||||
36
|
38
|
||||||
|
|
||||||
>>> loopsRoot = site['loops']
|
>>> loopsRoot = site['loops']
|
||||||
|
|
||||||
|
@ -47,11 +47,11 @@ Type- and text-based queries
|
||||||
>>> from loops.expert import query
|
>>> from loops.expert import query
|
||||||
>>> qu = query.Title('ty*')
|
>>> qu = query.Title('ty*')
|
||||||
>>> list(qu.apply())
|
>>> list(qu.apply())
|
||||||
[0, 2, 65]
|
[0, 2, 70]
|
||||||
|
|
||||||
>>> qu = query.Type('loops:*')
|
>>> qu = query.Type('loops:*')
|
||||||
>>> len(list(qu.apply()))
|
>>> len(list(qu.apply()))
|
||||||
36
|
38
|
||||||
|
|
||||||
>>> qu = query.Type('loops:concept:predicate')
|
>>> qu = query.Type('loops:concept:predicate')
|
||||||
>>> len(list(qu.apply()))
|
>>> len(list(qu.apply()))
|
||||||
|
|
|
@ -91,4 +91,18 @@
|
||||||
factory="loops.expert.browser.report.ResultsConceptView"
|
factory="loops.expert.browser.report.ResultsConceptView"
|
||||||
permission="zope.View" />
|
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>
|
</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 metal:define-macro="main">
|
||||||
<div tal:define="report item/reportInstance;
|
<div tal:define="report item/reportInstance;
|
||||||
reportView nocall:item"
|
reportView nocall:item;
|
||||||
|
renderer item/resultsRenderer"
|
||||||
tal:attributes="class string:content-$level;">
|
tal:attributes="class string:content-$level;">
|
||||||
<div metal:use-macro="item/report_macros/header" />
|
<div metal:use-macro="item/report_macros/header" />
|
||||||
<div metal:use-macro="item/resultsRenderer" />
|
<div metal:use-macro="item/resultsRenderer" />
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
<div metal:define-macro="buttons">
|
<div metal:define-macro="buttons">
|
||||||
<input type="submit" name="report_execute" value="Execute Report"
|
<input type="submit" name="report_execute" value="Execute Report"
|
||||||
tal:attributes="value item/reportExecuteTitle|string:Execute Report"
|
tal:attributes="value item/reportExecuteTitle|string:Execute Report"
|
||||||
|
tal:condition="item/queryFields"
|
||||||
i18n:attributes="value" />
|
i18n:attributes="value" />
|
||||||
<input type="submit"
|
<input type="submit"
|
||||||
tal:condition="item/reportDownload"
|
tal:condition="item/reportDownload"
|
||||||
|
@ -46,6 +48,13 @@
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
</form>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,8 @@ class ReportView(ConceptView):
|
||||||
""" A view for defining (editing) a report.
|
""" A view for defining (editing) a report.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
resultsRenderer = None # to be defined by subclass
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def report_macros(self):
|
def report_macros(self):
|
||||||
return self.controller.mergeTemplateMacros('report', report_template)
|
return self.controller.mergeTemplateMacros('report', report_template)
|
||||||
|
@ -59,6 +61,25 @@ class ReportView(ConceptView):
|
||||||
def dynamicParams(self):
|
def dynamicParams(self):
|
||||||
return self.request.form
|
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):
|
class ResultsView(NodeView):
|
||||||
|
|
||||||
|
@ -107,13 +128,6 @@ class ResultsView(NodeView):
|
||||||
def report(self):
|
def report(self):
|
||||||
return adapted(self.virtualTargetObject)
|
return adapted(self.virtualTargetObject)
|
||||||
|
|
||||||
@Lazy
|
|
||||||
def reportInstance(self):
|
|
||||||
instance = component.getAdapter(self.report, IReportInstance,
|
|
||||||
name=self.report.reportType)
|
|
||||||
instance.view = self
|
|
||||||
return instance
|
|
||||||
|
|
||||||
#@Lazy
|
#@Lazy
|
||||||
def results(self):
|
def results(self):
|
||||||
return self.reportInstance.getResults(self.params)
|
return self.reportInstance.getResults(self.params)
|
||||||
|
@ -193,6 +207,13 @@ class ResultsConceptView(ConceptView):
|
||||||
ri = component.getAdapter(self.report, IReportInstance,
|
ri = component.getAdapter(self.report, IReportInstance,
|
||||||
name=reportType)
|
name=reportType)
|
||||||
ri.view = self
|
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
|
return ri
|
||||||
|
|
||||||
def results(self):
|
def results(self):
|
||||||
|
@ -207,6 +228,31 @@ class ResultsConceptView(ConceptView):
|
||||||
def getColumnRenderer(self, col):
|
def getColumnRenderer(self, col):
|
||||||
return self.result_macros[col.renderer]
|
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):
|
class ReportConceptView(ResultsConceptView, ReportView):
|
||||||
""" View on a concept using a report.
|
""" View on a concept using a report.
|
||||||
|
|
|
@ -25,29 +25,61 @@
|
||||||
</div>
|
</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"
|
<table class="report"
|
||||||
tal:define="results reportView/results">
|
tal:define="results reportView/results">
|
||||||
<tr>
|
<tr>
|
||||||
<th tal:repeat="col results/displayedColumns"
|
<th style="white-space: nowrap"
|
||||||
tal:content="col/title"
|
tal:repeat="col results/displayedColumns">
|
||||||
tal:attributes="class col/cssClass"
|
<a title="tooltip_sort_column"
|
||||||
i18n:translate="" />
|
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>
|
||||||
<tr tal:repeat="row results">
|
<tr tal:repeat="row results"
|
||||||
<td tal:repeat="col results/displayedColumns"
|
tal:attributes="class python:(repeat['row'].index() % 2) and 'even' or 'odd'">
|
||||||
tal:attributes="class col/cssClass">
|
<td tal:repeat="col results/displayedColumns"
|
||||||
<metal:column use-macro="python:
|
tal:attributes="class col/cssClass">
|
||||||
reportView.getColumnRenderer(col)" />
|
<metal:column use-macro="python:
|
||||||
</td>
|
reportView.getColumnRenderer(col)" />
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr tal:define="row nocall:results/totals"
|
<tr tal:define="row nocall:results/totals"
|
||||||
tal:condition="nocall:row">
|
tal:condition="nocall:row">
|
||||||
<td tal:repeat="col results/displayedColumns"
|
<td tal:repeat="col results/displayedColumns"
|
||||||
tal:attributes="class col/cssClass">
|
tal:attributes="class col/cssClass">
|
||||||
<metal:column use-macro="python:
|
<metal:column use-macro="python:
|
||||||
reportView.getColumnRenderer(col)" />
|
reportView.getColumnRenderer(col)" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,6 +110,17 @@
|
||||||
</metal:state>
|
</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">
|
<metal:target define-macro="target">
|
||||||
<tal:column define="value python:col.getDisplayValue(row)">
|
<tal:column define="value python:col.getDisplayValue(row)">
|
||||||
<a tal:omit-tag="python:
|
<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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -91,7 +91,8 @@ class Search(ConceptView):
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def showActions(self):
|
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)
|
#return canWriteObject(self.context)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -166,12 +167,10 @@ class Search(ConceptView):
|
||||||
title = request.get('name')
|
title = request.get('name')
|
||||||
if title == '*':
|
if title == '*':
|
||||||
title = None
|
title = None
|
||||||
types = request.get('searchType')
|
#types = request.get('searchType')
|
||||||
data = []
|
data = []
|
||||||
types = self.getTypes()
|
types = self.getTypes()
|
||||||
if title or types:
|
if title or (types and types != u'loops:concept:*'):
|
||||||
#if title or (types and types not in
|
|
||||||
# (u'loops:concept:*', 'loops:concept:account')):
|
|
||||||
if title is not None:
|
if title is not None:
|
||||||
title = title.replace('(', ' ').replace(')', ' ').replace(' -', ' ')
|
title = title.replace('(', ' ').replace(')', ' ').replace(' -', ' ')
|
||||||
#title = title.split(' ', 1)[0]
|
#title = title.split(' ', 1)[0]
|
||||||
|
@ -302,8 +301,8 @@ class Search(ConceptView):
|
||||||
for state in states:
|
for state in states:
|
||||||
if stf.state == state:
|
if stf.state == state:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -22,6 +22,7 @@ Field definitions for reports.
|
||||||
|
|
||||||
from zope.app.form.browser.interfaces import ITerms
|
from zope.app.form.browser.interfaces import ITerms
|
||||||
from zope import component
|
from zope import component
|
||||||
|
from zope.i18n import translate
|
||||||
from zope.i18n.locales import locales
|
from zope.i18n.locales import locales
|
||||||
from zope.schema.interfaces import IVocabularyFactory, IContextSourceBinder
|
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.field import TableCellStyle
|
||||||
from cybertools.composer.report.result import ResultSet
|
from cybertools.composer.report.result import ResultSet
|
||||||
from cybertools.stateful.interfaces import IStateful, IStatesDefinition
|
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.common import baseObject
|
||||||
from loops.expert.report import ReportInstance
|
from loops.expert.report import ReportInstance
|
||||||
|
from loops.organize.work.browser import WorkItemDetails
|
||||||
from loops import util
|
from loops import util
|
||||||
|
|
||||||
|
|
||||||
class Field(BaseField):
|
class Field(BaseField):
|
||||||
|
|
||||||
|
def getContext(self, row):
|
||||||
|
return row.context
|
||||||
|
|
||||||
def getSelectValue(self, row):
|
def getSelectValue(self, row):
|
||||||
return self.getValue(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):
|
class TextField(Field):
|
||||||
|
|
||||||
format = 'text/restructured'
|
format = 'text/restructured'
|
||||||
|
@ -109,6 +124,11 @@ class DateField(Field):
|
||||||
renderer = cssClass = 'center'
|
renderer = cssClass = 'center'
|
||||||
dbtype = 'date'
|
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):
|
def getDisplayValue(self, row):
|
||||||
value = self.getRawValue(row)
|
value = self.getRawValue(row)
|
||||||
if not value:
|
if not value:
|
||||||
|
@ -127,16 +147,17 @@ class DateField(Field):
|
||||||
|
|
||||||
class StateField(Field):
|
class StateField(Field):
|
||||||
|
|
||||||
statesDefinition = 'workItemStates'
|
statesDefinition = None
|
||||||
renderer = 'state'
|
renderer = 'state'
|
||||||
|
|
||||||
def getDisplayValue(self, row):
|
def getDisplayValue(self, row):
|
||||||
if IStateful.providedBy(row.context):
|
context = self.getContext(row)
|
||||||
stf = row.context
|
if IStateful.providedBy(context):
|
||||||
elif row.context is None:
|
stf = context
|
||||||
|
elif context is None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
stf = component.getAdapter(baseObject(row.context), IStateful,
|
stf = component.getAdapter(context, IStateful,
|
||||||
name=self.statesDefinition)
|
name=self.statesDefinition)
|
||||||
stateObject = stf.getStateObject()
|
stateObject = stf.getStateObject()
|
||||||
icon = stateObject.icon or 'led%s.png' % stateObject.color
|
icon = stateObject.icon or 'led%s.png' % stateObject.color
|
||||||
|
@ -147,6 +168,27 @@ class StateField(Field):
|
||||||
return util._(text)
|
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):
|
class VocabularyField(Field):
|
||||||
|
|
||||||
vocabulary = None
|
vocabulary = None
|
||||||
|
@ -233,6 +275,14 @@ class RelationField(Field):
|
||||||
|
|
||||||
class TargetField(RelationField):
|
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):
|
def getValue(self, row):
|
||||||
value = self.getRawValue(row)
|
value = self.getRawValue(row)
|
||||||
if value is None:
|
if value is None:
|
||||||
|
@ -247,6 +297,57 @@ class MultiLineField(Field):
|
||||||
def getValue(self, row):
|
def getValue(self, row):
|
||||||
return self.getRawValue(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):
|
def getDisplayValues(self, row):
|
||||||
value = self.getValue(row)
|
value = self.getValue(row)
|
||||||
if not isinstance(value, (list, tuple)):
|
if not isinstance(value, (list, tuple)):
|
||||||
|
|
|
@ -88,6 +88,7 @@ class ReportInstance(BaseReport):
|
||||||
#headerRowFactory = Row
|
#headerRowFactory = Row
|
||||||
|
|
||||||
view = None # set upon creation
|
view = None # set upon creation
|
||||||
|
#headerRowFactory = Row
|
||||||
|
|
||||||
def __init__(self, context):
|
def __init__(self, context):
|
||||||
self.context = context
|
self.context = context
|
||||||
|
@ -120,7 +121,9 @@ class ReportInstance(BaseReport):
|
||||||
result = list(self.selectObjects(parts)) # may modify parts
|
result = list(self.selectObjects(parts)) # may modify parts
|
||||||
qc = CompoundQueryCriteria(parts)
|
qc = CompoundQueryCriteria(parts)
|
||||||
return ResultSet(self, result, rowFactory=self.rowFactory,
|
return ResultSet(self, result, rowFactory=self.rowFactory,
|
||||||
sortCriteria=self.getSortCriteria(), queryCriteria=qc,
|
sortCriteria=self.getSortCriteria(),
|
||||||
|
sortDescending=self.sortDescending,
|
||||||
|
queryCriteria=qc,
|
||||||
limits=limits)
|
limits=limits)
|
||||||
|
|
||||||
def selectObjects(self, parts):
|
def selectObjects(self, parts):
|
||||||
|
@ -173,3 +176,15 @@ class DefaultConceptReportInstance(ReportInstance):
|
||||||
|
|
||||||
label = u'Default Concept Report'
|
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()
|
>>> t = searchView.typesForSearch()
|
||||||
>>> len(t)
|
>>> len(t)
|
||||||
15
|
16
|
||||||
>>> t.getTermByToken('loops:resource:*').title
|
>>> t.getTermByToken('loops:resource:*').title
|
||||||
'Any Resource'
|
'Any Resource'
|
||||||
|
|
||||||
>>> t = searchView.conceptTypesForSearch()
|
>>> t = searchView.conceptTypesForSearch()
|
||||||
>>> len(t)
|
>>> len(t)
|
||||||
12
|
13
|
||||||
>>> t.getTermByToken('loops:concept:*').title
|
>>> t.getTermByToken('loops:concept:*').title
|
||||||
'Any Concept'
|
'Any Concept'
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ a controller attribute for the search view.
|
||||||
|
|
||||||
>>> searchView.submitReplacing('1.results', '1.search.form', pageView)
|
>>> searchView.submitReplacing('1.results', '1.search.form', pageView)
|
||||||
'submitReplacing("1.results", "1.search.form",
|
'submitReplacing("1.results", "1.search.form",
|
||||||
"http://127.0.0.1/loops/views/page/.target96/@@searchresults.html");...'
|
"http://127.0.0.1/loops/views/page/.target.../@@searchresults.html");...'
|
||||||
|
|
||||||
Basic (text/title) search
|
Basic (text/title) search
|
||||||
-------------------------
|
-------------------------
|
||||||
|
@ -177,7 +177,7 @@ of the concepts' titles:
|
||||||
>>> request = TestRequest(form=form)
|
>>> request = TestRequest(form=form)
|
||||||
>>> view = Search(page, request)
|
>>> view = Search(page, request)
|
||||||
>>> view.listConcepts()
|
>>> view.listConcepts()
|
||||||
u"{identifier: 'id', items: [{label: 'Zope (Thema)', name: 'Zope', id: '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
|
Preset Concept Types on Search Forms
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
@ -219,13 +219,13 @@ and thus include the customer type in the preset search types.
|
||||||
|
|
||||||
>>> searchView.conceptsForType('loops:concept:customer')
|
>>> searchView.conceptsForType('loops:concept:customer')
|
||||||
[{'token': 'none', 'title': u'not selected'},
|
[{'token': 'none', 'title': u'not selected'},
|
||||||
{'token': '74', 'title': u'Customer 1'},
|
{'token': '...', 'title': u'Customer 1'},
|
||||||
{'token': '76', 'title': u'Customer 2'},
|
{'token': '...', 'title': u'Customer 2'},
|
||||||
{'token': '78', 'title': u'Customer 3'}]
|
{'token': '...', 'title': u'Customer 3'}]
|
||||||
|
|
||||||
Let's use this new search option for querying:
|
Let's use this new search option for querying:
|
||||||
|
|
||||||
>>> form = {'search.4.text_selected': u'74'}
|
>>> form = {'search.4.text_selected': u'75'}
|
||||||
>>> resultsView = SearchResults(page, TestRequest(form=form))
|
>>> resultsView = SearchResults(page, TestRequest(form=form))
|
||||||
>>> results = list(resultsView.results)
|
>>> results = list(resultsView.results)
|
||||||
>>> results[0].title
|
>>> results[0].title
|
||||||
|
|
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()
|
>>> concepts, resources, views = t.setup()
|
||||||
>>> loopsRoot = site['loops']
|
>>> loopsRoot = site['loops']
|
||||||
>>> len(concepts), len(resources), len(views)
|
>>> len(concepts), len(resources), len(views)
|
||||||
(33, 3, 1)
|
(35, 3, 1)
|
||||||
|
|
||||||
|
|
||||||
Importing loops Objects
|
Importing loops Objects
|
||||||
|
@ -44,7 +44,7 @@ Creating the corresponding objects
|
||||||
>>> loader = Loader(loopsRoot)
|
>>> loader = Loader(loopsRoot)
|
||||||
>>> loader.load(elements)
|
>>> loader.load(elements)
|
||||||
>>> len(concepts), len(resources), len(views)
|
>>> len(concepts), len(resources), len(views)
|
||||||
(34, 3, 1)
|
(36, 3, 1)
|
||||||
|
|
||||||
>>> from loops.common import adapted
|
>>> from loops.common import adapted
|
||||||
>>> adMyquery = adapted(concepts['myquery'])
|
>>> adMyquery = adapted(concepts['myquery'])
|
||||||
|
@ -131,7 +131,7 @@ Extracting elements
|
||||||
>>> extractor = Extractor(loopsRoot, os.path.join(dataDirectory, 'export'))
|
>>> extractor = Extractor(loopsRoot, os.path.join(dataDirectory, 'export'))
|
||||||
>>> elements = list(extractor.extract())
|
>>> elements = list(extractor.extract())
|
||||||
>>> len(elements)
|
>>> len(elements)
|
||||||
66
|
69
|
||||||
|
|
||||||
Writing object information to the external storage
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,8 +18,6 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Integrator interfaces.
|
Integrator interfaces.
|
||||||
|
|
||||||
$Id$
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope.interface import Interface, Attribute
|
from zope.interface import Interface, Attribute
|
||||||
|
@ -133,3 +131,5 @@ class IOfficeFile(IExternalFile):
|
||||||
It provides access to the document content and properties.
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -26,7 +26,7 @@ from lxml import etree
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from time import strptime
|
from time import strptime
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile, BadZipfile
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
from zope import component
|
from zope import component
|
||||||
from zope.component import adapts
|
from zope.component import adapts
|
||||||
|
@ -52,12 +52,22 @@ class OfficeFile(ExternalFileAdapter):
|
||||||
|
|
||||||
implements(IOfficeFile)
|
implements(IOfficeFile)
|
||||||
|
|
||||||
|
_adapterAttributes = (ExternalFileAdapter._adapterAttributes +
|
||||||
|
('documentPropertiesAccessible',))
|
||||||
|
|
||||||
propertyMap = {u'Revision:': 'version'}
|
propertyMap = {u'Revision:': 'version'}
|
||||||
propFileName = 'docProps/custom.xml'
|
propFileName = 'docProps/custom.xml'
|
||||||
corePropFileName = 'docProps/core.xml'
|
corePropFileName = 'docProps/core.xml'
|
||||||
fileExtensions = ('.docm', '.docx', 'dotm', 'dotx', 'pptx', 'potx', 'ppsx',
|
fileExtensions = ('.docm', '.docx', 'dotm', 'dotx', 'pptx', 'potx', 'ppsx',
|
||||||
'.xlsm', '.xlsx', '.xltm', '.xltx')
|
'.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
|
@Lazy
|
||||||
def logger(self):
|
def logger(self):
|
||||||
return getLogger('loops.integrator.office.base.OfficeFile')
|
return getLogger('loops.integrator.office.base.OfficeFile')
|
||||||
|
@ -79,14 +89,19 @@ class OfficeFile(ExternalFileAdapter):
|
||||||
def docPropertyDom(self):
|
def docPropertyDom(self):
|
||||||
fn = self.docFilename
|
fn = self.docFilename
|
||||||
result = dict(core=[], custom=[])
|
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)
|
root, ext = os.path.splitext(fn)
|
||||||
if not ext.lower() in self.fileExtensions:
|
if not ext.lower() in self.fileExtensions:
|
||||||
return result
|
return result
|
||||||
try:
|
try:
|
||||||
zf = ZipFile(fn, 'r')
|
zf = ZipFile(fn, 'r')
|
||||||
except IOError, e:
|
self.documentPropertiesAccessible = True
|
||||||
|
except (IOError, BadZipfile), e:
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
self.logger.warn(e)
|
self.logger.warn(e)
|
||||||
|
self.documentPropertiesAccessible = False
|
||||||
return result
|
return result
|
||||||
if self.corePropFileName not in zf.namelist():
|
if self.corePropFileName not in zf.namelist():
|
||||||
self.logger.warn('Core properties not found in file %s.' %
|
self.logger.warn('Core properties not found in file %s.' %
|
||||||
|
@ -123,6 +138,8 @@ class OfficeFile(ExternalFileAdapter):
|
||||||
attributes = {}
|
attributes = {}
|
||||||
# get dc:description from core.xml
|
# get dc:description from core.xml
|
||||||
desc = self.getCoreProperty('description')
|
desc = self.getCoreProperty('description')
|
||||||
|
if not self.documentPropertiesAccessible:
|
||||||
|
return
|
||||||
if desc is not None:
|
if desc is not None:
|
||||||
attributes['comments'] = desc
|
attributes['comments'] = desc
|
||||||
dom = self.docPropertyDom['custom']
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -30,8 +30,13 @@ from cybertools.typology.interfaces import IType
|
||||||
from loops.browser.action import DialogAction
|
from loops.browser.action import DialogAction
|
||||||
from loops.browser.common import BaseView
|
from loops.browser.common import BaseView
|
||||||
from loops.browser.concept import ConceptView
|
from loops.browser.concept import ConceptView
|
||||||
|
from loops.common import adapted
|
||||||
from loops.knowledge.interfaces import IPerson, ITask
|
from loops.knowledge.interfaces import IPerson, ITask
|
||||||
from loops.organize.party import getPersonForUser
|
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 _
|
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):
|
class MyKnowledge(ConceptView):
|
||||||
|
|
||||||
template = template
|
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',
|
typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',
|
||||||
options=u'action.portlet:create_subtype,edit_concept')
|
options=u'action.portlet:create_subtype,edit_concept')
|
||||||
type(u'person', u'Person', viewName=u'',
|
type(u'person', u'Person', viewName=u'',
|
||||||
typeInterface=u'loops.knowledge.interfaces.IPerson',
|
typeInterface=u'loops.knowledge.interfaces.IPerson',
|
||||||
options=u'action.portlet:createQualification,editPerson')
|
options=u'action.portlet:createQualification,editPerson')
|
||||||
|
type(u'report', u'Report', viewName=u'',
|
||||||
|
typeInterface='loops.expert.report.IReport')
|
||||||
type(u'task', u'Aufgabe', viewName=u'',
|
type(u'task', u'Aufgabe', viewName=u'',
|
||||||
typeInterface=u'loops.knowledge.interfaces.ITask',
|
typeInterface=u'loops.knowledge.interfaces.ITask',
|
||||||
options=u'action.portlet:createTask,editTask')
|
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',
|
concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children',
|
||||||
predicateInterface='loops.interfaces.IIsSubtype')
|
predicateInterface='loops.interfaces.IIsSubtype')
|
||||||
|
|
||||||
|
# reports
|
||||||
|
concept(u'qualification_overview', u'Qualification Overview', u'report',
|
||||||
|
reportType=u'qualification_overview')
|
||||||
|
|
||||||
# structure
|
# structure
|
||||||
child(u'general', u'competence', u'standard')
|
child(u'general', u'competence', u'standard')
|
||||||
child(u'general', u'depends', 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'general', u'training', u'standard')
|
||||||
|
|
||||||
child(u'system', u'issubtype', 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'competence', u'issubtype')
|
||||||
#child(u'competence', u'training', u'issubtype', usePredicate=u'provides')
|
#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',
|
typeInterface=u'loops.knowledge.qualification.interfaces.ICompetence',
|
||||||
options=u'action.portlet:create_subtype,edit_concept')
|
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'',
|
# type(u'person', u'Person', viewName=u'',
|
||||||
# typeInterface=u'loops.knowledge.interfaces.IPerson',
|
# typeInterface=u'loops.knowledge.interfaces.IPerson',
|
||||||
# options=u'action.portlet:editPerson')
|
# options=u'action.portlet:editPerson')
|
||||||
|
@ -26,9 +34,16 @@ concept(u'requires', u'requires', u'predicate')
|
||||||
concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children',
|
concept(u'issubtype', u'is Subtype', u'predicate', options=u'hide_children',
|
||||||
predicateInterface='loops.interfaces.IIsSubtype')
|
predicateInterface='loops.interfaces.IIsSubtype')
|
||||||
|
|
||||||
|
# reports
|
||||||
|
concept(u'qualification_overview', u'Qualification Overview', u'report',
|
||||||
|
reportType=u'qualification_overview')
|
||||||
|
|
||||||
# structure
|
# structure
|
||||||
child(u'general', u'competence', u'standard')
|
child(u'general', u'competence', u'standard')
|
||||||
child(u'general', u'depends', 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'knows', u'standard')
|
||||||
#child(u'general', u'person', u'standard')
|
#child(u'general', u'person', u'standard')
|
||||||
child(u'general', u'provides', 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'general', u'training', u'standard')
|
||||||
|
|
||||||
child(u'system', u'issubtype', 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'competence', u'issubtype')
|
||||||
#child(u'competence', u'training', u'issubtype', usePredicate=u'provides')
|
#child(u'competence', u'training', u'issubtype', usePredicate=u'provides')
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
<!-- $Id$ -->
|
|
||||||
|
|
||||||
<configure
|
<configure
|
||||||
xmlns:zope="http://namespaces.zope.org/zope"
|
xmlns:zope="http://namespaces.zope.org/zope"
|
||||||
xmlns="http://namespaces.zope.org/browser"
|
xmlns="http://namespaces.zope.org/browser"
|
||||||
|
@ -28,14 +26,14 @@
|
||||||
name="create_glossaryitem.html"
|
name="create_glossaryitem.html"
|
||||||
for="loops.interfaces.INode"
|
for="loops.interfaces.INode"
|
||||||
class="loops.knowledge.glossary.browser.CreateGlossaryItemForm"
|
class="loops.knowledge.glossary.browser.CreateGlossaryItemForm"
|
||||||
permission="zope.ManageContent"
|
permission="zope.View"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<page
|
<page
|
||||||
name="edit_glossaryitem.html"
|
name="edit_glossaryitem.html"
|
||||||
for="loops.interfaces.INode"
|
for="loops.interfaces.INode"
|
||||||
class="loops.knowledge.glossary.browser.EditGlossaryItemForm"
|
class="loops.knowledge.glossary.browser.EditGlossaryItemForm"
|
||||||
permission="zope.ManageContent"
|
permission="zope.View"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<zope:adapter
|
<zope:adapter
|
||||||
|
@ -43,7 +41,7 @@
|
||||||
for="loops.browser.node.NodeView
|
for="loops.browser.node.NodeView
|
||||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||||
factory="loops.knowledge.glossary.browser.CreateGlossaryItem"
|
factory="loops.knowledge.glossary.browser.CreateGlossaryItem"
|
||||||
permission="zope.ManageContent"
|
permission="zope.View"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<zope:adapter
|
<zope:adapter
|
||||||
|
@ -51,7 +49,7 @@
|
||||||
for="loops.browser.node.NodeView
|
for="loops.browser.node.NodeView
|
||||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||||
factory="loops.knowledge.glossary.browser.EditGlossaryItem"
|
factory="loops.knowledge.glossary.browser.EditGlossaryItem"
|
||||||
permission="zope.ManageContent"
|
permission="zope.View"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</configure>
|
</configure>
|
||||||
|
|
|
@ -1,6 +1,27 @@
|
||||||
<html i18n:domain="loops">
|
<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:providers define-macro="requirement_providers">
|
||||||
<metal:block use-macro="view/concept_macros/conceptdata" />
|
<metal:block use-macro="view/concept_macros/conceptdata" />
|
||||||
<div>
|
<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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,19 +18,25 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Definition of view classes and other browser related stuff for the
|
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 import interface, component
|
||||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
|
|
||||||
|
from loops.browser.concept import ConceptView
|
||||||
|
from loops.expert.browser.export import ResultsConceptCSVExport
|
||||||
from loops.expert.browser.report import ResultsConceptView
|
from loops.expert.browser.report import ResultsConceptView
|
||||||
from loops.knowledge.browser import template, knowledge_macros
|
from loops.organize.party import getPersonForUser
|
||||||
from loops.knowledge.qualification.base import QualificationRecord
|
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 -->
|
<!-- 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>
|
</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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -23,6 +23,7 @@ surveys and self-assessments.
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
|
import math
|
||||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
from zope.i18n import translate
|
from zope.i18n import translate
|
||||||
|
@ -31,61 +32,305 @@ from cybertools.knowledge.survey.questionnaire import Response
|
||||||
from cybertools.util.date import formatTimeStamp
|
from cybertools.util.date import formatTimeStamp
|
||||||
from loops.browser.concept import ConceptView
|
from loops.browser.concept import ConceptView
|
||||||
from loops.browser.node import NodeView
|
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.knowledge.survey.response import Responses
|
||||||
from loops.organize.party import getPersonForUser
|
from loops.organize.party import getPersonForUser
|
||||||
|
from loops.security.common import checkPermission
|
||||||
from loops.util import getObjectForUid
|
from loops.util import getObjectForUid
|
||||||
from loops.util import _
|
from loops.util import _
|
||||||
|
|
||||||
|
|
||||||
template = ViewPageTemplateFile('view_macros.pt')
|
template = ViewPageTemplateFile('view_macros.pt')
|
||||||
|
|
||||||
class SurveyView(ConceptView):
|
class SurveyView(InstitutionMixin, ConceptView):
|
||||||
|
|
||||||
data = None
|
data = None
|
||||||
errors = None
|
errors = message = None
|
||||||
|
batchSize = 12
|
||||||
|
teamData = None
|
||||||
|
|
||||||
|
template = template
|
||||||
|
|
||||||
|
adminMaySelectAllInstitutions = False
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def macro(self):
|
def macro(self):
|
||||||
self.registerDojo()
|
self.registerDojo()
|
||||||
return template.macros['survey']
|
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
|
@Lazy
|
||||||
def tabview(self):
|
def tabview(self):
|
||||||
if self.editable:
|
if self.editable:
|
||||||
return 'index.html'
|
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 = []
|
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
|
form = self.request.form
|
||||||
if 'submit' in form:
|
action = None
|
||||||
self.data = {}
|
for k in ('submit', 'save'):
|
||||||
response = Response(self.adapted, None)
|
if k in form:
|
||||||
for key, value in form.items():
|
action = k
|
||||||
if key.startswith('question_'):
|
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_'):]
|
uid = key[len('question_'):]
|
||||||
question = adapted(self.getObjectForUid(uid))
|
question = adapted(self.getObjectForUid(uid))
|
||||||
if value != 'none':
|
if value.isdigit():
|
||||||
value = int(value)
|
value = int(value)
|
||||||
self.data[uid] = value
|
data[uid] = value
|
||||||
response.values[question] = value
|
response.values[question] = value
|
||||||
Responses(self.context).save(self.data)
|
values = response.getGroupedResult()
|
||||||
self.errors = self.check(response)
|
for v in values:
|
||||||
if self.errors:
|
data[self.getUidForObject(v['group'])] = v['score']
|
||||||
return []
|
respManager.save(data)
|
||||||
if response is not None:
|
if action == 'save':
|
||||||
result = response.getGroupedResult()
|
self.message = u'Your data have been saved.'
|
||||||
return [dict(category=r[0].title, text=r[1].text,
|
return []
|
||||||
score=int(round(r[2] * 100)))
|
self.data = data
|
||||||
for r in result]
|
self.errors = self.check(response)
|
||||||
|
if self.errors:
|
||||||
|
return []
|
||||||
|
result = [dict(category=r['group'].title, text=r['feedback'].text,
|
||||||
|
score=int(round(r['score'] * 100)), rank=r['rank'])
|
||||||
|
for r in values]
|
||||||
|
if self.showTeamResults:
|
||||||
|
self.teamData = self.getTeamData(respManager)
|
||||||
|
groups = [r['group'] for r in values]
|
||||||
|
teamValues = response.getTeamResult(groups, self.teamData)
|
||||||
|
for idx, r in enumerate(teamValues):
|
||||||
|
result[idx]['average'] = int(round(r['average'] * 100))
|
||||||
|
result[idx]['teamRank'] = r['rank']
|
||||||
|
return result
|
||||||
|
|
||||||
|
def teamResults(self, report):
|
||||||
|
result = []
|
||||||
|
respManager = Responses(self.context)
|
||||||
|
self.teamData = self.getTeamData(respManager)
|
||||||
|
response = Response(self.adapted, None)
|
||||||
|
groups = self.adapted.questionGroups
|
||||||
|
teamValues = response.getTeamResult(groups, self.teamData)
|
||||||
|
for idx, r in enumerate(teamValues):
|
||||||
|
group = r['group']
|
||||||
|
item = dict(category=group.title,
|
||||||
|
average=int(round(r['average'] * 100)),
|
||||||
|
teamRank=r['rank'])
|
||||||
|
if group.feedbackItems:
|
||||||
|
wScore = r['average'] * len(group.feedbackItems) - 0.00001
|
||||||
|
item['text'] = group.feedbackItems[int(wScore)].text
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getTeamResultsForQuestion(self, question, questionnaire):
|
||||||
|
result = dict(average=0.0, stddev=0.0)
|
||||||
|
if self.teamData is None:
|
||||||
|
respManager = Responses(self.context)
|
||||||
|
self.teamData = self.getTeamData(respManager)
|
||||||
|
answerRange = question.answerRange or questionnaire.defaultAnswerRange
|
||||||
|
values = [r.values.get(question) for r in self.teamData]
|
||||||
|
values = [v for v in values if v is not None]
|
||||||
|
if values:
|
||||||
|
average = float(sum(values)) / len(values)
|
||||||
|
if question.revertAnswerOptions:
|
||||||
|
average = answerRange - average - 1
|
||||||
|
devs = [(average - v) for v in values]
|
||||||
|
stddev = math.sqrt(sum(d * d for d in devs) / len(values))
|
||||||
|
average = average * 100 / (answerRange - 1)
|
||||||
|
stddev = stddev * 100 / (answerRange - 1)
|
||||||
|
result['average'] = int(round(average))
|
||||||
|
result['stddev'] = int(round(stddev))
|
||||||
|
texts = [r.texts.get(question) for r in self.teamData]
|
||||||
|
result['texts'] = '<br />'.join([unicode(t) for t in texts if t])
|
||||||
|
return result
|
||||||
|
|
||||||
|
def prefsResults(self, respManager, form, action):
|
||||||
|
result = []
|
||||||
|
data = {}
|
||||||
|
for key, value in form.items():
|
||||||
|
if key.startswith('group_') and value:
|
||||||
|
data[value] = 1
|
||||||
|
respManager.save(data)
|
||||||
|
if action == 'save':
|
||||||
|
self.message = u'Your data have been saved.'
|
||||||
|
return []
|
||||||
|
self.data = data
|
||||||
|
#self.errors = self.check(response)
|
||||||
|
if self.errors:
|
||||||
|
return []
|
||||||
|
for group in self.adapted.questionGroups:
|
||||||
|
score = 0
|
||||||
|
for qu in group.questions:
|
||||||
|
value = data.get(qu.uid) or 0
|
||||||
|
if qu.revertAnswerOptions:
|
||||||
|
value = -value
|
||||||
|
score += value
|
||||||
|
result.append(dict(category=group.title, score=score))
|
||||||
|
return result
|
||||||
|
|
||||||
def check(self, response):
|
def check(self, response):
|
||||||
errors = []
|
errors = []
|
||||||
values = response.values
|
values = response.values
|
||||||
for qu in self.adapted.questions:
|
for qu in self.adapted.questions:
|
||||||
if qu.required and qu not in values:
|
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
|
break
|
||||||
qugroups = {}
|
qugroups = {}
|
||||||
for qugroup in self.adapted.questionGroups:
|
for qugroup in self.adapted.questionGroups:
|
||||||
|
@ -97,7 +342,12 @@ class SurveyView(ConceptView):
|
||||||
if minAnswers in (u'', None):
|
if minAnswers in (u'', None):
|
||||||
minAnswers = len(qugroup.questions)
|
minAnswers = len(qugroup.questions)
|
||||||
if count < minAnswers:
|
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
|
break
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
@ -106,7 +356,8 @@ class SurveyView(ConceptView):
|
||||||
text = qugroup.description
|
text = qugroup.description
|
||||||
info = None
|
info = None
|
||||||
if qugroup.minAnswers in (u'', 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:
|
elif qugroup.minAnswers > 0:
|
||||||
info = translate(_(u'Please answer at least $minAnswers questions.',
|
info = translate(_(u'Please answer at least $minAnswers questions.',
|
||||||
mapping=dict(minAnswers=qugroup.minAnswers)),
|
mapping=dict(minAnswers=qugroup.minAnswers)),
|
||||||
|
@ -115,16 +366,46 @@ class SurveyView(ConceptView):
|
||||||
text = u'<i>%s</i><br />(%s)' % (text, info)
|
text = u'<i>%s</i><br />(%s)' % (text, info)
|
||||||
return text
|
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):
|
def getValues(self, question):
|
||||||
setting = None
|
setting = None
|
||||||
if self.data is None:
|
self.loadData()
|
||||||
self.data = Responses(self.context).load()
|
|
||||||
if self.data:
|
if self.data:
|
||||||
setting = self.data.get(question.uid)
|
setting = self.data.get(question.uid)
|
||||||
noAnswer = [dict(value='none', checked=(setting == None),
|
if setting is None:
|
||||||
radio=(not question.required))]
|
setting = 'none'
|
||||||
return noAnswer + [dict(value=i, checked=(setting == i), radio=True)
|
setting = str(setting)
|
||||||
for i in reversed(range(question.answerRange))]
|
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):
|
class SurveyCsvExport(NodeView):
|
||||||
|
@ -132,36 +413,43 @@ class SurveyCsvExport(NodeView):
|
||||||
encoding = 'ISO8859-15'
|
encoding = 'ISO8859-15'
|
||||||
|
|
||||||
def encode(self, text):
|
def encode(self, text):
|
||||||
text.encode(self.encoding)
|
return text.encode(self.encoding)
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def questions(self):
|
def questions(self):
|
||||||
result = []
|
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):
|
for idx2, qu in enumerate(qug.questions):
|
||||||
result.append((idx1, idx2, qug, qu))
|
result.append((idx1, idx2, qug, qu))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def columns(self):
|
def columns(self):
|
||||||
infoCols = ['Name', 'Timestamp']
|
infoCols = ['Institution', 'Name', 'Timestamp']
|
||||||
dataCols = ['%02i-%02i' % (item[0], item[1]) for item in self.questions]
|
dataCols = ['%02i-%02i' % (item[0], item[1]) for item in self.questions]
|
||||||
return infoCols + dataCols
|
return infoCols + dataCols
|
||||||
|
|
||||||
def getRows(self):
|
def getRows(self):
|
||||||
|
memberPred = self.conceptManager.get('ismember')
|
||||||
for tr in Responses(self.virtualTargetObject).getAllTracks():
|
for tr in Responses(self.virtualTargetObject).getAllTracks():
|
||||||
p = adapted(getObjectForUid(tr.userName))
|
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)
|
ts = formatTimeStamp(tr.timeStamp)
|
||||||
cells = [tr.data.get(qu.uid, -1)
|
cells = [tr.data.get(qu.uid, -1)
|
||||||
for (idx1, idx2, qug, qu) in self.questions]
|
for (idx1, idx2, qug, qu) in self.questions]
|
||||||
yield [name, ts] + cells
|
yield [inst, name, ts] + cells
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
f = StringIO()
|
f = StringIO()
|
||||||
writer = csv.writer(f, delimiter=',')
|
writer = csv.writer(f, delimiter=',')
|
||||||
writer.writerow(self.columns)
|
writer.writerow(self.columns)
|
||||||
for row in self.getRows():
|
for row in sorted(self.getRows()):
|
||||||
writer.writerow(row)
|
writer.writerow(row)
|
||||||
text = f.getvalue()
|
text = f.getvalue()
|
||||||
self.setDownloadHeader(text)
|
self.setDownloadHeader(text)
|
||||||
|
|
|
@ -23,21 +23,78 @@ Interfaces for surveys used in knowledge management.
|
||||||
from zope.interface import Interface, Attribute
|
from zope.interface import Interface, Attribute
|
||||||
from zope import interface, component, schema
|
from zope import interface, component, schema
|
||||||
|
|
||||||
|
from cybertools.composer.schema.grid.interfaces import Records
|
||||||
from cybertools.knowledge.survey import interfaces
|
from cybertools.knowledge.survey import interfaces
|
||||||
from loops.interfaces import IConceptSchema, ILoopsAdapter
|
from loops.interfaces import IConceptSchema, ILoopsAdapter
|
||||||
from loops.util import _
|
from loops.util import _, KeywordVocabulary
|
||||||
|
|
||||||
|
|
||||||
class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire):
|
class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire):
|
||||||
""" A collection of questions for setting up a survey.
|
""" 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(
|
defaultAnswerRange = schema.Int(
|
||||||
title=_(u'Answer Range'),
|
title=_(u'Answer Range'),
|
||||||
description=_(u'Number of items (answer options) to select from.'),
|
description=_(u'Number of items (answer options) to select from.'),
|
||||||
default=4,
|
default=4,
|
||||||
required=True)
|
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(
|
feedbackHeader = schema.Text(
|
||||||
title=_(u'Feedback Header'),
|
title=_(u'Feedback Header'),
|
||||||
description=_(u'Text that will appear at the top of the feedback page.'),
|
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.
|
""" 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(
|
required = schema.Bool(
|
||||||
title=_(u'Required'),
|
title=_(u'Required'),
|
||||||
description=_(u'Question must be answered.'),
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -34,18 +34,32 @@ class Responses(BaseRecordManager):
|
||||||
implements(IResponses)
|
implements(IResponses)
|
||||||
|
|
||||||
storageName = 'survey_responses'
|
storageName = 'survey_responses'
|
||||||
|
personId = None
|
||||||
|
institutionId = None
|
||||||
|
|
||||||
def __init__(self, context):
|
def __init__(self, context):
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
def save(self, data):
|
def save(self, data):
|
||||||
if self.personId:
|
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)
|
update=True, overwrite=True)
|
||||||
|
|
||||||
def load(self):
|
def load(self, personId=None, institutionId=None):
|
||||||
if self.personId:
|
if personId is None:
|
||||||
tracks = self.storage.getUserTracks(self.uid, 0, self.personId)
|
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:
|
if tracks:
|
||||||
return tracks[0].data
|
return tracks[0].data
|
||||||
return {}
|
return {}
|
||||||
|
|
|
@ -4,96 +4,273 @@
|
||||||
|
|
||||||
<metal:block define-macro="survey"
|
<metal:block define-macro="survey"
|
||||||
tal:define="feedback item/results;
|
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" />
|
<metal:title use-macro="item/conceptMacros/concepttitle_only" />
|
||||||
<tal:description condition="not:feedback">
|
<tal:description condition="not:feedback">
|
||||||
<metal:title use-macro="item/conceptMacros/conceptdescription" />
|
<div tal:define="header item/adapted/questionnaireHeader"
|
||||||
</tal:description>
|
|
||||||
<div tal:condition="feedback">
|
|
||||||
<h3 i18n:translate="">Feedback</h3>
|
|
||||||
<div tal:define="header item/adapted/feedbackHeader"
|
|
||||||
tal:condition="header"
|
tal:condition="header"
|
||||||
tal:content="structure python:item.renderText(header, 'text/restructured')" />
|
tal:content="structure python:
|
||||||
<table class="listing">
|
item.renderText(header, 'text/restructured')" />
|
||||||
<tr>
|
</tal:description>
|
||||||
<th i18n:translate="">Category</th>
|
|
||||||
<th i18n:translate="">Response</th>
|
<div tal:condition="feedback">
|
||||||
<th i18n:translate="">%</th>
|
<metal:block use-macro="item/template/macros/?reportMacro" />
|
||||||
</tr>
|
|
||||||
<tr tal:repeat="fbitem feedback">
|
|
||||||
<td tal:content="fbitem/category" />
|
|
||||||
<td tal:content="fbitem/text" />
|
|
||||||
<td tal:content="fbitem/score" />
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div class="button" id="show_questionnaire">
|
|
||||||
<a href="" onclick="back(); return false"
|
|
||||||
i18n:translate="">
|
|
||||||
Back to Questionnaire</a>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
<div tal:define="footer item/adapted/feedbackFooter"
|
|
||||||
tal:condition="footer"
|
|
||||||
tal:content="structure python:item.renderText(footer, 'text/restructured')" />
|
|
||||||
</div>
|
</div>
|
||||||
<div id="questionnaire"
|
<div id="questionnaire"
|
||||||
tal:condition="not:feedback">
|
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>
|
<h3 i18n:translate="">Questionnaire</h3>
|
||||||
<div class="error"
|
<div class="error"
|
||||||
tal:condition="errors">
|
tal:condition="errors">
|
||||||
<div tal:repeat="error errors">
|
<div tal:repeat="error errors">
|
||||||
<span i18n:translate=""
|
<span i18n:translate=""
|
||||||
tal:content="error" />
|
tal:content="error/text" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="message"
|
||||||
|
tal:condition="message"
|
||||||
|
i18n:translate=""
|
||||||
|
tal:content="message" />
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<table class="listing">
|
<table class="listing">
|
||||||
<tal:qugroup repeat="qugroup item/adapted/questionGroups">
|
<input type="hidden" name="person"
|
||||||
<tr><td colspan="6"> </td></tr>
|
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">
|
<tr class="vpad">
|
||||||
<td tal:define="infoText python:item.getInfoText(qugroup)">
|
<td tal:define="infoText group/infoText">
|
||||||
<b tal:content="qugroup/title" />
|
<b i18n:translate=""
|
||||||
|
tal:content="group/title" />
|
||||||
<div class="infotext"
|
<div class="infotext"
|
||||||
tal:condition="infoText">
|
tal:condition="infoText">
|
||||||
<span tal:content="structure infoText" />
|
<span tal:content="structure infoText" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center"
|
<td tal:repeat="opt python:[opt for opt in item.answerOptions
|
||||||
i18n:translate="">No answer</td>
|
if opt.get('colspan') != '0']"
|
||||||
<td colspan="2"
|
i18n:translate=""
|
||||||
i18n:translate="">Fully applies</td>
|
i18n:attributes="title"
|
||||||
<td colspan="2"
|
tal:attributes="title opt/description|string:;
|
||||||
style="text-align: right"
|
class python:opt.get('cssclass') or 'center';
|
||||||
i18n:translate="">Does not apply</td>
|
colspan python:opt.get('colspan')"
|
||||||
|
tal:content="opt/label|string:" />
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="vpad"
|
<tal:question repeat="question group/questions">
|
||||||
tal:repeat="question qugroup/questions">
|
<tal:question define="qutype python:
|
||||||
<td tal:content="question/text" />
|
question.questionType or 'value_selection'">
|
||||||
<td style="white-space: nowrap; text-align: center"
|
<metal:question use-macro="item/template/macros/?qutype" />
|
||||||
tal:repeat="value python:item.getValues(question)">
|
</tal:question>
|
||||||
<input type="radio"
|
</tal:question>
|
||||||
i18n:attributes="title"
|
</tal:group>
|
||||||
tal:condition="value/radio"
|
|
||||||
tal:attributes="
|
|
||||||
name string:question_${question/uid};
|
|
||||||
value value/value;
|
|
||||||
checked value/checked;
|
|
||||||
title string:survey_value_${value/value}" />
|
|
||||||
<span tal:condition="not:value/radio"
|
|
||||||
title="Obligatory question, must be answered"
|
|
||||||
i18n:attributes="title">***
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tal:qugroup>
|
|
||||||
</table>
|
</table>
|
||||||
<input type="submit" name="submit" value="Evaluate Questionnaire"
|
<input type="submit" name="submit" value="Evaluate Questionnaire"
|
||||||
i18n:attributes="value" />
|
i18n:attributes="value" />
|
||||||
|
<input type="submit" name="save" value="Save Data"
|
||||||
|
i18n:attributes="value" />
|
||||||
<input type="button" name="reset_responses" value="Reset Responses Entered"
|
<input type="button" name="reset_responses" value="Reset Responses Entered"
|
||||||
i18n:attributes="value"
|
i18n:attributes="value; onclick"
|
||||||
onclick="setRadioButtons('none'); return false" />
|
onclick="if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</metal:block>
|
||||||
|
|
||||||
|
|
||||||
|
<metal:block define-macro="quest_pref_selection">
|
||||||
|
<h3 i18n:translate="">Questionnaire</h3>
|
||||||
|
<div class="error"
|
||||||
|
tal:condition="errors">
|
||||||
|
<div tal:repeat="error errors">
|
||||||
|
<span i18n:translate=""
|
||||||
|
tal:content="error/text" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="message"
|
||||||
|
tal:condition="message"
|
||||||
|
i18n:translate=""
|
||||||
|
tal:content="message" />
|
||||||
|
<form method="post">
|
||||||
|
<table class="listing">
|
||||||
|
<input type="hidden" name="person"
|
||||||
|
tal:define="personId request/person|nothing"
|
||||||
|
tal:condition="personId"
|
||||||
|
tal:attributes="value personId" />
|
||||||
|
<tal:group repeat="group item/groups">
|
||||||
|
<tr><td> </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:attributes="name string:question_${question/uid};
|
||||||
|
value value/value;
|
||||||
|
checked value/checked;
|
||||||
|
title value/title" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</metal:block>
|
||||||
|
|
||||||
|
|
||||||
|
<metal:block define-macro="text">
|
||||||
|
<tr tal:attributes="class python:item.getCssClass(question)">
|
||||||
|
<td>
|
||||||
|
<div tal:content="question/text" />
|
||||||
|
<textarea style="width: 90%; margin-left: 20px"
|
||||||
|
tal:content="python:item.getTextValue(question)"
|
||||||
|
tal:attributes="name string:question_${question/uid}">
|
||||||
|
</textarea>
|
||||||
|
</td>
|
||||||
|
<td tal:repeat="opt item/answerOptions" />
|
||||||
|
</tr>
|
||||||
|
</metal:block>
|
||||||
|
|
||||||
|
|
||||||
|
<metal:block define-macro="report_standard">
|
||||||
|
<h3 i18n:translate="">Feedback</h3>
|
||||||
|
<div tal:define="header item/adapted/feedbackHeader"
|
||||||
|
tal:condition="header"
|
||||||
|
tal:content="structure python:
|
||||||
|
item.renderText(header, 'text/restructured')" />
|
||||||
|
<table class="listing">
|
||||||
|
<tr>
|
||||||
|
<th i18n:translate="">Category</th>
|
||||||
|
<th tal:repeat="col item/feedbackColumns"
|
||||||
|
i18n:translate=""
|
||||||
|
tal:attributes="class python:
|
||||||
|
col['name'] != 'text' and 'center' or None"
|
||||||
|
tal:content="col/label" />
|
||||||
|
</tr>
|
||||||
|
<tr style="vertical-align: top"
|
||||||
|
tal:repeat="fbitem feedback">
|
||||||
|
<td style="vertical-align: top"
|
||||||
|
tal:content="fbitem/category" />
|
||||||
|
<tal:cols repeat="col item/feedbackColumns">
|
||||||
|
<td style="vertical-align: top"
|
||||||
|
tal:define="name col/name"
|
||||||
|
tal:attributes="class python:name != 'text' and 'center' or None"
|
||||||
|
tal:content="fbitem/?name|string:" />
|
||||||
|
</tal:cols>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p tal:define="teamData item/teamData"
|
||||||
|
tal:condition="teamData">
|
||||||
|
<b><span i18n:translate="">Team Size</span>:
|
||||||
|
<span tal:content="python:len(teamData)" /></b><br />
|
||||||
|
</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>
|
</metal:block>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from zope import component
|
||||||
from zope.interface.verify import verifyClass
|
from zope.interface.verify import verifyClass
|
||||||
from zope.testing.doctestunit import DocFileSuite
|
from zope.testing.doctestunit import DocFileSuite
|
||||||
|
|
||||||
|
from loops.expert.report import IReport, Report
|
||||||
from loops.knowledge.qualification.base import Competence
|
from loops.knowledge.qualification.base import Competence
|
||||||
from loops.knowledge.survey.base import Questionnaire, Question, FeedbackItem
|
from loops.knowledge.survey.base import Questionnaire, Question, FeedbackItem
|
||||||
from loops.knowledge.survey.interfaces import IQuestionnaire, IQuestion, \
|
from loops.knowledge.survey.interfaces import IQuestionnaire, IQuestion, \
|
||||||
|
@ -19,6 +20,7 @@ importPath = os.path.join(os.path.dirname(__file__), 'data')
|
||||||
|
|
||||||
|
|
||||||
def importData(loopsRoot):
|
def importData(loopsRoot):
|
||||||
|
component.provideAdapter(Report, provides=IReport)
|
||||||
baseImportData(loopsRoot, importPath, 'knowledge_de.dmp')
|
baseImportData(loopsRoot, importPath, 'knowledge_de.dmp')
|
||||||
|
|
||||||
def importSurvey(loopsRoot):
|
def importSurvey(loopsRoot):
|
||||||
|
|
Binary file not shown.
|
@ -1,9 +1,9 @@
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
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"
|
"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"
|
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
|
||||||
"Language-Team: loops developers <helmutm@cy55.de>\n"
|
"Language-Team: loops developers <helmutm@cy55.de>\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -89,6 +89,14 @@ msgstr "Thema ändern"
|
||||||
msgid "Please correct the indicated errors."
|
msgid "Please correct the indicated errors."
|
||||||
msgstr "Bitte berichtigen Sie die angezeigten Fehler."
|
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
|
# blog
|
||||||
|
|
||||||
msgid "Edit Blog Post..."
|
msgid "Edit Blog Post..."
|
||||||
|
@ -178,6 +186,30 @@ msgstr "Glossareintrag anlegen."
|
||||||
msgid "Answer Range"
|
msgid "Answer Range"
|
||||||
msgstr "Abstufung Bewertungen"
|
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"
|
msgid "Feedback Footer"
|
||||||
msgstr "Auswertungs-Hinweis"
|
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."
|
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."
|
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"
|
msgid "Required"
|
||||||
msgstr "Pflichtfrage"
|
msgstr "Pflichtfrage"
|
||||||
|
|
||||||
|
@ -205,6 +246,9 @@ msgstr "Negative Polarität"
|
||||||
msgid "Value inversion: High selection means low value."
|
msgid "Value inversion: High selection means low value."
|
||||||
msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert."
|
msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert."
|
||||||
|
|
||||||
|
msgid "Question"
|
||||||
|
msgstr "Frage"
|
||||||
|
|
||||||
msgid "Questionnaire"
|
msgid "Questionnaire"
|
||||||
msgstr "Fragebogen"
|
msgstr "Fragebogen"
|
||||||
|
|
||||||
|
@ -241,15 +285,30 @@ msgstr "Trifft eher zu"
|
||||||
msgid "survey_value_3"
|
msgid "survey_value_3"
|
||||||
msgstr "Trifft für unser Unternehmen voll und ganz zu"
|
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"
|
msgid "Evaluate Questionnaire"
|
||||||
msgstr "Fragebogen auswerten"
|
msgstr "Fragebogen auswerten"
|
||||||
|
|
||||||
|
msgid "Save Data"
|
||||||
|
msgstr "Daten speichern"
|
||||||
|
|
||||||
msgid "Reset Responses Entered"
|
msgid "Reset Responses Entered"
|
||||||
msgstr "Eingaben zurücksetzen"
|
msgstr "Eingaben zurücksetzen"
|
||||||
|
|
||||||
msgid "Back to Questionnaire"
|
msgid "Back to Questionnaire"
|
||||||
msgstr "Zurück zum Fragebogen"
|
msgstr "Zurück zum Fragebogen"
|
||||||
|
|
||||||
|
msgid "Your data have been saved."
|
||||||
|
msgstr "Ihre Daten wurden gespeichert."
|
||||||
|
|
||||||
msgid "Please answer at least $minAnswers questions."
|
msgid "Please answer at least $minAnswers questions."
|
||||||
msgstr "Bitte beantworten Sie mindestens $minAnswers Fragen."
|
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."
|
msgid "Please answer the minimum number of questions."
|
||||||
msgstr "Bitte beantworten Sie die angegebene Mindestanzahl an Fragen je Fragengruppe."
|
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"
|
msgid "Obligatory question, must be answered"
|
||||||
msgstr "Pflichtfrage, muss beantwortet werden"
|
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)"
|
msgid "Validity Period (Months)"
|
||||||
msgstr "Gültigkeitszeitraum (Monate)"
|
msgstr "Gültigkeitszeitraum (Monate)"
|
||||||
|
@ -471,6 +557,8 @@ msgstr "Wer?"
|
||||||
msgid "When?"
|
msgid "When?"
|
||||||
msgstr "Wann?"
|
msgstr "Wann?"
|
||||||
|
|
||||||
|
# personal stuff
|
||||||
|
|
||||||
msgid "Favorites"
|
msgid "Favorites"
|
||||||
msgstr "Lesezeichen"
|
msgstr "Lesezeichen"
|
||||||
|
|
||||||
|
@ -495,6 +583,8 @@ msgstr "Anmelden"
|
||||||
msgid "Presence"
|
msgid "Presence"
|
||||||
msgstr "Anwesenheit"
|
msgstr "Anwesenheit"
|
||||||
|
|
||||||
|
# general
|
||||||
|
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr "Aktionen"
|
msgstr "Aktionen"
|
||||||
|
|
||||||
|
@ -819,6 +909,12 @@ msgstr "Beginn"
|
||||||
msgid "End date"
|
msgid "End date"
|
||||||
msgstr "Ende"
|
msgstr "Ende"
|
||||||
|
|
||||||
|
msgid "Start Day"
|
||||||
|
msgstr "Beginn"
|
||||||
|
|
||||||
|
msgid "End Day"
|
||||||
|
msgstr "Ende"
|
||||||
|
|
||||||
msgid "Knowledge"
|
msgid "Knowledge"
|
||||||
msgstr "Kompetenzen"
|
msgstr "Kompetenzen"
|
||||||
|
|
||||||
|
@ -891,6 +987,9 @@ msgstr "Kommentare"
|
||||||
msgid "Add Comment"
|
msgid "Add Comment"
|
||||||
msgstr "Kommentar hinzufügen"
|
msgstr "Kommentar hinzufügen"
|
||||||
|
|
||||||
|
msgid "Email Address"
|
||||||
|
msgstr "E-Mail-Adresse"
|
||||||
|
|
||||||
msgid "Subject"
|
msgid "Subject"
|
||||||
msgstr "Thema"
|
msgstr "Thema"
|
||||||
|
|
||||||
|
@ -965,6 +1064,21 @@ msgstr "Kalender"
|
||||||
msgid "Work Items"
|
msgid "Work Items"
|
||||||
msgstr "Aktivitäten"
|
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"
|
msgid "Work Items for $title"
|
||||||
msgstr "Aktivitäten für $title"
|
msgstr "Aktivitäten für $title"
|
||||||
|
|
||||||
|
@ -995,6 +1109,12 @@ msgstr "Dauer/Aufwand"
|
||||||
msgid "Duration / Effort (hh:mm)"
|
msgid "Duration / Effort (hh:mm)"
|
||||||
msgstr "Dauer / Aufwand (hh:mm)"
|
msgstr "Dauer / Aufwand (hh:mm)"
|
||||||
|
|
||||||
|
msgid "Priority"
|
||||||
|
msgstr "Priorität"
|
||||||
|
|
||||||
|
msgid "Activity"
|
||||||
|
msgstr "Leistungsart"
|
||||||
|
|
||||||
msgid "Action"
|
msgid "Action"
|
||||||
msgstr "Aktion"
|
msgstr "Aktion"
|
||||||
|
|
||||||
|
@ -1069,6 +1189,9 @@ msgstr "Bemerkung"
|
||||||
msgid "desc_transition_comments"
|
msgid "desc_transition_comments"
|
||||||
msgstr "Notizen zum Statusübergang."
|
msgstr "Notizen zum Statusübergang."
|
||||||
|
|
||||||
|
msgid "contact_states"
|
||||||
|
msgstr "Kontaktstatus"
|
||||||
|
|
||||||
# state names
|
# state names
|
||||||
|
|
||||||
msgid "accepted"
|
msgid "accepted"
|
||||||
|
@ -1137,6 +1260,12 @@ msgstr "unklassifiziert"
|
||||||
msgid "verified"
|
msgid "verified"
|
||||||
msgstr "verifiziert"
|
msgstr "verifiziert"
|
||||||
|
|
||||||
|
msgid "prospective"
|
||||||
|
msgstr "künftig"
|
||||||
|
|
||||||
|
msgid "inactive"
|
||||||
|
msgstr "inaktiv"
|
||||||
|
|
||||||
# transitions
|
# transitions
|
||||||
|
|
||||||
msgid "accept"
|
msgid "accept"
|
||||||
|
@ -1211,6 +1340,15 @@ msgstr "verifizieren"
|
||||||
msgid "work"
|
msgid "work"
|
||||||
msgstr "bearbeiten"
|
msgstr "bearbeiten"
|
||||||
|
|
||||||
|
msgid "activate"
|
||||||
|
msgstr "aktivieren"
|
||||||
|
|
||||||
|
msgid "inactivate"
|
||||||
|
msgstr "inaktiv setzen"
|
||||||
|
|
||||||
|
msgid "reset"
|
||||||
|
msgstr "zurücksetzen"
|
||||||
|
|
||||||
# calendar
|
# calendar
|
||||||
|
|
||||||
msgid "Monday"
|
msgid "Monday"
|
||||||
|
|
|
@ -228,6 +228,23 @@ We need a principal for testing the login stuff:
|
||||||
>>> pwcView.update()
|
>>> pwcView.update()
|
||||||
False
|
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
|
Pure Person-based Authentication
|
||||||
================================
|
================================
|
||||||
|
@ -410,7 +427,7 @@ Send Email to Members
|
||||||
>>> form.subject
|
>>> form.subject
|
||||||
u"loops Notification from '$site'"
|
u"loops Notification from '$site'"
|
||||||
>>> form.mailBody
|
>>> form.mailBody
|
||||||
u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.113\n\n'
|
u'\n\nEvent #1\nhttp://127.0.0.1/loops/views/menu/.118\n\n'
|
||||||
|
|
||||||
|
|
||||||
Show Presence of Other Users
|
Show Presence of Other Users
|
||||||
|
|
|
@ -45,6 +45,12 @@
|
||||||
class="loops.organize.browser.member.PasswordChange"
|
class="loops.organize.browser.member.PasswordChange"
|
||||||
permission="zope.View" />
|
permission="zope.View" />
|
||||||
|
|
||||||
|
<browser:page
|
||||||
|
for="loops.interfaces.INode"
|
||||||
|
name="reset_password.html"
|
||||||
|
class="loops.organize.browser.member.PasswordReset"
|
||||||
|
permission="zope.View" />
|
||||||
|
|
||||||
<zope:adapter
|
<zope:adapter
|
||||||
name="task.html"
|
name="task.html"
|
||||||
for="loops.interfaces.IConcept
|
for="loops.interfaces.IConcept
|
||||||
|
@ -146,4 +152,12 @@
|
||||||
permission="zope.ManageServices"
|
permission="zope.ManageServices"
|
||||||
menu="zmi_views" title="Prefix" />
|
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>
|
</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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -25,6 +25,7 @@ from datetime import datetime
|
||||||
from email.MIMEText import MIMEText
|
from email.MIMEText import MIMEText
|
||||||
from zope import interface, component
|
from zope import interface, component
|
||||||
from zope.app.authentication.principalfolder import InternalPrincipal
|
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.form.browser.textwidgets import PasswordWidget as BasePasswordWidget
|
||||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.app.principalannotation import annotations
|
from zope.app.principalannotation import annotations
|
||||||
|
@ -47,9 +48,11 @@ from loops.browser.node import NodeView
|
||||||
from loops.common import adapted
|
from loops.common import adapted
|
||||||
from loops.concept import Concept
|
from loops.concept import Concept
|
||||||
from loops.organize.interfaces import ANNOTATION_KEY, IMemberRegistrationManager
|
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.party import getPersonForUser, Person
|
||||||
from loops.organize.util import getInternalPrincipal, getPrincipalForUserId
|
from loops.organize.util import getInternalPrincipal, getPrincipalForUserId
|
||||||
|
from loops.organize.util import getPrincipalFolder
|
||||||
import loops.browser.util
|
import loops.browser.util
|
||||||
from loops.util import _
|
from loops.util import _
|
||||||
|
|
||||||
|
@ -86,7 +89,8 @@ class BaseMemberRegistration(NodeView):
|
||||||
template = form_macros
|
template = form_macros
|
||||||
|
|
||||||
formErrors = dict(
|
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.')),
|
duplicate_loginname=FormError(_('Login name already taken.')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -187,7 +191,9 @@ class MemberRegistration(BaseMemberRegistration, CreateForm):
|
||||||
result = regMan.register(login, pw,
|
result = regMan.register(login, pw,
|
||||||
form.get('lastName'), form.get('firstName'),
|
form.get('lastName'), form.get('firstName'),
|
||||||
email=form.get('email'),
|
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):
|
if isinstance(result, dict):
|
||||||
fi = formState.fieldInstances[result['fieldName']]
|
fi = formState.fieldInstances[result['fieldName']]
|
||||||
fi.setError(result['error'], self.formErrors)
|
fi.setError(result['error'], self.formErrors)
|
||||||
|
@ -210,6 +216,8 @@ class SecureMemberRegistration(BaseMemberRegistration, CreateForm):
|
||||||
@Lazy
|
@Lazy
|
||||||
def schema(self):
|
def schema(self):
|
||||||
schema = super(SecureMemberRegistration, self).schema
|
schema = super(SecureMemberRegistration, self).schema
|
||||||
|
schema.fields.remove('salutation')
|
||||||
|
schema.fields.remove('academicTitle')
|
||||||
schema.fields.remove('birthDate')
|
schema.fields.remove('birthDate')
|
||||||
schema.fields.remove('password')
|
schema.fields.remove('password')
|
||||||
schema.fields.remove('passwordConfirm')
|
schema.fields.remove('passwordConfirm')
|
||||||
|
@ -366,7 +374,8 @@ class PasswordChange(NodeView, Form):
|
||||||
message = _(u'Your password has been changed.')
|
message = _(u'Your password has been changed.')
|
||||||
|
|
||||||
formErrors = dict(
|
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.')),
|
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)
|
formState.severity = max(formState.severity, fi.severity)
|
||||||
return formState
|
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 = views['home']
|
||||||
>>> home.target = resources['d001.txt']
|
>>> home.target = resources['d001.txt']
|
||||||
|
|
||||||
|
>>> from loops.organize.comment.base import commentStates
|
||||||
|
>>> component.provideUtility(commentStates(), name='organize.commentStates')
|
||||||
|
|
||||||
Creating comments
|
Creating comments
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
@ -75,6 +78,12 @@ Viewing comments
|
||||||
('My comment', u'... ...', u'john')
|
('My comment', u'... ...', u'john')
|
||||||
|
|
||||||
|
|
||||||
|
Reporting
|
||||||
|
=========
|
||||||
|
|
||||||
|
>>> from loops.organize.comment.report import CommentsOverview
|
||||||
|
|
||||||
|
|
||||||
Fin de partie
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,24 +18,55 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Base classes for comments/discussions.
|
Base classes for comments/discussions.
|
||||||
|
|
||||||
$Id$
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope.component import adapts
|
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.btree import Track
|
||||||
from cybertools.tracking.interfaces import ITrackingStorage
|
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
|
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)
|
implements(IComment)
|
||||||
|
|
||||||
|
metadata_attributes = Track.metadata_attributes + ('state',)
|
||||||
|
index_attributes = metadata_attributes
|
||||||
typeName = 'Comment'
|
typeName = 'Comment'
|
||||||
|
typeInterface = IComment
|
||||||
|
statesDefinition = 'organize.commentStates'
|
||||||
|
|
||||||
contentType = 'text/restructured'
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -23,15 +23,17 @@ Definition of view classes and other browser related stuff for comments.
|
||||||
from zope import interface, component
|
from zope import interface, component
|
||||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
|
from zope.security import checkPermission
|
||||||
|
|
||||||
from cybertools.browser.action import actions
|
from cybertools.browser.action import actions
|
||||||
from cybertools.tracking.btree import TrackingStorage
|
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.common import BaseView
|
||||||
from loops.browser.form import ObjectForm, EditObject
|
from loops.browser.form import ObjectForm, EditObject
|
||||||
from loops.browser.node import NodeView
|
from loops.browser.node import NodeView
|
||||||
from loops.organize.comment.base import Comment
|
from loops.organize.comment.base import Comment
|
||||||
from loops.organize.party import getPersonForUser
|
from loops.organize.party import getPersonForUser
|
||||||
|
from loops.organize.stateful.browser import StateAction
|
||||||
from loops.organize.tracking.report import TrackDetails
|
from loops.organize.tracking.report import TrackDetails
|
||||||
from loops.security.common import canAccessObject
|
from loops.security.common import canAccessObject
|
||||||
from loops.setup import addObject
|
from loops.setup import addObject
|
||||||
|
@ -50,10 +52,17 @@ class CommentsView(NodeView):
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def allowed(self):
|
def allowed(self):
|
||||||
if self.isAnonymous:
|
if self.virtualTargetObject is None:
|
||||||
return False
|
return False
|
||||||
return (self.virtualTargetObject is not None and
|
opts = (self.globalOptions('organize.allowComments') or
|
||||||
self.globalOptions('organize.allowComments'))
|
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
|
@Lazy
|
||||||
def addUrl(self):
|
def addUrl(self):
|
||||||
|
@ -76,9 +85,47 @@ class CommentsView(NodeView):
|
||||||
result.append(CommentDetails(self, tr))
|
result.append(CommentDetails(self, tr))
|
||||||
return result
|
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):
|
class CommentDetails(TrackDetails):
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def poster(self):
|
||||||
|
name = self.track.data.get('name')
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
return self.user['title']
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def subject(self):
|
def subject(self):
|
||||||
return self.track.data['subject']
|
return self.track.data['subject']
|
||||||
|
@ -108,6 +155,8 @@ class CreateComment(EditObject):
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def personId(self):
|
def personId(self):
|
||||||
|
if self.view.isAnonymous:
|
||||||
|
return self.request.form.get('email')
|
||||||
p = getPersonForUser(self.context, self.request)
|
p = getPersonForUser(self.context, self.request)
|
||||||
if p is not None:
|
if p is not None:
|
||||||
return util.getUidForObject(p)
|
return util.getUidForObject(p)
|
||||||
|
@ -129,8 +178,11 @@ class CreateComment(EditObject):
|
||||||
if ts is None:
|
if ts is None:
|
||||||
ts = addObject(rm, TrackingStorage, 'comments', trackFactory=Comment)
|
ts = addObject(rm, TrackingStorage, 'comments', trackFactory=Comment)
|
||||||
uid = util.getUidForObject(self.object)
|
uid = util.getUidForObject(self.object)
|
||||||
ts.saveUserTrack(uid, 0, self.personId, dict(
|
data = dict(subject=subject, text=text)
|
||||||
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'
|
url = self.view.virtualTargetUrl + '?version=this'
|
||||||
self.request.response.redirect(url)
|
self.request.response.redirect(url)
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -14,10 +14,17 @@
|
||||||
<tal:comment tal:repeat="comment items">
|
<tal:comment tal:repeat="comment items">
|
||||||
<br />
|
<br />
|
||||||
<div class="comment">
|
<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>
|
<h3>
|
||||||
<span tal:content="comment/subject">Subject</span></h3>
|
<span tal:content="comment/subject">Subject</span></h3>
|
||||||
<div class="info">
|
<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>
|
<span tal:replace="comment/timeStamp">2007-03-30</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="content"
|
<p class="content"
|
||||||
|
@ -44,6 +51,18 @@
|
||||||
<input type="hidden" name="contentType" value="text/restructured" />
|
<input type="hidden" name="contentType" value="text/restructured" />
|
||||||
<div class="heading" i18n:translate="">Add Comment</div>
|
<div class="heading" i18n:translate="">Add Comment</div>
|
||||||
<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=""
|
<label i18n:translate=""
|
||||||
for="comment_subject">Subject</label>
|
for="comment_subject">Subject</label>
|
||||||
<div><input type="text" name="subject" id="comment_subject"
|
<div><input type="text" name="subject" id="comment_subject"
|
||||||
|
|
|
@ -12,6 +12,10 @@
|
||||||
set_schema="cybertools.tracking.comment.interfaces.IComment" />
|
set_schema="cybertools.tracking.comment.interfaces.IComment" />
|
||||||
</zope:class>
|
</zope:class>
|
||||||
|
|
||||||
|
<zope:utility
|
||||||
|
factory="loops.organize.comment.base.commentStates"
|
||||||
|
name="organize.commentStates" />
|
||||||
|
|
||||||
<!-- views -->
|
<!-- views -->
|
||||||
|
|
||||||
<browser:page
|
<browser:page
|
||||||
|
@ -33,4 +37,26 @@
|
||||||
factory="loops.organize.comment.browser.CreateComment"
|
factory="loops.organize.comment.browser.CreateComment"
|
||||||
permission="zope.View" />
|
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>
|
</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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -82,7 +82,7 @@ class LoginName(schema.TextLine):
|
||||||
super(LoginName, self)._validate(userId)
|
super(LoginName, self)._validate(userId)
|
||||||
if userId in getPrincipalFolder(self.context):
|
if userId in getPrincipalFolder(self.context):
|
||||||
raiseValidationError(
|
raiseValidationError(
|
||||||
_(u'There is alread a user with ID $userId.',
|
_(u'There is already a user with ID $userId.',
|
||||||
mapping=dict(userId=userId)))
|
mapping=dict(userId=userId)))
|
||||||
|
|
||||||
|
|
||||||
|
@ -124,6 +124,14 @@ class IPasswordChange(IPasswordEntry):
|
||||||
required=True,)
|
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):
|
class IMemberRegistration(IBasePerson, IPasswordEntry):
|
||||||
""" Schema for registering a new member (user + person).
|
""" 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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -57,6 +57,8 @@ PredicateInterfaceSourceList.predicateInterfaces += (IHasRole,)
|
||||||
|
|
||||||
|
|
||||||
def getPersonForUser(context, request=None, principal=None):
|
def getPersonForUser(context, request=None, principal=None):
|
||||||
|
if context is None:
|
||||||
|
return None
|
||||||
if principal is None:
|
if principal is None:
|
||||||
if request is None:
|
if request is None:
|
||||||
principal = getCurrentPrincipal()
|
principal = getCurrentPrincipal()
|
||||||
|
@ -95,9 +97,11 @@ class Person(AdapterBase, BasePerson):
|
||||||
return
|
return
|
||||||
person = getPersonForUser(self.context, principal=principal)
|
person = getPersonForUser(self.context, principal=principal)
|
||||||
if person is not None and person != self.context:
|
if person is not None and person != self.context:
|
||||||
raise ValueError(
|
name = getName(person)
|
||||||
'Error when creating user %s: There is already a person (%s) assigned to user %s.'
|
if name:
|
||||||
% (getName(self.context), getName(person), userId))
|
raise ValueError(
|
||||||
|
'There is already a person (%s) assigned to user %s.'
|
||||||
|
% (getName(person), userId))
|
||||||
pa = annotations(principal)
|
pa = annotations(principal)
|
||||||
loopsId = util.getUidForObject(self.context.getLoopsRoot())
|
loopsId = util.getUidForObject(self.context.getLoopsRoot())
|
||||||
ann = pa.get(ANNOTATION_KEY)
|
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 = list(favorites.query(userName=johnCId))
|
||||||
>>> favs
|
>>> favs
|
||||||
[<Favorite ['27', 1, '33', '...']: {}>]
|
[<Favorite ['27', 1, '33', '...']: {'type': 'favorite'}>]
|
||||||
|
|
||||||
>>> list(favAdapted.list(johnC))
|
>>> list(favAdapted.list(johnC))
|
||||||
['27']
|
['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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,8 +18,6 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Base classes for a notification framework.
|
Base classes for a notification framework.
|
||||||
|
|
||||||
$Id$
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope.component import adapts
|
from zope.component import adapts
|
||||||
|
@ -39,17 +37,20 @@ class Favorites(object):
|
||||||
def __init__(self, context):
|
def __init__(self, context):
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
def list(self, person, sortKey=None):
|
def list(self, person, sortKey=None, type='favorite'):
|
||||||
for item in self.listTracks(person, sortKey):
|
for item in self.listTracks(person, sortKey, type):
|
||||||
yield item.taskId
|
yield item.taskId
|
||||||
|
|
||||||
def listTracks(self, person, sortKey=None):
|
def listTracks(self, person, sortKey=None, type='favorite'):
|
||||||
if person is None:
|
if person is None:
|
||||||
return
|
return
|
||||||
personUid = util.getUidForObject(person)
|
personUid = util.getUidForObject(person)
|
||||||
if sortKey is None:
|
if sortKey is None:
|
||||||
sortKey = lambda x: -x.timeStamp
|
sortKey = lambda x: -x.timeStamp
|
||||||
for item in sorted(self.context.query(userName=personUid), key=sortKey):
|
for item in sorted(self.context.query(userName=personUid), key=sortKey):
|
||||||
|
if type is not None:
|
||||||
|
if item.type != type:
|
||||||
|
continue
|
||||||
yield item
|
yield item
|
||||||
|
|
||||||
def add(self, obj, person, data=None):
|
def add(self, obj, person, data=None):
|
||||||
|
@ -57,21 +58,23 @@ class Favorites(object):
|
||||||
return False
|
return False
|
||||||
uid = util.getUidForObject(obj)
|
uid = util.getUidForObject(obj)
|
||||||
personUid = util.getUidForObject(person)
|
personUid = util.getUidForObject(person)
|
||||||
if self.context.query(userName=personUid, taskId=uid):
|
|
||||||
return False
|
|
||||||
if data is None:
|
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)
|
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):
|
if None in (obj, person):
|
||||||
return False
|
return False
|
||||||
uid = util.getUidForObject(obj)
|
uid = util.getUidForObject(obj)
|
||||||
personUid = util.getUidForObject(person)
|
personUid = util.getUidForObject(person)
|
||||||
changed = False
|
changed = False
|
||||||
for t in self.context.query(userName=personUid, taskId=uid):
|
for track in self.context.query(userName=personUid, taskId=uid):
|
||||||
changed = True
|
if track.type == type:
|
||||||
self.context.removeTrack(t)
|
changed = True
|
||||||
|
self.context.removeTrack(track)
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,3 +84,46 @@ class Favorite(Track):
|
||||||
|
|
||||||
typeName = 'Favorite'
|
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
|
>>> from loops.organize.stateful.task import taskStates, publishableTask
|
||||||
|
|
||||||
|
|
||||||
|
Contact States
|
||||||
|
===========
|
||||||
|
|
||||||
|
>>> from loops.organize.stateful.contact import contactStates
|
||||||
|
|
||||||
|
|
||||||
Fin de partie
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -37,6 +37,7 @@ from loops.common import adapted
|
||||||
from loops.expert.query import And, Or, State, Type, getObjects
|
from loops.expert.query import And, Or, State, Type, getObjects
|
||||||
from loops.expert.browser.search import search_template
|
from loops.expert.browser.search import search_template
|
||||||
from loops.security.common import checkPermission
|
from loops.security.common import checkPermission
|
||||||
|
from loops import util
|
||||||
from loops.util import _
|
from loops.util import _
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,7 +46,8 @@ template = ViewPageTemplateFile('view_macros.pt')
|
||||||
statefulActions = ('classification_quality',
|
statefulActions = ('classification_quality',
|
||||||
'simple_publishing',
|
'simple_publishing',
|
||||||
'task_states',
|
'task_states',
|
||||||
'publishable_task',)
|
'publishable_task',
|
||||||
|
'contact_states',)
|
||||||
|
|
||||||
|
|
||||||
def registerStatesPortlet(controller, view, statesDefs,
|
def registerStatesPortlet(controller, view, statesDefs,
|
||||||
|
@ -65,6 +67,7 @@ class StateAction(Action):
|
||||||
url = None
|
url = None
|
||||||
definition = None
|
definition = None
|
||||||
msgFactory = _
|
msgFactory = _
|
||||||
|
cssClass = 'icon-action'
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def stateful(self):
|
def stateful(self):
|
||||||
|
@ -106,8 +109,10 @@ class ChangeStateBase(object):
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def stateful(self):
|
def stateful(self):
|
||||||
return component.getAdapter(self.view.virtualTargetObject, IStateful,
|
target = self.view.virtualTargetObject
|
||||||
name=self.definition)
|
if IStateful.providedBy(target):
|
||||||
|
return target
|
||||||
|
return component.getAdapter(target, IStateful, name=self.definition)
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def definition(self):
|
def definition(self):
|
||||||
|
@ -119,7 +124,8 @@ class ChangeStateBase(object):
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def transition(self):
|
def transition(self):
|
||||||
return self.stateful.getStatesDefinition().transitions[self.action]
|
if self.action:
|
||||||
|
return self.stateful.getStatesDefinition().transitions[self.action]
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def stateObject(self):
|
def stateObject(self):
|
||||||
|
@ -152,9 +158,17 @@ class ChangeStateForm(ChangeStateBase, ObjectForm):
|
||||||
|
|
||||||
class ChangeState(ChangeStateBase, EditObject):
|
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):
|
def update(self):
|
||||||
self.stateful.doTransition(self.action)
|
|
||||||
formData = self.request.form
|
formData = self.request.form
|
||||||
|
if 'target_uid' in formData:
|
||||||
|
self.target = util.getObjectForUid(formData['target_uid'])
|
||||||
# store data in target object (unless field.nostore)
|
# store data in target object (unless field.nostore)
|
||||||
self.object = self.target
|
self.object = self.target
|
||||||
formState = self.instance.applyTemplate(data=formData)
|
formState = self.instance.applyTemplate(data=formData)
|
||||||
|
@ -169,8 +183,9 @@ class ChangeState(ChangeStateBase, EditObject):
|
||||||
fi = formState.fieldInstances[name]
|
fi = formState.fieldInstances[name]
|
||||||
rawValue = fi.getRawValue(formData, name, u'')
|
rawValue = fi.getRawValue(formData, name, u'')
|
||||||
trackData[name] = fi.unmarshall(rawValue)
|
trackData[name] = fi.unmarshall(rawValue)
|
||||||
notify(ObjectModifiedEvent(self.view.virtualTargetObject, trackData))
|
self.stateful.doTransition(self.action)
|
||||||
self.request.response.redirect(self.request.getURL())
|
notify(ObjectModifiedEvent(self.target, trackData))
|
||||||
|
#self.request.response.redirect(self.request.getURL())
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,20 @@
|
||||||
set_schema="cybertools.stateful.interfaces.IStateful" />
|
set_schema="cybertools.stateful.interfaces.IStateful" />
|
||||||
</zope:class>
|
</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 -->
|
<!-- views and form controllers -->
|
||||||
|
|
||||||
<browser:page
|
<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',
|
return StatesDefinition('task_states',
|
||||||
State('draft', 'draft', ('release', 'cancel',),
|
State('draft', 'draft', ('release', 'cancel',),
|
||||||
color='blue'),
|
color='blue'),
|
||||||
State('active', 'active', ('finish', 'cancel',),
|
State('active', 'active', ('finish', 'reopen', 'cancel',),
|
||||||
color='yellow'),
|
color='yellow'),
|
||||||
State('finished', 'finished', ('reopen', 'archive',),
|
State('finished', 'finished', ('reopen', 'archive',),
|
||||||
color='green'),
|
color='green'),
|
||||||
|
|
|
@ -115,16 +115,33 @@
|
||||||
tal:define="stateObject view/stateful/getStateObject"
|
tal:define="stateObject view/stateful/getStateObject"
|
||||||
tal:content="stateObject/title" /> -
|
tal:content="stateObject/title" /> -
|
||||||
<span i18n:translate="">Transition</span>:
|
<span i18n:translate="">Transition</span>:
|
||||||
<span i18n:translate=""
|
<tal:transition condition="view/transition">
|
||||||
tal:content="view/transition/title" />
|
<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>
|
</div>
|
||||||
<input type="hidden" name="form.action" value="change_state">
|
<input type="hidden" name="form.action" value="change_state">
|
||||||
<input type="hidden" name="stdef"
|
<input type="hidden" name="stdef"
|
||||||
tal:attributes="value request/form/stdef|nothing">
|
tal:attributes="value request/form/stdef|nothing">
|
||||||
<input type="hidden" name="action"
|
<input type="hidden" name="target_uid"
|
||||||
tal:attributes="value request/form/action|nothing">
|
tal:define="uid request/target_uid|nothing"
|
||||||
|
tal:condition="uid"
|
||||||
|
tal:attributes="value uid" />
|
||||||
</div>
|
</div>
|
||||||
<div dojoType="dijit.layout.ContentPane" region="center">
|
<div dojoType="dijit.layout.ContentPane" region="center"
|
||||||
|
tal:condition="view/transition">
|
||||||
<table cellpadding="3" class="form">
|
<table cellpadding="3" class="form">
|
||||||
<tbody><tr><td colspan="5" style="padding-right: 15px">
|
<tbody><tr><td colspan="5" style="padding-right: 15px">
|
||||||
<div id="form.fields">
|
<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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -21,9 +21,11 @@ View classes for tracks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope import component
|
from zope import component
|
||||||
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.app.security.interfaces import IAuthentication, PrincipalLookupError
|
from zope.app.security.interfaces import IAuthentication, PrincipalLookupError
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
|
from zope.security.proxy import removeSecurityProxy
|
||||||
from zope.traversing.browser import absoluteURL
|
from zope.traversing.browser import absoluteURL
|
||||||
from zope.traversing.api import getName
|
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.organize.party import getPersonForUser
|
||||||
from loops import util
|
from loops import util
|
||||||
|
|
||||||
|
track_edit_template = ViewPageTemplateFile('edit_track.pt')
|
||||||
|
|
||||||
|
|
||||||
class BaseTrackView(TrackView):
|
class BaseTrackView(TrackView):
|
||||||
|
|
||||||
|
@ -62,7 +66,15 @@ class BaseTrackView(TrackView):
|
||||||
obj = util.getObjectForUid(uid)
|
obj = util.getObjectForUid(uid)
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
return obj
|
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
|
@Lazy
|
||||||
def authentication(self):
|
def authentication(self):
|
||||||
|
@ -102,6 +114,29 @@ class BaseTrackView(TrackView):
|
||||||
return self.request.principal.id
|
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):
|
class ChangeView(BaseTrackView):
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
for="cybertools.tracking.interfaces.ITrackingStorage"
|
for="cybertools.tracking.interfaces.ITrackingStorage"
|
||||||
name="index.html"
|
name="index.html"
|
||||||
class="cybertools.tracking.browser.TrackingStorageView"
|
class="cybertools.tracking.browser.TrackingStorageView"
|
||||||
permission="zope.View" />
|
permission="loops.ManageSite" />
|
||||||
|
|
||||||
<browser:menuItem
|
<browser:menuItem
|
||||||
menu="zmi_views"
|
menu="zmi_views"
|
||||||
|
@ -74,19 +74,26 @@
|
||||||
for="cybertools.tracking.interfaces.ITrack"
|
for="cybertools.tracking.interfaces.ITrack"
|
||||||
name="index.html"
|
name="index.html"
|
||||||
class="loops.organize.tracking.browser.BaseTrackView"
|
class="loops.organize.tracking.browser.BaseTrackView"
|
||||||
permission="zope.View" />
|
permission="loops.ManageSite" />
|
||||||
|
|
||||||
<browser:page
|
<browser:page
|
||||||
for="loops.organize.tracking.change.IChangeRecord"
|
for="loops.organize.tracking.change.IChangeRecord"
|
||||||
name="index.html"
|
name="index.html"
|
||||||
class="loops.organize.tracking.browser.ChangeView"
|
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
|
<browser:page
|
||||||
for="loops.organize.tracking.access.IAccessRecord"
|
for="loops.organize.tracking.access.IAccessRecord"
|
||||||
name="index.html"
|
name="index.html"
|
||||||
class="loops.organize.tracking.browser.AccessView"
|
class="loops.organize.tracking.browser.AccessView"
|
||||||
permission="zope.View" />
|
permission="loops.ManageSite" />
|
||||||
|
|
||||||
<browser:menuItem
|
<browser:menuItem
|
||||||
menu="zmi_views"
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -264,6 +264,9 @@ class TrackDetails(BaseView):
|
||||||
@Lazy
|
@Lazy
|
||||||
def objectData(self):
|
def objectData(self):
|
||||||
obj = self.object
|
obj = self.object
|
||||||
|
if obj is None:
|
||||||
|
return dict(object=None, title='-', type='-', url='',
|
||||||
|
version=None, canAccess=False)
|
||||||
node = self.view.nodeView
|
node = self.view.nodeView
|
||||||
url = node is not None and node.getUrlForTarget(obj) or ''
|
url = node is not None and node.getUrlForTarget(obj) or ''
|
||||||
view = self.view.getViewForObject(obj)
|
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
|
08/12/28 19:00 20:15
|
||||||
{'url': '.../home/.36', 'title': u'loops Development'}
|
{'url': '.../home/.36', 'title': u'loops Development'}
|
||||||
{'url': '.../home/.33', 'title': u'john'} 01:15 00:15
|
{'url': '.../home/.33', 'title': u'john'} 01:15 00:15
|
||||||
{'icon': 'cybertools.icons/ledgreen.png', 'title': u'finished'}
|
{'actions': [...]}
|
||||||
|
|
||||||
>>> results.totals.data
|
>>> results.totals.data
|
||||||
{'effort': 900}
|
{'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
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -22,12 +22,14 @@ View class(es) for work items.
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import time
|
import time
|
||||||
|
from urllib import urlencode
|
||||||
from zope import component
|
from zope import component
|
||||||
from zope.app.security.interfaces import IAuthentication, PrincipalLookupError
|
from zope.app.security.interfaces import IAuthentication, PrincipalLookupError
|
||||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
from zope.event import notify
|
from zope.event import notify
|
||||||
from zope.lifecycleevent import ObjectModifiedEvent
|
from zope.lifecycleevent import ObjectModifiedEvent
|
||||||
|
from zope.security.proxy import removeSecurityProxy
|
||||||
from zope.traversing.browser import absoluteURL
|
from zope.traversing.browser import absoluteURL
|
||||||
from zope.traversing.api import getName, getParent
|
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.form import ObjectForm, EditObject
|
||||||
from loops.browser.node import NodeView
|
from loops.browser.node import NodeView
|
||||||
from loops.common import adapted
|
from loops.common import adapted
|
||||||
|
from loops.interfaces import IConcept
|
||||||
from loops.organize.interfaces import IPerson
|
from loops.organize.interfaces import IPerson
|
||||||
from loops.organize.party import getPersonForUser
|
from loops.organize.party import getPersonForUser
|
||||||
from loops.organize.stateful.browser import StateAction
|
from loops.organize.stateful.browser import StateAction
|
||||||
|
@ -84,6 +87,14 @@ class WorkItemDetails(TrackDetails):
|
||||||
def deadline(self):
|
def deadline(self):
|
||||||
return self.formatTimeStamp(self.track.deadline, 'date')
|
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
|
@Lazy
|
||||||
def start(self):
|
def start(self):
|
||||||
result = self.formatTimeStamp(self.track.start, 'time')
|
result = self.formatTimeStamp(self.track.start, 'time')
|
||||||
|
@ -106,6 +117,11 @@ class WorkItemDetails(TrackDetails):
|
||||||
def startDay(self):
|
def startDay(self):
|
||||||
return self.formatTimeStamp(self.track.timeStamp, 'date')
|
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
|
@Lazy
|
||||||
def created(self):
|
def created(self):
|
||||||
return self.formatTimeStamp(self.track.created, 'dateTime')
|
return self.formatTimeStamp(self.track.created, 'dateTime')
|
||||||
|
@ -151,8 +167,8 @@ class WorkItemDetails(TrackDetails):
|
||||||
target=self.object,
|
target=self.object,
|
||||||
addParams=dict(id=self.track.__name__))
|
addParams=dict(id=self.track.__name__))
|
||||||
actions = [info, WorkItemStateAction(self)]
|
actions = [info, WorkItemStateAction(self)]
|
||||||
if self.isLastInRun and self.allowedToEditWorkItem:
|
#if self.isLastInRun and self.allowedToEditWorkItem:
|
||||||
#if self.allowedToEditWorkItem:
|
if self.allowedToEditWorkItem:
|
||||||
self.view.registerDojoDateWidget()
|
self.view.registerDojoDateWidget()
|
||||||
self.view.registerDojoNumberWidget()
|
self.view.registerDojoNumberWidget()
|
||||||
self.view.registerDojoTextarea()
|
self.view.registerDojoTextarea()
|
||||||
|
@ -169,12 +185,11 @@ class WorkItemDetails(TrackDetails):
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def allowedToEditWorkItem(self):
|
def allowedToEditWorkItem(self):
|
||||||
# if not canAccessObject(self.object.task):
|
#if checkPermission('loops.ManageSite', self.object):
|
||||||
# return False
|
if (self.object is None and
|
||||||
if checkPermission('loops.ManageSite', self.object):
|
checkPermission('zope.ManageContent', self.view.node)):
|
||||||
# or hasRole('loops.Master', self.object):
|
|
||||||
return True
|
return True
|
||||||
if self.track.data.get('creator') == self.personId:
|
if checkPermission('zope.ManageContent', self.object):
|
||||||
return True
|
return True
|
||||||
return self.user['object'] == getPersonForUser(self.object, self.view.request)
|
return self.user['object'] == getPersonForUser(self.object, self.view.request)
|
||||||
|
|
||||||
|
@ -342,6 +357,10 @@ class PersonWorkItems(BaseWorkItemsView, ConceptView):
|
||||||
|
|
||||||
class UserWorkItems(PersonWorkItems):
|
class UserWorkItems(PersonWorkItems):
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def title(self):
|
||||||
|
return self.adapted.title
|
||||||
|
|
||||||
def listWorkItems(self):
|
def listWorkItems(self):
|
||||||
criteria = self.getCriteria()
|
criteria = self.getCriteria()
|
||||||
p = getPersonForUser(self.context, self.request)
|
p = getPersonForUser(self.context, self.request)
|
||||||
|
@ -361,6 +380,10 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
|
||||||
def checkPermissions(self):
|
def checkPermissions(self):
|
||||||
return canAccessObject(self.task or self.target)
|
return canAccessObject(self.task or self.target)
|
||||||
|
|
||||||
|
def setupView(self):
|
||||||
|
self.setupController()
|
||||||
|
self.registerDojoComboBox()
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def macro(self):
|
def macro(self):
|
||||||
return self.template.macros['create_workitem']
|
return self.template.macros['create_workitem']
|
||||||
|
@ -385,6 +408,22 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
|
||||||
track.workItemType = types[0].name
|
track.workItemType = types[0].name
|
||||||
return track
|
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
|
@Lazy
|
||||||
def title(self):
|
def title(self):
|
||||||
return self.track.title or u''
|
return self.track.title or u''
|
||||||
|
@ -402,10 +441,11 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
|
||||||
task = self.task
|
task = self.task
|
||||||
if task is None:
|
if task is None:
|
||||||
task = self.target
|
task = self.target
|
||||||
options = IOptions(adapted(task.conceptType))
|
if IConcept.providedBy(task):
|
||||||
typeNames = options.workitem_types
|
options = IOptions(adapted(task.conceptType))
|
||||||
if typeNames:
|
typeNames = options.workitem_types
|
||||||
return [workItemTypes[name] for name in typeNames]
|
if typeNames:
|
||||||
|
return [workItemTypes[name] for name in typeNames]
|
||||||
return workItemTypes
|
return workItemTypes
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
|
@ -419,12 +459,27 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
|
||||||
return time.strftime('%Y-%m-%d', time.localtime(ts))
|
return time.strftime('%Y-%m-%d', time.localtime(ts))
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def 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
|
@Lazy
|
||||||
def defaultTimeStamp(self):
|
def defaultTimeStamp(self):
|
||||||
if self.workItemType.prefillDate:
|
if self.workItemType.prefillDate:
|
||||||
return getTimeStamp()
|
return getTimeStamp()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def defaultDate(self):
|
||||||
|
return time.strftime('%Y-%m-%dT%H:%M', time.localtime(getTimeStamp()))
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def date(self):
|
def date(self):
|
||||||
ts = self.track.start or self.defaultTimeStamp
|
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 time.strftime('%Y-%m-%d', time.localtime(ts))
|
||||||
return ''
|
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
|
@Lazy
|
||||||
def startTime(self):
|
def startTime(self):
|
||||||
ts = self.track.start or self.defaultTimeStamp
|
ts = self.track.start or self.defaultTimeStamp
|
||||||
|
@ -468,6 +530,8 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
|
||||||
task = self.task
|
task = self.task
|
||||||
if task is None:
|
if task is None:
|
||||||
task = self.target
|
task = self.target
|
||||||
|
if not IConcept.providedBy(task):
|
||||||
|
return []
|
||||||
options = IOptions(adapted(task.conceptType))
|
options = IOptions(adapted(task.conceptType))
|
||||||
return options.hidden_workitem_actions or []
|
return options.hidden_workitem_actions or []
|
||||||
|
|
||||||
|
@ -486,7 +550,10 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
|
||||||
return [dict(name=util.getUidForObject(p), title=p.title)
|
return [dict(name=util.getUidForObject(p), title=p.title)
|
||||||
for p in persons]
|
for p in persons]
|
||||||
|
|
||||||
taskTypes = ['task', 'event', 'agendaitem']
|
@Lazy
|
||||||
|
def taskTypes(self):
|
||||||
|
return (self.globalOptions('organize.work.task_types') or
|
||||||
|
['task', 'event', 'agendaitem'])
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def followUpTask(self):
|
def followUpTask(self):
|
||||||
|
@ -506,6 +573,22 @@ class CreateWorkItemForm(ObjectForm, BaseTrackView):
|
||||||
return [dict(name=util.getUidForObject(t), title=t.title)
|
return [dict(name=util.getUidForObject(t), title=t.title)
|
||||||
for t in tasks]
|
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
|
@Lazy
|
||||||
def duration(self):
|
def duration(self):
|
||||||
if self.state == 'running':
|
if self.state == 'running':
|
||||||
|
@ -564,13 +647,23 @@ class CreateWorkItem(EditObject, BaseTrackView):
|
||||||
setValue('party')
|
setValue('party')
|
||||||
if action == 'move':
|
if action == 'move':
|
||||||
setValue('task')
|
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()
|
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'
|
startTime = form.get('start_time', '').strip().replace('T', '') or '00:00:00'
|
||||||
endTime = form.get('end_time', '').strip().replace('T', '') or '00:00:00'
|
endTime = form.get('end_time', '').strip().replace('T', '') or '00:00:00'
|
||||||
if startDate:
|
if startDate:
|
||||||
result['start'] = parseDateTime('T'.join((startDate, startTime)))
|
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['duration'] = parseTime(form.get('duration'))
|
||||||
result['effort'] = parseTime(form.get('effort'))
|
result['effort'] = parseTime(form.get('effort'))
|
||||||
return action, result
|
return action, result
|
||||||
|
@ -589,6 +682,12 @@ class CreateWorkItem(EditObject, BaseTrackView):
|
||||||
#notify(ObjectModifiedEvent(obj))
|
#notify(ObjectModifiedEvent(obj))
|
||||||
url = self.view.virtualTargetUrl
|
url = self.view.virtualTargetUrl
|
||||||
#url = self.request.URL
|
#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)
|
self.request.response.redirect(url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -660,5 +759,31 @@ def formatTimeDelta(value):
|
||||||
if not value:
|
if not value:
|
||||||
return u''
|
return u''
|
||||||
h, m = divmod(int(value) / 60, 60)
|
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)
|
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
|
<browser:page
|
||||||
name="work.html"
|
name="work.html"
|
||||||
for="loops.organize.interfaces.ITask"
|
for="loops.organize.interfaces.IConceptSchema"
|
||||||
class="loops.organize.work.report.WorkStatementView"
|
class="loops.organize.work.report.WorkStatementView"
|
||||||
permission="zope.View" />
|
permission="zope.View" />
|
||||||
|
|
||||||
|
<browser:page
|
||||||
|
name="work.csv"
|
||||||
|
for="loops.organize.interfaces.IConceptSchema"
|
||||||
|
class="loops.organize.work.report.WorkStatementCSVExport"
|
||||||
|
permission="zope.View" />
|
||||||
|
|
||||||
<zope:adapter
|
<zope:adapter
|
||||||
name="meeting_minutes"
|
name="meeting_minutes"
|
||||||
factory="loops.organize.work.report.MeetingMinutes"
|
factory="loops.organize.work.report.MeetingMinutes"
|
||||||
|
@ -126,6 +132,14 @@
|
||||||
attribute="embed"
|
attribute="embed"
|
||||||
permission="zope.View" />
|
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 -->
|
<!-- setup -->
|
||||||
|
|
||||||
<zope:adapter factory="loops.organize.work.setup.SetupManager"
|
<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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -22,21 +22,25 @@ Work report definitions.
|
||||||
|
|
||||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
from zope.component import adapter
|
from zope.component import adapter, getAdapter
|
||||||
|
|
||||||
from cybertools.composer.report.base import Report
|
from cybertools.composer.report.base import Report
|
||||||
from cybertools.composer.report.base import LeafQueryCriteria, CompoundQueryCriteria
|
from cybertools.composer.report.base import LeafQueryCriteria, CompoundQueryCriteria
|
||||||
from cybertools.composer.report.field import CalculatedField
|
from cybertools.composer.report.field import CalculatedField
|
||||||
from cybertools.composer.report.result import ResultSet, Row as BaseRow
|
from cybertools.composer.report.result import ResultSet, Row as BaseRow
|
||||||
|
from cybertools.meta.interfaces import IOptions
|
||||||
from cybertools.organize.interfaces import IWorkItems
|
from cybertools.organize.interfaces import IWorkItems
|
||||||
|
from cybertools.stateful.interfaces import IStateful
|
||||||
from cybertools.util.date import timeStamp2Date, timeStamp2ISO
|
from cybertools.util.date import timeStamp2Date, timeStamp2ISO
|
||||||
from cybertools.util.format import formatDate
|
|
||||||
from cybertools.util.jeep import Jeep
|
from cybertools.util.jeep import Jeep
|
||||||
from loops.common import adapted, baseObject
|
from loops.common import adapted, baseObject
|
||||||
|
from loops.expert.browser.export import ResultsConceptCSVExport
|
||||||
from loops.expert.browser.report import ReportConceptView
|
from loops.expert.browser.report import ReportConceptView
|
||||||
from loops.expert.field import Field, TargetField, DateField, StateField, \
|
from loops.expert.field import Field, TargetField, DateField, StateField, \
|
||||||
TextField, HtmlTextField, UrlField
|
StringField, TextField, HtmlTextField, UrlField
|
||||||
from loops.expert.field import SubReport, SubReportField
|
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.expert.report import ReportInstance
|
||||||
from loops import util
|
from loops import util
|
||||||
|
|
||||||
|
@ -48,46 +52,13 @@ class WorkStatementView(ReportConceptView):
|
||||||
reportName = 'work_statement'
|
reportName = 'work_statement'
|
||||||
|
|
||||||
|
|
||||||
|
class WorkStatementCSVExport(ResultsConceptCSVExport):
|
||||||
|
|
||||||
|
reportName = 'work_statement'
|
||||||
|
|
||||||
|
|
||||||
# fields
|
# 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):
|
class DurationField(Field):
|
||||||
|
|
||||||
cssClass = 'right'
|
cssClass = 'right'
|
||||||
|
@ -108,6 +79,31 @@ class DurationField(Field):
|
||||||
return u'%02i:%02i' % divmod(value * 60, 60)
|
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
|
# common fields
|
||||||
|
|
||||||
tasks = Field('tasks', u'Tasks',
|
tasks = Field('tasks', u'Tasks',
|
||||||
|
@ -135,6 +131,14 @@ day = TrackDateField('day', u'Day',
|
||||||
description=u'The day the work was done.',
|
description=u'The day the work was done.',
|
||||||
cssClass='center',
|
cssClass='center',
|
||||||
executionSteps=['sort', 'output'])
|
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',
|
timeStart = TrackTimeField('start', u'Start',
|
||||||
description=u'The time the unit of work was started.',
|
description=u'The time the unit of work was started.',
|
||||||
executionSteps=['sort', 'output'])
|
executionSteps=['sort', 'output'])
|
||||||
|
@ -143,15 +147,15 @@ timeEnd = TrackTimeField('end', u'End',
|
||||||
executionSteps=['output'])
|
executionSteps=['output'])
|
||||||
task = TargetField('taskId', u'Task',
|
task = TargetField('taskId', u'Task',
|
||||||
description=u'The task to which work items belong.',
|
description=u'The task to which work items belong.',
|
||||||
executionSteps=['output'])
|
executionSteps=['sort', 'output'])
|
||||||
party = TargetField('userName', u'Party',
|
party = TargetField('userName', u'Party',
|
||||||
description=u'The party (usually a person) who did the work.',
|
description=u'The party (usually a person) who did the work.',
|
||||||
fieldType='selection',
|
fieldType='selection',
|
||||||
executionSteps=['query', 'sort', 'output'])
|
executionSteps=['query', 'sort', 'output'])
|
||||||
workTitle = Field('title', u'Title',
|
workTitle = StringField('title', u'Title',
|
||||||
description=u'The short description of the work.',
|
description=u'The short description of the work.',
|
||||||
executionSteps=['output'])
|
executionSteps=['sort', 'output'])
|
||||||
workDescription = Field('description', u'Description',
|
workDescription = StringField('description', u'Description',
|
||||||
description=u'The long description of the work.',
|
description=u'The long description of the work.',
|
||||||
executionSteps=['output'])
|
executionSteps=['output'])
|
||||||
duration = DurationField('duration', u'Duration',
|
duration = DurationField('duration', u'Duration',
|
||||||
|
@ -160,11 +164,16 @@ duration = DurationField('duration', u'Duration',
|
||||||
effort = DurationField('effort', u'Effort',
|
effort = DurationField('effort', u'Effort',
|
||||||
description=u'The effort of the work.',
|
description=u'The effort of the work.',
|
||||||
executionSteps=['output', 'totals'])
|
executionSteps=['output', 'totals'])
|
||||||
state = StateField('state', u'State',
|
state = WorkItemStateField('state', u'State',
|
||||||
description=u'The state of the work.',
|
description=u'The state of the work.',
|
||||||
cssClass='center',
|
cssClass='center',
|
||||||
statesDefinition='workItemStates',
|
statesDefinition='workItemStates',
|
||||||
executionSteps=['query', 'output'])
|
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
|
# basic definitions and work report instance
|
||||||
|
@ -182,6 +191,12 @@ class WorkRow(BaseRow):
|
||||||
def getDay(self, attr):
|
def getDay(self, attr):
|
||||||
return self.context.timeStamp
|
return self.context.timeStamp
|
||||||
|
|
||||||
|
def getStart(self, attr):
|
||||||
|
return self.context.start
|
||||||
|
|
||||||
|
def getEnd(self, attr):
|
||||||
|
return self.context.end
|
||||||
|
|
||||||
def getDuration(self, attr):
|
def getDuration(self, attr):
|
||||||
value = self.context.data.get('duration')
|
value = self.context.data.get('duration')
|
||||||
if value is None:
|
if value is None:
|
||||||
|
@ -194,8 +209,10 @@ class WorkRow(BaseRow):
|
||||||
value = self.getDuration(attr)
|
value = self.getDuration(attr)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
attributeHandlers = dict(day=getDay, dayFrom=getDay, dayTo=getDay,
|
attributeHandlers = dict(day=getDay,
|
||||||
duration=getDuration, effort=getEffort)
|
dayStart=getStart, dayEnd=getEnd,
|
||||||
|
dayFrom=getDay, dayTo=getDay,
|
||||||
|
duration=getDuration, effort=getEffort,)
|
||||||
|
|
||||||
|
|
||||||
class WorkReportInstance(ReportInstance):
|
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/end">20:00</td>
|
||||||
<td class="nowrap center" tal:content="row/duration">2:30</td>
|
<td class="nowrap center" tal:content="row/duration">2:30</td>
|
||||||
<td tal:condition="python: 'Task' in work.columns">
|
<td tal:condition="python: 'Task' in work.columns">
|
||||||
<a tal:attributes="href row/objectData/url"
|
<a tal:define="data row/objectData"
|
||||||
tal:content="row/objectData/title">Task</a></td>
|
tal:attributes="href data/url"
|
||||||
|
tal:content="data/title">Task</a></td>
|
||||||
<td tal:condition="python: 'User' in work.columns">
|
<td tal:condition="python: 'User' in work.columns">
|
||||||
<a tal:attributes="href row/user/url"
|
<a tal:attributes="href row/user/url"
|
||||||
tal:content="row/user/title">John</a></td>
|
tal:content="row/user/title">John</a></td>
|
||||||
|
@ -69,10 +70,13 @@
|
||||||
<form method="post" id="addWorkitem_form" class="dialog"
|
<form method="post" id="addWorkitem_form" class="dialog"
|
||||||
xx_dojoType="dijit.form.Form"
|
xx_dojoType="dijit.form.Form"
|
||||||
tal:define="workItemTypes view/workItemTypes;
|
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="form.action" value="create_workitem" />
|
||||||
<input type="hidden" name="id"
|
<input type="hidden" name="id"
|
||||||
tal:attributes="value request/form/id|nothing" />
|
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 class="heading" i18n:translate="">Add Work Item</div>
|
||||||
<div>
|
<div>
|
||||||
<tal:type condition="view/showTypes">
|
<tal:type condition="view/showTypes">
|
||||||
|
@ -91,8 +95,17 @@
|
||||||
tal:attributes="value python:workItemTypes[0].name" />
|
tal:attributes="value python:workItemTypes[0].name" />
|
||||||
</tal:type>
|
</tal:type>
|
||||||
<label i18n:translate="" for="title">Title</label>
|
<label i18n:translate="" for="title">Title</label>
|
||||||
<div>
|
<div tal:define="titleSelection view/titleSelection">
|
||||||
<input name="title" id="title" style="width: 60em"
|
<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
|
dojoType="dijit.form.ValidationTextBox" required
|
||||||
tal:attributes="value view/title" /></div>
|
tal:attributes="value view/title" /></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,12 +120,21 @@
|
||||||
<label i18n:translate="" for="action">Action</label>
|
<label i18n:translate="" for="action">Action</label>
|
||||||
<select name="workitem.action" id="action"
|
<select name="workitem.action" id="action"
|
||||||
onChange="showIfIn(this, [['move', 'target_task'],
|
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"
|
<option tal:repeat="action view/actions"
|
||||||
tal:attributes="value action/name"
|
tal:attributes="value action/name"
|
||||||
tal:content="action/title"
|
tal:content="action/title"
|
||||||
i18n:translate="" />
|
i18n:translate="" />
|
||||||
</select>
|
</select>
|
||||||
|
<input type="hidden" name="default_date" id="default_date"
|
||||||
|
tal:attributes="value view/defaultDate" />
|
||||||
<span id="target_party" style="display: none">
|
<span id="target_party" style="display: none">
|
||||||
<label i18n:translate="delegate_to_party" for="input_party"
|
<label i18n:translate="delegate_to_party" for="input_party"
|
||||||
style="display: inline">to</label>
|
style="display: inline">to</label>
|
||||||
|
@ -149,45 +171,92 @@
|
||||||
view.getUidForObject(view.followUpTask)" />
|
view.getUidForObject(view.followUpTask)" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div id="deadline"
|
||||||
<div id="deadline"
|
tal:condition="python:'deadline' in workItemType.fields">
|
||||||
tal:condition="python:'deadline' in workItemType.fields">
|
<label i18n:translate="" for="deadline-input">Deadline</label>
|
||||||
<label i18n:translate="" for="deadline-input">Deadline</label>
|
<div id="deadline-input">
|
||||||
<div id="deadline-input">
|
<input type="text" name="deadline" style="width: 8em"
|
||||||
<input type="text" name="deadline" style="width: 8em"
|
dojoType="dijit.form.DateTextBox"
|
||||||
dojoType="dijit.form.DateTextBox"
|
tal:attributes="value view/deadline" />
|
||||||
tal:attributes="value view/deadline" /></div>
|
<input type="text" name="deadline_time" style="width: 6em"
|
||||||
</div>
|
dojoType="dijit.form.TimeTextBox"
|
||||||
<div id="start-end"
|
tal:condition="view/deadlineWithTime"
|
||||||
tal:condition="python:'start-end' in workItemType.fields">
|
tal:attributes="value view/deadlineTime" />
|
||||||
<label i18n:translate="" for="start-end-input">Start - End</label>
|
|
||||||
<div id="start-end-input">
|
|
||||||
<input type="text" name="start_date" style="width: 8em"
|
|
||||||
dojoType="dijit.form.DateTextBox"
|
|
||||||
tal:attributes="value view/date" />
|
|
||||||
<input type="text" name="start_time" style="width: 6em"
|
|
||||||
dojoType="dijit.form.TimeTextBox"
|
|
||||||
tal:attributes="value view/startTime" /> -
|
|
||||||
<input type="text" name="end_time" style="width: 6em"
|
|
||||||
dojoType="dijit.form.TimeTextBox"
|
|
||||||
tal:attributes="value view/endTime" /></div>
|
|
||||||
</div>
|
|
||||||
<div id="duration-effort"
|
|
||||||
tal:condition="python:
|
|
||||||
'duration-effort' in workItemType.fields">
|
|
||||||
<label i18n:translate=""
|
|
||||||
for="duration-effort-input">Duration / Effort (hh:mm)</label>
|
|
||||||
<div id="duration-effort-input">
|
|
||||||
<input type="text" name="duration" style="width: 5em"
|
|
||||||
dojoType="dijit.form.ValidationTextBox"
|
|
||||||
regexp="-{0,1}[0-9]{1,2}(:[0-5][0-9]){0,1}"
|
|
||||||
tal:attributes="value view/duration" /> /
|
|
||||||
<input type="text" name="effort" style="width: 5em"
|
|
||||||
dojoType="dijit.form.ValidationTextBox"
|
|
||||||
regexp="-{0,1}[0-9]{1,2}(:[0-5][0-9]){0,1}"
|
|
||||||
tal:attributes="value view/effort" /></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="priority-activity"
|
||||||
|
tal:define="priorities view/priorities;
|
||||||
|
activities view/activities"
|
||||||
|
tal:condition="python:priorities or activities">
|
||||||
|
<table style="width: auto">
|
||||||
|
<tr>
|
||||||
|
<td tal:condition="priorities">
|
||||||
|
<label i18n:translate="" for="priority">Priority</label></td>
|
||||||
|
<td tal:condition="activities">
|
||||||
|
<label i18n:translate="" for="activity">Activity</label></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td tal:condition="priorities">
|
||||||
|
<select name="priority" id="priority"
|
||||||
|
tal:define="value view/track/data/priority|nothing">
|
||||||
|
<option tal:repeat="prio priorities"
|
||||||
|
tal:attributes="value prio/name;
|
||||||
|
selected python:prio['name'] == value"
|
||||||
|
tal:content="prio/title" />
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td tal:condition="activities">
|
||||||
|
<select name="activity" id="activity"
|
||||||
|
tal:define="value view/track/data/activity|nothing">
|
||||||
|
<option tal:repeat="act activities"
|
||||||
|
tal:attributes="value act/name;
|
||||||
|
selected python:act['name'] == value"
|
||||||
|
tal:content="act/title" />
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="start-end"
|
||||||
|
tal:condition="python:'start-end' in workItemType.fields">
|
||||||
|
<label i18n:translate="" for="start-end-input">Start - End</label>
|
||||||
|
<div id="start-end-input">
|
||||||
|
<input type="text" name="start_date" style="width: 8em"
|
||||||
|
id="start_date"
|
||||||
|
dojoType="dijit.form.DateTextBox"
|
||||||
|
tal:attributes="value view/date" />
|
||||||
|
<input type="text" name="start_time" id="start_time" style="width: 6em"
|
||||||
|
dojoType="dijit.form.TimeTextBox"
|
||||||
|
tal:attributes="value view/startTime" /> -
|
||||||
|
<input type="text" name="end_time" id="end_time" style="width: 6em"
|
||||||
|
dojoType="dijit.form.TimeTextBox"
|
||||||
|
tal:attributes="value view/endTime" /></div>
|
||||||
|
</div>
|
||||||
|
<div id="daterange"
|
||||||
|
tal:condition="python:'daterange' in workItemType.fields">
|
||||||
|
<label i18n:translate="" for="daterange-input">Start - End</label>
|
||||||
|
<div id="daterange-input">
|
||||||
|
<input type="text" name="start_date" style="width: 8em"
|
||||||
|
dojoType="dijit.form.DateTextBox"
|
||||||
|
tal:attributes="value view/date" />
|
||||||
|
<input type="text" name="end_date" style="width: 8em"
|
||||||
|
dojoType="dijit.form.DateTextBox"
|
||||||
|
tal:attributes="value view/endDate" /></div>
|
||||||
|
</div>
|
||||||
|
<div id="duration-effort"
|
||||||
|
tal:condition="python:
|
||||||
|
'duration-effort' in workItemType.fields">
|
||||||
|
<label i18n:translate=""
|
||||||
|
for="duration-effort-input">Duration / Effort (hh:mm)</label>
|
||||||
|
<div id="duration-effort-input">
|
||||||
|
<input type="text" name="duration" id="duration" style="width: 5em"
|
||||||
|
dojoType="dijit.form.ValidationTextBox"
|
||||||
|
regexp="-{0,1}[0-9]{1,2}(:[0-5][0-9]){0,1}"
|
||||||
|
tal:attributes="value view/duration" /> /
|
||||||
|
<input type="text" name="effort" id="effort" style="width: 5em"
|
||||||
|
dojoType="dijit.form.ValidationTextBox"
|
||||||
|
regexp="-{0,1}[0-9]{1,2}(:[0-5][0-9]){0,1}"
|
||||||
|
tal:attributes="value view/effort" /></div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label i18n:translate="" for="comment">Comment</label>
|
<label i18n:translate="" for="comment">Comment</label>
|
||||||
<div>
|
<div>
|
||||||
|
@ -227,17 +296,23 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span i18n:translate="">Task</span>:</td>
|
<td><span i18n:translate="">Task</span>:</td>
|
||||||
<td tal:content="item/object/title"></td>
|
<td tal:content="item/object/title|nothing"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span i18n:translate="">Deadline</span>:</td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span i18n:translate="">Start - End</span>:</td>
|
<td><span i18n:translate="">Start - End</span>:</td>
|
||||||
<td><span tal:content="item/startDay" />
|
<td><span tal:content="item/startDay" />
|
||||||
<span tal:content="item/start" /> -
|
<span tal:content="item/start" /> -
|
||||||
<span tal:content="item/end" /></td>
|
<span tal:content="item/endDay" />
|
||||||
|
<span tal:content="item/end" />
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span i18n:translate="">Duration/Effort</span>:</td>
|
<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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -36,6 +36,7 @@ from zope.securitypolicy.interfaces import IRolePermissionManager
|
||||||
from zope.traversing.api import getName
|
from zope.traversing.api import getName
|
||||||
from zope.traversing.interfaces import IPhysicallyLocatable
|
from zope.traversing.interfaces import IPhysicallyLocatable
|
||||||
|
|
||||||
|
from cybertools.meta.interfaces import IOptions
|
||||||
from loops.common import adapted
|
from loops.common import adapted
|
||||||
from loops.interfaces import ILoopsObject, IConcept
|
from loops.interfaces import ILoopsObject, IConcept
|
||||||
from loops.interfaces import IAssignmentEvent, IDeassignmentEvent
|
from loops.interfaces import IAssignmentEvent, IDeassignmentEvent
|
||||||
|
@ -66,13 +67,39 @@ workspaceGroupsFolderName = 'gloops_ws'
|
||||||
|
|
||||||
# checking and querying functions
|
# 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):
|
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):
|
def canListObject(obj, noCheck=False):
|
||||||
if noCheck:
|
if noCheck:
|
||||||
return True
|
return True
|
||||||
return canAccess(obj, 'title')
|
return canAccessObject(obj)
|
||||||
|
|
||||||
def canAccessRestricted(obj):
|
def canAccessRestricted(obj):
|
||||||
return checkPermission('loops.ViewRestricted', 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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -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.
|
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.app.security.settings import Allow, Deny, Unset
|
||||||
from zope import component
|
from zope import component
|
||||||
from zope.component import adapts
|
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.organize.util import getPrincipalFolder, getGroupsFolder, getGroupId
|
||||||
from loops.security.common import overrides, setRolePermission, setPrincipalRole
|
from loops.security.common import overrides, setRolePermission, setPrincipalRole
|
||||||
from loops.security.common import allRolesExceptOwner, acquiringPredicateNames
|
from loops.security.common import allRolesExceptOwner, acquiringPredicateNames
|
||||||
|
from loops.security.common import getOption
|
||||||
from loops.security.interfaces import ISecuritySetter
|
from loops.security.interfaces import ISecuritySetter
|
||||||
from loops.versioning.interfaces import IVersionable
|
from loops.versioning.interfaces import IVersionable
|
||||||
|
|
||||||
|
logger = getLogger('loops.security')
|
||||||
|
|
||||||
|
|
||||||
class BaseSecuritySetter(object):
|
class BaseSecuritySetter(object):
|
||||||
|
|
||||||
|
@ -55,10 +59,18 @@ class BaseSecuritySetter(object):
|
||||||
def baseObject(self):
|
def baseObject(self):
|
||||||
return baseObject(self.context)
|
return baseObject(self.context)
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def adapted(self):
|
||||||
|
return adapted(self.context)
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def conceptManager(self):
|
def conceptManager(self):
|
||||||
return self.baseObject.getLoopsRoot().getConceptManager()
|
return self.baseObject.getLoopsRoot().getConceptManager()
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def options(self):
|
||||||
|
return IOptions(self.adapted)
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def typeOptions(self):
|
def typeOptions(self):
|
||||||
type = self.baseObject.getType()
|
type = self.baseObject.getType()
|
||||||
|
@ -133,11 +145,17 @@ class LoopsObjectSecuritySetter(BaseSecuritySetter):
|
||||||
|
|
||||||
def acquireRolePermissions(self):
|
def acquireRolePermissions(self):
|
||||||
settings = {}
|
settings = {}
|
||||||
for p in self.parents:
|
#rpm = IRolePermissionMap(self.baseObject)
|
||||||
if p == self.baseObject:
|
#for p, r, s in rpm.getRolesAndPermissions():
|
||||||
|
# settings[(p, r)] = s
|
||||||
|
for parent in self.parents:
|
||||||
|
if parent == self.baseObject:
|
||||||
continue
|
continue
|
||||||
secProvider = p
|
if getOption(parent, 'security.no_propagate_rolepermissions',
|
||||||
wi = p.workspaceInformation
|
checkType=False):
|
||||||
|
continue
|
||||||
|
secProvider = parent
|
||||||
|
wi = parent.workspaceInformation
|
||||||
if wi:
|
if wi:
|
||||||
if wi.propagateRolePermissions == 'none':
|
if wi.propagateRolePermissions == 'none':
|
||||||
continue
|
continue
|
||||||
|
@ -147,6 +165,10 @@ class LoopsObjectSecuritySetter(BaseSecuritySetter):
|
||||||
for p, r, s in rpm.getRolesAndPermissions():
|
for p, r, s in rpm.getRolesAndPermissions():
|
||||||
current = settings.get((p, r))
|
current = settings.get((p, r))
|
||||||
if current is None or overrides(s, current):
|
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
|
settings[(p, r)] = s
|
||||||
self.setDefaultRolePermissions()
|
self.setDefaultRolePermissions()
|
||||||
self.setRolePermissions(settings)
|
self.setRolePermissions(settings)
|
||||||
|
@ -213,14 +235,20 @@ class ConceptSecuritySetter(LoopsObjectSecuritySetter):
|
||||||
|
|
||||||
adapts(IConceptSchema)
|
adapts(IConceptSchema)
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def noPropagateRolePermissions(self):
|
||||||
|
return getOption(self.baseObject, 'security.no_propagate_rolepermissions',
|
||||||
|
checkType=False)
|
||||||
|
|
||||||
def setAcquiredSecurity(self, relation, revert=False, updated=None):
|
def setAcquiredSecurity(self, relation, revert=False, updated=None):
|
||||||
if updated and relation.second in updated:
|
if updated and relation.second in updated:
|
||||||
return
|
return
|
||||||
if relation.predicate not in self.acquiringPredicates:
|
if relation.predicate not in self.acquiringPredicates:
|
||||||
return
|
return
|
||||||
setter = ISecuritySetter(adapted(relation.second))
|
setter = ISecuritySetter(adapted(relation.second))
|
||||||
setter.setDefaultRolePermissions()
|
if not self.noPropagateRolePermissions:
|
||||||
setter.acquireRolePermissions()
|
setter.setDefaultRolePermissions()
|
||||||
|
setter.acquireRolePermissions()
|
||||||
setter.acquirePrincipalRoles()
|
setter.acquirePrincipalRoles()
|
||||||
#wi = baseObject(self.context).workspaceInformation
|
#wi = baseObject(self.context).workspaceInformation
|
||||||
#if wi and not wi.propagateParentSecurity:
|
#if wi and not wi.propagateParentSecurity:
|
||||||
|
|
|
@ -18,7 +18,7 @@ Let's set up a loops site with basic and example concepts and resources.
|
||||||
>>> concepts, resources, views = t.setup()
|
>>> concepts, resources, views = t.setup()
|
||||||
>>> loopsRoot = site['loops']
|
>>> loopsRoot = site['loops']
|
||||||
>>> len(concepts), len(resources), len(views)
|
>>> len(concepts), len(resources), len(views)
|
||||||
(33, 3, 1)
|
(35, 3, 1)
|
||||||
|
|
||||||
>>> from cybertools.tracking.btree import TrackingStorage
|
>>> from cybertools.tracking.btree import TrackingStorage
|
||||||
>>> from loops.system.job import JobRecord
|
>>> from loops.system.job import JobRecord
|
||||||
|
|
20
table.py
20
table.py
|
@ -73,7 +73,10 @@ class DataTable(AdapterBase):
|
||||||
_adapterAttributes = AdapterBase._adapterAttributes + ('columns', 'data')
|
_adapterAttributes = AdapterBase._adapterAttributes + ('columns', 'data')
|
||||||
|
|
||||||
def getColumns(self):
|
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):
|
def setColumns(self, value):
|
||||||
self.context._columns = value
|
self.context._columns = value
|
||||||
columns = property(getColumns, setColumns)
|
columns = property(getColumns, setColumns)
|
||||||
|
@ -90,6 +93,21 @@ class DataTable(AdapterBase):
|
||||||
self.context._data = OOBTree(data)
|
self.context._data = OOBTree(data)
|
||||||
data = property(getData, setData)
|
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,)
|
TypeInterfaceSourceList.typeInterfaces += (IDataTable,)
|
||||||
|
|
||||||
|
|
2
type.py
2
type.py
|
@ -115,7 +115,7 @@ class LoopsType(BaseType):
|
||||||
@Lazy
|
@Lazy
|
||||||
def typeProvider(self):
|
def typeProvider(self):
|
||||||
# TODO: unify this type attribute naming...
|
# TODO: unify this type attribute naming...
|
||||||
return self.context.resourceType
|
return getattr(self.context, 'resourceType', None)
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def options(self):
|
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
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,8 +18,6 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
View classes for versioning.
|
View classes for versioning.
|
||||||
|
|
||||||
$Id$
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope import interface, component
|
from zope import interface, component
|
||||||
|
@ -51,8 +49,11 @@ class ListVersions(BaseView):
|
||||||
def versions(self):
|
def versions(self):
|
||||||
versionable = IVersionable(self.context)
|
versionable = IVersionable(self.context)
|
||||||
versions = versionable.versions
|
versions = versionable.versions
|
||||||
|
cls = getattr(self.controller, 'versionViewClass', None)
|
||||||
for v in sorted(versions):
|
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
|
from loops.browser.resource import ResourceView
|
||||||
yield ResourceView(versions[v], self.request)
|
yield ResourceView(versions[v], self.request)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -35,7 +35,7 @@ ZCML setup):
|
||||||
Let's look what setup has provided us with:
|
Let's look what setup has provided us with:
|
||||||
|
|
||||||
>>> len(concepts)
|
>>> len(concepts)
|
||||||
22
|
24
|
||||||
|
|
||||||
Now let's add a few more concepts:
|
Now let's add a few more concepts:
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ applied in an explicit assignment.
|
||||||
|
|
||||||
>>> sorted(t['name'] for t in xrf.getConceptTypes())
|
>>> sorted(t['name'] for t in xrf.getConceptTypes())
|
||||||
[u'competence', u'customer', u'domain', u'file', u'note', u'person',
|
[u'competence', u'customer', u'domain', u'file', u'note', u'person',
|
||||||
u'predicate', u'task', u'textdocument', u'topic', u'type']
|
u'predicate', u'report', u'task', u'textdocument', u'topic', u'type']
|
||||||
>>> sorted(t['name'] for t in xrf.getPredicates())
|
>>> sorted(t['name'] for t in xrf.getPredicates())
|
||||||
[u'depends', u'issubtype', u'knows', u'ownedby', u'provides', u'requires',
|
[u'depends', u'issubtype', u'knows', u'ownedby', u'provides', u'requires',
|
||||||
u'standard']
|
u'standard']
|
||||||
|
@ -96,7 +96,7 @@ All methods that retrieve one object also returns its children and parents:
|
||||||
u'hasType'
|
u'hasType'
|
||||||
>>> sorted(c['name'] for c in ch[0]['objects'])
|
>>> sorted(c['name'] for c in ch[0]['objects'])
|
||||||
[u'competence', u'customer', u'domain', u'file', u'note', u'person',
|
[u'competence', u'customer', u'domain', u'file', u'note', u'person',
|
||||||
u'predicate', u'task', u'textdocument', u'topic', u'type']
|
u'predicate', u'report', u'task', u'textdocument', u'topic', u'type']
|
||||||
|
|
||||||
>>> pa = defaultPred['parents']
|
>>> pa = defaultPred['parents']
|
||||||
>>> len(pa)
|
>>> len(pa)
|
||||||
|
@ -115,7 +115,7 @@ We can also retrieve children and parents explicitely:
|
||||||
u'hasType'
|
u'hasType'
|
||||||
>>> sorted(c['name'] for c in ch[0]['objects'])
|
>>> sorted(c['name'] for c in ch[0]['objects'])
|
||||||
[u'competence', u'customer', u'domain', u'file', u'note', u'person',
|
[u'competence', u'customer', u'domain', u'file', u'note', u'person',
|
||||||
u'predicate', u'task', u'textdocument', u'topic', u'type']
|
u'predicate', u'report', u'task', u'textdocument', u'topic', u'type']
|
||||||
|
|
||||||
>>> pa = xrf.getParents('5')
|
>>> pa = xrf.getParents('5')
|
||||||
>>> len(pa)
|
>>> len(pa)
|
||||||
|
@ -174,14 +174,14 @@ Updating the concept map
|
||||||
|
|
||||||
>>> topicId = xrf.getObjectByName('topic')['id']
|
>>> topicId = xrf.getObjectByName('topic')['id']
|
||||||
>>> xrf.createConcept(topicId, u'zope2', u'Zope 2')
|
>>> xrf.createConcept(topicId, u'zope2', u'Zope 2')
|
||||||
{'description': u'', 'title': u'Zope 2', 'type': '36', 'id': '72',
|
{'description': u'', 'title': u'Zope 2', 'type': '38', 'id': '77',
|
||||||
'name': u'zope2'}
|
'name': u'zope2'}
|
||||||
|
|
||||||
The name of the concept is checked by a name chooser; if the corresponding
|
The name of the concept is checked by a name chooser; if the corresponding
|
||||||
parameter is empty, the name will be generated from the title.
|
parameter is empty, the name will be generated from the title.
|
||||||
|
|
||||||
>>> xrf.createConcept(topicId, u'', u'Python')
|
>>> xrf.createConcept(topicId, u'', u'Python')
|
||||||
{'description': u'', 'title': u'Python', 'type': '36', 'id': '74',
|
{'description': u'', 'title': u'Python', 'type': '38', 'id': '79',
|
||||||
'name': u'python'}
|
'name': u'python'}
|
||||||
|
|
||||||
If we try to deassign a ``hasType`` relation nothing will happen; a
|
If we try to deassign a ``hasType`` relation nothing will happen; a
|
||||||
|
|
Loading…
Add table
Reference in a new issue