merge bbmaster2 into bbmaster resulting in branch 2master

This commit is contained in:
Helmut Merz 2020-03-04 18:05:37 +01:00
commit b471deef43
136 changed files with 4196 additions and 790 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
*.pyc
*.pyo
dist/
*.project
*.pydevproject
*.sublime-project

11
MANIFEST.in Normal file
View file

@ -0,0 +1,11 @@
global-include *.cfg
global-include *.css *.js
global-include *.gif *.jpg *.png
global-include *.dmp
global-include *.md *.txt
global-include *.mo *.po *.pot
global-include *.pdf
global-include *.pt
global-include *.zcml
graft loops/integrator/testdata

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# Introduction
This is the main part of the code of the semantic
web application platform *loops*, based on
Zope 3 / bluebream.
More information: see https://www.cyberconcepts.org.

View file

@ -737,7 +737,9 @@ on data provided in this form:
>>> component.provideAdapter(NameChooser)
>>> request = TestRequest(form={'title': u'Test Note',
... 'form.type': u'.loops/concepts/note'})
... 'form.type': u'.loops/concepts/note',
... 'contentType': u'text/restructured',
... 'linkUrl': u'http://'})
>>> view = NodeView(m112, request)
>>> cont = CreateObject(view, request)
>>> cont.update()
@ -802,7 +804,7 @@ The new technique uses the ``fields`` and ``data`` attributes...
linkText textline False None
>>> view.data
{'linkUrl': u'http://', 'contentType': 'text/restructured', 'data': u'',
{'linkUrl': u'http://', 'contentType': u'text/restructured', 'data': u'',
'linkText': u'', 'title': u'Test Note'}
The object is changed via a FormController adapter created for
@ -913,6 +915,12 @@ relates ISO country codes with the full name of the country.
>>> sorted(adapted(concepts['countries']).data.items())
[('at', ['Austria']), ('de', ['Germany'])]
>>> countries.dataAsRecords()
[{'value': 'Austria', 'key': 'at'}, {'value': 'Germany', 'key': 'de'}]
>>> countries.getRowsByValue('value', 'Germany')
[{'value': 'Germany', 'key': 'de'}]
Caching
=======
@ -932,6 +940,12 @@ Security
>>> from loops.security.browser import admin, audit
Paster Shell Utilities - Repair Scripts
=======================================
>>> from loops.repair.base import removeRecords
Import/Export
=============

View file

@ -1,22 +1,17 @@
#
# Copyright (c) 2008 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
#
# package loops
"""
# intid monkey patch for avoiding ForbiddenAttribute error
from zope import component
from zope.intid.interfaces import IIntIds
from zope import intid
from zope.security.proxy import removeSecurityProxy
def queryId(self, ob, default=None):
try:
return self.getId(removeSecurityProxy(ob))
except KeyError:
return default
intid.IntIds.queryId = queryId
$Id$
"""

View file

@ -1,5 +1,3 @@
# -*- coding: UTF-8 -*-
# -*- Mode: Python; py-indent-offset: 4 -*-
#
# Copyright (c) 2019 Helmut Merz helmutm@cy55.de
#
@ -19,7 +17,7 @@
#
"""
The loops container class.
Implementation of loops root object.
"""
from zope.app.container.btree import BTreeContainer

View file

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

View file

@ -22,7 +22,7 @@ Common base class for loops browser view classes.
from cgi import parse_qs, parse_qsl
#import mimetypes # use more specific assignments from cybertools.text
from datetime import datetime
from datetime import date, datetime
from logging import getLogger
import re
from time import strptime
@ -62,17 +62,21 @@ from cybertools.stateful.interfaces import IStateful
from cybertools.text import mimetypes
from cybertools.typology.interfaces import IType, ITypeManager
from cybertools.util.date import toLocalTime
from cybertools.util.format import formatDate
from cybertools.util.jeep import Jeep
from loops.browser.util import normalizeForUrl
from loops.common import adapted, baseObject
from loops.config.base import DummyOptions
from loops.i18n.browser import I18NView
from loops.interfaces import IResource, IView, INode, ITypeConcept
from loops.organize.personal import favorite
from loops.organize.party import getPersonForUser
from loops.organize.tracking import access
from loops.organize.util import getRolesForPrincipal
from loops.resource import Resource
from loops.security.common import checkPermission
from loops.security.common import canAccessObject, canListObject, canWriteObject
from loops.security.common import canEditRestricted
from loops.type import ITypeConcept, LoopsTypeInfo
from loops import util
from loops.util import _, saveRequest
@ -137,7 +141,58 @@ class EditForm(form.EditForm):
return parentUrl + '/contents.html'
class BaseView(GenericView, I18NView):
class SortableMixin(object):
@Lazy
def sortInfo(self):
result = {}
for k, v in self.request.form.items():
if k.startswith('sortinfo_'):
tableName = k[len('sortinfo_'):]
if ',' in v:
fn, dir = v.split(',')
else:
fn = v
dir = 'asc'
result[tableName] = dict(
colName=fn, ascending=(dir=='asc'), fparam=v)
result = favorite.updateSortInfo(getPersonForUser(
self.context, self.request), self.target, result)
return result
def isSortableColumn(self, tableName, colName):
return False # overwrite in subclass
def getSortUrl(self, tableName, colName):
url = str(self.request.URL)
paramChar = '?' in url and '&' or '?'
si = self.sortInfo.get(tableName)
if si is not None and si.get('colName') == colName:
dir = si['ascending'] and 'desc' or 'asc'
else:
dir = 'asc'
return '%s%ssortinfo_%s=%s,%s' % (url, paramChar, tableName, colName, dir)
def getSortParams(self, tableName):
url = str(self.request.URL)
paramChar = '?' in url and '&' or '?'
si = self.sortInfo.get(tableName)
if si is not None:
colName = si['colName']
dir = si['ascending'] and 'asc' or 'desc'
return '%ssortinfo_%s=%s,%s' % (paramChar, tableName, colName, dir)
return ''
def getSortImage(self, tableName, colName):
si = self.sortInfo.get(tableName)
if si is not None and si.get('colName') == colName:
if si['ascending']:
return '/@@/cybertools.icons/arrowdown.gif'
else:
return '/@@/cybertools.icons/arrowup.gif'
class BaseView(GenericView, I18NView, SortableMixin):
actions = {}
portlet_actions = []
@ -146,6 +201,7 @@ class BaseView(GenericView, I18NView):
icon = None
modeName = 'view'
isToplevel = False
isVisible = True
def __init__(self, context, request):
context = baseObject(context)
@ -163,6 +219,10 @@ class BaseView(GenericView, I18NView):
pass
saveRequest(request)
def todayFormatted(self):
return formatDate(date.today(), 'date', 'short',
self.languageInfo.language)
def checkPermissions(self):
return canAccessObject(self.context)
@ -214,6 +274,16 @@ class BaseView(GenericView, I18NView):
result.append(view)
return result
@Lazy
def urlParamString(self):
return self.getUrlParamString()
def getUrlParamString(self):
qs = self.request.get('QUERY_STRING')
if qs:
return '?' + qs
return ''
@Lazy
def principalId(self):
principal = self.request.principal
@ -347,6 +417,10 @@ class BaseView(GenericView, I18NView):
def isPartOfPredicate(self):
return self.conceptManager.get('ispartof')
@Lazy
def queryTargetPredicate(self):
return self.conceptManager.get('querytarget')
@Lazy
def memberPredicate(self):
return self.conceptManager.get('ismember')
@ -395,6 +469,10 @@ class BaseView(GenericView, I18NView):
def description(self):
return self.adapted.description
@Lazy
def tabTitle(self):
return u'Info'
@Lazy
def additionalInfos(self):
return []
@ -747,6 +825,8 @@ class BaseView(GenericView, I18NView):
return result
def checkState(self):
if checkPermission('loops.ManageSite', self.context):
return True
if not self.allStates:
return True
for stf in self.allStates:
@ -821,6 +901,10 @@ class BaseView(GenericView, I18NView):
def canAccessRestricted(self):
return checkPermission('loops.ViewRestricted', self.context)
@Lazy
def canEditRestricted(self):
return canEditRestricted(self.context)
def openEditWindow(self, viewName='edit.html'):
if self.editable:
if checkPermission('loops.ManageSite', self.context):
@ -943,6 +1027,12 @@ class BaseView(GenericView, I18NView):
jsCall = 'dojo.require("dojox.image.Lightbox");'
self.controller.macros.register('js-execute', jsCall, jsCall=jsCall)
def registerDojoComboBox(self):
self.registerDojo()
jsCall = ('dojo.require("dijit.form.ComboBox");')
self.controller.macros.register('js-execute',
'dojo.require.ComboBox', jsCall=jsCall)
def registerDojoFormAll(self):
self.registerDojo()
self.registerDojoEditor()
@ -996,6 +1086,7 @@ class LoggedIn(object):
params = parse_qsl(qs)
params = [(k, v) for k, v in params if k != 'loops.messages.top:record']
params.append(('loops.messages.top:record', message.encode('UTF-8')))
url = url.encode('utf-8')
return '%s?%s' % (url, urlencode(params))
# vocabulary stuff

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2016 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
@ -254,18 +254,35 @@ class ConceptView(BaseView):
result.append(view)
return result
def viewModes(self):
modes = Jeep()
current = self.request.form.get('loops.viewName')
parts = (self.options('view_tabs') or
self.typeOptions('view_tabs') or [])
if not parts:
return modes
activeMode = None
for p in parts:
view = component.queryMultiAdapter(
(self.adapted, self.request), name=p)
if view is None:
view = component.queryMultiAdapter(
(self.context, self.request), name=p)
if view is None:
continue
active = (activeMode is None and p == current)
if active:
activeMode = p
url = '%s?loops.viewName=%s' % (self.targetUrl, p)
modes.append(ViewMode(p, view.tabTitle, url, active))
if activeMode is None:
modes[0].active = True
return modes
@Lazy
def adapted(self):
return adapted(self.context, self.languageInfo)
@Lazy
def title(self):
return self.adapted.title or getName(self.context)
@Lazy
def description(self):
return self.adapted.description
@Lazy
def targetUrl(self):
return self.nodeView.getUrlForTarget(self.context)
@ -282,8 +299,17 @@ class ConceptView(BaseView):
def breadcrumbsTitle(self):
return self.title
@Lazy
def showInBreadcrumbs(self):
return (self.options('show_in_breadcrumbs') or
self.typeOptions('show_in_breadcrumbs'))
@Lazy
def breadcrumbsParent(self):
for p in self.context.getParents([self.defaultPredicate]):
view = self.nodeView.getViewForTarget(p)
if view.showInBreadcrumbs:
return view
return None
def getData(self, omit=('title', 'description')):
@ -389,7 +415,8 @@ class ConceptView(BaseView):
children = getChildren
def childrenAlphaGroups(self, predicates=None):
result = Jeep()
#result = Jeep()
result = {}
rels = self.getChildren(predicates=predicates or [self.defaultPredicate],
topLevelOnly=False, sort=False)
rels = sorted(rels, key=lambda r: r.title.lower())
@ -449,7 +476,7 @@ class ConceptView(BaseView):
if r.order != pos:
r.order = pos
def getResources(self):
def getResources(self, relView=None, sort='default'):
form = self.request.form
#if form.get('loops.viewName') == 'index.html' and self.editable:
if self.editable:
@ -458,13 +485,17 @@ class ConceptView(BaseView):
tokens = form.get('resources_tokens')
if tokens:
self.reorderResources(tokens)
from loops.browser.resource import ResourceRelationView
if relView is None:
from loops.browser.resource import ResourceRelationView
relView = ResourceRelationView
from loops.organize.personal.browser.filter import FilterView
fv = FilterView(self.context, self.request)
rels = self.context.getResourceRelations()
rels = self.context.getResourceRelations(sort=sort)
for r in rels:
if fv.check(r.first):
yield ResourceRelationView(r, self.request, contextIsSecond=True)
view = relView(r, self.request, contextIsSecond=True)
if view.checkState():
yield view
def resources(self):
return self.getResources()

View file

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

View file

@ -125,7 +125,7 @@
<containerViews
for="loops.interfaces.ILoops"
index="zope.View"
index="zope.ManageSite"
contents="loops.ManageSite"
add="loops.ManageSite" />
@ -365,7 +365,7 @@
<containerViews
for="loops.interfaces.IViewManager"
index="zope.View"
index="zope.ManageSite"
add="loops.ManageSite" />
<menuItem
@ -571,6 +571,14 @@
factory="loops.browser.concept.TabbedPage"
permission="zope.View" />
<!-- delete object action -->
<page
name="delete_object"
for="loops.interfaces.INode"
class="loops.browser.form.DeleteObject"
permission="zope.ManageContent" />
<!-- dialogs/forms (end-user views) -->
<page

View file

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

View file

@ -2,7 +2,7 @@
<metal:info define-macro="object_info"
tal:define="item nocall:view/item">
tal:define="item nocall:view/targetItem">
<table class="object_info" width="400">
<tr>
<td colspan="2"><h2 i18n:translate="">Object Information</h2><br /></td>
@ -52,7 +52,7 @@
<metal:info define-macro="meta_info"
tal:define="item nocall:view/item">
tal:define="item nocall:view/targetItem">
<table class="object_info" width="400">
<tr>
<td colspan="2">

View file

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

View file

@ -47,6 +47,35 @@ 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 setIf(node, cond, acts) {
if (node.value == cond) {
dojo.forEach(acts, function(act) {
target = dijit.byId(act[0]);
target.setValue(act[1]);
})
}
}
function setIfN(node, conds, acts) {
dojo.forEach(conds, function(cond) {
if (node.value == cond) {
dojo.forEach(acts, function(act) {
target = dijit.byId(act[0]);
target.setValue(act[1]);
})
}
})
}
function destroyWidgets(node) {
dojo.forEach(dojo.query('[widgetId]', node), function(n) {
w = dijit.byNode(n);
@ -103,7 +132,7 @@ function submitReplacing(targetId, formId, url) {
mimetype: "text/html",
load: function(response, ioArgs) {
replaceNode(response, targetId);
return resonse;
return response;
}
})
}
@ -115,7 +144,7 @@ function xhrSubmitPopup(formId, url) {
mimetype: "text/html",
load: function(response, ioArgs) {
window.close();
return resonse;
return response;
}
});
}

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
# Copyright (c) 2017 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
@ -86,10 +86,14 @@ class NodeView(BaseView):
super(NodeView, self).__init__(context, request)
self.viewAnnotations.setdefault('nodeView', self)
self.viewAnnotations.setdefault('node', self.context)
viewConfig = getViewConfiguration(context, request)
self.setSkin(viewConfig.get('skinName'))
self.setSkin(self.viewConfig.get('skinName'))
def __call__(self, *args, **kw):
if self.nodeType == 'raw':
vn = self.context.viewName
if vn:
self.request.response.setHeader('content-type', vn)
return self.context.body
tv = self.viewAnnotations.get('targetView')
if tv is not None:
if tv.isToplevel:
@ -98,6 +102,29 @@ class NodeView(BaseView):
self.controller.setMainPage()
return super(NodeView, self).__call__(*args, **kw)
@Lazy
def viewConfig(self):
return getViewConfiguration(self.context, self.request)
@Lazy
def viewConfigOptions(self):
result = {}
for opt in self.viewConfig.get('options') or []:
if ':' in opt:
k, v = opt.split(':', 1)
result[k] = v.split(',')
else:
result[opt] = True
return result
@Lazy
def copyright(self):
cr = self.viewConfigOptions.get('copyright')
if cr:
return cr[0]
cr = self.globalOptions('copyright')
return cr and cr[0] or 'cyberconcepts.org team'
@Lazy
def macro(self):
return self.template.macros['content']
@ -115,7 +142,9 @@ class NodeView(BaseView):
parts.extend(getParts(n))
return parts
def update(self):
def update(self, topLevel=True):
if topLevel and self.view != self:
return self.view.update(False)
result = super(NodeView, self).update()
self.recordAccess()
return result
@ -129,7 +158,7 @@ class NodeView(BaseView):
return []
menu = self.menu
data = [dict(label=menu.title, url=menu.url)]
menuItem = self.nearestMenuItem
menuItem = self.getNearestMenuItem(all=True)
if menuItem != menu.context:
data.append(dict(label=menuItem.title,
url=absoluteURL(menuItem, self.request)))
@ -140,6 +169,9 @@ class NodeView(BaseView):
url=absoluteURL(p, self.request)))
if self.virtualTarget:
data.extend(self.virtualTarget.breadcrumbs())
if data and not '?' in data[-1]['url']:
if self.urlParamString:
data[-1]['url'] += self.urlParamString
return data
def viewModes(self):
@ -366,6 +398,10 @@ class NodeView(BaseView):
def editable(self):
return canWrite(self.context, 'body')
def hasTopPage(self, name):
page = self.topMenu.context.get(name)
return page is not None
# menu stuff
@Lazy
@ -411,8 +447,9 @@ class NodeView(BaseView):
@Lazy
def menuItems(self):
return [NodeView(child, self.request)
items = [NodeView(child, self.request).view
for child in self.context.getMenuItems()]
return [item for item in items if item.isVisible]
@Lazy
def parents(self):
@ -420,10 +457,13 @@ class NodeView(BaseView):
@Lazy
def nearestMenuItem(self):
return self.getNearestMenuItem()
def getNearestMenuItem(self, all=False):
menu = self.menuObject
menuItem = None
for p in [self.context] + self.parents:
if not p.isMenuItem():
if not all and not p.isMenuItem():
menuItem = None
elif menuItem is None:
menuItem = p
@ -469,7 +509,7 @@ class NodeView(BaseView):
def targetView(self, name='index.html', methodName='show'):
if name == 'index.html': # only when called for default view
tv = self.viewAnnotations.get('targetView')
if tv is not None:
if tv is not None and callable(tv):
return tv()
if '?' in name:
name, params = name.split('?', 1)
@ -567,12 +607,21 @@ class NodeView(BaseView):
""" Return URL of given target view given as .XXX URL.
"""
if isinstance(target, BaseView):
miu = self.getMenuItemUrlForTarget(target.context)
if miu is not None:
return miu
return self.makeTargetUrl(self.url, target.uniqueId, target.title)
else:
target = baseObject(target)
return self.makeTargetUrl(self.url, util.getUidForObject(target),
target.title)
def getMenuItemUrlForTarget(self, tobj):
for node in tobj.getClients():
if node.nodeType == 'page' and node.getMenu() == self.menuObject:
return absoluteURL(node, self.request)
def getActions(self, category='object', page=None, target=None):
actions = []
#self.registerDojo()
@ -976,7 +1025,8 @@ class NodeTraverser(ItemTraverser):
if context.nodeType == 'menu':
setViewConfiguration(context, request)
if name == '.loops':
return self.context.getLoopsRoot()
name = self.getTargetUid(request)
#return self.context.getLoopsRoot()
if name.startswith('.'):
name = self.cleanUpTraversalStack(request, name)[1:]
target = self.getTarget(name)
@ -1008,17 +1058,34 @@ class NodeTraverser(ItemTraverser):
raise
return obj
def getTargetUid(self, request):
parent = self.context.getLoopsRoot()
stack = request._traversal_stack
for i in range(2):
name = stack.pop()
obj = parent.get(name)
if not obj:
return name
parent = obj
return '.' + util.getUidForObject(obj)
def cleanUpTraversalStack(self, request, name):
traversalStack = request._traversal_stack
while traversalStack and traversalStack[0].startswith('.'):
#traversalStack = request._traversal_stack
#while traversalStack and traversalStack[0].startswith('.'):
# skip obsolete target references in the url
name = traversalStack.pop(0)
# name = traversalStack.pop(0)
traversedNames = request._traversed_names
if traversedNames:
lastTraversed = traversedNames[-1]
if lastTraversed.startswith('.') and lastTraversed != name:
for n in list(traversedNames):
if n.startswith('.'):
# remove obsolete target refs
traversedNames.remove(n)
#if traversedNames:
# lastTraversed = traversedNames[-1]
# if lastTraversed.startswith('.') and lastTraversed != name:
# let <base .../> tag show the current object
traversedNames[-1] = name
# traversedNames[-1] = name
# let <base .../> tag show the current object
traversedNames.append(name)
return name
def getTarget(self, name):

View file

@ -30,7 +30,7 @@
item nocall:target"
tal:attributes="class string:content-$level;
id id;
ondblclick python: target.openEditWindow('configure.html')">
ondblclick python:target.openEditWindow('configure.html')">
<metal:body use-macro="item/macro">
The body
</metal:body>
@ -41,17 +41,22 @@
<metal:body define-macro="conceptbody">
<tal:body define="body item/body;">
<tal:body define="body item/body;
itemNum view/itemNum;
id string:$itemNum.body">
<div class="content-1" id="1"
tal:attributes="class string:content-$level;
id string:${view/itemNum}.body;
ondblclick python: item.openEditWindow('configure.html')">
ondblclick python:item.openEditWindow('configure.html')">
<span tal:content="structure body">Node Body</span>
</div>
<tal:concepts define="item nocall:item/targetObjectView;
macro item/macro">
<div metal:use-macro="macro" />
</tal:concepts>
<div tal:define="item nocall:item/targetObjectView;
macro item/macro">
<div tal:attributes="class string:content-$level;
id id;">
<div metal:use-macro="macro" />
</div>
</div>
</tal:body>
</metal:body>
@ -328,11 +333,12 @@
<metal:login define-macro="login">
<div>
<a href="login.html"
i18n:translate="">Log in</a></div>
tal:attributes="href string:${view/topMenu/url}/login.html"
i18n:translate="">Log in</a></div>
<div tal:define="register python:view.globalOptions('provideLogin')"
tal:condition="register">
<a tal:condition="python:register != True"
tal:attributes="href python:register[0]"
tal:condition="python:register and register != True">
<a tal:define="reg python:register[0]"
tal:attributes="href string:${view/topMenu/url}/$reg"
i18n:translate="">Register new member</a></div>
</metal:login>

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -20,6 +20,7 @@
View class for resource objects.
"""
import os.path
import urllib
from zope.cachedescriptors.property import Lazy
from zope import component
@ -47,7 +48,7 @@ from loops.browser.common import EditForm, BaseView
from loops.browser.concept import BaseRelationView, ConceptRelationView
from loops.browser.concept import ConceptConfigureView
from loops.browser.node import NodeView, node_macros
from loops.common import adapted, NameChooser, normalizeName
from loops.common import adapted, baseObject, NameChooser, normalizeName
from loops.interfaces import IBaseResource, IDocument, ITextDocument
from loops.interfaces import IMediaAsset as legacy_IMediaAsset
from loops.interfaces import ITypeConcept
@ -196,6 +197,9 @@ class ResourceView(BaseView):
context = self.context
ct = context.contentType
response = self.request.response
if self.typeOptions('x_robots_tag_header', None) is not None:
tagVal = ', '.join(self.typeOptions('x_robots_tag_header'))
response.setHeader('X-Robots-Tag', tagVal)
self.recordAccess('show', target=self.uniqueId)
if ct.startswith('image/'):
#response.setHeader('Cache-Control', 'public,max-age=86400')
@ -214,8 +218,18 @@ class ResourceView(BaseView):
data = context.data
if useAttachment:
if filename is None:
filename = (adapted(self.context).localFilename or
filename = (adapted(self.context).localFilename or
getName(self.context))
if self.typeOptions('use_title_for_download_filename'):
base, ext = os.path.splitext(filename)
filename = context.title
vr = IVersionable(baseObject(context))
if len(vr.versions) > 0:
filename = vr.generateName(filename, ext, vr.versionId)
else:
if not filename.endswith(ext):
filename += ext
filename = filename.encode('UTF-8')
if self.typeOptions('no_normalize_download_filename'):
filename = '"%s"' % filename
else:
@ -262,11 +276,17 @@ class ResourceView(BaseView):
#return util.toUnicode(wp.render(self.request))
return super(ResourceView, self).renderText(text, contentType)
showMore = True
def renderShortText(self):
return self.renderDescription() or self.createShortText(self.render())
def createShortText(self, text=None):
return extractFirstPart(text or self.render())
text = (text or self.render()).strip()
shortText = extractFirstPart(text)
if shortText == text:
self.showMore = False
return shortText
def download(self):
""" Force download, e.g. of a PDF file """
@ -471,4 +491,3 @@ class NoteView(DocumentView):
def linkUrl(self):
ad = self.typeAdapter
return ad and ad.linkUrl or ''

View file

@ -10,7 +10,7 @@
<div metal:use-macro="views/node_macros/object_actions" />
</tal:actions>
<h1><a tal:omit-tag="python: level > 1"
tal:attributes="href request/URL"
tal:attributes="href view/requestUrl"
tal:content="item/title">Title</a></h1>
<tal:desc define="description description|item/renderedDescription"
condition="description">
@ -51,7 +51,7 @@
<div tal:attributes="ondblclick python: item.openEditWindow('edit.html')">
<div metal:use-macro="views/node_macros/object_actions" />
<h1><a tal:omit-tag="python: level > 1"
tal:attributes="href request/URL"
tal:attributes="href view/requestUrl"
tal:content="item/title">Title</a></h1><br />
<img tal:attributes="src
string:${view/url}/.${view/targetId}/view?version=this" />
@ -96,6 +96,7 @@
</a>
</span>
</div>
<metal:custom define-slot="custom_info" />
<metal:fields use-macro="view/comment_macros/comments" />
</div>
</metal:block>

View file

@ -1,6 +1,4 @@
"""
$Id$
"""
# package loops.browser.skin
from cybertools.browser.liquid import Liquid
from cybertools.browser.blue import Blue

View file

@ -69,9 +69,9 @@
metal:define-macro="footer">
<metal:footer define-slot="footer">
&copy; Copyright <span tal:replace="view/currentYear" />,
cyberconcepts IT-Consulting Dr. Helmut Merz
(<a href="#"
tal:attributes="href string:${view/topMenu/url}/impressum">Impressum</a>)
<span tal:replace="view/topMenu/copyright" />
(<a i18n:translate=""
tal:attributes="href string:${view/topMenu/url}/impressum">Impressum</a>)
<br />
Powered by
<b><a href="http://www.wissen-statt-suchen.de">loops</a></b> &middot;

View file

@ -20,6 +20,7 @@ body {
#portlets {
margin-top: 1em;
background-color: #ffffff;
}
ul.view-modes {
@ -108,6 +109,14 @@ thead th {
background: none;
}
/* printing */
@media print {
.noprint {
display: none;
}
}
/* class-specific */
.breadcrumbs td {
@ -253,10 +262,26 @@ table.records th, table.records td {
border: 1px solid lightgrey;
}
table.report {
position: relative;
z-index: 99;
background: white;
}
table.report th {
border-bottom: 1px solid #bbbbbb;
font-weight: bold;
}
table.report td {
border-bottom: 1px dotted #dddddd;
vertical-align: top;
}
.report-meta table {
width: auto;
}
dl.docutils dt {
font-weight: bold;
margin-top: 0.3em;

View file

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

View file

@ -10,7 +10,7 @@
method="post" name="listing" action="."
tal:define="target nocall:view/target"
tal:condition="python: target or items"
tal:attributes="action request/URL">
tal:attributes="action view/requestUrl">
<input type="hidden" name="action" value="assign"
tal:attributes="value action" />
<table class="listing" summary="Currently assigned"
@ -82,7 +82,7 @@
<fieldset>
<legend i18n:translate="">Create Target</legend>
<form method="post" name="listing" action="."
tal:attributes="action request/URL">
tal:attributes="action view/requestUrl">
<input type="hidden" name="action" value="create" />
<div class="row">
<span i18n:translate="">Name</span>
@ -113,7 +113,7 @@
<metal:search define-macro="search">
<form method="post" name="listing" action="."
tal:attributes="action request/URL">
tal:attributes="action view/requestUrl">
<input type="hidden" name="action" value="search" />
<div class="row"
tal:define="searchTerm request/searchTerm | nothing;

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2007 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
"""
Adapters and others classes for analyzing resources.
$Id$
"""
from itertools import tee
@ -41,6 +39,7 @@ from loops.resource import Resource
from loops.setup import addAndConfigureObject
from loops.type import TypeInterfaceSourceList
logger = getLogger('Classifier')
TypeInterfaceSourceList.typeInterfaces += (IClassifier,)
@ -102,15 +101,15 @@ class Classifier(AdapterBase):
if resource not in resources:
concept.assignResource(resource, predicate)
message = u'Assigning: %s %s %s'
self.log(message % (resource.title, predicate.title, concept.title), 5)
else:
message = u'Already assigned: %s %s %s'
self.log(message % (resource.title, predicate.title, concept.title), 4)
self.log(message % (resource.title, predicate.title, concept.title), 4)
def log(self, message, level=5):
if level >= self.logLevel:
#print 'Classifier %s:' % getName(self.context), message
getLogger('Classifier').info(
u'%s: %s' % (getName(self.context), message))
logger.info(u'%s: %s' % (getName(self.context), message))
class Extractor(object):

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2007 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -18,17 +18,20 @@
"""
View class(es) for resource classifiers.
$Id$
"""
from logging import getLogger
import transaction
from zope import interface, component
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from zope.traversing.api import getName
from loops.browser.concept import ConceptView
from loops.common import adapted
logger = getLogger('ClassifierView')
class ClassifierView(ConceptView):
@ -42,12 +45,18 @@ class ClassifierView(ConceptView):
if 'update' in self.request.form:
cta = adapted(self.context)
if cta is not None:
for r in collectResources(self.context):
for idx, r in enumerate(collectResources(self.context)):
if idx % 1000 == 0:
logger.info('Committing, resource # %s' % idx)
transaction.commit()
cta.process(r)
logger.info('Finished processing')
transaction.commit()
return True
def collectResources(concept, checkedConcepts=None, result=None):
logger.info('Start collecting resources for %s' % getName(concept))
if result is None:
result = []
if checkedConcepts is None:
@ -59,4 +68,5 @@ def collectResources(concept, checkedConcepts=None, result=None):
if c not in checkedConcepts:
checkedConcepts.append(c)
collectResources(c, checkedConcepts, result)
logger.info('Collected %s resources' % len(result))
return result

View file

@ -1,7 +1,6 @@
import unittest, doctest
from zope.interface.verify import verifyClass
#from loops.versioning import versionable
class Test(unittest.TestCase):
"Basic tests for the classifier sub-package."

View file

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

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2017 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -34,6 +34,7 @@ from loops.browser.concept import ConceptRelationView as \
BaseConceptRelationView
from loops.browser.resource import ResourceView as BaseResourceView
from loops.common import adapted, baseObject
from loops.util import _
standard_template = standard.standard_template
@ -54,42 +55,6 @@ class Base(object):
def sectionType(self):
return self.conceptManager['section']
@Lazy
def isPartOfPredicate(self):
return self.conceptManager['ispartof']
@Lazy
def showNavigation(self):
return self.typeOptions.show_navigation
@Lazy
def breadcrumbsParent(self):
for p in self.context.getParents([self.isPartOfPredicate]):
return self.nodeView.getViewForTarget(p)
@Lazy
def neighbours(self):
pred = succ = None
parent = self.breadcrumbsParent
if parent is not None:
myself = None
children = list(parent.context.getChildren([self.isPartOfPredicate]))
for idx, c in enumerate(children):
if c == self.context:
if idx > 0:
pred = self.nodeView.getViewForTarget(children[idx-1])
if idx < len(children) - 1:
succ = self.nodeView.getViewForTarget(children[idx+1])
return pred, succ
@Lazy
def predecessor(self):
return self.neighbours[0]
@Lazy
def successor(self):
return self.neighbours[1]
@Lazy
def tabview(self):
if self.editable:
@ -107,6 +72,7 @@ class Base(object):
@Lazy
def textResources(self):
self.images = [[]]
self.otherResources = []
result = []
idx = 0
for rv in self.getResources():
@ -115,7 +81,7 @@ class Base(object):
idx += 1
result.append(rv)
self.images.append([])
else:
elif rv.context.contentType.startswith('image/'):
self.registerDojoLightbox()
url = self.nodeView.getUrlForTarget(rv.context)
src = '%s/mediaasset.html?v=small' % url
@ -123,6 +89,8 @@ class Base(object):
img = dict(src=src, fullImageUrl=fullSrc, title=rv.title,
description=rv.description, url=url, object=rv)
self.images[idx].append(img)
else:
self.otherResources.append(rv)
return result
def getDocumentTypeForResource(self, r):
@ -178,9 +146,47 @@ class SectionView(Base, ConceptView):
def macro(self):
return book_template.macros['section']
@Lazy
def isPartOfPredicate(self):
return self.conceptManager['ispartof']
@Lazy
def breadcrumbsParent(self):
for p in self.context.getParents([self.isPartOfPredicate]):
return self.nodeView.getViewForTarget(p)
@Lazy
def showNavigation(self):
return self.typeOptions.show_navigation
@Lazy
def neighbours(self):
pred = succ = None
parent = self.breadcrumbsParent
if parent is not None:
myself = None
children = list(parent.context.getChildren([self.isPartOfPredicate]))
for idx, c in enumerate(children):
if c == self.context:
if idx > 0:
pred = self.nodeView.getViewForTarget(children[idx-1])
if idx < len(children) - 1:
succ = self.nodeView.getViewForTarget(children[idx+1])
return pred, succ
@Lazy
def predecessor(self):
return self.neighbours[0]
@Lazy
def successor(self):
return self.neighbours[1]
class TopicView(Base, ConceptView):
tabTitle = _(u'title_bookTopicView')
@Lazy
def macro(self):
return book_template.macros['topic']

View file

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

View file

@ -45,7 +45,7 @@ from cybertools.typology.interfaces import IType, ITypeManager
from cybertools.util.jeep import Jeep
from loops.base import ParentInfo
from loops.common import adapted, AdapterBase
from loops.common import adapted, baseObject, AdapterBase
from loops.i18n.common import I18NValue
from loops.interfaces import IConcept, IConceptRelation, IConceptView
from loops.interfaces import IResource
@ -490,14 +490,14 @@ class IndexAttributes(object):
title = u''
if isinstance(title, I18NValue):
title = ' '.join(title.values())
return ' '.join((getName(context), title)).strip()
return ' '.join((getName(baseObject(context)), title)).strip()
def date(self):
if self.adaptedIndexAttributes is not None:
return self.adaptedIndexAttributes.date()
def creators(self):
cr = IZopeDublinCore(self.context).creators or []
cr = IZopeDublinCore(baseObject(self.context)).creators or []
pau = component.getUtility(IAuthentication)
creators = []
for c in cr:
@ -514,7 +514,7 @@ class IndexAttributes(object):
def identifier(self):
id = getattr(self.adapted, 'identifier', None)
if id is None:
return getName(self.context)
return getName(baseObject(self.context))
return id
def keywords(self):

View file

@ -1,6 +1,7 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:i18n="http://namespaces.zope.org/i18n"
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="loops">
<i18n:registerTranslations directory="locales" />
@ -478,6 +479,19 @@
component="loops.view.NodeTypeSourceList"
name="loops.nodeTypeSource" />
<!-- Markdown support -->
<utility
component="loops.util.MarkdownSourceFactory"
name="loops.util.markdown"
/>
<browser:view
name=""
for="loops.util.IMarkdownSource"
class="loops.util.MarkdownToHTMLRenderer"
permission="zope.Public" />
<include package=".browser" />
<include package=".classifier" />

View file

@ -1,11 +1,14 @@
# types
type(u'query', u'Abfrage', options=u'',
typeInterface='loops.expert.concept.IQueryConcept', viewName=u'')
type(u'datatable', u'Datentabelle', options=u'action.portlet:edit_concept',
typeInterface='loops.table.IDataTable', viewName=u'')
type(u'task', u'Aufgabe', options=u'',
typeInterface='loops.knowledge.interfaces.ITask', viewName=u'')
type(u'domain', u'Bereich', options=u'', typeInterface=u'', viewName=u'')
type(u'classifier', u'Classifier', options=u'',
typeInterface='loops.classifier.interfaces.IClassifier', viewName=u'classifier.html')
typeInterface='loops.classifier.interfaces.IClassifier',
viewName=u'classifier.html')
type(u'documenttype', u'Dokumentenart', options=u'', typeInterface=u'', viewName=u'')
type(u'extcollection', u'External Collection', options=u'',
typeInterface='loops.integrator.interfaces.IExternalCollection',
@ -14,20 +17,23 @@ type(u'folder', u'Ordner', options=u'', typeInterface=u'', viewName=u'')
type(u'glossaryitem', u'Glossareintrag', options=u'',
typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html')
type(u'media_asset', u'Media Asset',
options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset', viewName=u'image_medium.html')
options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset',
viewName=u'image_medium.html')
type(u'note', u'Notiz', options=u'', typeInterface='loops.interfaces.INote',
viewName='note.html')
type(u'person', u'Person', options=u'',
typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'')
type(u'predicate', u'Prädikat', options=u'',
typeInterface=u'loops.interfaces.IPredicate', viewName=u'')
type(u'event', u'Termin', options=u'', typeInterface='loops.organize.interfaces.ITask',
type(u'event', u'Termin', options=u'',
typeInterface='loops.organize.interfaces.ITask',
viewName=u'task.html')
type(u'textdocument', u'Text', options=u'', typeInterface='loops.interfaces.ITextDocument', viewName=u'')
type(u'topic', u'Thema', options=u'action.portlet:createTopic,editTopic',
type(u'textdocument', u'Text', options=u'',
typeInterface='loops.interfaces.ITextDocument', viewName=u'')
type(u'topic', u'Thema', options=u'action.portlet:editTopic,createTopic',
typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'')
type(u'type', u'Typ', options=u'', typeInterface='loops.interfaces.ITypeConcept',
viewName=u'')
type(u'type', u'Typ', options=u'',
typeInterface='loops.interfaces.ITypeConcept', viewName=u'')
# domains
concept(u'general', u'Allgemein', u'domain')
@ -74,16 +80,20 @@ child(u'system', u'media_asset', u'standard')
child(u'system', u'personal_info', u'standard')
child(u'topic', u'topic', u'issubtype', 1)
resource(u'homepage', u'Willkommen', u'textdocument', contentType='text/restructured')
resource(u'impressum', u'Impressum', u'textdocument', contentType='text/restructured')
# resources
resource(u'homepage', u'Willkommen', u'textdocument',
contentType='text/restructured')
resource(u'impressum', u'Impressum', u'textdocument',
contentType='text/restructured')
#nodes
node(u'home', u'Startseite', '', 'menu')
node(u'willkommen', u'Willkommen', u'home', u'text')
node(u'willkommen', u'Willkommen', u'home/willkommen', u'text',
node(u'willkommen', u'Willkommen', u'home', u'text',
target=u'resources/homepage')
node(u'participants', u'Teilnehmer', u'home', 'page', target=u'concepts/participants')
node(u'participants', u'Teilnehmer', u'home', 'page',
target=u'concepts/participants')
node(u'topics', u'Themen', u'home', 'page', target=u'concepts/topics')
node(u'glossary', u'Glossar', u'home', 'page', target=u'concepts/glossary')
node(u'search', u'Suche', u'home', 'page', target=u'concepts/search')
node(u'impressum', u'Impressum', u'home', u'info', target=u'resources/impressum')
node(u'impressum', u'Impressum', u'home', u'info',
target=u'resources/impressum')

View file

@ -1,60 +1,99 @@
# types
type(u'query', u'Query', options=u'',
typeInterface='loops.expert.concept.IQueryConcept', viewName=u'')
type(u'datatable', u'Data Table', options=u'action.portlet:edit_concept',
typeInterface='loops.table.IDataTable', viewName=u'')
type(u'task', u'Task', options=u'',
typeInterface='loops.knowledge.interfaces.ITask', viewName=u'')
type(u'domain', u'Domain', options=u'', typeInterface=u'', viewName=u'')
type(u'classifier', u'Classifier', options=u'',
typeInterface='loops.classifier.interfaces.IClassifier', viewName=u'classifier.html')
typeInterface='loops.classifier.interfaces.IClassifier',
viewName=u'classifier.html')
type(u'documenttype', u'Document Type', options=u'', typeInterface=u'', viewName=u'')
type(u'extcollection', u'External Collection', options=u'',
typeInterface='loops.integrator.interfaces.IExternalCollection',
viewName=u'collection.html')
type(u'folder', u'Ordner', options=u'', typeInterface=u'', viewName=u'')
type(u'glossaryitem', u'Glossary Item', options=u'',
typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html')
type(u'media_asset', u'Media Asset',
options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset', viewName=u'image_medium.html')
options=u'storage:varsubdir\nstorage_parameters:extfiles/sites_zzz\nasset_transform.minithumb: size(105)\nasset_transform.small: size(230)\nasset_transform.medium: size(480)', typeInterface='loops.media.interfaces.IMediaAsset',
viewName=u'image_medium.html')
type(u'note', u'Note', options=u'', typeInterface='loops.interfaces.INote',
viewName='note.html')
type(u'person', u'Person', options=u'',
typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'')
type(u'predicate', u'Predicate', options=u'',
typeInterface=u'loops.interfaces.IPredicate', viewName=u'')
type(u'event', u'Event', options=u'', typeInterface='loops.organize.interfaces.ITask',
type(u'event', u'Event', options=u'',
typeInterface='loops.organize.interfaces.ITask',
viewName=u'task.html')
type(u'textdocument', u'Text', options=u'', typeInterface='loops.interfaces.ITextDocument', viewName=u'')
type(u'topic', u'Topy', options=u'', typeInterface='loops.knowledge.interfaces.ITopic',
viewName=u'')
type(u'type', u'Type', options=u'', typeInterface='loops.interfaces.ITypeConcept',
viewName=u'')
type(u'textdocument', u'Text', options=u'',
typeInterface='loops.interfaces.ITextDocument', viewName=u'')
type(u'topic', u'Topic', options=u'action.portlet:editTopic,createTopic',
typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'')
type(u'type', u'Type', options=u'',
typeInterface='loops.interfaces.ITypeConcept', viewName=u'')
#domains
concept(u'general', u'General', u'domain')
concept(u'system', u'System', u'domain')
# predicates
concept(u'depends', u'depends', u'predicate')
concept(u'follows', u'follows', u'predicate')
concept(u'general', u'General', u'domain')
concept(u'glossary', u'Glossary', u'query', options=u'', viewName=u'glossary.html')
concept(u'hasType', u'has Type', u'predicate')
concept(u'ispartof', u'is Part of', u'predicate')
concept(u'issubtype', u'is Subtype', u'predicate')
concept(u'knows', u'knows', u'predicate')
concept(u'ownedby', u'owned by', u'predicate')
concept(u'personal_info', u'Personal Information', u'query', options=u'',
viewName=u'personal_info.html')
concept(u'provides', u'provides', u'predicate')
concept(u'querytarget', u'is Query Target', u'predicate')
concept(u'requires', u'requires', u'predicate')
concept(u'search', u'Search', u'query', options=u'', viewName=u'search')
concept(u'standard', u'subobject', u'predicate')
concept(u'system', u'System', u'domain')
#queries
concept(u'events', u'Events', u'query', options=u'delta:2',
viewName=u'list_events.html')
concept(u'glossary', u'Glossary', u'query', options=u'', viewName=u'glossary.html')
concept(u'personal_info', u'Personal Information', u'query', options=u'',
viewName=u'personal_info.html')
concept(u'participants', u'Participants', u'query', options=u'',
viewName=u'list_children.html')
concept(u'recenct_changes', u'Recent Changes', u'query',
options=u'types:concept:*,resource:*',
viewName=u'recent_changes.html')
concept(u'search', u'Search', u'query', options=u'', viewName=u'search')
concept(u'topics', u'Topics', u'query', options=u'action.portlet:createTopic',
viewName=u'list_children.html')
# child assignments
child(u'general', u'documenttype', u'standard')
child(u'general', u'event', u'standard')
child(u'general', u'events', u'standard')
child(u'general', u'participants', u'standard')
child(u'general', u'topics', u'standard')
child(u'system', u'classifier', u'standard')
child(u'system', u'extcollection', u'standard')
child(u'system', u'issubtype', u'standard')
child(u'system', u'media_asset', u'standard')
child(u'system', u'personal_info', u'standard')
node(u'home', u'Homepage', '', 'menu', body=u'Welcome\n=======)
node(u'participants', u'Participants', u'home', 'page',
body=u'Participants\n============', target=u'concepts/person',
viewName=u'listchildren')
node(u'topics', u'Topics', u'home', 'page', body=u'Topics\n======',
target=u'concepts/topic', viewName=u'listchildren')
child(u'topic', u'topic', u'issubtype', 1)
# resources
resource(u'homepage', u'Welcome', u'textdocument',
contentType='text/restructured')
resource(u'impressum', u'Legal Information', u'textdocument',
contentType='text/restructured')
#nodes
node(u'home', u'Home', '', 'menu')
node(u'welcome', u'Welcome', u'home', u'text',
target=u'resources/homepage')
node(u'participants', u'Participants', u'home', 'page',
target=u'concepts/participants')
node(u'topics', u'Topics', u'home', 'page', target=u'concepts/topics')
node(u'glossary', u'Glossary', u'home', 'page', target=u'concepts/glossary')
node(u'search', u'Search', u'home', 'page', target=u'concepts/search')
node(u'impressum', u'Legal Information', u'home', u'info',
target=u'resources/impressum')

View file

@ -0,0 +1,9 @@
# update for old loops sites
type(u'datatable', u'Datentabelle', options=u'action.portlet:edit_concept',
typeInterface='loops.table.IDataTable', viewName=u'')
concept(u'issubtype', u'is Subtype', u'predicate')
child(u'general', u'issubtype', u'datatable')
child(u'system', u'issubtype', u'standard')

View file

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

View file

@ -91,4 +91,26 @@
factory="loops.expert.browser.report.ResultsConceptView"
permission="zope.View" />
<zope:adapter
name="concept_report_embedded.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.expert.browser.report.EmbeddedReportConceptView"
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>

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

@ -0,0 +1,167 @@
#
# Copyright (c) 2017 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
import os
import time
from zope.cachedescriptors.property import Lazy
from zope.i18n import translate
from zope.i18nmessageid import Message
from zope.traversing.api import getName
from cybertools.meta.interfaces import IOptions
from cybertools.util.date import formatTimeStamp
from loops.common import adapted, normalizeName
from loops.expert.browser.report import ResultsConceptView
from loops.interfaces import ILoopsObject
from loops.util import _, getVarDirectory
try:
from main.config import office_data
except ImportError:
office_data = None
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
title = field.title
if not isinstance(title, Message):
title = _(title)
return encode(translate(title, target_language=lang),
self.encoding)
def getFilenames(self):
"""@return (data_fn, result_fn)"""
repName = getName(self.report.context)
ts = formatTimeStamp(None, format='%y%m%d%H%M%S')
name = '-'.join((ts, repName))
return (name + '.csv',
name + '.xlsx')
def getOfficeTemplatePath(self):
for res in self.report.context.getResources():
return adapted(res).getDataPath()
def renderCsv(self, scriptfn, datapath, tplpath, respath):
callable = os.path.join(office_data['script_path'], scriptfn)
command = ' '.join((callable, datapath, tplpath, respath))
#print '***', command
os.popen(command).read()
def __call__(self):
fields = self.displayedColumns
fieldNames = [f.name for f in fields]
reportOptions = IOptions(self.report)
csvRenderer = reportOptions('csv_renderer')
if not csvRenderer:
csvRenderer = self.globalOptions('csv_renderer')
if csvRenderer:
tplpath = self.getOfficeTemplatePath()
#print '***', csvRenderer, office_data, tplpath
if None in (tplpath, office_data):
csvRenderer = None
if csvRenderer:
csvRenderer = csvRenderer[0]
datafn, resfn = self.getFilenames()
datapath = os.path.join(office_data['data_path'], datafn)
respath = os.path.join(office_data['result_path'], resfn)
output = open(datapath, 'w')
else:
output = StringIO()
writer = csv.DictWriter(output, fieldNames, delimiter=self.delimiter)
if csvRenderer:
output.write(self.delimiter.join([f.name for f in fields]) + '\n')
else:
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:
lang = self.languageInfo.language
value = f.getExportValue(row, 'csv', lang)
if ILoopsObject.providedBy(value):
value = value.title
value = encode(value, self.encoding)
data[f.name] = value
writer.writerow(data)
if csvRenderer:
output.close()
self.renderCsv(csvRenderer, datapath, tplpath, respath)
input = open(respath, 'rb')
text = input.read()
input.close()
self.setDownloadHeader(text,
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xlsx')
else:
text = output.getvalue()
self.setDownloadHeader(text)
return text
def setDownloadHeader(self, text, ctype='text/csv', ext='csv'):
response = self.request.response
response.setHeader('Content-Disposition',
'attachment; filename=%s.%s' %
(self.getFileName(), ext))
response.setHeader('Cache-Control', '')
response.setHeader('Pragma', '')
response.setHeader('Content-Type', ctype)
response.setHeader('Content-Length', len(text))
def encode(text, encoding):
if not isinstance(text, unicode):
return text
try:
return text.encode(encoding)
except UnicodeEncodeError:
result = []
for c in text:
try:
result.append(c.encode(encoding))
except UnicodeEncodeError:
result.append('?')
return ''.join(result)
return '???'

View file

@ -3,10 +3,13 @@
<div metal:define-macro="main">
<div tal:define="report item/reportInstance;
reportView nocall:item"
reportView nocall:item;
renderer item/resultsRenderer"
tal:attributes="class string:content-$level;">
<div metal:use-macro="item/report_macros/header" />
<div metal:use-macro="item/resultsRenderer" />
<tal:renderer condition="renderer">
<div metal:use-macro="renderer" />
</tal:renderer>
</div>
</div>
@ -23,29 +26,65 @@
</div>
<div metal:define-macro="embedded_report">
<div tal:define="report item/reportInstance;
reportView nocall:item"
tal:attributes="class string:content-$level;">
<div metal:use-macro="item/report_macros/header" />
<div metal:use-macro="item/resultsRenderer" />
</div>
</div>
<div metal:define-macro="header">
<metal:block use-macro="view/concept_macros/concepttitle" />
<form method="get" name="report_data" class="report-meta">
<input type="hidden" name="show_results" value="True" />
<tal:hidden define="params item/dynamicParams"
tal:condition="nothing">
<input type="hidden"
tal:repeat="name params"
tal:attributes="name name;
value params/?name" /></tal:hidden>
<div metal:use-macro="item/report_macros/params" />
<div metal:define-macro="buttons">
<input type="submit" name="report_execute" value="Execute Report"
tal:attributes="value item/reportExecuteTitle|string:Execute Report"
i18n:attributes="value" />
<input type="submit"
tal:condition="item/reportDownload"
tal:attributes="name string:${item/reportDownload}:method;
value item/reportDownloadTitle"
i18n:attributes="value" />
</div>
<br />
</form>
<metal:block use-macro="view/concept_macros/concepttitle" />
<form method="get" name="report_data" class="report-meta">
<input type="hidden" name="show_results" value="True" />
<tal:hidden define="params item/dynamicParams">
<input type="hidden"
tal:repeat="name params"
tal:condition="nothing"
tal:attributes="name name;
value params/?name" />
<input type="hidden"
tal:define="viewName request/loops.viewName|nothing"
tal:condition="viewName"
tal:attributes="name string:loops.viewName;
value viewName" />
<input type="hidden"
tal:define="sortinfo request/sortinfo_results|nothing"
tal:condition="sortinfo"
tal:attributes="name string:sortinfo_results;
value sortinfo" />
<input type="hidden" name="report_name"
tal:define="reportName item/reportName"
tal:condition="reportName"
tal:attributes="value reportName" />
</tal:hidden>
<div metal:use-macro="item/report_macros/params" />
<div metal:define-macro="buttons">
<input type="submit" name="report_execute" value="Execute Report"
onclick="this.form.action = ''"
tal:attributes="value item/reportExecuteTitle|string:Execute Report"
tal:condition="item/queryFields"
i18n:attributes="value" />
<input type="submit" name="report_download"
tal:condition="item/reportDownload"
tal:attributes="value item/reportDownloadTitle;
onclick string:
this.form.action = '${item/reportDownload}'"
i18n:attributes="value" />
</div>
<br />
</form>
<tal:ignore condition="nothing">
<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>
</tal:ignore>
</div>
@ -113,7 +152,14 @@
<metal:field define-macro="selection">
<metal:use use-macro="item/report_macros/textline" />
<select tal:attributes="name name">
<option />
<option tal:repeat="opt python:field.getVocabularyItems(
context=item.adapted, request=request)"
tal:attributes="value opt/token;
selected python:value == opt['token']"
tal:content="opt/title" />
</select>
</metal:field>

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -20,6 +20,7 @@
View classes for reporting.
"""
from logging import getLogger
from urllib import urlencode
from zope import interface, component
from zope.app.pagetemplate import ViewPageTemplateFile
@ -46,6 +47,10 @@ class ReportView(ConceptView):
""" A view for defining (editing) a report.
"""
resultsRenderer = None # to be defined by subclass
reportDownload = None
reportName = None
@Lazy
def report_macros(self):
return self.controller.mergeTemplateMacros('report', report_template)
@ -55,10 +60,33 @@ class ReportView(ConceptView):
def macro(self):
return self.report_macros['main']
@Lazy
def tabTitle(self):
return self.report.title
@Lazy
def dynamicParams(self):
return self.request.form
@Lazy
def report(self):
return self.adapted
@Lazy
def reportInstance(self):
instance = component.getAdapter(self.report, IReportInstance,
name=self.report.reportType)
instance.view = self
return instance
@Lazy
def queryFields(self):
ri = self.reportInstance
qf = ri.getAllQueryFields()
if ri.userSettings:
return [f for f in qf if f in ri.userSettings]
return qf
class ResultsView(NodeView):
@ -107,13 +135,6 @@ class ResultsView(NodeView):
def report(self):
return adapted(self.virtualTargetObject)
@Lazy
def reportInstance(self):
instance = component.getAdapter(self.report, IReportInstance,
name=self.report.reportType)
instance.view = self
return instance
#@Lazy
def results(self):
return self.reportInstance.getResults(self.params)
@ -139,6 +160,8 @@ class ResultsConceptView(ConceptView):
""" View on a concept using the results of a report.
"""
logger = getLogger ('ResultsConceptView')
reportName = None # define in subclass if applicable
reportDownload = None
reportType = None # set for using special report instance adapter
@ -169,6 +192,9 @@ class ResultsConceptView(ConceptView):
@Lazy
def reportName(self):
rn = self.request.form.get('report_name')
if rn is not None:
return rn
return (self.getOptions('report_name') or [None])[0]
@Lazy
@ -179,7 +205,10 @@ class ResultsConceptView(ConceptView):
@Lazy
def report(self):
if self.reportName:
return adapted(self.conceptManager[self.reportName])
report = adapted(self.conceptManager.get(self.reportName))
if report is None:
self.logger.warn("Report '%s' not found." % self.reportName)
return report
reports = self.context.getParents([self.hasReportPredicate])
if not reports:
type = self.context.conceptType
@ -193,6 +222,13 @@ class ResultsConceptView(ConceptView):
ri = component.getAdapter(self.report, IReportInstance,
name=reportType)
ri.view = self
if not ri.sortCriteria:
si = self.sortInfo.get('results')
if si is not None:
fnames = (si['colName'],)
ri.sortCriteria = [f for f in ri.getSortFields()
if f.name in fnames]
ri.sortDescending = not si['ascending']
return ri
def results(self):
@ -207,6 +243,35 @@ class ResultsConceptView(ConceptView):
def getColumnRenderer(self, col):
return self.result_macros[col.renderer]
@Lazy
def downloadLink(self, format='csv'):
opt = self.options('download_' + format)
if not opt:
opt = self.typeOptions('download_' + format)
if opt:
return '/'.join((self.nodeView.virtualTargetUrl, opt[0]))
@Lazy
def reportDownload(self):
return self.downloadLink
def isSortableColumn(self, tableName, colName):
if tableName == 'results':
if colName in [f.name for f in self.reportInstance.getSortFields()]:
return True
return False
class EmbeddedResultsConceptView(ResultsConceptView):
@Lazy
def macro(self):
return self.result_macros['embedded_content']
@Lazy
def title(self):
return self.report.title
class ReportConceptView(ResultsConceptView, ReportView):
""" View on a concept using a report.
@ -229,6 +294,17 @@ class ReportConceptView(ResultsConceptView, ReportView):
return qf
class EmbeddedReportConceptView(ReportConceptView):
@Lazy
def macro(self):
return self.report_macros['embedded_report']
@Lazy
def title(self):
return self.report.title
class ReportParamsView(ReportConceptView):
""" Report view allowing to enter parameters before executing the report.
"""

View file

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

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -93,7 +93,8 @@ class Search(ConceptView):
@Lazy
def showActions(self):
return checkPermission('loops.ManageSite', self.context)
perm = (self.globalOptions('delete_permission') or ['loops.ManageSite'])[0]
return checkPermission(perm, self.context)
#return canWriteObject(self.context)
@property
@ -168,7 +169,7 @@ class Search(ConceptView):
title = request.get('name')
if title == '*':
title = None
types = request.get('searchType')
#types = request.get('searchType')
data = []
types = self.getTypes()
if title or types:
@ -305,8 +306,8 @@ class Search(ConceptView):
for state in states:
if stf.state == state:
break
else:
return False
else:
return False
return True

View file

@ -1,5 +1,3 @@
<!-- $Id$ -->
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"

View file

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

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# Copyright (c) 2017 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
@ -35,6 +35,7 @@ from cybertools.composer.report.interfaces import IReportParams
from cybertools.composer.report.result import ResultSet, Row
from cybertools.util.jeep import Jeep
from loops.common import AdapterBase
from loops.expert.concept import IQueryConcept, QueryConcept
from loops.interfaces import ILoopsAdapter
from loops.type import TypeInterfaceSourceList
from loops import util
@ -43,7 +44,7 @@ from loops.util import _
# interfaces
class IReport(ILoopsAdapter, IReportParams):
class IReport(ILoopsAdapter, IReportParams, IQueryConcept):
""" The report adapter for the persistent object (concept) that stores
the report in the concept map.
"""
@ -66,7 +67,7 @@ class IReportInstance(IBaseReport):
# report concept adapter and instances
class Report(AdapterBase):
class Report(QueryConcept):
implements(IReport)
@ -88,6 +89,7 @@ class ReportInstance(BaseReport):
#headerRowFactory = Row
view = None # set upon creation
#headerRowFactory = Row
def __init__(self, context):
self.context = context
@ -120,7 +122,9 @@ class ReportInstance(BaseReport):
result = list(self.selectObjects(parts)) # may modify parts
qc = CompoundQueryCriteria(parts)
return ResultSet(self, result, rowFactory=self.rowFactory,
sortCriteria=self.getSortCriteria(), queryCriteria=qc,
sortCriteria=self.getSortCriteria(),
sortDescending=self.sortDescending,
queryCriteria=qc,
limits=limits)
def selectObjects(self, parts):
@ -173,3 +177,15 @@ class DefaultConceptReportInstance(ReportInstance):
label = u'Default Concept Report'
# specialized rows
class TrackRow(Row):
@staticmethod
def getContextAttr(obj, attr):
if attr in obj.context.metadata_attributes:
return getattr(obj.context, attr)
return obj.context.data.get(attr)

View file

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

6
external/README.txt vendored
View file

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

6
external/pyfunc.py vendored
View file

@ -44,11 +44,15 @@ class PyReader(object):
class InputProcessor(dict):
_constants = dict(True=True, False=False)
def __init__(self):
self.elements = []
self['__builtins__'] = {} # security!
self['__builtins__'] = dict() # security!
def __getitem__(self, key):
if key in self._constants:
return self._constants[key]
def factory(*args, **kw):
element = elementTypes[key](*args, **kw)
if key in toplevelElements:

View file

@ -98,8 +98,9 @@ class I18NView(object):
return adapted(self.context, self.languageInfo)
def checkLanguage(self):
session = ISession(self.request)[packageId]
lang = session.get('language') or self.languageInfo.language
#session = ISession(self.request)[packageId]
#lang = session.get('language') or self.languageInfo.language
lang = self.languageInfo.language
if lang:
self.setLanguage(lang)

View file

@ -44,5 +44,7 @@ class ExternalCollectionView(ConceptView):
cta.update()
if cta.updateMessage is not None:
self.request.form['message'] = cta.updateMessage
if 'no_show_page' in self.request.form:
return False
return True

View file

@ -24,8 +24,10 @@ file system.
from datetime import datetime
from logging import getLogger
import os, re, stat
import transaction
from zope.app.container.interfaces import INameChooser
from zope.app.container.contained import ObjectRemovedEvent
from zope.cachedescriptors.property import Lazy
from zope import component
from zope.component import adapts
@ -51,6 +53,8 @@ from loops.versioning.interfaces import IVersionable
TypeInterfaceSourceList.typeInterfaces += (IExternalCollection,)
logger = getLogger('loops.integrator.collection')
class ExternalCollectionAdapter(AdapterBase):
""" A concept adapter for accessing an external collection.
@ -66,7 +70,7 @@ class ExternalCollectionAdapter(AdapterBase):
newResources = None
updateMessage = None
def getExclude(self):
return getattr(self.context, '_exclude', None) or []
def setExclude(self, value):
@ -83,10 +87,11 @@ class ExternalCollectionAdapter(AdapterBase):
print '###', vaddr, vobj, vid
versions.add(vaddr)
new = []
oldFound = []
oldFound = set([])
provider = component.getUtility(IExternalCollectionProvider,
name=self.providerName or '')
#print '*** old', old, versions, self.lastUpdated
changeCount = 0
for addr, mdate in provider.collect(self):
#print '***', addr, mdate
if addr in versions:
@ -94,8 +99,9 @@ class ExternalCollectionAdapter(AdapterBase):
if addr in old:
# may be it would be better to return a file's hash
# for checking for changes...
oldFound.append(addr)
oldFound.add(addr)
if self.lastUpdated is None or (mdate and mdate > self.lastUpdated):
changeCount +=1
obj = old[addr]
# update settings and regenerate scale variant for media asset
adobj = adapted(obj)
@ -110,29 +116,41 @@ class ExternalCollectionAdapter(AdapterBase):
self.updateMessage = message
# force reindexing
notify(ObjectModifiedEvent(obj))
if changeCount % 10 == 0:
logger.info('Updated: %i.' % changeCount)
transaction.commit()
else:
new.append(addr)
logger.info('%i objects updated.' % changeCount)
transaction.commit()
if new:
self.newResources = provider.createExtFileObjects(self, new)
for r in self.newResources:
self.context.assignResource(r)
logger.info('%i objects created.' % len(new))
transaction.commit()
for addr in old:
if str(addr) not in oldFound:
# not part of the collection any more
# TODO: only remove from collection but keep object?
self.remove(old[addr])
transaction.commit()
for r in self.context.getResources():
adobj = adapted(r)
if self.metaInfo != adobj.metaInfo and (
not adobj.metaInfo or self.overwriteMetaInfo):
adobj.metaInfo = self.metaInfo
self.lastUpdated = datetime.today()
logger.info('External collection updated.')
transaction.commit()
def clear(self):
for obj in self.context.getResources():
self.remove(obj)
def remove(self, obj):
logger.info('Removing object: %s.' % getName(obj))
notify(ObjectRemovedEvent(obj))
del self.resourceManager[getName(obj)]
@Lazy
@ -187,7 +205,7 @@ class DirectoryCollectionProvider(object):
for k, v in self.extFileTypeMapping.items())
container = client.context.getLoopsRoot().getResourceManager()
directory = self.getDirectory(client)
for addr in addresses:
for idx, addr in enumerate(addresses):
name = self.generateName(container, addr)
title = self.generateTitle(addr)
contentType = guess_content_type(addr,
@ -200,9 +218,8 @@ class DirectoryCollectionProvider(object):
if extFileType is None:
extFileType = extFileTypes['image/*']
if extFileType is None:
getLogger('loops.integrator.collection.DirectoryCollectionProvider'
).warn('No external file type found for %r, '
'content type: %r' % (name, contentType))
logger.warn('No external file type found for %r, '
'content type: %r' % (name, contentType))
obj = addAndConfigureObject(
container, Resource, name,
title=title,
@ -219,6 +236,9 @@ class DirectoryCollectionProvider(object):
message = client.updateMessage or u''
message += u'<br />'.join(adobj.processingErrors)
client.updateMessage = message
if idx and idx % 10 == 0:
logger.info('Created: %i.' % idx)
transaction.commit()
yield obj
def getDirectory(self, client):

View file

@ -2,7 +2,7 @@
<metal:block define-macro="render_collection"
tal:define="dummy item/update">
tal:condition="item/update">
<metal:block use-macro="view/concept_macros/conceptdata">
<metal:fill tal:condition="item/editable"

View file

@ -1,7 +1,6 @@
import unittest, doctest
from zope.interface.verify import verifyClass
#from loops.versioning import versionable
class Test(unittest.TestCase):
"Basic tests for the loops.integrator.content package."

View file

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

View file

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

View file

@ -39,6 +39,7 @@ class ExternalSourceInfo(object):
adapts(ILoopsObject)
def __init__(self, context):
#import pdb; pdb.set_trace()
self.context = self.__parent__ = context
def getSourceInfo(self):

View file

@ -1,7 +1,6 @@
import unittest, doctest
from zope.interface.verify import verifyClass
#from loops.versioning import versionable
class Test(unittest.TestCase):
"Basic tests for the integrator sub-package."

View file

@ -402,7 +402,7 @@ class IDocumentSchema(IResourceSchema):
contentType = schema.Choice(
title=_(u'Content Type'),
description=_(u'Content type (format) of the data field'),
values=('text/restructured', 'text/structured', 'text/html',
values=('text/markdown', 'text/restructured', 'text/structured', 'text/html',
'text/plain', 'text/xml', 'text/css'),
default='text/restructured',
required=True)
@ -968,5 +968,3 @@ class IViewConfiguratorSchema(Interface):
value_type=schema.TextLine(),
default=[],
required=False)

View file

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

View file

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

View file

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

View file

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

View file

@ -4,20 +4,28 @@
tal:define="data item/childrenAlphaGroups">
<metal:title use-macro="item/conceptMacros/concepttitle" />
<div><a name="top">&nbsp;</a></div>
<div>
<div tal:condition="nothing">
<span tal:repeat="letter python: [chr(c) for c in range(ord('A'), ord('Z')+1)]"
class="navlink">
<a href="#"
tal:omit-tag="python: letter not in data.keys()"
tal:attributes="href string:${request/URL/-1}#$letter"
tal:attributes="href string:${view/requestUrl/-1}#$letter"
tal:content="letter">A</a>
</span>
</div>
<div>
<span tal:repeat="letter python:sorted(data.keys())"
class="navlink">
<a href="#"
tal:attributes="href string:${view/requestUrl/-1}#$letter"
tal:content="letter">A</a>
</span>
</div>
<div>&nbsp;</div>
<div tal:repeat="letter data/keys">
<div tal:repeat="letter python:sorted(data.keys())">
<div class="subtitle"><a name="A" href="#top"
tal:attributes="name letter;
href string:${request/URL/-1}#top"
href string:${view/requestUrl/-1}#top"
tal:content="letter">A</a>
</div>
<div tal:repeat="related data/?letter|python:[]">

View file

@ -1,6 +1,27 @@
<html i18n:domain="loops">
<metal:institution define-macro="select_institution">
<form method="post">
<div style="font-size: 120%; padding: 10px 0 10px 0">
<span i18n:translate="">Organisation/Team</span>:
<b tal:content="item/institution/title" />
<img tal:condition="python:len(item.institutions) > 1"
src="/@@/cybertools.icons/application_edit.png"
onclick="dojo.byId('select_institution').style.display = 'inline'" />
<select name="select_institution" id="select_institution"
style="display: none"
onchange="submit()">
<option tal:repeat="inst item/institutions"
tal:content="inst/title"
tal:attributes="value inst/uid;
selected python:inst['object'] == item.institution" />
</select>
</div>
</form>
</metal:institution>
<metal:providers define-macro="requirement_providers">
<metal:block use-macro="view/concept_macros/conceptdata" />
<div>
@ -32,28 +53,32 @@
<metal:candidates define-macro="requirement_candidates">
<metal:block use-macro="view/concept_macros/conceptdata" />
<h3 i18n:translate="">Candidates for Task</h3>
<table class="listing">
<tr>
<th i18n:translate="">Candidate</th>
<th i18n:translate=""
title="coverage"
i18n:attributes="title description_fit">Fit</th>
<th i18n:translate="">Knowledge</th>
</tr>
<tr tal:repeat="candidate item/adapted/getCandidates">
<td tal:define="person candidate/person">
<b tal:omit-tag="python:candidate['fit'] < 1.0">
<a tal:attributes="href python:view.getUrlForTarget(person.context)"
tal:content="person/title" /></b></td>
<td tal:content="candidate/fit" />
<td>
<tal:knowledge tal:repeat="ke candidate/required">
<a tal:attributes="href python:view.getUrlForTarget(ke.context)"
tal:content="ke/title" /><tal:sep condition="not:repeat/ke/end">, </tal:sep>
</tal:knowledge></td>
</tr>
</table>
<div class="candidates"
tal:define="candidates item/adapted/getCandidates"
tal:condition="candidates">
<h3 i18n:translate="">Candidates for Task</h3>
<table class="listing">
<tr>
<th i18n:translate="">Candidate</th>
<th i18n:translate=""
title="coverage"
i18n:attributes="title description_fit">Fit</th>
<th i18n:translate="">Knowledge</th>
</tr>
<tr tal:repeat="candidate item/adapted/getCandidates">
<td tal:define="person candidate/person">
<b tal:omit-tag="python:candidate['fit'] < 1.0">
<a tal:attributes="href python:view.getUrlForTarget(person.context)"
tal:content="person/title" /></b></td>
<td tal:content="candidate/fit" />
<td>
<tal:knowledge tal:repeat="ke candidate/required">
<a tal:attributes="href python:view.getUrlForTarget(ke.context)"
tal:content="ke/title" /><tal:sep condition="not:repeat/ke/end">, </tal:sep>
</tal:knowledge></td>
</tr>
</table>
</div>
</metal:candidates>

View file

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

View file

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

View file

@ -0,0 +1,133 @@
#
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
Qualification management report definitions.
"""
from zope.cachedescriptors.property import Lazy
from cybertools.composer.report.base import LeafQueryCriteria, CompoundQueryCriteria
from cybertools.util.jeep import Jeep
from loops.expert.report import ReportInstance
from loops.organize.work.report import WorkRow
from loops.organize.work.report import deadline, day, task, party, state
from loops.organize.work.report import dayStart, dayEnd
from loops.organize.work.report import workTitle, workDescription
from loops.organize.work.report import partyState
from loops import util
class QualificationOverview(ReportInstance):
type = 'qualification_overview'
label = u'Qualification Overview'
rowFactory = WorkRow
fields = Jeep((task, party, workTitle, dayStart, dayEnd, state,
partyState,)) # +deadline?
defaultOutputFields = Jeep(list(fields)[:-1])
defaultSortCriteria = (party, task,)
def getOptions(self, option):
return self.view.options(option)
@Lazy
def states(self):
return self.getOptions('report_select_state' or ('planned',))
@property
def queryCriteria(self):
crit = self.context.queryCriteria or []
f = self.fields.partyState
crit.append(
LeafQueryCriteria(f.name, f.operator, 'active', f))
return CompoundQueryCriteria(crit)
def selectObjects(self, parts):
result = []
workItems = self.recordManager['work']
pred = self.conceptManager['querytarget']
types = self.view.context.getChildren([pred])
for t in types:
for c in t.getChildren([self.view.typePredicate]):
uid = util.getUidForObject(c)
for wi in workItems.query(taskId=uid, state=self.states):
result.append(wi)
return result
class Qualifications(QualificationOverview):
type = 'qualifications'
label = u'Qualifications'
taskTypeNames = ('competence',)
def getOptions(self, option):
return self.view.typeOptions(option)
def selectObjects(self, parts):
result = []
workItems = self.recordManager['work']
target = self.view.context
tasks = [target] + self.getAllSubtasks(target)
for t in tasks:
uid = util.getUidForObject(t)
for wi in workItems.query(taskId=uid, state=self.states):
result.append(wi)
return result
def getAllSubtasks(self, concept):
result = []
for c in concept.getChildren([self.view.defaultPredicate]):
if c.conceptType in self.taskTypes:
result.append(c)
result.extend(self.getAllSubtasks(c))
return result
@Lazy
def taskTypes(self):
return [c for c in [self.conceptManager.get(name)
for name in self.taskTypeNames]
if c is not None]
class PersonQualifications(QualificationOverview):
type = 'person_qualifications'
label = u'Qualifications for Person'
defaultSortCriteria = (task,)
def getOptions(self, option):
return self.view.typeOptions(option)
@property
def queryCriteria(self):
crit = self.context.queryCriteria or []
return CompoundQueryCriteria(crit)
def selectObjects(self, parts):
workItems = self.recordManager['work']
person = self.view.context
uid = util.getUidForObject(person)
return workItems.query(userName=uid, state=self.states)

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -41,16 +41,34 @@ class Questionnaire(AdapterBase, Questionnaire):
_contextAttributes = list(IQuestionnaire)
_adapterAttributes = AdapterBase._adapterAttributes + (
'teamBasedEvaluation',
'questionGroups', 'questions', 'responses',)
_noexportAttributes = _adapterAttributes
def getTeamBasedEvaluation(self):
return (self.questionnaireType == 'team' or
getattr(self.context, '_teamBasedEvaluation', False))
def setTeamBasedEvaluation(self, value):
if not value and getattr(self.context, '_teamBasedEvaluation', False):
self.context._teamBasedEvaluation = False
teamBasedEvaluation = property(getTeamBasedEvaluation, setTeamBasedEvaluation)
@property
def questionGroups(self):
return self.getQuestionGroups()
def getAllQuestionGroups(self, personId=None):
return [adapted(c) for c in self.context.getChildren()]
def getQuestionGroups(self, personId=None):
return self.getAllQuestionGroups()
@property
def questions(self):
for qug in self.questionGroups:
return self.getQuestions()
def getQuestions(self, personId=None):
for qug in self.getQuestionGroups(personId):
for qu in qug.questions:
#qu.questionnaire = self
yield qu
@ -65,12 +83,18 @@ class QuestionGroup(AdapterBase, QuestionGroup):
'questionnaire', 'questions', 'feedbackItems')
_noexportAttributes = _adapterAttributes
@property
def questionnaire(self):
def getQuestionnaires(self):
result = []
for p in self.context.getParents():
ap = adapted(p)
if IQuestionnaire.providedBy(ap):
return ap
result.append(ap)
return result
@property
def questionnaire(self):
for qu in self.getQuestionnaires():
return qu
@property
def subobjects(self):

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -23,6 +23,7 @@ surveys and self-assessments.
import csv
from cStringIO import StringIO
import math
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from zope.i18n import translate
@ -31,64 +32,318 @@ from cybertools.knowledge.survey.questionnaire import Response
from cybertools.util.date import formatTimeStamp
from loops.browser.concept import ConceptView
from loops.browser.node import NodeView
from loops.common import adapted
from loops.common import adapted, baseObject
from loops.knowledge.browser import InstitutionMixin
from loops.knowledge.survey.response import Responses
from loops.organize.party import getPersonForUser
from loops.security.common import checkPermission
from loops.util import getObjectForUid
from loops.util import _
template = ViewPageTemplateFile('view_macros.pt')
class SurveyView(ConceptView):
class SurveyView(InstitutionMixin, ConceptView):
data = None
errors = None
errors = message = None
batchSize = 12
teamData = None
template = template
#adminMaySelectAllInstitutions = False
@Lazy
def macro(self):
self.registerDojo()
return template.macros['survey']
@Lazy
def title(self):
title = self.context.title
if self.personId:
person = adapted(getObjectForUid(self.personId))
if person is not None:
return '%s: %s' % (title, person.title)
return title
@Lazy
def tabview(self):
if self.editable:
return 'index.html'
def results(self):
def getUrlParamString(self):
qs = super(SurveyView, self).getUrlParamString()
if qs.startswith('?report='):
return ''
return qs
@Lazy
def personId(self):
return self.request.form.get('person')
@Lazy
def report(self):
return self.request.form.get('report')
@Lazy
def questionnaireType(self):
return self.adapted.questionnaireType
def teamReports(self):
if self.adapted.teamBasedEvaluation:
if checkPermission('loops.ViewRestricted', self.context):
return [dict(name='standard', label='label_survey_report_standard'),
dict(name='questions',
label='label_survey_report_questions')]
def update(self):
instUid = self.request.form.get('select_institution')
if instUid:
return self.setInstitution(instUid)
@Lazy
def groups(self):
result = []
response = None
if self.questionnaireType == 'pref_selection':
groups = [g.questions for g in
self.adapted.getQuestionGroups(self.personId)]
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.getQuestions(self.personId))
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.getQuestionGroups(self.personId):
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, self.personId)
for qu in self.adapted.getQuestions(self.personId):
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.getQuestionGroups(self.personId):
if qg.uid in data:
resp.values[qg] = data[qg.uid]
else:
qgAvailable = False
if not qgAvailable:
values = resp.getGroupedResult()
for v in values:
resp.values[v['group']] = v['score']
result.append(resp)
return result
def results(self):
if self.report:
return self.teamResults(self.report)
form = self.request.form
if 'submit' in form:
self.data = {}
response = Response(self.adapted, None)
for key, value in form.items():
if key.startswith('question_'):
action = None
for k in ('submit', 'save'):
if k in form:
action = k
break
if action is None:
return []
respManager = Responses(self.context)
respManager.personId = (self.request.form.get('person') or
respManager.getPersonId())
if self.adapted.teamBasedEvaluation and self.institution:
respManager.institutionId = self.getUidForObject(
baseObject(self.institution))
if self.adapted.questionnaireType == 'person':
respManager.referrerId = respManager.getPersonId()
if self.adapted.questionnaireType == 'pref_selection':
return self.prefsResults(respManager, form, action)
data = {}
response = Response(self.adapted, self.personId)
for key, value in form.items():
if key.startswith('question_'):
if value != 'none':
uid = key[len('question_'):]
question = adapted(self.getObjectForUid(uid))
if value != 'none':
if value.isdigit():
value = int(value)
self.data[uid] = value
response.values[question] = value
Responses(self.context).save(self.data)
self.errors = self.check(response)
if self.errors:
return []
if response is not None:
result = response.getGroupedResult()
return [dict(category=r[0].title, text=r[1].text,
score=int(round(r[2] * 100)))
for r in result]
data[uid] = value
response.values[question] = value
values = response.getGroupedResult()
for v in values:
data[self.getUidForObject(v['group'])] = v['score']
self.data = data
self.errors = self.check(response)
if action == 'submit' and not self.errors:
data['state'] = 'active'
else:
data['state'] = 'draft'
respManager.save(data)
if action == 'save':
self.message = u'Your data have been saved.'
return []
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.getQuestionGroups(self.personId)
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.getQuestionGroups(self.personId):
score = 0
for qu in group.questions:
value = data.get(qu.uid) or 0
if qu.revertAnswerOptions:
value = -value
score += value
result.append(dict(category=group.title, score=score))
return result
def check(self, response):
errors = []
values = response.values
for qu in self.adapted.questions:
for qu in self.adapted.getQuestions(self.personId):
if qu.required and qu not in values:
errors.append('Please answer the obligatory questions.')
errors.append(dict(uid=qu.uid,
text='Please answer the obligatory questions.'))
break
qugroups = {}
for qugroup in self.adapted.questionGroups:
for qugroup in self.adapted.getQuestionGroups(self.personId):
qugroups[qugroup] = 0
for qu in values:
qugroups[qu.questionGroup] += 1
@ -97,7 +352,12 @@ class SurveyView(ConceptView):
if minAnswers in (u'', None):
minAnswers = len(qugroup.questions)
if count < minAnswers:
errors.append('Please answer the minimum number of questions.')
if self.adapted.noGrouping:
errors.append(dict(uid=qugroup.uid,
text='Please answer the highlighted questions.'))
else:
errors.append(dict(uid=qugroup.uid,
text='Please answer the minimum number of questions.'))
break
return errors
@ -106,7 +366,8 @@ class SurveyView(ConceptView):
text = qugroup.description
info = None
if qugroup.minAnswers in (u'', None):
info = translate(_(u'Please answer all questions.'), target_language=lang)
info = translate(_(u'Please answer all questions.'),
target_language=lang)
elif qugroup.minAnswers > 0:
info = translate(_(u'Please answer at least $minAnswers questions.',
mapping=dict(minAnswers=qugroup.minAnswers)),
@ -115,16 +376,48 @@ class SurveyView(ConceptView):
text = u'<i>%s</i><br />(%s)' % (text, info)
return text
def loadData(self):
if self.data is None:
respManager = Responses(self.context)
respManager.personId = (self.request.form.get('person') or
respManager.getPersonId())
if self.adapted.teamBasedEvaluation and self.institution:
respManager.institutionId = self.getUidForObject(
baseObject(self.institution))
if self.adapted.questionnaireType == 'person':
respManager.referrerId = respManager.getPersonId()
self.data = respManager.load()
def getValues(self, question):
setting = None
if self.data is None:
self.data = Responses(self.context).load()
self.loadData()
if self.data:
setting = self.data.get(question.uid)
noAnswer = [dict(value='none', checked=(setting == None),
radio=(not question.required))]
return noAnswer + [dict(value=i, checked=(setting == i), radio=True)
for i in reversed(range(question.answerRange))]
if setting is None:
setting = 'none'
setting = str(setting)
result = []
for opt in self.answerOptions:
value = str(opt['value'])
result.append(dict(value=value, checked=(setting == value),
title=opt.get('description') or u''))
return result
def getTextValue(self, question):
self.loadData()
if self.data:
return self.data.get(question.uid)
def getPrefsValue(self, question):
self.loadData()
if self.data:
return self.data.get(question.uid)
def getCssClass(self, question):
cls = ''
if self.errors and self.data.get(question.uid) is None:
cls = 'error '
return cls + 'vpad'
class SurveyCsvExport(NodeView):
@ -132,36 +425,43 @@ class SurveyCsvExport(NodeView):
encoding = 'ISO8859-15'
def encode(self, text):
text.encode(self.encoding)
return text.encode(self.encoding)
@Lazy
def questions(self):
result = []
for idx1, qug in enumerate(adapted(self.virtualTargetObject).questionGroups):
for idx1, qug in enumerate(
adapted(self.virtualTargetObject).questionGroups):
for idx2, qu in enumerate(qug.questions):
result.append((idx1, idx2, qug, qu))
return result
@Lazy
def columns(self):
infoCols = ['Name', 'Timestamp']
infoCols = ['Institution', 'Name', 'Timestamp']
dataCols = ['%02i-%02i' % (item[0], item[1]) for item in self.questions]
return infoCols + dataCols
def getRows(self):
memberPred = self.conceptManager.get('ismember')
for tr in Responses(self.virtualTargetObject).getAllTracks():
p = adapted(getObjectForUid(tr.userName))
name = p and p.title or u'???'
name = self.encode(p and p.title or u'???')
inst = u''
if memberPred is not None:
for i in baseObject(p).getParents([memberPred]):
inst = self.encode(i.title)
break
ts = formatTimeStamp(tr.timeStamp)
cells = [tr.data.get(qu.uid, -1)
for (idx1, idx2, qug, qu) in self.questions]
yield [name, ts] + cells
yield [inst, name, ts] + cells
def __call__(self):
f = StringIO()
writer = csv.writer(f, delimiter=',')
writer.writerow(self.columns)
for row in self.getRows():
for row in sorted(self.getRows()):
writer.writerow(row)
text = f.getvalue()
self.setDownloadHeader(text)

View file

@ -12,8 +12,6 @@
<zope:class class="loops.knowledge.survey.base.Questionnaire">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestionnaire" />
<require permission="zope.View"
attributes="context" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestionnaire" />
</zope:class>
@ -25,8 +23,6 @@
<zope:class class="loops.knowledge.survey.base.QuestionGroup">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestionGroup" />
<require permission="zope.View"
attributes="context" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestionGroup" />
</zope:class>
@ -38,8 +34,6 @@
<zope:class class="loops.knowledge.survey.base.Question">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestion" />
<require permission="zope.View"
attributes="context" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestion" />
</zope:class>
@ -51,8 +45,6 @@
<zope:class class="loops.knowledge.survey.base.FeedbackItem">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IFeedbackItem" />
<require permission="zope.View"
attributes="context" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IFeedbackItem" />
</zope:class>

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -23,21 +23,82 @@ Interfaces for surveys used in knowledge management.
from zope.interface import Interface, Attribute
from zope import interface, component, schema
from cybertools.composer.schema.grid.interfaces import Records
from cybertools.knowledge.survey import interfaces
from loops.interfaces import IConceptSchema, ILoopsAdapter
from loops.util import _
from loops.util import _, KeywordVocabulary
class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire):
class IQuestionnaire(ILoopsAdapter, interfaces.IQuestionnaire):
""" A collection of questions for setting up a survey.
"""
questionnaireHeader = schema.Text(
title=_(u'Questionnaire Header'),
description=_(u'Text that will appear at the top of the questionnaire.'),
default=u'',
missing_value=u'',
required=False)
questionnaireType = schema.Choice(
title=_(u'Questionnaire Type'),
description=_(u'Select the type of the questionnaire.'),
source=KeywordVocabulary((
('standard', _(u'Standard Questionnaire')),
('person', _(u'Person-related Questionnaire')),
('team', _(u'Team-related Questionnaire')),
('pref_selection', _(u'Preference Selection')),
)),
default='standard',
required=True)
defaultAnswerRange = schema.Int(
title=_(u'Answer Range'),
description=_(u'Number of items (answer options) to select from.'),
default=4,
required=True)
answerOptions = Records(
title=_(u'Answer Options'),
description=_(u'Values to select from with corresponding column '
u'labels and descriptions. There should be at '
u'least answer range items with numeric values.'),
default=[],
required=False)
answerOptions.column_types = [
schema.Text(__name__='value', title=u'Value',),
schema.Text(__name__='label', title=u'Label'),
schema.Text(__name__='description', title=u'Description'),
schema.Text(__name__='colspan', title=u'ColSpan'),
schema.Text(__name__='cssclass', title=u'CSS Class'),]
noGrouping = schema.Bool(
title=_(u'No Grouping of Questions'),
description=_(u'The questions should be presented in a linear manner, '
u'not grouped by categories or question groups.'),
default=False,
required=False)
teamBasedEvaluation = schema.Bool(
title=_(u'Team-based Evaluation'),
description=_(u'.'),
default=False,
required=False)
#teamBasedEvaluation = Attribute('Team-based Evaluation')
feedbackColumns = Records(
title=_(u'Feedback Columns'),
description=_(u'Column definitions for the results table '
u'on the feedback page.'),
default=[],
required=False)
feedbackColumns.column_types = [
schema.Text(__name__='name', title=u'Column Name',),
schema.Text(__name__='label', title=u'Column Label'),]
feedbackHeader = schema.Text(
title=_(u'Feedback Header'),
description=_(u'Text that will appear at the top of the feedback page.'),
@ -53,7 +114,7 @@ class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire):
required=False)
class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup):
class IQuestionGroup(ILoopsAdapter, interfaces.IQuestionGroup):
""" A group of questions within a questionnaire.
"""
@ -65,10 +126,20 @@ class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup):
required=False)
class IQuestion(IConceptSchema, interfaces.IQuestion):
class IQuestion(ILoopsAdapter, interfaces.IQuestion):
""" A single question within a questionnaire.
"""
questionType = schema.Choice(
title=_(u'Question Type'),
description=_(u'Select the type of the question.'),
source=KeywordVocabulary((
('value_selection', _(u'Value Selection')),
('text', _(u'Text')),
)),
default='value_selection',
required=True)
required = schema.Bool(
title=_(u'Required'),
description=_(u'Question must be answered.'),
@ -82,7 +153,7 @@ class IQuestion(IConceptSchema, interfaces.IQuestion):
required=False)
class IFeedbackItem(IConceptSchema, interfaces.IFeedbackItem):
class IFeedbackItem(ILoopsAdapter, interfaces.IFeedbackItem):
""" Some text (e.g. a recommendation) or some other kind of information
that may be deduced from the res)ponses to a questionnaire.
"""

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2016 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -34,22 +34,52 @@ class Responses(BaseRecordManager):
implements(IResponses)
storageName = 'survey_responses'
personId = None
institutionId = None
referrerId = None
def __init__(self, context):
self.context = context
def save(self, data):
if self.personId:
self.storage.saveUserTrack(self.uid, 0, self.personId, data,
update=True, overwrite=True)
id = self.personId
if self.institutionId:
id += '.' + self.institutionId
if self.referrerId:
id += '.' + self.referrerId
self.storage.saveUserTrack(self.uid, 0, id, data,
update=True, overwrite=False)
def load(self):
if self.personId:
tracks = self.storage.getUserTracks(self.uid, 0, self.personId)
def load(self, personId=None, referrerId=None, institutionId=None):
if personId is None:
personId = self.personId
if referrerId is None:
referrerId = self.referrerId
if institutionId is None:
institutionId = self.institutionId
if personId:
id = personId
if institutionId:
id += '.' + institutionId
if referrerId:
id += '.' + referrerId
tracks = self.storage.getUserTracks(self.uid, 0, id)
if not tracks: # then try without institution
tracks = self.storage.getUserTracks(self.uid, 0, personId)
if tracks:
return tracks[0].data
return {}
def loadRange(self, personId):
tracks = self.storage.getUserTracks(self.uid, 0, personId)
data = {}
for tr in tracks:
for k, v in tr.data.items():
item = data.setdefault(k, [])
item.append(v)
return data
def getAllTracks(self):
return self.storage.query(taskId=self.uid)

View file

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

View file

@ -1,4 +1,3 @@
# tests.py - loops.knowledge package
import os
import unittest, doctest
@ -6,6 +5,7 @@ from zope.app.testing import ztapi
from zope import component
from zope.interface.verify import verifyClass
from loops.expert.report import IReport, Report
from loops.knowledge.qualification.base import Competence
from loops.knowledge.survey.base import Questionnaire, Question, FeedbackItem
from loops.knowledge.survey.interfaces import IQuestionnaire, IQuestion, \
@ -18,6 +18,7 @@ importPath = os.path.join(os.path.dirname(__file__), 'data')
def importData(loopsRoot):
component.provideAdapter(Report, provides=IReport)
baseImportData(loopsRoot, importPath, 'knowledge_de.dmp')
def importSurvey(loopsRoot):

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2016 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
@ -27,6 +27,7 @@ from zope.proxy import removeAllProxies
from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL
from cybertools.browser.view import URLGetter
from cybertools.meta.interfaces import IOptions
from cybertools.util import format
from loops.common import adapted, baseObject
@ -42,6 +43,10 @@ class BaseView(object):
self.context = removeSecurityProxy(context) # this is the adapted concept!
self.request = request
@property
def requestUrl(self):
return URLGetter(self.request)
@Lazy
def loopsRoot(self):
return self.context.getLoopsRoot()
@ -86,6 +91,10 @@ class BaseView(object):
def title(self):
return self.context.title
@Lazy
def headTitle(self):
return self.title
@Lazy
def description(self):
return self.context.description

View file

@ -65,8 +65,9 @@ class LayoutNodeView(Page, BaseView):
if self.target is not None:
targetView = component.getMultiAdapter((self.target, self.request),
name='layout')
if targetView.title not in parts:
parts.append(targetView.title)
title = getattr(targetView, 'headTitle', targetView.title)
if title not in parts:
parts.append(title)
if self.globalOptions('reverseHeadTitle'):
parts.reverse()
return ' - '.join(parts)

View file

@ -50,3 +50,15 @@ class TextView(BaseView):
def render(self):
return self.renderText(self.context.data, self.context.contentType)
@Lazy
def canonicalUrl(self):
parents = self.context.context.getParents(
[self.conceptManager['standard']])
for parent in parents:
view = component.getMultiAdapter((adapted(parent),
self.request), name='layout')
if view:
url = getattr(view, 'canonicalUrl')
if url:
return url

Binary file not shown.

View file

@ -1,9 +1,9 @@
msgid ""
msgstr ""
"Project-Id-Version: 0.13.0\n"
"Project-Id-Version: 0.13.1\n"
"POT-Creation-Date: 2007-05-22 12:00 CET\n"
"PO-Revision-Date: 2016-01-27 12:00 CET\n"
"PO-Revision-Date: 2017-12-08 12:00 CET\n"
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
"Language-Team: loops developers <helmutm@cy55.de>\n"
"MIME-Version: 1.0\n"
@ -89,6 +89,14 @@ msgstr "Thema ändern"
msgid "Please correct the indicated errors."
msgstr "Bitte berichtigen Sie die angezeigten Fehler."
msgid "tooltip_sort_column"
msgstr "Nach dieser Spalte sortieren"
# expert (reporting)
msgid "Download Data"
msgstr "Download als Excel-Datei"
# blog
msgid "Edit Blog Post..."
@ -178,6 +186,30 @@ msgstr "Glossareintrag anlegen."
msgid "Answer Range"
msgstr "Abstufung Bewertungen"
msgid "Answer Options"
msgstr "Antwortmöglichkeiten"
msgid "Values to select from with corresponding column labels and descriptions. There should be at least answer range items with numeric values."
msgstr "Auszuwählende Werte mit zugehörigen Spaltenüberschriften und Beschreibungen. Es sollte mindestens so viele Einträge mit numerischen Werten geben wie durch das Feld 'Abstufung Bewertungen' vorgegeben."
msgid "No Grouping of Questions"
msgstr "Keine Gruppierung der Fragen"
msgid "The questions should be presented in a linear manner, not grouped by categories or question groups."
msgstr "Die Fragen sollen in linearer Reihenfolge ausgegeben und nicht nach Fragengruppen bzw. Kategorien gruppiert werden."
msgid "Questionnaire Header"
msgstr "Infotext zum Fragebogen"
msgid "Text that will appear at the top of the questionnaire."
msgstr "Text, der vor dem Fragebogen erscheinen soll"
msgid "Feedback Header"
msgstr "Infotext zur Auswertung"
msgid "Text that will appear at the top of the feedback page."
msgstr "Text, der oben auf der Auswertungsseite erscheinen soll."
msgid "Feedback Footer"
msgstr "Auswertungs-Hinweis"
@ -193,6 +225,15 @@ msgstr "Mindestanzahl an Antworten"
msgid "Minumum number of questions that have to be answered. Empty means all questions have to be answered."
msgstr "Anzahl der Fragen, die mindestens beantwortet werden müssen. Keine Angabe: Es müssen alle Fragen beantwortet werden."
msgid "Question Type"
msgstr "Fragentyp"
msgid "Select the type of the question."
msgstr "Bitte den Typ der Frage auswählen."
msgid "Value Selection"
msgstr "Auswahl Bewertung"
msgid "Required"
msgstr "Pflichtfrage"
@ -205,6 +246,9 @@ msgstr "Negative Polarität"
msgid "Value inversion: High selection means low value."
msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert."
msgid "Question"
msgstr "Frage"
msgid "Questionnaire"
msgstr "Fragebogen"
@ -241,15 +285,30 @@ msgstr "Trifft eher zu"
msgid "survey_value_3"
msgstr "Trifft für unser Unternehmen voll und ganz zu"
msgid "label_survey_show_report"
msgstr "Auswertung anzeigen"
msgid "label_survey_report_standard"
msgstr "Standard-Auswertung"
msgid "label_survey_report_questions"
msgstr "Einzelfragen-Auswertung"
msgid "Evaluate Questionnaire"
msgstr "Fragebogen auswerten"
msgid "Save Data"
msgstr "Daten speichern"
msgid "Reset Responses Entered"
msgstr "Eingaben zurücksetzen"
msgid "Back to Questionnaire"
msgstr "Zurück zum Fragebogen"
msgid "Your data have been saved."
msgstr "Ihre Daten wurden gespeichert."
msgid "Please answer at least $minAnswers questions."
msgstr "Bitte beantworten Sie mindestens $minAnswers Fragen."
@ -262,10 +321,37 @@ msgstr "Bitte beantworten Sie die Pflichtfragen."
msgid "Please answer the minimum number of questions."
msgstr "Bitte beantworten Sie die angegebene Mindestanzahl an Fragen je Fragengruppe."
msgid "Please answer the highlighted questions."
msgstr "Bitte beantworten Sie die markierten Fragen."
msgid "Obligatory question, must be answered"
msgstr "Pflichtfrage, muss beantwortet werden"
# competence (qualification)
msgid "Score"
msgstr "Ergebnis %"
msgid "Team Score"
msgstr "Durchschnitt Team %"
msgid "Rank"
msgstr "Rang"
msgid "Team Rank"
msgstr "Rang Team"
msgid "Average"
msgstr "Durchschnitt"
msgid "Deviation"
msgstr "Abweichung"
msgid "Team Size"
msgstr "Anzahl der vom Team ausgefüllten Fragebögen"
msgid "if (confirm('Do you really want to reset all response data?')) setRadioButtons('none'); return false"
msgstr "if (confirm('Wollen Sie wirklich alle eingegebenen Daten zurücksetzen?')) setRadioButtons('none'); return false"
# compentence and qualification management
msgid "Validity Period (Months)"
msgstr "Gültigkeitszeitraum (Monate)"
@ -471,6 +557,8 @@ msgstr "Wer?"
msgid "When?"
msgstr "Wann?"
# personal stuff
msgid "Favorites"
msgstr "Lesezeichen"
@ -519,6 +607,8 @@ msgstr "Anmelden"
msgid "Presence"
msgstr "Anwesenheit"
# general
msgid "Actions"
msgstr "Aktionen"
@ -531,9 +621,6 @@ msgstr "Informationen über dieses Objekt"
msgid "Information about this object."
msgstr "Informationen über dieses Objekt."
msgid "Send a link to this object by email."
msgstr "Einen Link zu diesem Objekt per E-Mail versenden."
msgid "Edit with external editor."
msgstr "Mit 'External Editor' bearbeiten."
@ -792,6 +879,9 @@ msgstr "Benutzer registrieren"
msgid "Register new member"
msgstr "Neu registrieren"
msgid "Login name not allowed."
msgstr "Die von Ihnen eingegebene Benutzerkennung enthält Sonderzeichen, z. B. Umlaute."
msgid "Login name already taken."
msgstr "Die von Ihnen eingegebene Benutzerkennung ist schon vergeben."
@ -846,6 +936,12 @@ msgstr "Beginn"
msgid "End date"
msgstr "Ende"
msgid "Start Day"
msgstr "Beginn"
msgid "End Day"
msgstr "Ende"
msgid "Knowledge"
msgstr "Kompetenzen"
@ -918,6 +1014,9 @@ msgstr "Kommentare"
msgid "Add Comment"
msgstr "Kommentar hinzufügen"
msgid "Email Address"
msgstr "E-Mail-Adresse"
msgid "Subject"
msgstr "Thema"
@ -930,6 +1029,9 @@ msgstr "Objekte löschen"
msgid "confirm('Do you really want to delete the selected objects?')"
msgstr "confirm('Wollen Sie die ausgewählten Objekte wirklich löschen?')"
msgid "title_bookTopicView"
msgstr "Übersicht"
# management interface
msgid "label_type"
@ -992,6 +1094,21 @@ msgstr "Kalender"
msgid "Work Items"
msgstr "Aktivitäten"
msgid "Work Item Type"
msgstr "Art der Aktivität"
msgid "Unit of Work"
msgstr "Standard-Aktivität"
msgid "Scheduled Event"
msgstr "Termin"
msgid "Deadline"
msgstr "Deadline"
msgid "Check-up"
msgstr "Überprüfung"
msgid "Work Items for $title"
msgstr "Aktivitäten für $title"
@ -1022,6 +1139,12 @@ msgstr "Dauer/Aufwand"
msgid "Duration / Effort (hh:mm)"
msgstr "Dauer / Aufwand (hh:mm)"
msgid "Priority"
msgstr "Priorität"
msgid "Activity"
msgstr "Leistungsart"
msgid "Action"
msgstr "Aktion"
@ -1096,6 +1219,9 @@ msgstr "Bemerkung"
msgid "desc_transition_comments"
msgstr "Notizen zum Statusübergang."
msgid "contact_states"
msgstr "Kontaktstatus"
# state names
msgid "accepted"
@ -1164,6 +1290,12 @@ msgstr "unklassifiziert"
msgid "verified"
msgstr "verifiziert"
msgid "prospective"
msgstr "künftig"
msgid "inactive"
msgstr "inaktiv"
# transitions
msgid "accept"
@ -1238,6 +1370,15 @@ msgstr "verifizieren"
msgid "work"
msgstr "bearbeiten"
msgid "activate"
msgstr "aktivieren"
msgid "inactivate"
msgstr "inaktiv setzen"
msgid "reset"
msgstr "zurücksetzen"
# calendar
msgid "Monday"
@ -1305,3 +1446,27 @@ msgstr "Zeitraum"
msgid "Technology"
msgstr "Technik"
# send mail
msgid "Send a link to this object by email."
msgstr "Einen Link zu diesem Objekt per E-Mail versenden."
msgid "Send Link by Email"
msgstr "Link per E-Mail versenden"
msgid "Mail Subject"
msgstr "Betreff"
msgid "Mail Body"
msgstr "Text"
msgid "Recipients"
msgstr "Empfänger"
msgid "Additional Recipients"
msgstr "Weitere Empfänger"
msgid "Send email"
msgstr "E-Mail senden"

View file

@ -3,7 +3,7 @@
<tal:actions condition="view/showObjectActions">
<div metal:use-macro="views/node_macros/object_actions" /></tal:actions>
<h1><a tal:omit-tag="python: level > 1"
tal:attributes="href request/URL"
tal:attributes="href view/requestUrl"
tal:content="item/title">Title</a></h1><br />
<p tal:define="url python: view.getUrlForTarget(item)">
<a tal:omit-tag="view/isAnonymous"

View file

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

View file

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

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -87,6 +87,7 @@ class BaseMemberRegistration(NodeView):
formErrors = dict(
confirm_nomatch=FormError(_(u'Password and password confirmation '
u'do not match.')),
illegal_loginname=FormError(_('Login name not allowed.')),
duplicate_loginname=FormError(_('Login name already taken.')),
)
@ -244,7 +245,7 @@ class SecureMemberRegistration(BaseMemberRegistration, CreateForm):
regMan = IMemberRegistrationManager(self.context.getLoopsRoot())
pw = generateName()
email = form.get('email')
try:
try:
result = regMan.register(login, pw,
form.get('lastName'), form.get('firstName'),
email=email,)

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
# Copyright (c) 2016 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
@ -32,7 +32,7 @@ from cybertools.ajax import innerHtml
from cybertools.browser.action import actions
from cybertools.browser.form import FormController
from loops.browser.action import DialogAction
from loops.browser.form import EditConceptForm
from loops.browser.form import CreateConceptForm, EditConceptForm
from loops.browser.node import NodeView
from loops.common import adapted
from loops.organize.party import getPersonForUser
@ -44,7 +44,8 @@ organize_macros = ViewPageTemplateFile('view_macros.pt')
actions.register('createPerson', 'portlet', DialogAction,
title=_(u'Create Person...'),
description=_(u'Create a new person.'),
viewName='create_concept.html',
#viewName='create_concept.html',
viewName='create_person.html',
dialogName='createPerson',
typeToken='.loops/concepts/person',
fixedType=True,
@ -115,24 +116,35 @@ actions.register('send_email', 'object', DialogAction,
)
class EditPersonForm(EditConceptForm):
class PersonForm(object):
@Lazy
def presetTypesForAssignment(self):
types = list(self.typeManager.listTypes(include=('workspace',)))
#assigned = [r.context for r in self.assignments]
#types = [t for t in types if t.typeProvider not in assigned]
predicates = [n for n in ['standard', 'ismember', 'ismaster', 'isowner']
if n in self.conceptManager]
return [dict(title=t.title, token=t.tokenForSearch, predicates=predicates)
for t in types]
class CreatePersonForm(PersonForm, CreateConceptForm):
pass
class EditPersonForm(PersonForm, EditConceptForm):
pass
class SendEmailForm(NodeView):
__call__ = innerHtml
def checkPermissions(self):
return (not self.isAnonymous and
super(SendEmailForm, self).checkPermissions())
@property
def macro(self):
return organize_macros.macros['send_email']
@ -171,6 +183,10 @@ class SendEmailForm(NodeView):
@Lazy
def subject(self):
optionKey = 'organize.sendmail_subject'
option = self.globalOptions(optionKey) or self.typeOptions(optionKey)
if option:
return option[0]
menu = self.context.getMenu()
zdc = IZopeDublinCore(menu)
zdc.languageInfo = self.languageInfo
@ -181,6 +197,12 @@ class SendEmailForm(NodeView):
class SendEmail(FormController):
bccToSender = False
def checkPermissions(self):
return (not self.isAnonymous and
super(SendEmail, self).checkPermissions())
def update(self):
form = self.request.form
subject = form.get('subject') or u''
@ -193,7 +215,10 @@ class SendEmail(FormController):
msg = MIMEText(message.encode('utf-8'), 'plain', 'utf-8')
msg['Subject'] = subject.encode('utf-8')
msg['From'] = sender
msg['To'] = ', '.join(r.strip() for r in recipients if r.strip())
recipients = [r.strip() for r in recipients if r.strip()]
msg['To'] = ', '.join(recipients)
if self.bccToSender:
recipients.append(sender)
mailhost = component.getUtility(IMailDelivery, 'Mail')
mailhost.send(sender, recipients, msg.as_string())
return True

View file

@ -123,20 +123,24 @@
<div class="heading">
<span i18n:translate="">Send Link by Email</span> -
<span tal:content="view/target/title"></span></div>
<div>
<label i18n:translate="" for="subject">Subject</label>
<metal:content define-macro="mail_content">
<div>
<input name="subject" id="subject" style="width: 60em"
dojoType="dijit.form.ValidationTextBox" required
tal:attributes="value view/subject" /></div>
</div>
<div>
<label i18n:translate="" for="mailbody">Mail Body</label>
<label i18n:translate="" for="subject">Mail Subject</label>
<div>
<input name="subject" id="subject" style="width: 60em"
dojoType="dijit.form.ValidationTextBox" required
tal:attributes="value view/subject" /></div>
</div>
<div>
<textarea name="mailbody" cols="80" rows="4" id="mailbody"
dojoType="dijit.form.SimpleTextarea" style="width: 60em"
tal:content="view/mailBody"></textarea></div>
</div>
<label i18n:translate=""
for="mailbody">Mail Body</label>
<div>
<textarea name="mailbody" cols="80" rows="4" id="mailbody"
dojoType="dijit.form.SimpleTextarea" style="width: 60em"
tal:attributes="rows view/contentHeight|string:4"
tal:content="view/mailBody"></textarea></div>
</div>
</metal:content>
<div>
<label i18n:translate="">Recipients</label>
<div tal:repeat="member view/members">
@ -152,7 +156,8 @@
<span i18n:translate="">Toggle all</span></div>
</div>
<div>
<label i18n:translate="" for="addrecipients">Additional recipients</label>
<label i18n:translate=""
for="addrecipients">Additional Recipients</label>
<div>
<textarea name="addrecipients" cols="80" rows="4" id="addrecipients"
dojoType="dijit.form.SimpleTextarea"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,26 @@
#
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
Interface definitions for comments - discussions - forums.
"""
from zope.interface import Interface, Attribute
from zope import schema
from cybertools.tracking.comment.interfaces import IComment

View file

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

View file

@ -0,0 +1,75 @@
#
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
Report views and definitions for comments listings and similar stuff.
"""
from cybertools.util.jeep import Jeep
from loops.expert.browser.report import ReportConceptView
from loops.expert.field import Field, StateField, TargetField
from loops.expert.field import TrackDateField
from loops.expert.report import ReportInstance, TrackRow
class CommentsOverview(ReportConceptView):
reportName = 'comments_overview'
timeStamp = TrackDateField('timeStamp', u'Timestamp',
description=u'The date and time the comment was posted.',
part='dateTime', descending=True,
executionSteps=['sort', 'output'])
target = TargetField('taskId', u'Target',
description=u'The resource or concept the comment was posted at.',
executionSteps=['output'])
name = Field('name', u'Name',
description=u'The name addres of the poster.',
executionSteps=['output'])
email = Field('email', u'E-Mail Address',
description=u'The email addres of the poster.',
executionSteps=['output'])
subject = Field('subject', u'Subject',
description=u'The subject of the comment.',
executionSteps=['output'])
state = StateField('state', u'State',
description=u'The state of the comment.',
cssClass='center',
statesDefinition='organize.commentStates',
executionSteps=['query', 'sort', 'output'])
class CommentsRow(TrackRow):
pass
class CommentsReportInstance(ReportInstance):
type = "comments_overview"
label = u'Comments Overview'
rowFactory = CommentsRow
fields = Jeep((timeStamp, target, name, email, subject, state))
defaultOutputFields = fields
defaultSortCriteria = (state, timeStamp)
def selectObjects(self, parts):
return self.recordManager['comments'].values()

View file

@ -0,0 +1,4 @@
concept(u'work_statement', u'Leistungsabrechnung', u'report',
report_type=u'work_report')
concept(u'work_plan', u'Aktivitätenplanung', u'report',
report_type=u'work_plan_report')

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -79,8 +79,10 @@ class MemberRegistrationManager(object):
if pfName is None:
pfName = options(self.principalfolder_key,
(self.default_principalfolder,))[0]
self.createPrincipal(pfName, userId, password, lastName, firstName,
useExisting=useExisting)
rc = self.createPrincipal(pfName, userId, password,
lastName, firstName, useExisting=useExisting)
if rc is not None:
return rc
if not groups:
groups = options(self.groups_key, ())
self.setGroupsForPrincipal(pfName, userId, groups=groups)
@ -90,6 +92,8 @@ class MemberRegistrationManager(object):
def createPrincipal(self, pfName, userId, password, lastName,
firstName=u'', groups=[], useExisting=False,
overwrite=False, **kw):
if not self.checkPrincipalId(userId):
return dict(fieldName='loginName', error='illegal_loginname')
pFolder = getPrincipalFolder(self.context, pfName)
if IPersonBasedAuthenticator.providedBy(pFolder):
pFolder.setPassword(userId, password)
@ -125,10 +129,18 @@ class MemberRegistrationManager(object):
if gFolder is not None:
group = gFolder.get(gName)
if group is not None:
members = list(group.principals)
members = [p for p in group.principals
if self.checkPrincipalId(p)]
members.append(pFolder.prefix + userId)
group.principals = members
def checkPrincipalId(self, pid):
try:
pid = str(pid)
return True
except UnicodeEncodeError:
return False
def createPersonForPrincipal(self, pfName, userId, lastName, firstName=u'',
useExisting=False, **kw):
concepts = self.context.getConceptManager()

View file

@ -24,6 +24,7 @@ from persistent.mapping import PersistentMapping
from zope import interface, component
from zope.app.principalannotation import annotations
from zope.app.security.interfaces import IAuthentication, PrincipalLookupError
from zope.app.security.interfaces import IUnauthenticatedPrincipal
from zope.component import adapts
from zope.interface import implements
from zope.cachedescriptors.property import Lazy
@ -57,11 +58,13 @@ PredicateInterfaceSourceList.predicateInterfaces += (IHasRole,)
def getPersonForUser(context, request=None, principal=None):
if context is None:
return None
if principal is None:
if request is None:
principal = getCurrentPrincipal()
else:
if request is not None:
principal = getattr(request, 'principal', None)
else:
principal = getPrincipal(context)
if principal is None:
return None
loops = baseObject(context).getLoopsRoot()
@ -76,6 +79,15 @@ def getPersonForUser(context, request=None, principal=None):
return pa.get(util.getUidForObject(loops))
def getPrincipal(context):
principal = getCurrentPrincipal()
if principal is not None:
if IUnauthenticatedPrincipal.providedBy(principal):
return None
return principal
return None
class Person(AdapterBase, BasePerson):
""" typeInterface adapter for concepts of type 'person'.
"""
@ -95,9 +107,11 @@ class Person(AdapterBase, BasePerson):
return
person = getPersonForUser(self.context, principal=principal)
if person is not None and person != self.context:
raise ValueError(
'Error when creating user %s: There is already a person (%s) assigned to user %s.'
% (getName(self.context), getName(person), userId))
name = getName(person)
if name:
raise ValueError(
'There is already a person (%s) assigned to user %s.'
% (getName(person), userId))
pa = annotations(principal)
loopsId = util.getUidForObject(self.context.getLoopsRoot())
ann = pa.get(ANNOTATION_KEY)

View file

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

View file

@ -57,15 +57,22 @@ class FavoriteView(NodeView):
def listFavorites(self):
if self.favorites is None:
return
for uid in self.favorites.list(self.person):
self.registerDojoDnd()
form = self.request.form
if 'favorites_change_order' in form:
uids = form.get('favorite_uids')
if uids:
self.favorites.reorder(uids)
for trackUid, uid in self.favorites.listWithTracks(self.person):
obj = util.getObjectForUid(uid)
if obj is not None:
adobj = adapted(obj)
yield dict(url=self.getUrlForTarget(obj),
uid=uid,
title=adobj.favTitle,
description=adobj.description,
object=obj)
title=obj.title,
description=obj.description,
object=obj,
trackUid=trackUid)
def add(self):
if self.favorites is None:

View file

@ -1,24 +1,42 @@
<metal:actions define-macro="favorites_portlet"
tal:define="view nocall:context/@@favorites_view;
targetUid view/targetUid">
<div tal:repeat="item view/listFavorites">
<span style="float:right" class="delete-item">&nbsp;<a href="removeFavorite.html"
tal:attributes="href
string:${view/virtualTargetUrl}/removeFavorite.html?id=${item/uid};
title string:Remove from favorites"
i18n:attributes="title">X</a>&nbsp;</span>
<a tal:attributes="href item/url;
title item/description"
tal:content="item/title">Some object</a>
<form method="post">
<div dojoType="dojo.dnd.Source" withHandles="true" id="favorites_list">
<div class="dojoDndItem dojoDndHandle" style="padding: 0"
tal:repeat="item view/listFavorites">
<span style="float:right" class="delete-item">&nbsp;<a
tal:attributes="href
string:${view/virtualTargetUrl}/removeFavorite.html?id=${item/uid};
title string:Remove from favorites"
i18n:attributes="title">X</a>&nbsp;</span>
<a tal:attributes="href item/url;
title item/description"
tal:content="item/title">Some object</a>
<input type="hidden" name="favorite_uids:list"
tal:attributes="value item/trackUid" />
</div>
</div>
<div id="addFavorite" class="action"
tal:condition="targetUid">
<a i18n:translate=""
tal:attributes="href
string:${view/virtualTargetUrl}/addFavorite.html?id=$targetUid;
title string:Add current object to favorites"
i18n:attributes="title">Add to Favorites</a>
<div>
<input type="submit" style="display: none"
name="favorites_change_order" id="favorites_change_order"
value="Save Changes"
i18n:attributes="value" />
<script language="javascript">
dojo.subscribe('/dnd/drop', function(data) {
if (data.node.id == 'favorites_list') {
dojo.byId('favorites_change_order').style.display = ''}});
</script>
</div>
</form>
<div id="addFavorite" class="action"
tal:condition="targetUid">
<a i18n:translate=""
tal:attributes="href
string:${view/virtualTargetUrl}/addFavorite.html?id=$targetUid;
title string:Add current object to favorites"
i18n:attributes="title">Add to Favorites</a>
</div>
</metal:actions>

View file

@ -41,12 +41,16 @@ class Favorites(object):
for item in self.listTracks(person, sortKey, type):
yield item.taskId
def listWithTracks(self, person, sortKey=None, type='favorite'):
for item in self.listTracks(person, sortKey, type):
yield util.getUidForObject(item), item.taskId
def listTracks(self, person, sortKey=None, type='favorite'):
if person is None:
return
personUid = util.getUidForObject(person)
if sortKey is None:
sortKey = lambda x: -x.timeStamp
sortKey = lambda x: (x.data.get('order', 100), -x.timeStamp)
for item in sorted(self.context.query(userName=personUid), key=sortKey):
if type is not None:
if item.type != type:
@ -59,7 +63,7 @@ class Favorites(object):
uid = util.getUidForObject(obj)
personUid = util.getUidForObject(person)
if data is None:
data = {'type': 'favorite'}
data = {'type': 'favorite', 'order': 100}
if nodups:
for track in self.context.query(userName=personUid, taskId=uid):
if track.type == data['type']: # already present
@ -78,6 +82,18 @@ class Favorites(object):
self.context.removeTrack(track)
return changed
def reorder(self, uids):
offset = 0
for idx, uid in enumerate(uids):
track = util.getObjectForUid(uid)
if track is not None:
data = track.data
order = data.get('order', 100)
if order < idx or (order >= 100 and order < idx + 100):
offset = 100
data['order'] = idx + offset
track.data = data
class Favorite(Track):
@ -110,7 +126,7 @@ def updateSortInfo(person, task, data):
break
else:
if data:
Favorites(favorites).add(task, person,
Favorites(favorites).add(task, person,
dict(type='sort', sortInfo=data))
return data

Some files were not shown because too many files have changed in this diff Show more