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 *.pyc
*.pyo *.pyo
dist/
*.project *.project
*.pydevproject *.pydevproject
*.sublime-project *.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) >>> component.provideAdapter(NameChooser)
>>> request = TestRequest(form={'title': u'Test Note', >>> 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) >>> view = NodeView(m112, request)
>>> cont = CreateObject(view, request) >>> cont = CreateObject(view, request)
>>> cont.update() >>> cont.update()
@ -802,7 +804,7 @@ The new technique uses the ``fields`` and ``data`` attributes...
linkText textline False None linkText textline False None
>>> view.data >>> 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'} 'linkText': u'', 'title': u'Test Note'}
The object is changed via a FormController adapter created for 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()) >>> sorted(adapted(concepts['countries']).data.items())
[('at', ['Austria']), ('de', ['Germany'])] [('at', ['Austria']), ('de', ['Germany'])]
>>> countries.dataAsRecords()
[{'value': 'Austria', 'key': 'at'}, {'value': 'Germany', 'key': 'de'}]
>>> countries.getRowsByValue('value', 'Germany')
[{'value': 'Germany', 'key': 'de'}]
Caching Caching
======= =======
@ -932,6 +940,12 @@ Security
>>> from loops.security.browser import admin, audit >>> from loops.security.browser import admin, audit
Paster Shell Utilities - Repair Scripts
=======================================
>>> from loops.repair.base import removeRecords
Import/Export Import/Export
============= =============

View file

@ -1,22 +1,17 @@
# # package loops
# 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
#
""" # 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 # 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 from zope.app.container.btree import BTreeContainer

View file

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

View file

@ -22,7 +22,7 @@ Common base class for loops browser view classes.
from cgi import parse_qs, parse_qsl from cgi import parse_qs, parse_qsl
#import mimetypes # use more specific assignments from cybertools.text #import mimetypes # use more specific assignments from cybertools.text
from datetime import datetime from datetime import date, datetime
from logging import getLogger from logging import getLogger
import re import re
from time import strptime from time import strptime
@ -62,17 +62,21 @@ from cybertools.stateful.interfaces import IStateful
from cybertools.text import mimetypes from cybertools.text import mimetypes
from cybertools.typology.interfaces import IType, ITypeManager from cybertools.typology.interfaces import IType, ITypeManager
from cybertools.util.date import toLocalTime from cybertools.util.date import toLocalTime
from cybertools.util.format import formatDate
from cybertools.util.jeep import Jeep from cybertools.util.jeep import Jeep
from loops.browser.util import normalizeForUrl from loops.browser.util import normalizeForUrl
from loops.common import adapted, baseObject from loops.common import adapted, baseObject
from loops.config.base import DummyOptions from loops.config.base import DummyOptions
from loops.i18n.browser import I18NView from loops.i18n.browser import I18NView
from loops.interfaces import IResource, IView, INode, ITypeConcept from loops.interfaces import IResource, IView, INode, ITypeConcept
from loops.organize.personal import favorite
from loops.organize.party import getPersonForUser
from loops.organize.tracking import access from loops.organize.tracking import access
from loops.organize.util import getRolesForPrincipal from loops.organize.util import getRolesForPrincipal
from loops.resource import Resource from loops.resource import Resource
from loops.security.common import checkPermission from loops.security.common import checkPermission
from loops.security.common import canAccessObject, canListObject, canWriteObject from loops.security.common import canAccessObject, canListObject, canWriteObject
from loops.security.common import canEditRestricted
from loops.type import ITypeConcept, LoopsTypeInfo from loops.type import ITypeConcept, LoopsTypeInfo
from loops import util from loops import util
from loops.util import _, saveRequest from loops.util import _, saveRequest
@ -137,7 +141,58 @@ class EditForm(form.EditForm):
return parentUrl + '/contents.html' return parentUrl + '/contents.html'
class BaseView(GenericView, I18NView): class SortableMixin(object):
@Lazy
def sortInfo(self):
result = {}
for k, v in self.request.form.items():
if k.startswith('sortinfo_'):
tableName = k[len('sortinfo_'):]
if ',' in v:
fn, dir = v.split(',')
else:
fn = v
dir = 'asc'
result[tableName] = dict(
colName=fn, ascending=(dir=='asc'), fparam=v)
result = favorite.updateSortInfo(getPersonForUser(
self.context, self.request), self.target, result)
return result
def isSortableColumn(self, tableName, colName):
return False # overwrite in subclass
def getSortUrl(self, tableName, colName):
url = str(self.request.URL)
paramChar = '?' in url and '&' or '?'
si = self.sortInfo.get(tableName)
if si is not None and si.get('colName') == colName:
dir = si['ascending'] and 'desc' or 'asc'
else:
dir = 'asc'
return '%s%ssortinfo_%s=%s,%s' % (url, paramChar, tableName, colName, dir)
def getSortParams(self, tableName):
url = str(self.request.URL)
paramChar = '?' in url and '&' or '?'
si = self.sortInfo.get(tableName)
if si is not None:
colName = si['colName']
dir = si['ascending'] and 'asc' or 'desc'
return '%ssortinfo_%s=%s,%s' % (paramChar, tableName, colName, dir)
return ''
def getSortImage(self, tableName, colName):
si = self.sortInfo.get(tableName)
if si is not None and si.get('colName') == colName:
if si['ascending']:
return '/@@/cybertools.icons/arrowdown.gif'
else:
return '/@@/cybertools.icons/arrowup.gif'
class BaseView(GenericView, I18NView, SortableMixin):
actions = {} actions = {}
portlet_actions = [] portlet_actions = []
@ -146,6 +201,7 @@ class BaseView(GenericView, I18NView):
icon = None icon = None
modeName = 'view' modeName = 'view'
isToplevel = False isToplevel = False
isVisible = True
def __init__(self, context, request): def __init__(self, context, request):
context = baseObject(context) context = baseObject(context)
@ -163,6 +219,10 @@ class BaseView(GenericView, I18NView):
pass pass
saveRequest(request) saveRequest(request)
def todayFormatted(self):
return formatDate(date.today(), 'date', 'short',
self.languageInfo.language)
def checkPermissions(self): def checkPermissions(self):
return canAccessObject(self.context) return canAccessObject(self.context)
@ -214,6 +274,16 @@ class BaseView(GenericView, I18NView):
result.append(view) result.append(view)
return result return result
@Lazy
def urlParamString(self):
return self.getUrlParamString()
def getUrlParamString(self):
qs = self.request.get('QUERY_STRING')
if qs:
return '?' + qs
return ''
@Lazy @Lazy
def principalId(self): def principalId(self):
principal = self.request.principal principal = self.request.principal
@ -347,6 +417,10 @@ class BaseView(GenericView, I18NView):
def isPartOfPredicate(self): def isPartOfPredicate(self):
return self.conceptManager.get('ispartof') return self.conceptManager.get('ispartof')
@Lazy
def queryTargetPredicate(self):
return self.conceptManager.get('querytarget')
@Lazy @Lazy
def memberPredicate(self): def memberPredicate(self):
return self.conceptManager.get('ismember') return self.conceptManager.get('ismember')
@ -395,6 +469,10 @@ class BaseView(GenericView, I18NView):
def description(self): def description(self):
return self.adapted.description return self.adapted.description
@Lazy
def tabTitle(self):
return u'Info'
@Lazy @Lazy
def additionalInfos(self): def additionalInfos(self):
return [] return []
@ -747,6 +825,8 @@ class BaseView(GenericView, I18NView):
return result return result
def checkState(self): def checkState(self):
if checkPermission('loops.ManageSite', self.context):
return True
if not self.allStates: if not self.allStates:
return True return True
for stf in self.allStates: for stf in self.allStates:
@ -821,6 +901,10 @@ class BaseView(GenericView, I18NView):
def canAccessRestricted(self): def canAccessRestricted(self):
return checkPermission('loops.ViewRestricted', self.context) return checkPermission('loops.ViewRestricted', self.context)
@Lazy
def canEditRestricted(self):
return canEditRestricted(self.context)
def openEditWindow(self, viewName='edit.html'): def openEditWindow(self, viewName='edit.html'):
if self.editable: if self.editable:
if checkPermission('loops.ManageSite', self.context): if checkPermission('loops.ManageSite', self.context):
@ -943,6 +1027,12 @@ class BaseView(GenericView, I18NView):
jsCall = 'dojo.require("dojox.image.Lightbox");' jsCall = 'dojo.require("dojox.image.Lightbox");'
self.controller.macros.register('js-execute', jsCall, jsCall=jsCall) self.controller.macros.register('js-execute', jsCall, jsCall=jsCall)
def registerDojoComboBox(self):
self.registerDojo()
jsCall = ('dojo.require("dijit.form.ComboBox");')
self.controller.macros.register('js-execute',
'dojo.require.ComboBox', jsCall=jsCall)
def registerDojoFormAll(self): def registerDojoFormAll(self):
self.registerDojo() self.registerDojo()
self.registerDojoEditor() self.registerDojoEditor()
@ -996,6 +1086,7 @@ class LoggedIn(object):
params = parse_qsl(qs) params = parse_qsl(qs)
params = [(k, v) for k, v in params if k != 'loops.messages.top:record'] params = [(k, v) for k, v in params if k != 'loops.messages.top:record']
params.append(('loops.messages.top:record', message.encode('UTF-8'))) params.append(('loops.messages.top:record', message.encode('UTF-8')))
url = url.encode('utf-8')
return '%s?%s' % (url, urlencode(params)) return '%s?%s' % (url, urlencode(params))
# vocabulary stuff # 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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -254,18 +254,35 @@ class ConceptView(BaseView):
result.append(view) result.append(view)
return result 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 @Lazy
def adapted(self): def adapted(self):
return adapted(self.context, self.languageInfo) 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 @Lazy
def targetUrl(self): def targetUrl(self):
return self.nodeView.getUrlForTarget(self.context) return self.nodeView.getUrlForTarget(self.context)
@ -282,8 +299,17 @@ class ConceptView(BaseView):
def breadcrumbsTitle(self): def breadcrumbsTitle(self):
return self.title return self.title
@Lazy
def showInBreadcrumbs(self):
return (self.options('show_in_breadcrumbs') or
self.typeOptions('show_in_breadcrumbs'))
@Lazy @Lazy
def breadcrumbsParent(self): def breadcrumbsParent(self):
for p in self.context.getParents([self.defaultPredicate]):
view = self.nodeView.getViewForTarget(p)
if view.showInBreadcrumbs:
return view
return None return None
def getData(self, omit=('title', 'description')): def getData(self, omit=('title', 'description')):
@ -389,7 +415,8 @@ class ConceptView(BaseView):
children = getChildren children = getChildren
def childrenAlphaGroups(self, predicates=None): def childrenAlphaGroups(self, predicates=None):
result = Jeep() #result = Jeep()
result = {}
rels = self.getChildren(predicates=predicates or [self.defaultPredicate], rels = self.getChildren(predicates=predicates or [self.defaultPredicate],
topLevelOnly=False, sort=False) topLevelOnly=False, sort=False)
rels = sorted(rels, key=lambda r: r.title.lower()) rels = sorted(rels, key=lambda r: r.title.lower())
@ -449,7 +476,7 @@ class ConceptView(BaseView):
if r.order != pos: if r.order != pos:
r.order = pos r.order = pos
def getResources(self): def getResources(self, relView=None, sort='default'):
form = self.request.form form = self.request.form
#if form.get('loops.viewName') == 'index.html' and self.editable: #if form.get('loops.viewName') == 'index.html' and self.editable:
if self.editable: if self.editable:
@ -458,13 +485,17 @@ class ConceptView(BaseView):
tokens = form.get('resources_tokens') tokens = form.get('resources_tokens')
if tokens: if tokens:
self.reorderResources(tokens) self.reorderResources(tokens)
from loops.browser.resource import ResourceRelationView if relView is None:
from loops.browser.resource import ResourceRelationView
relView = ResourceRelationView
from loops.organize.personal.browser.filter import FilterView from loops.organize.personal.browser.filter import FilterView
fv = FilterView(self.context, self.request) fv = FilterView(self.context, self.request)
rels = self.context.getResourceRelations() rels = self.context.getResourceRelations(sort=sort)
for r in rels: for r in rels:
if fv.check(r.first): if fv.check(r.first):
yield ResourceRelationView(r, self.request, contextIsSecond=True) view = relView(r, self.request, contextIsSecond=True)
if view.checkState():
yield view
def resources(self): def resources(self):
return self.getResources() return self.getResources()

View file

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

View file

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

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

View file

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

View file

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

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) { function destroyWidgets(node) {
dojo.forEach(dojo.query('[widgetId]', node), function(n) { dojo.forEach(dojo.query('[widgetId]', node), function(n) {
w = dijit.byNode(n); w = dijit.byNode(n);
@ -103,7 +132,7 @@ function submitReplacing(targetId, formId, url) {
mimetype: "text/html", mimetype: "text/html",
load: function(response, ioArgs) { load: function(response, ioArgs) {
replaceNode(response, targetId); replaceNode(response, targetId);
return resonse; return response;
} }
}) })
} }
@ -115,7 +144,7 @@ function xhrSubmitPopup(formId, url) {
mimetype: "text/html", mimetype: "text/html",
load: function(response, ioArgs) { load: function(response, ioArgs) {
window.close(); 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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -86,10 +86,14 @@ class NodeView(BaseView):
super(NodeView, self).__init__(context, request) super(NodeView, self).__init__(context, request)
self.viewAnnotations.setdefault('nodeView', self) self.viewAnnotations.setdefault('nodeView', self)
self.viewAnnotations.setdefault('node', self.context) self.viewAnnotations.setdefault('node', self.context)
viewConfig = getViewConfiguration(context, request) self.setSkin(self.viewConfig.get('skinName'))
self.setSkin(viewConfig.get('skinName'))
def __call__(self, *args, **kw): 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') tv = self.viewAnnotations.get('targetView')
if tv is not None: if tv is not None:
if tv.isToplevel: if tv.isToplevel:
@ -98,6 +102,29 @@ class NodeView(BaseView):
self.controller.setMainPage() self.controller.setMainPage()
return super(NodeView, self).__call__(*args, **kw) 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 @Lazy
def macro(self): def macro(self):
return self.template.macros['content'] return self.template.macros['content']
@ -115,7 +142,9 @@ class NodeView(BaseView):
parts.extend(getParts(n)) parts.extend(getParts(n))
return parts 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() result = super(NodeView, self).update()
self.recordAccess() self.recordAccess()
return result return result
@ -129,7 +158,7 @@ class NodeView(BaseView):
return [] return []
menu = self.menu menu = self.menu
data = [dict(label=menu.title, url=menu.url)] data = [dict(label=menu.title, url=menu.url)]
menuItem = self.nearestMenuItem menuItem = self.getNearestMenuItem(all=True)
if menuItem != menu.context: if menuItem != menu.context:
data.append(dict(label=menuItem.title, data.append(dict(label=menuItem.title,
url=absoluteURL(menuItem, self.request))) url=absoluteURL(menuItem, self.request)))
@ -140,6 +169,9 @@ class NodeView(BaseView):
url=absoluteURL(p, self.request))) url=absoluteURL(p, self.request)))
if self.virtualTarget: if self.virtualTarget:
data.extend(self.virtualTarget.breadcrumbs()) data.extend(self.virtualTarget.breadcrumbs())
if data and not '?' in data[-1]['url']:
if self.urlParamString:
data[-1]['url'] += self.urlParamString
return data return data
def viewModes(self): def viewModes(self):
@ -366,6 +398,10 @@ class NodeView(BaseView):
def editable(self): def editable(self):
return canWrite(self.context, 'body') return canWrite(self.context, 'body')
def hasTopPage(self, name):
page = self.topMenu.context.get(name)
return page is not None
# menu stuff # menu stuff
@Lazy @Lazy
@ -411,8 +447,9 @@ class NodeView(BaseView):
@Lazy @Lazy
def menuItems(self): def menuItems(self):
return [NodeView(child, self.request) items = [NodeView(child, self.request).view
for child in self.context.getMenuItems()] for child in self.context.getMenuItems()]
return [item for item in items if item.isVisible]
@Lazy @Lazy
def parents(self): def parents(self):
@ -420,10 +457,13 @@ class NodeView(BaseView):
@Lazy @Lazy
def nearestMenuItem(self): def nearestMenuItem(self):
return self.getNearestMenuItem()
def getNearestMenuItem(self, all=False):
menu = self.menuObject menu = self.menuObject
menuItem = None menuItem = None
for p in [self.context] + self.parents: for p in [self.context] + self.parents:
if not p.isMenuItem(): if not all and not p.isMenuItem():
menuItem = None menuItem = None
elif menuItem is None: elif menuItem is None:
menuItem = p menuItem = p
@ -469,7 +509,7 @@ class NodeView(BaseView):
def targetView(self, name='index.html', methodName='show'): def targetView(self, name='index.html', methodName='show'):
if name == 'index.html': # only when called for default view if name == 'index.html': # only when called for default view
tv = self.viewAnnotations.get('targetView') tv = self.viewAnnotations.get('targetView')
if tv is not None: if tv is not None and callable(tv):
return tv() return tv()
if '?' in name: if '?' in name:
name, params = name.split('?', 1) name, params = name.split('?', 1)
@ -567,12 +607,21 @@ class NodeView(BaseView):
""" Return URL of given target view given as .XXX URL. """ Return URL of given target view given as .XXX URL.
""" """
if isinstance(target, BaseView): 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) return self.makeTargetUrl(self.url, target.uniqueId, target.title)
else: else:
target = baseObject(target) target = baseObject(target)
return self.makeTargetUrl(self.url, util.getUidForObject(target), return self.makeTargetUrl(self.url, util.getUidForObject(target),
target.title) 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): def getActions(self, category='object', page=None, target=None):
actions = [] actions = []
#self.registerDojo() #self.registerDojo()
@ -976,7 +1025,8 @@ class NodeTraverser(ItemTraverser):
if context.nodeType == 'menu': if context.nodeType == 'menu':
setViewConfiguration(context, request) setViewConfiguration(context, request)
if name == '.loops': if name == '.loops':
return self.context.getLoopsRoot() name = self.getTargetUid(request)
#return self.context.getLoopsRoot()
if name.startswith('.'): if name.startswith('.'):
name = self.cleanUpTraversalStack(request, name)[1:] name = self.cleanUpTraversalStack(request, name)[1:]
target = self.getTarget(name) target = self.getTarget(name)
@ -1008,17 +1058,34 @@ class NodeTraverser(ItemTraverser):
raise raise
return obj 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): def cleanUpTraversalStack(self, request, name):
traversalStack = request._traversal_stack #traversalStack = request._traversal_stack
while traversalStack and traversalStack[0].startswith('.'): #while traversalStack and traversalStack[0].startswith('.'):
# skip obsolete target references in the url # skip obsolete target references in the url
name = traversalStack.pop(0) # name = traversalStack.pop(0)
traversedNames = request._traversed_names traversedNames = request._traversed_names
if traversedNames: for n in list(traversedNames):
lastTraversed = traversedNames[-1] if n.startswith('.'):
if lastTraversed.startswith('.') and lastTraversed != name: # 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 # 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 return name
def getTarget(self, name): def getTarget(self, name):

View file

@ -30,7 +30,7 @@
item nocall:target" item nocall:target"
tal:attributes="class string:content-$level; tal:attributes="class string:content-$level;
id id; id id;
ondblclick python: target.openEditWindow('configure.html')"> ondblclick python:target.openEditWindow('configure.html')">
<metal:body use-macro="item/macro"> <metal:body use-macro="item/macro">
The body The body
</metal:body> </metal:body>
@ -41,17 +41,22 @@
<metal:body define-macro="conceptbody"> <metal:body define-macro="conceptbody">
<tal:body define="body item/body;"> <tal:body define="body item/body;
itemNum view/itemNum;
id string:$itemNum.body">
<div class="content-1" id="1" <div class="content-1" id="1"
tal:attributes="class string:content-$level; tal:attributes="class string:content-$level;
id string:${view/itemNum}.body; id string:${view/itemNum}.body;
ondblclick python: item.openEditWindow('configure.html')"> ondblclick python:item.openEditWindow('configure.html')">
<span tal:content="structure body">Node Body</span> <span tal:content="structure body">Node Body</span>
</div> </div>
<tal:concepts define="item nocall:item/targetObjectView; <div tal:define="item nocall:item/targetObjectView;
macro item/macro"> macro item/macro">
<div metal:use-macro="macro" /> <div tal:attributes="class string:content-$level;
</tal:concepts> id id;">
<div metal:use-macro="macro" />
</div>
</div>
</tal:body> </tal:body>
</metal:body> </metal:body>
@ -328,11 +333,12 @@
<metal:login define-macro="login"> <metal:login define-macro="login">
<div> <div>
<a href="login.html" <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')" <div tal:define="register python:view.globalOptions('provideLogin')"
tal:condition="register"> tal:condition="python:register and register != True">
<a tal:condition="python:register != True" <a tal:define="reg python:register[0]"
tal:attributes="href python:register[0]" tal:attributes="href string:${view/topMenu/url}/$reg"
i18n:translate="">Register new member</a></div> i18n:translate="">Register new member</a></div>
</metal:login> </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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -20,6 +20,7 @@
View class for resource objects. View class for resource objects.
""" """
import os.path
import urllib import urllib
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope import component from zope import component
@ -47,7 +48,7 @@ from loops.browser.common import EditForm, BaseView
from loops.browser.concept import BaseRelationView, ConceptRelationView from loops.browser.concept import BaseRelationView, ConceptRelationView
from loops.browser.concept import ConceptConfigureView from loops.browser.concept import ConceptConfigureView
from loops.browser.node import NodeView, node_macros from loops.browser.node import NodeView, node_macros
from loops.common import adapted, NameChooser, normalizeName from loops.common import adapted, baseObject, NameChooser, normalizeName
from loops.interfaces import IBaseResource, IDocument, ITextDocument from loops.interfaces import IBaseResource, IDocument, ITextDocument
from loops.interfaces import IMediaAsset as legacy_IMediaAsset from loops.interfaces import IMediaAsset as legacy_IMediaAsset
from loops.interfaces import ITypeConcept from loops.interfaces import ITypeConcept
@ -196,6 +197,9 @@ class ResourceView(BaseView):
context = self.context context = self.context
ct = context.contentType ct = context.contentType
response = self.request.response 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) self.recordAccess('show', target=self.uniqueId)
if ct.startswith('image/'): if ct.startswith('image/'):
#response.setHeader('Cache-Control', 'public,max-age=86400') #response.setHeader('Cache-Control', 'public,max-age=86400')
@ -216,6 +220,16 @@ class ResourceView(BaseView):
if filename is None: if filename is None:
filename = (adapted(self.context).localFilename or filename = (adapted(self.context).localFilename or
getName(self.context)) getName(self.context))
if self.typeOptions('use_title_for_download_filename'):
base, ext = os.path.splitext(filename)
filename = context.title
vr = IVersionable(baseObject(context))
if len(vr.versions) > 0:
filename = vr.generateName(filename, ext, vr.versionId)
else:
if not filename.endswith(ext):
filename += ext
filename = filename.encode('UTF-8')
if self.typeOptions('no_normalize_download_filename'): if self.typeOptions('no_normalize_download_filename'):
filename = '"%s"' % filename filename = '"%s"' % filename
else: else:
@ -262,11 +276,17 @@ class ResourceView(BaseView):
#return util.toUnicode(wp.render(self.request)) #return util.toUnicode(wp.render(self.request))
return super(ResourceView, self).renderText(text, contentType) return super(ResourceView, self).renderText(text, contentType)
showMore = True
def renderShortText(self): def renderShortText(self):
return self.renderDescription() or self.createShortText(self.render()) return self.renderDescription() or self.createShortText(self.render())
def createShortText(self, text=None): 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): def download(self):
""" Force download, e.g. of a PDF file """ """ Force download, e.g. of a PDF file """
@ -471,4 +491,3 @@ class NoteView(DocumentView):
def linkUrl(self): def linkUrl(self):
ad = self.typeAdapter ad = self.typeAdapter
return ad and ad.linkUrl or '' return ad and ad.linkUrl or ''

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@
method="post" name="listing" action="." method="post" name="listing" action="."
tal:define="target nocall:view/target" tal:define="target nocall:view/target"
tal:condition="python: target or items" tal:condition="python: target or items"
tal:attributes="action request/URL"> tal:attributes="action view/requestUrl">
<input type="hidden" name="action" value="assign" <input type="hidden" name="action" value="assign"
tal:attributes="value action" /> tal:attributes="value action" />
<table class="listing" summary="Currently assigned" <table class="listing" summary="Currently assigned"
@ -82,7 +82,7 @@
<fieldset> <fieldset>
<legend i18n:translate="">Create Target</legend> <legend i18n:translate="">Create Target</legend>
<form method="post" name="listing" action="." <form method="post" name="listing" action="."
tal:attributes="action request/URL"> tal:attributes="action view/requestUrl">
<input type="hidden" name="action" value="create" /> <input type="hidden" name="action" value="create" />
<div class="row"> <div class="row">
<span i18n:translate="">Name</span> <span i18n:translate="">Name</span>
@ -113,7 +113,7 @@
<metal:search define-macro="search"> <metal:search define-macro="search">
<form method="post" name="listing" action="." <form method="post" name="listing" action="."
tal:attributes="action request/URL"> tal:attributes="action view/requestUrl">
<input type="hidden" name="action" value="search" /> <input type="hidden" name="action" value="search" />
<div class="row" <div class="row"
tal:define="searchTerm request/searchTerm | nothing; 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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Adapters and others classes for analyzing resources. Adapters and others classes for analyzing resources.
$Id$
""" """
from itertools import tee from itertools import tee
@ -41,6 +39,7 @@ from loops.resource import Resource
from loops.setup import addAndConfigureObject from loops.setup import addAndConfigureObject
from loops.type import TypeInterfaceSourceList from loops.type import TypeInterfaceSourceList
logger = getLogger('Classifier')
TypeInterfaceSourceList.typeInterfaces += (IClassifier,) TypeInterfaceSourceList.typeInterfaces += (IClassifier,)
@ -102,15 +101,15 @@ class Classifier(AdapterBase):
if resource not in resources: if resource not in resources:
concept.assignResource(resource, predicate) concept.assignResource(resource, predicate)
message = u'Assigning: %s %s %s' message = u'Assigning: %s %s %s'
self.log(message % (resource.title, predicate.title, concept.title), 5)
else: else:
message = u'Already assigned: %s %s %s' 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): def log(self, message, level=5):
if level >= self.logLevel: if level >= self.logLevel:
#print 'Classifier %s:' % getName(self.context), message #print 'Classifier %s:' % getName(self.context), message
getLogger('Classifier').info( logger.info(u'%s: %s' % (getName(self.context), message))
u'%s: %s' % (getName(self.context), message))
class Extractor(object): 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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,17 +18,20 @@
""" """
View class(es) for resource classifiers. View class(es) for resource classifiers.
$Id$
""" """
from logging import getLogger
import transaction
from zope import interface, component from zope import interface, component
from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.traversing.api import getName
from loops.browser.concept import ConceptView from loops.browser.concept import ConceptView
from loops.common import adapted from loops.common import adapted
logger = getLogger('ClassifierView')
class ClassifierView(ConceptView): class ClassifierView(ConceptView):
@ -42,12 +45,18 @@ class ClassifierView(ConceptView):
if 'update' in self.request.form: if 'update' in self.request.form:
cta = adapted(self.context) cta = adapted(self.context)
if cta is not None: 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) cta.process(r)
logger.info('Finished processing')
transaction.commit()
return True return True
def collectResources(concept, checkedConcepts=None, result=None): def collectResources(concept, checkedConcepts=None, result=None):
logger.info('Start collecting resources for %s' % getName(concept))
if result is None: if result is None:
result = [] result = []
if checkedConcepts is None: if checkedConcepts is None:
@ -59,4 +68,5 @@ def collectResources(concept, checkedConcepts=None, result=None):
if c not in checkedConcepts: if c not in checkedConcepts:
checkedConcepts.append(c) checkedConcepts.append(c)
collectResources(c, checkedConcepts, result) collectResources(c, checkedConcepts, result)
logger.info('Collected %s resources' % len(result))
return result return result

View file

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

View file

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

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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -34,6 +34,7 @@ from loops.browser.concept import ConceptRelationView as \
BaseConceptRelationView BaseConceptRelationView
from loops.browser.resource import ResourceView as BaseResourceView from loops.browser.resource import ResourceView as BaseResourceView
from loops.common import adapted, baseObject from loops.common import adapted, baseObject
from loops.util import _
standard_template = standard.standard_template standard_template = standard.standard_template
@ -54,42 +55,6 @@ class Base(object):
def sectionType(self): def sectionType(self):
return self.conceptManager['section'] 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 @Lazy
def tabview(self): def tabview(self):
if self.editable: if self.editable:
@ -107,6 +72,7 @@ class Base(object):
@Lazy @Lazy
def textResources(self): def textResources(self):
self.images = [[]] self.images = [[]]
self.otherResources = []
result = [] result = []
idx = 0 idx = 0
for rv in self.getResources(): for rv in self.getResources():
@ -115,7 +81,7 @@ class Base(object):
idx += 1 idx += 1
result.append(rv) result.append(rv)
self.images.append([]) self.images.append([])
else: elif rv.context.contentType.startswith('image/'):
self.registerDojoLightbox() self.registerDojoLightbox()
url = self.nodeView.getUrlForTarget(rv.context) url = self.nodeView.getUrlForTarget(rv.context)
src = '%s/mediaasset.html?v=small' % url src = '%s/mediaasset.html?v=small' % url
@ -123,6 +89,8 @@ class Base(object):
img = dict(src=src, fullImageUrl=fullSrc, title=rv.title, img = dict(src=src, fullImageUrl=fullSrc, title=rv.title,
description=rv.description, url=url, object=rv) description=rv.description, url=url, object=rv)
self.images[idx].append(img) self.images[idx].append(img)
else:
self.otherResources.append(rv)
return result return result
def getDocumentTypeForResource(self, r): def getDocumentTypeForResource(self, r):
@ -178,9 +146,47 @@ class SectionView(Base, ConceptView):
def macro(self): def macro(self):
return book_template.macros['section'] 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): class TopicView(Base, ConceptView):
tabTitle = _(u'title_bookTopicView')
@Lazy @Lazy
def macro(self): def macro(self):
return book_template.macros['topic'] return book_template.macros['topic']

View file

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

View file

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

View file

@ -1,6 +1,7 @@
<configure <configure
xmlns="http://namespaces.zope.org/zope" xmlns="http://namespaces.zope.org/zope"
xmlns:i18n="http://namespaces.zope.org/i18n" xmlns:i18n="http://namespaces.zope.org/i18n"
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="loops"> i18n_domain="loops">
<i18n:registerTranslations directory="locales" /> <i18n:registerTranslations directory="locales" />
@ -478,6 +479,19 @@
component="loops.view.NodeTypeSourceList" component="loops.view.NodeTypeSourceList"
name="loops.nodeTypeSource" /> 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=".browser" />
<include package=".classifier" /> <include package=".classifier" />

View file

@ -1,11 +1,14 @@
# types # types
type(u'query', u'Abfrage', options=u'', type(u'query', u'Abfrage', options=u'',
typeInterface='loops.expert.concept.IQueryConcept', viewName=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'', type(u'task', u'Aufgabe', options=u'',
typeInterface='loops.knowledge.interfaces.ITask', viewName=u'') typeInterface='loops.knowledge.interfaces.ITask', viewName=u'')
type(u'domain', u'Bereich', options=u'', typeInterface=u'', viewName=u'') type(u'domain', u'Bereich', options=u'', typeInterface=u'', viewName=u'')
type(u'classifier', u'Classifier', options=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'documenttype', u'Dokumentenart', options=u'', typeInterface=u'', viewName=u'')
type(u'extcollection', u'External Collection', options=u'', type(u'extcollection', u'External Collection', options=u'',
typeInterface='loops.integrator.interfaces.IExternalCollection', 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'', type(u'glossaryitem', u'Glossareintrag', options=u'',
typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html') typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html')
type(u'media_asset', u'Media Asset', 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', type(u'note', u'Notiz', options=u'', typeInterface='loops.interfaces.INote',
viewName='note.html') viewName='note.html')
type(u'person', u'Person', options=u'', type(u'person', u'Person', options=u'',
typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'') typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'')
type(u'predicate', u'Prädikat', options=u'', type(u'predicate', u'Prädikat', options=u'',
typeInterface=u'loops.interfaces.IPredicate', viewName=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') viewName=u'task.html')
type(u'textdocument', u'Text', options=u'', typeInterface='loops.interfaces.ITextDocument', viewName=u'') type(u'textdocument', u'Text', options=u'',
type(u'topic', u'Thema', options=u'action.portlet:createTopic,editTopic', typeInterface='loops.interfaces.ITextDocument', viewName=u'')
type(u'topic', u'Thema', options=u'action.portlet:editTopic,createTopic',
typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'') typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'')
type(u'type', u'Typ', options=u'', typeInterface='loops.interfaces.ITypeConcept', type(u'type', u'Typ', options=u'',
viewName=u'') typeInterface='loops.interfaces.ITypeConcept', viewName=u'')
# domains # domains
concept(u'general', u'Allgemein', u'domain') 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'system', u'personal_info', u'standard')
child(u'topic', u'topic', u'issubtype', 1) child(u'topic', u'topic', u'issubtype', 1)
resource(u'homepage', u'Willkommen', u'textdocument', contentType='text/restructured') # resources
resource(u'impressum', u'Impressum', u'textdocument', contentType='text/restructured') resource(u'homepage', u'Willkommen', u'textdocument',
contentType='text/restructured')
resource(u'impressum', u'Impressum', u'textdocument',
contentType='text/restructured')
#nodes #nodes
node(u'home', u'Startseite', '', 'menu') node(u'home', u'Startseite', '', 'menu')
node(u'willkommen', u'Willkommen', u'home', u'text') node(u'willkommen', u'Willkommen', u'home', u'text',
node(u'willkommen', u'Willkommen', u'home/willkommen', u'text',
target=u'resources/homepage') 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'topics', u'Themen', u'home', 'page', target=u'concepts/topics')
node(u'glossary', u'Glossar', u'home', 'page', target=u'concepts/glossary') 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'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'', type(u'query', u'Query', options=u'',
typeInterface='loops.expert.concept.IQueryConcept', viewName=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'', type(u'task', u'Task', options=u'',
typeInterface='loops.knowledge.interfaces.ITask', viewName=u'') typeInterface='loops.knowledge.interfaces.ITask', viewName=u'')
type(u'domain', u'Domain', options=u'', typeInterface=u'', viewName=u'') type(u'domain', u'Domain', options=u'', typeInterface=u'', viewName=u'')
type(u'classifier', u'Classifier', options=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'documenttype', u'Document Type', options=u'', typeInterface=u'', viewName=u'')
type(u'extcollection', u'External Collection', options=u'', type(u'extcollection', u'External Collection', options=u'',
typeInterface='loops.integrator.interfaces.IExternalCollection', typeInterface='loops.integrator.interfaces.IExternalCollection',
viewName=u'collection.html') viewName=u'collection.html')
type(u'folder', u'Ordner', options=u'', typeInterface=u'', viewName=u'')
type(u'glossaryitem', u'Glossary Item', options=u'', type(u'glossaryitem', u'Glossary Item', options=u'',
typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html') typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'glossaryitem.html')
type(u'media_asset', u'Media Asset', 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', type(u'note', u'Note', options=u'', typeInterface='loops.interfaces.INote',
viewName='note.html') viewName='note.html')
type(u'person', u'Person', options=u'', type(u'person', u'Person', options=u'',
typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'') typeInterface='loops.knowledge.interfaces.IPerson', viewName=u'')
type(u'predicate', u'Predicate', options=u'', type(u'predicate', u'Predicate', options=u'',
typeInterface=u'loops.interfaces.IPredicate', viewName=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') viewName=u'task.html')
type(u'textdocument', u'Text', options=u'', typeInterface='loops.interfaces.ITextDocument', viewName=u'') type(u'textdocument', u'Text', options=u'',
type(u'topic', u'Topy', options=u'', typeInterface='loops.knowledge.interfaces.ITopic', typeInterface='loops.interfaces.ITextDocument', viewName=u'')
viewName=u'') type(u'topic', u'Topic', options=u'action.portlet:editTopic,createTopic',
type(u'type', u'Type', options=u'', typeInterface='loops.interfaces.ITypeConcept', typeInterface='loops.knowledge.interfaces.ITopic', viewName=u'')
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'depends', u'depends', u'predicate')
concept(u'follows', u'follows', 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'hasType', u'has Type', u'predicate')
concept(u'ispartof', u'is Part of', u'predicate') concept(u'ispartof', u'is Part of', u'predicate')
concept(u'issubtype', u'is Subtype', u'predicate') concept(u'issubtype', u'is Subtype', u'predicate')
concept(u'knows', u'knows', u'predicate') concept(u'knows', u'knows', u'predicate')
concept(u'ownedby', u'owned by', 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'provides', u'provides', u'predicate')
concept(u'querytarget', u'is Query Target', u'predicate') concept(u'querytarget', u'is Query Target', u'predicate')
concept(u'requires', u'requires', 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'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'documenttype', u'standard')
child(u'general', u'event', 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'classifier', u'standard')
child(u'system', u'extcollection', u'standard') child(u'system', u'extcollection', u'standard')
child(u'system', u'issubtype', u'standard') child(u'system', u'issubtype', u'standard')
child(u'system', u'media_asset', u'standard') child(u'system', u'media_asset', u'standard')
child(u'system', u'personal_info', u'standard') child(u'system', u'personal_info', u'standard')
node(u'home', u'Homepage', '', 'menu', body=u'Welcome\n=======) 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', node(u'participants', u'Participants', u'home', 'page',
body=u'Participants\n============', target=u'concepts/person', target=u'concepts/participants')
viewName=u'listchildren') node(u'topics', u'Topics', u'home', 'page', target=u'concepts/topics')
node(u'topics', u'Topics', u'home', 'page', body=u'Topics\n======',
target=u'concepts/topic', viewName=u'listchildren')
node(u'glossary', u'Glossary', u'home', 'page', target=u'concepts/glossary') 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'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() >>> concepts, resources, views = t.setup()
>>> len(concepts) + len(resources) >>> len(concepts) + len(resources)
36 38
>>> loopsRoot = site['loops'] >>> loopsRoot = site['loops']
@ -47,11 +47,11 @@ Type- and text-based queries
>>> from loops.expert import query >>> from loops.expert import query
>>> qu = query.Title('ty*') >>> qu = query.Title('ty*')
>>> list(qu.apply()) >>> list(qu.apply())
[0, 2, 65] [0, 2, 70]
>>> qu = query.Type('loops:*') >>> qu = query.Type('loops:*')
>>> len(list(qu.apply())) >>> len(list(qu.apply()))
36 38
>>> qu = query.Type('loops:concept:predicate') >>> qu = query.Type('loops:concept:predicate')
>>> len(list(qu.apply())) >>> len(list(qu.apply()))

View file

@ -91,4 +91,26 @@
factory="loops.expert.browser.report.ResultsConceptView" factory="loops.expert.browser.report.ResultsConceptView"
permission="zope.View" /> 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> </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 metal:define-macro="main">
<div tal:define="report item/reportInstance; <div tal:define="report item/reportInstance;
reportView nocall:item" reportView nocall:item;
renderer item/resultsRenderer"
tal:attributes="class string:content-$level;"> tal:attributes="class string:content-$level;">
<div metal:use-macro="item/report_macros/header" /> <div metal:use-macro="item/report_macros/header" />
<div metal:use-macro="item/resultsRenderer" /> <tal:renderer condition="renderer">
<div metal:use-macro="renderer" />
</tal:renderer>
</div> </div>
</div> </div>
@ -23,29 +26,65 @@
</div> </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"> <div metal:define-macro="header">
<metal:block use-macro="view/concept_macros/concepttitle" /> <metal:block use-macro="view/concept_macros/concepttitle" />
<form method="get" name="report_data" class="report-meta"> <form method="get" name="report_data" class="report-meta">
<input type="hidden" name="show_results" value="True" /> <input type="hidden" name="show_results" value="True" />
<tal:hidden define="params item/dynamicParams" <tal:hidden define="params item/dynamicParams">
tal:condition="nothing"> <input type="hidden"
<input type="hidden" tal:repeat="name params"
tal:repeat="name params" tal:condition="nothing"
tal:attributes="name name; tal:attributes="name name;
value params/?name" /></tal:hidden> value params/?name" />
<div metal:use-macro="item/report_macros/params" /> <input type="hidden"
<div metal:define-macro="buttons"> tal:define="viewName request/loops.viewName|nothing"
<input type="submit" name="report_execute" value="Execute Report" tal:condition="viewName"
tal:attributes="value item/reportExecuteTitle|string:Execute Report" tal:attributes="name string:loops.viewName;
i18n:attributes="value" /> value viewName" />
<input type="submit" <input type="hidden"
tal:condition="item/reportDownload" tal:define="sortinfo request/sortinfo_results|nothing"
tal:attributes="name string:${item/reportDownload}:method; tal:condition="sortinfo"
value item/reportDownloadTitle" tal:attributes="name string:sortinfo_results;
i18n:attributes="value" /> value sortinfo" />
</div> <input type="hidden" name="report_name"
<br /> tal:define="reportName item/reportName"
</form> 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> </div>
@ -113,7 +152,14 @@
<metal:field define-macro="selection"> <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> </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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -20,6 +20,7 @@
View classes for reporting. View classes for reporting.
""" """
from logging import getLogger
from urllib import urlencode from urllib import urlencode
from zope import interface, component from zope import interface, component
from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.pagetemplate import ViewPageTemplateFile
@ -46,6 +47,10 @@ class ReportView(ConceptView):
""" A view for defining (editing) a report. """ A view for defining (editing) a report.
""" """
resultsRenderer = None # to be defined by subclass
reportDownload = None
reportName = None
@Lazy @Lazy
def report_macros(self): def report_macros(self):
return self.controller.mergeTemplateMacros('report', report_template) return self.controller.mergeTemplateMacros('report', report_template)
@ -55,10 +60,33 @@ class ReportView(ConceptView):
def macro(self): def macro(self):
return self.report_macros['main'] return self.report_macros['main']
@Lazy
def tabTitle(self):
return self.report.title
@Lazy @Lazy
def dynamicParams(self): def dynamicParams(self):
return self.request.form return self.request.form
@Lazy
def report(self):
return self.adapted
@Lazy
def reportInstance(self):
instance = component.getAdapter(self.report, IReportInstance,
name=self.report.reportType)
instance.view = self
return instance
@Lazy
def queryFields(self):
ri = self.reportInstance
qf = ri.getAllQueryFields()
if ri.userSettings:
return [f for f in qf if f in ri.userSettings]
return qf
class ResultsView(NodeView): class ResultsView(NodeView):
@ -107,13 +135,6 @@ class ResultsView(NodeView):
def report(self): def report(self):
return adapted(self.virtualTargetObject) return adapted(self.virtualTargetObject)
@Lazy
def reportInstance(self):
instance = component.getAdapter(self.report, IReportInstance,
name=self.report.reportType)
instance.view = self
return instance
#@Lazy #@Lazy
def results(self): def results(self):
return self.reportInstance.getResults(self.params) return self.reportInstance.getResults(self.params)
@ -139,6 +160,8 @@ class ResultsConceptView(ConceptView):
""" View on a concept using the results of a report. """ View on a concept using the results of a report.
""" """
logger = getLogger ('ResultsConceptView')
reportName = None # define in subclass if applicable reportName = None # define in subclass if applicable
reportDownload = None reportDownload = None
reportType = None # set for using special report instance adapter reportType = None # set for using special report instance adapter
@ -169,6 +192,9 @@ class ResultsConceptView(ConceptView):
@Lazy @Lazy
def reportName(self): 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] return (self.getOptions('report_name') or [None])[0]
@Lazy @Lazy
@ -179,7 +205,10 @@ class ResultsConceptView(ConceptView):
@Lazy @Lazy
def report(self): def report(self):
if self.reportName: 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]) reports = self.context.getParents([self.hasReportPredicate])
if not reports: if not reports:
type = self.context.conceptType type = self.context.conceptType
@ -193,6 +222,13 @@ class ResultsConceptView(ConceptView):
ri = component.getAdapter(self.report, IReportInstance, ri = component.getAdapter(self.report, IReportInstance,
name=reportType) name=reportType)
ri.view = self ri.view = self
if not ri.sortCriteria:
si = self.sortInfo.get('results')
if si is not None:
fnames = (si['colName'],)
ri.sortCriteria = [f for f in ri.getSortFields()
if f.name in fnames]
ri.sortDescending = not si['ascending']
return ri return ri
def results(self): def results(self):
@ -207,6 +243,35 @@ class ResultsConceptView(ConceptView):
def getColumnRenderer(self, col): def getColumnRenderer(self, col):
return self.result_macros[col.renderer] return self.result_macros[col.renderer]
@Lazy
def downloadLink(self, format='csv'):
opt = self.options('download_' + format)
if not opt:
opt = self.typeOptions('download_' + format)
if opt:
return '/'.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): class ReportConceptView(ResultsConceptView, ReportView):
""" View on a concept using a report. """ View on a concept using a report.
@ -229,6 +294,17 @@ class ReportConceptView(ResultsConceptView, ReportView):
return qf 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): class ReportParamsView(ReportConceptView):
""" Report view allowing to enter parameters before executing the report. """ Report view allowing to enter parameters before executing the report.
""" """

View file

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

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

View file

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

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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -35,6 +35,7 @@ from cybertools.composer.report.interfaces import IReportParams
from cybertools.composer.report.result import ResultSet, Row from cybertools.composer.report.result import ResultSet, Row
from cybertools.util.jeep import Jeep from cybertools.util.jeep import Jeep
from loops.common import AdapterBase from loops.common import AdapterBase
from loops.expert.concept import IQueryConcept, QueryConcept
from loops.interfaces import ILoopsAdapter from loops.interfaces import ILoopsAdapter
from loops.type import TypeInterfaceSourceList from loops.type import TypeInterfaceSourceList
from loops import util from loops import util
@ -43,7 +44,7 @@ from loops.util import _
# interfaces # interfaces
class IReport(ILoopsAdapter, IReportParams): class IReport(ILoopsAdapter, IReportParams, IQueryConcept):
""" The report adapter for the persistent object (concept) that stores """ The report adapter for the persistent object (concept) that stores
the report in the concept map. the report in the concept map.
""" """
@ -66,7 +67,7 @@ class IReportInstance(IBaseReport):
# report concept adapter and instances # report concept adapter and instances
class Report(AdapterBase): class Report(QueryConcept):
implements(IReport) implements(IReport)
@ -88,6 +89,7 @@ class ReportInstance(BaseReport):
#headerRowFactory = Row #headerRowFactory = Row
view = None # set upon creation view = None # set upon creation
#headerRowFactory = Row
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context
@ -120,7 +122,9 @@ class ReportInstance(BaseReport):
result = list(self.selectObjects(parts)) # may modify parts result = list(self.selectObjects(parts)) # may modify parts
qc = CompoundQueryCriteria(parts) qc = CompoundQueryCriteria(parts)
return ResultSet(self, result, rowFactory=self.rowFactory, return ResultSet(self, result, rowFactory=self.rowFactory,
sortCriteria=self.getSortCriteria(), queryCriteria=qc, sortCriteria=self.getSortCriteria(),
sortDescending=self.sortDescending,
queryCriteria=qc,
limits=limits) limits=limits)
def selectObjects(self, parts): def selectObjects(self, parts):
@ -173,3 +177,15 @@ class DefaultConceptReportInstance(ReportInstance):
label = u'Default Concept Report' label = u'Default Concept Report'
# specialized rows
class TrackRow(Row):
@staticmethod
def getContextAttr(obj, attr):
if attr in obj.context.metadata_attributes:
return getattr(obj.context, attr)
return obj.context.data.get(attr)

View file

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

6
external/README.txt vendored
View file

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

6
external/pyfunc.py vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import unittest, doctest import unittest, doctest
from zope.interface.verify import verifyClass from zope.interface.verify import verifyClass
#from loops.versioning import versionable
class Test(unittest.TestCase): class Test(unittest.TestCase):
"Basic tests for the loops.integrator.content package." "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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Integrator interfaces. Integrator interfaces.
$Id$
""" """
from zope.interface import Interface, Attribute from zope.interface import Interface, Attribute
@ -133,3 +131,5 @@ class IOfficeFile(IExternalFile):
It provides access to the document content and properties. It provides access to the document content and properties.
""" """
documentPropertiesAccessible = Attribute(
'Are document properties accessible?')

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

View file

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

View file

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

View file

@ -402,7 +402,7 @@ class IDocumentSchema(IResourceSchema):
contentType = schema.Choice( contentType = schema.Choice(
title=_(u'Content Type'), title=_(u'Content Type'),
description=_(u'Content type (format) of the data field'), 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'), 'text/plain', 'text/xml', 'text/css'),
default='text/restructured', default='text/restructured',
required=True) required=True)
@ -968,5 +968,3 @@ class IViewConfiguratorSchema(Interface):
value_type=schema.TextLine(), value_type=schema.TextLine(),
default=[], default=[],
required=False) 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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -30,8 +30,13 @@ from cybertools.typology.interfaces import IType
from loops.browser.action import DialogAction from loops.browser.action import DialogAction
from loops.browser.common import BaseView from loops.browser.common import BaseView
from loops.browser.concept import ConceptView from loops.browser.concept import ConceptView
from loops.common import adapted
from loops.knowledge.interfaces import IPerson, ITask from loops.knowledge.interfaces import IPerson, ITask
from loops.organize.party import getPersonForUser from loops.organize.party import getPersonForUser
from loops.organize.personal import favorite
from loops.organize.personal.interfaces import IFavorites
from loops.security.common import checkPermission
from loops import util
from loops.util import _ from loops.util import _
@ -69,6 +74,63 @@ actions.register('createQualification', 'portlet', DialogAction,
) )
class InstitutionMixin(object):
knowledge_macros = knowledge_macros
adminMaySelectAllInstitutions = True
@Lazy
def institutionType(self):
return self.conceptManager['institution']
@Lazy
def institutions(self):
if self.adminMaySelectAllInstitutions:
if checkPermission('loops.ManageWorkspaces', self.context):
return self.getAllInstitutions()
result = []
p = getPersonForUser(self.context, self.request)
if p is None:
return result
for parent in p.getParents(
[self.memberPredicate, self.masterPredicate]):
if parent.conceptType == self.institutionType:
result.append(dict(
object=adapted(parent),
title=parent.title,
uid=util.getUidForObject(parent)))
return result
def getAllInstitutions(self):
insts = self.institutionType.getChildren([self.typePredicate])
return [dict(object=adapted(inst),
title=inst.title,
uid=util.getUidForObject(inst)) for inst in insts]
def setInstitution(self, uid):
inst = util.getObjectForUid(uid)
person = getPersonForUser(self.context, self.request)
favorite.setInstitution(person, inst)
self.institution = inst
return True
def getSavedInstitution(self):
person = getPersonForUser(self.context, self.request)
favorites = IFavorites(self.loopsRoot.getRecordManager()['favorites'])
for inst in favorites.list(person, type='institution'):
return adapted(util.getObjectForUid(inst))
@Lazy
def institution(self):
saved = self.getSavedInstitution()
for inst in self.institutions:
if inst['object'] == saved:
return inst['object']
if self.institutions:
return self.institutions[0]['object']
class MyKnowledge(ConceptView): class MyKnowledge(ConceptView):
template = template template = template

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

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

View file

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

View file

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

View file

@ -1,6 +1,27 @@
<html i18n:domain="loops"> <html i18n:domain="loops">
<metal:institution define-macro="select_institution">
<form method="post">
<div style="font-size: 120%; padding: 10px 0 10px 0">
<span i18n:translate="">Organisation/Team</span>:
<b tal:content="item/institution/title" />
<img tal:condition="python:len(item.institutions) > 1"
src="/@@/cybertools.icons/application_edit.png"
onclick="dojo.byId('select_institution').style.display = 'inline'" />
<select name="select_institution" id="select_institution"
style="display: none"
onchange="submit()">
<option tal:repeat="inst item/institutions"
tal:content="inst/title"
tal:attributes="value inst/uid;
selected python:inst['object'] == item.institution" />
</select>
</div>
</form>
</metal:institution>
<metal:providers define-macro="requirement_providers"> <metal:providers define-macro="requirement_providers">
<metal:block use-macro="view/concept_macros/conceptdata" /> <metal:block use-macro="view/concept_macros/conceptdata" />
<div> <div>
@ -32,28 +53,32 @@
<metal:candidates define-macro="requirement_candidates"> <metal:candidates define-macro="requirement_candidates">
<metal:block use-macro="view/concept_macros/conceptdata" /> <metal:block use-macro="view/concept_macros/conceptdata" />
<h3 i18n:translate="">Candidates for Task</h3> <div class="candidates"
<table class="listing"> tal:define="candidates item/adapted/getCandidates"
<tr> tal:condition="candidates">
<th i18n:translate="">Candidate</th> <h3 i18n:translate="">Candidates for Task</h3>
<th i18n:translate="" <table class="listing">
title="coverage" <tr>
i18n:attributes="title description_fit">Fit</th> <th i18n:translate="">Candidate</th>
<th i18n:translate="">Knowledge</th> <th i18n:translate=""
</tr> title="coverage"
<tr tal:repeat="candidate item/adapted/getCandidates"> i18n:attributes="title description_fit">Fit</th>
<td tal:define="person candidate/person"> <th i18n:translate="">Knowledge</th>
<b tal:omit-tag="python:candidate['fit'] < 1.0"> </tr>
<a tal:attributes="href python:view.getUrlForTarget(person.context)" <tr tal:repeat="candidate item/adapted/getCandidates">
tal:content="person/title" /></b></td> <td tal:define="person candidate/person">
<td tal:content="candidate/fit" /> <b tal:omit-tag="python:candidate['fit'] < 1.0">
<td> <a tal:attributes="href python:view.getUrlForTarget(person.context)"
<tal:knowledge tal:repeat="ke candidate/required"> tal:content="person/title" /></b></td>
<a tal:attributes="href python:view.getUrlForTarget(ke.context)" <td tal:content="candidate/fit" />
tal:content="ke/title" /><tal:sep condition="not:repeat/ke/end">, </tal:sep> <td>
</tal:knowledge></td> <tal:knowledge tal:repeat="ke candidate/required">
</tr> <a tal:attributes="href python:view.getUrlForTarget(ke.context)"
</table> tal:content="ke/title" /><tal:sep condition="not:repeat/ke/end">, </tal:sep>
</tal:knowledge></td>
</tr>
</table>
</div>
</metal:candidates> </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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,19 +18,25 @@
""" """
Definition of view classes and other browser related stuff for the Definition of view classes and other browser related stuff for the
loops.knowledge package. loops.knowledge.qualification package.
""" """
from zope import interface, component from zope import interface, component
from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from loops.browser.concept import ConceptView
from loops.expert.browser.export import ResultsConceptCSVExport
from loops.expert.browser.report import ResultsConceptView from loops.expert.browser.report import ResultsConceptView
from loops.knowledge.browser import template, knowledge_macros from loops.organize.party import getPersonForUser
from loops.knowledge.qualification.base import QualificationRecord from loops.util import _
class PersonQualificationView(ResultsConceptView): class Qualifications(ResultsConceptView):
pass # obsolete because we can directly use ResultsConceptView
#reportName = 'qualification_overview'
pass # report assigned to query via hasReport relation

View file

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

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

View file

@ -12,8 +12,6 @@
<zope:class class="loops.knowledge.survey.base.Questionnaire"> <zope:class class="loops.knowledge.survey.base.Questionnaire">
<require permission="zope.View" <require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestionnaire" /> interface="loops.knowledge.survey.interfaces.IQuestionnaire" />
<require permission="zope.View"
attributes="context" />
<require permission="zope.ManageContent" <require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestionnaire" /> set_schema="loops.knowledge.survey.interfaces.IQuestionnaire" />
</zope:class> </zope:class>
@ -25,8 +23,6 @@
<zope:class class="loops.knowledge.survey.base.QuestionGroup"> <zope:class class="loops.knowledge.survey.base.QuestionGroup">
<require permission="zope.View" <require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestionGroup" /> interface="loops.knowledge.survey.interfaces.IQuestionGroup" />
<require permission="zope.View"
attributes="context" />
<require permission="zope.ManageContent" <require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestionGroup" /> set_schema="loops.knowledge.survey.interfaces.IQuestionGroup" />
</zope:class> </zope:class>
@ -38,8 +34,6 @@
<zope:class class="loops.knowledge.survey.base.Question"> <zope:class class="loops.knowledge.survey.base.Question">
<require permission="zope.View" <require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestion" /> interface="loops.knowledge.survey.interfaces.IQuestion" />
<require permission="zope.View"
attributes="context" />
<require permission="zope.ManageContent" <require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestion" /> set_schema="loops.knowledge.survey.interfaces.IQuestion" />
</zope:class> </zope:class>
@ -51,8 +45,6 @@
<zope:class class="loops.knowledge.survey.base.FeedbackItem"> <zope:class class="loops.knowledge.survey.base.FeedbackItem">
<require permission="zope.View" <require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IFeedbackItem" /> interface="loops.knowledge.survey.interfaces.IFeedbackItem" />
<require permission="zope.View"
attributes="context" />
<require permission="zope.ManageContent" <require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IFeedbackItem" /> set_schema="loops.knowledge.survey.interfaces.IFeedbackItem" />
</zope:class> </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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -23,21 +23,82 @@ Interfaces for surveys used in knowledge management.
from zope.interface import Interface, Attribute from zope.interface import Interface, Attribute
from zope import interface, component, schema from zope import interface, component, schema
from cybertools.composer.schema.grid.interfaces import Records
from cybertools.knowledge.survey import interfaces from cybertools.knowledge.survey import interfaces
from loops.interfaces import IConceptSchema, ILoopsAdapter from loops.interfaces import IConceptSchema, ILoopsAdapter
from loops.util import _ from loops.util import _, KeywordVocabulary
class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire): class IQuestionnaire(ILoopsAdapter, interfaces.IQuestionnaire):
""" A collection of questions for setting up a survey. """ A collection of questions for setting up a survey.
""" """
questionnaireHeader = schema.Text(
title=_(u'Questionnaire Header'),
description=_(u'Text that will appear at the top of the questionnaire.'),
default=u'',
missing_value=u'',
required=False)
questionnaireType = schema.Choice(
title=_(u'Questionnaire Type'),
description=_(u'Select the type of the questionnaire.'),
source=KeywordVocabulary((
('standard', _(u'Standard Questionnaire')),
('person', _(u'Person-related Questionnaire')),
('team', _(u'Team-related Questionnaire')),
('pref_selection', _(u'Preference Selection')),
)),
default='standard',
required=True)
defaultAnswerRange = schema.Int( defaultAnswerRange = schema.Int(
title=_(u'Answer Range'), title=_(u'Answer Range'),
description=_(u'Number of items (answer options) to select from.'), description=_(u'Number of items (answer options) to select from.'),
default=4, default=4,
required=True) required=True)
answerOptions = Records(
title=_(u'Answer Options'),
description=_(u'Values to select from with corresponding column '
u'labels and descriptions. There should be at '
u'least answer range items with numeric values.'),
default=[],
required=False)
answerOptions.column_types = [
schema.Text(__name__='value', title=u'Value',),
schema.Text(__name__='label', title=u'Label'),
schema.Text(__name__='description', title=u'Description'),
schema.Text(__name__='colspan', title=u'ColSpan'),
schema.Text(__name__='cssclass', title=u'CSS Class'),]
noGrouping = schema.Bool(
title=_(u'No Grouping of Questions'),
description=_(u'The questions should be presented in a linear manner, '
u'not grouped by categories or question groups.'),
default=False,
required=False)
teamBasedEvaluation = schema.Bool(
title=_(u'Team-based Evaluation'),
description=_(u'.'),
default=False,
required=False)
#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( feedbackHeader = schema.Text(
title=_(u'Feedback Header'), title=_(u'Feedback Header'),
description=_(u'Text that will appear at the top of the feedback page.'), description=_(u'Text that will appear at the top of the feedback page.'),
@ -53,7 +114,7 @@ class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire):
required=False) required=False)
class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup): class IQuestionGroup(ILoopsAdapter, interfaces.IQuestionGroup):
""" A group of questions within a questionnaire. """ A group of questions within a questionnaire.
""" """
@ -65,10 +126,20 @@ class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup):
required=False) required=False)
class IQuestion(IConceptSchema, interfaces.IQuestion): class IQuestion(ILoopsAdapter, interfaces.IQuestion):
""" A single question within a questionnaire. """ A single question within a questionnaire.
""" """
questionType = schema.Choice(
title=_(u'Question Type'),
description=_(u'Select the type of the question.'),
source=KeywordVocabulary((
('value_selection', _(u'Value Selection')),
('text', _(u'Text')),
)),
default='value_selection',
required=True)
required = schema.Bool( required = schema.Bool(
title=_(u'Required'), title=_(u'Required'),
description=_(u'Question must be answered.'), description=_(u'Question must be answered.'),
@ -82,7 +153,7 @@ class IQuestion(IConceptSchema, interfaces.IQuestion):
required=False) required=False)
class IFeedbackItem(IConceptSchema, interfaces.IFeedbackItem): class IFeedbackItem(ILoopsAdapter, interfaces.IFeedbackItem):
""" Some text (e.g. a recommendation) or some other kind of information """ Some text (e.g. a recommendation) or some other kind of information
that may be deduced from the res)ponses to a questionnaire. 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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -34,22 +34,52 @@ class Responses(BaseRecordManager):
implements(IResponses) implements(IResponses)
storageName = 'survey_responses' storageName = 'survey_responses'
personId = None
institutionId = None
referrerId = None
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context
def save(self, data): def save(self, data):
if self.personId: if self.personId:
self.storage.saveUserTrack(self.uid, 0, self.personId, data, id = self.personId
update=True, overwrite=True) 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): def load(self, personId=None, referrerId=None, institutionId=None):
if self.personId: if personId is None:
tracks = self.storage.getUserTracks(self.uid, 0, self.personId) 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: if tracks:
return tracks[0].data return tracks[0].data
return {} 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): def getAllTracks(self):
return self.storage.query(taskId=self.uid) return self.storage.query(taskId=self.uid)

View file

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

View file

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

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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -27,6 +27,7 @@ from zope.proxy import removeAllProxies
from zope.security.proxy import removeSecurityProxy from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL from zope.traversing.browser import absoluteURL
from cybertools.browser.view import URLGetter
from cybertools.meta.interfaces import IOptions from cybertools.meta.interfaces import IOptions
from cybertools.util import format from cybertools.util import format
from loops.common import adapted, baseObject from loops.common import adapted, baseObject
@ -42,6 +43,10 @@ class BaseView(object):
self.context = removeSecurityProxy(context) # this is the adapted concept! self.context = removeSecurityProxy(context) # this is the adapted concept!
self.request = request self.request = request
@property
def requestUrl(self):
return URLGetter(self.request)
@Lazy @Lazy
def loopsRoot(self): def loopsRoot(self):
return self.context.getLoopsRoot() return self.context.getLoopsRoot()
@ -86,6 +91,10 @@ class BaseView(object):
def title(self): def title(self):
return self.context.title return self.context.title
@Lazy
def headTitle(self):
return self.title
@Lazy @Lazy
def description(self): def description(self):
return self.context.description return self.context.description

View file

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

View file

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

View file

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

View file

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

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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -87,6 +87,7 @@ class BaseMemberRegistration(NodeView):
formErrors = dict( formErrors = dict(
confirm_nomatch=FormError(_(u'Password and password confirmation ' confirm_nomatch=FormError(_(u'Password and password confirmation '
u'do not match.')), u'do not match.')),
illegal_loginname=FormError(_('Login name not allowed.')),
duplicate_loginname=FormError(_('Login name already taken.')), duplicate_loginname=FormError(_('Login name already taken.')),
) )

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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -32,7 +32,7 @@ from cybertools.ajax import innerHtml
from cybertools.browser.action import actions from cybertools.browser.action import actions
from cybertools.browser.form import FormController from cybertools.browser.form import FormController
from loops.browser.action import DialogAction 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.browser.node import NodeView
from loops.common import adapted from loops.common import adapted
from loops.organize.party import getPersonForUser from loops.organize.party import getPersonForUser
@ -44,7 +44,8 @@ organize_macros = ViewPageTemplateFile('view_macros.pt')
actions.register('createPerson', 'portlet', DialogAction, actions.register('createPerson', 'portlet', DialogAction,
title=_(u'Create Person...'), title=_(u'Create Person...'),
description=_(u'Create a new person.'), description=_(u'Create a new person.'),
viewName='create_concept.html', #viewName='create_concept.html',
viewName='create_person.html',
dialogName='createPerson', dialogName='createPerson',
typeToken='.loops/concepts/person', typeToken='.loops/concepts/person',
fixedType=True, fixedType=True,
@ -115,24 +116,35 @@ actions.register('send_email', 'object', DialogAction,
) )
class EditPersonForm(EditConceptForm): class PersonForm(object):
@Lazy @Lazy
def presetTypesForAssignment(self): def presetTypesForAssignment(self):
types = list(self.typeManager.listTypes(include=('workspace',))) 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'] predicates = [n for n in ['standard', 'ismember', 'ismaster', 'isowner']
if n in self.conceptManager] if n in self.conceptManager]
return [dict(title=t.title, token=t.tokenForSearch, predicates=predicates) return [dict(title=t.title, token=t.tokenForSearch, predicates=predicates)
for t in types] for t in types]
class CreatePersonForm(PersonForm, CreateConceptForm):
pass
class EditPersonForm(PersonForm, EditConceptForm):
pass
class SendEmailForm(NodeView): class SendEmailForm(NodeView):
__call__ = innerHtml __call__ = innerHtml
def checkPermissions(self):
return (not self.isAnonymous and
super(SendEmailForm, self).checkPermissions())
@property @property
def macro(self): def macro(self):
return organize_macros.macros['send_email'] return organize_macros.macros['send_email']
@ -171,6 +183,10 @@ class SendEmailForm(NodeView):
@Lazy @Lazy
def subject(self): def subject(self):
optionKey = 'organize.sendmail_subject'
option = self.globalOptions(optionKey) or self.typeOptions(optionKey)
if option:
return option[0]
menu = self.context.getMenu() menu = self.context.getMenu()
zdc = IZopeDublinCore(menu) zdc = IZopeDublinCore(menu)
zdc.languageInfo = self.languageInfo zdc.languageInfo = self.languageInfo
@ -181,6 +197,12 @@ class SendEmailForm(NodeView):
class SendEmail(FormController): class SendEmail(FormController):
bccToSender = False
def checkPermissions(self):
return (not self.isAnonymous and
super(SendEmail, self).checkPermissions())
def update(self): def update(self):
form = self.request.form form = self.request.form
subject = form.get('subject') or u'' subject = form.get('subject') or u''
@ -193,7 +215,10 @@ class SendEmail(FormController):
msg = MIMEText(message.encode('utf-8'), 'plain', 'utf-8') msg = MIMEText(message.encode('utf-8'), 'plain', 'utf-8')
msg['Subject'] = subject.encode('utf-8') msg['Subject'] = subject.encode('utf-8')
msg['From'] = sender 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 = component.getUtility(IMailDelivery, 'Mail')
mailhost.send(sender, recipients, msg.as_string()) mailhost.send(sender, recipients, msg.as_string())
return True return True

View file

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

View file

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

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

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

View file

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

View file

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

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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -79,8 +79,10 @@ class MemberRegistrationManager(object):
if pfName is None: if pfName is None:
pfName = options(self.principalfolder_key, pfName = options(self.principalfolder_key,
(self.default_principalfolder,))[0] (self.default_principalfolder,))[0]
self.createPrincipal(pfName, userId, password, lastName, firstName, rc = self.createPrincipal(pfName, userId, password,
useExisting=useExisting) lastName, firstName, useExisting=useExisting)
if rc is not None:
return rc
if not groups: if not groups:
groups = options(self.groups_key, ()) groups = options(self.groups_key, ())
self.setGroupsForPrincipal(pfName, userId, groups=groups) self.setGroupsForPrincipal(pfName, userId, groups=groups)
@ -90,6 +92,8 @@ class MemberRegistrationManager(object):
def createPrincipal(self, pfName, userId, password, lastName, def createPrincipal(self, pfName, userId, password, lastName,
firstName=u'', groups=[], useExisting=False, firstName=u'', groups=[], useExisting=False,
overwrite=False, **kw): overwrite=False, **kw):
if not self.checkPrincipalId(userId):
return dict(fieldName='loginName', error='illegal_loginname')
pFolder = getPrincipalFolder(self.context, pfName) pFolder = getPrincipalFolder(self.context, pfName)
if IPersonBasedAuthenticator.providedBy(pFolder): if IPersonBasedAuthenticator.providedBy(pFolder):
pFolder.setPassword(userId, password) pFolder.setPassword(userId, password)
@ -125,10 +129,18 @@ class MemberRegistrationManager(object):
if gFolder is not None: if gFolder is not None:
group = gFolder.get(gName) group = gFolder.get(gName)
if group is not None: 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) members.append(pFolder.prefix + userId)
group.principals = members 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'', def createPersonForPrincipal(self, pfName, userId, lastName, firstName=u'',
useExisting=False, **kw): useExisting=False, **kw):
concepts = self.context.getConceptManager() concepts = self.context.getConceptManager()

View file

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

View file

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

View file

@ -57,15 +57,22 @@ class FavoriteView(NodeView):
def listFavorites(self): def listFavorites(self):
if self.favorites is None: if self.favorites is None:
return 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) obj = util.getObjectForUid(uid)
if obj is not None: if obj is not None:
adobj = adapted(obj) adobj = adapted(obj)
yield dict(url=self.getUrlForTarget(obj), yield dict(url=self.getUrlForTarget(obj),
uid=uid, uid=uid,
title=adobj.favTitle, title=obj.title,
description=adobj.description, description=obj.description,
object=obj) object=obj,
trackUid=trackUid)
def add(self): def add(self):
if self.favorites is None: if self.favorites is None:

View file

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

View file

@ -41,12 +41,16 @@ class Favorites(object):
for item in self.listTracks(person, sortKey, type): for item in self.listTracks(person, sortKey, type):
yield item.taskId 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'): def listTracks(self, person, sortKey=None, type='favorite'):
if person is None: if person is None:
return return
personUid = util.getUidForObject(person) personUid = util.getUidForObject(person)
if sortKey is None: if sortKey is None:
sortKey = lambda x: -x.timeStamp sortKey = lambda x: (x.data.get('order', 100), -x.timeStamp)
for item in sorted(self.context.query(userName=personUid), key=sortKey): for item in sorted(self.context.query(userName=personUid), key=sortKey):
if type is not None: if type is not None:
if item.type != type: if item.type != type:
@ -59,7 +63,7 @@ class Favorites(object):
uid = util.getUidForObject(obj) uid = util.getUidForObject(obj)
personUid = util.getUidForObject(person) personUid = util.getUidForObject(person)
if data is None: if data is None:
data = {'type': 'favorite'} data = {'type': 'favorite', 'order': 100}
if nodups: if nodups:
for track in self.context.query(userName=personUid, taskId=uid): for track in self.context.query(userName=personUid, taskId=uid):
if track.type == data['type']: # already present if track.type == data['type']: # already present
@ -78,6 +82,18 @@ class Favorites(object):
self.context.removeTrack(track) self.context.removeTrack(track)
return changed 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): class Favorite(Track):

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